Compare commits

..

49 Commits

Author SHA1 Message Date
Daniel Hougaard
17249d603b fix: add delete secrets event type to frontend 2025-01-10 17:15:26 +01:00
Daniel Hougaard
9bdff9c504 fix: explicit postgres time conversion 2025-01-10 17:15:14 +01:00
Vlad Matsiiako
5d8f32b774 Merge pull request #2955 from thomas-infisical/patch-1
Doc: Update Nov & Dec Changelogs
2025-01-08 21:42:49 -08:00
Maidul Islam
bb71f5eb7e Merge pull request #2956 from Infisical/daniel/update-k8s-manifest
fix: k8's manifest out of sync
2025-01-08 17:25:21 -05:00
Maidul Islam
30b431a255 Merge pull request #2958 from Infisical/bump-ecs-deploy-task-definition
ci: Bump aws-actions/amazon-ecs-deploy-task-definition to v2
2025-01-08 16:22:35 -05:00
max-infisical
fd32118685 Bump aws-actions/amazon-ecs-deploy-task-definition to v2
Bump aws-actions/amazon-ecs-deploy-task-definition to resolve:

```
Error: Failed to register task definition in ECS: Unexpected key 'enableFaultInjection' found in params
Error: Unexpected key 'enableFaultInjection' found in params
```

errors.
2025-01-08 13:20:41 -08:00
Maidul Islam
8528f3fd2a Merge pull request #2897 from Infisical/feat/resource-metadata
feat: resource metadata
2025-01-08 14:47:01 -05:00
Daniel Hougaard
eb358bcafd Update install-secrets-operator.yaml 2025-01-08 16:26:50 +01:00
Thomas
ffbd29c575 doc: update nov & dec changelog
First try at updating the Changelog from these past months.

Let me know if you think of anything I might have forgotten.
2025-01-08 15:50:53 +01:00
Sheen
a74f0170da Merge pull request #2954 from Infisical/fix/addressed-reported-migration-related-ui-issues
fix: resolved conditions permission not getting added from new policy…
2025-01-08 22:26:45 +08:00
=
a0fad34a6d fix: resolved conditions permission not getting added from new policy button 2025-01-08 19:48:13 +05:30
Sheen
0b9d890a51 Merge pull request #2953 from Infisical/fix/addressed-reported-migration-related-ui-issues
fix: address sso redirect and project role creation
2025-01-08 19:20:19 +08:00
Sheen Capadngan
5ba507bc1c misc: made installation ID optional for github 2025-01-08 19:17:38 +08:00
=
0ecc196e5d feat: added missing coerce in azure key vault 2025-01-08 16:30:46 +05:30
=
ddac9f7cc4 feat: made all redirect route to coerce it 2025-01-08 16:27:56 +05:30
Sheen Capadngan
34354994d8 fix: address sso redirect and project role creation 2025-01-08 18:29:25 +08:00
Maidul Islam
1576358805 Merge pull request #2947 from Infisical/auth0-saml
Add Support for Auth0 SAML
2025-01-07 15:04:44 -05:00
Scott Wilson
e6103d2d3f docs: update initial saml setup steps 2025-01-07 11:45:07 -08:00
Maidul Islam
8bf8bc77c9 Merge pull request #2951 from akhilmhdh/fix/broken-image
feat: resolved app not loading on org no access
2025-01-07 14:29:47 -05:00
=
3219723149 feat: resolved app not loading on org no access 2025-01-08 00:55:22 +05:30
Akhil Mohan
6d3793beff Merge pull request #2949 from akhilmhdh/fix/broken-image
Fixed broke image
2025-01-08 00:18:38 +05:30
Scott Wilson
0df41f3391 Merge pull request #2948 from Infisical/fix-user-groups-plan
Improvement: Clarify Enterprise Plan for User Group Feature Upgrade Modal
2025-01-07 10:44:32 -08:00
=
1acac9d479 feat: resolved broken gitlab image and hidden the standalone endpoints from api documentation 2025-01-08 00:14:31 +05:30
Tuan Dang
0cefd6f837 Run linter 2025-01-08 01:41:22 +07:00
Scott Wilson
5e9dc0b98d clarify enterprise plan for user group feature upgrade modal 2025-01-07 10:38:09 -08:00
Tuan Dang
f632847dc6 Merge branch 'main', remote-tracking branch 'origin' into auth0-saml 2025-01-08 01:35:11 +07:00
Tuan Dang
faa6d1cf40 Add support for Auth0 SAML 2025-01-08 01:34:03 +07:00
Maidul Islam
7fb18870e3 Merge pull request #2946 from akhilmhdh/feat/updated-saml-error-message
Updated saml error message
2025-01-07 10:57:03 -05:00
Maidul Islam
ae841715e5 update saml error message 2025-01-07 10:56:44 -05:00
=
baac87c16a feat: updated saml error message on missing email or first name attribute 2025-01-07 21:17:25 +05:30
Sheen Capadngan
291d29ec41 Merge remote-tracking branch 'origin/main' into feat/resource-metadata 2025-01-07 19:15:33 +08:00
Akhil Mohan
b726187ba3 Merge pull request #2917 from mr-ssd/patch-1
docs: add note for dynamic secrets as paid feature
2025-01-07 14:12:50 +05:30
Akhil Mohan
d98ff32b07 Merge pull request #2919 from mr-ssd/patch-2
docs: add note Approval Workflows as paid feature
2025-01-07 14:12:12 +05:30
Sheen
1fa510b32f Merge pull request #2944 from Infisical/feat/target-specific-azure-key-vault-tenant
feat: target specific azure key vault tenant
2025-01-07 14:05:14 +08:00
Maidul Islam
c57f0d8120 Merge pull request #2945 from Infisical/misc/made-secret-path-input-show-correct-folder
misc: made secret path input show correct folders
2025-01-06 23:28:36 -05:00
Sheen Capadngan
00490f2cff misc: made secret path input show correct folders based on env in integrations 2025-01-07 12:08:09 +08:00
Sheen Capadngan
ee58f538c0 feat: target specific azure key vault tenant 2025-01-07 11:53:17 +08:00
Maidul Islam
0fa20f7839 Merge pull request #2943 from Infisical/aws-sm-integration-force-delete
Fix: Set Force Flag on Delete Secret Command for AWS Secrets Manager
2025-01-06 20:51:48 -05:00
Sida Say
1190ca2d77 docs: add note Approval Workflows as paid feature 2024-12-25 15:27:38 +07:00
Sida Say
2fb60201bc add not for dynamic secrets as paid feature 2024-12-25 14:17:27 +07:00
Sheen Capadngan
e763a6f683 misc: added default for metadataSyncMode 2024-12-23 21:29:51 +08:00
Sheen Capadngan
cb1b006118 misc: add secret metadata to batch update 2024-12-23 14:46:23 +08:00
Sheen Capadngan
356e7c5958 feat: added approval handling for metadata in replicate 2024-12-23 14:36:41 +08:00
Sheen Capadngan
1a68765f15 feat: secret approval and replication 2024-12-21 02:51:47 +08:00
Sheen Capadngan
ae07d38c19 Merge remote-tracking branch 'origin/main' into feat/resource-metadata 2024-12-20 14:23:14 +08:00
Sheen Capadngan
025b4b8761 feat: aws secret manager metadata sync 2024-12-19 23:54:47 +08:00
Sheen Capadngan
ef688efc8d feat: integrated secret metadata to ui 2024-12-19 22:02:20 +08:00
Sheen Capadngan
8c98565715 feat: completed basic CRUD 2024-12-19 17:47:39 +08:00
Sheen Capadngan
e9358cd1d8 misc: backend setup + create secret metadata 2024-12-19 15:05:16 +08:00
105 changed files with 1301 additions and 193 deletions

View File

@@ -97,7 +97,7 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-gamma-stage
@@ -153,7 +153,7 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform
@@ -204,7 +204,7 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform

View File

@@ -218,6 +218,9 @@ import {
TRateLimit,
TRateLimitInsert,
TRateLimitUpdate,
TResourceMetadata,
TResourceMetadataInsert,
TResourceMetadataUpdate,
TSamlConfigs,
TSamlConfigsInsert,
TSamlConfigsUpdate,
@@ -887,6 +890,11 @@ declare module "knex/types/tables" {
TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate
>;
[TableName.ResourceMetadata]: KnexOriginal.CompositeTableType<
TResourceMetadata,
TResourceMetadataInsert,
TResourceMetadataUpdate
>;
[TableName.AppConnection]: KnexOriginal.CompositeTableType<
TAppConnections,
TAppConnectionsInsert,

View File

@@ -0,0 +1,40 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.ResourceMetadata))) {
await knex.schema.createTable(TableName.ResourceMetadata, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.string("key").notNullable();
tb.string("value", 1020).notNullable();
tb.uuid("orgId").notNullable();
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
tb.uuid("userId");
tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
tb.uuid("identityId");
tb.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
tb.uuid("secretId");
tb.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
tb.timestamps(true, true, true);
});
}
const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata");
if (!hasSecretMetadataField) {
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
t.jsonb("secretMetadata");
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.ResourceMetadata);
const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata");
if (hasSecretMetadataField) {
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
t.dropColumn("secretMetadata");
});
}
}

View File

@@ -71,6 +71,7 @@ export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles";
export * from "./projects";
export * from "./rate-limit";
export * from "./resource-metadata";
export * from "./saml-configs";
export * from "./scim-tokens";
export * from "./secret-approval-policies";

View File

@@ -80,6 +80,7 @@ export enum TableName {
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
// used by both identity and users
IdentityMetadata = "identity_metadata",
ResourceMetadata = "resource_metadata",
ScimToken = "scim_tokens",
AccessApprovalPolicy = "access_approval_policies",
AccessApprovalPolicyApprover = "access_approval_policies_approvers",

View File

@@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ResourceMetadataSchema = z.object({
id: z.string().uuid(),
key: z.string(),
value: z.string(),
orgId: z.string().uuid(),
userId: z.string().uuid().nullable().optional(),
identityId: z.string().uuid().nullable().optional(),
secretId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;
export type TResourceMetadataInsert = Omit<z.input<typeof ResourceMetadataSchema>, TImmutableDBKeys>;
export type TResourceMetadataUpdate = Partial<Omit<z.input<typeof ResourceMetadataSchema>, TImmutableDBKeys>>;

View File

@@ -24,7 +24,8 @@ export const SecretApprovalRequestsSecretsV2Schema = z.object({
requestId: z.string().uuid(),
op: z.string(),
secretId: z.string().uuid().nullable().optional(),
secretVersion: z.string().uuid().nullable().optional()
secretVersion: z.string().uuid().nullable().optional(),
secretMetadata: z.unknown().nullable().optional()
});
export type TSecretApprovalRequestsSecretsV2 = z.infer<typeof SecretApprovalRequestsSecretsV2Schema>;

View File

@@ -84,7 +84,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
samlConfig.audience = `spn:${ssoConfig.issuer}`;
}
}
if (ssoConfig.authProvider === SamlProviders.GOOGLE_SAML) {
if (
ssoConfig.authProvider === SamlProviders.GOOGLE_SAML ||
ssoConfig.authProvider === SamlProviders.AUTH0_SAML
) {
samlConfig.wantAssertionsSigned = false;
}
@@ -123,7 +126,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
`email: ${email} firstName: ${profile.firstName as string}`
);
throw new Error("Invalid saml request. Missing email or first name");
throw new BadRequestError({
message:
"Missing email or first name. Please double check your SAML attribute mapping for the selected provider."
});
}
const userMetadata = Object.keys(profile.attributes || {})

View File

@@ -12,6 +12,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
UsersSchema.pick({
@@ -274,6 +275,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
.extend({
op: z.string(),
tags: tagSchema,
secretMetadata: ResourceMetadataSchema.nullish(),
secret: z
.object({
id: z.string(),
@@ -291,7 +293,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
secretKey: z.string(),
secretValue: z.string().optional(),
secretComment: z.string().optional(),
tags: tagSchema
tags: tagSchema,
secretMetadata: ResourceMetadataSchema.nullish()
})
.optional()
})

View File

@@ -100,10 +100,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
// Filter by date range
if (startDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate]);
}
if (endDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" <= ?::timestamptz`, [endDate]);
}
// we timeout long running queries to prevent DB resource issues (2 minutes)

