Merge branch 'main' into feature/north-flank-app-connection

This commit is contained in:
Victor Santos
2025-10-24 18:07:23 -03:00
100 changed files with 4611 additions and 660 deletions
-57
View File
@@ -1,57 +0,0 @@
## @section Common parameters
##
## @param nameOverride Override release name
##
nameOverride: ""
## @param fullnameOverride Override release fullname
##
fullnameOverride: ""
## @section Infisical backend parameters
## Documentation : https://infisical.com/docs/self-hosting/deployments/kubernetes
##
infisical:
autoDatabaseSchemaMigration: false
enabled: false
name: infisical
replicaCount: 3
image:
repository: infisical/staging_infisical
tag: "latest"
pullPolicy: Always
deploymentAnnotations:
secrets.infisical.com/auto-reload: "true"
kubeSecretRef: "managed-secret"
ingress:
## @param ingress.enabled Enable ingress
##
enabled: true
## @param ingress.ingressClassName Ingress class name
##
ingressClassName: nginx
## @param ingress.nginx.enabled Ingress controller
##
# nginx:
# enabled: true
## @param ingress.annotations Ingress annotations
##
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hostName: "gamma.infisical.com"
tls:
- secretName: letsencrypt-prod
hosts:
- gamma.infisical.com
postgresql:
enabled: false
redis:
enabled: false
@@ -56,7 +56,7 @@ jobs:
--config ct.yaml \ --config ct.yaml \
--charts helm-charts/infisical-standalone-postgres \ --charts helm-charts/infisical-standalone-postgres \
--helm-extra-args="--timeout=300s" \ --helm-extra-args="--timeout=300s" \
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres" \ --helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.151.0" \
--namespace infisical-standalone-postgres --namespace infisical-standalone-postgres
release: release:
@@ -66,5 +66,5 @@ jobs:
--config ct.yaml \ --config ct.yaml \
--charts helm-charts/infisical-standalone-postgres \ --charts helm-charts/infisical-standalone-postgres \
--helm-extra-args="--timeout=300s" \ --helm-extra-args="--timeout=300s" \
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres --set infisical.autoBootstrap.enabled=true" \ --helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.151.0 --set infisical.autoBootstrap.enabled=true" \
--namespace infisical-standalone-postgres --namespace infisical-standalone-postgres
+1 -1
View File
@@ -158,7 +158,7 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
# Install Infisical CLI # Install Infisical CLI
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
&& apt-get update && apt-get install -y infisical=0.42.6 \ && apt-get update && apt-get install -y infisical=0.43.14 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
+1 -1
View File
@@ -142,7 +142,7 @@ RUN apt-get update && apt-get install -y \
# Install Infisical CLI # Install Infisical CLI
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
&& apt-get update && apt-get install -y infisical=0.42.6 \ && apt-get update && apt-get install -y infisical=0.43.14 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
WORKDIR / WORKDIR /
+1 -1
View File
@@ -55,7 +55,7 @@ COPY --from=build /app .
# Install Infisical CLI # Install Infisical CLI
RUN apt-get install -y curl bash && \ RUN apt-get install -y curl bash && \
curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \ curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
apt-get update && apt-get install -y infisical=0.41.89 git apt-get update && apt-get install -y infisical=0.43.14 git
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js CMD node healthcheck.js
+1 -1
View File
@@ -54,7 +54,7 @@ RUN apt-get install -y opensc
# Install Infisical CLI # Install Infisical CLI
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \ RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
apt-get update && \ apt-get update && \
apt-get install -y infisical=0.41.89 apt-get install -y infisical=0.43.14
WORKDIR /app WORKDIR /app
+1 -1
View File
@@ -67,7 +67,7 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
# Install Infisical CLI # Install Infisical CLI
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \ RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
apt-get update && \ apt-get update && \
apt-get install -y infisical=0.41.89 apt-get install -y infisical=0.43.14
WORKDIR /app WORKDIR /app
@@ -0,0 +1,60 @@
import { Knex } from "knex";
import { AccessScope, TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasGroupsTable = await knex.schema.hasTable(TableName.Groups);
const hasMembershipTable = await knex.schema.hasTable(TableName.Membership);
const hasMembershipRoleTable = await knex.schema.hasTable(TableName.MembershipRole);
if (!hasGroupsTable || !hasMembershipTable || !hasMembershipRoleTable) {
return;
}
const groupsWithoutMembership = await knex
.select(
`${TableName.Groups}.id`,
`${TableName.Groups}.orgId`,
`${TableName.Groups}.role`,
`${TableName.Groups}.roleId`
)
.from(TableName.Groups)
.leftJoin(TableName.Membership, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`)
.whereNull(`${TableName.Membership}.actorGroupId`);
if (groupsWithoutMembership.length > 0) {
const membershipInserts = groupsWithoutMembership.map((group) => ({
actorGroupId: group.id,
scope: AccessScope.Organization,
scopeOrgId: group.orgId,
isActive: true
}));
const insertedMemberships = await knex(TableName.Membership).insert(membershipInserts).returning("*");
const membershipRoleInserts = insertedMemberships.map((membership, index) => {
const group = groupsWithoutMembership[index];
return {
membershipId: membership.id,
role: group.role,
customRoleId: group.roleId
};
});
await knex(TableName.MembershipRole).insert(membershipRoleInserts);
}
await knex.schema.alterTable(TableName.Membership, (t) => {
t.check(
`("actorUserId" IS NOT NULL OR "actorIdentityId" IS NOT NULL OR "actorGroupId" IS NOT NULL)`,
undefined,
"at_least_one_actor"
);
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.Membership, (t) => {
t.dropChecks("at_least_one_actor");
});
}
@@ -1,3 +1,8 @@
import {
CreateMySQLAccountSchema,
SanitizedMySQLAccountWithResourceSchema,
UpdateMySQLAccountSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import { import {
CreatePostgresAccountSchema, CreatePostgresAccountSchema,
@@ -16,5 +21,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
createAccountSchema: CreatePostgresAccountSchema, createAccountSchema: CreatePostgresAccountSchema,
updateAccountSchema: UpdatePostgresAccountSchema updateAccountSchema: UpdatePostgresAccountSchema
}); });
},
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
registerPamResourceEndpoints({
server,
resourceType: PamResource.MySQL,
accountResponseSchema: SanitizedMySQLAccountWithResourceSchema,
createAccountSchema: CreateMySQLAccountSchema,
updateAccountSchema: UpdateMySQLAccountSchema
});
} }
}; };
@@ -2,6 +2,7 @@ import { z } from "zod";
import { PamFoldersSchema } from "@app/db/schemas"; import { PamFoldersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
@@ -10,8 +11,10 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
// Use z.union([...]) when more resources are added const SanitizedAccountSchema = z.union([
const SanitizedAccountSchema = SanitizedPostgresAccountWithResourceSchema; SanitizedPostgresAccountWithResourceSchema,
SanitizedMySQLAccountWithResourceSchema
]);
export const registerPamAccountRouter = async (server: FastifyZodProvider) => { export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -4,6 +4,11 @@ import {
SanitizedPostgresResourceSchema, SanitizedPostgresResourceSchema,
UpdatePostgresResourceSchema UpdatePostgresResourceSchema
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import {
CreateMySQLResourceSchema,
MySQLResourceSchema,
UpdateMySQLResourceSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { registerPamResourceEndpoints } from "./pam-resource-endpoints"; import { registerPamResourceEndpoints } from "./pam-resource-endpoints";
@@ -16,5 +21,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
createResourceSchema: CreatePostgresResourceSchema, createResourceSchema: CreatePostgresResourceSchema,
updateResourceSchema: UpdatePostgresResourceSchema updateResourceSchema: UpdatePostgresResourceSchema
}); });
},
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
registerPamResourceEndpoints({
server,
resourceType: PamResource.MySQL,
resourceResponseSchema: MySQLResourceSchema,
createResourceSchema: CreateMySQLResourceSchema,
updateResourceSchema: UpdateMySQLResourceSchema
});
} }
}; };
@@ -1,6 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
MySQLResourceListItemSchema,
SanitizedMySQLResourceSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { import {
PostgresResourceListItemSchema, PostgresResourceListItemSchema,
SanitizedPostgresResourceSchema SanitizedPostgresResourceSchema
@@ -9,10 +13,12 @@ import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
// Use z.union([...]) when more resources are added const SanitizedResourceSchema = z.union([SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema]);
const SanitizedResourceSchema = SanitizedPostgresResourceSchema;
const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]); const ResourceOptionsSchema = z.discriminatedUnion("resource", [
PostgresResourceListItemSchema,
MySQLResourceListItemSchema
]);
export const registerPamResourceRouter = async (server: FastifyZodProvider) => { export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -2,14 +2,14 @@ import { z } from "zod";
import { PamSessionsSchema } from "@app/db/schemas"; import { PamSessionsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { MySQLSessionCredentialsSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas"; import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
// Use z.union([]) once there's multiple const SessionCredentialsSchema = z.union([PostgresSessionCredentialsSchema, MySQLSessionCredentialsSchema]);
const SessionCredentialsSchema = PostgresSessionCredentialsSchema;
export const registerPamSessionRouter = async (server: FastifyZodProvider) => { export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
// Meant to be hit solely by gateway identities // Meant to be hit solely by gateway identities
@@ -40,6 +40,7 @@ import {
TOrgPlanDTO, TOrgPlanDTO,
TOrgPlansTableDTO, TOrgPlansTableDTO,
TOrgPmtMethodsDTO, TOrgPmtMethodsDTO,
TPlanBillingInfo,
TStartOrgTrialDTO, TStartOrgTrialDTO,
TUpdateOrgBillingDetailsDTO TUpdateOrgBillingDetailsDTO
} from "./license-types"; } from "./license-types";
@@ -465,6 +466,21 @@ export const licenseServiceFactory = ({
return { url }; return { url };
}; };
const getUsageMetrics = async (orgId: string) => {
const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([
orgDAL.countAllOrgMembers(orgId),
licenseDAL.countOfOrgIdentities(orgId),
projectDAL.countOfOrgProjects(orgId)
]);
return {
orgMembersUsed,
identityUsed,
projectCount,
totalIdentities: identityUsed + orgMembersUsed
};
};
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission({ const { permission } = await permissionService.getOrgPermission({
actorId, actorId,
@@ -483,10 +499,16 @@ export const licenseServiceFactory = ({
}); });
} }
if (instanceType === InstanceType.Cloud) { if (instanceType === InstanceType.Cloud) {
const { data } = await licenseServerCloudApi.request.get( const { data } = await licenseServerCloudApi.request.get<TPlanBillingInfo>(
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing` `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
); );
return data; const { identityUsed, orgMembersUsed } = await getUsageMetrics(orgId);
return {
...data,
users: orgMembersUsed,
identities: identityUsed
};
} }
return { return {
@@ -495,7 +517,9 @@ export const licenseServiceFactory = ({
interval: "month", interval: "month",
intervalCount: 1, intervalCount: 1,
amount: 0, amount: 0,
quantity: 1 quantity: 1,
users: 0,
identities: 0
}; };
}; };
@@ -539,21 +563,6 @@ export const licenseServiceFactory = ({
throw new Error(`Unsupported instance type for server-based plan table: ${instanceType}`); throw new Error(`Unsupported instance type for server-based plan table: ${instanceType}`);
}; };
const getUsageMetrics = async (orgId: string) => {
const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([
orgDAL.countAllOrgMembers(orgId),
licenseDAL.countOfOrgIdentities(orgId),
projectDAL.countOfOrgProjects(orgId)
]);
return {
orgMembersUsed,
identityUsed,
projectCount,
totalIdentities: identityUsed + orgMembersUsed
};
};
// returns org current plan feature table // returns org current plan feature table
const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission({ const { permission } = await permissionService.getOrgPermission({
@@ -22,6 +22,15 @@ export type TOfflineLicense = {
features: TFeatureSet; features: TFeatureSet;
}; };
export type TPlanBillingInfo = {
currentPeriodStart: number;
currentPeriodEnd: number;
interval: "month" | "year";
intervalCount: number;
amount: number;
quantity: number;
};
export type TFeatureSet = { export type TFeatureSet = {
_id: null; _id: null;
slug: string | null; slug: string | null;
@@ -0,0 +1,8 @@
import { MySQLResourceListItemSchema } from "./mysql-resource-schemas";
export const getMySQLResourceListItem = () => {
return {
name: MySQLResourceListItemSchema.shape.name.value,
resource: MySQLResourceListItemSchema.shape.resource.value
};
};
@@ -0,0 +1,76 @@
import { z } from "zod";
import { PamResource } from "../pam-resource-enums";
import {
BaseCreatePamAccountSchema,
BaseCreatePamResourceSchema,
BasePamAccountSchema,
BasePamAccountSchemaWithResource,
BasePamResourceSchema,
BaseUpdatePamAccountSchema,
BaseUpdatePamResourceSchema
} from "../pam-resource-schemas";
import {
BaseSqlAccountCredentialsSchema,
BaseSqlResourceConnectionDetailsSchema
} from "../shared/sql/sql-resource-schemas";
// Resources
export const MySQLResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema.extend({
// MySQL db in many cases the db will not be provided when making connection
database: z.string().trim()
});
export const MySQLAccountCredentialsSchema = BaseSqlAccountCredentialsSchema;
const BaseMySQLResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.MySQL) });
export const MySQLResourceSchema = BaseMySQLResourceSchema.extend({
connectionDetails: MySQLResourceConnectionDetailsSchema,
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
});
export const SanitizedMySQLResourceSchema = BaseMySQLResourceSchema.extend({
connectionDetails: MySQLResourceConnectionDetailsSchema,
rotationAccountCredentials: MySQLAccountCredentialsSchema.pick({
username: true
})
.nullable()
.optional()
});
export const MySQLResourceListItemSchema = z.object({
name: z.literal("MySQL"),
resource: z.literal(PamResource.MySQL)
});
export const CreateMySQLResourceSchema = BaseCreatePamResourceSchema.extend({
connectionDetails: MySQLResourceConnectionDetailsSchema,
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
});
export const UpdateMySQLResourceSchema = BaseUpdatePamResourceSchema.extend({
connectionDetails: MySQLResourceConnectionDetailsSchema.optional(),
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
});
// Accounts
export const MySQLAccountSchema = BasePamAccountSchema.extend({
credentials: MySQLAccountCredentialsSchema
});
export const CreateMySQLAccountSchema = BaseCreatePamAccountSchema.extend({
credentials: MySQLAccountCredentialsSchema
});
export const UpdateMySQLAccountSchema = BaseUpdatePamAccountSchema.extend({
credentials: MySQLAccountCredentialsSchema.optional()
});
export const SanitizedMySQLAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({
credentials: MySQLAccountCredentialsSchema.pick({
username: true
})
});
// Sessions
export const MySQLSessionCredentialsSchema = MySQLResourceConnectionDetailsSchema.and(MySQLAccountCredentialsSchema);
@@ -0,0 +1,16 @@
import { z } from "zod";
import {
MySQLAccountCredentialsSchema,
MySQLAccountSchema,
MySQLResourceConnectionDetailsSchema,
MySQLResourceSchema
} from "./mysql-resource-schemas";
// Resources
export type TMySQLResource = z.infer<typeof MySQLResourceSchema>;
export type TMySQLResourceConnectionDetails = z.infer<typeof MySQLResourceConnectionDetailsSchema>;
// Accounts
export type TMySQLAccount = z.infer<typeof MySQLAccountSchema>;
export type TMySQLAccountCredentials = z.infer<typeof MySQLAccountCredentialsSchema>;
@@ -1,3 +1,4 @@
export enum PamResource { export enum PamResource {
Postgres = "postgres" Postgres = "postgres",
MySQL = "mysql"
} }
@@ -5,5 +5,6 @@ import { sqlResourceFactory } from "./shared/sql/sql-resource-factory";
type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnectionDetails, TPamAccountCredentials>; type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnectionDetails, TPamAccountCredentials>;
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = { export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation [PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation,
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation
}; };
@@ -3,11 +3,12 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { decryptAccountCredentials } from "../pam-account/pam-account-fns"; import { decryptAccountCredentials } from "../pam-account/pam-account-fns";
import { getMySQLResourceListItem } from "./mysql/mysql-resource-fns";
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types"; import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns"; import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
export const listResourceOptions = () => { export const listResourceOptions = () => {
return [getPostgresResourceListItem()].sort((a, b) => a.name.localeCompare(b.name)); return [getPostgresResourceListItem(), getMySQLResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
}; };
// Resource // Resource
@@ -1,4 +1,10 @@
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
import {
TMySQLAccount,
TMySQLAccountCredentials,
TMySQLResource,
TMySQLResourceConnectionDetails
} from "./mysql/mysql-resource-types";
import { PamResource } from "./pam-resource-enums"; import { PamResource } from "./pam-resource-enums";
import { import {
TPostgresAccount, TPostgresAccount,
@@ -8,12 +14,13 @@ import {
} from "./postgres/postgres-resource-types"; } from "./postgres/postgres-resource-types";
// Resource types // Resource types
export type TPamResource = TPostgresResource; export type TPamResource = TPostgresResource | TMySQLResource;
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails; export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
// Account types // Account types
export type TPamAccount = TPostgresAccount; export type TPamAccount = TPostgresAccount | TMySQLAccount;
export type TPamAccountCredentials = TPostgresAccountCredentials; // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
// Resource DTOs // Resource DTOs
export type TCreateResourceDTO = Pick< export type TCreateResourceDTO = Pick<
@@ -1,4 +1,6 @@
import knex, { Knex } from "knex"; import knex from "knex";
import mysql, { Connection } from "mysql2/promise";
import * as pg from "pg";
import tls, { PeerCertificate } from "tls"; import tls, { PeerCertificate } from "tls";
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
@@ -20,30 +22,160 @@ const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test"; const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test";
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password"; const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";
const SIMPLE_QUERY = "select 1";
const SQL_CONNECTION_CLIENT_MAP = { export interface SqlResourceConnection {
[PamResource.Postgres]: "pg" /**
}; * Check and see if the connection is good or not.
*
* @param connectOnly when true, if we only want to know that making the connection is possible or not,
* we don't care about authentication failures
* @returns Promise to be resolved when the connection is good, otherwise an error will be errbacked
*/
validate: (connectOnly: boolean) => Promise<void>;
const getConnectionConfig = ( /**
resourceType: PamResource, * Rotate password and return the new credentials.
{ host, sslEnabled, sslRejectUnauthorized, sslCertificate }: TSqlResourceConnectionDetails *
) => { * @param currentCredentials the current credentials to rotate
switch (resourceType) { *
* @returns Promise to be resolved with the new credentials
*/
rotateCredentials: (currentCredentials: TSqlAccountCredentials) => Promise<TSqlAccountCredentials>;
/**
* Close the connection.
*
* @returns Promise for closing the connection
*/
close: () => Promise<void>;
}
const makeSqlConnection = (
proxyPort: number,
config: {
connectionDetails: TSqlResourceConnectionDetails;
resourceType: PamResource;
username?: string;
password?: string;
}
): SqlResourceConnection => {
const { connectionDetails, resourceType, username, password } = config;
const { host, sslEnabled, sslRejectUnauthorized, sslCertificate } = connectionDetails;
const actualUsername = username ?? TEST_CONNECTION_USERNAME; // Use provided username or fallback
const actualPassword = password ?? TEST_CONNECTION_PASSWORD; // Use provided password or fallback
switch (config.resourceType) {
case PamResource.Postgres: { case PamResource.Postgres: {
const client = knex({
client: "pg",
connection: {
host: "localhost",
port: proxyPort,
user: actualUsername,
password: actualPassword,
database: connectionDetails.database,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
ssl: sslEnabled
? {
rejectUnauthorized: sslRejectUnauthorized,
ca: sslCertificate,
servername: host,
// When using proxy, we need to bypass hostname validation since we connect to localhost
// but validate the certificate against the actual hostname
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
return tls.checkServerIdentity(host, cert);
}
}
: false
}
});
return { return {
ssl: sslEnabled validate: async (connectOnly) => {
? { try {
rejectUnauthorized: sslRejectUnauthorized, await client.raw(SIMPLE_QUERY);
ca: sslCertificate, } catch (error) {
servername: host, if (error instanceof pg.DatabaseError) {
// When using proxy, we need to bypass hostname validation since we connect to localhost // Hacky way to know if we successfully hit the database.
// but validate the certificate against the actual hostname // TODO: potentially two approaches to solve the problem.
checkServerIdentity: (hostname: string, cert: PeerCertificate) => { // 1. change the work flow, add account first then resource
return tls.checkServerIdentity(host, cert); // 2. modify relay to add a new endpoint for returning if the target host is healthy or not
// (like being able to do an auth handshake regardless pass or not)
if (
connectOnly &&
(error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"` ||
error.message.includes("no pg_hba.conf entry for host"))
) {
return;
} }
} }
: false throw new BadRequestError({
message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}`
});
}
},
rotateCredentials: async (currentCredentials) => {
const newPassword = alphaNumericNanoId(32);
// Note: The generated random password is not really going to make SQL Injection possible.
// The reason we are not using parameters binding is that the "ALTER USER" syntax is DDL,
// parameters binding is not supported. But just in case if the this code got copied
// around and repurposed, let's just do some naive escaping regardless
await client.raw(`ALTER USER :username: WITH PASSWORD '${newPassword.replace(/'/g, "''")}'`, {
username: currentCredentials.username
});
return { username: currentCredentials.username, password: newPassword };
},
close: () => client.destroy()
};
}
case PamResource.MySQL: {
return {
validate: async (connectOnly) => {
let client: Connection | null = null;
try {
// Notice: the reason we are not using Knex for mysql2 is because we don't need any fancy feature from Knex.
// mysql2 doesn't provide custom ssl verification function pass in.
// ref: https://github.com/sidorares/node-mysql2/blob/2543272a2ada8d8a07f74582549d7dd3fe948e2d/lib/base/connection.js#L358-L362
// and then even I tried to workaround it with Knex's pool afterCreate hook, but then encounter a bug:
// ref: https://github.com/knex/knex/issues/5352
// It appears that using Knex causing more troubles than not, we are just checking the connections,
// so it's much easier to create raw connection with the driver lib directly
client = await mysql.createConnection({
host: "localhost",
port: proxyPort,
user: actualUsername, // Use provided username or fallback
password: actualPassword, // Use provided password or fallback
database: connectionDetails.database,
ssl: sslEnabled
? {
rejectUnauthorized: sslRejectUnauthorized,
ca: sslCertificate
}
: undefined
});
await client.query(SIMPLE_QUERY);
} catch (error) {
if (connectOnly) {
// Hacky way to know if we successfully hit the database.
if (
error instanceof Error &&
error.message.startsWith(`Access denied for user '${TEST_CONNECTION_USERNAME}'@`)
) {
return;
}
}
// TODO: handle other errors, and throw standardlized errors providing user-friendly msg
throw error;
} finally {
await client?.end();
}
},
rotateCredentials: async () => {
// TODO: the pwd rotation for MySQL is not supported yet
throw new BadRequestError({
message: "Unsupported operation"
});
},
close: async () => {}
}; };
} }
default: default:
@@ -62,10 +194,9 @@ export const executeWithGateway = async <T>(
password?: string; password?: string;
}, },
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">, gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
operation: (client: Knex) => Promise<T> operation: (connection: SqlResourceConnection) => Promise<T>
): Promise<T> => { ): Promise<T> => {
const { connectionDetails, resourceType, gatewayId, username, password } = config; const { connectionDetails, gatewayId } = config;
const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true); const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true);
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
gatewayId, gatewayId,
@@ -79,22 +210,11 @@ export const executeWithGateway = async <T>(
return withGatewayV2Proxy( return withGatewayV2Proxy(
async (proxyPort) => { async (proxyPort) => {
const client = knex({ const connection = makeSqlConnection(proxyPort, config);
client: SQL_CONNECTION_CLIENT_MAP[resourceType],
connection: {
database: connectionDetails.database,
port: proxyPort,
host: "localhost",
user: username ?? TEST_CONNECTION_USERNAME, // Use provided username or fallback
password: password ?? TEST_CONNECTION_PASSWORD, // Use provided password or fallback
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
...getConnectionConfig(resourceType, connectionDetails)
}
});
try { try {
return await operation(client); return await operation(connection);
} finally { } finally {
await client.destroy(); await connection.close();
} }
}, },
{ {
@@ -115,25 +235,14 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
const validateConnection = async () => { const validateConnection = async () => {
try { try {
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => { await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
await client.raw("Select 1"); await client.validate(true);
}); });
return connectionDetails; return connectionDetails;
} catch (error) { } catch (error) {
// Hacky way to know if we successfully hit the database if (error instanceof BadRequestError && error.message === "Connection terminated unexpectedly") {
if (error instanceof BadRequestError) { throw new BadRequestError({
if (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"`) { message: "Connection terminated unexpectedly. Verify that host and port are correct"
return connectionDetails; });
}
if (error.message.includes("no pg_hba.conf entry for host")) {
return connectionDetails;
}
if (error.message === "Connection terminated unexpectedly") {
throw new BadRequestError({
message: "Connection terminated unexpectedly. Verify that host and port are correct"
});
}
} }
throw new BadRequestError({ throw new BadRequestError({
@@ -156,11 +265,12 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
}, },
gatewayV2Service, gatewayV2Service,
async (client) => { async (client) => {
await client.raw("Select 1"); await client.validate(false);
} }
); );
return credentials; return credentials;
} catch (error) { } catch (error) {
// TODO: extract these logic into each SQL connection
if (error instanceof BadRequestError) { if (error instanceof BadRequestError) {
if (error.message === `password authentication failed for user "${credentials.username}"`) { if (error.message === `password authentication failed for user "${credentials.username}"`) {
throw new BadRequestError({ throw new BadRequestError({
@@ -186,9 +296,7 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
currentCredentials currentCredentials
) => { ) => {
try { try {
const newPassword = alphaNumericNanoId(32); return await executeWithGateway(
await executeWithGateway(
{ {
connectionDetails, connectionDetails,
gatewayId, gatewayId,
@@ -197,20 +305,8 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
password: rotationAccountCredentials.password password: rotationAccountCredentials.password
}, },
gatewayV2Service, gatewayV2Service,
async (client) => { (client) => client.rotateCredentials(currentCredentials)
switch (resourceType) {
case PamResource.Postgres:
await client.raw(`ALTER USER ?? WITH PASSWORD '${newPassword}'`, [currentCredentials.username]);
break;
default:
throw new BadRequestError({
message: `Password rotation for ${resourceType as PamResource} is not supported.`
});
}
}
); );
return { username: currentCredentials.username, password: newPassword };
} catch (error) { } catch (error) {
if (error instanceof BadRequestError) { if (error instanceof BadRequestError) {
if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) { if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) {
@@ -1,7 +1,9 @@
import { TMySQLAccountCredentials, TMySQLResourceConnectionDetails } from "../../mysql/mysql-resource-types";
import { import {
TPostgresAccountCredentials, TPostgresAccountCredentials,
TPostgresResourceConnectionDetails TPostgresResourceConnectionDetails
} from "../../postgres/postgres-resource-types"; } from "../../postgres/postgres-resource-types";
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails; export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
export type TSqlAccountCredentials = TPostgresAccountCredentials; // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
export type TSqlAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
@@ -84,7 +84,7 @@ type TSamlConfigServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find">; membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find" | "create">;
}; };
export const samlConfigServiceFactory = ({ export const samlConfigServiceFactory = ({
@@ -183,6 +183,22 @@ export const samlConfigServiceFactory = ({
transaction transaction
); );
orgGroupsMap.set(groupName, newGroup); orgGroupsMap.set(groupName, newGroup);
const orgMembership = await membershipGroupDAL.create(
{
actorGroupId: newGroup.id,
scope: AccessScope.Organization,
scopeOrgId: orgId
},
transaction
);
await membershipRoleDAL.create(
{
membershipId: orgMembership.id,
role: OrgMembershipRole.NoAccess,
customRoleId: null
},
transaction
);
} }
} }
+1 -1
View File
@@ -3,13 +3,13 @@ import { z } from "zod";
import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { QueueWorkerProfile } from "@app/lib/types"; import { QueueWorkerProfile } from "@app/lib/types";
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { BadRequestError } from "../errors"; import { BadRequestError } from "../errors";
import { removeTrailingSlash } from "../fn"; import { removeTrailingSlash } from "../fn";
import { CustomLogger } from "../logger/logger"; import { CustomLogger } from "../logger/logger";
import { zpStr } from "../zod"; import { zpStr } from "../zod";
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
export const GITLAB_URL = "https://gitlab.com"; export const GITLAB_URL = "https://gitlab.com";
+3 -3
View File
@@ -43,6 +43,6 @@ export const GenericResourceNameSchema = z
export const BaseSecretNameSchema = z.string().trim().min(1); export const BaseSecretNameSchema = z.string().trim().min(1);
export const SecretNameSchema = BaseSecretNameSchema.refine( export const SecretNameSchema = BaseSecretNameSchema.refine(
(el) => !el.includes(":"), (el) => !el.includes(":") && !el.includes("/"),
"Secret name cannot contain colon." "Secret name cannot contain colon or forward slash."
).refine((el) => !el.includes("/"), "Secret name cannot contain forward slash."); );
@@ -8,7 +8,7 @@ interface PrincipalArnEntity {
SessionInfo: string; // Only populated for assumed-role SessionInfo: string; // Only populated for assumed-role
} }
export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => { export const extractPrincipalArnEntity = (arn: string, formatAsIamRole: boolean = false): PrincipalArnEntity => {
// split the ARN into parts using ":" as the delimiter // split the ARN into parts using ":" as the delimiter
const fullParts = arn.split(":"); const fullParts = arn.split(":");
if (fullParts.length !== 6) { if (fullParts.length !== 6) {
@@ -49,7 +49,7 @@ export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => {
} }
// assumed roles use a special format where the friendly name is the role name // assumed roles use a special format where the friendly name is the role name
const [roleName, sessionId] = rest; const [roleName, sessionId] = rest;
finalType = "assumed-role"; finalType = formatAsIamRole ? "role" : "assumed-role";
friendlyName = roleName; friendlyName = roleName;
sessionInfo = sessionId; sessionInfo = sessionId;
break; break;
@@ -83,9 +83,11 @@ export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => {
* Extracts the identity ARN from the GetCallerIdentity response to one of the following formats: * Extracts the identity ARN from the GetCallerIdentity response to one of the following formats:
* - arn:aws:iam::123456789012:user/MyUserName * - arn:aws:iam::123456789012:user/MyUserName
* - arn:aws:iam::123456789012:role/MyRoleName * - arn:aws:iam::123456789012:role/MyRoleName
* - arn:aws-us-gov:iam::123456789012:user/MyUserName (GovCloud)
* - arn:aws-us-gov:iam::123456789012:role/MyRoleName (GovCloud)
*/ */
export const extractPrincipalArn = (arn: string) => { export const extractPrincipalArn = (arn: string, formatAsIamRole: boolean = false) => {
const entity = extractPrincipalArnEntity(arn); const entity = extractPrincipalArnEntity(arn, formatAsIamRole);
return `arn:aws:${entity.Service}::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`; return `arn:${entity.Partition}:${formatAsIamRole ? "iam" : entity.Service}::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`;
}; };
@@ -158,7 +158,7 @@ export const identityAwsAuthServiceFactory = ({
// considers exact matches + wildcard matches // considers exact matches + wildcard matches
// heavily validated in router // heavily validated in router
const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`); const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`);
return regex.test(formattedArn); return regex.test(formattedArn) || regex.test(extractPrincipalArn(Arn, true));
}); });
if (!isArnAllowed) { if (!isArnAllowed) {
@@ -6,7 +6,7 @@ const twelveDigitRegex = new RE2(/^\d{12}$/);
// akhilmhdh: change this to a normal function later. Checked no redosable at the moment // akhilmhdh: change this to a normal function later. Checked no redosable at the moment
const arnRegex = new RE2( const arnRegex = new RE2(
/^arn:aws:(iam|sts)::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|assumed-role\/[a-zA-Z0-9_.@+*/-]+|\*)$/ /^arn:aws(?:-us-gov)?:(iam|sts)::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|assumed-role\/[a-zA-Z0-9_.@+*/-]+|\*)$/
); );
export const validateAccountIds = z export const validateAccountIds = z
@@ -55,7 +55,7 @@ export const validatePrincipalArns = z
}, },
{ {
message: message:
"Each ARN must be in the format of 'arn:aws:iam::123456789012:user/UserName', 'arn:aws:iam::123456789012:role/RoleName', or 'arn:aws:iam::123456789012:*', 'arn:aws:sts::123456789012:assumed-role/RoleName'." "Each ARN must be in the format of 'arn:aws:iam::123456789012:user/UserName', 'arn:aws:iam::123456789012:role/RoleName', or 'arn:aws:iam::123456789012:*', 'arn:aws:sts::123456789012:assumed-role/RoleName'. GovCloud ARNs (arn:aws-us-gov:...) are also supported."
} }
) )
// Transform to normalize the spaces around commas // Transform to normalize the spaces around commas
@@ -11,6 +11,7 @@ import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { SecretNameSchema } from "@app/server/lib/schemas";
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -408,6 +409,24 @@ export const secretSyncQueueFactory = ({
if (!Object.keys(importedSecrets).length) return {}; if (!Object.keys(importedSecrets).length) return {};
let invalidNameCount = 0;
let errorMessage = "";
for (const [key] of Object.entries(importedSecrets)) {
const result = SecretNameSchema.safeParse(key);
if (!result.success) {
invalidNameCount += 1;
if (errorMessage === "") errorMessage = result.error.issues[0]?.message;
}
}
if (invalidNameCount > 0) {
throw new SecretSyncError({
message: `Found ${invalidNameCount} invalid secret name${invalidNameCount === 1 ? "" : "s"}. ${errorMessage}`,
shouldRetry: false
});
}
const importedSecretMap: TSecretMap = {}; const importedSecretMap: TSecretMap = {};
const secretMap = await $getInfisicalSecrets(secretSync, false); const secretMap = await $getInfisicalSecrets(secretSync, false);
+38 -14
View File
@@ -49,10 +49,13 @@ User authentication is designed for individual developers and supports multiple
<ParamField query="Flags"> <ParamField query="Flags">
<Expandable title="properties"> <Expandable title="properties">
<ParamField query="email" type="string" optional> <ParamField query="email" type="string" optional>
Your email address. Required for direct login along with `--password`. Your email address. Required for direct login along with `--password` and `--organization-id`.
</ParamField> </ParamField>
<ParamField query="password" type="string" optional> <ParamField query="password" type="string" optional>
Your password. Required for direct login along with `--email`. Your password. Required for direct login along with `--email` and `--organization-id`.
</ParamField>
<ParamField query="organization-id" type="string" optional>
Your organization id. Required for direct login along with `--password` and `--email`.
</ParamField> </ParamField>
<ParamField query="interactive" type="boolean" optional> <ParamField query="interactive" type="boolean" optional>
Force interactive CLI login instead of browser-based authentication. Force interactive CLI login instead of browser-based authentication.
@@ -71,11 +74,12 @@ User authentication is designed for individual developers and supports multiple
</Accordion> </Accordion>
<Accordion title="Direct Login (CI/CD)"> <Accordion title="Direct Login (CI/CD)">
```bash ```bash
infisical login [email protected] --password=your-password infisical login [email protected] --password=your-password --organization-id=your-organization-id
# Or using environment variables # Or using environment variables
export INFISICAL_EMAIL="[email protected]" export INFISICAL_EMAIL="[email protected]"
export INFISICAL_PASSWORD="your-password" export INFISICAL_PASSWORD="your-password"
export INFISICAL_ORGANIZATION_ID="your-organization-id"
infisical login infisical login
``` ```
</Accordion> </Accordion>
@@ -86,7 +90,7 @@ User authentication is designed for individual developers and supports multiple
</Accordion> </Accordion>
<Accordion title="Plain Token Output (Useful for scripting and CI/CD)"> <Accordion title="Plain Token Output (Useful for scripting and CI/CD)">
```bash ```bash
export INFISICAL_TOKEN=$(infisical login [email protected] --password=your-password --plain --silent) export INFISICAL_TOKEN=$(infisical login [email protected] --password=your-password --organization-id=your-organization-id --plain --silent)
``` ```
</Accordion> </Accordion>
</AccordionGroup> </AccordionGroup>
@@ -404,11 +408,11 @@ The login command supports a number of flags that you can use for different auth
</Accordion> </Accordion>
<Accordion title="--email"> <Accordion title="--email">
```bash ```bash
infisical login --email=<email> --password=<password> infisical login --email=<email> --password=<password> --organization-id=<organization-id>
``` ```
#### Description #### Description
User email address. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--password` flag. User email address. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--password` and `--organization-id` flag.
<Tip> <Tip>
You can omit the **--method=user** if you want as it's the default method. You can omit the **--method=user** if you want as it's the default method.
@@ -421,11 +425,11 @@ The login command supports a number of flags that you can use for different auth
</Accordion> </Accordion>
<Accordion title="--password"> <Accordion title="--password">
```bash ```bash
infisical login --email=<email> --password=<password> infisical login --email=<email> --password=<password> --organization-id=<organization-id>
``` ```
#### Description #### Description
User password. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--email` flag. User password. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--email` and `--organization-id` flag.
<Warning> <Warning>
For security in CI/CD environments, prefer using the `INFISICAL_PASSWORD` environment variable instead of passing the password as a command-line flag. For security in CI/CD environments, prefer using the `INFISICAL_PASSWORD` environment variable instead of passing the password as a command-line flag.
@@ -439,6 +443,23 @@ The login command supports a number of flags that you can use for different auth
The `password` flag can be substituted with the `INFISICAL_PASSWORD` environment variable. The `password` flag can be substituted with the `INFISICAL_PASSWORD` environment variable.
</Tip> </Tip>
</Accordion>
<Accordion title="--organization-id">
```bash
infisical login --email=<email> --password=<password> --organization-id=<organization-id>
```
#### Description
User organization id. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--email` and `--password` flag.
<Tip>
You can omit the **--method=user** if you want as it's the default method.
</Tip>
<Tip>
The `organization-id` flag can be substituted with the `INFISICAL_ORGANIZATION_ID` environment variable.
</Tip>
</Accordion> </Accordion>
<Accordion title="--interactive"> <Accordion title="--interactive">
```bash ```bash
@@ -446,12 +467,12 @@ The login command supports a number of flags that you can use for different auth
``` ```
#### Description #### Description
Forces interactive CLI login where you'll be prompted to enter your email and password in the terminal, instead of opening a browser. Forces interactive CLI login where you'll be prompted to enter your email, password, and select your organization in the terminal, instead of opening a browser.
</Accordion> </Accordion>
<Accordion title="--plain"> <Accordion title="--plain">
```bash ```bash
infisical login --email=<email> --password=<password> --plain infisical login --email=<email> --password=<password> --organization-id=<organization-id> --plain
``` ```
#### Description #### Description
@@ -459,7 +480,7 @@ The login command supports a number of flags that you can use for different auth
```bash ```bash
# Example: Capture token in a variable # Example: Capture token in a variable
export INFISICAL_TOKEN=$(infisical login --email=<email> --password=<password> --plain --silent) export INFISICAL_TOKEN=$(infisical login --email=<email> --password=<password> --organization-id=<organization-id> --plain --silent)
``` ```
<Tip> <Tip>
@@ -506,13 +527,13 @@ The following examples demonstrate different ways to authenticate as a user with
```bash ```bash
# Basic direct login (defaults to US Cloud) # Basic direct login (defaults to US Cloud)
infisical login --email [email protected] --password "your-password" infisical login --email [email protected] --password "your-password" --organization-id "your-organization-id"
# EU Cloud (Custom domain) # EU Cloud (Custom domain)
infisical login --email [email protected] --password "your-password" --domain https://eu.infisical.com infisical login --email [email protected] --password "your-password" --organization-id "your-organization-id" --domain https://eu.infisical.com
# Output only JWT token for scripting # Output only JWT token for scripting
export INFISICAL_TOKEN=$(infisical login --email [email protected] --password "your-password" --plain --silent) export INFISICAL_TOKEN=$(infisical login --email [email protected] --password "your-password" --organization-id "your-organization-id" --plain --silent)
``` ```
#### Using Environment Variables (Recommended for CI/CD) #### Using Environment Variables (Recommended for CI/CD)
@@ -521,6 +542,7 @@ The following examples demonstrate different ways to authenticate as a user with
# Set credentials as environment variables # Set credentials as environment variables
export INFISICAL_EMAIL="[email protected]" export INFISICAL_EMAIL="[email protected]"
export INFISICAL_PASSWORD="your-password" export INFISICAL_PASSWORD="your-password"
export INFISICAL_ORGANIZATION_ID="your-organization-id"
# Login without additional flags # Login without additional flags
infisical login infisical login
@@ -542,6 +564,8 @@ The following examples demonstrate different ways to authenticate as a user with
- Email address - Email address
- Password - Password
After the prompt, you will be shown a list of organizations to choose from.
</Accordion> </Accordion>
</AccordionGroup> </AccordionGroup>
@@ -1428,7 +1428,7 @@ Enabling HSM encryption has a set of key benefits:
infisical: infisical:
image: image:
repository: infisical/infisical repository: infisical/infisical
tag: "v0.151.0-nightly-20251013.1" tag: "v0.151.0"
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
extraVolumeMounts: extraVolumeMounts:
+195 -74
View File
@@ -8,135 +8,256 @@ description: "Learn more about integrating with Infisical KMS using KMIP (Key Ma
[email protected] if you have any questions. [email protected] if you have any questions.
</Note> </Note>
## Overview Infisical KMS provides Key Management Interoperability Protocol (KMIP) support for integration with KMIP-compatible clients. This allows for enhanced key management across various applications that support the KMIP 1.4 protocol.
Infisical KMS provides **Key Management Interoperability Protocol (KMIP)** support, enabling seamless integration with KMIP-compatible clients. This allows for enhanced key management across various applications that support the **KMIP 1.4 protocol**. ## How KMIP Works with Infisical
## Supported Operations At a high level, the KMIP integration follows this architecture:
The Infisical KMIP server supports the following operations for **symmetric keys**:
- **Create** - Generate symmetric keys. ![KMIP Architecture Diagram](/images/kmip-diagram.png)
- **Register** - Register externally created keys.
- **Locate** - Find keys using attributes.
- **Get** - Retrieve keys securely.
- **Activate** - Enable keys for usage.
- **Revoke** - Revoke existing keys.
- **Destroy** - Permanently remove keys.
- **Get Attributes** - Retrieve metadata associated with keys.
- **Query** - Query server capabilities and supported operations.
## Benefits of KMIP Integration At a high level, the KMIP integration works as follows:
Integrating Infisical KMS with KMIP-compatible clients provides the following benefits: 1. KMIP clients (your applications or tools) communicate with the KMIP server
2. The KMIP server acts as a proxy and forwards requests to Infisical KMS
3. The KMIP server authenticates to Infisical using a machine identity
- **Standardized Key Management**: Allows interoperability with security and cryptographic applications that support KMIP. The KMIP server itself is deployed using the Infisical CLI (`infisical kmip start` command) and serves as an intermediary between your KMIP clients and Infisical's key management system.
- **Enterprise-Grade Security**: Utilizes Infisical’s encryption mechanisms to securely store and manage keys.
- **Centralized Key Management**: Enables a unified approach for managing cryptographic keys across multiple environments.
## Compatibility ### Supported Operations
Infisical KMIP supports **KMIP versions 1.0 to 1.4**, ensuring compatibility with a wide range of clients and security tools. The Infisical KMIP server supports the following operations for symmetric keys:
## Secure Communication & Authorization - **Create** - Generate symmetric keys
- **Register** - Register externally created keys
- **Locate** - Find keys using attributes
- **Get** - Retrieve keys securely
- **Activate** - Enable keys for usage
- **Revoke** - Revoke existing keys
- **Destroy** - Permanently remove keys
- **Get Attributes** - Retrieve metadata associated with keys
- **Query** - Query server capabilities and supported operations
KMIP client-server communication is secured using **mutual TLS (mTLS)**, ensuring strong identity verification and encrypted data exchange via **PKI certificates**. Each KMIP entity must possess valid certificates signed by a trusted Root CA to establish trust. ### Compatibility
For strong isolation, each Infisical organization has its own KMIP PKI (Public Key Infrastructure), ensuring that cryptographic operations and certificate authorities remain separate across organizations.
Infisical KMS enforces a **two-layer authorization model** for KMIP operations: Infisical KMIP supports KMIP versions 1.0 to 1.4, ensuring compatibility with a wide range of clients and security tools.
1. **KMIP Server Authorization** – The KMIP server, acting as a proxy, must have the `proxy KMIP` permission to forward client requests to Infisical KMS. This is done using a **machine identity** attached to the KMIP server. ### Network Requirements
2. **KMIP Client Authorization** – Clients must have the necessary KMIP-level permissions to perform specific key management operations.
By combining **mTLS for secure communication** and **machine identity-based proxying**, Infisical KMS ensures **strong authentication, controlled access, and centralized key management** for KMIP operations. Ensure the following network connectivity is in place:
## Setup Instructions - **KMIP Client → KMIP Server**: KMIP clients must be able to reach the KMIP server on port 5696 (or your configured port). Ensure firewalls allow this traffic and DNS resolution works if using hostnames.
### Setup KMIP for your organization - **KMIP Server → Infisical Platform**: The KMIP server needs outbound HTTP access to Infisical. For self-hosted instances, ensure connectivity to your custom domain.
## Configure and Deploy the KMIP Server
Follow these steps in order to set up KMIP integration with Infisical:
<Steps> <Steps>
<Step title="Navigate to the organization settings > KMIP"> <Step title="Enable KMIP at the Organization Level">
From there, press Setup KMIP. First, you need to enable KMIP for your entire Infisical organization and set up its PKI infrastructure.
Navigate to **Organization Settings > KMIP** and click **Setup KMIP**.
![KMIP org navigate](/images/platform/kms/kmip/kmip-org-setup-navigation.png) ![KMIP org navigate](/images/platform/kms/kmip/kmip-org-setup-navigation.png)
</Step>
<Step title="Configure KMIP PKI for the organization"> In the modal, select the desired key algorithm to use for the KMIP PKI of your organization, then click **Continue**.
In the modal, select the desired key algorithm to use for the KMIP PKI of your organization. Press continue.
![KMIP org PKI setup](/images/platform/kms/kmip/kmip-org-setup-modal.png) ![KMIP org PKI setup](/images/platform/kms/kmip/kmip-org-setup-modal.png)
This generates the KMIP PKI for your organization. After this, you can proceed to setting up your KMIP server. This generates the KMIP PKI for your organization, creating the cryptographic foundation that will be used for secure KMIP communications.
<Info>
You do not need to manage these certificates yourself; Infisical handles the PKI infrastructure for you.
</Info>
</Step> </Step>
</Steps>
### Deploying and Configuring the KMIP Server <Step title="Create a Machine Identity for the KMIP Server">
The KMIP server needs a machine identity to authenticate with Infisical and proxy requests on behalf of clients.
Follow these steps to configure and deploy a KMIP server. Configure a [machine identity](/documentation/platform/identities/machine-identities#machine-identities) by heading to your organization's **Access Control** and switching over to the **identities** tab.
From there you can click **Create Identity**.
<Steps> This guide assumes you'll be using the [Universal Auth](/documentation/platform/identities/universal-auth) method for the machine identity but you can choose any supported authentication method.
<Step title="Setup Machine Identity">
Configure a [machine identity](https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities) for the KMIP server to use.
![KMIP create machine identity](/images/platform/kms/kmip/kmip-create-mi.png) ![KMIP create machine identity](/images/platform/kms/kmip/kmip-create-mi.png)
This machine identity will be used by the KMIP server to authenticate and forward client requests to Infisical KMS.
</Step>
<Step title="Create a Custom Organization Role with Proxy KMIP Permission">
The machine identity needs permission to proxy KMIP requests.
Create a custom organization role and give it the **Proxy KMIP** permission. Create a custom organization role and give it the **Proxy KMIP** permission.
![KMIP create custom role](/images/platform/kms/kmip/kmip-create-custom-role.png) ![KMIP create custom role](/images/platform/kms/kmip/kmip-create-custom-role.png)
![KMIP assign proxy to role](/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png) ![KMIP assign proxy to role](/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png)
Assign the machine identity to the custom organization role. This allows the machine identity to serve KMIP client requests and forward them from your KMIP server to Infisical. This permission allows the KMIP server to act as an intermediary between KMIP clients and Infisical.
![KMIP assign role to machine identity](/images/platform/kms/kmip/kmip-assign-mi-to-role.png)
</Step> </Step>
<Step title="Start up the KMIP server"> <Step title="Assign the Organization Role to the Machine Identity">
To deploy the KMIP server, use the Infisical CLI’s `kmip start` command. Now connect the machine identity to the role you just created.
Before proceeding, make sure you have the [Infisical CLI installed](https://infisical.com/docs/cli/overview).
Once installed, launch the KMIP server with the following command: Assign the machine identity to the custom organization role.
![KMIP assign role to machine identity](/images/platform/kms/kmip/kmip-assign-mi-to-role.png)
This grants the machine identity the ability to serve KMIP client requests and forward them from your KMIP server to Infisical.
</Step>
<Step title="Deploy the KMIP Server">
Now you're ready to deploy the KMIP server.
You can run the KMIP server on any infrastructure that can reach the Infisical platform, such as a VM or container.
Once you have your infrastructure ready, you'll need to install the Infisical CLI on the server where you want to run the KMIP server.
To install the latest Infisical CLI visit [Infisical CLI instructions](https://infisical.com/docs/cli/overview).
If you need to install specific versions of the CLI, you can find them on the [Infisical CLI GitHub Releases](https://github.com/Infisical/cli/releases).
Then, launch the KMIP server with the following command:
```bash ```bash
infisical kmip start \ infisical kmip start \
--identity-client-id=<machine-identity-client-id> \ # This can be set by defining the INFISICAL_UNIVERSAL_AUTH_CLIENT_ID ENV variable --identity-client-id=example-client-id \
--identity-client-secret=<machine-identity-client-secret> \ # This can be set by defining the INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET ENV variable --identity-client-secret=example-client-secret \
--domain=https://app.infisical.com \ --domain=https://my-infisical-instance.com \
--listen-address="0.0.0.0:5696" \
--hostnames-or-ips="my-kmip-server.com" --hostnames-or-ips="my-kmip-server.com"
``` ```
The following flags are available for the `infisical kmip start` command:: **Available flags:**
- **listen-address** (default: localhost:5696): The address the KMIP server listens on. - **listen-address** (default: localhost:5696): The address the KMIP server listens on. In most cases you'll want to listen on all interfaces (0.0.0.0:5696)
- **identity-auth-method** (default: universal-auth): The authentication method for the machine identity. - **identity-auth-method** (default: universal-auth): The authentication method for the machine identity
- **identity-client-id**: The client ID of the machine identity. This can be set by defining the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` ENV variable. - **identity-client-id**: The client ID of the machine identity (can be set via `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` env var)
- **identity-client-secret**: The client secret of the machine identity. This can be set by defining the `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` ENV variable. - **identity-client-secret**: The client secret of the machine identity (can be set via `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` env var)
- **server-name** (default: "kmip-server"): The name of the KMIP server. - **server-name** (default: "kmip-server"): The name of the KMIP server
- **certificate-ttl** (default: "1y"): The duration for which the server certificate is valid. - **certificate-ttl** (default: "1y"): The duration for which the server certificate is valid
- **hostnames-or-ips:** A comma-separated list of hostnames or IPs the KMIP server will use (required). - **hostnames-or-ips**: The IP address or the hostname of the server where you have deployed the KMIP server.
Once started, your KMIP server is now running and ready to accept client connections. It will authenticate to Infisical using the machine identity and proxy all KMIP operations.
</Step> </Step>
</Steps>
### Add and Configure KMIP Clients <Step title="Navigate to Your KMS Project">
Now that the KMIP server is running, you need to register KMIP clients that will connect to it.
<Steps> Navigate to the desired KMS project if you already have one or create a new project of type KMS, then select **KMIP** once inside the project, and click **Add KMIP Client**.
<Step title="Navigate to the desired KMS project and select KMIP">
From there, press Add KMIP Client
![KMIP client overview](/images/platform/kms/kmip/kmip-client-overview.png) ![KMIP client overview](/images/platform/kms/kmip/kmip-client-overview.png)
</Step> </Step>
<Step title="Configure KMIP client">
In the modal, provide the details of your client. The selected permissions determine what KMIP operations can be performed in your KMS project. <Step title="Configure the KMIP Client">
Define the client and its permissions.
In the modal, provide the details of your client. The selected permissions determine what KMIP operations (Create, Get, Revoke, etc.) can be performed in your KMS project.
![KMIP client modal](/images/platform/kms/kmip/kmip-client-modal.png) ![KMIP client modal](/images/platform/kms/kmip/kmip-client-modal.png)
This creates a KMIP client entity in Infisical that will be authenticated via mTLS certificates.
</Step> </Step>
<Step title="Generate client certificate">
Once the KMIP client is created, you will have to generate a client certificate. <Step title="Generate Client Certificate">
Press Generate Certificate. Each KMIP client needs its own certificate for mTLS authentication.
Click **Generate Certificate** for your newly created client.
![KMIP generate client cert](/images/platform/kms/kmip/kmip-client-generate-cert.png) ![KMIP generate client cert](/images/platform/kms/kmip/kmip-client-generate-cert.png)
Provide the desired TTL and key algorithm to use and press Generate Client Certificate. Provide the desired TTL (time-to-live) and key algorithm, then click **Generate Client Certificate**.
![KMIP client cert config](/images/platform/kms/kmip/kmip-client-cert-config-modal.png) ![KMIP client cert config](/images/platform/kms/kmip/kmip-client-cert-config-modal.png)
Configure your KMIP clients to use the generated client certificate, certificate chain and private key. Download the generated client certificate, certificate chain, and private key.
![KMIP client cert modal](/images/platform/kms/kmip/kmip-client-certificate-modal.png) ![KMIP client cert modal](/images/platform/kms/kmip/kmip-client-certificate-modal.png)
Configure your KMIP-compatible applications or tools to use these credentials when connecting to the KMIP server. The client will now authenticate via mTLS and perform authorized key management operations through the KMIP server, which proxies requests to Infisical KMS.
</Step> </Step>
</Steps> </Steps>
## Additional Resources ## Connecting your KMIP Client to Infisical
- [KMIP 1.4 Specification](http://docs.oasis-open.org/kmip/spec/v1.4/os/kmip-spec-v1.4-os.html) After completing the setup, configure your KMIP compatible application to connect to the KMIP server.
While exact configuration steps vary by application, you'll generally need to provide:
1. **KMIP Server Address**: The hostname or IP and port where your KMIP server is listening (e.g., `my-kmip-server.com:5696`)
2. **Client Certificates**: The certificate credentials generated from your Infisical KMS project:
- **Client Certificate** (`client-cert.pem`) - Identifies your KMIP client
- **Client Private Key** (`client-key.pem`) - Used for mTLS authentication
- **Certificate Chain** (`cert-chain.pem`) - Verifies the KMIP server
### General Configuration Steps
<Steps>
<Step title="Identify Your KMIP Server Endpoint">
Determine the address where your KMIP server is accessible. This should match one of the hostnames or IPs you specified when starting the KMIP server with the `--hostnames-or-ips` flag.
**Example endpoints:**
- `my-kmip-server.com:5696`
- `10.0.1.50:5696`
- `kmip.example.com:5696`
The default port is `5696`, but this can be changed using the `--listen-address` flag when starting the server.
</Step>
<Step title="Prepare Certificate Files">
Organize the certificate materials you downloaded when generating the client certificate from the Infisical KMS project. You should have three files:
- **client-cert.pem** - The client certificate
- **cert-chain.pem** - The certificate chain (includes intermediate and root CA certificates)
- **client-key.pem** - The private key
Most KMIP clients require these files in PEM format, which is what Infisical provides by default.
</Step>
<Step title="Configure Your KMIP Client Application">
The exact configuration steps vary depending on your KMIP client application. Generally, you'll need to specify:
**Common configuration parameters:**
- **Server hostname/IP**: Your KMIP server address (e.g., `my-kmip-server.com`)
- **Server port**: Default is `5696`
- **Client certificate**: Path to `client-cert.pem`
- **Client private key**: Path to `client-key.pem`
- **CA certificate**: Path to `cert-chain.pem` (used to verify the server)
- **Protocol version**: KMIP 1.0 through 1.4 are supported
**Example configuration for PyKMIP:**
```ini
[client]
host=my-kmip-server.com
port=5696
certfile=/path/to/client-cert.pem
keyfile=/path/to/client-key.pem
ca_certs=/path/to/cert-chain.pem
```
</Step>
<Step title="Test the Connection">
Once configured, test the connection by performing a simple KMIP operation, such as:
- Querying server capabilities
- Creating a test key
- Listing available keys
If the connection is successful, your KMIP client is now integrated with Infisical KMS and can perform key management operations according to the permissions you assigned.
**Troubleshooting connection issues:**
- Verify network connectivity between your KMIP client and the KMIP server
- Check that certificate files are readable and in the correct format
- Ensure the KMIP server is running and accessible
- Review KMIP server logs for authentication errors
- Confirm the client certificate has not expired
If you require further verification of your certificate details and connectivity to the KMIP server from your KMIP client, you can use the following command from your client machine:
```bash
openssl s_client -connect kmip-server-ip-here:5696 --cert /path/to/client-cert.pem --key /path/to/client-cert.pem --CAfile /path/to/cert-chain.pem --tls1_2 --showcerts --state --debug
```
This command attempts to establish a TLS connection to the KMIP server using your client certificate and key, displaying detailed information about the handshake process.
If the connection is successful, you'll see the server's certificate chain and a message indicating that the handshake was completed.
</Step>
</Steps>
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

@@ -116,6 +116,27 @@ The platform utilizes Postgres to persist all of its data and Redis for caching
<ParamField query="DB_ROOT_CERT" type="string" default="" optional> <ParamField query="DB_ROOT_CERT" type="string" default="" optional>
Configure the SSL certificate for securing a Postgres connection by first encoding it in base64. Configure the SSL certificate for securing a Postgres connection by first encoding it in base64.
Use the following command to encode your certificate: `echo "<certificate>" | base64` Use the following command to encode your certificate: `echo "<certificate>" | base64`
Many cloud providers provide a CA certificate for their data regions that you can use to secure your connection with SSL.
<AccordionGroup>
<Accordion title="AWS RDS">
If you're hosting your database on AWS RDS, you can use their publicly available CA certificate as the database root certificate.
You can find all the available CA certificates for AWS RDS on the official [AWS RDS documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html).
As an example, if your RDS cluster is hosted in `us-east-1` _(US East, N. Virginia)_, you can use the following root certificate: https://truststore.pki.rds.amazonaws.com/us-east-1/us-east-1-bundle.pem.
All the available CA certificates can be found in the AWS RDS documentation linked above.
Remember to base64 encode the certificate before setting it as the `DB_ROOT_CERT` environment variable. `cat /path/to/certificate.pem | base64`.
```bash
DB_ROOT_CERT=LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1 # .... (base64 encoded certificate)
DB_CONNECTION_URI=<rds-endpoint>?sslmode=verify-ca # or verify-full depending on your security policies
```
</Accordion>
</AccordionGroup>
</ParamField> </ParamField>
<ParamField query="DB_READ_REPLICAS" type="string" default="" optional> <ParamField query="DB_READ_REPLICAS" type="string" default="" optional>
@@ -1,8 +1,10 @@
--- ---
title: "Kubernetes via Helm Chart" title: "Kubernetes via Helm Chart"
description: "Learn how to use Helm chart to install Infisical on your Kubernetes cluster." description: "Learn how to use Helm chart to install Infisical on your Kubernetes cluster."
--- ---
**Prerequisites** **Prerequisites**
- You have extensive understanding of [Kubernetes](https://kubernetes.io/) - You have extensive understanding of [Kubernetes](https://kubernetes.io/)
- Installed [Helm package manager](https://helm.sh/) version v3.11.3 or greater - Installed [Helm package manager](https://helm.sh/) version v3.11.3 or greater
- You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster - You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster
@@ -12,7 +14,7 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete
```bash ```bash
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
``` ```
``` ```bash
helm repo update helm repo update
``` ```
</Step> </Step>
@@ -61,6 +63,7 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete
</Tab> </Tab>
<Tab title="Production deployment"> <Tab title="Production deployment">
For production environments, we recommend using Cloud-based Platform as a Service (PaaS) solutions for PostgreSQL and Redis to ensure high availability. In on-premise setups, it's recommended to configure Redis and Postgres for high availability, either by using Bitnami charts or a custom configuration. For production environments, we recommend using Cloud-based Platform as a Service (PaaS) solutions for PostgreSQL and Redis to ensure high availability. In on-premise setups, it's recommended to configure Redis and Postgres for high availability, either by using Bitnami charts or a custom configuration.
```yaml simple-values-example.yaml ```yaml simple-values-example.yaml
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
@@ -74,6 +77,10 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete
DB_CONNECTION_URI: <> DB_CONNECTION_URI: <>
SITE_URL: <> SITE_URL: <>
``` ```
<Tip>
If you need to configure the SSL certificate for your production Postgres instance, you can use the `DB_ROOT_CERT` environment variable. [Learn more about configuring the SSL certificate](/self-hosting/configuration/envars#aws-rds).
</Tip>
</Tab> </Tab>
</Tabs> </Tabs>
</Step> </Step>
+3
View File
@@ -22,3 +22,6 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
*storybook.log
storybook-static
@@ -0,0 +1,12 @@
import { useEffect } from "react";
import type { Decorator } from "@storybook/react-vite";
export const DocumentDecorator: Decorator = (Story) => {
useEffect(() => {
const root = document.getElementsByTagName("html")[0];
root.setAttribute("class", "overflow-visible");
}, []);
return <Story />;
};
@@ -0,0 +1,17 @@
import { useMemo } from "react";
import type { Decorator } from "@storybook/react-vite";
import { createRootRoute, createRouter, RouterProvider } from "@tanstack/react-router";
export const RouterDecorator: Decorator = (Story) => {
const router = useMemo(() => {
const routeTree = createRootRoute({
component: Story
});
return createRouter({
routeTree
});
}, [Story]);
return <RouterProvider router={router as any} />;
};
+2
View File
@@ -0,0 +1,2 @@
export * from "./DocumentDecorator";
export * from "./RouterDecorator";
+14
View File
@@ -0,0 +1,14 @@
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: [
"../src/components/v3/**/*.mdx",
"../src/components/v3/**/*.stories.@(js|jsx|mjs|ts|tsx)"
],
addons: ["@storybook/addon-docs", "@storybook/addon-a11y"],
framework: {
name: "@storybook/react-vite",
options: {}
}
};
export default config;
+36
View File
@@ -0,0 +1,36 @@
import type { Preview } from "@storybook/react-vite";
import { DocumentDecorator, RouterDecorator } from "./decorators";
import "../src/index.css";
const preview: Preview = {
decorators: [DocumentDecorator, RouterDecorator],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i
}
},
docs: {
backgroundColor: "var(--background)"
},
a11y: {
test: "todo"
},
backgrounds: {
default: "dark",
options: {
dark: { name: "Dark", value: "var(--background)" }
}
}
},
initialGlobals: {
backgrounds: {
value: "dark"
}
}
};
export default preview;
+5 -1
View File
@@ -1,3 +1,6 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import storybook from "eslint-plugin-storybook";
import js from "@eslint/js"; import js from "@eslint/js";
import globals from "globals"; import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks"; import reactHooks from "eslint-plugin-react-hooks";
@@ -133,5 +136,6 @@ export default tseslint.config(
rules: Object.fromEntries( rules: Object.fromEntries(
Object.keys(stylisticPlugin.configs["all-flat"].rules ?? {}).map((key) => [key, "off"]) Object.keys(stylisticPlugin.configs["all-flat"].rules ?? {}).map((key) => [key, "off"])
) )
} },
storybook.configs["flat/recommended"]
); );
+1925 -164
View File
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -9,7 +9,9 @@
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint ./src", "lint": "eslint ./src",
"lint:fix": "eslint --fix ./src", "lint:fix": "eslint --fix ./src",
"type:check": "tsc --noEmit --project ./tsconfig.app.json" "type:check": "tsc --noEmit --project ./tsconfig.app.json",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
}, },
"overrides": { "overrides": {
"sha.js": "2.4.12" "sha.js": "2.4.12"
@@ -47,7 +49,9 @@
"@radix-ui/react-popper": "^1.2.1", "@radix-ui/react-popper": "^1.2.1",
"@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.1.3", "@radix-ui/react-select": "^2.1.3",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.3", "@radix-ui/react-toast": "^1.2.3",
@@ -64,6 +68,7 @@
"argon2-browser": "^1.18.0", "argon2-browser": "^1.18.0",
"axios": "^1.12.0", "axios": "^1.12.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"clsx": "^2.1.1",
"cva": "npm:class-variance-authority@^0.7.1", "cva": "npm:class-variance-authority@^0.7.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dompurify": "^3.2.4", "dompurify": "^3.2.4",
@@ -76,6 +81,7 @@
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lexical": "^0.29.0", "lexical": "^0.29.0",
"lucide-react": "^0.544.0",
"ms": "^2.1.3", "ms": "^2.1.3",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
@@ -105,6 +111,9 @@
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.15.0", "@eslint/js": "^9.15.0",
"@kesills/eslint-config-airbnb-typescript": "^20.0.0", "@kesills/eslint-config-airbnb-typescript": "^20.0.0",
"@storybook/addon-a11y": "^9.1.9",
"@storybook/addon-docs": "^9.1.9",
"@storybook/react-vite": "^9.1.9",
"@stylistic/eslint-plugin": "^2.12.1", "@stylistic/eslint-plugin": "^2.12.1",
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/postcss": "^4.1.14",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
@@ -130,6 +139,7 @@
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.14", "eslint-plugin-react-refresh": "^0.4.14",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-storybook": "^9.1.9",
"globals": "^15.12.0", "globals": "^15.12.0",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"prettier": "3.4.2", "prettier": "3.4.2",
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long
@@ -8,22 +8,40 @@ type Props = {
isOpen?: boolean; isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void; onOpenChange?: (isOpen: boolean) => void;
text: string; text: string;
isEnterpriseFeature?: boolean;
}; };
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => { export const UpgradePlanModal = ({
text,
isOpen,
onOpenChange,
isEnterpriseFeature = false
}: Props): JSX.Element => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { mutateAsync, isPending } = useGetOrgTrialUrl(); const { mutateAsync, isPending } = useGetOrgTrialUrl();
const link =
subscription && subscription.slug !== null const getLink = () => {
? ("/organization/billing" as const) // self-hosting
: "https://infisical.com/scheduledemo"; if (!subscription || subscription.slug === null) {
return "https://infisical.com/scheduledemo";
}
// Infisical cloud
if (isEnterpriseFeature) {
return "https://infisical.com/talk-to-us";
}
return "/organization/billing" as const;
};
const link = getLink();
const handleUpgradeBtnClick = async () => { const handleUpgradeBtnClick = async () => {
try { try {
if (!subscription || !currentOrg) return; if (!subscription || !currentOrg) return;
if (!subscription.has_used_trial) { if (!subscription.has_used_trial && !isEnterpriseFeature) {
// direct user to start pro trial // direct user to start pro trial
const url = await mutateAsync({ const url = await mutateAsync({
@@ -40,6 +58,17 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
console.error(err); console.error(err);
} }
}; };
const getUpgradePlanLabel = () => {
if (subscription) {
if (isEnterpriseFeature) {
return "Talk to Us";
}
if (!subscription.has_used_trial) {
return "Start Pro Free Trial";
}
}
return "Upgrade Plan";
};
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
@@ -55,7 +84,7 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
onClick={handleUpgradeBtnClick} onClick={handleUpgradeBtnClick}
className="mr-4" className="mr-4"
> >
{subscription && !subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"} {getUpgradePlanLabel()}
</Button> </Button>
<Button <Button
colorSchema="secondary" colorSchema="secondary"
@@ -0,0 +1,307 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Link } from "@tanstack/react-router";
import {
BanIcon,
BoxesIcon,
BoxIcon,
CheckIcon,
ChevronsUpDownIcon,
CircleXIcon,
ExternalLinkIcon,
GlobeIcon,
InfoIcon,
RadarIcon,
TriangleAlertIcon
} from "lucide-react";
import { Badge } from "./Badge";
/**
* Badges act as an indicator that can optionally be made interactable.
* You can place text and icons inside a badge.
* Badges are often used for the indication of a status, state or scope.
*/
const meta = {
title: "Generic/Badge",
component: Badge,
parameters: {
layout: "centered"
},
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["neutral", "success", "info", "warning", "danger", "project", "org", "sub-org"]
},
isTruncatable: {
table: {
disable: true
}
},
asChild: {
table: {
disable: true
}
},
children: {
table: {
disable: true
}
}
},
args: { children: "Badge", isTruncatable: false }
} satisfies Meta<typeof Badge>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Neutral: Story = {
name: "Variant: Neutral",
args: {
variant: "neutral",
children: (
<>
<BanIcon />
Disabled
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating neutral or disabled states."
}
}
}
};
export const Success: Story = {
name: "Variant: Success",
args: {
variant: "success",
children: (
<>
<CheckIcon />
Success
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating successful or healthy states."
}
}
}
};
export const Info: Story = {
name: "Variant: Info",
args: {
variant: "info",
children: (
<>
<InfoIcon />
Info
</>
)
},
parameters: {
docs: {
description: {
story:
"Use this variant when indicating informational states or linking to external references."
}
}
}
};
export const Warning: Story = {
name: "Variant: Warning",
args: {
variant: "warning",
children: (
<>
<TriangleAlertIcon />
Warning
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating activity or attention warranting states."
}
}
}
};
export const Danger: Story = {
name: "Variant: Danger",
args: {
variant: "danger",
children: (
<>
<CircleXIcon />
Danger
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating destructive or error states."
}
}
}
};
export const Organization: Story = {
name: "Variant: Organization",
args: {
variant: "org",
children: (
<>
<GlobeIcon />
Organization
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating organization scope or links."
}
}
}
};
export const SubOrganization: Story = {
name: "Variant: Sub-Organization",
args: {
variant: "sub-org",
children: (
<>
<BoxesIcon />
Sub-Organization
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating sub-organization scope or links."
}
}
}
};
export const Project: Story = {
name: "Variant: Project",
args: {
variant: "project",
children: (
<>
<BoxIcon />
Project
</>
)
},
parameters: {
docs: {
description: {
story: "Use this variant when indicating project scope or links."
}
}
}
};
export const AsExternalLink: Story = {
name: "Example: As External Link",
args: {
variant: "info",
asChild: true,
children: (
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com/">
Link <ExternalLinkIcon />
</a>
)
},
parameters: {
docs: {
description: {
story: "Use the `asChild` prop with an `a` tag to use a badge as an external link."
}
}
}
};
export const AsRouterLink: Story = {
name: "Example: As Router Link",
args: {
variant: "project",
asChild: true,
children: (
<Link to=".">
<RadarIcon />
Secret Scanning
</Link>
)
},
parameters: {
docs: {
description: {
story: "Use the `asChild` prop with a `Link` component to use a badge as an internal link."
}
}
}
};
export const AsButton: Story = {
name: "Example: As Button",
args: {
variant: "org",
asChild: true,
children: (
<button type="button" onClick={() => console.log("click")}>
<GlobeIcon />
Organization
<ChevronsUpDownIcon />
</button>
)
},
parameters: {
docs: {
description: {
story:
"Use the `asChild` prop with a `button` tag to use a badge as a button. Do not use a styled `Button` component."
}
}
}
};
export const IsTruncatable: Story = {
name: "Example: isTruncatable",
args: {
isTruncatable: true,
children: (
<>
<GlobeIcon />
<span>Infisical Infrastructure</span>
</>
)
},
parameters: {
docs: {
description: {
story:
"Use the `isTruncatable` prop with a `span` tag wrapping the text content to support truncation."
}
}
},
decorators: (Story) => (
<div className="flex w-32">
<Story />
</div>
)
};
@@ -0,0 +1,58 @@
import { forwardRef } from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "cva";
import { cn } from "@app/components/v3/utils";
const badgeVariants = cva(
[
"select-none items-center rounded-sm px-1.5 py-0.5 text-xs",
"gap-x-1 [a&,button&]:cursor-pointer inline-flex",
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&>svg]:size-3",
"transition duration-200 ease-in-out"
],
{
variants: {
isTruncatable: {
true: "[&>span,&>p]:truncate min-w-0",
false: "w-fit shrink-0 whitespace-nowrap overflow-hidden"
},
variant: {
neutral: "bg-neutral/30 text-neutral [a&,button&]:hover:bg-neutral/40",
success: "bg-success/30 text-success [a&,button&]:hover:bg-success/40",
info: "bg-info/30 text-info [a&,button&]:hover:bg-info/40",
warning: "bg-warning/30 text-warning [a&,button&]:hover:bg-warning/40",
danger: "bg-danger/30 text-danger [a&,button&]:hover:bg-danger/40",
project: "bg-project/30 text-project [a&,button&]:hover:bg-project/40",
org: "bg-org/30 text-org [a&,button&]:hover:bg-org/40",
"sub-org": "bg-sub-org/30 text-sub-org [a&,button&]:hover:bg-sub-org/40"
}
},
defaultVariants: {
variant: "success"
}
}
);
type TBadgeProps = VariantProps<typeof badgeVariants> &
React.ComponentProps<"span"> & {
asChild?: boolean;
};
const Badge = forwardRef<HTMLSpanElement, TBadgeProps>(
({ className, variant, asChild = false, isTruncatable = false, ...props }, ref): JSX.Element => {
const Comp = asChild ? Slot : "span";
return (
<Comp
ref={ref}
data-slot="badge"
className={cn(badgeVariants({ variant, isTruncatable }), className)}
{...props}
/>
);
}
);
Badge.displayName = "Badge";
export { Badge, badgeVariants, type TBadgeProps };
@@ -0,0 +1 @@
export * from "./Badge";
@@ -0,0 +1 @@
export * from "./Badge";
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+5 -1
View File
@@ -62,7 +62,11 @@ export const ROUTE_PATHS = Object.freeze({
"/organization/app-connections/$appConnection/oauth/callback", "/organization/app-connections/$appConnection/oauth/callback",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
) )
} },
NetworkingPage: setRoute(
"/organization/networking",
"/_authenticate/_inject-org-details/_org-layout/organization/networking"
)
}, },
SecretManager: { SecretManager: {
ApprovalPage: setRoute( ApprovalPage: setRoute(
+1
View File
@@ -31,6 +31,7 @@ export * from "./pkiSubscriber";
export * from "./projects"; export * from "./projects";
export * from "./projectUserAdditionalPrivilege"; export * from "./projectUserAdditionalPrivilege";
export * from "./rateLimit"; export * from "./rateLimit";
export * from "./relays";
export * from "./roles"; export * from "./roles";
export * from "./scim"; export * from "./scim";
export * from "./secretApproval"; export * from "./secretApproval";
@@ -66,6 +66,8 @@ export type PlanBillingInfo = {
interval: "month" | "year"; interval: "month" | "year";
intervalCount: number; intervalCount: number;
quantity: number; quantity: number;
users: number;
identities: number;
}; };
export type Invoice = { export type Invoice = {
+1
View File
@@ -1,5 +1,6 @@
export enum PamResourceType { export enum PamResourceType {
Postgres = "postgres", Postgres = "postgres",
MySQL = "mysql",
RDP = "rdp", RDP = "rdp",
SSH = "ssh", SSH = "ssh",
Kubernetes = "kubernetes" Kubernetes = "kubernetes"
+1
View File
@@ -5,6 +5,7 @@ export const PAM_RESOURCE_TYPE_MAP: Record<
{ name: string; image: string; size?: number } { name: string; image: string; size?: number }
> = { > = {
[PamResourceType.Postgres]: { name: "PostgreSQL", image: "Postgres.png" }, [PamResourceType.Postgres]: { name: "PostgreSQL", image: "Postgres.png" },
[PamResourceType.MySQL]: { name: "MySQL", image: "MySql.png" },
[PamResourceType.RDP]: { name: "RDP", image: "RDP.png" }, [PamResourceType.RDP]: { name: "RDP", image: "RDP.png" },
[PamResourceType.SSH]: { name: "SSH", image: "SSH.png" }, [PamResourceType.SSH]: { name: "SSH", image: "SSH.png" },
[PamResourceType.Kubernetes]: { name: "Kubernetes", image: "Kubernetes.png" } [PamResourceType.Kubernetes]: { name: "Kubernetes", image: "Kubernetes.png" }
+4 -2
View File
@@ -1,11 +1,13 @@
import { PamResourceType, PamSessionStatus } from "../enums"; import { PamResourceType, PamSessionStatus } from "../enums";
import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
import { TMySQLAccount, TMySQLResource } from "./mysql-resource";
export * from "./postgres-resource"; export * from "./postgres-resource";
export * from "./mysql-resource";
export type TPamResource = TPostgresResource; export type TPamResource = TPostgresResource | TMySQLResource;
export type TPamAccount = TPostgresAccount; export type TPamAccount = TPostgresAccount | TMySQLAccount;
export type TPamFolder = { export type TPamFolder = {
id: string; id: string;
@@ -0,0 +1,14 @@
import { PamResourceType } from "../enums";
import { TBaseSqlConnectionDetails, TBaseSqlCredentials } from "./shared/sql-resource";
import { TBasePamAccount } from "./base-account";
import { TBasePamResource } from "./base-resource";
// Resources
export type TMySQLResource = TBasePamResource & { resourceType: PamResourceType.MySQL } & {
connectionDetails: TBaseSqlConnectionDetails;
};
// Accounts
export type TMySQLAccount = TBasePamAccount & {
credentials: TBaseSqlCredentials;
};
+16 -2
View File
@@ -37,12 +37,26 @@
} }
@theme { @theme {
/*legacy color schema */
/* Fonts */ /* Fonts */
--font-inter: "Inter", sans-serif; --font-inter: "Inter", sans-serif;
--max-width-8xl: 88rem; /* 1408px */
/* Colors v2 */
--color-background: #19191c;
--color-foreground: white;
--color-success: #2ecc71;
--color-info: #34c2db;
--color-warning: #f1c40f;
--color-danger: #e74c3c;
--color-org: #30B3FF;
--color-sub-org: #96ff59;
--color-project: #e0ed34;
--color-neutral: #adaeb0;
/*legacy color schema */
--color-org-v1: #30B3FF; --color-org-v1: #30B3FF;
--color-namespace-v1: #96ff59; --color-namespace-v1: #96ff59;
--max-width-8xl: 88rem; /* 1408px */
/* Primary */ /* Primary */
--color-primary-50: #fffff5; --color-primary-50: #fffff5;
--color-primary-100: #fcfce8; --color-primary-100: #fcfce8;
@@ -111,6 +111,7 @@ type TCertificateDetails = {
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => { export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null); const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const [shouldShowSubjectSection, setShouldShowSubjectSection] = useState(true);
const { currentProject } = useProject(); const { currentProject } = useProject();
const inputSerialNumber = const inputSerialNumber =
@@ -127,19 +128,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const { mutateAsync: createCertificate } = useCreateCertificateV3(); const { mutateAsync: createCertificate } = useCreateCertificateV3();
const selectedProfileId = useMemo(() => { const formResolver = useMemo(() => {
const form = document.querySelector('select[name="profileId"]') as HTMLSelectElement; return zodResolver(createSchema(shouldShowSubjectSection));
return form?.value || profileId || ""; }, [shouldShowSubjectSection]);
}, [profileId]);
const selectedProfile = useMemo(
() => profilesData?.certificateProfiles?.find((p) => p.id === selectedProfileId),
[profilesData?.certificateProfiles, selectedProfileId]
);
const { data: templateData } = useGetCertificateTemplateV2ById({
templateId: selectedProfile?.certificateTemplateId || ""
});
const { const {
control, control,
@@ -150,7 +141,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
formState, formState,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(createSchema((templateData?.subject?.length || 0) > 0)), resolver: formResolver,
defaultValues: { defaultValues: {
profileId: profileId || "", profileId: profileId || "",
subjectAttributes: [], subjectAttributes: [],
@@ -169,6 +160,16 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
[profilesData?.certificateProfiles, actualSelectedProfileId] [profilesData?.certificateProfiles, actualSelectedProfileId]
); );
const { data: templateData } = useGetCertificateTemplateV2ById({
templateId: actualSelectedProfile?.certificateTemplateId || ""
});
useEffect(() => {
if (templateData !== undefined) {
setShouldShowSubjectSection((templateData?.subject?.length || 0) > 0);
}
}, [templateData]);
const { const {
constraints, constraints,
filteredKeyUsages, filteredKeyUsages,
@@ -186,6 +187,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const resetAllState = useCallback(() => { const resetAllState = useCallback(() => {
setCertificateDetails(null); setCertificateDetails(null);
setShouldShowSubjectSection(true);
resetConstraints(); resetConstraints();
reset(); reset();
}, [reset, resetConstraints]); }, [reset, resetConstraints]);
@@ -1,10 +1,10 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { faArrowUpRightFromSquare, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button, Tooltip } from "@app/components/v2";
import { import {
OrgPermissionBillingActions, OrgPermissionBillingActions,
OrgPermissionSubjects, OrgPermissionSubjects,
@@ -27,6 +27,9 @@ export const PreviewSection = () => {
const { subscription } = useSubscription(true); const { subscription } = useSubscription(true);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? ""); const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? "");
const totalAmount = data?.amount ? data.amount * (data.users + data.identities) : 0;
const getOrgTrialUrl = useGetOrgTrialUrl(); const getOrgTrialUrl = useGetOrgTrialUrl();
const createCustomerPortalSession = useCreateCustomerPortalSession(); const createCustomerPortalSession = useCreateCustomerPortalSession();
@@ -190,14 +193,28 @@ export const PreviewSection = () => {
</OrgPermissionCan> </OrgPermissionCan>
)} )}
</div> </div>
<div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> {subscription.slug !== "enterprise" ? (
<p className="mb-2 text-gray-400">Price</p> <div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-8 text-2xl font-medium text-mineshaft-50"> <p className="mb-2 text-gray-400">Price</p>
{subscription.status === "trialing" <p className="mb-8 text-2xl font-medium text-mineshaft-50">
? "$0.00 / month" {subscription.status === "trialing" ? (
: `${formatAmount(data.amount)} / ${data.interval}`} "$0.00 / month"
</p> ) : (
</div> <>
{formatAmount(totalAmount)} / {data.interval}
{(subscription.slug === "pro" || subscription.slug === "pro-annual") && (
<Tooltip
content={`Total price is based on the number of users and machine identities at ${formatAmount(data.amount)} each. You have ${data.users} ${data.users > 1 ? "users" : "user"} and ${data.identities} ${data.identities > 1 ? "machine identities" : "machine identity"}.`}
className="max-w-lg"
>
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" size="xs" />
</Tooltip>
)}
</>
)}
</p>
</div>
) : null}
<div className="flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-2 text-gray-400">Subscription renews on</p> <p className="mb-2 text-gray-400">Subscription renews on</p>
<p className="mb-8 text-2xl font-medium text-mineshaft-50"> <p className="mb-8 text-2xl font-medium text-mineshaft-50">
@@ -8,6 +8,7 @@ import {
faEllipsisV, faEllipsisV,
faInfoCircle, faInfoCircle,
faMagnifyingGlass, faMagnifyingGlass,
faPlus,
faSearch, faSearch,
faTrash faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
@@ -17,6 +18,7 @@ import { useQuery } from "@tanstack/react-query";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button,
DeleteActionModal, DeleteActionModal,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -47,6 +49,7 @@ import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways
import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2"; import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2";
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal"; import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
import { GatewayDeployModal } from "./components/GatewayDeployModal";
const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => {
const heartbeatDate = heartbeat ? new Date(heartbeat) : null; const heartbeatDate = heartbeat ? new Date(heartbeat) : null;
@@ -73,6 +76,7 @@ export const GatewayTab = withPermission(
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"deployGateway",
"deleteGateway", "deleteGateway",
"editDetails" "editDetails"
] as const); ] as const);
@@ -101,8 +105,8 @@ export const GatewayTab = withPermission(
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex grow items-center gap-2">
<h3 className="text-lg font-medium text-mineshaft-100">Gateways</h3> <h3 className="text-lg font-medium text-mineshaft-100">Gateways</h3>
<a <a
href="https://infisical.com/docs/documentation/platform/gateways/overview" href="https://infisical.com/docs/documentation/platform/gateways/overview"
@@ -118,6 +122,14 @@ export const GatewayTab = withPermission(
/> />
</div> </div>
</a> </a>
<div className="flex grow" />
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("deployGateway")}
>
Deploy Gateway
</Button>
</div> </div>
</div> </div>
<p className="mb-4 text-sm text-mineshaft-400"> <p className="mb-4 text-sm text-mineshaft-400">
@@ -257,6 +269,10 @@ export const GatewayTab = withPermission(
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => handleDeleteGateway()} onDeleteApproved={() => handleDeleteGateway()}
/> />
<GatewayDeployModal
isOpen={popUp.deployGateway.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deployGateway", isOpen)}
/>
</TableContainer> </TableContainer>
</div> </div>
</div> </div>
@@ -0,0 +1,387 @@
import { useMemo, useState } from "react";
import { SingleValue } from "react-select";
import { faCopy, faQuestionCircle, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FilterableSelect,
FormLabel,
IconButton,
Input,
ModalClose,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
OrgPermissionIdentityActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import {
useAddIdentityTokenAuth,
useCreateTokenIdentityTokenAuth,
useGetIdentityMembershipOrgs,
useGetIdentityTokenAuth,
useGetRelays
} from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
import { RelayOption } from "./RelayOption";
const baseFormSchema = z.object({
name: slugSchema({ field: "name" }),
instanceDomain: z.string().url("Must be a valid URL").or(z.literal("")),
relay: z
.object(
{
id: z.string(),
name: z.string()
},
{ required_error: "Relay is required" }
)
.nullable()
.refine((val) => val !== null, { message: "Relay is required" })
});
const formSchemaWithIdentity = baseFormSchema.extend({
identity: z
.object(
{
id: z.string(),
name: z.string()
},
{ required_error: "Identity is required" }
)
.nullable()
.refine((val) => val !== null, { message: "Identity is required" })
});
const formSchemaWithToken = baseFormSchema.extend({
identityToken: z.string().min(1, "Token is required")
});
export const GatewayCliDeploymentMethod = () => {
const { protocol, hostname, port } = window.location;
const portSuffix = port && port !== "80" ? `:${port}` : "";
const siteURL = `${protocol}//${hostname}${portSuffix}`;
const navigate = useNavigate({
from: ROUTE_PATHS.Organization.NetworkingPage.path
});
const [autogenerateToken, setAutogenerateToken] = useState(true);
const [step, setStep] = useState<"form" | "command">("form");
const [name, setName] = useState("");
const [instanceDomain, setInstanceDomain] = useState(siteURL);
const [relay, setRelay] = useState<null | {
id: string;
name: string;
}>(null);
const [identity, setIdentity] = useState<null | {
id: string;
name: string;
}>(null);
const [identityToken, setIdentityToken] = useState("");
const [formErrors, setFormErrors] = useState<z.ZodIssue[]>([]);
const errors = useMemo(() => {
const errorMap: Record<string, string | undefined> = {};
formErrors.forEach((issue) => {
if (issue.path.length > 0) {
errorMap[String(issue.path[0])] = issue.message;
}
});
return errorMap;
}, [formErrors]);
const { data: relays, isPending: isRelaysLoading } = useGetRelays();
const { currentOrg } = useOrganization();
const organizationId = currentOrg?.id || "";
const { permission } = useOrgPermission();
const canCreateToken = permission.can(
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity
);
const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } =
useGetIdentityMembershipOrgs({
organizationId,
limit: 20000
});
const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || [];
const { mutateAsync: createToken, isPending: isCreatingToken } =
useCreateTokenIdentityTokenAuth();
const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } =
useAddIdentityTokenAuth();
const { refetch } = useGetIdentityTokenAuth(identity?.id ?? "");
const handleGenerateCommand = async () => {
setFormErrors([]);
if (canCreateToken && autogenerateToken) {
const validation = formSchemaWithIdentity.safeParse({
name,
relay,
identity,
instanceDomain
});
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
const validatedIdentity = validation.data.identity;
try {
const { data: identityTokenAuth } = await refetch();
if (!identityTokenAuth) {
await addIdentityTokenAuth({
identityId: validatedIdentity.id,
organizationId,
accessTokenTTL: 2592000,
accessTokenMaxTTL: 2592000,
accessTokenNumUsesLimit: 0,
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
createNotification({
text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.",
type: "warning"
});
}
const token = await createToken({
identityId: validatedIdentity.id,
name: `gateway token for ${name} (autogenerated)`
});
setIdentityToken(token.accessToken);
createNotification({
text: "Automatically generated a token for the selected identity.",
type: "info"
});
setStep("command");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to generate token for the selected identity",
type: "error"
});
setIdentityToken("");
}
} else {
const validation = formSchemaWithToken.safeParse({
name,
relay,
identityToken,
instanceDomain
});
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
setStep("command");
}
};
const command = useMemo(() => {
const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : "";
return `infisical gateway start --name=${name} --relay=${
relay?.name || ""
}${domainFlag} --token=${identityToken}`;
}, [name, relay, identityToken, instanceDomain]);
if (step === "command") {
return (
<>
<FormLabel label="CLI Command" />
<div className="flex gap-2">
<Input value={command} isDisabled />
<IconButton
ariaLabel="copy"
variant="outline_bg"
colorSchema="secondary"
onClick={() => {
navigator.clipboard.writeText(command);
createNotification({
text: "Command copied to clipboard",
type: "info"
});
}}
className="w-10"
>
<FontAwesomeIcon icon={faCopy} />
</IconButton>
</div>
<a
href="https://infisical.com/docs/cli/overview"
target="_blank"
className="mt-2 flex h-4 w-fit items-center gap-2 border-b border-mineshaft-400 text-sm text-mineshaft-400 transition-colors duration-100 hover:border-yellow-400 hover:text-yellow-400"
rel="noreferrer"
>
<span>Install the Infisical CLI</span>
<FontAwesomeIcon icon={faUpRightFromSquare} className="size-3" />
</a>
<div className="mt-6 flex items-center">
<ModalClose asChild>
<Button className="mr-4" size="sm" colorSchema="secondary">
Done
</Button>
</ModalClose>
</div>
</>
);
}
return (
<>
<FormLabel label="Name" tooltipText="The name for your gateway." />
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter gateway name..."
isError={Boolean(errors.name)}
/>
{errors.name && <p className="mt-1 text-sm text-red">{errors.name}</p>}
<FormLabel label="Relay" tooltipText="The relay to use with your gateway." className="mt-4" />
<FilterableSelect
value={relay}
onChange={(newValue) => {
if ((newValue as SingleValue<{ id: string }>)?.id === "_create") {
navigate({
search: (prev) => ({ ...prev, selectedTab: "relays", action: "deploy-relay" })
});
return;
}
setRelay(newValue as SingleValue<{ id: string; name: string }>);
}}
isLoading={isRelaysLoading}
options={[
{
id: "_create",
name: "Deploy New Relay"
},
...(relays || [])
]}
placeholder="Select relay..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: RelayOption }}
/>
{errors.relay && <p className="mt-1 text-sm text-red">{errors.relay}</p>}
<FormLabel
label="Infisical Instance Host Address"
tooltipText="The host address of the infisical instance that's accessible by the gateway."
className="mt-4"
/>
<Input
value={instanceDomain}
onChange={(e) => setInstanceDomain(e.target.value)}
placeholder="https://app.infisical.com"
isError={Boolean(errors.instanceDomain)}
/>
{errors.instanceDomain && <p className="mt-1 text-sm text-red">{errors.instanceDomain}</p>}
{canCreateToken && autogenerateToken ? (
<>
<FormLabel
label="Identity"
tooltipText="The identity that your gateway will use for authentication."
className="mt-4"
/>
<FilterableSelect
value={identity}
onChange={(e) =>
setIdentity(
e as SingleValue<{
id: string;
name: string;
}>
)
}
isLoading={isIdentitiesLoading}
placeholder="Select identity..."
options={identityMembershipOrgs.map((membership) => membership.identity)}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
{errors.identity && <p className="mt-1 text-sm text-red">{errors.identity}</p>}
</>
) : (
<>
<FormLabel
label="Identity Token"
tooltipText="The identity token that your relay will use for authentication."
className="mt-4"
/>
<Input
value={identityToken}
onChange={(e) => setIdentityToken(e.target.value)}
placeholder="Enter identity token..."
isError={Boolean(errors.identityToken)}
/>
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
</>
)}
{canCreateToken && (
<div className="mt-2">
<Checkbox
isChecked={autogenerateToken}
onCheckedChange={(e) => {
setAutogenerateToken(Boolean(e));
}}
id="autogenerate-token"
className="mr-2"
>
<div className="flex items-center">
<span>Automatically enable token auth and generate a token for identity</span>
<Tooltip
className="max-w-md"
content={
<>
Token authentication will be automatically enabled for the selected identity if
it isn&apos;t already configured. By default, it will be configured to allow all
IP addresses with a token TTL of 30 days. You can manage these settings in
Access Control.
<br />
<br />A token will automatically be generated to be used with the CLI command.
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="mt-0.5 ml-1" />
</Tooltip>
</div>
</Checkbox>
</div>
)}
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
colorSchema="secondary"
onClick={handleGenerateCommand}
isLoading={isCreatingToken || isAddingTokenAuth}
>
Continue
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</>
);
};
@@ -0,0 +1,45 @@
import { useState } from "react";
import { Modal, ModalContent } from "@app/components/v2";
import { GatewayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect";
import { GatewayCliDeploymentMethod } from "./GatewayCliDeploymentMethod";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const GatewayDeploymentInfoMap = {
cli: { name: "CLI", image: "SSH.png", component: GatewayCliDeploymentMethod }
} as const;
export type GatewayDeploymentMethod = keyof typeof GatewayDeploymentInfoMap;
const Content = () => {
const [selectedMethod, setSelectedMethod] = useState<null | GatewayDeploymentMethod>(null);
if (selectedMethod) {
const ComponentToRender = GatewayDeploymentInfoMap[selectedMethod]?.component;
if (ComponentToRender) {
return <ComponentToRender />;
}
}
return <GatewayDeploymentMethodSelect onSelect={setSelectedMethod} />;
};
export const GatewayDeployModal = ({ isOpen, onOpenChange }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
className="max-w-2xl"
title="Deploy Gateway"
subTitle="Select a deployment method to use for the gateway."
bodyClassName="overflow-visible"
>
<Content />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,54 @@
import { useMemo } from "react";
import {
GatewayDeploymentInfoMap,
GatewayDeploymentMethod
} from "@app/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeployModal";
type Props = {
onSelect: (method: GatewayDeploymentMethod) => void;
};
export const GatewayDeploymentMethodSelect = ({ onSelect }: Props) => {
const deploymentOptions = useMemo(
() =>
(Object.keys(GatewayDeploymentInfoMap) as GatewayDeploymentMethod[]).map((method) => ({
method,
name: GatewayDeploymentInfoMap[method].name,
image: GatewayDeploymentInfoMap[method].image
})),
[]
);
const handleResourceSelect = (method: GatewayDeploymentMethod) => {
onSelect(method);
};
return (
<div className="grid h-fit grid-cols-4 content-start gap-2">
{deploymentOptions.map((option) => {
const { image, name, method } = option;
return (
<button
key={method}
type="button"
onClick={() => handleResourceSelect(method)}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<div className="relative">
<img
src={`/images/integrations/${image}`}
className="mt-auto w-12"
alt={`${name} logo`}
/>
</div>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
</div>
);
};
@@ -0,0 +1,32 @@
import { components, OptionProps } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export const RelayOption = ({
isSelected,
children,
...props
}: OptionProps<{ id: string; name: string }>) => {
const isCreateOption = props.data.id === "_create";
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
{isCreateOption ? (
<div className="flex items-center gap-x-1 text-mineshaft-400">
<FontAwesomeIcon icon={faPlus} size="sm" />
<span className="mr-auto">Deploy New Relay</span>
</div>
) : (
<>
<p className="truncate">{children}</p>
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</>
)}
</div>
</components.Option>
);
};
@@ -1,15 +1,20 @@
import { useState } from "react"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { GatewayTab } from "../GatewayTab/GatewayTab"; import { GatewayTab } from "../GatewayTab/GatewayTab";
import { RelayTab } from "../RelayTab/RelayTab"; import { RelayTab } from "../RelayTab/RelayTab";
export const NetworkingTabGroup = () => { export const NetworkingTabGroup = () => {
const search = useSearch({ const navigate = useNavigate({
from: "/_authenticate/_inject-org-details/_org-layout/organization/networking/" from: ROUTE_PATHS.Organization.NetworkingPage.path
});
const selectedTab = useSearch({
from: ROUTE_PATHS.Organization.NetworkingPage.id,
select: (el) => el.selectedTab,
structuralSharing: true
}); });
const tabs = [ const tabs = [
@@ -17,12 +22,16 @@ export const NetworkingTabGroup = () => {
{ name: "Relays", key: "relays", component: RelayTab } { name: "Relays", key: "relays", component: RelayTab }
]; ];
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key); const handleTabChange = (tab: string) => {
navigate({
search: { selectedTab: tab }
});
};
const { isSubOrganization } = useOrganization(); const { isSubOrganization } = useOrganization();
return ( return (
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={handleTabChange}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab variant={isSubOrganization ? "namespace" : "org"} value={tab.key} key={tab.key}> <Tab variant={isSubOrganization ? "namespace" : "org"} value={tab.key} key={tab.key}>
@@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { import {
faArrowUpRightFromSquare, faArrowUpRightFromSquare,
faBookOpen, faBookOpen,
@@ -7,15 +7,18 @@ import {
faEllipsisV, faEllipsisV,
faInfoCircle, faInfoCircle,
faMagnifyingGlass, faMagnifyingGlass,
faPlus,
faSearch, faSearch,
faTrash faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { formatRelative } from "date-fns"; import { formatRelative } from "date-fns";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button,
DeleteActionModal, DeleteActionModal,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -34,6 +37,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { import {
OrgPermissionSubjects, OrgPermissionSubjects,
OrgRelayPermissionActions OrgRelayPermissionActions
@@ -42,6 +46,8 @@ import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays"; import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays";
import { RelayDeployModal } from "./components/RelayDeployModal";
const RelayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { const RelayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => {
const heartbeatDate = heartbeat ? new Date(heartbeat) : null; const heartbeatDate = heartbeat ? new Date(heartbeat) : null;
const now = new Date(); const now = new Date();
@@ -66,7 +72,29 @@ export const RelayTab = withPermission(
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const { data: relays, isPending: isRelaysLoading } = useGetRelays(); const { data: relays, isPending: isRelaysLoading } = useGetRelays();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteRelay"] as const); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"deleteRelay",
"deployRelay"
] as const);
const action = useSearch({
from: ROUTE_PATHS.Organization.NetworkingPage.id,
select: (s) => s.action
});
const navigate = useNavigate({
from: ROUTE_PATHS.Organization.NetworkingPage.path
});
useEffect(() => {
if (action === "deploy-relay") {
handlePopUpOpen("deployRelay");
navigate({
search: (prev) => ({ ...prev, action: undefined }),
replace: true
});
}
}, [action, handlePopUpOpen, navigate]);
const deleteRelayById = useDeleteRelayById(); const deleteRelayById = useDeleteRelayById();
@@ -87,8 +115,8 @@ export const RelayTab = withPermission(
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex grow items-center gap-2">
<h3 className="text-lg font-medium text-mineshaft-100">Relays</h3> <h3 className="text-lg font-medium text-mineshaft-100">Relays</h3>
<a <a
href="https://infisical.com/docs/documentation/platform/gateways/relay-deployment" href="https://infisical.com/docs/documentation/platform/gateways/relay-deployment"
@@ -104,6 +132,14 @@ export const RelayTab = withPermission(
/> />
</div> </div>
</a> </a>
<div className="flex grow" />
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("deployRelay")}
>
Deploy Relay
</Button>
</div> </div>
</div> </div>
<p className="mb-4 text-sm text-mineshaft-400"> <p className="mb-4 text-sm text-mineshaft-400">
@@ -222,6 +258,10 @@ export const RelayTab = withPermission(
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => handleDeleteRelay()} onDeleteApproved={() => handleDeleteRelay()}
/> />
<RelayDeployModal
isOpen={popUp.deployRelay.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deployRelay", isOpen)}
/>
</TableContainer> </TableContainer>
</div> </div>
</div> </div>
@@ -0,0 +1,352 @@
import { useMemo, useState } from "react";
import { SingleValue } from "react-select";
import { faCopy, faQuestionCircle, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FilterableSelect,
FormLabel,
IconButton,
Input,
ModalClose,
Tooltip
} from "@app/components/v2";
import {
OrgPermissionIdentityActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import {
useAddIdentityTokenAuth,
useCreateTokenIdentityTokenAuth,
useGetIdentityMembershipOrgs,
useGetIdentityTokenAuth
} from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
const baseFormSchema = z.object({
name: slugSchema({ field: "name" }),
host: z.string().min(1, "Host is required"),
instanceDomain: z.string().url("Must be a valid URL").or(z.literal(""))
});
const formSchemaWithIdentity = baseFormSchema.extend({
identity: z
.object(
{
id: z.string(),
name: z.string()
},
{ required_error: "Identity is required" }
)
.nullable()
.refine((val) => val !== null, { message: "Identity is required" })
});
const formSchemaWithToken = baseFormSchema.extend({
identityToken: z.string().min(1, "Token is required")
});
export const RelayCliDeploymentMethod = () => {
const { protocol, hostname, port } = window.location;
const portSuffix = port && port !== "80" ? `:${port}` : "";
const siteURL = `${protocol}//${hostname}${portSuffix}`;
const [autogenerateToken, setAutogenerateToken] = useState(true);
const [step, setStep] = useState<"form" | "command">("form");
const [name, setName] = useState("");
const [host, setHost] = useState("");
const [instanceDomain, setInstanceDomain] = useState(siteURL);
const [identity, setIdentity] = useState<null | {
id: string;
name: string;
}>(null);
const [identityToken, setIdentityToken] = useState("");
const [formErrors, setFormErrors] = useState<z.ZodIssue[]>([]);
const errors = useMemo(() => {
const errorMap: Record<string, string | undefined> = {};
formErrors.forEach((issue) => {
if (issue.path.length > 0) {
errorMap[String(issue.path[0])] = issue.message;
}
});
return errorMap;
}, [formErrors]);
const { currentOrg } = useOrganization();
const organizationId = currentOrg?.id || "";
const { permission } = useOrgPermission();
const canCreateToken = permission.can(
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity
);
const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } =
useGetIdentityMembershipOrgs({
organizationId,
limit: 20000
});
const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || [];
const { mutateAsync: createToken, isPending: isCreatingToken } =
useCreateTokenIdentityTokenAuth();
const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } =
useAddIdentityTokenAuth();
const { refetch } = useGetIdentityTokenAuth(identity?.id ?? "");
const handleGenerateCommand = async () => {
setFormErrors([]);
if (canCreateToken && autogenerateToken) {
const validation = formSchemaWithIdentity.safeParse({ name, host, instanceDomain, identity });
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
const validatedIdentity = validation.data.identity;
try {
const { data: identityTokenAuth } = await refetch();
if (!identityTokenAuth) {
await addIdentityTokenAuth({
identityId: validatedIdentity.id,
organizationId,
accessTokenTTL: 2592000,
accessTokenMaxTTL: 2592000,
accessTokenNumUsesLimit: 0,
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
createNotification({
text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.",
type: "warning"
});
}
const token = await createToken({
identityId: validatedIdentity.id,
name: `relay token for ${name} (autogenerated)`
});
setIdentityToken(token.accessToken);
createNotification({
text: "Automatically generated a token for the selected identity.",
type: "info"
});
setStep("command");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to generate token for the selected identity",
type: "error"
});
setIdentityToken("");
}
} else {
const validation = formSchemaWithToken.safeParse({
name,
host,
instanceDomain,
identityToken
});
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
setStep("command");
}
};
const handleIdentityChange = (
selectedIdentity: SingleValue<{
id: string;
name: string;
}>
) => {
setIdentity(selectedIdentity);
};
const command = useMemo(() => {
const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : "";
return `infisical relay start --name=${name}${domainFlag} --host=${host} --token=${identityToken}`;
}, [name, instanceDomain, host, identityToken]);
if (step === "command") {
return (
<>
<FormLabel label="CLI Command" />
<div className="flex gap-2">
<Input value={command} isDisabled />
<IconButton
ariaLabel="copy"
variant="outline_bg"
colorSchema="secondary"
onClick={() => {
navigator.clipboard.writeText(command);
createNotification({
text: "Command copied to clipboard",
type: "info"
});
}}
className="w-10"
>
<FontAwesomeIcon icon={faCopy} />
</IconButton>
</div>
<a
href="https://infisical.com/docs/cli/overview"
target="_blank"
className="mt-2 flex h-4 w-fit items-center gap-2 border-b border-mineshaft-400 text-sm text-mineshaft-400 transition-colors duration-100 hover:border-yellow-400 hover:text-yellow-400"
rel="noreferrer"
>
<span>Install the Infisical CLI</span>
<FontAwesomeIcon icon={faUpRightFromSquare} className="size-3" />
</a>
<div className="mt-6 flex items-center">
<ModalClose asChild>
<Button className="mr-4" size="sm" colorSchema="secondary">
Done
</Button>
</ModalClose>
</div>
</>
);
}
return (
<>
<FormLabel label="Name" tooltipText="The name for your relay." />
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter relay name..."
isError={Boolean(errors.name)}
/>
{errors.name && <p className="mt-1 text-sm text-red">{errors.name}</p>}
<FormLabel
label="Host"
tooltipText="The public IP address of the system you're deploying the relay to."
className="mt-4"
/>
<Input
value={host}
onChange={(e) => setHost(e.target.value)}
placeholder="0.0.0.0"
isError={Boolean(errors.host)}
/>
{errors.host && <p className="mt-1 text-sm text-red">{errors.host}</p>}
<FormLabel
label="Infisical Instance Host Address"
tooltipText="The host address of the infisical instance that's accessible by the relay."
className="mt-4"
/>
<Input
value={instanceDomain}
onChange={(e) => setInstanceDomain(e.target.value)}
placeholder="https://app.infisical.com"
isError={Boolean(errors.instanceDomain)}
/>
{errors.instanceDomain && <p className="mt-1 text-sm text-red">{errors.instanceDomain}</p>}
{canCreateToken && autogenerateToken ? (
<>
<FormLabel
label="Identity"
tooltipText="The identity that your relay will use for authentication."
className="mt-4"
/>
<FilterableSelect
value={identity}
onChange={(e) =>
handleIdentityChange(
e as SingleValue<{
id: string;
name: string;
}>
)
}
isLoading={isIdentitiesLoading}
placeholder="Select identity..."
options={identityMembershipOrgs.map((membership) => membership.identity)}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
{errors.identity && <p className="mt-1 text-sm text-red">{errors.identity}</p>}
</>
) : (
<>
<FormLabel
label="Identity Token"
tooltipText="The identity token that your relay will use for authentication."
className="mt-4"
/>
<Input
value={identityToken}
onChange={(e) => setIdentityToken(e.target.value)}
placeholder="Enter identity token..."
isError={Boolean(errors.identityToken)}
/>
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
</>
)}
{canCreateToken && (
<div className="mt-2">
<Checkbox
isChecked={autogenerateToken}
onCheckedChange={(e) => {
setAutogenerateToken(Boolean(e));
}}
id="autogenerate-token"
className="mr-2"
>
<div className="flex items-center">
<span>Automatically enable token auth and generate a token for identity</span>
<Tooltip
className="max-w-md"
content={
<>
Token authentication will be automatically enabled for the selected identity if
it isn&apos;t already configured. By default, it will be configured to allow all
IP addresses with a token TTL of 30 days. You can manage these settings in
Access Control.
<br />
<br />A token will automatically be generated to be used with the CLI command.
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="mt-0.5 ml-1" />
</Tooltip>
</div>
</Checkbox>
</div>
)}
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
colorSchema="secondary"
onClick={handleGenerateCommand}
isLoading={isCreatingToken || isAddingTokenAuth}
>
Continue
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</>
);
};
@@ -0,0 +1,45 @@
import { useState } from "react";
import { Modal, ModalContent } from "@app/components/v2";
import { RelayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect";
import { RelayCliDeploymentMethod } from "./RelayCliDeploymentMethod";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const RelayDeploymentInfoMap = {
cli: { name: "CLI", image: "SSH.png", component: RelayCliDeploymentMethod }
} as const;
export type RelayDeploymentMethod = keyof typeof RelayDeploymentInfoMap;
const Content = () => {
const [selectedMethod, setSelectedMethod] = useState<null | RelayDeploymentMethod>(null);
if (selectedMethod) {
const ComponentToRender = RelayDeploymentInfoMap[selectedMethod]?.component;
if (ComponentToRender) {
return <ComponentToRender />;
}
}
return <RelayDeploymentMethodSelect onSelect={setSelectedMethod} />;
};
export const RelayDeployModal = ({ isOpen, onOpenChange }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
className="max-w-2xl"
title="Deploy Relay"
subTitle="Select a deployment method to use for the relay."
bodyClassName="overflow-visible"
>
<Content />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,54 @@
import { useMemo } from "react";
import {
RelayDeploymentInfoMap,
RelayDeploymentMethod
} from "@app/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal";
type Props = {
onSelect: (method: RelayDeploymentMethod) => void;
};
export const RelayDeploymentMethodSelect = ({ onSelect }: Props) => {
const deploymentOptions = useMemo(
() =>
(Object.keys(RelayDeploymentInfoMap) as RelayDeploymentMethod[]).map((method) => ({
method,
name: RelayDeploymentInfoMap[method].name,
image: RelayDeploymentInfoMap[method].image
})),
[]
);
const handleResourceSelect = (method: RelayDeploymentMethod) => {
onSelect(method);
};
return (
<div className="grid h-fit grid-cols-4 content-start gap-2">
{deploymentOptions.map((option) => {
const { image, name, method } = option;
return (
<button
key={method}
type="button"
onClick={() => handleResourceSelect(method)}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<div className="relative">
<img
src={`/images/integrations/${image}`}
className="mt-auto w-12"
alt={`${name} logo`}
/>
</div>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
</div>
);
};
@@ -5,16 +5,17 @@ import { z } from "zod";
import { NetworkingPage } from "./NetworkingPage"; import { NetworkingPage } from "./NetworkingPage";
const NetworkingPageQueryParams = z.object({ const NetworkingPageQueryParams = z.object({
selectedTab: z.string().catch("") selectedTab: z.string().catch("gateways"),
action: z.string().optional()
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/networking/" "/_authenticate/_inject-org-details/_org-layout/organization/networking"
)({ )({
component: NetworkingPage, component: NetworkingPage,
validateSearch: zodValidator(NetworkingPageQueryParams), validateSearch: zodValidator(NetworkingPageQueryParams),
search: { search: {
middlewares: [stripSearchParams({ selectedTab: "" })] middlewares: [stripSearchParams({ selectedTab: "gateways" })]
}, },
context: () => ({ context: () => ({
breadcrumbs: [ breadcrumbs: [
@@ -19,12 +19,53 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props)
const isDurationValid = useMemo(() => duration && ms(duration || "1s") > 0, [duration]); const isDurationValid = useMemo(() => duration && ms(duration || "1s") > 0, [duration]);
const cliDuration = useMemo(() => {
if (!duration) return duration;
const unit = duration.replace(/[\d\s.-]/g, "");
const dayOrLargerUnits = [
"d",
"day",
"days",
"w",
"week",
"weeks",
"y",
"yr",
"yrs",
"year",
"years"
];
// ms library does not handle months (M) so we do it separately
if (unit === "M") {
const value = parseInt(duration, 10);
if (!Number.isNaN(value) && value > 0) {
const hours = value * 30 * 24;
return `${hours}h`;
}
} else if (dayOrLargerUnits.includes(unit.toLowerCase())) {
const valueInMs = ms(duration);
const oneHourInMs = 1000 * 60 * 60;
if (typeof valueInMs === "number" && valueInMs > 0) {
const hours = Math.floor(valueInMs / oneHourInMs);
return `${hours}h`;
}
}
return duration;
}, [duration]);
const command = useMemo( const command = useMemo(
() => () =>
account && account.resource.resourceType === PamResourceType.Postgres account &&
? `infisical pam db access-account ${account.id} --duration ${duration}` (account.resource.resourceType === PamResourceType.Postgres ||
account.resource.resourceType === PamResourceType.MySQL)
? `infisical pam db access-account ${account.id} --duration ${cliDuration}`
: "", : "",
[account, duration] [account, cliDuration]
); );
if (!account) return null; if (!account) return null;
@@ -48,7 +89,7 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props)
/> />
<FormLabel label="CLI Command" className="mt-4" /> <FormLabel label="CLI Command" className="mt-4" />
<div className="flex gap-2"> <div className="flex gap-2">
<Input value={command} isDisabled className="opacity-50" /> <Input value={command} isDisabled />
<IconButton <IconButton
ariaLabel="copy" ariaLabel="copy"
variant="outline_bg" variant="outline_bg"
@@ -0,0 +1,79 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2";
import { PamResourceType, TMySQLAccount } from "@app/hooks/api/pam";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields";
type Props = {
account?: TMySQLAccount;
resourceId?: string;
resourceType?: PamResourceType;
onSubmit: (formData: FormData) => Promise<void>;
};
const formSchema = genericAccountFieldsSchema.extend({
credentials: BaseSqlAccountSchema,
// We don't support rotation for now, just feed a false value to
// make the schema happy
rotationEnabled: z.boolean().default(false)
});
type FormData = z.infer<typeof formSchema>;
export const MySQLAccountForm = ({ account, onSubmit }: Props) => {
const isUpdate = Boolean(account);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: account
? {
...account,
credentials: {
...account.credentials,
password: UNCHANGED_PASSWORD_SENTINEL
}
}
: undefined
});
const {
handleSubmit,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form
onSubmit={(e) => {
handleSubmit(onSubmit)(e);
}}
>
<GenericAccountFields />
<SqlAccountFields isUpdate={isUpdate} />
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Account" : "Create Account"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -8,6 +8,7 @@ import {
import { DiscriminativePick } from "@app/types"; import { DiscriminativePick } from "@app/types";
import { PamAccountHeader } from "../PamAccountHeader"; import { PamAccountHeader } from "../PamAccountHeader";
import { MySQLAccountForm } from "./MySQLAccountForm";
import { PostgresAccountForm } from "./PostgresAccountForm"; import { PostgresAccountForm } from "./PostgresAccountForm";
type FormProps = { type FormProps = {
@@ -35,10 +36,7 @@ const CreateForm = ({
const createPamAccount = useCreatePamAccount(); const createPamAccount = useCreatePamAccount();
const onSubmit = async ( const onSubmit = async (
formData: DiscriminativePick< formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
TPamAccount,
"name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds"
>
) => { ) => {
try { try {
const account = await createPamAccount.mutateAsync({ const account = await createPamAccount.mutateAsync({
@@ -72,6 +70,10 @@ const CreateForm = ({
resourceType={resourceType} resourceType={resourceType}
/> />
); );
case PamResourceType.MySQL:
return (
<MySQLAccountForm onSubmit={onSubmit} resourceId={resourceId} resourceType={resourceType} />
);
default: default:
throw new Error(`Unhandled resource: ${resourceType}`); throw new Error(`Unhandled resource: ${resourceType}`);
} }
@@ -81,10 +83,7 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
const updatePamAccount = useUpdatePamAccount(); const updatePamAccount = useUpdatePamAccount();
const onSubmit = async ( const onSubmit = async (
formData: DiscriminativePick< formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
TPamAccount,
"name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds"
>
) => { ) => {
try { try {
const updatedAccount = await updatePamAccount.mutateAsync({ const updatedAccount = await updatePamAccount.mutateAsync({
@@ -110,6 +109,8 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
switch (account.resource.resourceType) { switch (account.resource.resourceType) {
case PamResourceType.Postgres: case PamResourceType.Postgres:
return <PostgresAccountForm account={account} onSubmit={onSubmit} />; return <PostgresAccountForm account={account} onSubmit={onSubmit} />;
case PamResourceType.MySQL:
return <MySQLAccountForm account={account} onSubmit={onSubmit} />;
default: default:
throw new Error(`Unhandled resource: ${account.resource.resourceType}`); throw new Error(`Unhandled resource: ${account.resource.resourceType}`);
} }
@@ -1,16 +1,21 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2"; import { Button, ModalClose } from "@app/components/v2";
import { PamResourceType, TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam"; import {
PamResourceType,
TPostgresAccount,
TPostgresResource,
useGetPamResourceById
} from "@app/hooks/api/pam";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants"; import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields";
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields"; import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields"; import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields";
type Props = { type Props = {
account?: TPostgresAccount; account?: TPostgresAccount;
@@ -56,7 +61,9 @@ export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmi
if (account) { if (account) {
setRotationCredentialsConfigured(account.resource.rotationCredentialsConfigured); setRotationCredentialsConfigured(account.resource.rotationCredentialsConfigured);
} else { } else {
setRotationCredentialsConfigured(!!resource?.rotationAccountCredentials); setRotationCredentialsConfigured(
!!(resource as TPostgresResource)?.rotationAccountCredentials
);
} }
}, [account, resource]); }, [account, resource]);
@@ -0,0 +1,84 @@
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2";
import { PamResourceType, TMySQLResource } from "@app/hooks/api/pam";
import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas";
import { SqlResourceFields } from "./shared/SqlResourceFields";
import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields";
type Props = {
resource?: TMySQLResource;
onSubmit: (formData: FormData) => Promise<void>;
};
const formSchema = genericResourceFieldsSchema.extend({
resourceType: z.literal(PamResourceType.MySQL),
connectionDetails: BaseSqlResourceSchema.extend({
database: z.string().trim().optional().default("")
})
});
type FormData = z.infer<typeof formSchema>;
export const MySQLResourceForm = ({ resource, onSubmit }: Props) => {
const isUpdate = Boolean(resource);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: resource ?? {
resourceType: PamResourceType.MySQL,
connectionDetails: {
host: "",
port: 3306,
database: "",
sslEnabled: true,
sslRejectUnauthorized: true,
sslCertificate: undefined
}
}
});
const {
handleSubmit,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form
onSubmit={(e) => {
setSelectedTabIndex(0);
handleSubmit(onSubmit)(e);
}}
>
<GenericResourceFields />
<SqlResourceFields
selectedTabIndex={selectedTabIndex}
setSelectedTabIndex={setSelectedTabIndex}
/>
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Details" : "Create Resource"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -9,6 +9,7 @@ import {
import { DiscriminativePick } from "@app/types"; import { DiscriminativePick } from "@app/types";
import { PamResourceHeader } from "../PamResourceHeader"; import { PamResourceHeader } from "../PamResourceHeader";
import { MySQLResourceForm } from "./MySQLResourceForm";
import { PostgresResourceForm } from "./PostgresResourceForm"; import { PostgresResourceForm } from "./PostgresResourceForm";
type FormProps = { type FormProps = {
@@ -57,6 +58,8 @@ const CreateForm = ({ resourceType, onComplete, projectId }: CreateFormProps) =>
switch (resourceType) { switch (resourceType) {
case PamResourceType.Postgres: case PamResourceType.Postgres:
return <PostgresResourceForm onSubmit={onSubmit} />; return <PostgresResourceForm onSubmit={onSubmit} />;
case PamResourceType.MySQL:
return <MySQLResourceForm onSubmit={onSubmit} />;
default: default:
throw new Error(`Unhandled resource: ${resourceType}`); throw new Error(`Unhandled resource: ${resourceType}`);
} }
@@ -92,8 +95,10 @@ const UpdateForm = ({ resource, onComplete }: UpdateFormProps) => {
switch (resource.resourceType) { switch (resource.resourceType) {
case PamResourceType.Postgres: case PamResourceType.Postgres:
return <PostgresResourceForm resource={resource} onSubmit={onSubmit} />; return <PostgresResourceForm resource={resource} onSubmit={onSubmit} />;
case PamResourceType.MySQL:
return <MySQLResourceForm resource={resource} onSubmit={onSubmit} />;
default: default:
throw new Error(`Unhandled resource: ${resource.resourceType}`); throw new Error(`Unhandled resource: ${(resource as any).resourceType}`);
} }
}; };
@@ -110,7 +110,7 @@ export const SqlResourceFields = ({ setSelectedTabIndex, selectedTabIndex }: Pro
errorText={error?.message} errorText={error?.message}
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
className={sslEnabled ? "" : "opacity-50"} className={sslEnabled ? "" : "opacity-50"}
label="SSL Certificate" label="Trusted CA SSL Certificate"
isOptional isOptional
> >
<TextArea className="h-14 resize-none!" {...field} isDisabled={!sslEnabled} /> <TextArea className="h-14 resize-none!" {...field} isDisabled={!sslEnabled} />
@@ -1172,7 +1172,9 @@ export const OverviewPage = () => {
handlePopUpClose("misc"); handlePopUpClose("misc");
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
});
}} }}
isDisabled={userAvailableDynamicSecretEnvs.length === 0} isDisabled={userAvailableDynamicSecretEnvs.length === 0}
variant="outline_bg" variant="outline_bg"
@@ -1679,10 +1681,11 @@ export const OverviewPage = () => {
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
text={ text={
subscription.slug === null subscription.slug === null
? "You can perform this action under an Enterprise license" ? "You can perform this action under an Enterprise license"
: "You can perform this action if you switch to Infisical's Team plan" : "You can perform this action if you switch to Infisical's Enterprise plan"
} }
/> />
)} )}
@@ -1019,7 +1019,9 @@ export const ActionBar = ({
handlePopUpClose("misc"); handlePopUpClose("misc");
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
});
}} }}
isDisabled={!isAllowed} isDisabled={!isAllowed}
variant="outline_bg" variant="outline_bg"
@@ -1274,10 +1276,13 @@ export const ActionBar = ({
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
text={ text={
subscription.slug === null subscription.slug === null
? "You can perform this action under an Enterprise license" ? "You can perform this action under an Enterprise license"
: "You can perform this action if you switch to Infisical's Team plan" : `You can perform this action if you switch to Infisical's ${
popUp.upgradePlan.data.isEnterpriseFeature ? "Enterprise" : "Pro"
} plan`
} }
/> />
)} )}
@@ -226,7 +226,7 @@ export const EnvironmentTabs = ({ secretPath }: Props) => {
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan." text="You can add custom environments if you switch to Infisical's Pro plan."
/> />
<AddEnvironmentModal <AddEnvironmentModal
isOpen={popUp.createEnvironment.isOpen} isOpen={popUp.createEnvironment.isOpen}
@@ -397,7 +397,7 @@ const Page = () => {
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add secret rotation if you switch to Infisical's Team plan." text="You can add secret rotation if you switch to Infisical's Pro plan."
/> />
<Modal <Modal
isOpen={popUp.secretRotationV2.isOpen} isOpen={popUp.secretRotationV2.isOpen}
@@ -123,7 +123,7 @@ export const EnvironmentSection = () => {
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan." text="You can add custom environments if you switch to Infisical's Pro plan."
/> />
</div> </div>
); );
+29 -74
View File
@@ -50,6 +50,7 @@ import { Route as adminCachingPageRouteImport } from './pages/admin/CachingPage/
import { Route as adminAuthenticationPageRouteImport } from './pages/admin/AuthenticationPage/route' import { Route as adminAuthenticationPageRouteImport } from './pages/admin/AuthenticationPage/route'
import { Route as adminAccessManagementPageRouteImport } from './pages/admin/AccessManagementPage/route' import { Route as adminAccessManagementPageRouteImport } from './pages/admin/AccessManagementPage/route'
import { Route as organizationProjectsPageRouteImport } from './pages/organization/ProjectsPage/route' import { Route as organizationProjectsPageRouteImport } from './pages/organization/ProjectsPage/route'
import { Route as organizationNetworkingPageRouteImport } from './pages/organization/NetworkingPage/route'
import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route' import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route'
import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route' import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route'
import { Route as organizationAccessManagementPageRouteImport } from './pages/organization/AccessManagementPage/route' import { Route as organizationAccessManagementPageRouteImport } from './pages/organization/AccessManagementPage/route'
@@ -62,7 +63,6 @@ import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages
import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route' import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route'
import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route' import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route'
import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route' import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route'
import { Route as organizationNetworkingPageRouteImport } from './pages/organization/NetworkingPage/route'
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route' import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
import { Route as sshLayoutImport } from './pages/ssh/layout' import { Route as sshLayoutImport } from './pages/ssh/layout'
import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout' import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout'
@@ -270,10 +270,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport =
createFileRoute( createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing', '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing',
)() )()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/networking',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport = const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport =
createFileRoute( createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections', '/_authenticate/_inject-org-details/_org-layout/organization/app-connections',
@@ -594,14 +590,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any) } as any)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport.update({
id: '/networking',
path: '/networking',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute = const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport.update({ AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport.update({
id: '/app-connections', id: '/app-connections',
@@ -658,6 +646,14 @@ const organizationProjectsPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any) } as any)
const organizationNetworkingPageRouteRoute =
organizationNetworkingPageRouteImport.update({
id: '/networking',
path: '/networking',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationBillingPageRouteRoute = const organizationBillingPageRouteRoute =
organizationBillingPageRouteImport.update({ organizationBillingPageRouteImport.update({
id: '/billing', id: '/billing',
@@ -799,14 +795,6 @@ const organizationSecretSharingPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute, AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute,
} as any) } as any)
const organizationNetworkingPageRouteRoute =
organizationNetworkingPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute,
} as any)
const organizationAppConnectionsAppConnectionsPageRouteRoute = const organizationAppConnectionsAppConnectionsPageRouteRoute =
organizationAppConnectionsAppConnectionsPageRouteImport.update({ organizationAppConnectionsAppConnectionsPageRouteImport.update({
id: '/', id: '/',
@@ -2476,6 +2464,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationBillingPageRouteImport preLoaderRoute: typeof organizationBillingPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
} }
'/_authenticate/_inject-org-details/_org-layout/organization/networking': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/networking'
path: '/networking'
fullPath: '/organization/networking'
preLoaderRoute: typeof organizationNetworkingPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/projects': { '/_authenticate/_inject-org-details/_org-layout/organization/projects': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/projects' id: '/_authenticate/_inject-org-details/_org-layout/organization/projects'
path: '/projects' path: '/projects'
@@ -2532,13 +2527,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
} }
'/_authenticate/_inject-org-details/_org-layout/organization/networking': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/networking'
path: '/networking'
fullPath: '/organization/networking'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': { '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing' id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
path: '/secret-sharing' path: '/secret-sharing'
@@ -2567,13 +2555,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationAppConnectionsAppConnectionsPageRouteImport preLoaderRoute: typeof organizationAppConnectionsAppConnectionsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
} }
'/_authenticate/_inject-org-details/_org-layout/organization/networking/': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/networking/'
path: '/'
fullPath: '/organization/networking/'
preLoaderRoute: typeof organizationNetworkingPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': { '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/' id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/'
path: '/' path: '/'
@@ -4001,20 +3982,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithCh
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren,
) )
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren {
organizationNetworkingPageRouteRoute: typeof organizationNetworkingPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren =
{
organizationNetworkingPageRouteRoute: organizationNetworkingPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren { interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren {
organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
} }
@@ -4051,9 +4018,9 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute
organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute
organizationNetworkingPageRouteRoute: typeof organizationNetworkingPageRouteRoute
organizationProjectsPageRouteRoute: typeof organizationProjectsPageRouteRoute organizationProjectsPageRouteRoute: typeof organizationProjectsPageRouteRoute
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute
@@ -4068,11 +4035,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
organizationAccessManagementPageRouteRoute, organizationAccessManagementPageRouteRoute,
organizationAuditLogsPageRouteRoute: organizationAuditLogsPageRouteRoute, organizationAuditLogsPageRouteRoute: organizationAuditLogsPageRouteRoute,
organizationBillingPageRouteRoute: organizationBillingPageRouteRoute, organizationBillingPageRouteRoute: organizationBillingPageRouteRoute,
organizationNetworkingPageRouteRoute: organizationNetworkingPageRouteRoute,
organizationProjectsPageRouteRoute: organizationProjectsPageRouteRoute, organizationProjectsPageRouteRoute: organizationProjectsPageRouteRoute,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute:
@@ -5055,6 +5021,7 @@ export interface FileRoutesByFullPath {
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute '/organization/access-management': typeof organizationAccessManagementPageRouteRoute
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/networking': typeof organizationNetworkingPageRouteRoute
'/organization/projects': typeof organizationProjectsPageRouteRoute '/organization/projects': typeof organizationProjectsPageRouteRoute
'/admin/access-management': typeof adminAccessManagementPageRouteRoute '/admin/access-management': typeof adminAccessManagementPageRouteRoute
'/admin/authentication': typeof adminAuthenticationPageRouteRoute '/admin/authentication': typeof adminAuthenticationPageRouteRoute
@@ -5063,12 +5030,10 @@ export interface FileRoutesByFullPath {
'/admin/environment': typeof adminEnvironmentPageRouteRoute '/admin/environment': typeof adminEnvironmentPageRouteRoute
'/admin/integrations': typeof adminIntegrationsPageRouteRoute '/admin/integrations': typeof adminIntegrationsPageRouteRoute
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren '/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/organization/networking': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren
'/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren '/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
'/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren '/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
'/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren '/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute '/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/networking/': typeof organizationNetworkingPageRouteRoute
'/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute '/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute
'/organization/settings/': typeof organizationSettingsPageRouteRoute '/organization/settings/': typeof organizationSettingsPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
@@ -5292,6 +5257,7 @@ export interface FileRoutesByTo {
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute '/organization/access-management': typeof organizationAccessManagementPageRouteRoute
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/networking': typeof organizationNetworkingPageRouteRoute
'/organization/projects': typeof organizationProjectsPageRouteRoute '/organization/projects': typeof organizationProjectsPageRouteRoute
'/admin/access-management': typeof adminAccessManagementPageRouteRoute '/admin/access-management': typeof adminAccessManagementPageRouteRoute
'/admin/authentication': typeof adminAuthenticationPageRouteRoute '/admin/authentication': typeof adminAuthenticationPageRouteRoute
@@ -5301,7 +5267,6 @@ export interface FileRoutesByTo {
'/admin/integrations': typeof adminIntegrationsPageRouteRoute '/admin/integrations': typeof adminIntegrationsPageRouteRoute
'/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren '/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute '/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/networking': typeof organizationNetworkingPageRouteRoute
'/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute '/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
'/organization/settings': typeof organizationSettingsPageRouteRoute '/organization/settings': typeof organizationSettingsPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
@@ -5527,6 +5492,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/organization/access-management': typeof organizationAccessManagementPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/access-management': typeof organizationAccessManagementPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/networking': typeof organizationNetworkingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/projects': typeof organizationProjectsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/projects': typeof organizationProjectsPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/access-management': typeof adminAccessManagementPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/access-management': typeof adminAccessManagementPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/authentication': typeof adminAuthenticationPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/authentication': typeof adminAuthenticationPageRouteRoute
@@ -5535,12 +5501,10 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/admin/_admin-layout/environment': typeof adminEnvironmentPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/environment': typeof adminEnvironmentPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/integrations': typeof adminIntegrationsPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/integrations': typeof adminIntegrationsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/networking': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/networking/': typeof organizationNetworkingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/settings/': typeof organizationSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/settings/': typeof organizationSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
@@ -5776,6 +5740,7 @@ export interface FileRouteTypes {
| '/organization/access-management' | '/organization/access-management'
| '/organization/audit-logs' | '/organization/audit-logs'
| '/organization/billing' | '/organization/billing'
| '/organization/networking'
| '/organization/projects' | '/organization/projects'
| '/admin/access-management' | '/admin/access-management'
| '/admin/authentication' | '/admin/authentication'
@@ -5784,12 +5749,10 @@ export interface FileRouteTypes {
| '/admin/environment' | '/admin/environment'
| '/admin/integrations' | '/admin/integrations'
| '/organization/app-connections' | '/organization/app-connections'
| '/organization/networking'
| '/organization/secret-sharing' | '/organization/secret-sharing'
| '/organization/settings' | '/organization/settings'
| '/secret-manager/$projectId' | '/secret-manager/$projectId'
| '/organization/app-connections/' | '/organization/app-connections/'
| '/organization/networking/'
| '/organization/secret-sharing/' | '/organization/secret-sharing/'
| '/organization/settings/' | '/organization/settings/'
| '/organization/groups/$groupId' | '/organization/groups/$groupId'
@@ -6012,6 +5975,7 @@ export interface FileRouteTypes {
| '/organization/access-management' | '/organization/access-management'
| '/organization/audit-logs' | '/organization/audit-logs'
| '/organization/billing' | '/organization/billing'
| '/organization/networking'
| '/organization/projects' | '/organization/projects'
| '/admin/access-management' | '/admin/access-management'
| '/admin/authentication' | '/admin/authentication'
@@ -6021,7 +5985,6 @@ export interface FileRouteTypes {
| '/admin/integrations' | '/admin/integrations'
| '/secret-manager/$projectId' | '/secret-manager/$projectId'
| '/organization/app-connections' | '/organization/app-connections'
| '/organization/networking'
| '/organization/secret-sharing' | '/organization/secret-sharing'
| '/organization/settings' | '/organization/settings'
| '/organization/groups/$groupId' | '/organization/groups/$groupId'
@@ -6245,6 +6208,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/organization/access-management' | '/_authenticate/_inject-org-details/_org-layout/organization/access-management'
| '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/organization/billing' | '/_authenticate/_inject-org-details/_org-layout/organization/billing'
| '/_authenticate/_inject-org-details/_org-layout/organization/networking'
| '/_authenticate/_inject-org-details/_org-layout/organization/projects' | '/_authenticate/_inject-org-details/_org-layout/organization/projects'
| '/_authenticate/_inject-org-details/admin/_admin-layout/access-management' | '/_authenticate/_inject-org-details/admin/_admin-layout/access-management'
| '/_authenticate/_inject-org-details/admin/_admin-layout/authentication' | '/_authenticate/_inject-org-details/admin/_admin-layout/authentication'
@@ -6253,12 +6217,10 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/admin/_admin-layout/environment' | '/_authenticate/_inject-org-details/admin/_admin-layout/environment'
| '/_authenticate/_inject-org-details/admin/_admin-layout/integrations' | '/_authenticate/_inject-org-details/admin/_admin-layout/integrations'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/organization/networking'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
| '/_authenticate/_inject-org-details/_org-layout/organization/settings' | '/_authenticate/_inject-org-details/_org-layout/organization/settings'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/'
| '/_authenticate/_inject-org-details/_org-layout/organization/networking/'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/'
| '/_authenticate/_inject-org-details/_org-layout/organization/settings/' | '/_authenticate/_inject-org-details/_org-layout/organization/settings/'
| '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
@@ -6701,9 +6663,9 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/access-management", "/_authenticate/_inject-org-details/_org-layout/organization/access-management",
"/_authenticate/_inject-org-details/_org-layout/organization/audit-logs", "/_authenticate/_inject-org-details/_org-layout/organization/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/organization/billing", "/_authenticate/_inject-org-details/_org-layout/organization/billing",
"/_authenticate/_inject-org-details/_org-layout/organization/networking",
"/_authenticate/_inject-org-details/_org-layout/organization/projects", "/_authenticate/_inject-org-details/_org-layout/organization/projects",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections", "/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
"/_authenticate/_inject-org-details/_org-layout/organization/networking",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/settings", "/_authenticate/_inject-org-details/_org-layout/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId",
@@ -6742,6 +6704,10 @@ export const routeTree = rootRoute
"filePath": "organization/BillingPage/route.tsx", "filePath": "organization/BillingPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization" "parent": "/_authenticate/_inject-org-details/_org-layout/organization"
}, },
"/_authenticate/_inject-org-details/_org-layout/organization/networking": {
"filePath": "organization/NetworkingPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/_org-layout/organization/projects": { "/_authenticate/_inject-org-details/_org-layout/organization/projects": {
"filePath": "organization/ProjectsPage/route.tsx", "filePath": "organization/ProjectsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization" "parent": "/_authenticate/_inject-org-details/_org-layout/organization"
@@ -6778,13 +6744,6 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
] ]
}, },
"/_authenticate/_inject-org-details/_org-layout/organization/networking": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
"children": [
"/_authenticate/_inject-org-details/_org-layout/organization/networking/"
]
},
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing": { "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing": {
"filePath": "", "filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization", "parent": "/_authenticate/_inject-org-details/_org-layout/organization",
@@ -6811,10 +6770,6 @@ export const routeTree = rootRoute
"filePath": "organization/AppConnections/AppConnectionsPage/route.tsx", "filePath": "organization/AppConnections/AppConnectionsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections" "parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections"
}, },
"/_authenticate/_inject-org-details/_org-layout/organization/networking/": {
"filePath": "organization/NetworkingPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/networking"
},
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/": { "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/": {
"filePath": "organization/SecretSharingPage/route.tsx", "filePath": "organization/SecretSharingPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing" "parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
+1 -1
View File
@@ -37,7 +37,7 @@ const organizationRoutes = route("/organization", [
"organization/AppConnections/OauthCallbackPage/route.tsx" "organization/AppConnections/OauthCallbackPage/route.tsx"
) )
]), ]),
route("/networking", [index("organization/NetworkingPage/route.tsx")]) route("/networking", "organization/NetworkingPage/route.tsx")
]); ]);
const secretManagerRoutes = route("/projects/secret-management/$projectId", [ const secretManagerRoutes = route("/projects/secret-management/$projectId", [
+2
View File
@@ -31,5 +31,7 @@
"include": [ "include": [
"./src/**/*.ts", "./src/**/*.ts",
"./src/**/*.tsx", "./src/**/*.tsx",
"./.storybook/**/*.ts",
"./.storybook/**/*.tsx"
], ],
} }
@@ -1,3 +1,8 @@
## 1.7.2 (October 20, 2025)
Changes:
* Updated the default `infisical.image.tag` value to `v0.151.0`.
* `autoDatabaseSchemaMigration` has been fully removed as all newer versions of Infisical automatically run migrations as apart of the startup process.
## 1.7.1 (October 10, 2025) ## 1.7.1 (October 10, 2025)
Changes: Changes:
@@ -9,4 +9,4 @@ dependencies:
repository: oci://registry-1.docker.io/bitnamicharts repository: oci://registry-1.docker.io/bitnamicharts
version: 18.14.1 version: 18.14.1
digest: sha256:57a18fb5258fc153d27b633f6570104c7628af651f08f3ae7e1cf8920c2c31fa digest: sha256:57a18fb5258fc153d27b633f6570104c7628af651f08f3ae7e1cf8920c2c31fa
generated: "2025-09-30T18:44:50.303037+04:00" generated: "2025-10-21T22:30:21.313884+04:00"
@@ -7,7 +7,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.7.1 version: 1.7.2
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
@@ -18,7 +18,6 @@ A helm chart to deploy Infisical
|-----|------|---------|-------------| |-----|------|---------|-------------|
| fullnameOverride | string | `""` | Overrides the full name of the release, affecting resource names | | fullnameOverride | string | `""` | Overrides the full name of the release, affecting resource names |
| infisical.affinity | object | `{}` | Node affinity settings for pod placement | | infisical.affinity | object | `{}` | Node affinity settings for pod placement |
| infisical.autoDatabaseSchemaMigration | bool | `true` | Automatically migrates new database schema when deploying |
| infisical.databaseSchemaMigrationJob.image.pullPolicy | string | `"IfNotPresent"` | Pulls image only if not present on the node | | infisical.databaseSchemaMigrationJob.image.pullPolicy | string | `"IfNotPresent"` | Pulls image only if not present on the node |
| infisical.databaseSchemaMigrationJob.image.repository | string | `"ghcr.io/groundnuty/k8s-wait-for"` | Image repository for migration wait job | | infisical.databaseSchemaMigrationJob.image.repository | string | `"ghcr.io/groundnuty/k8s-wait-for"` | Image repository for migration wait job |
| infisical.databaseSchemaMigrationJob.image.tag | string | `"no-root-v2.0"` | Image tag version | | infisical.databaseSchemaMigrationJob.image.tag | string | `"no-root-v2.0"` | Image tag version |
@@ -44,16 +44,6 @@ spec:
{{- if $infisicalValues.image.imagePullSecrets }} {{- if $infisicalValues.image.imagePullSecrets }}
imagePullSecrets: imagePullSecrets:
{{- toYaml $infisicalValues.image.imagePullSecrets | nindent 6 }} {{- toYaml $infisicalValues.image.imagePullSecrets | nindent 6 }}
{{- end }}
{{- if $infisicalValues.autoDatabaseSchemaMigration }}
serviceAccountName: {{ include "infisical.serviceAccountName" . }}
initContainers:
- name: "migration-init"
image: "{{ $infisicalValues.databaseSchemaMigrationJob.image.repository }}:{{ $infisicalValues.databaseSchemaMigrationJob.image.tag }}"
imagePullPolicy: {{ $infisicalValues.databaseSchemaMigrationJob.image.pullPolicy }}
args:
- "job"
- "{{ .Release.Name }}-schema-migration-{{ .Release.Revision }}"
{{- end }} {{- end }}
containers: containers:
- name: {{ template "infisical.name" . }}-{{ $infisicalValues.name }} - name: {{ template "infisical.name" . }}-{{ $infisicalValues.name }}
@@ -1,52 +0,0 @@
{{- $infisicalValues := .Values.infisical }}
{{- if $infisicalValues.autoDatabaseSchemaMigration }}
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ .Release.Name }}-schema-migration-{{ .Release.Revision }}"
labels:
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
backoffLimit: 10
template:
metadata:
name: "{{ .Release.Name }}-create-tables"
labels:
app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
serviceAccountName: {{ include "infisical.serviceAccountName" . }}
{{- if $infisicalValues.image.imagePullSecrets }}
imagePullSecrets:
{{- toYaml $infisicalValues.image.imagePullSecrets | nindent 6 }}
{{- end }}
restartPolicy: OnFailure
containers:
- name: infisical-schema-migration
image: "{{ $infisicalValues.image.repository }}:{{ $infisicalValues.image.tag }}"
command: ["npm", "run", "migration:latest"]
env:
{{- if .Values.postgresql.useExistingPostgresSecret.enabled }}
- name: DB_CONNECTION_URI
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.useExistingPostgresSecret.existingConnectionStringSecret.name }}
key: {{ .Values.postgresql.useExistingPostgresSecret.existingConnectionStringSecret.key }}
{{- end }}
{{- if .Values.postgresql.enabled }}
- name: DB_CONNECTION_URI
value: {{ include "infisical.postgresDBConnectionString" . }}
{{- end }}
envFrom:
- secretRef:
name: {{ $infisicalValues.kubeSecretRef }}
{{- with $infisicalValues.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with $infisicalValues.extraVolumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
@@ -10,9 +10,6 @@ infisical:
# -- Sets the name of the deployment within this chart # -- Sets the name of the deployment within this chart
name: infisical name: infisical
# -- Automatically migrates new database schema when deploying
autoDatabaseSchemaMigration: true
autoBootstrap: autoBootstrap:
# -- Enable auto-bootstrap of the Infisical instance # -- Enable auto-bootstrap of the Infisical instance
enabled: false enabled: false
@@ -68,7 +65,7 @@ infisical:
# -- Image repository for the Infisical service # -- Image repository for the Infisical service
repository: infisical/infisical repository: infisical/infisical
# -- Specific version tag of the Infisical image. View the latest version here https://hub.docker.com/r/infisical/infisical # -- Specific version tag of the Infisical image. View the latest version here https://hub.docker.com/r/infisical/infisical
tag: "v0.93.1-postgres" tag: "v0.151.0"
# -- Pulls image only if not already present on the node # -- Pulls image only if not already present on the node
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
# -- Secret references for pulling the image, if needed # -- Secret references for pulling the image, if needed
@@ -118,8 +115,7 @@ ingress:
# -- Custom annotations for ingress resource # -- Custom annotations for ingress resource
annotations: {} annotations: {}
# -- TLS settings for HTTPS access # -- TLS settings for HTTPS access
tls: tls: []
[]
# -- TLS secret name for HTTPS # -- TLS secret name for HTTPS
# - secretName: letsencrypt-prod # - secretName: letsencrypt-prod
# -- Domain name to associate with the TLS certificate # -- Domain name to associate with the TLS certificate