mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-31 10:38:12 +00:00
Compare commits
1 Commits
feat/azure
...
fix/oracle
Author | SHA1 | Date | |
---|---|---|---|
|
8e829bdf85 |
@@ -2245,9 +2245,7 @@ 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.",
|
||||
clientId: "The Client ID to use to connect with Azure Client Secrets.",
|
||||
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
|
||||
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
|
||||
},
|
||||
AZURE_DEVOPS: {
|
||||
code: "The OAuth code to use to connect with Azure DevOps.",
|
||||
|
@@ -1,4 +1,3 @@
|
||||
export enum AzureClientSecretsConnectionMethod {
|
||||
OAuth = "oauth",
|
||||
ClientSecret = "client-secret"
|
||||
OAuth = "oauth"
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -17,7 +16,6 @@ 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";
|
||||
@@ -28,10 +26,7 @@ export const getAzureClientSecretsConnectionListItem = () => {
|
||||
return {
|
||||
name: "Azure Client Secrets" as const,
|
||||
app: AppConnection.AzureClientSecrets as const,
|
||||
methods: Object.values(AzureClientSecretsConnectionMethod) as [
|
||||
AzureClientSecretsConnectionMethod.OAuth,
|
||||
AzureClientSecretsConnectionMethod.ClientSecret
|
||||
],
|
||||
methods: Object.values(AzureClientSecretsConnectionMethod) as [AzureClientSecretsConnectionMethod.OAuth],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||
};
|
||||
};
|
||||
@@ -42,6 +37,12 @@ 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) {
|
||||
@@ -62,81 +63,34 @@ 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 updatedCredentials = {
|
||||
...credentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: currentTime + data.expires_in * 1000,
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
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 encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
const updatedCredentials = {
|
||||
...credentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: currentTime + data.expires_in * 1000,
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||
|
||||
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}`
|
||||
});
|
||||
}
|
||||
return data.access_token;
|
||||
};
|
||||
|
||||
export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => {
|
||||
@@ -144,103 +98,69 @@ 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,36 +26,6 @@ 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
|
||||
@@ -64,14 +34,6 @@ 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
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -81,13 +43,9 @@ export const CreateAzureClientSecretsConnectionSchema = ValidateAzureClientSecre
|
||||
|
||||
export const UpdateAzureClientSecretsConnectionSchema = z
|
||||
.object({
|
||||
credentials: z
|
||||
.union([
|
||||
AzureClientSecretsConnectionOAuthInputCredentialsSchema,
|
||||
AzureClientSecretsConnectionClientSecretInputCredentialsSchema
|
||||
])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials)
|
||||
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
|
||||
|
||||
@@ -101,10 +59,6 @@ export const AzureClientSecretsConnectionSchema = z.intersection(
|
||||
z.object({
|
||||
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
|
||||
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
|
||||
credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -115,13 +69,6 @@ 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,7 +4,6 @@ import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
AzureClientSecretsConnectionClientSecretOutputCredentialsSchema,
|
||||
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
|
||||
AzureClientSecretsConnectionSchema,
|
||||
CreateAzureClientSecretsConnectionSchema,
|
||||
@@ -31,10 +30,6 @@ export type TAzureClientSecretsConnectionCredentials = z.infer<
|
||||
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||
>;
|
||||
|
||||
export type TAzureClientSecretsConnectionClientSecretCredentials = z.infer<
|
||||
typeof AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
|
||||
>;
|
||||
|
||||
export interface ExchangeCodeAzureResponse {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
|
@@ -164,7 +164,7 @@ export const validateSqlConnectionCredentials = async (
|
||||
) => {
|
||||
try {
|
||||
await executeWithPotentialGateway(config, gatewayService, async (client) => {
|
||||
await client.raw(`Select 1`);
|
||||
await client.raw(config.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`);
|
||||
});
|
||||
return config.credentials;
|
||||
} catch (error) {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 452 KiB |
@@ -72,38 +72,6 @@ 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)
|
||||
|
||||
**Azure App Configuration**
|
||||
- `KeyValue.Delete` (Delegated)
|
||||
- `KeyValue.Read` (Delegated)
|
||||
- `KeyValue.Write` (Delegated)
|
||||
|
||||
**Access Key Vault**
|
||||
- `user_impersonation` (Delegated)
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
## Setup Azure Connection in Infisical
|
||||
|
||||
<Steps>
|
||||
@@ -114,31 +82,21 @@ 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="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.
|
||||
<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>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</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 title="Connection Created">
|
||||
Your **Azure Client Secrets Connection** is now available for use. 
|
||||
</Step>
|
||||
|
@@ -171,8 +171,7 @@ 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}`);
|
||||
}
|
||||
|
@@ -2,26 +2,15 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum AzureClientSecretsConnectionMethod {
|
||||
OAuth = "oauth",
|
||||
ClientSecret = "client-secret"
|
||||
OAuth = "oauth"
|
||||
}
|
||||
|
||||
export type TAzureClientSecretsConnection = TRootAppConnection & {
|
||||
app: AppConnection.AzureClientSecrets;
|
||||
} & (
|
||||
| {
|
||||
method: AzureClientSecretsConnectionMethod.OAuth;
|
||||
credentials: {
|
||||
code: string;
|
||||
tenantId: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
method: AzureClientSecretsConnectionMethod.ClientSecret;
|
||||
credentials: {
|
||||
clientSecret: string;
|
||||
clientId: string;
|
||||
tenantId: string;
|
||||
};
|
||||
}
|
||||
);
|
||||
} & {
|
||||
method: AzureClientSecretsConnectionMethod.OAuth;
|
||||
credentials: {
|
||||
code: string;
|
||||
tenantId: string;
|
||||
};
|
||||
};
|
||||
|
@@ -114,7 +114,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||
case AppConnection.Camunda:
|
||||
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureClientSecrets:
|
||||
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} />;
|
||||
return <AzureClientSecretsConnectionForm />;
|
||||
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} onSubmit={onSubmit} />;
|
||||
return <AzureClientSecretsConnectionForm appConnection={appConnection} />;
|
||||
case AppConnection.AzureDevOps:
|
||||
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||
case AppConnection.Windmill:
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useState } from "react";
|
||||
@@ -21,83 +20,19 @@ import {
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
|
||||
|
||||
type Props = {
|
||||
appConnection?: TAzureClientSecretsConnection;
|
||||
onSubmit: (formData: ClientSecretForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const baseSchema = genericAppConnectionFieldsSchema.extend({
|
||||
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||
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>;
|
||||
|
||||
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) => {
|
||||
export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
@@ -108,51 +43,70 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: getDefaultValues(appConnection)
|
||||
defaultValues: appConnection
|
||||
? {
|
||||
...appConnection,
|
||||
tenantId: appConnection.credentials.tenantId
|
||||
}
|
||||
: {
|
||||
app: AppConnection.AzureClientSecrets,
|
||||
method: AzureClientSecretsConnectionMethod.OAuth
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
const selectedMethod = watch("method");
|
||||
|
||||
const onSubmitHandler = (formData: FormData) => {
|
||||
const onSubmit = (formData: FormData) => {
|
||||
setIsRedirecting(true);
|
||||
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:
|
||||
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://azconfig.io/.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 =
|
||||
selectedMethod === AzureClientSecretsConnectionMethod.OAuth && !oauthClientId;
|
||||
const isMissingConfig = !oauthClientId;
|
||||
|
||||
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmitHandler)}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!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}
|
||||
@@ -192,61 +146,6 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
|
||||
</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"
|
||||
|
Reference in New Issue
Block a user