View File

@@ -246,8 +246,7 @@ export const licenseServiceFactory = ({
};
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const plan = await getPlan(orgId, projectId);
return plan;
};

View File

@@ -6,7 +6,8 @@ export enum SamlProviders {
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",
GOOGLE_SAML = "google-saml",
KEYCLOAK_SAML = "keycloak-saml"
KEYCLOAK_SAML = "keycloak-saml",
AUTH0_SAML = "auth0-saml"
}
export type TCreateSamlCfgDTO = {

View File

@@ -256,6 +256,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
db.ref("id").withSchema("secVerTag")
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
.select({
secVerTagId: "secVerTag.id",
@@ -279,6 +280,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment")
)
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
);
const formatedDoc = sqlNestRelationships({
data: doc,
@@ -338,9 +344,19 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
})
}
]
},
{
key: "metadataId",
label: "oldSecretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
...el,
secret: secret?.[0],

View File

@@ -22,6 +22,8 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import {
decryptSecretWithBot,
@@ -91,6 +93,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "findById">;
@@ -138,7 +141,8 @@ export const secretApprovalRequestServiceFactory = ({
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
licenseService,
projectSlackConfigDAL
projectSlackConfigDAL,
resourceMetadataDAL
}: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@@ -241,6 +245,7 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.key,
id: el.id,
version: el.version,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
@@ -269,7 +274,8 @@ export const secretApprovalRequestServiceFactory = ({
secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: "",
tags: el.secretVersion.tags
tags: el.secretVersion.tags,
secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO
}
: undefined
}));
@@ -543,6 +549,7 @@ export const secretApprovalRequestServiceFactory = ({
? await fnSecretV2BridgeBulkInsert({
tx,
folderId,
orgId: actorOrgId,
inputSecrets: secretCreationCommits.map((el) => ({
tagIds: el?.tags.map(({ id }) => id),
version: 1,
@@ -550,6 +557,7 @@ export const secretApprovalRequestServiceFactory = ({
encryptedValue: el.encryptedValue,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
references: el.encryptedValue
? getAllSecretReferencesV2Bridge(
secretManagerDecryptor({
@@ -559,6 +567,7 @@ export const secretApprovalRequestServiceFactory = ({
: [],
type: SecretType.Shared
})),
resourceMetadataDAL,
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
@@ -568,6 +577,7 @@ export const secretApprovalRequestServiceFactory = ({
const updatedSecrets = secretUpdationCommits.length
? await fnSecretV2BridgeBulkUpdate({
folderId,
orgId: actorOrgId,
tx,
inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue =
@@ -592,6 +602,7 @@ export const secretApprovalRequestServiceFactory = ({
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
tags: el?.tags.map(({ id }) => id),
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
...encryptedValue
}
};
@@ -599,7 +610,8 @@ export const secretApprovalRequestServiceFactory = ({
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
resourceMetadataDAL
})
: [];
const deletedSecret = secretDeletionCommits.length
@@ -824,6 +836,7 @@ export const secretApprovalRequestServiceFactory = ({
}
await secretQueueService.syncSecrets({
projectId,
orgId: actorOrgId,
secretPath: folder.path,
environmentSlug: folder.environmentSlug,
actorId,
@@ -1208,6 +1221,7 @@ export const secretApprovalRequestServiceFactory = ({
),
skipMultilineEncoding: createdSecret.skipMultilineEncoding,
key: createdSecret.secretKey,
secretMetadata: createdSecret.secretMetadata,
type: SecretType.Shared
}))
);
@@ -1263,12 +1277,14 @@ export const secretApprovalRequestServiceFactory = ({
reminderNote,
secretComment,
metadata,
skipMultilineEncoding
skipMultilineEncoding,
secretMetadata
}) => {
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
return {
...latestSecretVersions[secretId],
secretMetadata,
key: newSecretName || secretKey,
encryptedComment: setKnexStringValue(
secretComment,
@@ -1370,7 +1386,8 @@ export const secretApprovalRequestServiceFactory = ({
reminderRepeatDays,
encryptedValue,
secretId,
secretVersion
secretVersion,
secretMetadata
}) => ({
version,
requestId: doc.id,
@@ -1383,7 +1400,8 @@ export const secretApprovalRequestServiceFactory = ({
reminderRepeatDays,
reminderNote,
encryptedComment,
key
key,
secretMetadata: JSON.stringify(secretMetadata)
})
),
tx

View File

@@ -1,5 +1,6 @@
import { TImmutableDBKeys, TSecretApprovalPolicies, TSecretApprovalRequestsSecrets } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretOperations } from "@app/services/secret/secret-types";
export enum RequestState {
@@ -34,6 +35,7 @@ export type TApprovalCreateSecretV2Bridge = {
reminderRepeatDays?: number | null;
skipMultilineEncoding?: boolean;
metadata?: Record<string, string>;
secretMetadata?: ResourceMetadataDTO;
tagIds?: string[];
};

View File

@@ -13,6 +13,8 @@ import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue";
@@ -56,6 +58,7 @@ type TSecretReplicationServiceFactoryDep = {
>;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
secretVersionV2TagBridgeDAL: Pick<TSecretVersionV2TagDALFactory, "find" | "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">;
queueService: Pick<TQueueServiceFactory, "start" | "listen" | "queue" | "stopJobById">;
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
@@ -121,7 +124,8 @@ export const secretReplicationServiceFactory = ({
secretVersionV2TagBridgeDAL,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
kmsService
kmsService,
resourceMetadataDAL
}: TSecretReplicationServiceFactoryDep) => {
const $getReplicatedSecrets = (
botKey: string,
@@ -151,8 +155,10 @@ export const secretReplicationServiceFactory = ({
};
const $getReplicatedSecretsV2 = (
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[],
importedSecrets: { secrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[] }[]
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[],
importedSecrets: {
secrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[];
}[]
) => {
const deDupe = new Set<string>();
const secrets = [...localSecrets];
@@ -178,6 +184,7 @@ export const secretReplicationServiceFactory = ({
secretPath,
environmentSlug,
projectId,
orgId,
actorId,
actor,
pickOnlyImportIds,
@@ -222,6 +229,7 @@ export const secretReplicationServiceFactory = ({
.map(({ folderId }) =>
secretQueueService.replicateSecrets({
projectId,
orgId,
secretPath: foldersGroupedById[folderId][0]?.path as string,
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
actorId,
@@ -267,6 +275,7 @@ export const secretReplicationServiceFactory = ({
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined
}));
const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets);
const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key);
@@ -333,13 +342,29 @@ export const secretReplicationServiceFactory = ({
.map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create
const locallyUpdatedSecrets = sourceSecrets
.filter(
({ key, secretKey, secretValue }) =>
.filter(({ key, secretKey, secretValue, secretMetadata }) => {
const sourceSecretMetadataJson = JSON.stringify(
(secretMetadata ?? []).map((entry) => ({
key: entry.key,
value: entry.value
}))
);
const destinationSecretMetadataJson = JSON.stringify(
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretMetadata ?? []).map((entry) => ({
key: entry.key,
value: entry.value
}))
);
return (
destinationLocalSecretsGroupedByKey[key]?.[0] &&
// if key or value changed
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretKey !== secretKey ||
destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue)
)
destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue ||
sourceSecretMetadataJson !== destinationSecretMetadataJson)
);
})
.map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create
const locallyDeletedSecrets = destinationLocalSecrets
@@ -387,6 +412,7 @@ export const secretReplicationServiceFactory = ({
op: operation,
requestId: approvalRequestDoc.id,
metadata: doc.metadata,
secretMetadata: JSON.stringify(doc.secretMetadata),
key: doc.key,
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
@@ -406,10 +432,12 @@ export const secretReplicationServiceFactory = ({
if (locallyCreatedSecrets.length) {
await fnSecretV2BridgeBulkInsert({
folderId: destinationReplicationFolderId,
orgId,
secretVersionDAL: secretVersionV2BridgeDAL,
secretDAL: secretV2BridgeDAL,
tx,
secretTagDAL,
resourceMetadataDAL,
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
@@ -419,6 +447,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
secretMetadata: doc.secretMetadata,
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
};
})
@@ -426,10 +455,12 @@ export const secretReplicationServiceFactory = ({
}
if (locallyUpdatedSecrets.length) {
await fnSecretV2BridgeBulkUpdate({
orgId,
folderId: destinationReplicationFolderId,
secretVersionDAL: secretVersionV2BridgeDAL,
secretDAL: secretV2BridgeDAL,
tx,
resourceMetadataDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
inputSecrets: locallyUpdatedSecrets.map((doc) => {
@@ -445,6 +476,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue as Buffer,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
secretMetadata: doc.secretMetadata,
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
}
};
@@ -466,6 +498,7 @@ export const secretReplicationServiceFactory = ({
await secretQueueService.syncSecrets({
projectId,
orgId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environmentSlug,
actorId,
@@ -751,6 +784,7 @@ export const secretReplicationServiceFactory = ({
await secretQueueService.syncSecrets({
projectId,
orgId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environmentSlug,
actorId,

View File

@@ -1150,7 +1150,8 @@ export const INTEGRATION = {
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
shouldEnableDelete: "The flag to enable deletion of secrets.",
octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy."
octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy.",
metadataSyncMode: "The mode for syncing metadata to external system"
}
},
UPDATE: {

View File

@@ -21,6 +21,9 @@ export const registerServeUI = async (
server.route({
method: "GET",
url: "/runtime-ui-env.js",
schema: {
hide: true
},
handler: (_req, res) => {
const appCfg = getConfig();
void res.type("application/javascript");
@@ -43,12 +46,19 @@ export const registerServeUI = async (
wildcard: false
});
server.get("/*", (request, reply) => {
if (request.url.startsWith("/api")) {
reply.callNotFound();
return;
server.route({
method: "GET",
url: "/*",
schema: {
hide: true
},
handler: (request, reply) => {
if (request.url.startsWith("/api")) {
reply.callNotFound();
return;
}
void reply.sendFile("index.html");
}
void reply.sendFile("index.html");
});
}
};

View File

@@ -181,6 +181,7 @@ import { projectUserMembershipRoleDALFactory } from "@app/services/project-membe
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { secretDALFactory } from "@app/services/secret/secret-dal";
import { secretQueueFactory } from "@app/services/secret/secret-queue";
import { secretServiceFactory } from "@app/services/secret/secret-service";
@@ -374,6 +375,7 @@ export const registerRoutes = async (
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
const projectTemplateDAL = projectTemplateDALFactory(db);
const resourceMetadataDAL = resourceMetadataDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
@@ -854,7 +856,8 @@ export const registerRoutes = async (
secretApprovalRequestDAL,
projectKeyDAL,
projectUserMembershipRoleDAL,
orgService
orgService,
resourceMetadataDAL
});
const projectService = projectServiceFactory({
@@ -980,7 +983,8 @@ export const registerRoutes = async (
secretApprovalPolicyService,
secretApprovalRequestSecretDAL,
kmsService,
snapshotService
snapshotService,
resourceMetadataDAL
});
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
@@ -1007,7 +1011,8 @@ export const registerRoutes = async (
projectEnvDAL,
userDAL,
licenseService,
projectSlackConfigDAL
projectSlackConfigDAL,
resourceMetadataDAL
});
const secretService = secretServiceFactory({
@@ -1086,8 +1091,10 @@ export const registerRoutes = async (
kmsService,
secretV2BridgeDAL,
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL,
secretVersionV2BridgeDAL
secretVersionV2BridgeDAL,
resourceMetadataDAL
});
const secretRotationQueue = secretRotationQueueFactory({
telemetryService,
secretRotationDAL,
@@ -1339,7 +1346,8 @@ export const registerRoutes = async (
folderDAL,
secretDAL: secretV2BridgeDAL,
queueService,
secretV2BridgeService
secretV2BridgeService,
resourceMetadataDAL
});
const migrationService = externalMigrationServiceFactory({

View File

@@ -17,6 +17,7 @@ import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -116,6 +117,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
@@ -408,6 +410,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
@@ -693,6 +696,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
@@ -864,6 +868,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,

View File

@@ -18,6 +18,7 @@ import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -205,6 +206,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
@@ -220,7 +222,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string(),
environment: z.string(),
folderId: z.string().optional(),
secrets: secretRawSchema.omit({ createdAt: true, updatedAt: true }).array()
secrets: secretRawSchema
.omit({ createdAt: true, updatedAt: true })
.extend({
secretMetadata: ResourceMetadataSchema.optional()
})
.array()
})
.array()
.optional()
@@ -348,7 +355,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
})
.extend({ name: z.string() })
.array()
.optional()
.optional(),
secretMetadata: ResourceMetadataSchema.optional()
})
})
}
@@ -450,6 +458,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.describe(RAW_SECRETS.CREATE.secretValue),
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
secretMetadata: ResourceMetadataSchema.optional(),
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type),
@@ -484,6 +493,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretValue: req.body.secretValue,
skipMultilineEncoding: req.body.skipMultilineEncoding,
secretComment: req.body.secretComment,
secretMetadata: req.body.secretMetadata,
tagIds: req.body.tagIds,
secretReminderNote: req.body.secretReminderNote,
secretReminderRepeatDays: req.body.secretReminderRepeatDays
@@ -558,6 +568,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type),
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
metadata: z.record(z.string()).optional(),
secretMetadata: ResourceMetadataSchema.optional(),
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
secretReminderRepeatDays: z
.number()
@@ -595,8 +606,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretReminderNote: req.body.secretReminderNote,
metadata: req.body.metadata,
newSecretName: req.body.newSecretName,
secretComment: req.body.secretComment
secretComment: req.body.secretComment,
secretMetadata: req.body.secretMetadata
});
if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval };
}
@@ -1850,6 +1863,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
metadata: z.record(z.string()).optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds)
})
.array()
@@ -1952,6 +1966,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
newSecretName: z.string().min(1).optional().describe(RAW_SECRETS.UPDATE.newSecretName),
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
secretMetadata: ResourceMetadataSchema.optional(),
secretReminderRepeatDays: z
.number()
.optional()

View File

@@ -16,6 +16,7 @@ import { TProjectDALFactory } from "../project/project-dal";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
@@ -35,6 +36,8 @@ export type TImportDataIntoInfisicalDTO = {
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findById">;
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
@@ -503,6 +506,7 @@ export const importDataIntoInfisicalFn = async ({
secretTagDAL,
secretVersionTagDAL,
folderDAL,
resourceMetadataDAL,
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
}: TImportDataIntoInfisicalDTO) => {
// Import data to infisical
@@ -762,6 +766,8 @@ export const importDataIntoInfisicalFn = async ({
};
}),
folderId: selectedFolder.id,
orgId: actorOrgId,
resourceMetadataDAL,
secretDAL,
secretVersionDAL,
secretTagDAL,

View File

@@ -8,6 +8,7 @@ import { TProjectDALFactory } from "../project/project-dal";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
@@ -35,6 +36,8 @@ export type TExternalMigrationQueueFactoryDep = {
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
@@ -52,7 +55,8 @@ export const externalMigrationQueueFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
folderDAL,
resourceMetadataDAL
}: TExternalMigrationQueueFactoryDep) => {
const startImport = async (dto: {
actorEmail: string;
@@ -109,7 +113,8 @@ export const externalMigrationQueueFactory = ({
kmsService,
projectService,
projectEnvService,
secretV2BridgeService
secretV2BridgeService,
resourceMetadataDAL
});
if (projectsNotImported.length) {

View File

@@ -427,3 +427,8 @@ export const getIntegrationOptions = async () => {
return INTEGRATION_OPTIONS;
};
export enum IntegrationMetadataSyncMode {
CUSTOM = "custom",
SECRET_METADATA = "secret-metadata"
}

View File

@@ -38,6 +38,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
import {
CircleCiScope,
@@ -48,6 +49,7 @@ import {
import {
IntegrationInitialSyncBehavior,
IntegrationMappingBehavior,
IntegrationMetadataSyncMode,
Integrations,
IntegrationUrls
} from "./integration-list";
@@ -1082,14 +1084,14 @@ const syncSecretsAWSSecretManager = async ({
projectId
}: {
integration: TIntegrations;
secrets: Record<string, { value: string; comment?: string }>;
secrets: Record<string, { value: string; comment?: string; secretMetadata?: ResourceMetadataDTO }>;
accessId: string | null;
accessToken: string;
awsAssumeRoleArn: string | null;
projectId?: string;
}) => {
const appCfg = getConfig();
const metadata = z.record(z.any()).parse(integration.metadata || {});
const metadata = IntegrationMetadataSchema.parse(integration.metadata || {});
if (!accessId && !awsAssumeRoleArn) {
throw new Error("AWS access ID/AWS Assume Role is required");
@@ -1137,8 +1139,25 @@ const syncSecretsAWSSecretManager = async ({
const processAwsSecret = async (
secretId: string,
secretValue: Record<string, string | null | undefined> | string
secretValue: Record<string, string | null | undefined> | string,
secretMetadata?: ResourceMetadataDTO
) => {
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
const shouldTag =
(secretAWSTag && secretAWSTag.length) ||
(metadata.metadataSyncMode === IntegrationMetadataSyncMode.SECRET_METADATA &&
metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE);
const tagArray =
(metadata.metadataSyncMode === IntegrationMetadataSyncMode.SECRET_METADATA ? secretMetadata : secretAWSTag) ?? [];
const integrationTagObj = tagArray.reduce(
(acc, item) => {
acc[item.key] = item.value;
return acc;
},
{} as Record<string, string>
);
try {
const awsSecretManagerSecret = await secretsManager.send(
new GetSecretValueCommand({
@@ -1174,9 +1193,7 @@ const syncSecretsAWSSecretManager = async ({
}
}
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
if (secretAWSTag && secretAWSTag.length) {
if (shouldTag) {
const describedSecret = await secretsManager.send(
// requires secretsmanager:DescribeSecret policy
new DescribeSecretCommand({
@@ -1186,14 +1203,6 @@ const syncSecretsAWSSecretManager = async ({
if (!describedSecret.Tags) return;
const integrationTagObj = secretAWSTag.reduce(
(acc, item) => {
acc[item.key] = item.value;
return acc;
},
{} as Record<string, string>
);
const awsTagObj = (describedSecret.Tags || []).reduce(
(acc, item) => {
if (item.Key && item.Value) {
@@ -1225,7 +1234,7 @@ const syncSecretsAWSSecretManager = async ({
}
});
secretAWSTag?.forEach((tag) => {
tagArray.forEach((tag) => {
if (!(tag.key in awsTagObj)) {
// create tag in AWS secret manager
tagsToUpdate.push({
@@ -1262,8 +1271,8 @@ const syncSecretsAWSSecretManager = async ({
Name: secretId,
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue),
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Tags: shouldTag
? tagArray.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
@@ -1280,7 +1289,7 @@ const syncSecretsAWSSecretManager = async ({
if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) {
for await (const [key, value] of Object.entries(secrets)) {
await processAwsSecret(key, value.value);
await processAwsSecret(key, value.value, value.secretMetadata);
}
} else {
await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets));
@@ -4440,7 +4449,7 @@ export const syncIntegrationSecrets = async ({
secretPath: string;
};
integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>;
secrets: Record<string, { value: string; comment?: string; secretMetadata?: ResourceMetadataDTO }>;
accessId: string | null;
awsAssumeRoleArn: string | null;
accessToken: string;

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { INTEGRATION } from "@app/lib/api-docs";
import { IntegrationMappingBehavior } from "../integration-auth/integration-list";
import { IntegrationMappingBehavior, IntegrationMetadataSyncMode } from "../integration-auth/integration-list";
export const IntegrationMetadataSchema = z.object({
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
@@ -50,6 +50,11 @@ export const IntegrationMetadataSchema = z.object({
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets),
metadataSyncMode: z
.nativeEnum(IntegrationMetadataSyncMode)
.optional()
.describe(INTEGRATION.CREATE.metadata.metadataSyncMode),
octopusDeployScopeValues: z
.object({
// in Octopus Deploy Scope Value Format

View File

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

View File

@@ -0,0 +1,10 @@
import z from "zod";
export const ResourceMetadataSchema = z
.object({
key: z.string().trim().min(1),
value: z.string().trim().default("")
})
.array();
export type ResourceMetadataDTO = z.infer<typeof ResourceMetadataSchema>;

View File

@@ -1,6 +1,7 @@
import { SecretType, TSecretImports, TSecrets, TSecretsV2 } from "@app/db/schemas";
import { groupBy, unique } from "@app/lib/fn";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
@@ -39,6 +40,7 @@ type TSecretImportSecretsV2 = {
// But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string;
secretComment: string;
secretMetadata?: ResourceMetadataDTO;
})[];
};

View File

@@ -160,6 +160,7 @@ export const secretImportServiceFactory = ({
if (secImport.isReplication && sourceFolder) {
await secretQueueService.replicateSecrets({
secretPath: secImport.importPath,
orgId: actorOrgId,
projectId,
environmentSlug: importEnv.slug,
pickOnlyImportIds: [secImport.id],
@@ -169,6 +170,7 @@ export const secretImportServiceFactory = ({
} else {
await secretQueueService.syncSecrets({
secretPath,
orgId: actorOrgId,
projectId,
environmentSlug: environment,
actorId,
@@ -340,6 +342,7 @@ export const secretImportServiceFactory = ({
await secretQueueService.syncSecrets({
secretPath,
orgId: actorOrgId,
projectId,
environmentSlug: environment,
actor,
@@ -415,6 +418,7 @@ export const secretImportServiceFactory = ({
if (membership && sourceFolder) {
await secretQueueService.replicateSecrets({
orgId: actorOrgId,
secretPath: secretImportDoc.importPath,
projectId,
environmentSlug: secretImportDoc.importEnv.slug,

View File

@@ -78,6 +78,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
@@ -103,6 +109,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
slug,
name: slug
})
},
{
key: "metadataId",
label: "secretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
@@ -221,7 +236,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
const secs = await (tx || db.replicaNode())(TableName.SecretV2)
.where({ folderId })
.where((bd) => {
void bd.whereNull("userId").orWhere({ userId: userId || null });
void bd
.whereNull(`${TableName.SecretV2}.userId`)
.orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null });
})
.leftJoin(
TableName.SecretV2JnTag,
@@ -233,10 +250,16 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
)
.orderBy("id", "asc");
const data = sqlNestRelationships({
@@ -253,6 +276,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
slug,
name: slug
})
},
{
key: "metadataId",
label: "secretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
@@ -367,7 +399,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
}
})
.where((bd) => {
void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null });
void bd
.whereNull(`${TableName.SecretV2}.userId`)
.orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null });
})
.leftJoin(
TableName.SecretV2JnTag,
@@ -379,13 +413,23 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(
selectAllTableCols(TableName.SecretV2),
db.raw(`DENSE_RANK() OVER (ORDER BY "key" ${filters?.orderDirection ?? OrderByDirection.ASC}) as rank`)
db.raw(
`DENSE_RANK() OVER (ORDER BY "${TableName.SecretV2}".key ${
filters?.orderDirection ?? OrderByDirection.ASC
}) as rank`
)
)
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
)
.where((bd) => {
const slugs = filters?.tagSlugs?.filter(Boolean);
if (slugs && slugs.length > 0) {
@@ -425,6 +469,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
slug,
name: slug
})
},
{
key: "metadataId",
label: "secretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
@@ -545,10 +598,17 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
);
const docs = sqlNestRelationships({
data: rawDocs,
key: "id",
@@ -563,6 +623,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
slug,
name: slug
})
},
{
key: "metadataId",
label: "secretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});

View File

@@ -6,6 +6,7 @@ import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
@@ -54,9 +55,11 @@ export const getAllSecretReferences = (maybeSecretReference: string) => {
export const fnSecretBulkInsert = async ({
// TODO: Pick types here
folderId,
orgId,
inputSecrets,
secretDAL,
secretVersionDAL,
resourceMetadataDAL,
secretTagDAL,
secretVersionTagDAL,
tx
@@ -91,6 +94,7 @@ export const fnSecretBulkInsert = async ({
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
tx
);
const newSecretGroupedByKeyName = groupBy(newSecrets, (item) => item.key);
const newSecretTags = inputSecrets.flatMap(({ tagIds: secretTags = [], key }) =>
secretTags.map((tag) => ({
@@ -106,6 +110,7 @@ export const fnSecretBulkInsert = async ({
})),
tx
);
await secretDAL.upsertSecretReferences(
inputSecrets.map(({ references = [], key }) => ({
secretId: newSecretGroupedByKeyName[key][0].id,
@@ -113,6 +118,22 @@ export const fnSecretBulkInsert = async ({
})),
tx
);
await resourceMetadataDAL.insertMany(
inputSecrets.flatMap(({ key: secretKey, secretMetadata }) => {
if (secretMetadata) {
return secretMetadata.map(({ key, value }) => ({
key,
value,
secretId: newSecretGroupedByKeyName[secretKey][0].id,
orgId
}));
}
return [];
}),
tx
);
if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
@@ -120,6 +141,7 @@ export const fnSecretBulkInsert = async ({
[`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id,
[`${TableName.SecretTag}Id` as const]: secret_tagsId
}));
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
}
@@ -130,10 +152,12 @@ export const fnSecretBulkUpdate = async ({
tx,
inputSecrets,
folderId,
orgId,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
secretVersionTagDAL,
resourceMetadataDAL
}: TFnSecretBulkUpdate) => {
const sanitizedInputSecrets = inputSecrets.map(
({
@@ -231,6 +255,34 @@ export const fnSecretBulkUpdate = async ({
}
}
const inputSecretIdsWithMetadata = inputSecrets
.filter((sec) => Boolean(sec.data.secretMetadata))
.map((sec) => sec.filter.id);
await resourceMetadataDAL.delete(
{
$in: {
secretId: inputSecretIdsWithMetadata
}
},
tx
);
await resourceMetadataDAL.insertMany(
inputSecrets.flatMap(({ filter: { id }, data: { secretMetadata } }) => {
if (secretMetadata) {
return secretMetadata.map(({ key, value }) => ({
key,
value,
secretId: id,
orgId
}));
}
return [];
}),
tx
);
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
};
@@ -570,6 +622,7 @@ export const reshapeBridgeSecret = (
color?: string | null;
name: string;
}[];
secretMetadata?: ResourceMetadataDTO;
}
) => ({
secretKey: secret.key,
@@ -588,6 +641,7 @@ export const reshapeBridgeSecret = (
secretReminderRepeatDays: secret.reminderRepeatDays,
secretReminderNote: secret.reminderNote,
metadata: secret.metadata,
secretMetadata: secret.secretMetadata,
createdAt: secret.createdAt,
updatedAt: secret.updatedAt
});

View File

@@ -18,6 +18,7 @@ import { ActorType } from "../auth/auth-type";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { TSecretQueueFactory } from "../secret/secret-queue";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
@@ -74,6 +75,7 @@ type TSecretV2BridgeServiceFactoryDep = {
"insertV2Bridge" | "insertApprovalSecretV2Tags"
>;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
export type TSecretV2BridgeServiceFactory = ReturnType<typeof secretV2BridgeServiceFactory>;
@@ -95,7 +97,8 @@ export const secretV2BridgeServiceFactory = ({
secretApprovalPolicyService,
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL,
kmsService
kmsService,
resourceMetadataDAL
}: TSecretV2BridgeServiceFactoryDep) => {
const $validateSecretReferences = async (
projectId: string,
@@ -141,7 +144,7 @@ export const secretV2BridgeServiceFactory = ({
},
{
operator: "eq",
field: "key",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
}
]
@@ -186,6 +189,7 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
projectId,
secretPath,
secretMetadata,
...inputSecret
}: TCreateSecretDTO) => {
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
@@ -255,6 +259,7 @@ export const secretV2BridgeServiceFactory = ({
const secret = await secretDAL.transaction((tx) =>
fnSecretBulkInsert({
folderId,
orgId: actorOrgId,
inputSecrets: [
{
version: 1,
@@ -272,9 +277,11 @@ export const secretV2BridgeServiceFactory = ({
key: secretName,
userId: inputSecret.type === SecretType.Personal ? actorId : null,
tagIds: inputSecret.tagIds,
references: nestedReferences
references: nestedReferences,
secretMetadata
}
],
resourceMetadataDAL,
secretDAL,
secretVersionDAL,
secretTagDAL,
@@ -287,6 +294,7 @@ export const secretV2BridgeServiceFactory = ({
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
secretPath,
orgId: actorOrgId,
actorId,
actor,
projectId,
@@ -309,6 +317,7 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
projectId,
secretPath,
secretMetadata,
...inputSecret
}: TUpdateSecretDTO) => {
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
@@ -435,6 +444,8 @@ export const secretV2BridgeServiceFactory = ({
const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId,
orgId: actorOrgId,
resourceMetadataDAL,
inputSecrets: [
{
filter: { id: secretId },
@@ -448,6 +459,7 @@ export const secretV2BridgeServiceFactory = ({
skipMultilineEncoding: inputSecret.skipMultilineEncoding,
key: inputSecret.newSecretName || secretName,
tags: inputSecret.tagIds,
secretMetadata,
...encryptedValue
}
}
@@ -475,6 +487,7 @@ export const secretV2BridgeServiceFactory = ({
actorId,
actor,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
}
@@ -562,6 +575,7 @@ export const secretV2BridgeServiceFactory = ({
actorId,
actor,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
}
@@ -961,8 +975,8 @@ export const secretV2BridgeServiceFactory = ({
? secretDAL.findOneWithTags({
folderId,
type: secretType,
key: secretName,
userId: secretType === SecretType.Personal ? actorId : null
[`${TableName.SecretV2}.key` as "key"]: secretName,
[`${TableName.SecretV2}.userId` as "userId"]: secretType === SecretType.Personal ? actorId : null
})
: secretVersionDAL
.findOne({
@@ -1113,7 +1127,7 @@ export const secretV2BridgeServiceFactory = ({
value: [
{
operator: "eq",
field: "key",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
@@ -1185,11 +1199,14 @@ export const secretV2BridgeServiceFactory = ({
key: el.secretKey,
tagIds: el.tagIds,
references,
secretMetadata: el.secretMetadata,
type: SecretType.Shared
};
}),
folderId,
orgId: actorOrgId,
secretDAL,
resourceMetadataDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
@@ -1203,6 +1220,7 @@ export const secretV2BridgeServiceFactory = ({
actorId,
secretPath,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@@ -1254,7 +1272,7 @@ export const secretV2BridgeServiceFactory = ({
value: [
{
operator: "eq",
field: "key",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
@@ -1319,7 +1337,7 @@ export const secretV2BridgeServiceFactory = ({
value: [
{
operator: "eq",
field: "key",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
@@ -1371,6 +1389,7 @@ export const secretV2BridgeServiceFactory = ({
const secrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId,
orgId: actorOrgId,
tx,
inputSecrets: inputSecrets.map((el) => {
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
@@ -1394,6 +1413,7 @@ export const secretV2BridgeServiceFactory = ({
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.newSecretName || el.secretKey,
tags: el.tagIds,
secretMetadata: el.secretMetadata,
...encryptedValue
}
};
@@ -1401,7 +1421,8 @@ export const secretV2BridgeServiceFactory = ({
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
secretVersionTagDAL,
resourceMetadataDAL
})
);
await snapshotService.performSnapshot(folderId);
@@ -1410,6 +1431,7 @@ export const secretV2BridgeServiceFactory = ({
actorId,
secretPath,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@@ -1461,7 +1483,7 @@ export const secretV2BridgeServiceFactory = ({
value: [
{
operator: "eq",
field: "key",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
@@ -1512,6 +1534,7 @@ export const secretV2BridgeServiceFactory = ({
actorId,
secretPath,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@@ -1815,10 +1838,12 @@ export const secretV2BridgeServiceFactory = ({
if (locallyCreatedSecrets.length) {
await fnSecretBulkInsert({
folderId: destinationFolder.id,
orgId: actorOrgId,
secretVersionDAL,
secretDAL,
tx,
secretTagDAL,
resourceMetadataDAL,
secretVersionTagDAL,
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
@@ -1830,6 +1855,7 @@ export const secretV2BridgeServiceFactory = ({
skipMultilineEncoding: doc.skipMultilineEncoding,
reminderNote: doc.reminderNote,
reminderRepeatDays: doc.reminderRepeatDays,
secretMetadata: doc.secretMetadata,
references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : []
};
})
@@ -1838,6 +1864,8 @@ export const secretV2BridgeServiceFactory = ({
if (locallyUpdatedSecrets.length) {
await fnSecretBulkUpdate({
folderId: destinationFolder.id,
orgId: actorOrgId,
resourceMetadataDAL,
secretVersionDAL,
secretDAL,
tx,
@@ -1855,6 +1883,7 @@ export const secretV2BridgeServiceFactory = ({
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
reminderNote: doc.reminderNote,
secretMetadata: doc.secretMetadata,
reminderRepeatDays: doc.reminderRepeatDays,
...(doc.encryptedValue
? {
@@ -1938,6 +1967,7 @@ export const secretV2BridgeServiceFactory = ({
await snapshotService.performSnapshot(destinationFolder.id);
await secretQueueService.syncSecrets({
projectId,
orgId: actorOrgId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environment.slug,
actorId,
@@ -1949,6 +1979,7 @@ export const secretV2BridgeServiceFactory = ({
await snapshotService.performSnapshot(sourceFolder.id);
await secretQueueService.syncSecrets({
projectId,
orgId: actorOrgId,
secretPath: sourceFolder.path,
environmentSlug: sourceFolder.environment.slug,
actorId,

View File

@@ -7,6 +7,8 @@ import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TSecretVersionV2DALFactory } from "./secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal";
@@ -58,6 +60,7 @@ export type TCreateSecretDTO = TProjectPermission & {
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretMetadata?: ResourceMetadataDTO;
};
export type TUpdateSecretDTO = TProjectPermission & {
@@ -75,6 +78,7 @@ export type TUpdateSecretDTO = TProjectPermission & {
metadata?: {
source?: string;
};
secretMetadata?: ResourceMetadataDTO;
};
export type TDeleteSecretDTO = TProjectPermission & {
@@ -94,6 +98,7 @@ export type TCreateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
secretComment?: string;
skipMultilineEncoding?: boolean;
tagIds?: string[];
secretMetadata?: ResourceMetadataDTO;
metadata?: {
source?: string;
};
@@ -113,6 +118,7 @@ export type TUpdateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
tagIds?: string[];
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretMetadata?: ResourceMetadataDTO;
}[];
};
@@ -136,8 +142,16 @@ export type TSecretReference = { environment: string; secretPath: string; secret
export type TFnSecretBulkInsert = {
folderId: string;
orgId: string;
tx?: Knex;
inputSecrets: Array<Omit<TSecretsV2Insert, "folderId"> & { tagIds?: string[]; references: TSecretReference[] }>;
inputSecrets: Array<
Omit<TSecretsV2Insert, "folderId"> & {
tagIds?: string[];
references: TSecretReference[];
secretMetadata?: ResourceMetadataDTO;
}
>;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
@@ -156,10 +170,12 @@ type TRequireReferenceIfValue =
export type TFnSecretBulkUpdate = {
folderId: string;
orgId: string;
inputSecrets: {
filter: Partial<TSecretsV2>;
data: TRequireReferenceIfValue & { tags?: string[] };
data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO };
}[];
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;

View File

@@ -749,7 +749,8 @@ export const createManySecretsRawFnFactory = ({
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL,
kmsService
kmsService,
resourceMetadataDAL
}: TCreateManySecretsRawFnFactory) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
const createManySecretsRawFn = async ({
@@ -760,7 +761,7 @@ export const createManySecretsRawFnFactory = ({
userId
}: TCreateManySecretsRawFn) => {
const { botKey, shouldUseSecretV2Bridge } = await getBotKeyFn(projectId);
const project = await projectDAL.findById(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
throw new NotFoundError({
@@ -814,7 +815,9 @@ export const createManySecretsRawFnFactory = ({
tagIds: el.tags
})),
folderId,
orgId: project.orgId,
secretDAL: secretV2BridgeDAL,
resourceMetadataDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
@@ -909,6 +912,7 @@ export const updateManySecretsRawFnFactory = ({
secretVersionTagV2BridgeDAL,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
resourceMetadataDAL,
kmsService
}: TUpdateManySecretsRawFnFactory) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
@@ -920,6 +924,7 @@ export const updateManySecretsRawFnFactory = ({
userId
}: TUpdateManySecretsRawFn): Promise<Array<{ id: string }>> => {
const { botKey, shouldUseSecretV2Bridge } = await getBotKeyFn(projectId);
const project = await projectDAL.findById(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
@@ -988,11 +993,13 @@ export const updateManySecretsRawFnFactory = ({
const updatedSecrets = await secretDAL.transaction(async (tx) =>
fnSecretV2BridgeBulkUpdate({
folderId,
orgId: project.orgId,
tx,
inputSecrets: inputSecrets.map((el) => ({
filter: { id: secretsToUpdateInDBGroupedByKey[el.key][0].id, type: SecretType.Shared },
data: el
})),
resourceMetadataDAL,
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,

View File

@@ -47,6 +47,8 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
@@ -104,6 +106,7 @@ type TSecretQueueFactoryDep = {
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
export type TGetSecrets = {
@@ -120,7 +123,12 @@ export const uniqueSecretQueueKey = (environment: string, secretPath: string) =>
type TIntegrationSecret = Record<
string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
{
value: string;
comment?: string;
skipMultilineEncoding?: boolean | null | undefined;
secretMetadata?: ResourceMetadataDTO;
}
>;
// TODO(akhilmhdh): split this into multiple queue
@@ -157,7 +165,8 @@ export const secretQueueFactory = ({
auditLogService,
orgService,
projectUserMembershipRoleDAL,
projectKeyDAL
projectKeyDAL,
resourceMetadataDAL
}: TSecretQueueFactoryDep) => {
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
@@ -306,7 +315,8 @@ export const secretQueueFactory = ({
kmsService,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL
secretVersionTagV2BridgeDAL,
resourceMetadataDAL
});
const updateManySecretsRawFn = updateManySecretsRawFnFactory({
@@ -321,7 +331,8 @@ export const secretQueueFactory = ({
kmsService,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL
secretVersionTagV2BridgeDAL,
resourceMetadataDAL
});
/**
@@ -372,6 +383,7 @@ export const secretQueueFactory = ({
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
content[secretKey].secretMetadata = secret.secretMetadata;
})
);
@@ -397,7 +409,8 @@ export const secretQueueFactory = ({
content[importedSecret.key] = {
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
comment: importedSecret.secretComment,
value: importedSecret.secretValue || ""
value: importedSecret.secretValue || "",
secretMetadata: importedSecret.secretMetadata
};
}
}
@@ -597,6 +610,7 @@ export const secretQueueFactory = ({
_depth: depth,
secretPath,
projectId,
orgId,
environmentSlug: environment,
excludeReplication,
actorId,
@@ -625,6 +639,7 @@ export const secretQueueFactory = ({
_deDupeReplicationQueue: deDupeReplicationQueue,
_depth: depth,
projectId,
orgId,
secretPath,
actorId,
actor,
@@ -681,6 +696,7 @@ export const secretQueueFactory = ({
if (!folder) {
throw new Error("Secret path not found");
}
const project = await projectDAL.findById(projectId);
// find all imports made with the given environment and secret path
const linkSourceDto = {
@@ -715,6 +731,7 @@ export const secretQueueFactory = ({
.map(({ folderId }) =>
syncSecrets({
projectId,
orgId: project.orgId,
secretPath: foldersGroupedById[folderId][0]?.path as string,
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,
@@ -767,6 +784,7 @@ export const secretQueueFactory = ({
.map((folderId) =>
syncSecrets({
projectId,
orgId: project.orgId,
secretPath: referencedFoldersGroupedById[folderId][0]?.path as string,
environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,

View File

@@ -288,6 +288,7 @@ export const secretServiceFactory = ({
actorId,
actor,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
}
@@ -429,6 +430,7 @@ export const secretServiceFactory = ({
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
secretPath: path,
orgId: actorOrgId,
actorId,
actor,
projectId,
@@ -526,6 +528,7 @@ export const secretServiceFactory = ({
actorId,
actor,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
}
@@ -820,6 +823,7 @@ export const secretServiceFactory = ({
actorId,
secretPath: path,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@@ -928,6 +932,7 @@ export const secretServiceFactory = ({
actorId,
secretPath: path,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@@ -1014,6 +1019,7 @@ export const secretServiceFactory = ({
actorId,
secretPath: path,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@@ -1385,7 +1391,8 @@ export const secretServiceFactory = ({
skipMultilineEncoding,
tagIds,
secretReminderNote,
secretReminderRepeatDays
secretReminderRepeatDays,
secretMetadata
}: TCreateSecretRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const policy =
@@ -1412,7 +1419,8 @@ export const secretServiceFactory = ({
secretValue,
tagIds,
reminderNote: secretReminderNote,
reminderRepeatDays: secretReminderRepeatDays
reminderRepeatDays: secretReminderRepeatDays,
secretMetadata
}
]
}
@@ -1435,7 +1443,8 @@ export const secretServiceFactory = ({
tagIds,
secretReminderNote,
skipMultilineEncoding,
secretReminderRepeatDays
secretReminderRepeatDays,
secretMetadata
});
return { secret, type: SecretProtectionType.Direct as const };
}
@@ -1525,7 +1534,8 @@ export const secretServiceFactory = ({
secretReminderRepeatDays,
metadata,
secretComment,
newSecretName
newSecretName,
secretMetadata
}: TUpdateSecretRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const policy =
@@ -1553,7 +1563,8 @@ export const secretServiceFactory = ({
secretValue,
tagIds,
reminderNote: secretReminderNote,
reminderRepeatDays: secretReminderRepeatDays
reminderRepeatDays: secretReminderRepeatDays,
secretMetadata
}
]
}
@@ -1577,7 +1588,8 @@ export const secretServiceFactory = ({
secretName,
newSecretName,
metadata,
secretValue
secretValue,
secretMetadata
});
return { type: SecretProtectionType.Direct as const, secret };
}
@@ -1793,7 +1805,8 @@ export const secretServiceFactory = ({
secretComment: el.secretComment,
metadata: el.metadata,
skipMultilineEncoding: el.skipMultilineEncoding,
secretKey: el.secretKey
secretKey: el.secretKey,
secretMetadata: el.secretMetadata
}))
}
});
@@ -1919,7 +1932,8 @@ export const secretServiceFactory = ({
secretValue: el.secretValue,
secretComment: el.secretComment,
skipMultilineEncoding: el.skipMultilineEncoding,
secretKey: el.secretKey
secretKey: el.secretKey,
secretMetadata: el.secretMetadata
}))
}
});
@@ -2262,6 +2276,7 @@ export const secretServiceFactory = ({
await secretQueueService.syncSecrets({
secretPath,
projectId: project.id,
orgId: project.orgId,
environmentSlug: environment,
excludeReplication: true
});
@@ -2370,6 +2385,7 @@ export const secretServiceFactory = ({
await secretQueueService.syncSecrets({
secretPath,
projectId: project.id,
orgId: project.orgId,
environmentSlug: environment,
excludeReplication: true
});
@@ -2828,6 +2844,7 @@ export const secretServiceFactory = ({
await snapshotService.performSnapshot(destinationFolder.id);
await secretQueueService.syncSecrets({
projectId: project.id,
orgId: project.orgId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environment.slug,
actorId,
@@ -2839,6 +2856,7 @@ export const secretServiceFactory = ({
await snapshotService.performSnapshot(sourceFolder.id);
await secretQueueService.syncSecrets({
projectId: project.id,
orgId: project.orgId,
secretPath: sourceFolder.path,
environmentSlug: sourceFolder.environment.slug,
actorId,

View File

@@ -14,6 +14,8 @@ import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { ActorType } from "../auth/auth-type";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
@@ -211,6 +213,7 @@ export type TCreateSecretRawDTO = TProjectPermission & {
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretMetadata?: ResourceMetadataDTO;
};
export type TUpdateSecretRawDTO = TProjectPermission & {
@@ -228,6 +231,7 @@ export type TUpdateSecretRawDTO = TProjectPermission & {
metadata?: {
source?: string;
};
secretMetadata?: ResourceMetadataDTO;
};
export type TDeleteSecretRawDTO = TProjectPermission & {
@@ -248,6 +252,7 @@ export type TCreateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
secretComment?: string;
skipMultilineEncoding?: boolean;
tagIds?: string[];
secretMetadata?: ResourceMetadataDTO;
metadata?: {
source?: string;
};
@@ -266,6 +271,7 @@ export type TUpdateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
secretComment?: string;
skipMultilineEncoding?: boolean;
tagIds?: string[];
secretMetadata?: ResourceMetadataDTO;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
}[];
@@ -293,7 +299,13 @@ export type TSecretReference = { environment: string; secretPath: string };
export type TFnSecretBulkInsert = {
folderId: string;
tx?: Knex;
inputSecrets: Array<Omit<TSecretsInsert, "folderId"> & { tags?: string[]; references?: TSecretReference[] }>;
inputSecrets: Array<
Omit<TSecretsInsert, "folderId"> & {
tags?: string[];
references?: TSecretReference[];
secretMetadata?: ResourceMetadataDTO;
}
>;
secretDAL: Pick<TSecretDALFactory, "insertMany" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
@@ -389,6 +401,7 @@ export type TCreateManySecretsRawFnFactory = {
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
};
export type TCreateManySecretsRawFn = {
@@ -425,6 +438,7 @@ export type TUpdateManySecretsRawFnFactory = {
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
export type TUpdateManySecretsRawFn = {
@@ -460,6 +474,7 @@ export type TSyncSecretsDTO<T extends boolean = false> = {
_depth?: number;
secretPath: string;
projectId: string;
orgId: string;
environmentSlug: string;
// cases for just doing sync integration and webhook
excludeReplication?: T;

View File

@@ -4,6 +4,27 @@ title: "Changelog"
The changelog below reflects new product developments and updates on a monthly basis.
## December 2024
- Added [GCP KMS](https://infisical.com/docs/documentation/platform/kms/overview) integration support.
- Added support for [K8s CSI integration](https://infisical.com/docs/integrations/platforms/kubernetes-csi) and ability to point K8s operator to specific secret versions.
- Fixed [Java SDK](https://github.com/Infisical/java-sdk) compatibility issues with Alpine Linux.
- Fixed SCIM group role assignment issues.
- Added Group View Page for improved team management.
- Added instance URL to email verification for Infisical accounts.
- Added ability to copy full path of nested folders.
- Added custom templating support for K8s operator, allowing flexible secret key mapping and additional fields
- Optimized secrets versions table performance.
## November 2024
- Improved EnvKey migration functionality with support for Blocks, Inheritance, and Branches.
- Added [Hardware Security Module (HSM) Encryption](https://infisical.com/docs/documentation/platform/kms/hsm-integration) support.
- Updated permissions handling in [Infisical Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) to use lists instead of sets.
- Enhanced [SCIM](https://infisical.com/docs/documentation/platform/scim/overview) implementation to remove SAML dependency.
- Enhanced [OIDC Authentication](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general) implementation and added Default Org Slug support.
- Added support for multiple authentication methods per identity.
- Added AWS Parameter Store integration sync improvements.
- Added new screen and API for managing additional privileges.
- Added Dynamic Secrets support for SQL Server.
## October 2024
- Significantly improved performance of audit log operations in UI.

View File

@@ -4,6 +4,13 @@ sidebarTitle: "Overview"
description: "Learn how to generate secrets dynamically on-demand."
---
<Info>
Note that Dynamic Secrets is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**
If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
</Info>
## Introduction
Contrary to static key-value secrets, which require manual input of data into the secure Infisical storage, **dynamic secrets are generated on-demand upon access**.

View File

@@ -3,6 +3,13 @@ title: "Approval Workflows"
description: "Learn how to enable a set of policies to manage changes to sensitive secrets and environments."
---
<Info>
Approval Workflows is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier** and **Enterprise Tire**.
If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
</Info>
## Problem at hand
Updating secrets in high-stakes environments (e.g., production) can have a number of problematic issues:
@@ -40,4 +47,4 @@ When a user submits a change to an enviropnment that is under a particular polic
Approvers are notified by email and/or Slack as soon as the request is initiated. In the Infisical Dashboard, they will be able to `approve` and `merge` (or `deny`) a request for a change in a particular environment. After that, depending on the workflows setup, the change will be automatically propagated to the right applications (e.g., using [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes)).
![secrets update pull request](../../images/platform/pr-workflows/secret-update-pr.png)
![secrets update pull request](../../images/platform/pr-workflows/secret-update-pr.png)

View File

@@ -15,7 +15,7 @@ Prerequisites:
<Steps>
<Step title="Create a SCIM token in Infisical">
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
In Infisical, head to your Organization Settings > Security > SCIM Configuration and
press the **Enable SCIM provisioning** toggle to allow Azure to provision/deprovision users for your organization.
![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png)

View File

@@ -15,7 +15,7 @@ Prerequisites:
<Steps>
<Step title="Create a SCIM token in Infisical">
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
In Infisical, head to your Organization Settings > Security > SCIM Configuration and
press the **Enable SCIM provisioning** toggle to allow JumpCloud to provision/deprovision users and user groups for your organization.
![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png)

View File

@@ -15,7 +15,7 @@ Prerequisites:
<Steps>
<Step title="Create a SCIM token in Infisical">
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
In Infisical, head to your Organization Settings > Security > SCIM Configuration and
press the **Enable SCIM provisioning** toggle to allow Okta to provision/deprovision users and user groups for your organization.
![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png)

View File

@@ -0,0 +1,93 @@
---
title: "Auth0 SAML"
description: "Learn how to configure Auth0 SAML for Infisical SSO."
---
<Info>
Auth0 SAML SSO feature is a paid feature. If you're using Infisical Cloud,
then it is available under the **Pro Tier**. If you're self-hosting Infisical,
then you should contact sales@infisical.com to purchase an enterprise license
to use it.
</Info>
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Auth0, then click **Connect** again.
Next, note the **Application Callback URL** and **Audience** to use when configuring the Auth0 SAML application.
![Auth0 SAML initial configuration](../../../images/sso/auth0-saml/init-config.png)
</Step>
<Step title="Create a SAML application in Auth0">
2.1. In your Auth0 account, head to Applications and create an application.
![Auth0 SAML app creation](../../../images/sso/auth0-saml/create-application.png)
Select **Regular Web Application** and press **Create**.
![Auth0 SAML app creation](../../../images/sso/auth0-saml/create-application-2.png)
2.2. In the Application head to Settings > Application URIs and add the **Application Callback URL** from step 1 into the **Allowed Callback URLs** field.
![Auth0 SAML allowed callback URLs](../../../images/sso/auth0-saml/auth0-config.png)
2.3. In the Application head to Addons > SAML2 Web App and copy the **Issuer**, **Identity Provider Login URL**, and **Identity Provider Certificate** from the **Usage** tab.
![Auth0 SAML config](../../../images/sso/auth0-saml/auth0-config-2.png)
2.4. Back in Infisical, set **Issuer**, **Identity Provider Login URL**, and **Certificate** to the corresponding items from step 2.3.
![Auth0 SAML Infisical config](../../../images/sso/auth0-saml/infisical-config.png)
2.5. Back in Auth0, in the **Settings** tab, set the **Application Callback URL** to the **Application Callback URL** from step 1
and update the **Settings** field with the JSON under the picture below (replacing `<audience-from-infisical>` with the **Audience** from step 1).
![Auth0 SAML config](../../../images/sso/auth0-saml/auth0-config-3.png)
```json
{
"audience": "<audience-from-infisical>",
"mappings": {
"email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email",
"given_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName",
"family_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/lastName"
},
"signatureAlgorithm": "rsa-sha256",
"digestAlgorithm": "sha256",
"signResponse": true
}
```
Click **Save**.
</Step>
<Step title="Enable SAML SSO in Infisical">
Enabling SAML SSO allows members in your organization to log into Infisical via Auth0.
![Auth0 SAML enable](../../../images/sso/auth0-saml/enable-saml.png)
</Step>
<Step title="Enforce SAML SSO in Infisical">
Enforcing SAML SSO ensures that members in your organization can only access Infisical
by logging into the organization via Auth0.
To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Auth0 user with Infisical;
Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO.
</Step>
</Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite SAML login.
</Tip>
<Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work:
<div class="height:1px;"/>
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64
32`.
<div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note>

View File

@@ -12,7 +12,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO."
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Azure / Entra, then click **Connect** again.
Next, copy the **Reply URL (Assertion Consumer Service URL)** and **Identifier (Entity ID)** to use when configuring the Azure SAML application.

View File

@@ -12,7 +12,7 @@ description: "Learn how to configure Google SAML for Infisical SSO."
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Google, then click **Connect** again.
Next, note the **ACS URL** and **SP Entity ID** to use when configuring the Google SAML application.

View File

@@ -12,7 +12,7 @@ description: "Learn how to configure JumpCloud SAML for Infisical SSO."
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select JumpCloud, then click **Connect** again.
Next, copy the **ACS URL** and **SP Entity ID** to use when configuring the JumpCloud SAML application.

View File

@@ -12,7 +12,7 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO."
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Manage**.
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Keycloak, then click **Connect** again.
![Keycloak SAML organization security section](../../../images/sso/keycloak/org-security-section.png)

View File

@@ -12,7 +12,7 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
<Steps>
<Step title="Prepare the SAML SSO configuration in Infisical">
In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**.
In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Okta, then click **Connect** again.
Next, copy the **Single sign-on URL** and **Audience URI (SP Entity ID)** to use when configuring the Okta SAML 2.0 application.
![Okta SAML initial configuration](../../../images/sso/okta/init-config.png)

View File

@@ -28,6 +28,7 @@ Infisical supports these and many other identity providers:
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud)
- [Keycloak SAML](/documentation/platform/sso/keycloak-saml)
- [Google SAML](/documentation/platform/sso/google-saml)
- [Auth0 SAML](/documentation/platform/sso/auth0-saml)
- [Keycloak OIDC](/documentation/platform/sso/keycloak-oidc)
- [Auth0 OIDC](/documentation/platform/sso/auth0-oidc)
- [General OIDC](/documentation/platform/sso/general-oidc)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

View File

@@ -17,6 +17,7 @@ Prerequisites:
If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2.
The following steps are for instances not deployed on AWS
<Steps>
<Step title="Create an IAM User">
Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console.
@@ -40,9 +41,10 @@ The following steps are for instances not deployed on AWS
<Step title="Obtain the IAM User Credentials">
Obtain the AWS access key ID and secret access key for your IAM User by navigating to IAM > Users > [Your User] > Security credentials > Access keys.
![Access Key Step 1](../../images/integrations/aws/integrations-aws-access-key-1.png)
![Access Key Step 2](../../images/integrations/aws/integrations-aws-access-key-2.png)
![Access Key Step 3](../../images/integrations/aws/integrations-aws-access-key-3.png)
![Access Key Step 1](../../images/integrations/aws/integrations-aws-access-key-1.png)
![Access Key Step 2](../../images/integrations/aws/integrations-aws-access-key-2.png)
![Access Key Step 3](../../images/integrations/aws/integrations-aws-access-key-3.png)
</Step>
<Step title="Set Up Integration Keys">
1. Set the access key as **CLIENT_ID_AWS_INTEGRATION**.
@@ -59,6 +61,7 @@ The following steps are for instances not deployed on AWS
2. Select **AWS Account** as the **Trusted Entity Type**.
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
4. Optionally, enable **Require external ID** and enter your **project ID** to further enhance security.
</Step>
<Step title="Add Required Permissions for the IAM Role">
@@ -89,11 +92,13 @@ The following steps are for instances not deployed on AWS
]
}
```
</Step>
<Step title="Copy the AWS IAM Role ARN">
![Copy IAM Role ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png)
</Step>
<Step title="Copy the AWS IAM Role ARN">
![Copy IAM Role
ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png)
</Step>
<Step title="Authorize Infisical for AWS Secrets Manager">
1. Navigate to your project's integrations tab in Infisical.
@@ -104,6 +109,7 @@ The following steps are for instances not deployed on AWS
![Select Assume Role](../../images/integrations/aws/integration-aws-iam-assume-select.png)
4. Provide the **AWS IAM Role ARN** obtained from the previous step.
</Step> <Step title="Start integration">
Select how you want to integration to work by specifying a number of parameters:
@@ -127,6 +133,12 @@ The following steps are for instances not deployed on AWS
Optionally, you can add tags or specify the encryption key of all the secrets created via this integration:
<ParamField path="Tag Sync Mode" type="string" optional>
The sync mode for AWS tags. The supported options are `Secret Metadata` and `Custom`. If `Secret Metadata` is selected,
the metadata of the Infisical secrets are used as tags in AWS. If custom is selected, then the key/value of the **Secret Tag** field is used. `Secret Metadata` mode
is only supported for one-to-one integrations.
</ParamField>
<ParamField path="Secret Tag" type="string" optional>
The Key/Value of a tag that will be added to secrets in AWS. Please note that it is possible to add multiple tags via API.
</ParamField>

View File

@@ -248,6 +248,7 @@
"documentation/platform/sso/jumpcloud",
"documentation/platform/sso/keycloak-saml",
"documentation/platform/sso/google-saml",
"documentation/platform/sso/auth0-saml",
"documentation/platform/sso/keycloak-oidc",
"documentation/platform/sso/auth0-oidc",
"documentation/platform/sso/general-oidc"

View File

@@ -101,6 +101,10 @@ export const ROUTE_PATHS = Object.freeze({
"/secret-manager/$projectId/integrations/azure-devops/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create"
),
AzureKeyVaultAuthorizePage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/authorize",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
),
AzureKeyVaultOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"

View File

@@ -3,6 +3,7 @@ import { EventType, UserAgentType } from "./enums";
export const eventToNameMap: { [K in EventType]: string } = {
[EventType.GET_SECRETS]: "List secrets",
[EventType.GET_SECRET]: "Read secret",
[EventType.DELETE_SECRETS]: "Delete secrets",
[EventType.CREATE_SECRET]: "Create secret",
[EventType.UPDATE_SECRET]: "Update secret",
[EventType.DELETE_SECRET]: "Delete secret",

View File

@@ -17,6 +17,7 @@ export enum UserAgentType {
export enum EventType {
GET_SECRETS = "get-secrets",
DELETE_SECRETS = "delete-secrets",
GET_SECRET = "get-secret",
CREATE_SECRET = "create-secret",
UPDATE_SECRET = "update-secret",

View File

@@ -4,7 +4,12 @@ import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace";
import { TCloudIntegration, TIntegrationWithEnv, TOctopusDeployScopeValues } from "./types";
import {
IntegrationMetadataSyncMode,
TCloudIntegration,
TIntegrationWithEnv,
TOctopusDeployScopeValues
} from "./types";
export const integrationQueryKeys = {
getIntegrations: () => ["integrations"] as const,
@@ -88,6 +93,7 @@ export const useCreateIntegration = () => {
shouldProtectSecrets?: boolean;
shouldEnableDelete?: boolean;
octopusDeployScopeValues?: TOctopusDeployScopeValues;
metadataSyncMode?: IntegrationMetadataSyncMode;
};
}) => {
const {

View File

@@ -62,6 +62,7 @@ export type TIntegration = {
octopusDeployScopeValues?: TOctopusDeployScopeValues;
awsIamRole?: string;
region?: string;
metadataSyncMode?: IntegrationMetadataSyncMode;
};
};
@@ -92,3 +93,8 @@ export enum IntegrationMappingBehavior {
ONE_TO_ONE = "one-to-one",
MANY_TO_ONE = "many-to-one"
}
export enum IntegrationMetadataSyncMode {
CUSTOM = "custom",
SECRET_METADATA = "secret-metadata"
}

View File

@@ -82,6 +82,7 @@ export type TSecretApprovalRequest = {
conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>;
commits: ({
// if there is no secret means it was creation
secretMetadata?: { key: string; value: string }[];
secret?: { version: number };
secretVersion: SecretV3Raw;
// if there is no new version its for Delete

View File

@@ -84,7 +84,8 @@ export const useUpdateSecretV3 = ({
secretReminderRepeatDays,
secretReminderNote,
newSecretName,
skipMultilineEncoding
skipMultilineEncoding,
secretMetadata
}) => {
const { data } = await apiRequest.patch(`/api/v3/secrets/raw/${secretKey}`, {
workspaceId,
@@ -97,7 +98,8 @@ export const useUpdateSecretV3 = ({
newSecretName,
secretComment,
tagIds,
secretValue
secretValue,
secretMetadata
});
return data;
},

View File

@@ -67,7 +67,8 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
updatedAt: el.updatedAt,
version: el.version,
skipMultilineEncoding: el.skipMultilineEncoding,
path: el.secretPath
path: el.secretPath,
secretMetadata: el.secretMetadata
};
if (el.type === SecretType.Personal) {

View File

@@ -48,6 +48,7 @@ export type SecretV3RawSanitized = {
overrideAction?: string;
folderId?: string;
skipMultilineEncoding?: boolean;
secretMetadata?: { key: string; value: string }[];
};
export type SecretV3Raw = {
@@ -63,6 +64,7 @@ export type SecretV3Raw = {
secretComment?: string;
secretReminderNote?: string;
secretReminderRepeatDays?: number;
secretMetadata?: { key: string; value: string }[];
skipMultilineEncoding?: boolean;
metadata?: Record<string, string>;
tags?: WsTag[];
@@ -148,6 +150,7 @@ export type TUpdateSecretsV3DTO = {
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
tagIds?: string[];
secretMetadata?: { key: string; value: string }[];
};
export type TDeleteSecretsV3DTO = {

View File

@@ -48,7 +48,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${orgSlug}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`;
navigate({ to: redirectUrl });
window.location.assign(redirectUrl);
};
const redirectToOidc = (orgSlug: string) => {
@@ -56,7 +57,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const redirectUrl = `/api/v1/sso/oidc/login?orgSlug=${orgSlug}${
callbackPort ? `&callbackPort=${callbackPort}` : ""
}`;
navigate({ to: redirectUrl });
window.location.assign(redirectUrl);
};
useEffect(() => {

View File

@@ -27,7 +27,7 @@ export const OrgGroupsSection = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description:
"You can manage users more efficiently with groups if you upgrade your Infisical plan."
"You can manage users more efficiently with groups if you upgrade your Infisical plan to an Enterprise license."
});
} else {
handlePopUpOpen("group");

View File

@@ -5,9 +5,9 @@ import { z } from "zod";
import { GitHubOAuthCallbackPage } from "./GithubOauthCallbackPage";
const GitHubOAuthCallbackPageQueryParamsSchema = z.object({
code: z.string().catch(""),
code: z.coerce.string().catch(""),
state: z.string().catch(""),
installation_id: z.coerce.string().catch("")
installation_id: z.coerce.string().optional().catch("")
});
export const Route = createFileRoute(

View File

@@ -25,7 +25,8 @@ enum AuthProvider {
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",
KEYCLOAK_SAML = "keycloak-saml",
GOOGLE_SAML = "google-saml"
GOOGLE_SAML = "google-saml",
AUTH0_SAML = "auth0-saml"
}
const ssoAuthProviders = [
@@ -33,7 +34,8 @@ const ssoAuthProviders = [
{ label: "Azure / Entra SAML", value: AuthProvider.AZURE_SAML },
{ label: "JumpCloud SAML", value: AuthProvider.JUMPCLOUD_SAML },
{ label: "Keycloak SAML", value: AuthProvider.KEYCLOAK_SAML },
{ label: "Google SAML", value: AuthProvider.GOOGLE_SAML }
{ label: "Google SAML", value: AuthProvider.GOOGLE_SAML },
{ label: "Auth0 SAML", value: AuthProvider.AUTH0_SAML }
];
const schema = z
@@ -191,6 +193,15 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelet
issuer: "Issuer",
issuerPlaceholder: window.origin
};
case AuthProvider.AUTH0_SAML:
return {
acsUrl: "Application Callback URL",
entityId: "Audience",
entryPoint: "Identity Provider Login URL",
entryPointPlaceholder: "https://xxx.auth0.com/samlp/xxx",
issuer: "Issuer",
issuerPlaceholder: "urn:xxx-xxx.us.auth0.com"
};
default:
return {
acsUrl: "ACS URL",

View File

@@ -33,7 +33,7 @@ export const GroupsSection = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description:
"You can manage users more efficiently with groups if you upgrade your Infisical plan."
"You can manage users more efficiently with groups if you upgrade your Infisical plan to an Enterprise license."
});
} else {
handlePopUpOpen("group");

View File

@@ -25,7 +25,7 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a
import { usePopUp } from "@app/hooks";
import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { RoleModal } from "@app/pages/organization/RoleByIDPage/components";
import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal";
export const ProjectRoleList = () => {
const navigate = useNavigate();

View File

@@ -201,7 +201,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...[]],
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {

View File

@@ -173,7 +173,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...[]],
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {

View File

@@ -98,7 +98,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...[]],
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {

View File

@@ -32,7 +32,8 @@ const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]
shouldEnableDelete: "GitHub Secret Deletion Enabled",
octopusDeployScopeValues: "Octopus Deploy Scope Values",
awsIamRole: "AWS IAM Role",
region: "Region"
region: "Region",
metadataSyncMode: "Metadata Sync Mode"
} as const;
export const IntegrationSettingsSection = ({ integration }: Props) => {

View File

@@ -80,8 +80,16 @@ export const redirectForProviderAuth = (
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
return;
}
const link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
window.location.assign(link);
navigate({
to: "/secret-manager/$projectId/integrations/azure-key-vault/authorize",
params: {
projectId
},
search: {
clientId: integrationOption.clientId,
state,
}
});
break;
}
case "azure-app-configuration": {

View File

@@ -1,4 +1,4 @@
import { faExclamationTriangle, faInfo } from "@fortawesome/free-solid-svg-icons";
import { faExclamationTriangle, faInfo, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
@@ -18,7 +18,10 @@ import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/h
export type Props = {
op: CommitType;
secretVersion?: SecretV3Raw;
newVersion?: Omit<TSecretApprovalSecChange, "tags"> & { tags?: WsTag[] };
newVersion?: Omit<TSecretApprovalSecChange, "tags"> & {
tags?: WsTag[];
secretMetadata?: { key: string; value: string }[];
};
presentSecretVersionNumber: number;
hasMerged?: boolean;
conflicts: Array<{ secretId: string; op: CommitType }>;
@@ -80,11 +83,12 @@ export const SecretApprovalRequestChangeItem = ({
<Table>
<THead>
<Tr>
{op === CommitType.UPDATE && <Th className="w-12" />}
<Th className="min-table-row">Secret</Th>
<Th>Value</Th>
<Th className="min-table-row">Comment</Th>
<Th className="min-table-row">Tags</Th>
{op === CommitType.UPDATE && <Th className="w-12 shrink-0" />}
<Th className="w-48 shrink-0">Secret</Th>
<Th className="min-w-0 flex-1">Value</Th>
<Th className="w-24 shrink-0">Comment</Th>
<Th className="w-24 shrink-0">Tags</Th>
<Th className="w-40 shrink-0">Metadata</Th>
</Tr>
</THead>
{op === CommitType.UPDATE ? (
@@ -110,6 +114,33 @@ export const SecretApprovalRequestChangeItem = ({
</Tag>
))}
</Td>
<Td>
{secretVersion?.secretMetadata?.length ? (
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
{secretVersion.secretMetadata?.map((el) => (
<div key={el.key} className="flex items-center">
<Tag
size="xs"
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
>
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div>
</Tag>
<Tag
size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
>
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tag>
</div>
))}
</div>
) : (
<p className="text-sm text-mineshaft-300">-</p>
)}
</Td>
</Tr>
<Tr>
<Td className="text-green-600">NEW</Td>
@@ -132,6 +163,33 @@ export const SecretApprovalRequestChangeItem = ({
</Tag>
))}
</Td>
<Td>
{newVersion?.secretMetadata?.length ? (
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
{newVersion.secretMetadata?.map((el) => (
<div key={el.key} className="flex items-center">
<Tag
size="xs"
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
>
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div>
</Tag>
<Tag
size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
>
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tag>
</div>
))}
</div>
) : (
<p className="text-sm text-mineshaft-300">-</p>
)}
</Td>
</Tr>
</TBody>
) : (
@@ -173,6 +231,33 @@ export const SecretApprovalRequestChangeItem = ({
)
)}
</Td>
<Td>
{newVersion?.secretMetadata?.length ? (
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
{newVersion.secretMetadata?.map((el) => (
<div key={el.key} className="flex items-center">
<Tag
size="xs"
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
>
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div>
</Tag>
<Tag
size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
>
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tag>
</div>
))}
</div>
) : (
<p className="text-sm text-mineshaft-300">-</p>
)}
</Td>
</Tr>
</TBody>
)}

View File

@@ -8,7 +8,8 @@ import {
faClock,
faPlus,
faShare,
faTag
faTag,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -26,6 +27,7 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
FormControl,
FormLabel,
IconButton,
Input,
Switch,
@@ -87,11 +89,16 @@ export const SecretDetailSidebar = ({
const { permission } = useProjectPermission();
const { fields, append, remove } = useFieldArray({
const tagFields = useFieldArray({
control,
name: "tags"
});
const metadataFormFields = useFieldArray({
control,
name: "secretMetadata"
});
const secretKey = secret?.key || "";
const selectedTags = watch("tags", []) || [];
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
@@ -153,10 +160,10 @@ export const SecretDetailSidebar = ({
if (selectedTagsGroupById?.[tag.id]) {
const tagPos = selectedTags.findIndex(({ id }) => id === tag.id);
if (tagPos !== -1) {
remove(tagPos);
tagFields.remove(tagPos);
}
} else {
append(tag);
tagFields.append(tag);
}
};
@@ -277,9 +284,73 @@ export const SecretDetailSidebar = ({
)}
/>
)}
<FormControl label="Metadata">
<div className="flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2">
<div className="flex-grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`secretMetadata.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<div className="flex-grow">
{i === 0 && (
<FormLabel
label="Value"
className="text-xs text-mineshaft-400"
isOptional
/>
)}
<Controller
control={control}
name={`secretMetadata.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete key"
className="bottom-0.5 max-h-8"
variant="outline_bg"
onClick={() => metadataFormFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div className="mt-2">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => metadataFormFields.append({ key: "", value: "" })}
>
Add Key
</Button>
</div>
</div>
</FormControl>
<FormControl label="Tags" className="">
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
{fields.map(({ tagColor, id: formId, slug, id }) => (
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}

View File

@@ -78,7 +78,8 @@ export const SecretListView = ({
tags,
skipMultilineEncoding,
newKey,
secretId
secretId,
secretMetadata
}: Partial<{
value: string;
comment: string;
@@ -88,6 +89,7 @@ export const SecretListView = ({
skipMultilineEncoding: boolean;
newKey: string;
secretId: string;
secretMetadata?: { key: string; value: string }[];
}> = {}
) => {
if (operation === "delete") {
@@ -115,7 +117,8 @@ export const SecretListView = ({
secretReminderRepeatDays: reminderRepeatDays,
secretReminderNote: reminderNote,
skipMultilineEncoding,
newSecretName: newKey
newSecretName: newKey,
secretMetadata
});
return;
}
@@ -138,7 +141,10 @@ export const SecretListView = ({
const handleSaveSecret = useCallback(
async (
orgSecret: SecretV3RawSanitized,
modSecret: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
modSecret: Omit<SecretV3RawSanitized, "tags"> & {
tags?: { id: string }[];
secretMetadata?: { key: string; value: string }[];
},
cb?: () => void
) => {
const { key: oldKey } = orgSecret;
@@ -151,7 +157,8 @@ export const SecretListView = ({
tags,
comment,
reminderRepeatDays,
reminderNote
reminderNote,
secretMetadata
} = modSecret;
const hasKeyChanged = oldKey !== key && key;
@@ -166,7 +173,8 @@ export const SecretListView = ({
"comment",
"skipMultilineEncoding",
"reminderRepeatDays",
"reminderNote"
"reminderNote",
"secretMetadata"
] as const
).every((el) => orgSecret[el] === modSecret[el]) && isSameTags;
@@ -199,7 +207,8 @@ export const SecretListView = ({
reminderNote,
secretId: orgSecret.id,
newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding
skipMultilineEncoding: modSecret.skipMultilineEncoding,
secretMetadata
});
if (cb) cb();
}

View File

@@ -47,7 +47,13 @@ export const formSchema = z.object({
.nullable()
.optional(),
reminderNote: z.string().trim().nullable().optional(),
secretMetadata: z
.object({
key: z.string().trim().min(1),
value: z.string().trim().default("")
})
.array()
.optional(),
tags: z
.object({
id: z.string(),

View File

@@ -34,7 +34,10 @@ import { useWorkspace } from "@app/context";
import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
import {
IntegrationMappingBehavior,
IntegrationMetadataSyncMode
} from "@app/hooks/api/integrations/types";
enum TabSections {
Connection = "connection",
@@ -94,6 +97,7 @@ const schema = z
mappingBehavior: z.nativeEnum(IntegrationMappingBehavior),
kmsKeyId: z.string().optional(),
shouldTag: z.boolean().optional(),
metadataSyncMode: z.nativeEnum(IntegrationMetadataSyncMode).optional(),
tags: z
.object({
key: z.string(),
@@ -136,6 +140,7 @@ export const AwsSecretManagerConfigurePage = () => {
});
const shouldTagState = watch("shouldTag");
const selectedMetadataSyncMode = watch("metadataSyncMode");
const selectedSourceEnvironment = watch("sourceEnvironment");
const selectedAWSRegion = watch("awsRegion");
const selectedMappingBehavior = watch("mappingBehavior");
@@ -171,7 +176,8 @@ export const AwsSecretManagerConfigurePage = () => {
tags,
secretPrefix,
kmsKeyId,
mappingBehavior
mappingBehavior,
metadataSyncMode
}: TFormSchema) => {
try {
if (!integrationAuth?.id) return;
@@ -186,7 +192,8 @@ export const AwsSecretManagerConfigurePage = () => {
metadata: {
...(shouldTag
? {
secretAWSTag: tags
secretAWSTag: tags,
metadataSyncMode
}
: {}),
...(secretPrefix && { secretPrefix }),
@@ -296,7 +303,7 @@ export const AwsSecretManagerConfigurePage = () => {
errorText={error?.message}
isError={Boolean(error)}
>
<SecretPathInput {...field} />
<SecretPathInput {...field} environment={selectedSourceEnvironment} />
</FormControl>
)}
/>
@@ -339,7 +346,12 @@ export const AwsSecretManagerConfigurePage = () => {
>
<Select
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
onValueChange={(e) => {
if (e === IntegrationMappingBehavior.MANY_TO_ONE) {
setValue("metadataSyncMode", IntegrationMetadataSyncMode.CUSTOM);
}
onChange(e);
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-full"
>
@@ -386,14 +398,25 @@ export const AwsSecretManagerConfigurePage = () => {
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="ml-1 mt-2">
<div className="mb-3 ml-1 mt-2">
<Controller
control={control}
name="shouldTag"
render={({ field: { onChange, value } }) => (
<Switch
id="tag-aws"
onCheckedChange={(isChecked) => onChange(isChecked)}
onCheckedChange={(isChecked) => {
if (
isChecked &&
selectedMappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE
) {
setValue(
"metadataSyncMode",
IntegrationMetadataSyncMode.SECRET_METADATA
);
}
onChange(isChecked);
}}
isChecked={value}
>
Tag in AWS Secrets Manager
@@ -401,37 +424,76 @@ export const AwsSecretManagerConfigurePage = () => {
)}
/>
</div>
{shouldTagState && (
<div className="mt-4 flex justify-between">
{shouldTagState &&
selectedMappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE && (
<Controller
control={control}
name="tags.0.key"
render={({ field, fieldState: { error } }) => (
name="metadataSyncMode"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Tag Key"
label="Tag Sync Mode"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="managed-by" {...field} />
<Select
defaultValue={field.value}
onValueChange={(e) => {
setValue("tags", []);
onChange(e);
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-full"
>
<SelectItem
value={IntegrationMetadataSyncMode.SECRET_METADATA}
className="text-left"
key={`sync-mode-${IntegrationMetadataSyncMode.SECRET_METADATA}`}
>
Secret Metadata
</SelectItem>
<SelectItem
value={IntegrationMetadataSyncMode.CUSTOM}
className="text-left"
key={`sync-mode-${IntegrationMetadataSyncMode.CUSTOM}`}
>
Custom
</SelectItem>
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="tags.0.value"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tag Value"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="infisical" {...field} />
</FormControl>
)}
/>
</div>
)}
)}
{shouldTagState &&
selectedMetadataSyncMode === IntegrationMetadataSyncMode.CUSTOM && (
<div className="mt-4 flex justify-between">
<Controller
control={control}
name="tags.0.key"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tag Key"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="managed-by" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="tags.0.value"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tag Value"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="infisical" {...field} />
</FormControl>
)}
/>
</div>
)}
<Controller
control={control}
name="secretPrefix"

View File

@@ -76,6 +76,7 @@ export const AzureAppConfigurationConfigurePage = () => {
}
});
const selectedEnvironment = watch("sourceEnvironment");
const { mutateAsync } = useCreateIntegration();
const integrationAuthId = useSearch({
@@ -248,7 +249,7 @@ export const AzureAppConfigurationConfigurePage = () => {
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
<SecretPathInput {...field} />
<SecretPathInput {...field} environment={selectedEnvironment} />
</FormControl>
)}
/>

View File

@@ -6,7 +6,7 @@ import { AzureAppConfigurationOauthCallbackPage } from "./AzureAppConfigurationO
export const AzureAppConfigurationOauthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

View File

@@ -0,0 +1,97 @@
import { Controller, useForm } from "react-hook-form";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { Helmet } from "react-helmet";
import { useSearch } from "@tanstack/react-router";
import { ROUTE_PATHS } from "@app/const/routes";
const schema = z.object({
tenantId: z.string().trim().optional()
});
type FormData = z.infer<typeof schema>;
export function AzureKeyVaultAuthorizePage() {
const { state, clientId } = useSearch({
from: ROUTE_PATHS.SecretManager.Integratons.AzureKeyVaultAuthorizePage.id,
});
const { control, handleSubmit } = useForm<FormData>({
resolver: zodResolver(schema)
});
const onFormSubmit = async ({ tenantId }: FormData) => {
const link = `https://login.microsoftonline.com/${
tenantId ?? "common"
}/oauth2/v2.0/authorize?client_id=${clientId}&response_type=code&redirect_uri=${
window.location.origin
}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
window.location.assign(link);
};
return (
<div className="flex h-full w-full items-center justify-center">
<Helmet>
<title>Authorize Azure Key Vault Integration</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle="Authenticate with a specific tenant ID or let OAuth handle it automatically."
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<img src="/images/integrations/GitHub.png" height={30} width={30} alt="Github logo" />
</div>
<span className="ml-2.5">Azure Key Vault Integration </span>
<a
href="https://infisical.com/docs/integrations/cloud/azure-key-vault"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</div>
</CardTitle>
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6 pb-8 text-right">
<Controller
control={control}
name="tenantId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tenant ID (optional)"
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="2e39537c-9a01-4bd6-a7b8-c3b88cbb8db9" />
</FormControl>
)}
/>
<Button
colorSchema="primary"
variant="outline_bg"
className="mt-2 w-min"
size="sm"
type="submit"
>
Connect to Azure Key Vault
</Button>
</form>
</Card>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { createFileRoute } from "@tanstack/react-router";
import z from 'zod'
import { AzureKeyVaultAuthorizePage } from "./AzureKeyVaultAuthorizePage";
const PageQueryParamsSchema = z.object({
state: z.string(),
clientId: z.string().optional(),
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
)({
component: AzureKeyVaultAuthorizePage,
validateSearch: PageQueryParamsSchema
});

View File

@@ -6,7 +6,7 @@ import { AzureKeyVaultOauthCallbackPage } from "./AzureKeyVaultOauthCallback";
export const AzureKeyVaultOauthCallbackQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

View File

@@ -6,7 +6,7 @@ import { BitbucketOauthCallbackPage } from "./BitbucketOauthCallbackPage";
export const BitbucketOauthCallbackQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

View File

@@ -62,6 +62,7 @@ export const CircleCIConfigurePage = () => {
const selectedScope = watch("scope");
const selectedOrg = watch("targetOrg");
const selectedEnvironment = watch("sourceEnvironment");
const { data: circleCIOrganizations, isPending: isCircleCIOrganizationsLoading } =
useGetIntegrationAuthCircleCIOrganizations(integrationAuthId);
@@ -186,7 +187,7 @@ export const CircleCIConfigurePage = () => {
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
<SecretPathInput {...field} />
<SecretPathInput {...field} environment={selectedEnvironment.slug}/>
</FormControl>
)}
/>

View File

@@ -6,7 +6,7 @@ import { GcpSecretManagerOauthCallbackPage } from "./GcpSecretManagerOauthCallba
export const GcpSecretManagerOAuthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

View File

@@ -6,8 +6,8 @@ import { GithubOauthCallbackPage } from "./GithubOauthCallbackPage";
export const GithubOAuthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
installation_id: z.coerce.string().catch(""),
code: z.string().catch("")
installation_id: z.coerce.string().optional().catch(""),
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

View File

@@ -72,7 +72,7 @@ export const GitlabAuthorizePage = () => {
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<img src="/images/integrations/Gitlab.png" height={28} width={28} alt="Gitlab logo" />
<img src="/images/integrations/GitLab.png" height={28} width={28} alt="Gitlab logo" />
</div>
<span className="ml-2.5">GitLab Integration </span>
<a

View File

@@ -197,7 +197,7 @@ export const GitlabConfigurePage = () => {
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<img src="/images/integrations/Gitlab.png" height={28} width={28} alt="Gitlab logo" />
<img src="/images/integrations/GitLab.png" height={28} width={28} alt="Gitlab logo" />
</div>
<span className="ml-2.5">GitLab Integration </span>
<a

View File

@@ -6,7 +6,7 @@ import { GitLabOAuthCallbackPage } from "./GitlabOauthCallbackPage";
export const GitlabOAuthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

View File

@@ -6,7 +6,7 @@ import { HerokuOAuthCallbackPage } from "./HerokuOauthCallbackPage";
export const HerokuOAuthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

Some files were not shown because too many files have changed in this diff Show More