mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-13 07:12:51 +00:00
Compare commits
18 Commits
daniel/fip
...
fix/oracle
Author | SHA1 | Date | |
---|---|---|---|
|
8df4616265 | ||
|
484f34a257 | ||
|
32851565a7 | ||
|
68401a799e | ||
|
0adf2c830d | ||
|
c68138ac21 | ||
|
d8e39aed16 | ||
|
72ee468208 | ||
|
18238b46a7 | ||
|
d0ffae2c10 | ||
|
7ce11cde95 | ||
|
af32948a05 | ||
|
25753fc995 | ||
|
cd71848800 | ||
|
4afc7a1981 | ||
|
7365f60835 | ||
|
5af939992c | ||
|
aec4ee905e |
@@ -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");
|
||||||
|
}
|
@@ -7,12 +7,13 @@ import {
|
|||||||
TRotationFactoryRevokeCredentials,
|
TRotationFactoryRevokeCredentials,
|
||||||
TRotationFactoryRotateCredentials
|
TRotationFactoryRotateCredentials
|
||||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import {
|
import {
|
||||||
executeWithPotentialGateway,
|
executeWithPotentialGateway,
|
||||||
SQL_CONNECTION_ALTER_LOGIN_STATEMENT
|
SQL_CONNECTION_ALTER_LOGIN_STATEMENT
|
||||||
} from "@app/services/app-connection/shared/sql";
|
} from "@app/services/app-connection/shared/sql";
|
||||||
|
|
||||||
import { generatePassword } from "../utils";
|
import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../utils";
|
||||||
import {
|
import {
|
||||||
TSqlCredentialsRotationGeneratedCredentials,
|
TSqlCredentialsRotationGeneratedCredentials,
|
||||||
TSqlCredentialsRotationWithConnection
|
TSqlCredentialsRotationWithConnection
|
||||||
@@ -32,6 +33,11 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
|
|||||||
return redactedMessage;
|
return redactedMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ORACLE_PASSWORD_REQUIREMENTS = {
|
||||||
|
...DEFAULT_PASSWORD_REQUIREMENTS,
|
||||||
|
length: 30
|
||||||
|
};
|
||||||
|
|
||||||
export const sqlCredentialsRotationFactory: TRotationFactory<
|
export const sqlCredentialsRotationFactory: TRotationFactory<
|
||||||
TSqlCredentialsRotationWithConnection,
|
TSqlCredentialsRotationWithConnection,
|
||||||
TSqlCredentialsRotationGeneratedCredentials
|
TSqlCredentialsRotationGeneratedCredentials
|
||||||
@@ -43,6 +49,9 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
|||||||
secretsMapping
|
secretsMapping
|
||||||
} = secretRotation;
|
} = secretRotation;
|
||||||
|
|
||||||
|
const passwordRequirement =
|
||||||
|
connection.app === AppConnection.OracleDB ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
|
||||||
|
|
||||||
const executeOperation = <T>(
|
const executeOperation = <T>(
|
||||||
operation: (client: Knex) => Promise<T>,
|
operation: (client: Knex) => Promise<T>,
|
||||||
credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number]
|
credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number]
|
||||||
@@ -65,7 +74,7 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
|||||||
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
|
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
|
||||||
try {
|
try {
|
||||||
await executeOperation(async (client) => {
|
await executeOperation(async (client) => {
|
||||||
await client.raw("SELECT 1");
|
await client.raw(connection.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`);
|
||||||
}, credentials);
|
}, credentials);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(redactPasswords(error, [credentials]));
|
throw new Error(redactPasswords(error, [credentials]));
|
||||||
@@ -75,11 +84,13 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
|||||||
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
||||||
callback
|
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
|
// For SQL, since we get existing users, we change both their passwords
|
||||||
// on issue to invalidate their existing passwords
|
// on issue to invalidate their existing passwords
|
||||||
const credentialsSet = [
|
const credentialsSet = [
|
||||||
{ username: username1, password: generatePassword() },
|
{ username: username1, password: generatePassword(passwordRequirement) },
|
||||||
{ username: username2, password: generatePassword() }
|
{ username: username2, password: generatePassword(passwordRequirement) }
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -105,7 +116,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
|||||||
credentialsToRevoke,
|
credentialsToRevoke,
|
||||||
callback
|
callback
|
||||||
) => {
|
) => {
|
||||||
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
|
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({
|
||||||
|
username,
|
||||||
|
password: generatePassword(passwordRequirement)
|
||||||
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executeOperation(async (client) => {
|
await executeOperation(async (client) => {
|
||||||
@@ -128,7 +142,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
|||||||
callback
|
callback
|
||||||
) => {
|
) => {
|
||||||
// generate new password for the next active user
|
// 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 {
|
try {
|
||||||
await executeOperation(async (client) => {
|
await executeOperation(async (client) => {
|
||||||
|
@@ -11,7 +11,7 @@ type TPasswordRequirements = {
|
|||||||
allowedSymbols?: string;
|
allowedSymbols?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
|
export const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
|
||||||
length: 48,
|
length: 48,
|
||||||
required: {
|
required: {
|
||||||
lowercase: 1,
|
lowercase: 1,
|
||||||
|
@@ -2245,7 +2245,9 @@ export const AppConnections = {
|
|||||||
},
|
},
|
||||||
AZURE_CLIENT_SECRETS: {
|
AZURE_CLIENT_SECRETS: {
|
||||||
code: "The OAuth code to use to connect with 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: {
|
AZURE_DEVOPS: {
|
||||||
code: "The OAuth code to use to connect with Azure DevOps.",
|
code: "The OAuth code to use to connect with Azure DevOps.",
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
export enum AzureClientSecretsConnectionMethod {
|
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 { AxiosError, AxiosResponse } from "axios";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
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 { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
|
||||||
import {
|
import {
|
||||||
ExchangeCodeAzureResponse,
|
ExchangeCodeAzureResponse,
|
||||||
|
TAzureClientSecretsConnectionClientSecretCredentials,
|
||||||
TAzureClientSecretsConnectionConfig,
|
TAzureClientSecretsConnectionConfig,
|
||||||
TAzureClientSecretsConnectionCredentials
|
TAzureClientSecretsConnectionCredentials
|
||||||
} from "./azure-client-secrets-connection-types";
|
} from "./azure-client-secrets-connection-types";
|
||||||
@@ -26,7 +28,10 @@ export const getAzureClientSecretsConnectionListItem = () => {
|
|||||||
return {
|
return {
|
||||||
name: "Azure Client Secrets" as const,
|
name: "Azure Client Secrets" as const,
|
||||||
app: AppConnection.AzureClientSecrets 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
|
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -37,12 +42,6 @@ export const getAzureConnectionAccessToken = async (
|
|||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
) => {
|
) => {
|
||||||
const appCfg = getConfig();
|
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);
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
if (!appConnection) {
|
if (!appConnection) {
|
||||||
@@ -63,7 +62,13 @@ export const getAzureConnectionAccessToken = async (
|
|||||||
|
|
||||||
const { refreshToken } = credentials;
|
const { refreshToken } = credentials;
|
||||||
const currentTime = Date.now();
|
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>(
|
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
@@ -91,6 +96,47 @@ export const getAzureConnectionAccessToken = async (
|
|||||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||||
|
|
||||||
return data.access_token;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => {
|
||||||
@@ -98,6 +144,8 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA
|
|||||||
|
|
||||||
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
if (!SITE_URL) {
|
if (!SITE_URL) {
|
||||||
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
|
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
|
||||||
}
|
}
|
||||||
@@ -153,14 +201,46 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (method) {
|
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
|
||||||
return {
|
return {
|
||||||
tenantId: inputCredentials.tenantId,
|
tenantId: inputCredentials.tenantId,
|
||||||
accessToken: tokenResp.data.access_token,
|
accessToken: tokenResp.data.access_token,
|
||||||
refreshToken: tokenResp.data.refresh_token,
|
refreshToken: tokenResp.data.refresh_token,
|
||||||
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
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:
|
default:
|
||||||
throw new InternalServerError({
|
throw new InternalServerError({
|
||||||
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
|
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
|
||||||
|
@@ -26,6 +26,36 @@ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object
|
|||||||
expiresAt: z.number()
|
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", [
|
export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
z.object({
|
z.object({
|
||||||
method: z
|
method: z
|
||||||
@@ -34,6 +64,14 @@ export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discrimin
|
|||||||
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
|
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
|
||||||
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
|
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
|
export const UpdateAzureClientSecretsConnectionSchema = z
|
||||||
.object({
|
.object({
|
||||||
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.optional().describe(
|
credentials: z
|
||||||
AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials
|
.union([
|
||||||
)
|
AzureClientSecretsConnectionOAuthInputCredentialsSchema,
|
||||||
|
AzureClientSecretsConnectionClientSecretInputCredentialsSchema
|
||||||
|
])
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials)
|
||||||
})
|
})
|
||||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
|
||||||
|
|
||||||
@@ -59,6 +101,10 @@ export const AzureClientSecretsConnectionSchema = z.intersection(
|
|||||||
z.object({
|
z.object({
|
||||||
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
|
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
|
||||||
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
|
||||||
|
credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
@@ -69,6 +115,13 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion(
|
|||||||
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
|
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
|
||||||
tenantId: true
|
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 { AppConnection } from "../app-connection-enums";
|
||||||
import {
|
import {
|
||||||
|
AzureClientSecretsConnectionClientSecretOutputCredentialsSchema,
|
||||||
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
|
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
|
||||||
AzureClientSecretsConnectionSchema,
|
AzureClientSecretsConnectionSchema,
|
||||||
CreateAzureClientSecretsConnectionSchema,
|
CreateAzureClientSecretsConnectionSchema,
|
||||||
@@ -30,6 +31,10 @@ export type TAzureClientSecretsConnectionCredentials = z.infer<
|
|||||||
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type TAzureClientSecretsConnectionClientSecretCredentials = z.infer<
|
||||||
|
typeof AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export interface ExchangeCodeAzureResponse {
|
export interface ExchangeCodeAzureResponse {
|
||||||
token_type: string;
|
token_type: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
|
@@ -173,12 +173,6 @@ export const dailyReminderQueueServiceFactory = ({
|
|||||||
{ pattern: "0 */1 * * *", utc: true },
|
{ pattern: "0 */1 * * *", utc: true },
|
||||||
QueueName.SecretReminderMigration // just a job id
|
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) => {
|
queueService.listen(QueueName.DailyReminders, "failed", (_, err) => {
|
||||||
|
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)
|
- `Application.ReadWrite.All` (Delegated)
|
||||||
- `Directory.ReadWrite.All` (Delegated)
|
- `Directory.ReadWrite.All` (Delegated)
|
||||||
- `User.Read` (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>
|
||||||
|
|
||||||
|
<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
|
## Setup Azure Connection in Infisical
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
@@ -82,20 +100,30 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
|||||||
<Step title="Add Connection">
|
<Step title="Add Connection">
|
||||||
Select the **Azure Connection** option from the connection options modal. 
|
Select the **Azure Connection** option from the connection options modal. 
|
||||||
</Step>
|
</Step>
|
||||||
|
<Step title="Create Connection">
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="OAuth">
|
||||||
<Step title="Authorize Connection">
|
<Step title="Authorize Connection">
|
||||||
Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous step.
|
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>
|
||||||
<Step title="Grant Access">
|
<Step title="Grant Access">
|
||||||
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
|
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. 
|
Authorization](/images/app-connections/azure/grant-access.png)
|
||||||
|
</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>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Connection Created">
|
<Step title="Connection Created">
|
||||||
Your **Azure Client Secrets Connection** is now available for use. 
|
Your **Azure Client Secrets Connection** is now available for use. 
|
||||||
|
@@ -171,7 +171,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
|||||||
case RenderConnectionMethod.ApiKey:
|
case RenderConnectionMethod.ApiKey:
|
||||||
case ChecklyConnectionMethod.ApiKey:
|
case ChecklyConnectionMethod.ApiKey:
|
||||||
return { name: "API Key", icon: faKey };
|
return { name: "API Key", icon: faKey };
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
return { name: "Client Secret", icon: faKey };
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
}
|
}
|
||||||
|
@@ -2,15 +2,26 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
|||||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||||
|
|
||||||
export enum AzureClientSecretsConnectionMethod {
|
export enum AzureClientSecretsConnectionMethod {
|
||||||
OAuth = "oauth"
|
OAuth = "oauth",
|
||||||
|
ClientSecret = "client-secret"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TAzureClientSecretsConnection = TRootAppConnection & {
|
export type TAzureClientSecretsConnection = TRootAppConnection & {
|
||||||
app: AppConnection.AzureClientSecrets;
|
app: AppConnection.AzureClientSecrets;
|
||||||
} & {
|
} & (
|
||||||
|
| {
|
||||||
method: AzureClientSecretsConnectionMethod.OAuth;
|
method: AzureClientSecretsConnectionMethod.OAuth;
|
||||||
credentials: {
|
credentials: {
|
||||||
code: string;
|
code: string;
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: AzureClientSecretsConnectionMethod.ClientSecret;
|
||||||
|
credentials: {
|
||||||
|
clientSecret: string;
|
||||||
|
clientId: string;
|
||||||
|
tenantId: string;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@@ -114,7 +114,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
|||||||
case AppConnection.Camunda:
|
case AppConnection.Camunda:
|
||||||
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.AzureClientSecrets:
|
case AppConnection.AzureClientSecrets:
|
||||||
return <AzureClientSecretsConnectionForm />;
|
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.AzureDevOps:
|
case AppConnection.AzureDevOps:
|
||||||
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
|
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Windmill:
|
case AppConnection.Windmill:
|
||||||
@@ -222,7 +222,7 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
|||||||
case AppConnection.Camunda:
|
case AppConnection.Camunda:
|
||||||
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.AzureClientSecrets:
|
case AppConnection.AzureClientSecrets:
|
||||||
return <AzureClientSecretsConnectionForm appConnection={appConnection} />;
|
return <AzureClientSecretsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||||
case AppConnection.AzureDevOps:
|
case AppConnection.AzureDevOps:
|
||||||
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||||
case AppConnection.Windmill:
|
case AppConnection.Windmill:
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-case-declarations */
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -20,19 +21,83 @@ import {
|
|||||||
GenericAppConnectionsFields
|
GenericAppConnectionsFields
|
||||||
} from "./GenericAppConnectionFields";
|
} from "./GenericAppConnectionFields";
|
||||||
|
|
||||||
|
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appConnection?: TAzureClientSecretsConnection;
|
appConnection?: TAzureClientSecretsConnection;
|
||||||
|
onSubmit: (formData: ClientSecretForm) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = genericAppConnectionFieldsSchema.extend({
|
const baseSchema = genericAppConnectionFieldsSchema.extend({
|
||||||
app: z.literal(AppConnection.AzureClientSecrets),
|
app: z.literal(AppConnection.AzureClientSecrets),
|
||||||
method: z.nativeEnum(AzureClientSecretsConnectionMethod),
|
method: z.nativeEnum(AzureClientSecretsConnectionMethod)
|
||||||
tenantId: z.string().trim().min(1, "Tenant ID is required")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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>;
|
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 isUpdate = Boolean(appConnection);
|
||||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||||
|
|
||||||
@@ -43,70 +108,51 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
|||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: appConnection
|
defaultValues: getDefaultValues(appConnection)
|
||||||
? {
|
|
||||||
...appConnection,
|
|
||||||
tenantId: appConnection.credentials.tenantId
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
app: AppConnection.AzureClientSecrets,
|
|
||||||
method: AzureClientSecretsConnectionMethod.OAuth
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { isSubmitting, isDirty }
|
formState: { isSubmitting, isDirty }
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const selectedMethod = watch("method");
|
const selectedMethod = watch("method");
|
||||||
|
|
||||||
const onSubmit = (formData: FormData) => {
|
const onSubmitHandler = (formData: FormData) => {
|
||||||
setIsRedirecting(true);
|
|
||||||
const state = crypto.randomBytes(16).toString("hex");
|
const state = crypto.randomBytes(16).toString("hex");
|
||||||
|
switch (formData.method) {
|
||||||
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
setIsRedirecting(true);
|
||||||
localStorage.setItem("latestCSRFToken", state);
|
localStorage.setItem("latestCSRFToken", state);
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"azureClientSecretsConnectionFormData",
|
"azureClientSecretsConnectionFormData",
|
||||||
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (formData.method) {
|
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
|
||||||
window.location.assign(
|
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`
|
`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;
|
break;
|
||||||
|
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
onSubmit(formData);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMissingConfig = !oauthClientId;
|
const isMissingConfig =
|
||||||
|
selectedMethod === AzureClientSecretsConnectionMethod.OAuth && !oauthClientId;
|
||||||
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmitHandler)}>
|
||||||
{!isUpdate && <GenericAppConnectionsFields />}
|
{!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
|
<Controller
|
||||||
name="method"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -146,6 +192,61 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
|||||||
</FormControl>
|
</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">
|
<div className="mt-8 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
|
Reference in New Issue
Block a user