Compare commits

...

44 Commits

Author SHA1 Message Date
Daniel Hougaard
677ff62b5c fix(auth): cli auth bug 2025-05-16 17:22:18 +04:00
Daniel Hougaard
8cc2e08f24 fix(auth): cli auth bug 2025-05-16 16:58:01 +04:00
Maidul Islam
d90178f49a Merge pull request #3590 from Infisical/daniel/k8s-auth-gateway
feat(gateway): gateway support for identities
2025-05-16 00:10:16 -07:00
x032205
7074fdbac3 Merge pull request #3609 from Infisical/ENG-2736
feat(org-settings): Option to hide certain products from the sidebar
2025-05-15 23:24:14 -04:00
x032205
517c613d05 migration fix 2025-05-15 22:50:09 -04:00
x032205
ae8cf06ec6 greptile review fixes 2025-05-15 21:05:39 -04:00
x032205
818778ddc5 Update frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-15 21:01:46 -04:00
x032205
2e12d9a13c Update frontend/src/pages/organization/SettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-15 21:01:30 -04:00
x032205
e678c9d1cf remove comments 2025-05-15 20:49:01 -04:00
x032205
da0b07ce2a added the other two products and small UI tweaks 2025-05-15 20:45:32 -04:00
x032205
3306a9ca69 Merge pull request #3608 from Infisical/key-schema-tweak
allow underscores in key schema
2025-05-15 18:55:45 -04:00
Maidul Islam
e9af34a6ba Merge pull request #3607 from Infisical/key-schema-doc-tweaks
feat(docs): Key Schema Tweaks
2025-05-15 15:51:23 -07:00
x032205
3de8ed169f allow underscores in key schema 2025-05-15 18:49:30 -04:00
Scott Wilson
d1eb350bdd Merge pull request #3606 from Infisical/oidc-groups-claim-handle-string
improvement(oidc-group-membership-mapping): Update OIDC group claims to handle single group string
2025-05-15 14:47:46 -07:00
Scott Wilson
0c1ccf7c2e fix: update oidc group claims to handle single group string 2025-05-15 14:39:07 -07:00
x032205
d268f52a1c small ui tweak 2025-05-15 16:50:37 -04:00
x032205
c519cee5d1 frontend 2025-05-15 16:32:57 -04:00
Maidul Islam
b55a39dd24 Merge pull request #3604 from Infisical/misc/add-identity-support-for-audit-log-retention
misc: add identity support for audit log retention
2025-05-15 09:25:49 -07:00
Sheen
7b880f85cc misc: add identity support for audit log retention 2025-05-15 16:19:47 +00:00
x032205
c7dc595e1a doc overview update 2025-05-15 12:05:06 -04:00
x032205
6e494f198b Merge pull request #3603 from Infisical/fix-oci-machine-identity
fix oci machine identity
2025-05-15 11:42:58 -04:00
x032205
e1f3eaf1a0 Comment for regex 2025-05-15 11:41:00 -04:00
Daniel Hougaard
be26dc9872 requested changes 2025-05-15 16:55:36 +04:00
Daniel Hougaard
aaeb6e73fe requested changes 2025-05-15 16:06:20 +04:00
x032205
1e11702c58 remove unused import 2025-05-15 01:17:38 -04:00
x032205
3b81cdb16e fix oci machine identity 2025-05-15 01:12:33 -04:00
x032205
6584166815 Merge pull request #3598 from Infisical/ENG-2755
feat(secret-sync): Secret Key Schema
2025-05-14 23:57:18 -04:00
x032205
827cb35194 review fixes 2025-05-14 23:52:05 -04:00
Maidul Islam
89a6a0ba13 Merge pull request #3602 from Infisical/general-oidc-group-mapping-docs
docs(oidc-group-membership-mapping): Add general OIDC group membership mapping documentation
2025-05-14 16:25:26 -07:00
x032205
3f74d3a80d update import 2025-05-14 13:49:25 -04:00
x032205
4a44dc6119 format a frontend file 2025-05-14 13:45:45 -04:00
x032205
dd4bc4bc73 more doc tweaks 2025-05-14 13:43:23 -04:00
x032205
b0c4fddf86 review fixes 2025-05-14 11:23:12 -04:00
Daniel Hougaard
cd028ae133 Update 20250212191958_create-gateway.ts 2025-05-14 16:01:07 +04:00
Daniel Hougaard
63c71fabcd fix: migrate project gateway 2025-05-14 16:00:27 +04:00
Daniel Hougaard
e90166f1f0 Merge branch 'heads/main' into daniel/k8s-auth-gateway 2025-05-14 14:26:05 +04:00
x032205
cccd4ba9e5 doc changes and other tweaks 2025-05-14 01:32:09 -04:00
x032205
63f0f8e299 final release 2025-05-14 01:16:42 -04:00
x032205
bae62421ae with stripSchema and filterForSchema 2025-05-13 23:08:54 -04:00
Daniel Hougaard
8adf4787b9 Update 20250513081738_remove-gateway-project-link.ts 2025-05-13 15:31:13 +04:00
Daniel Hougaard
a12522db55 requested changes 2025-05-13 15:18:23 +04:00
Daniel Hougaard
49ab487dc2 Update organization-permissions.mdx 2025-05-13 15:04:21 +04:00
Daniel Hougaard
daf0731580 feat(gateways): decouple gateways from projects 2025-05-13 14:59:58 +04:00
Daniel Hougaard
fb2b64cb19 feat(identities/k8s): gateway support 2025-05-12 15:19:42 +04:00
95 changed files with 1393 additions and 680 deletions

View File

@@ -0,0 +1,25 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId");
if (!hasGatewayIdColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.uuid("gatewayId").nullable();
table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId");
if (hasGatewayIdColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.dropForeign("gatewayId");
table.dropColumn("gatewayId");
});
}
}

View File

