mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-09 06:02:53 +00:00
Compare commits
21 Commits
improve-in
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
bec1fefee8 | ||
|
cd03107a60 | ||
|
07965de1db | ||
|
b20ff0f029 | ||
|
691cbe0a4f | ||
|
0787128803 | ||
|
837158e344 | ||
|
03bd1471b2 | ||
|
092695089d | ||
|
2d80681597 | ||
|
cf23f98170 | ||
|
c4c8e121f0 | ||
|
0701c996e5 | ||
|
4ca6f165b7 | ||
|
b9dd565926 | ||
|
136b0bdcb5 | ||
|
7266d1f310 | ||
|
9c6ec807cb | ||
|
5fcae35fae | ||
|
359e19f804 | ||
|
b4ed1fa96a |
55
backend/package-lock.json
generated
55
backend/package-lock.json
generated
@@ -7,7 +7,6 @@
|
||||
"": {
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-elasticache": "^3.637.0",
|
||||
@@ -34,7 +33,7 @@
|
||||
"@gitbeaker/rest": "^42.5.0",
|
||||
"@google-cloud/kms": "^4.5.0",
|
||||
"@infisical/quic": "^1.0.8",
|
||||
"@node-saml/passport-saml": "^5.0.1",
|
||||
"@node-saml/passport-saml": "^5.1.0",
|
||||
"@octokit/auth-app": "^7.1.1",
|
||||
"@octokit/core": "^5.2.1",
|
||||
"@octokit/plugin-paginate-graphql": "^4.0.1",
|
||||
@@ -9574,20 +9573,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.0.1.tgz",
|
||||
"integrity": "sha512-YQzFPEC+CnsfO9AFYnwfYZKIzOLx3kITaC1HrjHVLTo6hxcQhc+LgHODOMvW4VCV95Gwrz1MshRUWCPzkDqmnA==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.1.0.tgz",
|
||||
"integrity": "sha512-t3cJnZ4aC7HhPZ6MGylGZULvUtBOZ6FzuUndaHGXjmIZHXnLfC/7L8a57O9Q9V7AxJGKAiRM5zu2wNm9EsvQpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/qs": "^6.9.11",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@types/xml-encryption": "^1.2.4",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@xmldom/is-dom-node": "^1.0.1",
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"debug": "^4.3.4",
|
||||
"xml-crypto": "^6.0.1",
|
||||
"xml-encryption": "^3.0.2",
|
||||
"debug": "^4.4.0",
|
||||
"xml-crypto": "^6.1.2",
|
||||
"xml-encryption": "^3.1.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"xpath": "^0.0.34"
|
||||
@@ -9597,9 +9596,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -9636,14 +9635,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/passport-saml": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-5.0.1.tgz",
|
||||
"integrity": "sha512-fMztg3zfSnjLEgxvpl6HaDMNeh0xeQX4QHiF9e2Lsie2dc4qFE37XYbQZhVmn8XJ2awPpSWLQ736UskYgGU8lQ==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-5.1.0.tgz",
|
||||
"integrity": "sha512-pBm+iFjv9eihcgeJuSUs4c0AuX1QEFdHwP8w1iaWCfDzXdeWZxUBU5HT2bY2S4dvNutcy+A9hYsH7ZLBGtgwDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@node-saml/node-saml": "^5.0.1",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/passport": "^1.0.16",
|
||||
"@node-saml/node-saml": "^5.1.0",
|
||||
"@types/express": "^4.17.23",
|
||||
"@types/passport": "^1.0.17",
|
||||
"@types/passport-strategy": "^0.2.38",
|
||||
"passport": "^0.7.0",
|
||||
"passport-strategy": "^1.0.0"
|
||||
@@ -13351,9 +13350,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz",
|
||||
"integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.33",
|
||||
@@ -13523,9 +13523,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/passport": {
|
||||
"version": "1.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz",
|
||||
"integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==",
|
||||
"version": "1.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
|
||||
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
@@ -31953,9 +31954,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-crypto": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.0.1.tgz",
|
||||
"integrity": "sha512-v05aU7NS03z4jlZ0iZGRFeZsuKO1UfEbbYiaeRMiATBFs6Jq9+wqKquEMTn4UTrYZ9iGD8yz3KT4L9o2iF682w==",
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.1.2.tgz",
|
||||
"integrity": "sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xmldom/is-dom-node": "^1.0.1",
|
||||
|
@@ -153,7 +153,7 @@
|
||||
"@gitbeaker/rest": "^42.5.0",
|
||||
"@google-cloud/kms": "^4.5.0",
|
||||
"@infisical/quic": "^1.0.8",
|
||||
"@node-saml/passport-saml": "^5.0.1",
|
||||
"@node-saml/passport-saml": "^5.1.0",
|
||||
"@octokit/auth-app": "^7.1.1",
|
||||
"@octokit/core": "^5.2.1",
|
||||
"@octokit/plugin-paginate-graphql": "^4.0.1",
|
||||
|
@@ -5,13 +5,14 @@
|
||||
// TODO(akhilmhdh): With tony find out the api structure and fill it here
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { AxiosError } from "axios";
|
||||
import { CronJob } from "cron";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { verifyOfflineLicense } from "@app/lib/crypto";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
@@ -603,10 +604,22 @@ export const licenseServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerCloudApi.request.delete(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods/${pmtMethodId}`
|
||||
);
|
||||
return data;
|
||||
try {
|
||||
const { data } = await licenseServerCloudApi.request.delete(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods/${pmtMethodId}`
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to remove payment method: ${error.response?.data?.message}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to remove payment method"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => {
|
||||
|
@@ -21,6 +21,8 @@ const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0";
|
||||
|
||||
type AzureErrorResponse = { error: { message: string } };
|
||||
|
||||
const EXPIRY_PADDING_IN_DAYS = 3;
|
||||
|
||||
const sleep = async () =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
@@ -33,7 +35,8 @@ export const azureClientSecretRotationFactory: TRotationFactory<
|
||||
const {
|
||||
connection,
|
||||
parameters: { objectId, clientId: clientIdParam },
|
||||
secretsMapping
|
||||
secretsMapping,
|
||||
rotationInterval
|
||||
} = secretRotation;
|
||||
|
||||
/**
|
||||
@@ -50,7 +53,7 @@ export const azureClientSecretRotationFactory: TRotationFactory<
|
||||
)}-${now.getFullYear()}`;
|
||||
|
||||
const endDateTime = new Date();
|
||||
endDateTime.setFullYear(now.getFullYear() + 5);
|
||||
endDateTime.setDate(now.getDate() + rotationInterval * 2 + EXPIRY_PADDING_IN_DAYS); // give 72 hour buffer
|
||||
|
||||
try {
|
||||
const { data } = await request.post<AzureAddPasswordResponse>(
|
||||
@@ -195,6 +198,12 @@ export const azureClientSecretRotationFactory: TRotationFactory<
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateClientSecret();
|
||||
|
||||
// 2.5 years as expiry is set to x2 interval for the inactive period of credential
|
||||
if (rotationInterval > Math.floor(365 * 2.5) - EXPIRY_PADDING_IN_DAYS) {
|
||||
throw new BadRequestError({ message: "Azure does not support token duration over 5 years" });
|
||||
}
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
|
@@ -51,6 +51,7 @@ const baseSecretRotationV2Query = ({
|
||||
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
|
||||
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
|
||||
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
|
||||
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"),
|
||||
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
|
||||
db
|
||||
@@ -104,6 +105,7 @@ const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRota
|
||||
connectionCreatedAt,
|
||||
connectionUpdatedAt,
|
||||
connectionVersion,
|
||||
connectionGatewayId,
|
||||
connectionIsPlatformManagedCredentials,
|
||||
...el
|
||||
} = secretRotation;
|
||||
@@ -123,6 +125,7 @@ const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRota
|
||||
createdAt: connectionCreatedAt,
|
||||
updatedAt: connectionUpdatedAt,
|
||||
version: connectionVersion,
|
||||
gatewayId: connectionGatewayId,
|
||||
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
|
||||
},
|
||||
folder: {
|
||||
|
@@ -2253,7 +2253,9 @@ export const AppConnections = {
|
||||
AZURE_DEVOPS: {
|
||||
code: "The OAuth code to use to connect with Azure DevOps.",
|
||||
tenantId: "The Tenant ID to use to connect with Azure DevOps.",
|
||||
orgName: "The Organization name to use to connect with Azure DevOps."
|
||||
orgName: "The Organization name to use to connect with Azure DevOps.",
|
||||
clientId: "The Client ID to use to connect with Azure Client Secrets.",
|
||||
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
|
||||
},
|
||||
OCI: {
|
||||
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
|
||||
@@ -2400,12 +2402,18 @@ export const SecretSyncs = {
|
||||
env: "The name of the GitHub environment."
|
||||
},
|
||||
AZURE_KEY_VAULT: {
|
||||
vaultBaseUrl: "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/"
|
||||
vaultBaseUrl: "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/",
|
||||
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_APP_CONFIGURATION: {
|
||||
configurationUrl:
|
||||
"The URL of the Azure App Configuration to sync secrets to. Example: https://example.azconfig.io/",
|
||||
label: "An optional label to assign to secrets created in Azure App Configuration."
|
||||
label: "An optional label to assign to secrets created in Azure App Configuration.",
|
||||
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: {
|
||||
devopsProjectId: "The ID of the Azure DevOps project to sync secrets to.",
|
||||
|
@@ -496,7 +496,7 @@ export const overwriteSchema: {
|
||||
]
|
||||
},
|
||||
azureAppConfiguration: {
|
||||
name: "Azure App Configuration",
|
||||
name: "Azure App Connection: App Configuration",
|
||||
fields: [
|
||||
{
|
||||
key: "INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID",
|
||||
@@ -509,7 +509,7 @@ export const overwriteSchema: {
|
||||
]
|
||||
},
|
||||
azureKeyVault: {
|
||||
name: "Azure Key Vault",
|
||||
name: "Azure App Connection: Key Vault",
|
||||
fields: [
|
||||
{
|
||||
key: "INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID",
|
||||
@@ -522,7 +522,7 @@ export const overwriteSchema: {
|
||||
]
|
||||
},
|
||||
azureClientSecrets: {
|
||||
name: "Azure Client Secrets",
|
||||
name: "Azure App Connection: Client Secrets",
|
||||
fields: [
|
||||
{
|
||||
key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID",
|
||||
@@ -535,7 +535,7 @@ export const overwriteSchema: {
|
||||
]
|
||||
},
|
||||
azureDevOps: {
|
||||
name: "Azure DevOps",
|
||||
name: "Azure App Connection: DevOps",
|
||||
fields: [
|
||||
{
|
||||
key: "INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID",
|
||||
|
@@ -1,3 +1,4 @@
|
||||
export enum AzureAppConfigurationConnectionMethod {
|
||||
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";
|
||||
@@ -19,7 +20,10 @@ export const getAzureAppConfigurationConnectionListItem = () => {
|
||||
return {
|
||||
name: "Azure App Configuration" as const,
|
||||
app: AppConnection.AzureAppConfiguration as const,
|
||||
methods: Object.values(AzureAppConfigurationConnectionMethod) as [AzureAppConfigurationConnectionMethod.OAuth],
|
||||
methods: Object.values(AzureAppConfigurationConnectionMethod) as [
|
||||
AzureAppConfigurationConnectionMethod.OAuth,
|
||||
AzureAppConfigurationConnectionMethod.ClientSecret
|
||||
],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID
|
||||
};
|
||||
};
|
||||
@@ -35,71 +39,111 @@ export const validateAzureAppConfigurationConnectionCredentials = async (
|
||||
SITE_URL
|
||||
} = getConfig();
|
||||
|
||||
if (
|
||||
!INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID ||
|
||||
!INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_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://azconfig.io/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_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 AzureAppConfigurationConnectionMethod.OAuth:
|
||||
if (
|
||||
!INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID ||
|
||||
!INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_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;
|
||||
const oauthCredentials = inputCredentials as { code: string; tenantId?: string };
|
||||
try {
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", oauthCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: oauthCredentials.code,
|
||||
scope: `openid offline_access https://azconfig.io/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_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,
|
||||
tenantId: oauthCredentials.tenantId,
|
||||
accessToken: tokenResp.data.access_token,
|
||||
refreshToken: tokenResp.data.refresh_token,
|
||||
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||
};
|
||||
|
||||
case AzureAppConfigurationConnectionMethod.ClientSecret:
|
||||
const { tenantId, clientId, clientSecret } = inputCredentials as {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
try {
|
||||
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: `https://azconfig.io/.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 AzureAppConfigurationConnectionMethod}`
|
||||
message: `Unhandled Azure App Configuration connection method: ${method as AzureAppConfigurationConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -22,6 +22,29 @@ export const AzureAppConfigurationConnectionOAuthOutputCredentialsSchema = z.obj
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const AzureAppConfigurationConnectionClientSecretInputCredentialsSchema = z.object({
|
||||
clientId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.trim()
|
||||
.min(1, "Client ID required")
|
||||
.max(50, "Client ID must be at most 50 characters long"),
|
||||
clientSecret: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client Secret required")
|
||||
.max(50, "Client Secret must be at most 50 characters long"),
|
||||
tenantId: z.string().uuid().trim().min(1, "Tenant ID required")
|
||||
});
|
||||
|
||||
export const AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
tenantId: z.string(),
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const ValidateAzureAppConfigurationConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
@@ -30,6 +53,14 @@ export const ValidateAzureAppConfigurationConnectionCredentialsSchema = z.discri
|
||||
credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureAppConfiguration).credentials
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
method: z
|
||||
.literal(AzureAppConfigurationConnectionMethod.ClientSecret)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureAppConfiguration).method),
|
||||
credentials: AzureAppConfigurationConnectionClientSecretInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureAppConfiguration).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -39,9 +70,13 @@ export const CreateAzureAppConfigurationConnectionSchema = ValidateAzureAppConfi
|
||||
|
||||
export const UpdateAzureAppConfigurationConnectionSchema = z
|
||||
.object({
|
||||
credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.AzureAppConfiguration).credentials
|
||||
)
|
||||
credentials: z
|
||||
.union([
|
||||
AzureAppConfigurationConnectionOAuthInputCredentialsSchema,
|
||||
AzureAppConfigurationConnectionClientSecretInputCredentialsSchema
|
||||
])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.AzureAppConfiguration).credentials)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureAppConfiguration));
|
||||
|
||||
@@ -55,6 +90,10 @@ export const AzureAppConfigurationConnectionSchema = z.intersection(
|
||||
z.object({
|
||||
method: z.literal(AzureAppConfigurationConnectionMethod.OAuth),
|
||||
credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AzureAppConfigurationConnectionMethod.ClientSecret),
|
||||
credentials: AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -65,6 +104,13 @@ export const SanitizedAzureAppConfigurationConnectionSchema = z.discriminatedUni
|
||||
credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema.pick({
|
||||
tenantId: true
|
||||
})
|
||||
}),
|
||||
BaseAzureAppConfigurationConnectionSchema.extend({
|
||||
method: z.literal(AzureAppConfigurationConnectionMethod.ClientSecret),
|
||||
credentials: AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema.pick({
|
||||
clientId: true,
|
||||
tenantId: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema,
|
||||
AzureAppConfigurationConnectionOAuthOutputCredentialsSchema,
|
||||
AzureAppConfigurationConnectionSchema,
|
||||
CreateAzureAppConfigurationConnectionSchema,
|
||||
@@ -39,3 +40,7 @@ export type ExchangeCodeAzureResponse = {
|
||||
export type TAzureAppConfigurationConnectionCredentials = z.infer<
|
||||
typeof AzureAppConfigurationConnectionOAuthOutputCredentialsSchema
|
||||
>;
|
||||
|
||||
export type TAzureAppConfigurationConnectionClientSecretCredentials = z.infer<
|
||||
typeof AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema
|
||||
>;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export enum AzureDevOpsConnectionMethod {
|
||||
OAuth = "oauth",
|
||||
AccessToken = "access-token"
|
||||
AccessToken = "access-token",
|
||||
ClientSecret = "client-secret"
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ import { AppConnection } from "../app-connection-enums";
|
||||
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
|
||||
import {
|
||||
ExchangeCodeAzureResponse,
|
||||
TAzureDevOpsConnectionClientSecretCredentials,
|
||||
TAzureDevOpsConnectionConfig,
|
||||
TAzureDevOpsConnectionCredentials
|
||||
} from "./azure-devops-types";
|
||||
@@ -30,7 +31,8 @@ export const getAzureDevopsConnectionListItem = () => {
|
||||
app: AppConnection.AzureDevOps as const,
|
||||
methods: Object.values(AzureDevOpsConnectionMethod) as [
|
||||
AzureDevOpsConnectionMethod.OAuth,
|
||||
AzureDevOpsConnectionMethod.AccessToken
|
||||
AzureDevOpsConnectionMethod.AccessToken,
|
||||
AzureDevOpsConnectionMethod.ClientSecret
|
||||
],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID
|
||||
};
|
||||
@@ -53,11 +55,7 @@ export const getAzureDevopsConnection = async (
|
||||
});
|
||||
}
|
||||
|
||||
const credentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureDevOpsConnectionCredentials;
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Handle different connection methods
|
||||
switch (appConnection.method) {
|
||||
@@ -69,12 +67,17 @@ export const getAzureDevopsConnection = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (!("refreshToken" in credentials)) {
|
||||
const oauthCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureDevOpsConnectionCredentials;
|
||||
|
||||
if (!("refreshToken" in oauthCredentials)) {
|
||||
throw new BadRequestError({ message: "Invalid OAuth credentials" });
|
||||
}
|
||||
|
||||
const { refreshToken, tenantId } = credentials;
|
||||
const currentTime = Date.now();
|
||||
const { refreshToken, tenantId } = oauthCredentials;
|
||||
|
||||
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||
@@ -87,29 +90,75 @@ export const getAzureDevopsConnection = async (
|
||||
})
|
||||
);
|
||||
|
||||
const updatedCredentials = {
|
||||
...credentials,
|
||||
const updatedOAuthCredentials = {
|
||||
...oauthCredentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: currentTime + data.expires_in * 1000,
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
const encryptedOAuthCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedOAuthCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials });
|
||||
|
||||
return data.access_token;
|
||||
|
||||
case AzureDevOpsConnectionMethod.AccessToken:
|
||||
if (!("accessToken" in credentials)) {
|
||||
const accessTokenCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as { accessToken: string };
|
||||
|
||||
if (!("accessToken" in accessTokenCredentials)) {
|
||||
throw new BadRequestError({ message: "Invalid API token credentials" });
|
||||
}
|
||||
// For access token, return the basic auth token directly
|
||||
return credentials.accessToken;
|
||||
return accessTokenCredentials.accessToken;
|
||||
|
||||
case AzureDevOpsConnectionMethod.ClientSecret:
|
||||
const clientSecretCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureDevOpsConnectionClientSecretCredentials;
|
||||
|
||||
const { accessToken, expiresAt, clientId, clientSecret, tenantId: clientTenantId } = clientSecretCredentials;
|
||||
|
||||
// Check if token is still valid (with 5 minute buffer)
|
||||
if (accessToken && expiresAt && expiresAt > currentTime + 300000) {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", clientTenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: `https://app.vssps.visualstudio.com/.default`,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
})
|
||||
);
|
||||
|
||||
const updatedClientCredentials = {
|
||||
...clientSecretCredentials,
|
||||
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 BadRequestError({ message: `Unsupported connection method` });
|
||||
@@ -138,7 +187,7 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev
|
||||
let tokenError: AxiosError | null = null;
|
||||
|
||||
try {
|
||||
const oauthCredentials = inputCredentials as { code: string; tenantId: string };
|
||||
const oauthCredentials = inputCredentials as { code: string; tenantId: string; orgName: string };
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", oauthCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
@@ -262,9 +311,67 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev
|
||||
});
|
||||
}
|
||||
|
||||
case AzureDevOpsConnectionMethod.ClientSecret:
|
||||
const { tenantId, clientId, clientSecret, orgName } = inputCredentials as {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
orgName: string;
|
||||
};
|
||||
|
||||
try {
|
||||
// First, get the access token using client credentials flow
|
||||
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: `https://app.vssps.visualstudio.com/.default`,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
})
|
||||
);
|
||||
|
||||
// Validate access to the specific organization
|
||||
const response = await request.get(
|
||||
`${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/projects?api-version=7.2-preview.2&$top=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${clientData.access_token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate connection to organization '${orgName}': ${response.status}`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tenantId,
|
||||
clientId,
|
||||
clientSecret,
|
||||
orgName,
|
||||
accessToken: clientData.access_token,
|
||||
expiresAt: Date.now() + clientData.expires_in * 1000
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to authenticate with Azure DevOps using client credentials: ${
|
||||
(e?.response?.data as { error_description?: string })?.error_description || e.message
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to validate Azure DevOps client credentials"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
message: `Unhandled Azure connection method: ${method as AzureDevOpsConnectionMethod}`
|
||||
message: `Unhandled Azure DevOps connection method: ${method as AzureDevOpsConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -38,6 +38,42 @@ export const AzureDevOpsConnectionAccessTokenOutputCredentialsSchema = z.object(
|
||||
orgName: z.string()
|
||||
});
|
||||
|
||||
export const AzureDevOpsConnectionClientSecretInputCredentialsSchema = 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_DEVOPS.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_DEVOPS.clientSecret),
|
||||
tenantId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.trim()
|
||||
.min(1, "Tenant ID required")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.tenantId),
|
||||
orgName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Organization name required")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.orgName)
|
||||
});
|
||||
|
||||
export const AzureDevOpsConnectionClientSecretOutputCredentialsSchema = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
tenantId: z.string(),
|
||||
orgName: z.string(),
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const ValidateAzureDevOpsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
@@ -54,6 +90,14 @@ export const ValidateAzureDevOpsConnectionCredentialsSchema = z.discriminatedUni
|
||||
credentials: AzureDevOpsConnectionAccessTokenInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureDevOps).credentials
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
method: z
|
||||
.literal(AzureDevOpsConnectionMethod.ClientSecret)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureDevOps).method),
|
||||
credentials: AzureDevOpsConnectionClientSecretInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureDevOps).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -64,7 +108,11 @@ export const CreateAzureDevOpsConnectionSchema = ValidateAzureDevOpsConnectionCr
|
||||
export const UpdateAzureDevOpsConnectionSchema = z
|
||||
.object({
|
||||
credentials: z
|
||||
.union([AzureDevOpsConnectionOAuthInputCredentialsSchema, AzureDevOpsConnectionAccessTokenInputCredentialsSchema])
|
||||
.union([
|
||||
AzureDevOpsConnectionOAuthInputCredentialsSchema,
|
||||
AzureDevOpsConnectionAccessTokenInputCredentialsSchema,
|
||||
AzureDevOpsConnectionClientSecretInputCredentialsSchema
|
||||
])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.AzureDevOps).credentials)
|
||||
})
|
||||
@@ -84,6 +132,10 @@ export const AzureDevOpsConnectionSchema = z.intersection(
|
||||
z.object({
|
||||
method: z.literal(AzureDevOpsConnectionMethod.AccessToken),
|
||||
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AzureDevOpsConnectionMethod.ClientSecret),
|
||||
credentials: AzureDevOpsConnectionClientSecretOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -101,6 +153,14 @@ export const SanitizedAzureDevOpsConnectionSchema = z.discriminatedUnion("method
|
||||
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema.pick({
|
||||
orgName: true
|
||||
})
|
||||
}),
|
||||
BaseAzureDevOpsConnectionSchema.extend({
|
||||
method: z.literal(AzureDevOpsConnectionMethod.ClientSecret),
|
||||
credentials: AzureDevOpsConnectionClientSecretOutputCredentialsSchema.pick({
|
||||
clientId: true,
|
||||
tenantId: true,
|
||||
orgName: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
|
@@ -52,6 +52,11 @@ const getAuthHeaders = (appConnection: TAzureDevOpsConnection, accessToken: stri
|
||||
Authorization: `Basic ${basicAuthToken}`,
|
||||
Accept: "application/json"
|
||||
};
|
||||
case AzureDevOpsConnectionMethod.ClientSecret:
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
};
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unsupported connection method" });
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
AzureDevOpsConnectionClientSecretOutputCredentialsSchema,
|
||||
AzureDevOpsConnectionOAuthOutputCredentialsSchema,
|
||||
AzureDevOpsConnectionSchema,
|
||||
CreateAzureDevOpsConnectionSchema,
|
||||
@@ -27,6 +28,10 @@ export type TAzureDevOpsConnectionConfig = DiscriminativePick<
|
||||
|
||||
export type TAzureDevOpsConnectionCredentials = z.infer<typeof AzureDevOpsConnectionOAuthOutputCredentialsSchema>;
|
||||
|
||||
export type TAzureDevOpsConnectionClientSecretCredentials = z.infer<
|
||||
typeof AzureDevOpsConnectionClientSecretOutputCredentialsSchema
|
||||
>;
|
||||
|
||||
export interface ExchangeCodeAzureResponse {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
export enum AzureKeyVaultConnectionMethod {
|
||||
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,25 +17,16 @@ import { AppConnection } from "../app-connection-enums";
|
||||
import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums";
|
||||
import {
|
||||
ExchangeCodeAzureResponse,
|
||||
TAzureKeyVaultConnectionClientSecretCredentials,
|
||||
TAzureKeyVaultConnectionConfig,
|
||||
TAzureKeyVaultConnectionCredentials
|
||||
} from "./azure-key-vault-connection-types";
|
||||
|
||||
export const getAzureConnectionAccessToken = async (
|
||||
connectionId: string,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
if (
|
||||
!appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID ||
|
||||
!appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `Azure environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
|
||||
if (!appConnection) {
|
||||
@@ -49,49 +41,101 @@ export const getAzureConnectionAccessToken = async (
|
||||
throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not a valid Azure connection` });
|
||||
}
|
||||
|
||||
const credentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureKeyVaultConnectionCredentials;
|
||||
const currentTime = Date.now();
|
||||
|
||||
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`,
|
||||
client_id: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
|
||||
client_secret: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
|
||||
refresh_token: credentials.refreshToken
|
||||
})
|
||||
);
|
||||
switch (appConnection.method) {
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
const appCfg = getConfig();
|
||||
if (
|
||||
!appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID ||
|
||||
!appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `Azure environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
const accessExpiresAt = new Date();
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
const oauthCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureKeyVaultConnectionCredentials;
|
||||
|
||||
const updatedCredentials = {
|
||||
...credentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: accessExpiresAt.getTime(),
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", oauthCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
scope: `openid offline_access https://vault.azure.net/.default`,
|
||||
client_id: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
|
||||
client_secret: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
|
||||
refresh_token: oauthCredentials.refreshToken
|
||||
})
|
||||
);
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
const updatedOAuthCredentials = {
|
||||
...oauthCredentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: currentTime + data.expires_in * 1000,
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
|
||||
await appConnectionDAL.update(
|
||||
{ id: connectionId },
|
||||
{
|
||||
encryptedCredentials
|
||||
}
|
||||
);
|
||||
const encryptedOAuthCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedOAuthCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: data.access_token
|
||||
};
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials });
|
||||
|
||||
return {
|
||||
accessToken: data.access_token
|
||||
};
|
||||
|
||||
case AzureKeyVaultConnectionMethod.ClientSecret:
|
||||
const clientSecretCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureKeyVaultConnectionClientSecretCredentials;
|
||||
|
||||
const { accessToken, expiresAt, clientId, clientSecret, tenantId } = clientSecretCredentials;
|
||||
|
||||
// Check if token is still valid (with 5 minute buffer)
|
||||
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://vault.azure.net/.default`,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
})
|
||||
);
|
||||
|
||||
const updatedClientCredentials = {
|
||||
...clientSecretCredentials,
|
||||
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 { accessToken: clientData.access_token };
|
||||
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
message: `Unhandled Azure Key Vault connection method: ${appConnection.method as AzureKeyVaultConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getAzureKeyVaultConnectionListItem = () => {
|
||||
@@ -100,7 +144,10 @@ export const getAzureKeyVaultConnectionListItem = () => {
|
||||
return {
|
||||
name: "Azure Key Vault" as const,
|
||||
app: AppConnection.AzureKeyVault as const,
|
||||
methods: Object.values(AzureKeyVaultConnectionMethod) as [AzureKeyVaultConnectionMethod.OAuth],
|
||||
methods: Object.values(AzureKeyVaultConnectionMethod) as [
|
||||
AzureKeyVaultConnectionMethod.OAuth,
|
||||
AzureKeyVaultConnectionMethod.ClientSecret
|
||||
],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID
|
||||
};
|
||||
};
|
||||
@@ -111,68 +158,108 @@ export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureK
|
||||
const { INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID, INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET, SITE_URL } =
|
||||
getConfig();
|
||||
|
||||
if (!INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || !INF_APP_CONNECTION_AZURE_KEY_VAULT_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://vault.azure.net/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_KEY_VAULT_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 AzureKeyVaultConnectionMethod.OAuth:
|
||||
if (!INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || !INF_APP_CONNECTION_AZURE_KEY_VAULT_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;
|
||||
const oauthCredentials = inputCredentials as { code: string; tenantId?: string };
|
||||
try {
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", oauthCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: oauthCredentials.code,
|
||||
scope: `openid offline_access https://vault.azure.net/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_KEY_VAULT_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,
|
||||
tenantId: oauthCredentials.tenantId,
|
||||
accessToken: tokenResp.data.access_token,
|
||||
refreshToken: tokenResp.data.refresh_token,
|
||||
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||
};
|
||||
|
||||
case AzureKeyVaultConnectionMethod.ClientSecret:
|
||||
const { tenantId, clientId, clientSecret } = inputCredentials as {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
try {
|
||||
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: `https://vault.azure.net/.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 AzureKeyVaultConnectionMethod}`
|
||||
message: `Unhandled Azure Key Vault connection method: ${method as AzureKeyVaultConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -22,6 +22,29 @@ export const AzureKeyVaultConnectionOAuthOutputCredentialsSchema = z.object({
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const AzureKeyVaultConnectionClientSecretInputCredentialsSchema = z.object({
|
||||
clientId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.trim()
|
||||
.min(1, "Client ID required")
|
||||
.max(50, "Client ID must be at most 50 characters long"),
|
||||
clientSecret: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client Secret required")
|
||||
.max(50, "Client Secret must be at most 50 characters long"),
|
||||
tenantId: z.string().uuid().trim().min(1, "Tenant ID required")
|
||||
});
|
||||
|
||||
export const AzureKeyVaultConnectionClientSecretOutputCredentialsSchema = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
tenantId: z.string(),
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const ValidateAzureKeyVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
@@ -30,6 +53,14 @@ export const ValidateAzureKeyVaultConnectionCredentialsSchema = z.discriminatedU
|
||||
credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureKeyVault).credentials
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
method: z
|
||||
.literal(AzureKeyVaultConnectionMethod.ClientSecret)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureKeyVault).method),
|
||||
credentials: AzureKeyVaultConnectionClientSecretInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureKeyVault).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -39,9 +70,13 @@ export const CreateAzureKeyVaultConnectionSchema = ValidateAzureKeyVaultConnecti
|
||||
|
||||
export const UpdateAzureKeyVaultConnectionSchema = z
|
||||
.object({
|
||||
credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.AzureKeyVault).credentials
|
||||
)
|
||||
credentials: z
|
||||
.union([
|
||||
AzureKeyVaultConnectionOAuthInputCredentialsSchema,
|
||||
AzureKeyVaultConnectionClientSecretInputCredentialsSchema
|
||||
])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.AzureKeyVault).credentials)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureKeyVault));
|
||||
|
||||
@@ -55,6 +90,10 @@ export const AzureKeyVaultConnectionSchema = z.intersection(
|
||||
z.object({
|
||||
method: z.literal(AzureKeyVaultConnectionMethod.OAuth),
|
||||
credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AzureKeyVaultConnectionMethod.ClientSecret),
|
||||
credentials: AzureKeyVaultConnectionClientSecretOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -65,6 +104,13 @@ export const SanitizedAzureKeyVaultConnectionSchema = z.discriminatedUnion("meth
|
||||
credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema.pick({
|
||||
tenantId: true
|
||||
})
|
||||
}),
|
||||
BaseAzureKeyVaultConnectionSchema.extend({
|
||||
method: z.literal(AzureKeyVaultConnectionMethod.ClientSecret),
|
||||
credentials: AzureKeyVaultConnectionClientSecretOutputCredentialsSchema.pick({
|
||||
clientId: true,
|
||||
tenantId: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
AzureKeyVaultConnectionClientSecretOutputCredentialsSchema,
|
||||
AzureKeyVaultConnectionOAuthOutputCredentialsSchema,
|
||||
AzureKeyVaultConnectionSchema,
|
||||
CreateAzureKeyVaultConnectionSchema,
|
||||
@@ -36,3 +37,7 @@ export type ExchangeCodeAzureResponse = {
|
||||
};
|
||||
|
||||
export type TAzureKeyVaultConnectionCredentials = z.infer<typeof AzureKeyVaultConnectionOAuthOutputCredentialsSchema>;
|
||||
|
||||
export type TAzureKeyVaultConnectionClientSecretCredentials = z.infer<
|
||||
typeof AzureKeyVaultConnectionClientSecretOutputCredentialsSchema
|
||||
>;
|
||||
|
@@ -13,7 +13,7 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types";
|
||||
|
||||
type TAzureAppConfigurationSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import { SecretSyncError } from "../secret-sync-errors";
|
||||
import { GetAzureKeyVaultSecret, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault-sync-types";
|
||||
|
||||
type TAzureKeyVaultSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 446 KiB |
Binary file not shown.
After Width: | Height: | Size: 424 KiB |
Binary file not shown.
After Width: | Height: | Size: 442 KiB |
@@ -3,7 +3,7 @@ title: "Azure App Configuration Connection"
|
||||
description: "Learn how to configure a Azure App Configuration Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical currently only supports one method for connecting to Azure, which is OAuth.
|
||||
Infisical currently only supports two methods for connecting to Azure, which are OAuth and Client Secrets.
|
||||
|
||||
<Accordion title="Self-Hosted Instance">
|
||||
Using the Azure App Configuration connection on a self-hosted instance of Infisical requires configuring an application in Azure
|
||||
@@ -58,6 +58,26 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
||||
</Steps>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Client Secret Authentication">
|
||||
To use client secret authentication, ensure your Azure Service Principal has the required permissions and is connected to the Azure App Configuration resources you want to use.
|
||||
|
||||
**Prerequisites:**
|
||||
- Set up Azure and have an existing App Configuration instance.
|
||||
- The service principal must be connected to your target Azure App Configuration resource(s)
|
||||
|
||||
<Steps>
|
||||
<Step title="Assign API permissions to the service principal">
|
||||
|
||||
Configure the required API permissions for your App Registration to interact with Azure App Configuration:
|
||||
|
||||
#### Azure App Configuration permissions
|
||||
|
||||
Set the API permissions of your Azure service principal to include the following Azure App Configuration permissions: `KeyValue.Delete`, `KeyValue.Read`, and `KeyValue.Write`.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
## Setup Azure Connection in Infisical
|
||||
|
||||
@@ -66,25 +86,35 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
||||
Navigate to the **App Connections** tab on the **Organization Settings** page. 
|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **Azure Connection** option from the connection options modal. 
|
||||
</Step>
|
||||
<Step title="Authorize Connection">
|
||||
You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID.
|
||||
<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">
|
||||
You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID.
|
||||
|
||||
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 redirect you 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 redirect you back to Infisical's App Connections page. 
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||

|
||||
</Step>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **Azure App Configuration Connection** is now available for use. 
|
||||
</Step>
|
||||
</Steps>
|
||||
|
@@ -3,7 +3,7 @@ title: "Azure Client Secrets Connection"
|
||||
description: "Learn how to configure an Azure Client Secrets Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical currently only supports one method for connecting to Azure, which is OAuth.
|
||||
Infisical currently only supports two methods for connecting to Azure, which are OAuth and Client Secrets.
|
||||
|
||||
<Accordion title="Self-Hosted Instance">
|
||||
Using the Azure Client Secrets connection on a self-hosted instance of Infisical requires configuring an application in Azure
|
||||
|
@@ -3,7 +3,7 @@ title: "Azure DevOps Connection"
|
||||
description: "Learn how to configure an Azure DevOps Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical currently supports two methods for connecting to Azure DevOps, which are OAuth and Azure DevOps Personal Access Token.
|
||||
Infisical currently supports three methods for connecting to Azure DevOps, which are OAuth, Azure DevOps Personal Access Token and Client Secrets.
|
||||
|
||||
<Accordion title="Azure OAuth on a Self-Hosted Instance">
|
||||
Using the Azure DevOps <b>OAuth connection</b> on a self-hosted instance of Infisical requires configuring an application in Azure
|
||||
@@ -87,6 +87,32 @@ Infisical currently supports two methods for connecting to Azure DevOps, which a
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Client Secret Authentication">
|
||||
To use client secret authentication, ensure your Azure Service Principal has the required permissions and is connected to the Azure DevOps organization and projects you want to use.
|
||||
|
||||
**Prerequisites:**
|
||||
- Set up Azure and have an existing Azure DevOps organization.
|
||||
- The service principal must be connected to your target Azure DevOps organization and project(s)
|
||||
|
||||
<Steps>
|
||||
<Step title="Assign API permissions to the service principal">
|
||||
|
||||
Configure the required API permissions for your App Registration to interact with Azure DevOps:
|
||||
|
||||
#### Azure DevOps permissions
|
||||
|
||||
Set the API permissions of your Azure service principal to include the following Azure DevOps permissions:
|
||||
- Azure DevOps
|
||||
- `user_impersonation`
|
||||
- `vso.project_write`
|
||||
- `vso.variablegroups_manage`
|
||||
- `vso.variablegroups_write`
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
## Setup Azure Connection in Infisical
|
||||
|
||||
<Steps>
|
||||
@@ -129,6 +155,17 @@ Infisical currently supports two methods for connecting to Azure DevOps, which a
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
<Tab title="Client Secret">
|
||||
<Steps>
|
||||
<Step title="Create Connection">
|
||||
Fill in the **Tenant ID**, **Client ID**, **Client Secret** and **Organization Name** fields with the Directory (Tenant) ID, Application (Client) ID, Client Secret and the organization name you obtained in the previous step.
|
||||
<Tip>
|
||||
You can find the **Organization Name** on https://dev.azure.com/
|
||||
</Tip>
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
|
@@ -3,7 +3,7 @@ title: "Azure Key Vault Connection"
|
||||
description: "Learn how to configure a Azure Key Vault Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical currently only supports one method for connecting to Azure, which is OAuth.
|
||||
Infisical currently only supports two methods for connecting to Azure, which are OAuth and Client Secrets.
|
||||
|
||||
<Accordion title="Self-Hosted Instance">
|
||||
Using the Azure Key Vault connection on a self-hosted instance of Infisical requires configuring an application in Azure
|
||||
@@ -58,6 +58,27 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Client Secret Authentication">
|
||||
To use client secret authentication, ensure your Azure Service Principal has the required permissions and is connected to the Azure Key Vault instances you want to use.
|
||||
|
||||
**Prerequisites:**
|
||||
- Set up Azure and have an existing Key Vault instance.
|
||||
- The service principal must be connected to your target Azure Key Vault instance(s)
|
||||
|
||||
<Steps>
|
||||
<Step title="Assign API permissions to the service principal">
|
||||
|
||||
Configure the required API permissions for your App Registration to interact with Azure Key Vault:
|
||||
|
||||
#### Azure Key Vault permissions
|
||||
|
||||
Set the API permissions of your Azure service principal to include `user_impersonation` for the Key Vault API.
|
||||

|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
## Setup Azure Connection in Infisical
|
||||
|
||||
<Steps>
|
||||
@@ -68,21 +89,37 @@ 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">
|
||||
You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID.
|
||||
<Step title="Create Connection">
|
||||
<Tabs>
|
||||
<Tab title="OAuth">
|
||||
<Steps>
|
||||
<Step title="Authorize Connection">
|
||||
You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID.
|
||||
|
||||
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 redirect you back to Infisical's App Connections page. 
|
||||
</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 redirect you back to Infisical's App Connections page. 
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
<Tab title="Client Secret">
|
||||
<Steps>
|
||||
<Step title="Create Connection">
|
||||
Fill in the **Tenant ID**, **Client ID**, **Client Secret** fields with the Directory (Tenant) ID, Application (Client) ID, Client Secret you obtained in the previous step.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **Azure Key Vault Connection** is now available for use. 
|
||||
</Step>
|
||||
|
@@ -172,6 +172,9 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
case ChecklyConnectionMethod.ApiKey:
|
||||
return { name: "API Key", icon: faKey };
|
||||
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||
case AzureAppConfigurationConnectionMethod.ClientSecret:
|
||||
case AzureKeyVaultConnectionMethod.ClientSecret:
|
||||
case AzureDevOpsConnectionMethod.ClientSecret:
|
||||
return { name: "Client Secret", icon: faKey };
|
||||
default:
|
||||
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";
|
||||
|
||||
export enum AzureAppConfigurationConnectionMethod {
|
||||
OAuth = "oauth"
|
||||
OAuth = "oauth",
|
||||
ClientSecret = "client-secret"
|
||||
}
|
||||
|
||||
export type TAzureAppConfigurationConnection = TRootAppConnection & {
|
||||
app: AppConnection.AzureAppConfiguration;
|
||||
} & {
|
||||
method: AzureAppConfigurationConnectionMethod.OAuth;
|
||||
credentials: {
|
||||
code: string;
|
||||
tenantId?: string;
|
||||
};
|
||||
};
|
||||
} & (
|
||||
| {
|
||||
method: AzureAppConfigurationConnectionMethod.OAuth;
|
||||
credentials: {
|
||||
code: string;
|
||||
tenantId?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
method: AzureAppConfigurationConnectionMethod.ClientSecret;
|
||||
credentials: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
tenantId: string;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@@ -3,7 +3,8 @@ import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-con
|
||||
|
||||
export enum AzureDevOpsConnectionMethod {
|
||||
OAuth = "oauth",
|
||||
AccessToken = "access-token"
|
||||
AccessToken = "access-token",
|
||||
ClientSecret = "client-secret"
|
||||
}
|
||||
|
||||
export type TAzureDevOpsConnection = TRootAppConnection & {
|
||||
@@ -24,4 +25,13 @@ export type TAzureDevOpsConnection = TRootAppConnection & {
|
||||
orgName: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
method: AzureDevOpsConnectionMethod.ClientSecret;
|
||||
credentials: {
|
||||
clientSecret: string;
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
orgName: string;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@@ -2,13 +2,24 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum AzureKeyVaultConnectionMethod {
|
||||
OAuth = "oauth"
|
||||
OAuth = "oauth",
|
||||
ClientSecret = "client-secret"
|
||||
}
|
||||
|
||||
export type TAzureKeyVaultConnection = TRootAppConnection & { app: AppConnection.AzureKeyVault } & {
|
||||
method: AzureKeyVaultConnectionMethod.OAuth;
|
||||
credentials: {
|
||||
code: string;
|
||||
tenantId?: string;
|
||||
};
|
||||
};
|
||||
export type TAzureKeyVaultConnection = TRootAppConnection & { app: AppConnection.AzureKeyVault } & (
|
||||
| {
|
||||
method: AzureKeyVaultConnectionMethod.OAuth;
|
||||
credentials: {
|
||||
code: string;
|
||||
tenantId?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
method: AzureKeyVaultConnectionMethod.ClientSecret;
|
||||
credentials: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
tenantId: string;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@@ -8,10 +8,13 @@ import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
||||
import { Banner } from "@app/components/page-frames/Banner";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useFetchServerStatus } from "@app/hooks/api";
|
||||
|
||||
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
|
||||
import { Navbar } from "./components/NavBar";
|
||||
import { OrgSidebar } from "./components/OrgSidebar";
|
||||
import { RedisBanner } from "./components/RedisBanner";
|
||||
import { SmtpBanner } from "./components/SmtpBanner";
|
||||
|
||||
export const OrganizationLayout = () => {
|
||||
const { config } = useServerConfig();
|
||||
@@ -27,6 +30,8 @@ export const OrganizationLayout = () => {
|
||||
|
||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||
|
||||
const { data: serverDetails, isLoading } = useFetchServerStatus();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Banner />
|
||||
@@ -34,6 +39,8 @@ export const OrganizationLayout = () => {
|
||||
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
|
||||
>
|
||||
<Navbar />
|
||||
{!isLoading && !serverDetails?.redisConfigured && <RedisBanner />}
|
||||
{!isLoading && !serverDetails?.emailConfigured && <SmtpBanner />}
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<OrgSidebar isHidden={isInsideProject} />
|
||||
|
@@ -1,37 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton } from "@app/components/v2";
|
||||
import { OrgAlertBanner } from "../OrgAlertBanner";
|
||||
|
||||
export const InsecureConnectionBanner = () => {
|
||||
const [isAcknowledged, setIsAcknowledged] = useState(
|
||||
localStorage.getItem("insecureConnectionAcknowledged") ?? false
|
||||
);
|
||||
|
||||
const handleDismiss = () => {
|
||||
setIsAcknowledged(true);
|
||||
localStorage.setItem("insecureConnectionAcknowledged", "true");
|
||||
};
|
||||
|
||||
if (isAcknowledged) return null;
|
||||
|
||||
return (
|
||||
<div className="flex w-screen items-start border-b border-red-900 bg-red-700 px-2 py-1 font-inter text-sm text-mineshaft-200">
|
||||
<FontAwesomeIcon className="ml-3.5 mt-1" icon={faWarning} />
|
||||
<span className="mx-1 ml-2 mt-[0.04rem]">
|
||||
Your connection to this Infisical instance is not secured via HTTPS. Some features may not
|
||||
behave as expected.
|
||||
</span>
|
||||
<IconButton
|
||||
size="xs"
|
||||
className="ml-auto"
|
||||
colorSchema="danger"
|
||||
onClick={handleDismiss}
|
||||
ariaLabel="Dismiss banner"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<OrgAlertBanner
|
||||
text="Your connection to this Infisical instance is not secured via HTTPS. Some features may not
|
||||
behave as expected."
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@@ -0,0 +1,52 @@
|
||||
import { faArrowUpRightFromSquare, faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
export const OrgAlertBanner = ({ text, link }: Props) => {
|
||||
const [isDismissed, setIsDismissed] = useToggle(false);
|
||||
|
||||
if (isDismissed) return null;
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center border-b border-yellow/50 bg-yellow/30 px-4 py-2 text-sm text-yellow-200">
|
||||
<FontAwesomeIcon icon={faWarning} className="mr-2.5 text-base text-yellow" />
|
||||
{text}{" "}
|
||||
{link && (
|
||||
<>
|
||||
Learn how to configure it
|
||||
<a
|
||||
href={link}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="group flex items-center"
|
||||
>
|
||||
<span className="cursor-pointer pl-1 text-yellow-500 underline underline-offset-2 duration-100 group-hover:text-mineshaft-100 group-hover:decoration-mineshaft-100">
|
||||
here
|
||||
</span>
|
||||
<FontAwesomeIcon
|
||||
className="ml-0.5 mt-0.5 text-yellow group-hover:text-mineshaft-100"
|
||||
icon={faArrowUpRightFromSquare}
|
||||
size="xs"
|
||||
/>
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
<IconButton
|
||||
className="ml-auto p-0 text-yellow-200"
|
||||
ariaLabel="Dismiss banner"
|
||||
variant="plain"
|
||||
onClick={() => setIsDismissed.on()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export * from "./OrgAlertBanner";
|
@@ -0,0 +1,10 @@
|
||||
import { OrgAlertBanner } from "../OrgAlertBanner";
|
||||
|
||||
export const RedisBanner = () => {
|
||||
return (
|
||||
<OrgAlertBanner
|
||||
text="Attention: Updated versions of Infisical now require Redis for full functionality."
|
||||
link="https://infisical.com/docs/self-hosting/configuration/requirements#redis"
|
||||
/>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export * from "./RedisBanner";
|
@@ -0,0 +1,10 @@
|
||||
import { OrgAlertBanner } from "../OrgAlertBanner";
|
||||
|
||||
export const SmtpBanner = () => {
|
||||
return (
|
||||
<OrgAlertBanner
|
||||
text="Attention: SMTP has not been configured for this instance."
|
||||
link="https://infisical.com/docs/self-hosting/configuration/envars#email-service"
|
||||
/>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export * from "./SmtpBanner";
|
@@ -92,9 +92,9 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||
case AppConnection.GCP:
|
||||
return <GcpConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureKeyVault:
|
||||
return <AzureKeyVaultConnectionForm />;
|
||||
return <AzureKeyVaultConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureAppConfiguration:
|
||||
return <AzureAppConfigurationConnectionForm />;
|
||||
return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Databricks:
|
||||
return <DatabricksConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Humanitec:
|
||||
@@ -200,9 +200,11 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
case AppConnection.GCP:
|
||||
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureKeyVault:
|
||||
return <AzureKeyVaultConnectionForm appConnection={appConnection} />;
|
||||
return <AzureKeyVaultConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureAppConfiguration:
|
||||
return <AzureAppConfigurationConnectionForm appConnection={appConnection} />;
|
||||
return (
|
||||
<AzureAppConfigurationConnectionForm appConnection={appConnection} onSubmit={onSubmit} />
|
||||
);
|
||||
case AppConnection.Databricks:
|
||||
return <DatabricksConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Humanitec:
|
||||
|
@@ -20,19 +20,83 @@ import {
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
|
||||
|
||||
type Props = {
|
||||
appConnection?: TAzureAppConfigurationConnection;
|
||||
onSubmit: (formData: ClientSecretForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||
const baseSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.AzureAppConfiguration),
|
||||
method: z.nativeEnum(AzureAppConfigurationConnectionMethod),
|
||||
tenantId: z.string().trim().optional()
|
||||
method: z.nativeEnum(AzureAppConfigurationConnectionMethod)
|
||||
});
|
||||
|
||||
const oauthSchema = baseSchema.extend({
|
||||
tenantId: z.string().trim().min(1, "Tenant ID is required"),
|
||||
method: z.literal(AzureAppConfigurationConnectionMethod.OAuth)
|
||||
});
|
||||
|
||||
const clientSecretSchema = baseSchema.extend({
|
||||
method: z.literal(AzureAppConfigurationConnectionMethod.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 AzureAppConfigurationConnectionForm = ({ appConnection }: Props) => {
|
||||
const getDefaultValues = (appConnection?: TAzureAppConfigurationConnection): Partial<FormData> => {
|
||||
if (!appConnection) {
|
||||
return {
|
||||
app: AppConnection.AzureAppConfiguration,
|
||||
method: AzureAppConfigurationConnectionMethod.OAuth
|
||||
};
|
||||
}
|
||||
|
||||
const base = {
|
||||
name: appConnection.name,
|
||||
description: appConnection.description,
|
||||
app: appConnection.app,
|
||||
method: appConnection.method
|
||||
};
|
||||
const { credentials } = appConnection;
|
||||
|
||||
switch (appConnection.method) {
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
if ("tenantId" in credentials) {
|
||||
return {
|
||||
...base,
|
||||
method: AzureAppConfigurationConnectionMethod.OAuth,
|
||||
tenantId: credentials.tenantId
|
||||
};
|
||||
}
|
||||
break;
|
||||
case AzureAppConfigurationConnectionMethod.ClientSecret:
|
||||
if ("clientSecret" in credentials && "clientId" in credentials) {
|
||||
return {
|
||||
...base,
|
||||
method: AzureAppConfigurationConnectionMethod.ClientSecret,
|
||||
credentials: {
|
||||
clientSecret: credentials.clientSecret,
|
||||
clientId: credentials.clientId,
|
||||
tenantId: credentials.tenantId
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
@@ -43,41 +107,36 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) =>
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection
|
||||
? {
|
||||
...appConnection,
|
||||
tenantId: appConnection.credentials.tenantId
|
||||
}
|
||||
: {
|
||||
app: AppConnection.AzureAppConfiguration,
|
||||
method: AzureAppConfigurationConnectionMethod.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 = async (formData: FormData) => {
|
||||
const state = crypto.randomBytes(16).toString("hex");
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
localStorage.setItem(
|
||||
"azureAppConfigurationConnectionFormData",
|
||||
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||
);
|
||||
|
||||
switch (formData.method) {
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
setIsRedirecting(true);
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
localStorage.setItem(
|
||||
"azureAppConfigurationConnectionFormData",
|
||||
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-app-configuration`
|
||||
);
|
||||
break;
|
||||
case AzureAppConfigurationConnectionMethod.ClientSecret:
|
||||
await onSubmit(formData);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||
}
|
||||
@@ -89,6 +148,9 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) =>
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
isMissingConfig = !oauthClientId;
|
||||
break;
|
||||
case AzureAppConfigurationConnectionMethod.ClientSecret:
|
||||
isMissingConfig = false;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Azure Connection method: ${selectedMethod}`);
|
||||
}
|
||||
@@ -97,25 +159,8 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) =>
|
||||
|
||||
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 Azure Active Directory (Entra ID) Tenant ID."
|
||||
isError={Boolean(error?.message)}
|
||||
label="Tenant ID"
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
@@ -155,6 +200,65 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) =>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="tenantId"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="The Azure Active Directory (Entra ID) Tenant ID."
|
||||
isError={Boolean(error?.message)}
|
||||
label="Tenant ID"
|
||||
isOptional={selectedMethod === AzureAppConfigurationConnectionMethod.OAuth}
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Client Secret-specific fields */}
|
||||
{selectedMethod === AzureAppConfigurationConnectionMethod.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="~JzD8e6S.tH~w8XRaNnKcb7W1fM4rCns7FY"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
@@ -121,7 +121,7 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
|
||||
|
||||
const selectedMethod = watch("method");
|
||||
|
||||
const onSubmitHandler = (formData: FormData) => {
|
||||
const onSubmitHandler = async (formData: FormData) => {
|
||||
const state = crypto.randomBytes(16).toString("hex");
|
||||
switch (formData.method) {
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
@@ -137,7 +137,7 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
|
||||
break;
|
||||
|
||||
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||
onSubmit(formData);
|
||||
await onSubmit(formData);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||
@@ -240,7 +240,11 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
|
||||
label="Client Secret"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" placeholder="Enter your Client Secret" />
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
placeholder="~JzD8e6S.tH~w8XRaNnKcb7W1fM4rCns7FY"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@@ -21,13 +21,6 @@ import {
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type AccessTokenForm = z.infer<typeof accessTokenSchema>;
|
||||
|
||||
type Props = {
|
||||
appConnection?: TAzureDevOpsConnection;
|
||||
onSubmit: (formData: AccessTokenForm) => Promise<void>;
|
||||
};
|
||||
|
||||
// Base schema with common fields
|
||||
const baseSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.AzureDevOps),
|
||||
@@ -49,10 +42,30 @@ const accessTokenSchema = baseSchema.extend({
|
||||
})
|
||||
});
|
||||
|
||||
const clientSecretSchema = baseSchema.extend({
|
||||
method: z.literal(AzureDevOpsConnectionMethod.ClientSecret),
|
||||
credentials: z.object({
|
||||
clientSecret: z.string().trim().min(1, "Client Secret is required"),
|
||||
tenantId: z.string().trim().min(1, "Tenant ID is required"),
|
||||
clientId: z.string().trim().min(1, "Client ID is required"),
|
||||
orgName: z.string().trim().min(1, "Organization name is required")
|
||||
})
|
||||
});
|
||||
|
||||
// Union schema
|
||||
const formSchema = z.discriminatedUnion("method", [oauthSchema, accessTokenSchema]);
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
oauthSchema,
|
||||
accessTokenSchema,
|
||||
clientSecretSchema
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
type OnSubmitForm = z.infer<typeof accessTokenSchema> | z.infer<typeof clientSecretSchema>;
|
||||
|
||||
type Props = {
|
||||
appConnection?: TAzureDevOpsConnection;
|
||||
onSubmit: (formData: OnSubmitForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial<FormData> => {
|
||||
if (!appConnection) {
|
||||
@@ -93,6 +106,25 @@ const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial<FormD
|
||||
};
|
||||
}
|
||||
break;
|
||||
case AzureDevOpsConnectionMethod.ClientSecret:
|
||||
if (
|
||||
"clientSecret" in credentials &&
|
||||
"tenantId" in credentials &&
|
||||
"clientId" in credentials &&
|
||||
"orgName" in credentials
|
||||
) {
|
||||
return {
|
||||
...base,
|
||||
method: AzureDevOpsConnectionMethod.ClientSecret,
|
||||
credentials: {
|
||||
clientSecret: credentials.clientSecret,
|
||||
tenantId: credentials.tenantId,
|
||||
clientId: credentials.clientId,
|
||||
orgName: credentials.orgName
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
@@ -118,7 +150,8 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
formState: { isSubmitting, isDirty }
|
||||
formState: { isSubmitting, isDirty },
|
||||
setValue
|
||||
} = form;
|
||||
|
||||
const selectedMethod = watch("method");
|
||||
@@ -138,11 +171,12 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
|
||||
`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-devops`
|
||||
);
|
||||
break;
|
||||
|
||||
case AzureDevOpsConnectionMethod.AccessToken:
|
||||
onSubmit(formData);
|
||||
await onSubmit(formData);
|
||||
break;
|
||||
case AzureDevOpsConnectionMethod.ClientSecret:
|
||||
await onSubmit(formData);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||
}
|
||||
@@ -196,8 +230,8 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* OAuth-specific fields */}
|
||||
{selectedMethod === AzureDevOpsConnectionMethod.OAuth && (
|
||||
{(selectedMethod === AzureDevOpsConnectionMethod.OAuth ||
|
||||
selectedMethod === AzureDevOpsConnectionMethod.ClientSecret) && (
|
||||
<>
|
||||
<Controller
|
||||
name="tenantId"
|
||||
@@ -209,7 +243,14 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
|
||||
label="Tenant ID"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="00000000-0000-0000-0000-000000000000"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
setValue("credentials.tenantId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -223,7 +264,50 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
|
||||
label="Organization Name"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="myorganization" />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="myorganization"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
setValue("credentials.orgName", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Client Secret-specific fields */}
|
||||
{selectedMethod === AzureDevOpsConnectionMethod.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="~JzD8e6S.tH~w8XRaNnKcb7W1fM4rCns7FY"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@@ -20,19 +20,83 @@ import {
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
|
||||
|
||||
type Props = {
|
||||
appConnection?: TAzureKeyVaultConnection;
|
||||
onSubmit: (formData: ClientSecretForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||
const baseSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.AzureKeyVault),
|
||||
method: z.nativeEnum(AzureKeyVaultConnectionMethod),
|
||||
tenantId: z.string().trim().optional()
|
||||
method: z.nativeEnum(AzureKeyVaultConnectionMethod)
|
||||
});
|
||||
|
||||
const oauthSchema = baseSchema.extend({
|
||||
tenantId: z.string().trim().min(1, "Tenant ID is required"),
|
||||
method: z.literal(AzureKeyVaultConnectionMethod.OAuth)
|
||||
});
|
||||
|
||||
const clientSecretSchema = baseSchema.extend({
|
||||
method: z.literal(AzureKeyVaultConnectionMethod.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 AzureKeyVaultConnectionForm = ({ appConnection }: Props) => {
|
||||
const getDefaultValues = (appConnection?: TAzureKeyVaultConnection): Partial<FormData> => {
|
||||
if (!appConnection) {
|
||||
return {
|
||||
app: AppConnection.AzureKeyVault,
|
||||
method: AzureKeyVaultConnectionMethod.OAuth
|
||||
};
|
||||
}
|
||||
|
||||
const base = {
|
||||
name: appConnection.name,
|
||||
description: appConnection.description,
|
||||
app: appConnection.app,
|
||||
method: appConnection.method
|
||||
};
|
||||
const { credentials } = appConnection;
|
||||
|
||||
switch (appConnection.method) {
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
if ("tenantId" in credentials) {
|
||||
return {
|
||||
...base,
|
||||
method: AzureKeyVaultConnectionMethod.OAuth,
|
||||
tenantId: credentials.tenantId
|
||||
};
|
||||
}
|
||||
break;
|
||||
case AzureKeyVaultConnectionMethod.ClientSecret:
|
||||
if ("clientSecret" in credentials && "clientId" in credentials) {
|
||||
return {
|
||||
...base,
|
||||
method: AzureKeyVaultConnectionMethod.ClientSecret,
|
||||
credentials: {
|
||||
clientSecret: credentials.clientSecret,
|
||||
clientId: credentials.clientId,
|
||||
tenantId: credentials.tenantId
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
@@ -43,41 +107,37 @@ export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => {
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection
|
||||
? {
|
||||
...appConnection,
|
||||
tenantId: appConnection.credentials.tenantId
|
||||
}
|
||||
: {
|
||||
app: AppConnection.AzureKeyVault,
|
||||
method: AzureKeyVaultConnectionMethod.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 = async (formData: FormData) => {
|
||||
const state = crypto.randomBytes(16).toString("hex");
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
localStorage.setItem(
|
||||
"azureKeyVaultConnectionFormData",
|
||||
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||
);
|
||||
|
||||
switch (formData.method) {
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
setIsRedirecting(true);
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
localStorage.setItem(
|
||||
"azureKeyVaultConnectionFormData",
|
||||
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://vault.azure.net/.default%20openid%20offline_access&state=${state}<:>azure-key-vault`
|
||||
);
|
||||
break;
|
||||
case AzureKeyVaultConnectionMethod.ClientSecret:
|
||||
await onSubmit(formData);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||
}
|
||||
@@ -89,6 +149,9 @@ export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => {
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
isMissingConfig = !oauthClientId;
|
||||
break;
|
||||
case AzureKeyVaultConnectionMethod.ClientSecret:
|
||||
isMissingConfig = false;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Azure Connection method: ${selectedMethod}`);
|
||||
}
|
||||
@@ -97,25 +160,9 @@ export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => {
|
||||
|
||||
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 Azure Active Directory (Entra ID) Tenant ID."
|
||||
isError={Boolean(error?.message)}
|
||||
label="Tenant ID"
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
@@ -155,6 +202,66 @@ export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="tenantId"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="The Azure Active Directory (Entra ID) Tenant ID."
|
||||
isError={Boolean(error?.message)}
|
||||
label="Tenant ID"
|
||||
isOptional={selectedMethod === AzureKeyVaultConnectionMethod.OAuth}
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Client Secret-specific fields */}
|
||||
{selectedMethod === AzureKeyVaultConnectionMethod.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="~JzD8e6S.tH~w8XRaNnKcb7W1fM4rCns7FY"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
@@ -408,7 +408,7 @@ export const OAuthCallbackPage = () => {
|
||||
credentials: {
|
||||
code: code as string,
|
||||
installationId: installationId as string,
|
||||
host: credentials.host
|
||||
...(credentials?.host && { host: credentials.host })
|
||||
},
|
||||
gatewayId
|
||||
}
|
||||
@@ -416,7 +416,7 @@ export const OAuthCallbackPage = () => {
|
||||
connectionId,
|
||||
credentials: {
|
||||
code: code as string,
|
||||
host: credentials.host
|
||||
...(credentials?.host && { host: credentials.host })
|
||||
},
|
||||
gatewayId
|
||||
})
|
||||
@@ -432,7 +432,7 @@ export const OAuthCallbackPage = () => {
|
||||
credentials: {
|
||||
code: code as string,
|
||||
installationId: installationId as string,
|
||||
host: credentials.host
|
||||
...(credentials?.host && { host: credentials.host })
|
||||
},
|
||||
gatewayId
|
||||
}
|
||||
@@ -440,7 +440,7 @@ export const OAuthCallbackPage = () => {
|
||||
method: GitHubConnectionMethod.OAuth,
|
||||
credentials: {
|
||||
code: code as string,
|
||||
host: credentials.host
|
||||
...(credentials?.host && { host: credentials.host })
|
||||
},
|
||||
gatewayId
|
||||
})
|
||||
|
@@ -32,6 +32,13 @@ export const PmtMethodsTable = () => {
|
||||
|
||||
const handleDeletePmtMethodBtnClick = async () => {
|
||||
if (!currentOrg?.id || !pmtMethodToRemove) return;
|
||||
if (data?.length === 1) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "You must have at least one payment method"
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteOrgPmtMethod.mutateAsync({
|
||||
organizationId: currentOrg.id,
|
||||
|
@@ -2,14 +2,11 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { NewProjectModal } from "@app/components/projects";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AllProjectView } from "./components/AllProjectView";
|
||||
@@ -38,8 +35,6 @@ export const ProjectsPage = () => {
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const { data: serverDetails, isLoading } = useFetchServerStatus();
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||
@@ -52,30 +47,6 @@ export const ProjectsPage = () => {
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Helmet>
|
||||
|
||||
{!isLoading && !serverDetails?.redisConfigured && (
|
||||
<div className="mb-4 flex flex-col items-start justify-start text-3xl">
|
||||
<p className="mb-4 mr-4 font-semibold text-white">Announcements</p>
|
||||
<div className="flex w-full items-center rounded-md border border-blue-400/70 bg-blue-900/70 p-2 text-base text-mineshaft-100">
|
||||
<FontAwesomeIcon
|
||||
icon={faExclamationCircle}
|
||||
className="mr-4 p-4 text-2xl text-mineshaft-50"
|
||||
/>
|
||||
Attention: Updated versions of Infisical now require Redis for full functionality. Learn
|
||||
how to configure it
|
||||
<a
|
||||
href="https://infisical.com/docs/self-hosting/configuration/redis"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="cursor-pointer pl-1 text-white underline underline-offset-2 duration-100 hover:text-blue-200 hover:decoration-blue-400">
|
||||
here
|
||||
</span>
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4 flex flex-col items-start justify-start">
|
||||
<PageHeader
|
||||
title={
|
||||
|
Reference in New Issue
Block a user