mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-07 17:58:25 +00:00
Compare commits
27 Commits
infisical/
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
c421057cf1 | ||
|
8df4616265 | ||
|
484f34a257 | ||
|
32851565a7 | ||
|
68401a799e | ||
|
0adf2c830d | ||
|
c68138ac21 | ||
|
d4f0301104 | ||
|
253c46f21d | ||
|
d8e39aed16 | ||
|
72ee468208 | ||
|
18238b46a7 | ||
|
d0ffae2c10 | ||
|
7ce11cde95 | ||
|
af32948a05 | ||
|
25753fc995 | ||
|
cd71848800 | ||
|
4afc7a1981 | ||
|
11ca76ccca | ||
|
418aca8af0 | ||
|
7365f60835 | ||
|
929822514e | ||
|
616ccb97f2 | ||
|
7917a767e6 | ||
|
83e59ae160 | ||
|
5af939992c | ||
|
aec4ee905e |
@@ -145,7 +145,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
||||
&& cd openssl-3.1.2 \
|
||||
&& ./Configure enable-fips \
|
||||
&& make \
|
||||
&& make install_fips
|
||||
&& make install_fips \
|
||||
&& cd / \
|
||||
&& rm -rf /openssl-build \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
|
||||
@@ -186,12 +190,11 @@ ENV NODE_ENV production
|
||||
ENV STANDALONE_BUILD true
|
||||
ENV STANDALONE_MODE true
|
||||
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||
ENV NODE_OPTIONS="--max-old-space-size=8192 --force-fips"
|
||||
|
||||
# FIPS mode of operation:
|
||||
ENV OPENSSL_CONF=/backend/nodejs.fips.cnf
|
||||
ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules
|
||||
ENV NODE_OPTIONS=--force-fips
|
||||
ENV FIPS_ENABLED=true
|
||||
|
||||
|
||||
|
@@ -59,7 +59,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
||||
&& cd openssl-3.1.2 \
|
||||
&& ./Configure enable-fips \
|
||||
&& make \
|
||||
&& make install_fips
|
||||
&& make install_fips \
|
||||
&& cd / \
|
||||
&& rm -rf /openssl-build \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# ? App setup
|
||||
|
||||
|
@@ -0,0 +1,111 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { chunkArray } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { TReminders, TRemindersInsert } from "../schemas/reminders";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
logger.info("Initializing secret reminders migration");
|
||||
const hasReminderTable = await knex.schema.hasTable(TableName.Reminder);
|
||||
|
||||
if (hasReminderTable) {
|
||||
const secretsWithLatestVersions = await knex(TableName.SecretV2)
|
||||
.whereNotNull(`${TableName.SecretV2}.reminderRepeatDays`)
|
||||
.whereRaw(`"${TableName.SecretV2}"."reminderRepeatDays" > 0`)
|
||||
.innerJoin(TableName.SecretVersionV2, (qb) => {
|
||||
void qb
|
||||
.on(`${TableName.SecretVersionV2}.secretId`, "=", `${TableName.SecretV2}.id`)
|
||||
.andOn(`${TableName.SecretVersionV2}.reminderRepeatDays`, "=", `${TableName.SecretV2}.reminderRepeatDays`);
|
||||
})
|
||||
.whereIn([`${TableName.SecretVersionV2}.secretId`, `${TableName.SecretVersionV2}.version`], (qb) => {
|
||||
void qb
|
||||
.select(["v2.secretId", knex.raw("MIN(v2.version) as version")])
|
||||
.from(`${TableName.SecretVersionV2} as v2`)
|
||||
.innerJoin(`${TableName.SecretV2} as s2`, "v2.secretId", "s2.id")
|
||||
.whereRaw(`v2."reminderRepeatDays" = s2."reminderRepeatDays"`)
|
||||
.whereNotNull("v2.reminderRepeatDays")
|
||||
.whereRaw(`v2."reminderRepeatDays" > 0`)
|
||||
.groupBy("v2.secretId");
|
||||
})
|
||||
// Add LEFT JOIN with Reminder table to check for existing reminders
|
||||
.leftJoin(TableName.Reminder, `${TableName.Reminder}.secretId`, `${TableName.SecretV2}.id`)
|
||||
// Only include secrets that don't already have reminders
|
||||
.whereNull(`${TableName.Reminder}.secretId`)
|
||||
.select(
|
||||
knex.ref("id").withSchema(TableName.SecretV2).as("secretId"),
|
||||
knex.ref("reminderRepeatDays").withSchema(TableName.SecretV2).as("reminderRepeatDays"),
|
||||
knex.ref("reminderNote").withSchema(TableName.SecretV2).as("reminderNote"),
|
||||
knex.ref("createdAt").withSchema(TableName.SecretVersionV2).as("createdAt")
|
||||
);
|
||||
|
||||
logger.info(`Found ${secretsWithLatestVersions.length} reminders to migrate`);
|
||||
|
||||
const reminderInserts: TRemindersInsert[] = [];
|
||||
if (secretsWithLatestVersions.length > 0) {
|
||||
secretsWithLatestVersions.forEach((secret) => {
|
||||
if (!secret.reminderRepeatDays) return;
|
||||
|
||||
const now = new Date();
|
||||
const createdAt = new Date(secret.createdAt);
|
||||
let nextReminderDate = new Date(createdAt);
|
||||
nextReminderDate.setDate(nextReminderDate.getDate() + secret.reminderRepeatDays);
|
||||
|
||||
// If the next reminder date is in the past, calculate the proper next occurrence
|
||||
if (nextReminderDate < now) {
|
||||
const daysSinceCreation = Math.floor((now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24));
|
||||
const daysIntoCurrentCycle = daysSinceCreation % secret.reminderRepeatDays;
|
||||
const daysUntilNextReminder = secret.reminderRepeatDays - daysIntoCurrentCycle;
|
||||
|
||||
nextReminderDate = new Date(now);
|
||||
nextReminderDate.setDate(now.getDate() + daysUntilNextReminder);
|
||||
}
|
||||
|
||||
reminderInserts.push({
|
||||
secretId: secret.secretId,
|
||||
message: secret.reminderNote,
|
||||
repeatDays: secret.reminderRepeatDays,
|
||||
nextReminderDate
|
||||
});
|
||||
});
|
||||
|
||||
const commitBatches = chunkArray(reminderInserts, 2000);
|
||||
for (const commitBatch of commitBatches) {
|
||||
const insertedReminders = (await knex
|
||||
.batchInsert(TableName.Reminder, commitBatch)
|
||||
.returning("*")) as TReminders[];
|
||||
|
||||
const insertedReminderSecretIds = insertedReminders.map((reminder) => reminder.secretId).filter(Boolean);
|
||||
|
||||
const recipients = await knex(TableName.SecretReminderRecipients)
|
||||
.whereRaw(`??.?? IN (${insertedReminderSecretIds.map(() => "?").join(",")})`, [
|
||||
TableName.SecretReminderRecipients,
|
||||
"secretId",
|
||||
...insertedReminderSecretIds
|
||||
])
|
||||
.select(
|
||||
knex.ref("userId").withSchema(TableName.SecretReminderRecipients).as("userId"),
|
||||
knex.ref("secretId").withSchema(TableName.SecretReminderRecipients).as("secretId")
|
||||
);
|
||||
const reminderRecipients = recipients.map((recipient) => ({
|
||||
reminderId: insertedReminders.find((reminder) => reminder.secretId === recipient.secretId)?.id,
|
||||
userId: recipient.userId
|
||||
}));
|
||||
|
||||
const filteredRecipients = reminderRecipients.filter((recipient) => Boolean(recipient.reminderId));
|
||||
await knex.batchInsert(TableName.ReminderRecipient, filteredRecipients);
|
||||
}
|
||||
logger.info(`Successfully migrated ${reminderInserts.length} secret reminders`);
|
||||
}
|
||||
|
||||
logger.info("Secret reminders migration completed");
|
||||
} else {
|
||||
logger.warn("Reminder table does not exist, skipping migration");
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
logger.info("Rollback not implemented for secret reminders fix migration");
|
||||
}
|
@@ -53,7 +53,7 @@ export const getMigrationEnvConfig = async (superAdminDAL: TSuperAdminDALFactory
|
||||
|
||||
let envCfg = Object.freeze(parsedEnv.data);
|
||||
|
||||
const fipsEnabled = await crypto.initialize(superAdminDAL);
|
||||
const fipsEnabled = await crypto.initialize(superAdminDAL, envCfg);
|
||||
|
||||
// Fix for 128-bit entropy encryption key expansion issue:
|
||||
// In FIPS it is not ideal to expand a 128-bit key into 256-bit. We solved this issue in the past by creating the ROOT_ENCRYPTION_KEY.
|
||||
|
@@ -7,12 +7,13 @@ import {
|
||||
TRotationFactoryRevokeCredentials,
|
||||
TRotationFactoryRotateCredentials
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
executeWithPotentialGateway,
|
||||
SQL_CONNECTION_ALTER_LOGIN_STATEMENT
|
||||
} from "@app/services/app-connection/shared/sql";
|
||||
|
||||
import { generatePassword } from "../utils";
|
||||
import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../utils";
|
||||
import {
|
||||
TSqlCredentialsRotationGeneratedCredentials,
|
||||
TSqlCredentialsRotationWithConnection
|
||||
@@ -32,6 +33,11 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
|
||||
return redactedMessage;
|
||||
};
|
||||
|
||||
const ORACLE_PASSWORD_REQUIREMENTS = {
|
||||
...DEFAULT_PASSWORD_REQUIREMENTS,
|
||||
length: 30
|
||||
};
|
||||
|
||||
export const sqlCredentialsRotationFactory: TRotationFactory<
|
||||
TSqlCredentialsRotationWithConnection,
|
||||
TSqlCredentialsRotationGeneratedCredentials
|
||||
@@ -43,6 +49,9 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
||||
secretsMapping
|
||||
} = secretRotation;
|
||||
|
||||
const passwordRequirement =
|
||||
connection.app === AppConnection.OracleDB ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
|
||||
const executeOperation = <T>(
|
||||
operation: (client: Knex) => Promise<T>,
|
||||
credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number]
|
||||
@@ -65,7 +74,7 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
||||
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
|
||||
try {
|
||||
await executeOperation(async (client) => {
|
||||
await client.raw("SELECT 1");
|
||||
await client.raw(connection.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`);
|
||||
}, credentials);
|
||||
} catch (error) {
|
||||
throw new Error(redactPasswords(error, [credentials]));
|
||||
@@ -75,11 +84,13 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
// For SQL, since we get existing users, we change both their passwords
|
||||
// on issue to invalidate their existing passwords
|
||||
// For SQL, since we get existing users, we change both their passwords
|
||||
// on issue to invalidate their existing passwords
|
||||
const credentialsSet = [
|
||||
{ username: username1, password: generatePassword() },
|
||||
{ username: username2, password: generatePassword() }
|
||||
{ username: username1, password: generatePassword(passwordRequirement) },
|
||||
{ username: username2, password: generatePassword(passwordRequirement) }
|
||||
];
|
||||
|
||||
try {
|
||||
@@ -105,7 +116,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
||||
credentialsToRevoke,
|
||||
callback
|
||||
) => {
|
||||
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
|
||||
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({
|
||||
username,
|
||||
password: generatePassword(passwordRequirement)
|
||||
}));
|
||||
|
||||
try {
|
||||
await executeOperation(async (client) => {
|
||||
@@ -128,7 +142,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
||||
callback
|
||||
) => {
|
||||
// generate new password for the next active user
|
||||
const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() };
|
||||
const credentials = {
|
||||
username: activeIndex === 0 ? username2 : username1,
|
||||
password: generatePassword(passwordRequirement)
|
||||
};
|
||||
|
||||
try {
|
||||
await executeOperation(async (client) => {
|
||||
|
@@ -11,7 +11,7 @@ type TPasswordRequirements = {
|
||||
allowedSymbols?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
|
||||
export const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
|
@@ -2245,7 +2245,9 @@ export const AppConnections = {
|
||||
},
|
||||
AZURE_CLIENT_SECRETS: {
|
||||
code: "The OAuth code to use to connect with Azure Client Secrets.",
|
||||
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
|
||||
tenantId: "The Tenant ID to use to connect with Azure Client Secrets.",
|
||||
clientId: "The Client ID to use to connect with Azure Client Secrets.",
|
||||
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
|
||||
},
|
||||
AZURE_DEVOPS: {
|
||||
code: "The OAuth code to use to connect with Azure DevOps.",
|
||||
@@ -2373,6 +2375,10 @@ export const SecretSyncs = {
|
||||
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||
tags: "Optional tags to add to secrets synced by Infisical.",
|
||||
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.`
|
||||
},
|
||||
RENDER: {
|
||||
autoRedeployServices:
|
||||
"Whether Infisical should automatically redeploy the configured Render service upon secret changes."
|
||||
}
|
||||
},
|
||||
DESTINATION_CONFIG: {
|
||||
|
@@ -14,7 +14,7 @@ import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal
|
||||
import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
import { isBase64 } from "../../base64";
|
||||
import { getConfig } from "../../config/env";
|
||||
import { getConfig, TEnvConfig } from "../../config/env";
|
||||
import { CryptographyError } from "../../errors";
|
||||
import { logger } from "../../logger";
|
||||
import { asymmetricFipsValidated } from "./asymmetric-fips";
|
||||
@@ -106,12 +106,12 @@ const cryptographyFactory = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const $setFipsModeEnabled = (enabled: boolean) => {
|
||||
const $setFipsModeEnabled = (enabled: boolean, envCfg?: Pick<TEnvConfig, "ENCRYPTION_KEY">) => {
|
||||
// If FIPS is enabled, we need to validate that the ENCRYPTION_KEY is in a base64 format, and is a 256-bit key.
|
||||
if (enabled) {
|
||||
crypto.setFips(true);
|
||||
|
||||
const appCfg = getConfig();
|
||||
const appCfg = envCfg || getConfig();
|
||||
|
||||
if (appCfg.ENCRYPTION_KEY) {
|
||||
// we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key
|
||||
@@ -141,14 +141,14 @@ const cryptographyFactory = () => {
|
||||
$isInitialized = true;
|
||||
};
|
||||
|
||||
const initialize = async (superAdminDAL: TSuperAdminDALFactory) => {
|
||||
const initialize = async (superAdminDAL: TSuperAdminDALFactory, envCfg?: Pick<TEnvConfig, "ENCRYPTION_KEY">) => {
|
||||
if ($isInitialized) {
|
||||
return isFipsModeEnabled();
|
||||
}
|
||||
|
||||
if (process.env.FIPS_ENABLED !== "true") {
|
||||
logger.info("Cryptography module initialized in normal operation mode.");
|
||||
$setFipsModeEnabled(false);
|
||||
$setFipsModeEnabled(false, envCfg);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -158,11 +158,11 @@ const cryptographyFactory = () => {
|
||||
if (serverCfg) {
|
||||
if (serverCfg.fipsEnabled) {
|
||||
logger.info("[FIPS]: Instance is configured for FIPS mode of operation. Continuing startup with FIPS enabled.");
|
||||
$setFipsModeEnabled(true);
|
||||
$setFipsModeEnabled(true, envCfg);
|
||||
return true;
|
||||
}
|
||||
logger.info("[FIPS]: Instance age predates FIPS mode inception date. Continuing without FIPS.");
|
||||
$setFipsModeEnabled(false);
|
||||
$setFipsModeEnabled(false, envCfg);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ const cryptographyFactory = () => {
|
||||
// TODO(daniel): check if it's an enterprise deployment
|
||||
|
||||
// if there is no server cfg, and FIPS_MODE is `true`, its a fresh FIPS deployment. We need to set the fipsEnabled to true.
|
||||
$setFipsModeEnabled(true);
|
||||
$setFipsModeEnabled(true, envCfg);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
export enum AzureClientSecretsConnectionMethod {
|
||||
OAuth = "oauth"
|
||||
OAuth = "oauth",
|
||||
ClientSecret = "client-secret"
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -16,6 +17,7 @@ import { AppConnection } from "../app-connection-enums";
|
||||
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
|
||||
import {
|
||||
ExchangeCodeAzureResponse,
|
||||
TAzureClientSecretsConnectionClientSecretCredentials,
|
||||
TAzureClientSecretsConnectionConfig,
|
||||
TAzureClientSecretsConnectionCredentials
|
||||
} from "./azure-client-secrets-connection-types";
|
||||
@@ -26,7 +28,10 @@ export const getAzureClientSecretsConnectionListItem = () => {
|
||||
return {
|
||||
name: "Azure Client Secrets" as const,
|
||||
app: AppConnection.AzureClientSecrets as const,
|
||||
methods: Object.values(AzureClientSecretsConnectionMethod) as [AzureClientSecretsConnectionMethod.OAuth],
|
||||
methods: Object.values(AzureClientSecretsConnectionMethod) as [
|
||||
AzureClientSecretsConnectionMethod.OAuth,
|
||||
AzureClientSecretsConnectionMethod.ClientSecret
|
||||
],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||
};
|
||||
};
|
||||
@@ -37,12 +42,6 @@ export const getAzureConnectionAccessToken = async (
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||
throw new BadRequestError({
|
||||
message: `Azure environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
|
||||
if (!appConnection) {
|
||||
@@ -63,34 +62,81 @@ export const getAzureConnectionAccessToken = async (
|
||||
|
||||
const { refreshToken } = credentials;
|
||||
const currentTime = Date.now();
|
||||
switch (appConnection.method) {
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||
throw new BadRequestError({
|
||||
message: `Azure OAuth environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
scope: `openid offline_access https://graph.microsoft.com/.default`,
|
||||
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
refresh_token: refreshToken
|
||||
})
|
||||
);
|
||||
|
||||
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
scope: `openid offline_access https://graph.microsoft.com/.default`,
|
||||
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
refresh_token: refreshToken
|
||||
})
|
||||
);
|
||||
const updatedCredentials = {
|
||||
...credentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: currentTime + data.expires_in * 1000,
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
|
||||
const updatedCredentials = {
|
||||
...credentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: currentTime + data.expires_in * 1000,
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||
return data.access_token;
|
||||
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||
const accessTokenCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureClientSecretsConnectionClientSecretCredentials;
|
||||
const { accessToken, expiresAt, clientId, clientSecret, tenantId } = accessTokenCredentials;
|
||||
if (accessToken && expiresAt && expiresAt > currentTime + 300000) {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
return data.access_token;
|
||||
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: `https://graph.microsoft.com/.default`,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
})
|
||||
);
|
||||
|
||||
const updatedClientCredentials = {
|
||||
...accessTokenCredentials,
|
||||
accessToken: clientData.access_token,
|
||||
expiresAt: currentTime + clientData.expires_in * 1000
|
||||
};
|
||||
|
||||
const encryptedClientCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedClientCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });
|
||||
|
||||
return clientData.access_token;
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
message: `Unhandled Azure connection method: ${appConnection.method as AzureClientSecretsConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => {
|
||||
@@ -98,69 +144,103 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA
|
||||
|
||||
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
||||
|
||||
if (!SITE_URL) {
|
||||
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
|
||||
}
|
||||
|
||||
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||
throw new InternalServerError({
|
||||
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||
let tokenError: AxiosError | null = null;
|
||||
|
||||
try {
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: inputCredentials.code,
|
||||
scope: `openid offline_access https://graph.microsoft.com/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenError) {
|
||||
if (tokenError instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get access token: ${
|
||||
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get access token"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!tokenResp) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to get access token: Token was empty with no error`
|
||||
});
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
if (!SITE_URL) {
|
||||
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
|
||||
}
|
||||
|
||||
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||
throw new InternalServerError({
|
||||
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||
let tokenError: AxiosError | null = null;
|
||||
|
||||
try {
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: inputCredentials.code,
|
||||
scope: `openid offline_access https://graph.microsoft.com/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenError) {
|
||||
if (tokenError instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get access token: ${
|
||||
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get access token"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!tokenResp) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to get access token: Token was empty with no error`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tenantId: inputCredentials.tenantId,
|
||||
accessToken: tokenResp.data.access_token,
|
||||
refreshToken: tokenResp.data.refresh_token,
|
||||
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||
};
|
||||
|
||||
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||
const { tenantId, clientId, clientSecret } = inputCredentials;
|
||||
try {
|
||||
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: `https://graph.microsoft.com/.default`,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
tenantId,
|
||||
accessToken: clientData.access_token,
|
||||
expiresAt: Date.now() + clientData.expires_in * 1000,
|
||||
clientId,
|
||||
clientSecret
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get access token: ${
|
||||
(e?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get access token"
|
||||
});
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
|
||||
|
@@ -26,6 +26,36 @@ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const AzureClientSecretsConnectionClientSecretInputCredentialsSchema = z.object({
|
||||
clientId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.trim()
|
||||
.min(1, "Client ID required")
|
||||
.max(50, "Client ID must be at most 50 characters long")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientId),
|
||||
clientSecret: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client Secret required")
|
||||
.max(50, "Client Secret must be at most 50 characters long")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientSecret),
|
||||
tenantId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.trim()
|
||||
.min(1, "Tenant ID required")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId)
|
||||
});
|
||||
|
||||
export const AzureClientSecretsConnectionClientSecretOutputCredentialsSchema = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
tenantId: z.string(),
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
@@ -34,6 +64,14 @@ export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discrimin
|
||||
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
method: z
|
||||
.literal(AzureClientSecretsConnectionMethod.ClientSecret)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureClientSecrets).method),
|
||||
credentials: AzureClientSecretsConnectionClientSecretInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -43,9 +81,13 @@ export const CreateAzureClientSecretsConnectionSchema = ValidateAzureClientSecre
|
||||
|
||||
export const UpdateAzureClientSecretsConnectionSchema = z
|
||||
.object({
|
||||
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials
|
||||
)
|
||||
credentials: z
|
||||
.union([
|
||||
AzureClientSecretsConnectionOAuthInputCredentialsSchema,
|
||||
AzureClientSecretsConnectionClientSecretInputCredentialsSchema
|
||||
])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
|
||||
|
||||
@@ -59,6 +101,10 @@ export const AzureClientSecretsConnectionSchema = z.intersection(
|
||||
z.object({
|
||||
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
|
||||
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
|
||||
credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -69,6 +115,13 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion(
|
||||
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
|
||||
tenantId: true
|
||||
})
|
||||
}),
|
||||
BaseAzureClientSecretsConnectionSchema.extend({
|
||||
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
|
||||
credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema.pick({
|
||||
clientId: true,
|
||||
tenantId: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
AzureClientSecretsConnectionClientSecretOutputCredentialsSchema,
|
||||
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
|
||||
AzureClientSecretsConnectionSchema,
|
||||
CreateAzureClientSecretsConnectionSchema,
|
||||
@@ -30,6 +31,10 @@ export type TAzureClientSecretsConnectionCredentials = z.infer<
|
||||
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||
>;
|
||||
|
||||
export type TAzureClientSecretsConnectionClientSecretCredentials = z.infer<
|
||||
typeof AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
|
||||
>;
|
||||
|
||||
export interface ExchangeCodeAzureResponse {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
|
@@ -173,12 +173,6 @@ export const dailyReminderQueueServiceFactory = ({
|
||||
{ pattern: "0 */1 * * *", utc: true },
|
||||
QueueName.SecretReminderMigration // just a job id
|
||||
);
|
||||
|
||||
await queueService.queue(QueueName.SecretReminderMigration, QueueJobs.SecretReminderMigration, undefined, {
|
||||
delay: 5000,
|
||||
jobId: QueueName.SecretReminderMigration,
|
||||
repeat: { pattern: "0 */1 * * *", utc: true }
|
||||
});
|
||||
};
|
||||
|
||||
queueService.listen(QueueName.DailyReminders, "failed", (_, err) => {
|
||||
|
@@ -97,6 +97,28 @@ const batchUpdateEnvironmentSecrets = async (
|
||||
);
|
||||
};
|
||||
|
||||
const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiKey }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
await makeRequestWithRetry(() =>
|
||||
request.post(
|
||||
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const RenderSyncFns = {
|
||||
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
|
||||
@@ -131,6 +153,10 @@ export const RenderSyncFns = {
|
||||
}
|
||||
|
||||
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
|
||||
|
||||
if (secretSync.syncOptions.autoRedeployServices) {
|
||||
await redeployService(secretSync);
|
||||
}
|
||||
},
|
||||
|
||||
getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise<TSecretMap> => {
|
||||
@@ -151,5 +177,9 @@ export const RenderSyncFns = {
|
||||
}
|
||||
}
|
||||
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
|
||||
|
||||
if (secretSync.syncOptions.autoRedeployServices) {
|
||||
await redeployService(secretSync);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -20,23 +20,33 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
||||
})
|
||||
]);
|
||||
|
||||
const RenderSyncOptionsSchema = z.object({
|
||||
autoRedeployServices: z.boolean().optional().describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.RENDER.autoRedeployServices)
|
||||
});
|
||||
|
||||
const RenderSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const RenderSyncSchema = BaseSecretSyncSchema(SecretSync.Render, RenderSyncOptionsConfig).extend({
|
||||
export const RenderSyncSchema = BaseSecretSyncSchema(
|
||||
SecretSync.Render,
|
||||
RenderSyncOptionsConfig,
|
||||
RenderSyncOptionsSchema
|
||||
).extend({
|
||||
destination: z.literal(SecretSync.Render),
|
||||
destinationConfig: RenderSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateRenderSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.Render,
|
||||
RenderSyncOptionsConfig
|
||||
RenderSyncOptionsConfig,
|
||||
RenderSyncOptionsSchema
|
||||
).extend({
|
||||
destinationConfig: RenderSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateRenderSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.Render,
|
||||
RenderSyncOptionsConfig
|
||||
RenderSyncOptionsConfig,
|
||||
RenderSyncOptionsSchema
|
||||
).extend({
|
||||
destinationConfig: RenderSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
@@ -206,13 +206,6 @@
|
||||
"documentation/platform/external-migrations/vault"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "External Migrations",
|
||||
"pages": [
|
||||
"documentation/platform/workflow-integrations/slack-integration",
|
||||
"documentation/platform/workflow-integrations/microsoft-teams-integration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Admin Consoles",
|
||||
"pages": [
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 452 KiB |
@@ -43,12 +43,6 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
||||
- `Application.ReadWrite.All` (Delegated)
|
||||
- `Directory.ReadWrite.All` (Delegated)
|
||||
- `User.Read` (Delegated)
|
||||
- Azure App Configuration
|
||||
- `KeyValue.Delete` (Delegated)
|
||||
- `KeyValue.Read` (Delegated)
|
||||
- `KeyValue.Write` (Delegated)
|
||||
- Access Key Vault
|
||||
- `user_impersonation` (Delegated)
|
||||
|
||||

|
||||
|
||||
@@ -72,6 +66,30 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Client Secret Authentication">
|
||||
Ensure your Azure application has the required permissions that Infisical needs for the Azure Client Secrets connection to work.
|
||||
|
||||
**Prerequisites:**
|
||||
- An active Azure setup.
|
||||
|
||||
<Steps>
|
||||
<Step title="Assign API permissions to the application">
|
||||
For the Azure Client Secrets connection to work, assign the following permissions to your Azure application:
|
||||
|
||||
#### Required API Permissions
|
||||
|
||||
**Microsoft Graph**
|
||||
- `Application.ReadWrite.All`
|
||||
- `Application.ReadWrite.OwnedBy`
|
||||
- `Application.ReadWrite.All` (Delegated)
|
||||
- `Directory.ReadWrite.All` (Delegated)
|
||||
- `User.Read` (Delegated)
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
## Setup Azure Connection in Infisical
|
||||
|
||||
<Steps>
|
||||
@@ -82,21 +100,31 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
||||
<Step title="Add Connection">
|
||||
Select the **Azure Connection** option from the connection options modal. 
|
||||
</Step>
|
||||
<Step title="Authorize Connection">
|
||||
Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous step.
|
||||
<Step title="Create Connection">
|
||||
<Tabs>
|
||||
<Tab title="OAuth">
|
||||
<Step title="Authorize Connection">
|
||||
Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous step.
|
||||
|
||||
Now select the **OAuth** method and click **Connect to Azure**.
|
||||
Now select the **OAuth** method and click **Connect to Azure**.
|
||||
|
||||

|
||||

|
||||
</Step>
|
||||
<Step title="Grant Access">
|
||||
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
|
||||
you will be redirected back to Infisical's App Connections page. 
|
||||
</Step>
|
||||
</Tab>
|
||||
<Tab title="Client Secret">
|
||||
<Step title="Create Connection">
|
||||
Fill in the **Tenant ID**, **Client ID** and **Client Secret** fields with the Directory (Tenant) ID, Application (Client) ID and Client Secret you obtained in the previous step.
|
||||
|
||||
|
||||
|
||||
</Step>
|
||||
<Step title="Grant Access">
|
||||
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
|
||||
you will be redirected back to Infisical's App Connections page. 
|
||||
</Step>
|
||||

|
||||
</Step>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **Azure Client Secrets Connection** is now available for use. 
|
||||
</Step>
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
useRenderConnectionListServices
|
||||
} from "@app/hooks/api/appConnections/render";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync";
|
||||
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
|
@@ -0,0 +1,40 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FormControl, Switch, Tooltip } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const RenderSyncOptionsFields = () => {
|
||||
const { control } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="syncOptions.autoRedeployServices"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl className="mt-4" isError={Boolean(error?.message)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="auto-redeploy-services"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
>
|
||||
Auto Redeploy Services On Sync
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<p>If enabled, services will be automatically redeployed upon secret changes.</p>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
@@ -14,6 +14,7 @@ import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
|
||||
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
|
||||
import { RenderSyncOptionsFields } from "./RenderSyncOptionsFields";
|
||||
|
||||
type Props = {
|
||||
hideInitialSync?: boolean;
|
||||
@@ -38,6 +39,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
case SecretSync.AWSSecretsManager:
|
||||
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsFields />;
|
||||
break;
|
||||
case SecretSync.Render:
|
||||
AdditionalSyncOptionsFieldsComponent = <RenderSyncOptionsFields />;
|
||||
break;
|
||||
case SecretSync.GitHub:
|
||||
case SecretSync.GCPSecretManager:
|
||||
case SecretSync.AzureKeyVault:
|
||||
@@ -54,7 +58,6 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
case SecretSync.OnePass:
|
||||
case SecretSync.OCIVault:
|
||||
case SecretSync.Heroku:
|
||||
case SecretSync.Render:
|
||||
case SecretSync.Flyio:
|
||||
case SecretSync.GitLab:
|
||||
case SecretSync.CloudflarePages:
|
||||
|
@@ -2,8 +2,29 @@ import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const RenderSyncOptionsReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||
|
||||
const [{ autoRedeployServices }] = watch(["syncOptions"]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{autoRedeployServices ? (
|
||||
<GenericFieldLabel label="Auto Redeploy Services">
|
||||
<Badge variant="success">Enabled</Badge>
|
||||
</GenericFieldLabel>
|
||||
) : (
|
||||
<GenericFieldLabel label="Auto Redeploy Services">
|
||||
<Badge variant="danger">Disabled</Badge>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const RenderSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||
const serviceName = watch("destinationConfig.serviceName");
|
||||
|
@@ -35,7 +35,7 @@ import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
|
||||
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
|
||||
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
|
||||
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
|
||||
import { RenderSyncReviewFields } from "./RenderSyncReviewFields";
|
||||
import { RenderSyncOptionsReviewFields, RenderSyncReviewFields } from "./RenderSyncReviewFields";
|
||||
import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields";
|
||||
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
|
||||
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
|
||||
@@ -121,6 +121,7 @@ export const SecretSyncReviewFields = () => {
|
||||
break;
|
||||
case SecretSync.Render:
|
||||
DestinationFieldsComponent = <RenderSyncReviewFields />;
|
||||
AdditionalSyncOptionsFieldsComponent = <RenderSyncOptionsReviewFields />;
|
||||
break;
|
||||
case SecretSync.Flyio:
|
||||
DestinationFieldsComponent = <FlyioSyncReviewFields />;
|
||||
|
@@ -2,9 +2,13 @@ import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync";
|
||||
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
export const RenderSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
export const RenderSyncDestinationSchema = BaseSecretSyncSchema(
|
||||
z.object({
|
||||
autoRedeployServices: z.boolean().optional()
|
||||
})
|
||||
).merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.Render),
|
||||
destinationConfig: z.discriminatedUnion("scope", [
|
||||
|
@@ -171,7 +171,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
case RenderConnectionMethod.ApiKey:
|
||||
case ChecklyConnectionMethod.ApiKey:
|
||||
return { name: "API Key", icon: faKey };
|
||||
|
||||
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||
return { name: "Client Secret", icon: faKey };
|
||||
default:
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
}
|
||||
|
@@ -4,9 +4,9 @@ import {
|
||||
SecretSyncImportBehavior,
|
||||
SecretSyncInitialSyncBehavior
|
||||
} from "@app/hooks/api/secretSyncs";
|
||||
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/render-sync";
|
||||
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
|
||||
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
|
||||
[SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" },
|
||||
|
@@ -2,15 +2,26 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum AzureClientSecretsConnectionMethod {
|
||||
OAuth = "oauth"
|
||||
OAuth = "oauth",
|
||||
ClientSecret = "client-secret"
|
||||
}
|
||||
|
||||
export type TAzureClientSecretsConnection = TRootAppConnection & {
|
||||
app: AppConnection.AzureClientSecrets;
|
||||
} & {
|
||||
method: AzureClientSecretsConnectionMethod.OAuth;
|
||||
credentials: {
|
||||
code: string;
|
||||
tenantId: string;
|
||||
};
|
||||
};
|
||||
} & (
|
||||
| {
|
||||
method: AzureClientSecretsConnectionMethod.OAuth;
|
||||
credentials: {
|
||||
code: string;
|
||||
tenantId: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
method: AzureClientSecretsConnectionMethod.ClientSecret;
|
||||
credentials: {
|
||||
clientSecret: string;
|
||||
clientId: string;
|
||||
tenantId: string;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs";
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
import { TRenderSync } from "../render-sync";
|
||||
import { TOnePassSync } from "./1password-sync";
|
||||
import { TAwsParameterStoreSync } from "./aws-parameter-store-sync";
|
||||
import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync";
|
||||
@@ -24,6 +23,7 @@ import { THerokuSync } from "./heroku-sync";
|
||||
import { THumanitecSync } from "./humanitec-sync";
|
||||
import { TOCIVaultSync } from "./oci-vault-sync";
|
||||
import { TRailwaySync } from "./railway-sync";
|
||||
import { TRenderSync } from "./render-sync";
|
||||
import { TSupabaseSync } from "./supabase";
|
||||
import { TTeamCitySync } from "./teamcity-sync";
|
||||
import { TTerraformCloudSync } from "./terraform-cloud-sync";
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
export type TRenderSync = TRootSecretSync & {
|
||||
destination: SecretSync.Render;
|
||||
@@ -16,6 +16,10 @@ export type TRenderSync = TRootSecretSync & {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
syncOptions: RootSyncOptions & {
|
||||
autoRedeployServices?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export enum RenderSyncScope {
|
71
frontend/src/hooks/useResizableColWidth.tsx
Normal file
71
frontend/src/hooks/useResizableColWidth.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
type Params = {
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
initialWidth: number;
|
||||
};
|
||||
|
||||
export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth }: Params) => {
|
||||
const [colWidth, setColWidth] = useState(initialWidth);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const startX = useRef(0);
|
||||
const startWidth = useRef(0);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsResizing(true);
|
||||
startX.current = e.clientX;
|
||||
startWidth.current = colWidth;
|
||||
},
|
||||
[colWidth]
|
||||
);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const deltaX = e.clientX - startX.current;
|
||||
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth.current + deltaX));
|
||||
|
||||
setColWidth(newWidth);
|
||||
},
|
||||
[isResizing]
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isResizing) {
|
||||
document.addEventListener(
|
||||
"mousemove",
|
||||
// @ts-expect-error native discrepancy
|
||||
handleMouseMove
|
||||
);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "ew-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
"mousemove",
|
||||
// @ts-expect-error native discrepancy
|
||||
handleMouseMove
|
||||
);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
}, [isResizing, handleMouseMove, handleMouseUp]);
|
||||
|
||||
return {
|
||||
colWidth,
|
||||
handleMouseDown,
|
||||
isResizing
|
||||
};
|
||||
};
|
@@ -114,7 +114,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||
case AppConnection.Camunda:
|
||||
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureClientSecrets:
|
||||
return <AzureClientSecretsConnectionForm />;
|
||||
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureDevOps:
|
||||
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Windmill:
|
||||
@@ -222,7 +222,7 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
case AppConnection.Camunda:
|
||||
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.AzureClientSecrets:
|
||||
return <AzureClientSecretsConnectionForm appConnection={appConnection} />;
|
||||
return <AzureClientSecretsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureDevOps:
|
||||
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||
case AppConnection.Windmill:
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useState } from "react";
|
||||
@@ -20,19 +21,83 @@ import {
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
|
||||
|
||||
type Props = {
|
||||
appConnection?: TAzureClientSecretsConnection;
|
||||
onSubmit: (formData: ClientSecretForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||
const baseSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.AzureClientSecrets),
|
||||
method: z.nativeEnum(AzureClientSecretsConnectionMethod),
|
||||
tenantId: z.string().trim().min(1, "Tenant ID is required")
|
||||
method: z.nativeEnum(AzureClientSecretsConnectionMethod)
|
||||
});
|
||||
|
||||
const oauthSchema = baseSchema.extend({
|
||||
tenantId: z.string().trim().min(1, "Tenant ID is required"),
|
||||
method: z.literal(AzureClientSecretsConnectionMethod.OAuth)
|
||||
});
|
||||
|
||||
const clientSecretSchema = baseSchema.extend({
|
||||
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
|
||||
credentials: z.object({
|
||||
clientSecret: z.string().trim().min(1, "Client Secret is required"),
|
||||
clientId: z.string().trim().min(1, "Client ID is required"),
|
||||
tenantId: z.string().trim().min(1, "Tenant ID is required")
|
||||
})
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [oauthSchema, clientSecretSchema]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
||||
const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partial<FormData> => {
|
||||
if (!appConnection) {
|
||||
return {
|
||||
app: AppConnection.AzureClientSecrets,
|
||||
method: AzureClientSecretsConnectionMethod.OAuth
|
||||
};
|
||||
}
|
||||
|
||||
const base = {
|
||||
name: appConnection.name,
|
||||
description: appConnection.description,
|
||||
app: appConnection.app,
|
||||
method: appConnection.method
|
||||
};
|
||||
const { credentials } = appConnection;
|
||||
|
||||
switch (appConnection.method) {
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
if ("tenantId" in credentials) {
|
||||
return {
|
||||
...base,
|
||||
method: AzureClientSecretsConnectionMethod.OAuth,
|
||||
tenantId: credentials.tenantId
|
||||
};
|
||||
}
|
||||
break;
|
||||
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||
if ("clientSecret" in credentials && "clientId" in credentials) {
|
||||
return {
|
||||
...base,
|
||||
method: AzureClientSecretsConnectionMethod.ClientSecret,
|
||||
credentials: {
|
||||
clientSecret: credentials.clientSecret,
|
||||
clientId: credentials.clientId,
|
||||
tenantId: credentials.tenantId
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
@@ -43,70 +108,51 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection
|
||||
? {
|
||||
...appConnection,
|
||||
tenantId: appConnection.credentials.tenantId
|
||||
}
|
||||
: {
|
||||
app: AppConnection.AzureClientSecrets,
|
||||
method: AzureClientSecretsConnectionMethod.OAuth
|
||||
}
|
||||
defaultValues: getDefaultValues(appConnection)
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
const selectedMethod = watch("method");
|
||||
|
||||
const onSubmit = (formData: FormData) => {
|
||||
setIsRedirecting(true);
|
||||
const onSubmitHandler = (formData: FormData) => {
|
||||
const state = crypto.randomBytes(16).toString("hex");
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
localStorage.setItem(
|
||||
"azureClientSecretsConnectionFormData",
|
||||
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||
);
|
||||
|
||||
switch (formData.method) {
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
window.location.assign(
|
||||
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
|
||||
setIsRedirecting(true);
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
localStorage.setItem(
|
||||
"azureClientSecretsConnectionFormData",
|
||||
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||
);
|
||||
window.location.assign(
|
||||
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://graph.microsoft.com/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
|
||||
);
|
||||
break;
|
||||
|
||||
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||
onSubmit(formData);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||
}
|
||||
};
|
||||
|
||||
const isMissingConfig = !oauthClientId;
|
||||
|
||||
const isMissingConfig =
|
||||
selectedMethod === AzureClientSecretsConnectionMethod.OAuth && !oauthClientId;
|
||||
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<form onSubmit={handleSubmit(onSubmitHandler)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
|
||||
<Controller
|
||||
name="tenantId"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="The Directory (tenant) ID."
|
||||
isError={Boolean(error?.message)}
|
||||
label="Tenant ID"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
@@ -146,6 +192,61 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="tenantId"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="The Directory (tenant) ID."
|
||||
isError={Boolean(error?.message)}
|
||||
label="Tenant ID"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="00000000-0000-0000-0000-000000000000"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
setValue("credentials.tenantId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Access Token-specific fields */}
|
||||
{selectedMethod === AzureClientSecretsConnectionMethod.ClientSecret && (
|
||||
<>
|
||||
<Controller
|
||||
name="credentials.clientId"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
label="Client ID"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.clientSecret"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
label="Client Secret"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" placeholder="Enter your Client Secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
|
||||
import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync";
|
||||
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { subject } from "@casl/ability";
|
||||
@@ -53,6 +53,7 @@ import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
|
||||
import { useResizableColWidth } from "@app/hooks/useResizableColWidth";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { RequestAccessModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/RequestAccessModal";
|
||||
import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView";
|
||||
@@ -104,6 +105,8 @@ const Page = () => {
|
||||
const { permission } = useProjectPermission();
|
||||
const { mutateAsync: createCommit } = useCreateCommit();
|
||||
|
||||
const tableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const { isBatchMode, pendingChanges } = useBatchMode();
|
||||
const { loadPendingChanges, setExistingKeys } = useBatchModeActions();
|
||||
@@ -483,6 +486,14 @@ const Page = () => {
|
||||
handlePopUpClose("snapshots");
|
||||
}, []);
|
||||
|
||||
const { handleMouseDown, isResizing, colWidth } = useResizableColWidth({
|
||||
initialWidth: 320,
|
||||
minWidth: 100,
|
||||
maxWidth: tableRef.current
|
||||
? tableRef.current.clientWidth - 148 // ensure value column can't collapse completely
|
||||
: 800
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// restore filters for path if set
|
||||
const restore = filterHistory.get(secretPath);
|
||||
@@ -787,6 +798,7 @@ const Page = () => {
|
||||
}
|
||||
/>
|
||||
<div
|
||||
ref={tableRef}
|
||||
className={twMerge(
|
||||
"thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md bg-mineshaft-800 text-left text-sm text-bunker-300",
|
||||
isNotEmpty && "rounded-b-none"
|
||||
@@ -820,20 +832,34 @@ const Page = () => {
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div
|
||||
className="flex w-80 flex-shrink-0 items-center border-r border-mineshaft-600 py-2 pl-4"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleSortToggle}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") handleSortToggle();
|
||||
}}
|
||||
>
|
||||
Key
|
||||
<FontAwesomeIcon
|
||||
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
|
||||
className="ml-2"
|
||||
<div className="relative">
|
||||
<div
|
||||
tabIndex={-1}
|
||||
role="button"
|
||||
className={`absolute -right-[0.05rem] z-40 h-full w-0.5 cursor-ew-resize hover:bg-blue-400/20 ${
|
||||
isResizing ? "bg-blue-400/75" : "bg-transparent"
|
||||
}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
<div className="pointer-events-none absolute -right-[0.04rem] top-2 z-30">
|
||||
<div className="h-5 w-0.5 rounded-[1.5px] bg-gray-400 opacity-50" />
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-shrink-0 items-center border-r border-mineshaft-600 py-2 pl-4"
|
||||
style={{ width: colWidth }}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleSortToggle}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") handleSortToggle();
|
||||
}}
|
||||
>
|
||||
Key
|
||||
<FontAwesomeIcon
|
||||
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow px-4 py-2">Value</div>
|
||||
</div>
|
||||
@@ -927,6 +953,7 @@ const Page = () => {
|
||||
)}
|
||||
{canReadSecret && Boolean(mergedSecrets?.length) && (
|
||||
<SecretListView
|
||||
colWidth={colWidth}
|
||||
secrets={mergedSecrets}
|
||||
tags={tags}
|
||||
isVisible={isVisible}
|
||||
|
@@ -235,7 +235,7 @@ export const FolderListView = ({
|
||||
)}
|
||||
</div>
|
||||
{isPending ? (
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||
<div className="flex w-16 items-center justify-between border-l border-mineshaft-600 px-3 py-3">
|
||||
<IconButton
|
||||
ariaLabel="edit-folder"
|
||||
variant="plain"
|
||||
|
@@ -90,6 +90,7 @@ type Props = {
|
||||
}[];
|
||||
isPending?: boolean;
|
||||
pendingAction?: PendingAction;
|
||||
colWidth: number;
|
||||
};
|
||||
|
||||
export const SecretItem = memo(
|
||||
@@ -108,7 +109,8 @@ export const SecretItem = memo(
|
||||
handleSecretShare,
|
||||
importedBy,
|
||||
isPending,
|
||||
pendingAction
|
||||
pendingAction,
|
||||
colWidth
|
||||
}: Props) => {
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
"editSecret"
|
||||
@@ -383,7 +385,10 @@ export const SecretItem = memo(
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
|
||||
<div
|
||||
className="flex h-11 flex-shrink-0 items-center px-4 py-2"
|
||||
style={{ width: colWidth }}
|
||||
>
|
||||
<Controller
|
||||
name="key"
|
||||
control={control}
|
||||
|
@@ -51,6 +51,7 @@ type Props = {
|
||||
isImported: boolean;
|
||||
}[];
|
||||
}[];
|
||||
colWidth: number;
|
||||
};
|
||||
|
||||
export const SecretListView = ({
|
||||
@@ -62,7 +63,8 @@ export const SecretListView = ({
|
||||
isVisible,
|
||||
isProtectedBranch = false,
|
||||
usedBySecretSyncs,
|
||||
importedBy
|
||||
importedBy,
|
||||
colWidth
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
@@ -554,6 +556,7 @@ export const SecretListView = ({
|
||||
))}
|
||||
{secrets.map((secret) => (
|
||||
<SecretItem
|
||||
colWidth={colWidth}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
tags={wsTags}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
|
||||
import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync";
|
||||
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TRenderSync;
|
||||
|
@@ -0,0 +1,21 @@
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TRenderSync;
|
||||
};
|
||||
|
||||
export const RenderSyncOptionsSection = ({ secretSync }: Props) => {
|
||||
const {
|
||||
syncOptions: { autoRedeployServices }
|
||||
} = secretSync;
|
||||
|
||||
return (
|
||||
<GenericFieldLabel label="Auto Redeploy Services">
|
||||
<Badge variant={autoRedeployServices ? "success" : "danger"}>
|
||||
{autoRedeployServices ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
);
|
||||
};
|
@@ -13,6 +13,7 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection";
|
||||
import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection";
|
||||
import { RenderSyncOptionsSection } from "./RenderSyncOptionsSection";
|
||||
|
||||
type Props = {
|
||||
secretSync: TSecretSync;
|
||||
@@ -40,6 +41,9 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
<AwsSecretsManagerSyncOptionsSection secretSync={secretSync} />
|
||||
);
|
||||
break;
|
||||
case SecretSync.Render:
|
||||
AdditionalSyncOptionsComponent = <RenderSyncOptionsSection secretSync={secretSync} />;
|
||||
break;
|
||||
case SecretSync.GitHub:
|
||||
case SecretSync.GCPSecretManager:
|
||||
case SecretSync.AzureKeyVault:
|
||||
@@ -56,7 +60,6 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
case SecretSync.OCIVault:
|
||||
case SecretSync.OnePass:
|
||||
case SecretSync.Heroku:
|
||||
case SecretSync.Render:
|
||||
case SecretSync.Flyio:
|
||||
case SecretSync.GitLab:
|
||||
case SecretSync.CloudflarePages:
|
||||
|
Reference in New Issue
Block a user