@@ -0,0 +1,110 @@
import { Knex } from "knex";
import { inMemoryKeyStore } from "@app/keystore/memory";
import { selectAllTableCols } from "@app/lib/knex";
import { initLogger } from "@app/lib/logger";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TableName } from "../schemas";
import { getMigrationEnvConfig } from "./utils/env-config";
import { getMigrationEncryptionServices } from "./utils/services";
// Note(daniel): We aren't dropping tables or columns in this migrations so we can easily rollback if needed.
// In the future we need to drop the projectGatewayId on the dynamic secrets table, and drop the project_gateways table entirely.
const BATCH_SIZE = 500;
export async function up(knex: Knex): Promise<void> {
// eslint-disable-next-line no-param-reassign
knex.replicaNode = () => {
return knex;
};
if (!(await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId"))) {
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
table.uuid("gatewayId").nullable();
table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL");
table.index("gatewayId");
});
const existingDynamicSecretsWithProjectGatewayId = await knex(TableName.DynamicSecret)
.select(selectAllTableCols(TableName.DynamicSecret))
.whereNotNull(`${TableName.DynamicSecret}.projectGatewayId`)
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.id`, `${TableName.DynamicSecret}.projectGatewayId`)
.whereNotNull(`${TableName.ProjectGateway}.gatewayId`)
.select(
knex.ref("projectId").withSchema(TableName.ProjectGateway).as("projectId"),
knex.ref("gatewayId").withSchema(TableName.ProjectGateway).as("projectGatewayGatewayId")
);
initLogger();
const envConfig = getMigrationEnvConfig();
const keyStore = inMemoryKeyStore();
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
const updatedDynamicSecrets = await Promise.all(
existingDynamicSecretsWithProjectGatewayId.map(async (existingDynamicSecret) => {
if (!existingDynamicSecret.projectGatewayGatewayId) {
const result = {
...existingDynamicSecret,
gatewayId: null
};
const { projectId, projectGatewayGatewayId, ...rest } = result;
return rest;
}
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: existingDynamicSecret.projectId
});
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: existingDynamicSecret.projectId
});
let decryptedStoredInput = JSON.parse(
secretManagerDecryptor({ cipherTextBlob: Buffer.from(existingDynamicSecret.encryptedInput) }).toString()
) as object;
// We're not removing the existing projectGatewayId from the input so we can easily rollback without having to re-encrypt the input
decryptedStoredInput = {
...decryptedStoredInput,
gatewayId: existingDynamicSecret.projectGatewayGatewayId
};
const encryptedInput = secretManagerEncryptor({
plainText: Buffer.from(JSON.stringify(decryptedStoredInput))
}).cipherTextBlob;
const result = {
...existingDynamicSecret,
encryptedInput,
gatewayId: existingDynamicSecret.projectGatewayGatewayId
};
const { projectId, projectGatewayGatewayId, ...rest } = result;
return rest;
})
);
for (let i = 0; i < updatedDynamicSecrets.length; i += BATCH_SIZE) {
// eslint-disable-next-line no-await-in-loop
await knex(TableName.DynamicSecret)
.insert(updatedDynamicSecrets.slice(i, i + BATCH_SIZE))
.onConflict("id")
.merge();
}
}
}
export async function down(knex: Knex): Promise<void> {
// no re-encryption needed as we keep the old projectGatewayId in the input
if (await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId")) {
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
table.dropForeign("gatewayId");
table.dropColumn("gatewayId");
});
}
}

View File

@@ -0,0 +1,53 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const columns = await knex.table(TableName.Organization).columnInfo();
await knex.schema.alterTable(TableName.Organization, (t) => {
if (!columns.secretsProductEnabled) {
t.boolean("secretsProductEnabled").defaultTo(true);
}
if (!columns.pkiProductEnabled) {
t.boolean("pkiProductEnabled").defaultTo(true);
}
if (!columns.kmsProductEnabled) {
t.boolean("kmsProductEnabled").defaultTo(true);
}
if (!columns.sshProductEnabled) {
t.boolean("sshProductEnabled").defaultTo(true);
}
if (!columns.scannerProductEnabled) {
t.boolean("scannerProductEnabled").defaultTo(true);
}
if (!columns.shareSecretsProductEnabled) {
t.boolean("shareSecretsProductEnabled").defaultTo(true);
}
});
}
export async function down(knex: Knex): Promise<void> {
const columns = await knex.table(TableName.Organization).columnInfo();
await knex.schema.alterTable(TableName.Organization, (t) => {
if (columns.secretsProductEnabled) {
t.dropColumn("secretsProductEnabled");
}
if (columns.pkiProductEnabled) {
t.dropColumn("pkiProductEnabled");
}
if (columns.kmsProductEnabled) {
t.dropColumn("kmsProductEnabled");
}
if (columns.sshProductEnabled) {
t.dropColumn("sshProductEnabled");
}
if (columns.scannerProductEnabled) {
t.dropColumn("scannerProductEnabled");
}
if (columns.shareSecretsProductEnabled) {
t.dropColumn("shareSecretsProductEnabled");
}
});
}

View File

@@ -27,7 +27,8 @@ export const DynamicSecretsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
encryptedInput: zodBuffer,
projectGatewayId: z.string().uuid().nullable().optional()
projectGatewayId: z.string().uuid().nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
});
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;

View File

@@ -29,7 +29,8 @@ export const IdentityKubernetesAuthsSchema = z.object({
allowedNames: z.string(),
allowedAudience: z.string(),
encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional()
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
});
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;

View File

@@ -28,7 +28,13 @@ export const OrganizationsSchema = z.object({
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
bypassOrgAuthEnabled: z.boolean().default(false),
userTokenExpiration: z.string().nullable().optional()
userTokenExpiration: z.string().nullable().optional(),
secretsProductEnabled: z.boolean().default(true).nullable().optional(),
pkiProductEnabled: z.boolean().default(true).nullable().optional(),
kmsProductEnabled: z.boolean().default(true).nullable().optional(),
sshProductEnabled: z.boolean().default(true).nullable().optional(),
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional()
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;

View File

@@ -121,14 +121,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({
name: z.string(),
id: z.string()
}),
projects: z
.object({
name: z.string(),
id: z.string(),
slug: z.string()
})
.array()
})
}).array()
})
}
@@ -158,17 +151,15 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({
name: z.string(),
id: z.string()
}),
projectGatewayId: z.string()
})
}).array()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => {
const gateways = await server.services.gateway.getProjectGateways({
projectId: req.params.projectId,
projectPermission: req.permission
const gateways = await server.services.gateway.listGateways({
orgPermission: req.permission
});
return { gateways };
}
@@ -216,8 +207,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
id: z.string()
}),
body: z.object({
name: slugSchema({ field: "name" }).optional(),
projectIds: z.string().array().optional()
name: slugSchema({ field: "name" }).optional()
}),
response: {
200: z.object({
@@ -230,8 +220,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
const gateway = await server.services.gateway.updateGatewayById({
orgPermission: req.permission,
id: req.params.id,
name: req.body.name,
projectIds: req.body.projectIds
name: req.body.name
});
return { gateway };
}

View File

@@ -17,7 +17,8 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal";
import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import {
DynamicSecretStatus,
@@ -44,9 +45,9 @@ type TDynamicSecretServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
gatewayDAL: Pick<TGatewayDALFactory, "findOne" | "find">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
@@ -62,7 +63,7 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretQueueService,
projectDAL,
kmsService,
projectGatewayDAL,
gatewayDAL,
resourceMetadataDAL
}: TDynamicSecretServiceFactoryDep) => {
const create = async ({
@@ -117,15 +118,31 @@ export const dynamicSecretServiceFactory = ({
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
let selectedGatewayId: string | null = null;
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) {
const projectGatewayId = inputs.projectGatewayId as string;
if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
const gatewayId = inputs.gatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
if (!projectGateway)
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
if (!gateway) {
throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found`
message: `Gateway with ID ${gatewayId} not found`
});
selectedGatewayId = projectGateway.id;
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
}
const isConnected = await selectedProvider.validateConnection(provider.inputs);
@@ -146,7 +163,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL,
folderId: folder.id,
name,
projectGatewayId: selectedGatewayId
gatewayId: selectedGatewayId
},
tx
);
@@ -255,20 +272,30 @@ export const dynamicSecretServiceFactory = ({
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
let selectedGatewayId: string | null = null;
if (
updatedInput &&
typeof updatedInput === "object" &&
"projectGatewayId" in updatedInput &&
updatedInput?.projectGatewayId
) {
const projectGatewayId = updatedInput.projectGatewayId as string;
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
const gatewayId = updatedInput.gatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
if (!projectGateway)
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
if (!gateway) {
throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found`
message: `Gateway with ID ${gatewayId} not found`
});
selectedGatewayId = projectGateway.id;
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
}
const isConnected = await selectedProvider.validateConnection(newInput);
@@ -284,7 +311,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL,
name: newName ?? name,
status: null,
projectGatewayId: selectedGatewayId
gatewayId: selectedGatewayId
},
tx
);

View File

@@ -18,7 +18,7 @@ import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp";
type TBuildDynamicSecretProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
};
export const buildDynamicSecretProviders = ({

View File

@@ -137,7 +137,7 @@ export const DynamicSecretSqlDBSchema = z.object({
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
ca: z.string().optional(),
projectGatewayId: z.string().nullable().optional()
gatewayId: z.string().nullable().optional()
});
export const DynamicSecretCassandraSchema = z.object({

View File

@@ -112,14 +112,14 @@ const generateUsername = (provider: SqlProviders) => {
};
type TSqlDatabaseProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
};
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, {
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
});
@@ -168,7 +168,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
gatewayCallback: (host: string, port: number) => Promise<void>
) => {
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string);
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
await withGatewayProxy(
async (port) => {
@@ -202,7 +202,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy();
};
if (providerInputs.projectGatewayId) {
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
@@ -238,7 +238,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy();
}
};
if (providerInputs.projectGatewayId) {
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
@@ -265,7 +265,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy();
}
};
if (providerInputs.projectGatewayId) {
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
@@ -301,7 +301,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy();
}
};
if (providerInputs.projectGatewayId) {
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();

View File

@@ -1,37 +1,34 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import {
buildFindFilter,
ormify,
selectAllTableCols,
sqlNestRelationships,
TFindFilter,
TFindOpt
} from "@app/lib/knex";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
export const gatewayDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.Gateway);
const find = async (filter: TFindFilter<TGateways>, { offset, limit, sort, tx }: TFindOpt<TGateways> = {}) => {
const find = async (
filter: TFindFilter<TGateways> & { orgId?: string },
{ offset, limit, sort, tx }: TFindOpt<TGateways> = {}
) => {
try {
const query = (tx || db)(TableName.Gateway)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter))
.where(buildFindFilter(filter, TableName.Gateway, ["orgId"]))
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`)
.join(
TableName.IdentityOrgMembership,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.Gateway}.identityId`
)
.select(selectAllTableCols(TableName.Gateway))
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug"),
db.ref("id").withSchema(TableName.Project).as("projectId")
);
.select(db.ref("orgId").withSchema(TableName.IdentityOrgMembership).as("identityOrgId"))
.select(db.ref("name").withSchema(TableName.Identity).as("identityName"));
if (filter.orgId) {
void query.where(`${TableName.IdentityOrgMembership}.orgId`, filter.orgId);
}
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
@@ -39,48 +36,16 @@ export const gatewayDALFactory = (db: TDbClient) => {
}
const docs = await query;
return sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (data) => ({
...GatewaysSchema.parse(data),
identity: { id: data.identityId, name: data.identityName }
}),
childrenMapper: [
{
key: "projectId",
label: "projects" as const,
mapper: ({ projectId, projectName, projectSlug }) => ({
id: projectId,
name: projectName,
slug: projectSlug
})
}
]
});
return docs.map((el) => ({
...GatewaysSchema.parse(el),
orgId: el.identityOrgId as string, // todo(daniel): figure out why typescript is not inferring this as a string
identity: { id: el.identityId, name: el.identityName }
}));
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
}
};
const findByProjectId = async (projectId: string, tx?: Knex) => {
try {
const query = (tx || db)(TableName.Gateway)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
.select(selectAllTableCols(TableName.Gateway))
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId")
)
.where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId });
const docs = await query;
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
}
};
return { ...orm, find, findByProjectId };
return { ...orm, find };
};

View File

@@ -4,7 +4,6 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas";
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -27,17 +26,14 @@ import { TGatewayDALFactory } from "./gateway-dal";
import {
TExchangeAllocatedRelayAddressDTO,
TGetGatewayByIdDTO,
TGetProjectGatewayByIdDTO,
THeartBeatDTO,
TListGatewaysDTO,
TUpdateGatewayByIdDTO
} from "./gateway-types";
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
type TGatewayServiceFactoryDep = {
gatewayDAL: TGatewayDALFactory;
projectGatewayDAL: TProjectGatewayDALFactory;
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
@@ -57,8 +53,7 @@ export const gatewayServiceFactory = ({
kmsService,
permissionService,
orgGatewayConfigDAL,
keyStore,
projectGatewayDAL
keyStore
}: TGatewayServiceFactoryDep) => {
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
// if (!licenseService.onPremFeatures.gateway) {
@@ -526,7 +521,7 @@ export const gatewayServiceFactory = ({
return gateway;
};
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => {
const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
@@ -543,15 +538,6 @@ export const gatewayServiceFactory = ({
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
if (projectIds) {
await projectGatewayDAL.transaction(async (tx) => {
await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx);
await projectGatewayDAL.insertMany(
projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })),
tx
);
});
}
return gateway;
};
@@ -576,27 +562,7 @@ export const gatewayServiceFactory = ({
return gateway;
};
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
await permissionService.getProjectPermission({
projectId,
actor: projectPermission.type,
actorId: projectPermission.id,
actorOrgId: projectPermission.orgId,
actorAuthMethod: projectPermission.authMethod,
actionProjectType: ActionProjectType.Any
});
const gateways = await gatewayDAL.findByProjectId(projectId);
return gateways;
};
// this has no permission check and used for dynamic secrets directly
// assumes permission check is already done
const fnGetGatewayClientTls = async (projectGatewayId: string) => {
const projectGateway = await projectGatewayDAL.findById(projectGatewayId);
if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` });
const { gatewayId } = projectGateway;
const fnGetGatewayClientTlsByGatewayId = async (gatewayId: string) => {
const gateway = await gatewayDAL.findById(gatewayId);
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
@@ -645,8 +611,7 @@ export const gatewayServiceFactory = ({
getGatewayById,
updateGatewayById,
deleteGatewayById,
getProjectGateways,
fnGetGatewayClientTls,
fnGetGatewayClientTlsByGatewayId,
heartbeat
};
};

View File

@@ -20,7 +20,6 @@ export type TGetGatewayByIdDTO = {
export type TUpdateGatewayByIdDTO = {
id: string;
name?: string;
projectIds?: string[];
orgPermission: OrgServiceActor;
};

View File

@@ -1,10 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectGatewayDALFactory = ReturnType<typeof projectGatewayDALFactory>;
export const projectGatewayDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.ProjectGateway);
return orm;
};

View File

@@ -714,13 +714,15 @@ export const oidcConfigServiceFactory = ({
}
}
const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined);
oidcLogin({
email: claims.email,
externalId: claims.sub,
firstName: claims.given_name ?? "",
lastName: claims.family_name ?? "",
orgId: org.id,
groups: claims.groups as string[] | undefined,
groups,
callbackPort,
manageGroupMemberships: oidcCfg.manageGroupMemberships
})

