mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 08:28:59 +00:00
feat: Add dynamic secrets for Azure Entra ID
This commit is contained in:
@@ -3,7 +3,10 @@ import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
|
||||
import {
|
||||
DynamicSecretDataFetchTypes,
|
||||
DynamicSecretProviderSchema
|
||||
} from "@app/ee/services/dynamic-secret/providers/models";
|
||||
import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
@@ -77,6 +80,32 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/fetch-provider-data",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
dataFetchType: z.string().min(1).describe("Type of data to fetch"),
|
||||
provider: DynamicSecretProviderSchema.describe(DYNAMIC_SECRETS.CREATE.provider)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
data: z.unknown()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const dataFetchType =
|
||||
DynamicSecretDataFetchTypes[req.body.dataFetchType as keyof typeof DynamicSecretDataFetchTypes];
|
||||
const data = await server.services.dynamicSecret.fetchData({ provider: req.body.provider, dataFetchType });
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:name",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
TCreateDynamicSecretDTO,
|
||||
TDeleteDynamicSecretDTO,
|
||||
TDetailsDynamicSecretDTO,
|
||||
TDynamicSecretsFetchDataDTO,
|
||||
TListDynamicSecretsDTO,
|
||||
TUpdateDynamicSecretDTO
|
||||
} from "./dynamic-secret-types";
|
||||
@@ -332,11 +333,20 @@ export const dynamicSecretServiceFactory = ({
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
|
||||
const fetchData = async ({ provider, dataFetchType }: TDynamicSecretsFetchDataDTO) => {
|
||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||
if (selectedProvider.fetchData) {
|
||||
const data = selectedProvider.fetchData(provider.inputs, dataFetchType);
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
updateByName,
|
||||
deleteByName,
|
||||
getDetails,
|
||||
list
|
||||
list,
|
||||
fetchData
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||
import { DynamicSecretDataFetchTypes, DynamicSecretProviderSchema } from "./providers/models";
|
||||
|
||||
// various status for dynamic secret that happens in background
|
||||
export enum DynamicSecretStatus {
|
||||
@@ -52,3 +52,8 @@ export type TListDynamicSecretsDTO = {
|
||||
environmentSlug: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDynamicSecretsFetchDataDTO = {
|
||||
provider: TProvider;
|
||||
dataFetchType: DynamicSecretDataFetchTypes;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import axios from "axios";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { AzureEntraIDSchema, DynamicSecretDataFetchTypes, TDynamicProviderFns } from "./models";
|
||||
|
||||
const MSFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/";
|
||||
const MSFT_LOGIN_URL = "https://login.microsoftonline.com";
|
||||
|
||||
const generatePassword = () => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
export const AzureEntraIDProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await AzureEntraIDSchema.parseAsync(inputs);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getToken = async (tenantId: string): Promise<{ token?: string; success: boolean }> => {
|
||||
const appCfg = getConfig();
|
||||
const response = await axios.post<{ access_token: string }>(
|
||||
`${MSFT_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`,
|
||||
{
|
||||
grant_type: "client_credentials",
|
||||
client_id: appCfg.MSFT_ENTRA_ID_APPLICATION_ID,
|
||||
client_secret: appCfg.MSFT_ENTRA_ID_CLIENT_SECRET,
|
||||
scope: "https://graph.microsoft.com/.default"
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
return { token: response.data.access_token, success: true };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await getToken(providerInputs.tenantId);
|
||||
return data.success;
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string) => {
|
||||
// Do nothing
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await getToken(providerInputs.tenantId);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
|
||||
const password = generatePassword();
|
||||
try {
|
||||
const response = await axios.patch(
|
||||
`${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`,
|
||||
{
|
||||
passwordProfile: {
|
||||
forceChangePasswordNextSignIn: false,
|
||||
password
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${data.token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
if (response.status !== 204) {
|
||||
throw new BadRequestError({ message: "Failed to update password" });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.info(JSON.stringify(error));
|
||||
}
|
||||
|
||||
return { entityId: providerInputs.userId, data: { userId: providerInputs.userId, password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
await create(inputs);
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const fetchData = async (inputs: unknown, toFetch: DynamicSecretDataFetchTypes) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const data = await getToken(providerInputs.tenantId);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
|
||||
switch (toFetch) {
|
||||
case DynamicSecretDataFetchTypes.Users: {
|
||||
const response = await axios.get<{ value: [{ displayName: string; id: string }] }>(
|
||||
`${MSFT_GRAPH_API_URL}/users`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization: `Bearer ${data.token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
const users = response.data.value.map((user: { displayName: string; id: string }) => {
|
||||
return {
|
||||
name: user.displayName,
|
||||
id: user.id
|
||||
};
|
||||
});
|
||||
return {
|
||||
data: {
|
||||
users
|
||||
}
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown data to fetch" });
|
||||
}
|
||||
};
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew,
|
||||
fetchData
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { ElasticSearchProvider } from "./elastic-search";
|
||||
import { DynamicSecretProviders } from "./models";
|
||||
@@ -18,5 +19,6 @@ export const buildDynamicSecretProviders = () => ({
|
||||
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
|
||||
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
|
||||
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider()
|
||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
||||
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider()
|
||||
});
|
||||
|
||||
@@ -166,6 +166,11 @@ export const DynamicSecretMongoDBSchema = z.object({
|
||||
)
|
||||
});
|
||||
|
||||
export const AzureEntraIDSchema = z.object({
|
||||
tenantId: z.string().trim().min(1),
|
||||
userId: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
SqlDatabase = "sql-database",
|
||||
Cassandra = "cassandra",
|
||||
@@ -175,7 +180,12 @@ export enum DynamicSecretProviders {
|
||||
MongoAtlas = "mongo-db-atlas",
|
||||
ElasticSearch = "elastic-search",
|
||||
MongoDB = "mongo-db",
|
||||
RabbitMq = "rabbit-mq"
|
||||
RabbitMq = "rabbit-mq",
|
||||
AzureEntraID = "azure-entra-id"
|
||||
}
|
||||
|
||||
export enum DynamicSecretDataFetchTypes {
|
||||
Users = "users"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
@@ -187,7 +197,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema })
|
||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema })
|
||||
]);
|
||||
|
||||
export type TDynamicProviderFns = {
|
||||
@@ -196,4 +207,5 @@ export type TDynamicProviderFns = {
|
||||
validateProviderInputs: (inputs: object) => Promise<unknown>;
|
||||
revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>;
|
||||
renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>;
|
||||
fetchData?: (inputs: unknown, toFetch: DynamicSecretDataFetchTypes) => Promise<{ data: unknown }>;
|
||||
};
|
||||
|
||||
@@ -134,6 +134,9 @@ const envSchema = z
|
||||
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
|
||||
LICENSE_KEY: zpStr(z.string().optional()),
|
||||
LICENSE_KEY_OFFLINE: zpStr(z.string().optional()),
|
||||
// MICROSOFT ENTRA ID APP
|
||||
MSFT_ENTRA_ID_APPLICATION_ID: zpStr(z.string().optional()),
|
||||
MSFT_ENTRA_ID_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
// GENERIC
|
||||
STANDALONE_MODE: z
|
||||
|
||||
Reference in New Issue
Block a user