View File

@@ -41,7 +41,8 @@ export enum OrgPermissionGatewayActions {
CreateGateways = "create-gateways",
ListGateways = "list-gateways",
EditGateways = "edit-gateways",
DeleteGateways = "delete-gateways"
DeleteGateways = "delete-gateways",
AttachGateways = "attach-gateways"
}
export enum OrgPermissionIdentityActions {
@@ -337,6 +338,7 @@ const buildAdminPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
@@ -378,6 +380,7 @@ const buildMemberPermission = () => {
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
return rules;
};

View File

@@ -393,6 +393,7 @@ export const KUBERNETES_AUTH = {
allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.",
allowedAudience:
"The optional audience claim that the service account JWT token must have to authenticate with Infisical.",
gatewayId: "The ID of the gateway to use when performing kubernetes API requests.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
@@ -409,6 +410,7 @@ export const KUBERNETES_AUTH = {
allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.",
allowedAudience:
"The new optional audience claim that the service account JWT token must have to authenticate with Infisical.",
gatewayId: "The ID of the gateway to use when performing kubernetes API requests.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.",
@@ -2144,6 +2146,7 @@ export const SecretSyncs = {
const destinationName = SECRET_SYNC_NAME_MAP[destination];
return {
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`,
keySchema: `Specify the format to use for structuring secret keys in the ${destinationName} destination.`,
disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.`
};
},

View File

@@ -174,6 +174,8 @@ const setupProxyServer = async ({
return new Promise((resolve, reject) => {
const server = net.createServer();
let streamClosed = false;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on("connection", async (clientConn) => {
try {
@@ -202,9 +204,15 @@ const setupProxyServer = async ({
// Handle client connection close
clientConn.on("end", () => {
writer.close().catch((err) => {
logger.error(err);
});
if (!streamClosed) {
try {
writer.close().catch((err) => {
logger.debug(err, "Error closing writer (already closed)");
});
} catch (error) {
logger.debug(error, "Error in writer close");
}
}
});
clientConn.on("error", (clientConnErr) => {
@@ -249,14 +257,29 @@ const setupProxyServer = async ({
setupCopy();
// Handle connection closure
clientConn.on("close", () => {
stream.destroy().catch((err) => {
proxyErrorMsg.push((err as Error)?.message);
});
if (!streamClosed) {
streamClosed = true;
stream.destroy().catch((err) => {
logger.debug(err, "Stream already destroyed during close event");
});
}
});
const cleanup = async () => {
clientConn?.destroy();
await stream.destroy();
try {
clientConn?.destroy();
} catch (err) {
logger.debug(err, "Error destroying client connection");
}
if (!streamClosed) {
streamClosed = true;
try {
await stream.destroy();
} catch (err) {
logger.debug(err, "Error destroying stream (might be already closed)");
}
}
};
clientConn.on("error", (clientConnErr) => {
@@ -301,8 +324,17 @@ const setupProxyServer = async ({
server,
port: address.port,
cleanup: async () => {
server.close();
await quicClient?.destroy();
try {
server.close();
} catch (err) {
logger.debug(err, "Error closing server");
}
try {
await quicClient?.destroy();
} catch (err) {
logger.debug(err, "Error destroying QUIC client");
}
},
getProxyError: () => proxyErrorMsg.join(",")
});
@@ -320,10 +352,10 @@ interface ProxyOptions {
orgId: string;
}
export const withGatewayProxy = async (
callback: (port: number) => Promise<void>,
export const withGatewayProxy = async <T>(
callback: (port: number) => Promise<T>,
options: ProxyOptions
): Promise<void> => {
): Promise<T> => {
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
// Setup the proxy server
@@ -339,7 +371,7 @@ export const withGatewayProxy = async (
try {
// Execute the callback with the allocated port
await callback(port);
return await callback(port);
} catch (err) {
const proxyErrorMessage = getProxyError();
if (proxyErrorMessage) {

View File

@@ -32,13 +32,13 @@ export const buildFindFilter =
<R extends object = object>(
{ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>,
tableName?: TableName,
excludeKeys?: Array<keyof R>
excludeKeys?: string[]
) =>
(bd: Knex.QueryBuilder<R, R>) => {
const processedFilter = tableName
? Object.fromEntries(
Object.entries(filter)
.filter(([key]) => !excludeKeys || !excludeKeys.includes(key as keyof R))
.filter(([key]) => !excludeKeys || !excludeKeys.includes(key))
.map(([key, value]) => [`${tableName}.${key}`, value])
)
: filter;

View File

@@ -32,7 +32,6 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal";
import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
import { groupDALFactory } from "@app/ee/services/group/group-dal";
@@ -439,7 +438,6 @@ export const registerRoutes = async (
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
const gatewayDAL = gatewayDALFactory(db);
const projectGatewayDAL = projectGatewayDALFactory(db);
const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db);
const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
@@ -1422,12 +1420,24 @@ export const registerRoutes = async (
identityUaDAL,
licenseService
});
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore
});
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
identityKubernetesAuthDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
permissionService,
licenseService,
gatewayService,
gatewayDAL,
kmsService
});
const identityGcpAuthService = identityGcpAuthServiceFactory({
@@ -1490,16 +1500,6 @@ export const registerRoutes = async (
identityDAL
});
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore,
projectGatewayDAL
});
const dynamicSecretProviders = buildDynamicSecretProviders({
gatewayService
});
@@ -1521,7 +1521,7 @@ export const registerRoutes = async (
permissionService,
licenseService,
kmsService,
projectGatewayDAL,
gatewayDAL,
resourceMetadataDAL
});

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
import { IdentityKubernetesAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -21,7 +22,8 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick(
kubernetesHost: true,
allowedNamespaces: true,
allowedNames: true,
allowedAudience: true
allowedAudience: true,
gatewayId: true
}).extend({
caCert: z.string(),
tokenReviewerJwt: z.string().optional().nullable()
@@ -100,12 +102,30 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
}),
body: z
.object({
kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost),
kubernetesHost: z
.string()
.trim()
.min(1)
.describe(KUBERNETES_AUTH.ATTACH.kubernetesHost)
.refine(
(val) =>
characterValidator([
CharacterType.Alphabets,
CharacterType.Numbers,
CharacterType.Colon,
CharacterType.Period,
CharacterType.ForwardSlash
])(val),
{
message: "Kubernetes host must only contain alphabets, numbers, colons, periods, and forward slashes."
}
),
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.ATTACH.gatewayId),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
@@ -199,12 +219,34 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
}),
body: z
.object({
kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost),
kubernetesHost: z
.string()
.trim()
.min(1)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.kubernetesHost)
.refine(
(val) => {
if (!val) return true;
return characterValidator([
CharacterType.Alphabets,
CharacterType.Numbers,
CharacterType.Colon,
CharacterType.Period,
CharacterType.ForwardSlash
])(val);
},
{
message: "Kubernetes host must only contain alphabets, numbers, colons, periods, and forward slashes."
}
),
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),
gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.UPDATE.gatewayId),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()

View File

@@ -275,7 +275,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
},
{ message: "Duration value must be at least 1" }
)
.optional()
.optional(),
secretsProductEnabled: z.boolean().optional(),
pkiProductEnabled: z.boolean().optional(),
kmsProductEnabled: z.boolean().optional(),
sshProductEnabled: z.boolean().optional(),
scannerProductEnabled: z.boolean().optional(),
shareSecretsProductEnabled: z.boolean().optional()
}),
response: {
200: z.object({

View File

@@ -511,7 +511,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspace = await server.services.project.updateAuditLogsRetention({
actorId: req.permission.id,

View File

@@ -4,8 +4,14 @@ import https from "https";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
OrgPermissionGatewayActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
@@ -13,6 +19,7 @@ import {
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { withGatewayProxy } from "@app/lib/gateway";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -43,6 +50,8 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayService: TGatewayServiceFactory;
gatewayDAL: Pick<TGatewayDALFactory, "find">;
};
export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>;
@@ -53,8 +62,44 @@ export const identityKubernetesAuthServiceFactory = ({
identityAccessTokenDAL,
permissionService,
licenseService,
gatewayService,
gatewayDAL,
kmsService
}: TIdentityKubernetesAuthServiceFactoryDep) => {
const $gatewayProxyWrapper = async <T>(
inputs: {
gatewayId: string;
targetHost: string;
targetPort: number;
},
gatewayCallback: (host: string, port: number) => Promise<T>
): Promise<T> => {
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
const callbackResult = await withGatewayProxy(
async (port) => {
const res = await gatewayCallback("localhost", port);
return res;
},
{
targetHost: inputs.targetHost,
targetPort: inputs.targetPort,
relayHost,
relayPort: Number(relayPort),
identityId: relayDetails.identityId,
orgId: relayDetails.orgId,
tlsOptions: {
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey.toString()
}
}
);
return callbackResult;
};
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (!identityKubernetesAuth) {
@@ -92,46 +137,69 @@ export const identityKubernetesAuthServiceFactory = ({
tokenReviewerJwt = serviceAccountJwt;
}
const { data } = await axios
.post<TCreateTokenReviewResponse>(
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
// if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: !!caCert
})
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
const tokenReviewCallback = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => {
let baseUrl = `https://${host}`;
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
if (port) {
baseUrl += `:${port}`;
}
const res = await axios
.post<TCreateTokenReviewResponse>(
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
// if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: !!caCert
})
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
}
}
}
throw err;
});
throw err;
});
return res.data;
};
const [k8sHost, k8sPort] = identityKubernetesAuth.kubernetesHost.split(":");
const data = identityKubernetesAuth.gatewayId
? await $gatewayProxyWrapper(
{
gatewayId: identityKubernetesAuth.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort ? Number(k8sPort) : 443
},
tokenReviewCallback
)
: await tokenReviewCallback();
if ("error" in data.status)
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
@@ -222,6 +290,7 @@ export const identityKubernetesAuthServiceFactory = ({
const attachKubernetesAuth = async ({
identityId,
gatewayId,
kubernetesHost,
caCert,
tokenReviewerJwt,
@@ -280,6 +349,27 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
}
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identityMembershipOrg.orgId
@@ -296,6 +386,7 @@ export const identityKubernetesAuthServiceFactory = ({
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
gatewayId,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt
? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob
@@ -318,6 +409,7 @@ export const identityKubernetesAuthServiceFactory = ({
allowedNamespaces,
allowedNames,
allowedAudience,
gatewayId,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
@@ -373,11 +465,33 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
}
const updateQuery: TIdentityKubernetesAuthsUpdate = {
kubernetesHost,
allowedNamespaces,
allowedNames,
allowedAudience,
gatewayId,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,

View File

@@ -13,6 +13,7 @@ export type TAttachKubernetesAuthDTO = {
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
gatewayId?: string | null;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
@@ -28,6 +29,7 @@ export type TUpdateKubernetesAuthDTO = {
allowedNamespaces?: string;
allowedNames?: string;
allowedAudience?: string;
gatewayId?: string | null;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;

View File

@@ -17,7 +17,6 @@ import { request } from "@app/lib/config/request";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
@@ -59,9 +58,7 @@ export const identityOciAuthServiceFactory = ({
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityOciAuth.identityId });
await blockLocalAndPrivateIpAddresses(headers.host);
// Validate OCI host format
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
throw new BadRequestError({
message: "Invalid OCI host format. Expected format: identity.<region>.oraclecloud.com"

View File

@@ -18,5 +18,11 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
privilegeUpgradeInitiatedByUsername: true,
privilegeUpgradeInitiatedAt: true,
bypassOrgAuthEnabled: true,
userTokenExpiration: true
userTokenExpiration: true,
secretsProductEnabled: true,
pkiProductEnabled: true,
kmsProductEnabled: true,
sshProductEnabled: true,
scannerProductEnabled: true,
shareSecretsProductEnabled: true
});

View File

@@ -355,7 +355,13 @@ export const orgServiceFactory = ({
selectedMfaMethod,
allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled,
userTokenExpiration
userTokenExpiration,
secretsProductEnabled,
pkiProductEnabled,
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
shareSecretsProductEnabled
}
}: TUpdateOrgDTO) => {
const appCfg = getConfig();
@@ -457,7 +463,13 @@ export const orgServiceFactory = ({
selectedMfaMethod,
allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled,
userTokenExpiration
userTokenExpiration,
secretsProductEnabled,
pkiProductEnabled,
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
shareSecretsProductEnabled
});
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org;

View File

@@ -75,6 +75,12 @@ export type TUpdateOrgDTO = {
allowSecretSharingOutsideOrganization: boolean;
bypassOrgAuthEnabled: boolean;
userTokenExpiration: string;
secretsProductEnabled: boolean;
pkiProductEnabled: boolean;
kmsProductEnabled: boolean;
sshProductEnabled: boolean;
scannerProductEnabled: boolean;
shareSecretsProductEnabled: boolean;
}>;
} & TOrgPermission;

View File

@@ -2,6 +2,7 @@ import AWS, { AWSError } from "aws-sdk";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types";
@@ -389,6 +390,9 @@ export const AwsParameterStoreSyncFns = {
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
const [key, parameter] = entry;
// eslint-disable-next-line no-continue
if (!matchesSchema(key, syncOptions.keySchema)) continue;
if (!(key in secretMap) || !secretMap[key].value) {
parametersToDelete.push(parameter);
}

View File

@@ -27,6 +27,7 @@ import {
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-sync-types";
@@ -399,6 +400,9 @@ export const AwsSecretsManagerSyncFns = {
if (syncOptions.disableSecretDeletion) return;
for await (const secretKey of Object.keys(awsSecretsRecord)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secretKey, syncOptions.keySchema)) continue;
if (!(secretKey in secretMap) || !secretMap[secretKey].value) {
try {
await deleteSecret(client, secretKey);

View File

@@ -7,6 +7,7 @@ import { TAppConnectionDALFactory } from "@app/services/app-connection/app-conne
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
import { isAzureKeyVaultReference } from "@app/services/integration-auth/integration-sync-secret-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types";
@@ -139,6 +140,9 @@ export const azureAppConfigurationSyncFactory = ({
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const key of Object.keys(azureAppConfigSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
const azureSecret = azureAppConfigSecrets[key];
if (
!(key in secretMap) ||

View File

@@ -5,6 +5,7 @@ import { request } from "@app/lib/config/request";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { SecretSyncError } from "../secret-sync-errors";
@@ -192,7 +193,9 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const deleteSecretKey of deleteSecrets.filter(
(secret) => !setSecrets.find((setSecret) => setSecret.key === secret)
(secret) =>
matchesSchema(secret, secretSync.syncOptions.keySchema) &&
!setSecrets.find((setSecret) => setSecret.key === secret)
)) {
await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, {
headers: {

View File

@@ -12,6 +12,7 @@ import {
TCamundaSyncWithCredentials
} from "@app/services/secret-sync/camunda/camunda-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "../secret-sync-types";
@@ -116,6 +117,9 @@ export const camundaSyncFactory = ({ kmsService, appConnectionDAL }: TCamundaSec
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const secret of Object.keys(camundaSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secret, secretSync.syncOptions.keySchema)) continue;
if (!(secret in secretMap) || !secretMap[secret].value) {
try {
await deleteCamundaSecret({

View File

@@ -11,6 +11,7 @@ import {
TDatabricksSyncWithCredentials
} from "@app/services/secret-sync/databricks/databricks-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretMap } from "../secret-sync-types";
@@ -115,6 +116,9 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const secret of databricksSecretKeys) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secret.key, secretSync.syncOptions.keySchema)) continue;
if (!(secret.key in secretMap)) {
await deleteDatabricksSecrets({
key: secret.key,

View File

@@ -4,6 +4,7 @@ import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger";
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SecretSyncError } from "../secret-sync-errors";
import { TSecretMap } from "../secret-sync-types";
@@ -153,6 +154,9 @@ export const GcpSyncFns = {
}
for await (const key of Object.keys(gcpSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
try {
if (!(key in secretMap) || !secretMap[key].value) {
// eslint-disable-next-line no-continue

View File

@@ -4,6 +4,7 @@ import sodium from "libsodium-wrappers";
import { getGitHubClient } from "@app/services/app-connection/github";
import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
@@ -222,6 +223,9 @@ export const GithubSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const encryptedSecret of encryptedSecrets) {
// eslint-disable-next-line no-continue
if (!matchesSchema(encryptedSecret.name, secretSync.syncOptions.keySchema)) continue;
if (!(encryptedSecret.name in secretMap)) {
await deleteSecret(client, secretSync, encryptedSecret);
}

View File

@@ -11,6 +11,7 @@ import {
TPostHCVaultVariable
} from "@app/services/secret-sync/hc-vault/hc-vault-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => {
@@ -68,7 +69,7 @@ export const HCVaultSyncFns = {
const {
connection,
destinationConfig: { mount, path },
syncOptions: { disableSecretDeletion }
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const { namespace } = connection.credentials;
@@ -95,6 +96,9 @@ export const HCVaultSyncFns = {
if (disableSecretDeletion) return;
for await (const [key] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, keySchema)) continue;
if (!(key in secretMap)) {
delete variables[key];
tainted = true;

View File

@@ -2,6 +2,7 @@ import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
@@ -199,6 +200,9 @@ export const HumanitecSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const humanitecSecret of humanitecSecrets) {
// eslint-disable-next-line no-continue
if (!matchesSchema(humanitecSecret.key, secretSync.syncOptions.keySchema)) continue;
if (!secretMap[humanitecSecret.key]) {
await deleteSecret(secretSync, humanitecSecret);
}

View File

@@ -11,6 +11,7 @@ import {
TUpdateOCIVaultVariable
} from "@app/services/secret-sync/oci-vault/oci-vault-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
const listOCIVaultVariables = async ({ provider, compartmentId, vaultId, onlyActive }: TOCIVaultListVariables) => {
@@ -211,6 +212,9 @@ export const OCIVaultSyncFns = {
// Update and delete secrets
for await (const [key, variable] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
// Only update / delete active secrets
if (variable.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
if (key in secretMap && secretMap[key].value.length > 0) {

View File

@@ -1,4 +1,5 @@
import { AxiosError } from "axios";
import RE2 from "re2";
import {
AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
@@ -61,45 +62,63 @@ type TSyncSecretDeps = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
// const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
// let secretMap = { ...unprocessedSecretMap };
//
// const { appendSuffix, prependPrefix } = secretSync.syncOptions;
//
// if (appendSuffix || prependPrefix) {
// secretMap = {};
// Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
// secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value;
// });
// }
//
// return secretMap;
// };
//
// const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
// let secretMap = { ...unprocessedSecretMap };
//
// const { appendSuffix, prependPrefix } = secretSync.syncOptions;
//
// if (appendSuffix || prependPrefix) {
// secretMap = {};
// Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
// let processedKey = key;
//
// if (prependPrefix && processedKey.startsWith(prependPrefix)) {
// processedKey = processedKey.slice(prependPrefix.length);
// }
//
// if (appendSuffix && processedKey.endsWith(appendSuffix)) {
// processedKey = processedKey.slice(0, -appendSuffix.length);
// }
//
// secretMap[processedKey] = value;
// });
// }
//
// return secretMap;
// };
// Add schema to secret keys
const addSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
if (!schema) return unprocessedSecretMap;
const processedSecretMap: TSecretMap = {};
for (const [key, value] of Object.entries(unprocessedSecretMap)) {
const newKey = new RE2("{{secretKey}}").replace(schema, key);
processedSecretMap[newKey] = value;
}
return processedSecretMap;
};
// Strip schema from secret keys
const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
if (!schema) return unprocessedSecretMap;
const [prefix, suffix] = schema.split("{{secretKey}}");
const strippedMap: TSecretMap = {};
for (const [key, value] of Object.entries(unprocessedSecretMap)) {
if (!key.startsWith(prefix) || !key.endsWith(suffix)) {
// eslint-disable-next-line no-continue
continue;
}
const strippedKey = key.slice(prefix.length, key.length - suffix.length);
strippedMap[strippedKey] = value;
}
return strippedMap;
};
// Checks if a key matches a schema
export const matchesSchema = (key: string, schema?: string): boolean => {
if (!schema) return true;
const [prefix, suffix] = schema.split("{{secretKey}}");
if (prefix === undefined || suffix === undefined) return true;
return key.startsWith(prefix) && key.endsWith(suffix);
};
// Filter only for secrets with keys that match the schema
const filterForSchema = (secretMap: TSecretMap, schema?: string): TSecretMap => {
const filteredMap: TSecretMap = {};
for (const [key, value] of Object.entries(secretMap)) {
if (matchesSchema(key, schema)) {
filteredMap[key] = value;
}
}
return filteredMap;
};
export const SecretSyncFns = {
syncSecrets: (
@@ -107,51 +126,51 @@ export const SecretSyncFns = {
secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => {
// const affixedSecretMap = addAffixes(secretSync, secretMap);
const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
return AwsParameterStoreSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap);
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub:
return GithubSyncFns.syncSecrets(secretSync, secretMap);
return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GCPSecretManager:
return GcpSyncFns.syncSecrets(secretSync, secretMap);
return GcpSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault:
return azureKeyVaultSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
}).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureAppConfiguration:
return azureAppConfigurationSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
}).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Databricks:
return databricksSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
}).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Humanitec:
return HumanitecSyncFns.syncSecrets(secretSync, secretMap);
return HumanitecSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.TerraformCloud:
return TerraformCloudSyncFns.syncSecrets(secretSync, secretMap);
return TerraformCloudSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Camunda:
return camundaSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
}).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Vercel:
return VercelSyncFns.syncSecrets(secretSync, secretMap);
return VercelSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Windmill:
return WindmillSyncFns.syncSecrets(secretSync, secretMap);
return WindmillSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.HCVault:
return HCVaultSyncFns.syncSecrets(secretSync, secretMap);
return HCVaultSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.TeamCity:
return TeamCitySyncFns.syncSecrets(secretSync, secretMap);
return TeamCitySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault:
return OCIVaultSyncFns.syncSecrets(secretSync, secretMap);
return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -226,59 +245,58 @@ export const SecretSyncFns = {
);
}
return secretMap;
// return stripAffixes(secretSync, secretMap);
return stripSchema(filterForSchema(secretMap), secretSync.syncOptions.keySchema);
},
removeSecrets: (
secretSync: TSecretSyncWithCredentials,
secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => {
// const affixedSecretMap = addAffixes(secretSync, secretMap);
const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
return AwsParameterStoreSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap);
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub:
return GithubSyncFns.removeSecrets(secretSync, secretMap);
return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GCPSecretManager:
return GcpSyncFns.removeSecrets(secretSync, secretMap);
return GcpSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault:
return azureKeyVaultSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
}).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureAppConfiguration:
return azureAppConfigurationSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
}).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Databricks:
return databricksSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
}).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Humanitec:
return HumanitecSyncFns.removeSecrets(secretSync, secretMap);
return HumanitecSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.TerraformCloud:
return TerraformCloudSyncFns.removeSecrets(secretSync, secretMap);
return TerraformCloudSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Camunda:
return camundaSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
}).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Vercel:
return VercelSyncFns.removeSecrets(secretSync, secretMap);
return VercelSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Windmill:
return WindmillSyncFns.removeSecrets(secretSync, secretMap);
return WindmillSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.HCVault:
return HCVaultSyncFns.removeSecrets(secretSync, secretMap);
return HCVaultSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.TeamCity:
return TeamCitySyncFns.removeSecrets(secretSync, secretMap);
return TeamCitySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault:
return OCIVaultSyncFns.removeSecrets(secretSync, secretMap);
return OCIVaultSyncFns.removeSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`

View File

@@ -1,3 +1,4 @@
import RE2 from "re2";
import { AnyZodObject, z } from "zod";
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
@@ -24,6 +25,14 @@ const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
? z.nativeEnum(SecretSyncInitialSyncBehavior)
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior),
keySchema: z
.string()
.optional()
.refine((val) => !val || new RE2(/^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/).test(val), {
message:
"Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder."
})
.describe(SecretSyncs.SYNC_OPTIONS(destination).keySchema),
disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion)
});

View File

@@ -1,6 +1,7 @@
import { request } from "@app/lib/config/request";
import { getTeamCityInstanceUrl } from "@app/services/app-connection/teamcity";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import {
TDeleteTeamCityVariable,
@@ -125,6 +126,9 @@ export const TeamCitySyncFns = {
const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
for await (const [key, variable] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
if (!(key in secretMap)) {
try {
await deleteTeamCityVariable({

View File

@@ -4,6 +4,7 @@ import { AxiosResponse } from "axios";
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
@@ -231,6 +232,9 @@ export const TerraformCloudSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for (const terraformCloudVariable of terraformCloudVariables) {
// eslint-disable-next-line no-continue
if (!matchesSchema(terraformCloudVariable.key, secretSync.syncOptions.keySchema)) continue;
if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) {
await deleteVariable(secretSync, terraformCloudVariable);
}

View File

@@ -2,6 +2,7 @@
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { VercelEnvironmentType } from "./vercel-sync-enums";
@@ -290,6 +291,9 @@ export const VercelSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const vercelSecret of vercelSecrets) {
// eslint-disable-next-line no-continue
if (!matchesSchema(vercelSecret.key, secretSync.syncOptions.keySchema)) continue;
if (!secretMap[vercelSecret.key]) {
await deleteSecret(secretSync, vercelSecret);
}

View File

@@ -1,6 +1,7 @@
import { request } from "@app/lib/config/request";
import { getWindmillInstanceUrl } from "@app/services/app-connection/windmill";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import {
TDeleteWindmillVariable,
TPostWindmillVariable,
@@ -128,7 +129,7 @@ export const WindmillSyncFns = {
const {
connection,
destinationConfig: { path },
syncOptions: { disableSecretDeletion }
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
// url needs to be lowercase
@@ -169,6 +170,9 @@ export const WindmillSyncFns = {
if (disableSecretDeletion) return;
for await (const [key, variable] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, keySchema)) continue;
if (!(key in secretMap)) {
try {
await deleteWindmillVariable({

View File

@@ -158,14 +158,4 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
To confirm your Gateway is working, check the deployment status by looking for the message **"Gateway started successfully"** in the Gateway logs. This indicates the Gateway is running properly. Next, verify its registration by opening your Infisical dashboard, navigating to **Organization Access Control**, and selecting the **Gateways** tab. Your newly deployed Gateway should appear in the list.
![Gateway List](../../../images/platform/gateways/gateway-list.png)
</Step>
<Step title="Link Gateway to Projects">
To enable Infisical features like dynamic secrets or secret rotation to access private resources through the Gateway, you need to link the Gateway to the relevant projects.
Start by accessing the **Gateway settings** then locate the Gateway in the list, click the options menu (**:**), and select **Edit Details**.
![Edit Gateway Option](../../../images/platform/gateways/edit-gateway.png)
In the edit modal that appears, choose the projects you want the Gateway to access and click **Save** to confirm your selections.
![Project Assignment Modal](../../../images/platform/gateways/assign-project.png)
Once added to a project, the Gateway becomes available for use by any feature that supports Gateways within that project.
</Step>
</Steps>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 885 KiB

After

Width:  |  Height:  |  Size: 782 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 KiB

After

Width:  |  Height:  |  Size: 779 KiB

View File

@@ -22,11 +22,11 @@ description: "How to sync secrets from Infisical to Heroku"
</Step>
<Step title="Start integration">
Select which Infisical environment secrets you want to sync to which Heroku app and press create integration to start syncing secrets to Heroku.
![integrations heroku](../../images/integrations/heroku/integrations-heroku-create.png)
Here's some guidance on each field:
- Project Environment: The environment in the current Infisical project from which you want to sync secrets from.
- Secrets Path: The path in the current Infisical project from which you want to sync secrets from such as `/` (for secrets that do not reside in a folder) or `/foo/bar` (for secrets nested in a folder, in this case a folder called `bar` in another folder called `foo`).
- Heroku App: The application in Heroku that you want to sync secrets to.
@@ -34,7 +34,7 @@ description: "How to sync secrets from Infisical to Heroku"
- **No Import - Overwrite all values in Heroku**: Sync secrets and overwrite any existing secrets in Heroku.
- **Import - Prefer values from Infisical**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, do nothing. Afterwards, sync secrets to Heroku.
- **Import - Prefer values from Heroku**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, replace its value with the one from Heroku. Afterwards, sync secrets to Heroku.
![integrations heroku](../../images/integrations/heroku/integrations-heroku.png)
</Step>
</Steps>
@@ -46,27 +46,26 @@ description: "How to sync secrets from Infisical to Heroku"
<Step title="Create an API client in Heroku">
Navigate to your user Account settings > Applications to create a new API client.
![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-settings.png)
![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-applications.png)
![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app.png)
![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-settings.png)
![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-applications.png)
![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app.png)
Create the API client. As part of the form, set the **OAuth callback URL** to `https://your-domain.com/integrations/heroku/oauth2/callback`.
![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app-form.png)
![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app-form.png)
</Step>
<Step title="Add your Heroku API client credentials to Infisical">
Obtain the **Client ID** and **Client Secret** for your Heroku API client.
![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-credentials.png)
![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-credentials.png)
Back in your Infisical instance, add two new environment variables for the credentials of your Heroku API client.
- `CLIENT_ID_HEROKU`: The **Client ID** of your Heroku API client.
- `CLIENT_SECRET_HEROKU`: The **Client Secret** of your Heroku API client.
Once added, restart your Infisical instance and use the Heroku integration.
</Step>
</Steps>
</Tab>
</Tabs>

View File

@@ -40,6 +40,10 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict.
- **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with.
- **Tags**: Optional resource tags to add to parameters synced by Infisical.
- **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical.

View File

@@ -43,6 +43,10 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
- **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with.
- **Tags**: Optional tags to add to secrets synced by Infisical.
- **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical.

View File

@@ -48,7 +48,10 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
- **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.

View File

@@ -51,6 +51,10 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
- **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.

View File

@@ -39,6 +39,10 @@ description: "Learn how to configure a Camunda Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Camunda when keys conflict.
- **Import Secrets (Prioritize Camunda)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Camunda over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.

View File

@@ -46,6 +46,10 @@ description: "Learn how to configure a Databricks Sync for Infisical."
<Note>
Databricks does not support importing secrets.
</Note>
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.

View File

@@ -42,6 +42,10 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict.
- **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.

View File

@@ -62,6 +62,10 @@ description: "Learn how to configure a GitHub Sync for Infisical."
<Note>
GitHub does not support importing secrets.
</Note>
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.

View File

@@ -54,6 +54,10 @@ description: "Learn how to configure a Hashicorp Vault Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Hashicorp Vault when keys conflict.
- **Import Secrets (Prioritize Hashicorp Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Hashicorp Vault over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
</Step>

View File

@@ -55,6 +55,10 @@ description: "Learn how to configure a Humanitec Sync for Infisical."
<Note>
Humanitec does not support importing secrets.
</Note>
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.

View File

@@ -47,10 +47,13 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f
![Configure Sync Options](/images/secret-syncs/oci-vault/configure-sync-options.png)
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over OCI Vault when keys conflict.
- **Import Secrets (Prioritize OCI Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from OCI Vault over Infisical when keys conflict.
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over OCI Vault when keys conflict.
- **Import Secrets (Prioritize OCI Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from OCI Vault over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
</Step>

View File

@@ -93,4 +93,26 @@ via the UI or API for the third-party service you intend to sync secrets to.
<Note>
Infisical is continuously expanding it's Secret Sync third-party service support. If the service you need isn't available,
you can still use our Native Integrations in the interim, or contact us at team@infisical.com to make a request .
</Note>
</Note>
## Key Schemas
Key Schemas transform your secret keys by applying a prefix, suffix, or format pattern during sync to external destinations. This makes it clear which secrets are managed by Infisical and prevents accidental changes to unrelated secrets.
**Example:**
- Infisical key: `SECRET_1`
- Schema: `INFISICAL_{{secretKey}}`
- Synced key: `INFISICAL_SECRET_1`
<div align="center">
```mermaid
graph LR
A[Infisical: **SECRET_1**] -->|Apply Schema| B[Destination: **INFISICAL_SECRET_1**]
style B fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px
style A fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px
```
</div>
<Note>
When importing secrets from the destination into Infisical, the schema is stripped from imported secret keys.
</Note>

View File

@@ -48,7 +48,10 @@ description: "Learn how to configure a TeamCity Sync for Infisical."
<Note>
Infisical only syncs secrets from within the target scope; inherited secrets will not be imported.
</Note>
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.

View File

@@ -56,6 +56,10 @@ description: "Learn how to configure a Terraform Cloud Sync for Infisical."
<Note>
Terraform Cloud does not support importing secrets.
</Note>
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.

View File

@@ -43,6 +43,10 @@ description: "Learn how to configure a Vercel Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Vercel when keys conflict.
- **Import Secrets (Prioritize Vercel)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Vercel over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.

View File

@@ -44,6 +44,10 @@ description: "Learn how to configure a Windmill Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Windmill when keys conflict.
- **Import Secrets (Prioritize Windmill)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Windmill over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.

View File

@@ -218,3 +218,4 @@ Supports conditions and permission inversion
| `create-gateways` | Add new gateways to organization |
| `edit-gateways` | Modify existing gateway settings |
| `delete-gateways` | Remove gateways from organization |
| `attach-gateways` | Attach gateways to resources |

View File

@@ -44,7 +44,9 @@ const Content = ({ secretSync, onComplete }: ContentProps) => {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = useForm<TFormData>({ resolver: zodResolver(FormSchema) });
} = useForm<TFormData>({
resolver: zodResolver(FormSchema)
});
const triggerImportSecrets = useTriggerSecretSyncImportSecrets();

View File

@@ -1,9 +1,13 @@
import { ReactNode } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { faQuestionCircle, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import {
faCircleInfo,
faQuestionCircle,
faTriangleExclamation
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/components/v2";
import { FormControl, Input, Select, SelectItem, Switch, Tooltip } from "@app/components/v2";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
@@ -122,6 +126,45 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
)}
</>
)}
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
tooltipText="When a secret is synced, its key will be injected into the key schema before it reaches the destination. This is useful for organization."
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Key Schema"
helperText={
<Tooltip
className="max-w-md"
content={
<span>
We highly recommend using a{" "}
<a
href="https://infisical.com/docs/integrations/secret-syncs/overview#key-schemas"
target="_blank"
>
Key Schema
</a>{" "}
to ensure that Infisical only manages the specific keys you intend, keeping
everything else untouched.
</span>
}
>
<div>
<span>Infisical strongly advises setting a Key Schema</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<Input value={value} onChange={onChange} placeholder="INFISICAL_{{secretKey}}" />
</FormControl>
)}
control={control}
name="syncOptions.keySchema"
/>
{AdditionalSyncOptionsFieldsComponent}
<Controller
control={control}
@@ -161,34 +204,6 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
);
}}
/>
{/* <Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Prepend Prefix"
>
<Input className="uppercase" value={value} onChange={onChange} placeholder="INF_" />
</FormControl>
)}
control={control}
name="syncOptions.prependPrefix"
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Append Suffix"
>
<Input className="uppercase" value={value} onChange={onChange} placeholder="_INF" />
</FormControl>
)}
control={control}
name="syncOptions.appendSuffix"
/> */}
</>
);
};

View File

@@ -41,11 +41,7 @@ export const SecretSyncReviewFields = () => {
connection,
environment,
secretPath,
syncOptions: {
// appendSuffix, prependPrefix,
disableSecretDeletion,
initialSyncBehavior
},
syncOptions: { disableSecretDeletion, initialSyncBehavior, keySchema },
destination,
isAutoSyncEnabled
} = watch();
@@ -137,8 +133,7 @@ export const SecretSyncReviewFields = () => {
<GenericFieldLabel label="Initial Sync Behavior">
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destinationName).name}
</GenericFieldLabel>
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
<GenericFieldLabel label="Key Schema">{keySchema}</GenericFieldLabel>
{AdditionalSyncOptionsFieldsComponent}
{disableSecretDeletion && (
<GenericFieldLabel label="Secret Deletion">

View File

@@ -8,18 +8,18 @@ export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefi
) => {
const baseSyncOptionsSchema = z.object({
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior),
disableSecretDeletion: z.boolean().optional().default(false)
// scott: removed temporarily for evaluation of template formatting
// prependPrefix: z
// .string()
// .trim()
// .transform((str) => str.toUpperCase())
// .optional(),
// appendSuffix: z
// .string()
// .trim()
// .transform((str) => str.toUpperCase())
// .optional()
disableSecretDeletion: z.boolean().optional().default(false),
keySchema: z
.string()
.optional()
.refine(
(val) =>
!val || /^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/.test(val),
{
message:
"Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder."
}
)
});
const syncOptionsSchema = additionalSyncOptions

View File

@@ -12,7 +12,8 @@ export enum OrgGatewayPermissionActions {
CreateGateways = "create-gateways",
ListGateways = "list-gateways",
EditGateways = "edit-gateways",
DeleteGateways = "delete-gateways"
DeleteGateways = "delete-gateways",
AttachGateways = "attach-gateways"
}
export enum OrgPermissionSubjects {

View File

@@ -20,8 +20,8 @@ export const useDeleteGatewayById = () => {
export const useUpdateGatewayById = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, name, projectIds }: TUpdateGatewayDTO) => {
return apiRequest.patch(`/api/v1/gateways/${id}`, { name, projectIds });
mutationFn: ({ id, name }: TUpdateGatewayDTO) => {
return apiRequest.patch(`/api/v1/gateways/${id}`, { name });
},
onSuccess: () => {
queryClient.invalidateQueries(gatewaysQueryKeys.list());

View File

@@ -2,7 +2,7 @@ import { queryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TGateway, TListProjectGatewayDTO, TProjectGateway } from "./types";
import { TGateway } from "./types";
export const gatewaysQueryKeys = {
allKey: () => ["gateways"],
@@ -14,20 +14,5 @@ export const gatewaysQueryKeys = {
const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways");
return data.gateways;
}
}),
listProjectGatewayKey: ({ projectId }: TListProjectGatewayDTO) => [
...gatewaysQueryKeys.allKey(),
"list",
{ projectId }
],
listProjectGateways: ({ projectId }: TListProjectGatewayDTO) =>
queryOptions({
queryKey: gatewaysQueryKeys.listProjectGatewayKey({ projectId }),
queryFn: async () => {
const { data } = await apiRequest.get<{ gateways: TProjectGateway[] }>(
`/api/v1/gateways/projects/${projectId}`
);
return data.gateways;
}
})
};

View File

@@ -11,39 +11,13 @@ export type TGateway = {
name: string;
id: string;
};
projects: {
name: string;
id: string;
slug: string;
}[];
};
export type TProjectGateway = {
id: string;
identityId: string;
name: string;
createdAt: string;
updatedAt: string;
issuedAt: string;
serialNumber: string;
heartbeat: string;
projectGatewayId: string;
identity: {
name: string;
id: string;
};
};
export type TUpdateGatewayDTO = {
id: string;
name?: string;
projectIds?: string[];
};
export type TDeleteGatewayDTO = {
id: string;
};
export type TListProjectGatewayDTO = {
projectId: string;
};

View File

@@ -840,7 +840,8 @@ export const useAddIdentityKubernetesAuth = () => {
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
accessTokenTrustedIps,
gatewayId
}) => {
const {
data: { identityKubernetesAuth }
@@ -856,7 +857,8 @@ export const useAddIdentityKubernetesAuth = () => {
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
accessTokenTrustedIps,
gatewayId
}
);
@@ -945,7 +947,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
accessTokenTrustedIps,
gatewayId
}) => {
const {
data: { identityKubernetesAuth }
@@ -961,7 +964,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
accessTokenTrustedIps,
gatewayId
}
);

View File

@@ -388,6 +388,7 @@ export type IdentityKubernetesAuth = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: IdentityTrustedIp[];
gatewayId?: string | null;
};
export type AddIdentityKubernetesAuthDTO = {
@@ -398,6 +399,7 @@ export type AddIdentityKubernetesAuthDTO = {
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
gatewayId?: string | null;
caCert: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
@@ -415,6 +417,7 @@ export type UpdateIdentityKubernetesAuthDTO = {
allowedNamespaces?: string;
allowedNames?: string;
allowedAudience?: string;
gatewayId?: string | null;
caCert?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;

View File

@@ -112,7 +112,13 @@ export const useUpdateOrg = () => {
selectedMfaMethod,
allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled,
userTokenExpiration
userTokenExpiration,
secretsProductEnabled,
pkiProductEnabled,
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
shareSecretsProductEnabled
}) => {
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
name,
@@ -124,7 +130,13 @@ export const useUpdateOrg = () => {
selectedMfaMethod,
allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled,
userTokenExpiration
userTokenExpiration,
secretsProductEnabled,
pkiProductEnabled,
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
shareSecretsProductEnabled
});
},
onSuccess: () => {

View File

@@ -20,6 +20,12 @@ export type Organization = {
allowSecretSharingOutsideOrganization?: boolean;
userTokenExpiration?: string;
userRole: string;
secretsProductEnabled: boolean;
pkiProductEnabled: boolean;
kmsProductEnabled: boolean;
sshProductEnabled: boolean;
scannerProductEnabled: boolean;
shareSecretsProductEnabled: boolean;
};
export type UpdateOrgDTO = {
@@ -34,6 +40,12 @@ export type UpdateOrgDTO = {
allowSecretSharingOutsideOrganization?: boolean;
bypassOrgAuthEnabled?: boolean;
userTokenExpiration?: string;
secretsProductEnabled?: boolean;
pkiProductEnabled?: boolean;
kmsProductEnabled?: boolean;
sshProductEnabled?: boolean;
scannerProductEnabled?: boolean;
shareSecretsProductEnabled?: boolean;
};
export type BillingDetails = {

View File

@@ -4,8 +4,7 @@ import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/
export type RootSyncOptions = {
initialSyncBehavior: SecretSyncInitialSyncBehavior;
disableSecretDeletion?: boolean;
// prependPrefix?: string;
// appendSuffix?: string;
keySchema?: string;
};
export type TRootSecretSync = {

View File

@@ -268,77 +268,91 @@ export const MinimizedOrgSidebar = () => {
</DropdownMenu>
</div>
<div className="space-y-1">
<Link to="/organization/secret-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(
`/organization/${ProjectType.SecretManager}`
)
}
icon="sliding-carousel"
>
Secrets
</MenuIconButton>
)}
</Link>
<Link to="/organization/cert-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(
`/organization/${ProjectType.CertificateManager}`
)
}
icon="note"
>
PKI
</MenuIconButton>
)}
</Link>
<Link to="/organization/kms/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/organization/${ProjectType.KMS}`)
}
icon="unlock"
>
KMS
</MenuIconButton>
)}
</Link>
<Link to="/organization/ssh/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/organization/${ProjectType.SSH}`)
}
icon="verified"
>
SSH
</MenuIconButton>
)}
</Link>
<div className="w-full bg-mineshaft-500" style={{ height: "1px" }} />
<Link to="/organization/secret-scanning">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="secret-scan">
Scanner
</MenuIconButton>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="lock-closed">
Share
</MenuIconButton>
)}
</Link>
{currentOrg.secretsProductEnabled && (
<Link to="/organization/secret-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(
`/organization/${ProjectType.SecretManager}`
)
}
icon="sliding-carousel"
>
Secrets
</MenuIconButton>
)}
</Link>
)}
{currentOrg.pkiProductEnabled && (
<Link to="/organization/cert-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(
`/organization/${ProjectType.CertificateManager}`
)
}
icon="note"
>
PKI
</MenuIconButton>
)}
</Link>
)}
{currentOrg.kmsProductEnabled && (
<Link to="/organization/kms/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/organization/${ProjectType.KMS}`)
}
icon="unlock"
>
KMS
</MenuIconButton>
)}
</Link>
)}
{currentOrg.sshProductEnabled && (
<Link to="/organization/ssh/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/organization/${ProjectType.SSH}`)
}
icon="verified"
>
SSH
</MenuIconButton>
)}
</Link>
)}
{(currentOrg.scannerProductEnabled || currentOrg.shareSecretsProductEnabled) && (
<div className="w-full bg-mineshaft-500" style={{ height: "1px" }} />
)}
{currentOrg.scannerProductEnabled && (
<Link to="/organization/secret-scanning">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="secret-scan">
Scanner
</MenuIconButton>
)}
</Link>
)}
{currentOrg.shareSecretsProductEnabled && (
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="lock-closed">
Share
</MenuIconButton>
)}
</Link>
)}
<div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} />
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger

View File

@@ -10,7 +10,7 @@ import { useNavigateToSelectOrganization } from "./Login.utils";
export const LoginPage = ({ isAdmin }: { isAdmin?: boolean }) => {
const { t } = useTranslation();
const [step, setStep] = useState(0);
const [step, setStep] = useState<number | null>(null);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
@@ -36,6 +36,8 @@ export const LoginPage = ({ isAdmin }: { isAdmin?: boolean }) => {
if (isLoggedIn()) {
handleRedirects();
} else {
setStep(0);
}
}, []);

View File

@@ -3,22 +3,32 @@ import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tab,
TabList,
TabPanel,
Tabs,
TextArea
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import {
gatewaysQueryKeys,
useAddIdentityKubernetesAuth,
useGetIdentityKubernetesAuth,
useUpdateIdentityKubernetesAuth
@@ -32,6 +42,7 @@ const schema = z
.object({
kubernetesHost: z.string().min(1),
tokenReviewerJwt: z.string().optional(),
gatewayId: z.string().optional().nullable(),
allowedNames: z.string(),
allowedNamespaces: z.string(),
allowedAudience: z.string(),
@@ -79,6 +90,8 @@ export const IdentityKubernetesAuthForm = ({
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list());
const { data } = useGetIdentityKubernetesAuth(identityId ?? "", {
enabled: isUpdate
});
@@ -96,6 +109,7 @@ export const IdentityKubernetesAuthForm = ({
tokenReviewerJwt: "",
allowedNames: "",
allowedNamespaces: "",
gatewayId: "",
allowedAudience: "",
caCert: "",
accessTokenTTL: "2592000",
@@ -120,6 +134,7 @@ export const IdentityKubernetesAuthForm = ({
allowedNamespaces: data.allowedNamespaces,
allowedAudience: data.allowedAudience,
caCert: data.caCert,
gatewayId: data.gatewayId || null,
accessTokenTTL: String(data.accessTokenTTL),
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
@@ -157,6 +172,7 @@ export const IdentityKubernetesAuthForm = ({
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
gatewayId,
accessTokenTrustedIps
}: FormData) => {
try {
@@ -172,6 +188,7 @@ export const IdentityKubernetesAuthForm = ({
allowedAudience,
caCert,
identityId,
gatewayId: gatewayId || null,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@@ -186,6 +203,7 @@ export const IdentityKubernetesAuthForm = ({
allowedNames: allowedNames || "",
allowedNamespaces: allowedNamespaces || "",
allowedAudience: allowedAudience || "",
gatewayId: gatewayId || null,
caCert: caCert || "",
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
@@ -217,6 +235,7 @@ export const IdentityKubernetesAuthForm = ({
[
"kubernetesHost",
"tokenReviewerJwt",
"gatewayId",
"accessTokenTTL",
"accessTokenMaxTTL",
"accessTokenNumUsesLimit",
@@ -280,6 +299,62 @@ export const IdentityKubernetesAuthForm = ({
</FormControl>
)}
/>
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
isOptional
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={(v) => {
if (v !== "") {
onChange(v);
}
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewayLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(null)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
<Controller
control={control}
name="allowedNames"

View File

@@ -32,7 +32,6 @@ import {
Table,
TableContainer,
TableSkeleton,
Tag,
TBody,
Td,
Th,
@@ -128,7 +127,6 @@ export const GatewayListPage = withPermission(
<Tr>
<Th className="w-1/3">Name</Th>
<Th>Cert Issued At</Th>
<Th>Projects</Th>
<Th>Identity</Th>
<Th>
Health Check
@@ -151,13 +149,6 @@ export const GatewayListPage = withPermission(
<Tr key={el.id}>
<Td>{el.name}</Td>
<Td>{format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")}</Td>
<Td>
{el.projects.map((projectDetails) => (
<Tag key={projectDetails.id} size="xs">
{projectDetails.name}
</Tag>
))}
</Td>
<Td>{el.identity.name}</Td>
<Td>
{el.heartbeat

View File

@@ -3,10 +3,10 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
import { useGetUserWorkspaces, useUpdateGatewayById } from "@app/hooks/api";
import { Button, FormControl, Input } from "@app/components/v2";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { useUpdateGatewayById } from "@app/hooks/api";
import { TGateway } from "@app/hooks/api/gateways/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
type Props = {
gatewayDetails: TGateway;
@@ -14,13 +14,7 @@ type Props = {
};
const schema = z.object({
name: z.string(),
projects: z
.object({
id: z.string(),
name: z.string()
})
.array()
name: z.string()
});
export type FormData = z.infer<typeof schema>;
@@ -38,20 +32,13 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
});
const updateGatewayById = useUpdateGatewayById();
// when gateway goes to other products switch to all
const { data: secretManagerWorkspaces, isLoading: isSecretManagerLoading } = useGetUserWorkspaces(
{
type: ProjectType.SecretManager
}
);
const onFormSubmit = ({ name, projects }: FormData) => {
const onFormSubmit = ({ name }: FormData) => {
if (isSubmitting) return;
updateGatewayById.mutate(
{
id: gatewayDetails.id,
name,
projectIds: projects.map((el) => el.id)
name
},
{
onSuccess: () => {
@@ -67,6 +54,16 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<NoticeBannerV2 className="mx-auto mb-4" title="Project Linking">
<p className="mt-1 text-xs text-mineshaft-300">
Since the 15th May 2025, all gateways are automatically available for use in all projects
and you no longer need to link them.
<br />
Organization members with the &quot;Attach Gateways&quot; permission can use gateways
anywhere within the organization.
</p>
</NoticeBannerV2>
<Controller
control={control}
name="name"
@@ -76,30 +73,6 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
</FormControl>
)}
/>
<Controller
control={control}
name="projects"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
className="w-full"
label="Projects"
tooltipText="Select the project(s) that you'd like to add this gateway to"
errorText={error?.message}
isError={Boolean(error)}
>
<FilterableSelect
options={secretManagerWorkspaces}
placeholder="Select projects..."
value={value}
onChange={onChange}
isMulti
isLoading={isSecretManagerLoading}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
<div className="mt-4 flex items-center">
<Button className="mr-4" size="sm" type="submit" isLoading={isSubmitting}>
Update

View File

@@ -1,8 +1,10 @@
import { useMemo } from "react";
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQuery } from "@tanstack/react-query";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { useGetIdentityKubernetesAuth } from "@app/hooks/api";
import { gatewaysQueryKeys, useGetIdentityKubernetesAuth } from "@app/hooks/api";
import { IdentityKubernetesAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
@@ -16,8 +18,14 @@ export const ViewIdentityKubernetesAuthContent = ({
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data: gateways } = useQuery(gatewaysQueryKeys.list());
const { data, isPending } = useGetIdentityKubernetesAuth(identityId);
const selectedGateway = useMemo(() => {
return gateways?.find((gateway) => gateway.id === data?.gatewayId) || null;
}, [gateways, data?.gatewayId]);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
@@ -69,6 +77,7 @@ export const ViewIdentityKubernetesAuthContent = ({
>
{data.kubernetesHost}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Gateway">{selectedGateway?.name}</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Token Reviewer JWT">
{data.tokenReviewerJwt ? (
<Tooltip

View File

@@ -69,7 +69,8 @@ const orgGatewayPermissionSchema = z
[OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.EditGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.DeleteGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.CreateGateways]: z.boolean().optional()
[OrgGatewayPermissionActions.CreateGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.AttachGateways]: z.boolean().optional()
})
.optional();

View File

@@ -27,7 +27,8 @@ const PERMISSION_ACTIONS = [
{ action: OrgGatewayPermissionActions.ListGateways, label: "List Gateways" },
{ action: OrgGatewayPermissionActions.CreateGateways, label: "Create Gateways" },
{ action: OrgGatewayPermissionActions.EditGateways, label: "Edit Gateways" },
{ action: OrgGatewayPermissionActions.DeleteGateways, label: "Delete Gateways" }
{ action: OrgGatewayPermissionActions.DeleteGateways, label: "Delete Gateways" },
{ action: OrgGatewayPermissionActions.AttachGateways, label: "Attach Gateways" }
] as const;
export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props) => {

View File

@@ -3,14 +3,18 @@ import { useOrgPermission } from "@app/context";
import { OrgDeleteSection } from "../OrgDeleteSection";
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
import { OrgNameChangeSection } from "../OrgNameChangeSection";
import { OrgProductSelectSection } from "../OrgProductSelectSection";
export const OrgGeneralTab = () => {
const { membership } = useOrgPermission();
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgNameChangeSection />
<OrgIncidentContactsSection />
{membership && membership.role === "admin" && <OrgDeleteSection />}
</div>
<>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgNameChangeSection />
<OrgIncidentContactsSection />
{membership && membership.role === "admin" && <OrgDeleteSection />}
</div>
{membership && membership.role === "admin" && <OrgProductSelectSection />}
</>
);
};

View File

@@ -0,0 +1,105 @@
import { useEffect, useState } from "react";
import { Switch } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useUpdateOrg } from "@app/hooks/api";
import axios from "axios";
import { createNotification } from "@app/components/notifications";
export const OrgProductSelectSection = () => {
const [toggledProducts, setToggledProducts] = useState<{
[key: string]: { name: string; enabled: boolean };
}>({
secretsProductEnabled: {
name: "Secret Management",
enabled: true
},
pkiProductEnabled: {
name: "Certificate Management",
enabled: true
},
kmsProductEnabled: {
name: "KMS",
enabled: true
},
sshProductEnabled: {
name: "SSH",
enabled: true
},
scannerProductEnabled: {
name: "Scanner",
enabled: true
},
shareSecretsProductEnabled: {
name: "Share Secrets",
enabled: true
}
});
const [isLoading, setIsLoading] = useState(false);
const { currentOrg } = useOrganization();
const { mutateAsync } = useUpdateOrg();
useEffect(() => {
Object.entries(currentOrg).forEach(([key, value]) => {
if (key in toggledProducts && typeof value === "boolean") {
setToggledProducts((products) => ({
...products,
[key]: { ...products[key], enabled: value }
}));
}
});
}, [currentOrg]);
const onProductToggle = async (value: boolean, key: string) => {
setIsLoading(true);
setToggledProducts((products) => ({
...products,
[key]: { ...products[key], enabled: value }
}));
try {
await mutateAsync({
orgId: currentOrg.id,
[key]: value
});
} catch (e) {
if (axios.isAxiosError(e)) {
const { message = "Something went wrong" } = e.response?.data as { message: string };
createNotification({
type: "error",
text: message
});
}
}
setIsLoading(false);
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<h2 className="text-xl font-semibold text-mineshaft-100">Organization Products</h2>
<p className="mb-4 text-gray-400">
Select which products are available for your organization.
</p>
<div className="grid grid-cols-2 gap-3">
{Object.entries(toggledProducts).map(([key, product]) => (
<Switch
key={key}
id={`enable-${key}`}
isDisabled={isLoading}
onCheckedChange={(value) => onProductToggle(value, key)}
isChecked={product.enabled}
className="ml-0"
containerClassName="flex-row-reverse gap-3 w-fit"
>
{product.name}
</Switch>
))}
</div>
</div>
);
};

View File

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

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Accordion,
AccordionContent,
@@ -18,9 +19,13 @@ import {
SecretInput,
Select,
SelectItem,
TextArea
TextArea,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
@@ -61,7 +66,7 @@ const formSchema = z.object({
revocationStatement: z.string().min(1),
renewStatement: z.string().optional(),
ca: z.string().optional(),
projectGatewayId: z.string().optional()
gatewayId: z.string().optional()
}),
defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val);
@@ -164,8 +169,6 @@ export const SqlDatabaseInputForm = ({
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const { currentWorkspace } = useWorkspace();
const {
control,
setValue,
@@ -193,9 +196,7 @@ export const SqlDatabaseInputForm = ({
});
const createDynamicSecret = useCreateDynamicSecret();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
const handleCreateDynamicSecret = async ({
name,
@@ -301,40 +302,55 @@ export const SqlDatabaseInputForm = ({
Configuration
</div>
<div>
<Controller
control={control}
name="provider.projectGatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Select
value={value}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isProjectGatewaysLoading}
placeholder="Internet gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="provider.gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
Internet Gateway
</SelectItem>
{projectGateways?.map((el) => (
<SelectItem value={el.projectGatewayId} key={el.projectGatewayId}>
{el.name}
</SelectItem>
))}
</Select>
</FormControl>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
/>
</OrgPermissionCan>
</div>
<div className="flex flex-col">
<div className="pb-0.5 pl-1 text-sm text-mineshaft-400">Service</div>

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Accordion,
AccordionContent,
@@ -17,9 +18,11 @@ import {
SecretInput,
Select,
SelectItem,
TextArea
TextArea,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { OrgPermissionSubjects } from "@app/context";
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
@@ -60,7 +63,7 @@ const formSchema = z.object({
revocationStatement: z.string().min(1),
renewStatement: z.string().optional(),
ca: z.string().optional(),
projectGatewayId: z.string().optional().nullable()
gatewayId: z.string().optional().nullable()
})
.partial(),
defaultTTL: z.string().superRefine((val, ctx) => {
@@ -147,15 +150,11 @@ export const EditDynamicSecretSqlProviderForm = ({
}
});
const { currentWorkspace } = useWorkspace();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
const updateDynamicSecret = useUpdateDynamicSecret();
const selectedProjectGatewayId = watch("inputs.projectGatewayId");
const isGatewayInActive =
projectGateways?.findIndex((el) => el.projectGatewayId === selectedProjectGatewayId) === -1;
const selectedGatewayId = watch("inputs.gatewayId");
const isGatewayInActive = gateways?.findIndex((el) => el.id === selectedGatewayId) === -1;
const handleUpdateDynamicSecret = async ({
inputs,
@@ -177,7 +176,7 @@ export const EditDynamicSecretSqlProviderForm = ({
defaultTTL,
inputs: {
...inputs,
projectGatewayId: isGatewayInActive ? null : inputs.projectGatewayId
gatewayId: isGatewayInActive ? null : inputs.gatewayId
},
newName: newName === dynamicSecret.name ? undefined : newName,
metadata
@@ -250,45 +249,60 @@ export const EditDynamicSecretSqlProviderForm = ({
<div>
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
<div>
<Controller
control={control}
name="inputs.projectGatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message) || isGatewayInActive}
errorText={
isGatewayInActive && selectedProjectGatewayId
? `Project Gateway ${selectedProjectGatewayId} is removed`
: error?.message
}
label="Gateway"
helperText=""
>
<Select
value={value || undefined}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isProjectGatewaysLoading}
placeholder="Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="inputs.gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message) || isGatewayInActive}
errorText={
isGatewayInActive && selectedGatewayId
? `Project Gateway ${selectedGatewayId} is removed`
: error?.message
}
label="Gateway"
helperText=""
>
Internet Gateway
</SelectItem>
{projectGateways?.map((el) => (
<SelectItem value={el.projectGatewayId} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</FormControl>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value || undefined}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
/>
</OrgPermissionCan>
</div>
<div className="flex flex-col">
<Controller

View File

@@ -21,12 +21,7 @@ type Props = {
export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => {
const {
destination,
syncOptions: {
// appendSuffix,
// prependPrefix,
initialSyncBehavior,
disableSecretDeletion
}
syncOptions: { initialSyncBehavior, disableSecretDeletion, keySchema }
} = secretSync;
let AdditionalSyncOptionsComponent: ReactNode;
@@ -88,8 +83,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
<GenericFieldLabel label="Initial Sync Behavior">
{SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name}
</GenericFieldLabel>
{/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
<GenericFieldLabel label="Key Schema">{keySchema}</GenericFieldLabel>
{AdditionalSyncOptionsComponent}
{disableSecretDeletion && (
<GenericFieldLabel label="Secret Deletion">