Compare commits
41 Commits
help-fix-f
...
feat/azure
Author | SHA1 | Date | |
---|---|---|---|
|
3f2de2c5ef | ||
|
f515cc83d7 | ||
|
17bbdbe7bb | ||
|
427de068d5 | ||
|
baa05714ab | ||
|
c487614c38 | ||
|
a55c8cacea | ||
|
55aa1e87c0 | ||
|
c5c7adbc42 | ||
|
f686882ce6 | ||
|
e35417e11b | ||
|
ff0f4cf46a | ||
|
64093e9175 | ||
|
78fd852588 | ||
|
0c1f761a9a | ||
|
c363f485eb | ||
|
433d83641d | ||
|
35bb7f299c | ||
|
160e2b773b | ||
|
f0a70e23ac | ||
|
a6271a6187 | ||
|
b2fbec740f | ||
|
86e5f46d89 | ||
|
720789025c | ||
|
811b3d5934 | ||
|
cac702415f | ||
|
dbe7acdc80 | ||
|
b33985b338 | ||
|
670376336e | ||
|
c59eddb00a | ||
|
fe40ba497b | ||
|
c5b7e3d8be | ||
|
47e778a0b8 | ||
|
8b443e0957 | ||
|
f7fb015bd8 | ||
|
0d7cd357c3 | ||
|
e40f65836f | ||
|
ff5f66a75f | ||
|
bf72638600 | ||
|
d9bc4da6f1 | ||
|
7f8d5ec11a |
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasConfigColumn = await knex.schema.hasColumn(TableName.DynamicSecretLease, "config");
|
||||||
|
if (!hasConfigColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.DynamicSecretLease, (table) => {
|
||||||
|
table.jsonb("config");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasConfigColumn = await knex.schema.hasColumn(TableName.DynamicSecretLease, "config");
|
||||||
|
if (hasConfigColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.DynamicSecretLease, (table) => {
|
||||||
|
table.dropColumn("config");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,45 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
const BATCH_SIZE = 1000;
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasKubernetesHostColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "kubernetesHost");
|
||||||
|
|
||||||
|
if (hasKubernetesHostColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||||
|
table.string("kubernetesHost").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasKubernetesHostColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "kubernetesHost");
|
||||||
|
|
||||||
|
// find all rows where kubernetesHost is null
|
||||||
|
const rows = await knex(TableName.IdentityKubernetesAuth)
|
||||||
|
.whereNull("kubernetesHost")
|
||||||
|
.select(selectAllTableCols(TableName.IdentityKubernetesAuth));
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
||||||
|
const batch = rows.slice(i, i + BATCH_SIZE);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await knex(TableName.IdentityKubernetesAuth)
|
||||||
|
.whereIn(
|
||||||
|
"id",
|
||||||
|
batch.map((row) => row.id)
|
||||||
|
)
|
||||||
|
.update({ kubernetesHost: "" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasKubernetesHostColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||||
|
table.string("kubernetesHost").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -16,7 +16,8 @@ export const DynamicSecretLeasesSchema = z.object({
|
|||||||
statusDetails: z.string().nullable().optional(),
|
statusDetails: z.string().nullable().optional(),
|
||||||
dynamicSecretId: z.string().uuid(),
|
dynamicSecretId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
config: z.unknown().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TDynamicSecretLeases = z.infer<typeof DynamicSecretLeasesSchema>;
|
export type TDynamicSecretLeases = z.infer<typeof DynamicSecretLeasesSchema>;
|
||||||
|
@@ -18,7 +18,7 @@ export const IdentityKubernetesAuthsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
identityId: z.string().uuid(),
|
identityId: z.string().uuid(),
|
||||||
kubernetesHost: z.string(),
|
kubernetesHost: z.string().nullable().optional(),
|
||||||
encryptedCaCert: z.string().nullable().optional(),
|
encryptedCaCert: z.string().nullable().optional(),
|
||||||
caCertIV: z.string().nullable().optional(),
|
caCertIV: z.string().nullable().optional(),
|
||||||
caCertTag: z.string().nullable().optional(),
|
caCertTag: z.string().nullable().optional(),
|
||||||
|
@@ -36,7 +36,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
|||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
}),
|
}),
|
||||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
||||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.path)
|
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
|
||||||
|
config: z.any().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@@ -0,0 +1,67 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||||
|
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
||||||
|
import { daysToMillisecond } from "@app/lib/dates";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerKubernetesDynamicSecretLeaseRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.DynamicSecrets],
|
||||||
|
body: z.object({
|
||||||
|
dynamicSecretName: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.dynamicSecretName).toLowerCase(),
|
||||||
|
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.projectSlug),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(DYNAMIC_SECRET_LEASES.CREATE.ttl)
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" });
|
||||||
|
if (valMs > daysToMillisecond(1))
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
||||||
|
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
|
||||||
|
config: z
|
||||||
|
.object({
|
||||||
|
namespace: z.string().min(1).optional().describe(DYNAMIC_SECRET_LEASES.KUBERNETES.CREATE.config.namespace)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
lease: DynamicSecretLeasesSchema,
|
||||||
|
dynamicSecret: SanitizedDynamicSecretSchema,
|
||||||
|
data: z.unknown()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { data, lease, dynamicSecret } = await server.services.dynamicSecretLease.create({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
name: req.body.dynamicSecretName,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return { lease, data, dynamicSecret };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -6,6 +6,7 @@ import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
|
|||||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||||
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
|
import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router";
|
||||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
import { registerExternalKmsRouter } from "./external-kms-router";
|
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||||
import { registerGatewayRouter } from "./gateway-router";
|
import { registerGatewayRouter } from "./gateway-router";
|
||||||
@@ -71,6 +72,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
async (dynamicSecretRouter) => {
|
async (dynamicSecretRouter) => {
|
||||||
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
||||||
await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
|
await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
|
||||||
|
await dynamicSecretRouter.register(registerKubernetesDynamicSecretLeaseRouter, { prefix: "/leases/kubernetes" });
|
||||||
},
|
},
|
||||||
{ prefix: "/dynamic-secrets" }
|
{ prefix: "/dynamic-secrets" }
|
||||||
);
|
);
|
||||||
|
@@ -10,6 +10,7 @@ import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
|||||||
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
||||||
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
|
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
|
||||||
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
|
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
|
||||||
|
import { TDynamicSecretLeaseConfig } from "./dynamic-secret-lease-types";
|
||||||
|
|
||||||
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
@@ -134,10 +135,15 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
|
|
||||||
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
dynamicSecretLeases.map(({ externalEntityId }) =>
|
dynamicSecretLeases.map(({ externalEntityId, config }) =>
|
||||||
selectedProvider.revoke(decryptedStoredInput, externalEntityId, {
|
selectedProvider.revoke(
|
||||||
projectId: folder.projectId
|
decryptedStoredInput,
|
||||||
})
|
externalEntityId,
|
||||||
|
{
|
||||||
|
projectId: folder.projectId
|
||||||
|
},
|
||||||
|
config as TDynamicSecretLeaseConfig
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,7 @@ import {
|
|||||||
TCreateDynamicSecretLeaseDTO,
|
TCreateDynamicSecretLeaseDTO,
|
||||||
TDeleteDynamicSecretLeaseDTO,
|
TDeleteDynamicSecretLeaseDTO,
|
||||||
TDetailsDynamicSecretLeaseDTO,
|
TDetailsDynamicSecretLeaseDTO,
|
||||||
|
TDynamicSecretLeaseConfig,
|
||||||
TListDynamicSecretLeasesDTO,
|
TListDynamicSecretLeasesDTO,
|
||||||
TRenewDynamicSecretLeaseDTO
|
TRenewDynamicSecretLeaseDTO
|
||||||
} from "./dynamic-secret-lease-types";
|
} from "./dynamic-secret-lease-types";
|
||||||
@@ -77,7 +78,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
ttl
|
ttl,
|
||||||
|
config
|
||||||
}: TCreateDynamicSecretLeaseDTO) => {
|
}: TCreateDynamicSecretLeaseDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
@@ -163,7 +165,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
expireAt: expireAt.getTime(),
|
expireAt: expireAt.getTime(),
|
||||||
usernameTemplate: dynamicSecretCfg.usernameTemplate,
|
usernameTemplate: dynamicSecretCfg.usernameTemplate,
|
||||||
identity,
|
identity,
|
||||||
metadata: { projectId }
|
metadata: { projectId },
|
||||||
|
config
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
|
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
|
||||||
@@ -177,8 +180,10 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
expireAt,
|
expireAt,
|
||||||
version: 1,
|
version: 1,
|
||||||
dynamicSecretId: dynamicSecretCfg.id,
|
dynamicSecretId: dynamicSecretCfg.id,
|
||||||
externalEntityId: entityId
|
externalEntityId: entityId,
|
||||||
|
config
|
||||||
});
|
});
|
||||||
|
|
||||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
|
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
|
||||||
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
|
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
|
||||||
};
|
};
|
||||||
@@ -342,7 +347,12 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
) as object;
|
) as object;
|
||||||
|
|
||||||
const revokeResponse = await selectedProvider
|
const revokeResponse = await selectedProvider
|
||||||
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { projectId })
|
.revoke(
|
||||||
|
decryptedStoredInput,
|
||||||
|
dynamicSecretLease.externalEntityId,
|
||||||
|
{ projectId },
|
||||||
|
dynamicSecretLease.config as TDynamicSecretLeaseConfig
|
||||||
|
)
|
||||||
.catch(async (err) => {
|
.catch(async (err) => {
|
||||||
// only propogate this error if forced is false
|
// only propogate this error if forced is false
|
||||||
if (!isForced) return { error: err as Error };
|
if (!isForced) return { error: err as Error };
|
||||||
|
@@ -10,6 +10,7 @@ export type TCreateDynamicSecretLeaseDTO = {
|
|||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
ttl?: string;
|
ttl?: string;
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
config?: TDynamicSecretLeaseConfig;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDetailsDynamicSecretLeaseDTO = {
|
export type TDetailsDynamicSecretLeaseDTO = {
|
||||||
@@ -41,3 +42,9 @@ export type TRenewDynamicSecretLeaseDTO = {
|
|||||||
ttl?: string;
|
ttl?: string;
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDynamicSecretKubernetesLeaseConfig = {
|
||||||
|
namespace?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDynamicSecretLeaseConfig = TDynamicSecretKubernetesLeaseConfig;
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import axios from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
|
|
||||||
import { InternalServerError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
||||||
|
|
||||||
|
import { TDynamicSecretKubernetesLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
|
||||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
import {
|
import {
|
||||||
DynamicSecretKubernetesSchema,
|
DynamicSecretKubernetesSchema,
|
||||||
@@ -19,6 +20,9 @@ import {
|
|||||||
|
|
||||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
|
// This value is just a placeholder. When using gateway auth method, the url is irrelevant.
|
||||||
|
const GATEWAY_AUTH_DEFAULT_URL = "https://kubernetes.default.svc.cluster.local";
|
||||||
|
|
||||||
type TKubernetesProviderDTO = {
|
type TKubernetesProviderDTO = {
|
||||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
};
|
};
|
||||||
@@ -36,7 +40,7 @@ const generateUsername = (usernameTemplate?: string | null) => {
|
|||||||
export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => {
|
export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
|
||||||
if (!providerInputs.gatewayId) {
|
if (!providerInputs.gatewayId && providerInputs.url) {
|
||||||
await blockLocalAndPrivateIpAddresses(providerInputs.url);
|
await blockLocalAndPrivateIpAddresses(providerInputs.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,135 +107,173 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const serviceAccountName = generateUsername();
|
const serviceAccountName = generateUsername();
|
||||||
const roleBindingName = `${serviceAccountName}-role-binding`;
|
const roleBindingName = `${serviceAccountName}-role-binding`;
|
||||||
|
|
||||||
// 1. Create a test service account
|
const namespaces = providerInputs.namespace.split(",").map((namespace) => namespace.trim());
|
||||||
await axios.post(
|
|
||||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts`,
|
|
||||||
{
|
|
||||||
metadata: {
|
|
||||||
name: serviceAccountName,
|
|
||||||
namespace: providerInputs.namespace
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
httpsAgent
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Create a test role binding
|
// Test each namespace sequentially instead of in parallel to simplify cleanup
|
||||||
const roleBindingUrl =
|
for await (const namespace of namespaces) {
|
||||||
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
try {
|
||||||
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
// 1. Create a test service account
|
||||||
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings`;
|
await axios.post(
|
||||||
|
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts`,
|
||||||
const roleBindingMetadata = {
|
|
||||||
name: roleBindingName,
|
|
||||||
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace: providerInputs.namespace })
|
|
||||||
};
|
|
||||||
|
|
||||||
await axios.post(
|
|
||||||
roleBindingUrl,
|
|
||||||
{
|
|
||||||
metadata: roleBindingMetadata,
|
|
||||||
roleRef: {
|
|
||||||
kind: providerInputs.roleType === KubernetesRoleType.ClusterRole ? "ClusterRole" : "Role",
|
|
||||||
name: providerInputs.role,
|
|
||||||
apiGroup: "rbac.authorization.k8s.io"
|
|
||||||
},
|
|
||||||
subjects: [
|
|
||||||
{
|
{
|
||||||
kind: "ServiceAccount",
|
metadata: {
|
||||||
name: serviceAccountName,
|
name: serviceAccountName,
|
||||||
namespace: providerInputs.namespace
|
namespace
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
}
|
}
|
||||||
]
|
);
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
httpsAgent
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Request a token for the test service account
|
// 2. Create a test role binding
|
||||||
await axios.post(
|
const roleBindingUrl =
|
||||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}/token`,
|
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
||||||
{
|
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
||||||
spec: {
|
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings`;
|
||||||
expirationSeconds: 600, // 10 minutes
|
|
||||||
...(providerInputs.audiences?.length ? { audiences: providerInputs.audiences } : {})
|
const roleBindingMetadata = {
|
||||||
|
name: roleBindingName,
|
||||||
|
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace })
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(
|
||||||
|
roleBindingUrl,
|
||||||
|
{
|
||||||
|
metadata: roleBindingMetadata,
|
||||||
|
roleRef: {
|
||||||
|
kind: providerInputs.roleType === KubernetesRoleType.ClusterRole ? "ClusterRole" : "Role",
|
||||||
|
name: providerInputs.role,
|
||||||
|
apiGroup: "rbac.authorization.k8s.io"
|
||||||
|
},
|
||||||
|
subjects: [
|
||||||
|
{
|
||||||
|
kind: "ServiceAccount",
|
||||||
|
name: serviceAccountName,
|
||||||
|
namespace
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Request a token for the test service account
|
||||||
|
await axios.post(
|
||||||
|
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}/token`,
|
||||||
|
{
|
||||||
|
spec: {
|
||||||
|
expirationSeconds: 600, // 10 minutes
|
||||||
|
...(providerInputs.audiences?.length ? { audiences: providerInputs.audiences } : {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Cleanup: delete role binding and service account
|
||||||
|
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
||||||
|
await axios.delete(
|
||||||
|
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings/${roleBindingName}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
httpsAgent
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. Cleanup: delete role binding and service account
|
await axios.delete(`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}`, {
|
||||||
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
|
||||||
await axios.delete(
|
|
||||||
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings/${roleBindingName}`,
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const cleanupInfo = `You may need to manually clean up the following resources in namespace "${namespace}": Service Account - ${serviceAccountName}, ${providerInputs.roleType === KubernetesRoleType.Role ? "Role" : "Cluster Role"} Binding - ${roleBindingName}.`;
|
||||||
|
let mainErrorMessage = "Unknown error";
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
mainErrorMessage = (error.response?.data as { message: string })?.message;
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
mainErrorMessage = error.message;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
httpsAgent
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await axios.delete(
|
throw new Error(`${mainErrorMessage}. ${cleanupInfo}`);
|
||||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const serviceAccountStaticCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
|
const serviceAccountStaticCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
|
||||||
@@ -247,17 +289,23 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = new URL(providerInputs.url);
|
const rawUrl =
|
||||||
|
providerInputs.authMethod === KubernetesAuthMethod.Gateway ? GATEWAY_AUTH_DEFAULT_URL : providerInputs.url || "";
|
||||||
|
const url = new URL(rawUrl);
|
||||||
const k8sGatewayHost = url.hostname;
|
const k8sGatewayHost = url.hostname;
|
||||||
const k8sPort = url.port ? Number(url.port) : 443;
|
const k8sPort = url.port ? Number(url.port) : 443;
|
||||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||||
@@ -315,11 +363,13 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const create = async ({
|
const create = async ({
|
||||||
inputs,
|
inputs,
|
||||||
expireAt,
|
expireAt,
|
||||||
usernameTemplate
|
usernameTemplate,
|
||||||
|
config
|
||||||
}: {
|
}: {
|
||||||
inputs: unknown;
|
inputs: unknown;
|
||||||
expireAt: number;
|
expireAt: number;
|
||||||
usernameTemplate?: string | null;
|
usernameTemplate?: string | null;
|
||||||
|
config?: TDynamicSecretKubernetesLeaseConfig;
|
||||||
}) => {
|
}) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
@@ -331,26 +381,44 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const baseUrl = port ? `${host}:${port}` : host;
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
const serviceAccountName = generateUsername(usernameTemplate);
|
const serviceAccountName = generateUsername(usernameTemplate);
|
||||||
const roleBindingName = `${serviceAccountName}-role-binding`;
|
const roleBindingName = `${serviceAccountName}-role-binding`;
|
||||||
|
const allowedNamespaces = providerInputs.namespace.split(",").map((namespace) => namespace.trim());
|
||||||
|
|
||||||
|
if (config?.namespace && !allowedNamespaces?.includes(config?.namespace)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Namespace ${config?.namespace} is not allowed. Allowed namespaces: ${allowedNamespaces?.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const namespace = config?.namespace || allowedNamespaces[0];
|
||||||
|
if (!namespace) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "No namespace provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Create the service account
|
// 1. Create the service account
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts`,
|
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts`,
|
||||||
{
|
{
|
||||||
metadata: {
|
metadata: {
|
||||||
name: serviceAccountName,
|
name: serviceAccountName,
|
||||||
namespace: providerInputs.namespace
|
namespace
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -358,11 +426,11 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const roleBindingUrl =
|
const roleBindingUrl =
|
||||||
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
||||||
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
||||||
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings`;
|
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings`;
|
||||||
|
|
||||||
const roleBindingMetadata = {
|
const roleBindingMetadata = {
|
||||||
name: roleBindingName,
|
name: roleBindingName,
|
||||||
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace: providerInputs.namespace })
|
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace })
|
||||||
};
|
};
|
||||||
|
|
||||||
await axios.post(
|
await axios.post(
|
||||||
@@ -378,7 +446,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
{
|
{
|
||||||
kind: "ServiceAccount",
|
kind: "ServiceAccount",
|
||||||
name: serviceAccountName,
|
name: serviceAccountName,
|
||||||
namespace: providerInputs.namespace
|
namespace
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -386,18 +454,22 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Request a token for the service account
|
// 3. Request a token for the service account
|
||||||
const res = await axios.post<TKubernetesTokenRequest>(
|
const res = await axios.post<TKubernetesTokenRequest>(
|
||||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}/token`,
|
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}/token`,
|
||||||
{
|
{
|
||||||
spec: {
|
spec: {
|
||||||
expirationSeconds: Math.floor((expireAt - Date.now()) / 1000),
|
expirationSeconds: Math.floor((expireAt - Date.now()) / 1000),
|
||||||
@@ -408,12 +480,16 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -425,6 +501,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
throw new Error("invalid callback");
|
throw new Error("invalid callback");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config?.namespace && config.namespace !== providerInputs.namespace) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Namespace ${config?.namespace} is not allowed. Allowed namespace: ${providerInputs.namespace}.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const baseUrl = port ? `${host}:${port}` : host;
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
|
|
||||||
const res = await axios.post<TKubernetesTokenRequest>(
|
const res = await axios.post<TKubernetesTokenRequest>(
|
||||||
@@ -439,19 +521,25 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ...res.data, serviceAccountName: providerInputs.serviceAccountName };
|
return { ...res.data, serviceAccountName: providerInputs.serviceAccountName };
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = new URL(providerInputs.url);
|
const rawUrl =
|
||||||
|
providerInputs.authMethod === KubernetesAuthMethod.Gateway ? GATEWAY_AUTH_DEFAULT_URL : providerInputs.url || "";
|
||||||
|
const url = new URL(rawUrl);
|
||||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||||
const k8sGatewayHost = url.hostname;
|
const k8sGatewayHost = url.hostname;
|
||||||
const k8sPort = url.port ? Number(url.port) : 443;
|
const k8sPort = url.port ? Number(url.port) : 443;
|
||||||
@@ -511,7 +599,13 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (
|
||||||
|
inputs: unknown,
|
||||||
|
entityId: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_metadata: { projectId: string },
|
||||||
|
config?: TDynamicSecretKubernetesLeaseConfig
|
||||||
|
) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
const serviceAccountDynamicCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
|
const serviceAccountDynamicCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
|
||||||
@@ -522,19 +616,25 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const baseUrl = port ? `${host}:${port}` : host;
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
const roleBindingName = `${entityId}-role-binding`;
|
const roleBindingName = `${entityId}-role-binding`;
|
||||||
|
|
||||||
|
const namespace = config?.namespace ?? providerInputs.namespace.split(",")[0].trim();
|
||||||
|
|
||||||
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
||||||
await axios.delete(
|
await axios.delete(
|
||||||
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings/${roleBindingName}`,
|
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings/${roleBindingName}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -542,31 +642,44 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the service account
|
// Delete the service account
|
||||||
await axios.delete(`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${entityId}`, {
|
await axios.delete(`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${entityId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) {
|
if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) {
|
||||||
const url = new URL(providerInputs.url);
|
const rawUrl =
|
||||||
|
providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? GATEWAY_AUTH_DEFAULT_URL
|
||||||
|
: providerInputs.url || "";
|
||||||
|
|
||||||
|
const url = new URL(rawUrl);
|
||||||
const k8sGatewayHost = url.hostname;
|
const k8sGatewayHost = url.hostname;
|
||||||
const k8sPort = url.port ? Number(url.port) : 443;
|
const k8sPort = url.port ? Number(url.port) : 443;
|
||||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||||
|
@@ -1,5 +1,10 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
|
|
||||||
|
import { TDynamicSecretLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
|
||||||
|
|
||||||
export type PasswordRequirements = {
|
export type PasswordRequirements = {
|
||||||
length: number;
|
length: number;
|
||||||
required: {
|
required: {
|
||||||
@@ -323,24 +328,54 @@ export const LdapSchema = z.union([
|
|||||||
export const DynamicSecretKubernetesSchema = z
|
export const DynamicSecretKubernetesSchema = z
|
||||||
.discriminatedUnion("credentialType", [
|
.discriminatedUnion("credentialType", [
|
||||||
z.object({
|
z.object({
|
||||||
url: z.string().url().trim().min(1),
|
url: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((val: string | undefined) => !val || new RE2(/^https?:\/\/.+/).test(val), {
|
||||||
|
message: "Invalid URL. Must start with http:// or https:// (e.g. https://example.com)"
|
||||||
|
}),
|
||||||
clusterToken: z.string().trim().optional(),
|
clusterToken: z.string().trim().optional(),
|
||||||
ca: z.string().optional(),
|
ca: z.string().optional(),
|
||||||
sslEnabled: z.boolean().default(false),
|
sslEnabled: z.boolean().default(false),
|
||||||
credentialType: z.literal(KubernetesCredentialType.Static),
|
credentialType: z.literal(KubernetesCredentialType.Static),
|
||||||
serviceAccountName: z.string().trim().min(1),
|
serviceAccountName: z.string().trim().min(1),
|
||||||
namespace: z.string().trim().min(1),
|
namespace: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.refine((val) => !val.includes(","), "Namespace must be a single value, not a comma-separated list")
|
||||||
|
.refine(
|
||||||
|
(val) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(val),
|
||||||
|
"Invalid namespace format"
|
||||||
|
),
|
||||||
gatewayId: z.string().optional(),
|
gatewayId: z.string().optional(),
|
||||||
audiences: z.array(z.string().trim().min(1)),
|
audiences: z.array(z.string().trim().min(1)),
|
||||||
authMethod: z.nativeEnum(KubernetesAuthMethod).default(KubernetesAuthMethod.Api)
|
authMethod: z.nativeEnum(KubernetesAuthMethod).default(KubernetesAuthMethod.Api)
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
url: z.string().url().trim().min(1),
|
url: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.optional()
|
||||||
|
.refine((val: string | undefined) => !val || new RE2(/^https?:\/\/.+/).test(val), {
|
||||||
|
message: "Invalid URL. Must start with http:// or https:// (e.g. https://example.com)"
|
||||||
|
}),
|
||||||
clusterToken: z.string().trim().optional(),
|
clusterToken: z.string().trim().optional(),
|
||||||
ca: z.string().optional(),
|
ca: z.string().optional(),
|
||||||
sslEnabled: z.boolean().default(false),
|
sslEnabled: z.boolean().default(false),
|
||||||
credentialType: z.literal(KubernetesCredentialType.Dynamic),
|
credentialType: z.literal(KubernetesCredentialType.Dynamic),
|
||||||
namespace: z.string().trim().min(1),
|
namespace: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.refine((val) => {
|
||||||
|
const namespaces = val.split(",").map((ns) => ns.trim());
|
||||||
|
return (
|
||||||
|
namespaces.length > 0 &&
|
||||||
|
namespaces.every((ns) => ns.length > 0) &&
|
||||||
|
namespaces.every((ns) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(ns))
|
||||||
|
);
|
||||||
|
}, "Must be a valid comma-separated list of namespace values"),
|
||||||
gatewayId: z.string().optional(),
|
gatewayId: z.string().optional(),
|
||||||
audiences: z.array(z.string().trim().min(1)),
|
audiences: z.array(z.string().trim().min(1)),
|
||||||
roleType: z.nativeEnum(KubernetesRoleType),
|
roleType: z.nativeEnum(KubernetesRoleType),
|
||||||
@@ -356,12 +391,21 @@ export const DynamicSecretKubernetesSchema = z
|
|||||||
message: "When auth method is set to Gateway, a gateway must be selected"
|
message: "When auth method is set to Gateway, a gateway must be selected"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ((data.authMethod === KubernetesAuthMethod.Api || !data.authMethod) && !data.clusterToken) {
|
if (data.authMethod === KubernetesAuthMethod.Api || !data.authMethod) {
|
||||||
ctx.addIssue({
|
if (!data.clusterToken) {
|
||||||
path: ["clusterToken"],
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
path: ["clusterToken"],
|
||||||
message: "When auth method is set to Manual Token, a cluster token must be provided"
|
code: z.ZodIssueCode.custom,
|
||||||
});
|
message: "When auth method is set to Token, a cluster token must be provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.url) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["url"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "When auth method is set to Token, a cluster URL must be provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -475,10 +519,16 @@ export type TDynamicProviderFns = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
metadata: { projectId: string };
|
metadata: { projectId: string };
|
||||||
|
config?: TDynamicSecretLeaseConfig;
|
||||||
}) => Promise<{ entityId: string; data: unknown }>;
|
}) => Promise<{ entityId: string; data: unknown }>;
|
||||||
validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>;
|
validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>;
|
||||||
validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise<unknown>;
|
validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise<unknown>;
|
||||||
revoke: (inputs: unknown, entityId: string, metadata: { projectId: string }) => Promise<{ entityId: string }>;
|
revoke: (
|
||||||
|
inputs: unknown,
|
||||||
|
entityId: string,
|
||||||
|
metadata: { projectId: string },
|
||||||
|
config?: TDynamicSecretLeaseConfig
|
||||||
|
) => Promise<{ entityId: string }>;
|
||||||
renew: (
|
renew: (
|
||||||
inputs: unknown,
|
inputs: unknown,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -246,7 +247,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { hasRole } = await permissionService.getProjectPermission({
|
const { hasRole, permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -262,6 +263,12 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasSecretReadAccess = permission.can(
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
ProjectPermissionSub.Secrets
|
||||||
|
);
|
||||||
|
const hiddenSecretValue = "******";
|
||||||
|
|
||||||
let secrets;
|
let secrets;
|
||||||
if (shouldUseSecretV2Bridge) {
|
if (shouldUseSecretV2Bridge) {
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
@@ -278,9 +285,9 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
version: el.version,
|
version: el.version,
|
||||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||||
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
||||||
secretValue:
|
secretValue: !hasSecretReadAccess
|
||||||
// eslint-disable-next-line no-nested-ternary
|
? hiddenSecretValue
|
||||||
el.secret && el.secret.isRotatedSecret
|
: el.secret && el.secret.isRotatedSecret
|
||||||
? undefined
|
? undefined
|
||||||
: el.encryptedValue
|
: el.encryptedValue
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||||
@@ -293,9 +300,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretKey: el.secret.key,
|
secretKey: el.secret.key,
|
||||||
id: el.secret.id,
|
id: el.secret.id,
|
||||||
version: el.secret.version,
|
version: el.secret.version,
|
||||||
secretValue: el.secret.encryptedValue
|
secretValue: !hasSecretReadAccess
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
? hiddenSecretValue
|
||||||
: "",
|
: el.secret.encryptedValue
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||||
|
: "",
|
||||||
secretComment: el.secret.encryptedComment
|
secretComment: el.secret.encryptedComment
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
|
||||||
: ""
|
: ""
|
||||||
@@ -306,9 +315,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretKey: el.secretVersion.key,
|
secretKey: el.secretVersion.key,
|
||||||
id: el.secretVersion.id,
|
id: el.secretVersion.id,
|
||||||
version: el.secretVersion.version,
|
version: el.secretVersion.version,
|
||||||
secretValue: el.secretVersion.encryptedValue
|
secretValue: !hasSecretReadAccess
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
? hiddenSecretValue
|
||||||
: "",
|
: el.secretVersion.encryptedValue
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||||
|
: "",
|
||||||
secretComment: el.secretVersion.encryptedComment
|
secretComment: el.secretVersion.encryptedComment
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
||||||
: "",
|
: "",
|
||||||
|
@@ -101,10 +101,56 @@ export const azureClientSecretRotationFactory: TRotationFactory<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a credential with the given keyId exists.
|
||||||
|
*/
|
||||||
|
const credentialExists = async (keyId: string): Promise<boolean> => {
|
||||||
|
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
|
||||||
|
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/passwordCredentials`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await request.get<{ value: Array<{ keyId: string }> }>(endpoint, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.value?.some((credential) => credential.keyId === keyId) || false;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
let message;
|
||||||
|
if (
|
||||||
|
error.response?.data &&
|
||||||
|
typeof error.response.data === "object" &&
|
||||||
|
"error" in error.response.data &&
|
||||||
|
typeof (error.response.data as AzureErrorResponse).error.message === "string"
|
||||||
|
) {
|
||||||
|
message = (error.response.data as AzureErrorResponse).error.message;
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to check credential existence for app ${objectId}: ${
|
||||||
|
message || error.message || "Unknown error"
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to validate connection: verify credentials"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revokes a client secret from the Azure app using its keyId.
|
* Revokes a client secret from the Azure app using its keyId.
|
||||||
|
* First checks if the credential exists before attempting revocation.
|
||||||
*/
|
*/
|
||||||
const revokeCredential = async (keyId: string) => {
|
const revokeCredential = async (keyId: string) => {
|
||||||
|
// Check if credential exists before attempting revocation
|
||||||
|
const exists = await credentialExists(keyId);
|
||||||
|
if (!exists) {
|
||||||
|
return; // Credential doesn't exist, nothing to revoke
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
|
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
|
||||||
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/removePassword`;
|
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/removePassword`;
|
||||||
|
|
||||||
|
@@ -1113,6 +1113,14 @@ export const DYNAMIC_SECRET_LEASES = {
|
|||||||
leaseId: "The ID of the dynamic secret lease.",
|
leaseId: "The ID of the dynamic secret lease.",
|
||||||
isForced:
|
isForced:
|
||||||
"A boolean flag to delete the the dynamic secret from Infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
|
"A boolean flag to delete the the dynamic secret from Infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
|
||||||
|
},
|
||||||
|
KUBERNETES: {
|
||||||
|
CREATE: {
|
||||||
|
config: {
|
||||||
|
namespace:
|
||||||
|
"The Kubernetes namespace to create the lease in. If not specified, the first namespace defined in the configuration will be used."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
export const SECRET_TAGS = {
|
export const SECRET_TAGS = {
|
||||||
@@ -2162,6 +2170,11 @@ export const AppConnections = {
|
|||||||
code: "The OAuth code to use to connect with Azure Client Secrets.",
|
code: "The OAuth code to use to connect with Azure Client Secrets.",
|
||||||
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
|
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
|
||||||
},
|
},
|
||||||
|
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."
|
||||||
|
},
|
||||||
OCI: {
|
OCI: {
|
||||||
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
|
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
|
||||||
tenancyOcid: "The OCID (Oracle Cloud Identifier) of the tenancy in Oracle Cloud Infrastructure.",
|
tenancyOcid: "The OCID (Oracle Cloud Identifier) of the tenancy in Oracle Cloud Infrastructure.",
|
||||||
@@ -2276,6 +2289,10 @@ export const SecretSyncs = {
|
|||||||
"The URL of the Azure App Configuration to sync secrets to. Example: https://example.azconfig.io/",
|
"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."
|
||||||
},
|
},
|
||||||
|
AZURE_DEVOPS: {
|
||||||
|
devopsProjectId: "The ID of the Azure DevOps project to sync secrets to.",
|
||||||
|
devopsProjectName: "The name of the Azure DevOps project to sync secrets to."
|
||||||
|
},
|
||||||
GCP: {
|
GCP: {
|
||||||
scope: "The Google project scope that secrets should be synced to.",
|
scope: "The Google project scope that secrets should be synced to.",
|
||||||
projectId: "The ID of the Google project secrets should be synced to.",
|
projectId: "The ID of the Google project secrets should be synced to.",
|
||||||
|
@@ -149,8 +149,8 @@ const setupProxyServer = async ({
|
|||||||
protocol = GatewayProxyProtocol.Tcp,
|
protocol = GatewayProxyProtocol.Tcp,
|
||||||
httpsAgent
|
httpsAgent
|
||||||
}: {
|
}: {
|
||||||
targetHost: string;
|
targetHost?: string;
|
||||||
targetPort: number;
|
targetPort?: number;
|
||||||
relayPort: number;
|
relayPort: number;
|
||||||
relayHost: string;
|
relayHost: string;
|
||||||
tlsOptions: TGatewayTlsOptions;
|
tlsOptions: TGatewayTlsOptions;
|
||||||
@@ -183,27 +183,44 @@ const setupProxyServer = async ({
|
|||||||
let command: string;
|
let command: string;
|
||||||
|
|
||||||
if (protocol === GatewayProxyProtocol.Http) {
|
if (protocol === GatewayProxyProtocol.Http) {
|
||||||
const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http)
|
if (!targetHost && !targetPort) {
|
||||||
command = `FORWARD-HTTP ${targetUrl}`;
|
command = `FORWARD-HTTP`;
|
||||||
logger.debug(`Using HTTP proxy mode: ${command.trim()}`);
|
logger.debug(`Using HTTP proxy mode, no target URL provided [command=${command.trim()}]`);
|
||||||
|
} else {
|
||||||
|
if (!targetHost || targetPort === undefined) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Target host and port are required for HTTP proxy mode with custom target`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// extract ca certificate from httpsAgent if present
|
const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http)
|
||||||
if (httpsAgent && targetHost.startsWith("https://")) {
|
command = `FORWARD-HTTP ${targetUrl}`;
|
||||||
const agentOptions = httpsAgent.options;
|
logger.debug(`Using HTTP proxy mode, custom target URL provided [command=${command.trim()}]`);
|
||||||
if (agentOptions && agentOptions.ca) {
|
|
||||||
const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca;
|
|
||||||
const caB64 = Buffer.from(caCert as string).toString("base64");
|
|
||||||
command += ` ca=${caB64}`;
|
|
||||||
|
|
||||||
const rejectUnauthorized = agentOptions.rejectUnauthorized !== false;
|
// extract ca certificate from httpsAgent if present
|
||||||
command += ` verify=${rejectUnauthorized}`;
|
if (httpsAgent && targetHost.startsWith("https://")) {
|
||||||
|
const agentOptions = httpsAgent.options;
|
||||||
|
if (agentOptions && agentOptions.ca) {
|
||||||
|
const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca;
|
||||||
|
const caB64 = Buffer.from(caCert as string).toString("base64");
|
||||||
|
command += ` ca=${caB64}`;
|
||||||
|
|
||||||
logger.debug(`Using HTTP proxy mode [command=${command.trim()}]`);
|
const rejectUnauthorized = agentOptions.rejectUnauthorized !== false;
|
||||||
|
command += ` verify=${rejectUnauthorized}`;
|
||||||
|
|
||||||
|
logger.debug(`Using HTTP proxy mode, custom target URL provided [command=${command.trim()}]`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
command += "\n";
|
command += "\n";
|
||||||
} else if (protocol === GatewayProxyProtocol.Tcp) {
|
} else if (protocol === GatewayProxyProtocol.Tcp) {
|
||||||
|
if (!targetHost || !targetPort) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Target host and port are required for TCP proxy mode`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// For TCP mode, send FORWARD-TCP with host:port
|
// For TCP mode, send FORWARD-TCP with host:port
|
||||||
command = `FORWARD-TCP ${targetHost}:${targetPort}\n`;
|
command = `FORWARD-TCP ${targetHost}:${targetPort}\n`;
|
||||||
logger.debug(`Using TCP proxy mode: ${command.trim()}`);
|
logger.debug(`Using TCP proxy mode: ${command.trim()}`);
|
||||||
|
@@ -10,12 +10,13 @@ export enum GatewayProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum GatewayHttpProxyActions {
|
export enum GatewayHttpProxyActions {
|
||||||
InjectGatewayK8sServiceAccountToken = "inject-k8s-sa-auth-token"
|
InjectGatewayK8sServiceAccountToken = "inject-k8s-sa-auth-token",
|
||||||
|
UseGatewayK8sServiceAccount = "use-k8s-sa"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGatewayProxyOptions {
|
export interface IGatewayProxyOptions {
|
||||||
targetHost: string;
|
targetHost?: string;
|
||||||
targetPort: number;
|
targetPort?: number;
|
||||||
relayHost: string;
|
relayHost: string;
|
||||||
relayPort: number;
|
relayPort: number;
|
||||||
tlsOptions: TGatewayTlsOptions;
|
tlsOptions: TGatewayTlsOptions;
|
||||||
|
@@ -19,6 +19,10 @@ import {
|
|||||||
AzureClientSecretsConnectionListItemSchema,
|
AzureClientSecretsConnectionListItemSchema,
|
||||||
SanitizedAzureClientSecretsConnectionSchema
|
SanitizedAzureClientSecretsConnectionSchema
|
||||||
} from "@app/services/app-connection/azure-client-secrets";
|
} from "@app/services/app-connection/azure-client-secrets";
|
||||||
|
import {
|
||||||
|
AzureDevOpsConnectionListItemSchema,
|
||||||
|
SanitizedAzureDevOpsConnectionSchema
|
||||||
|
} from "@app/services/app-connection/azure-devops/azure-devops-schemas";
|
||||||
import {
|
import {
|
||||||
AzureKeyVaultConnectionListItemSchema,
|
AzureKeyVaultConnectionListItemSchema,
|
||||||
SanitizedAzureKeyVaultConnectionSchema
|
SanitizedAzureKeyVaultConnectionSchema
|
||||||
@@ -75,6 +79,7 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedGcpConnectionSchema.options,
|
...SanitizedGcpConnectionSchema.options,
|
||||||
...SanitizedAzureKeyVaultConnectionSchema.options,
|
...SanitizedAzureKeyVaultConnectionSchema.options,
|
||||||
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
||||||
|
...SanitizedAzureDevOpsConnectionSchema.options,
|
||||||
...SanitizedDatabricksConnectionSchema.options,
|
...SanitizedDatabricksConnectionSchema.options,
|
||||||
...SanitizedHumanitecConnectionSchema.options,
|
...SanitizedHumanitecConnectionSchema.options,
|
||||||
...SanitizedTerraformCloudConnectionSchema.options,
|
...SanitizedTerraformCloudConnectionSchema.options,
|
||||||
@@ -100,6 +105,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
GcpConnectionListItemSchema,
|
GcpConnectionListItemSchema,
|
||||||
AzureKeyVaultConnectionListItemSchema,
|
AzureKeyVaultConnectionListItemSchema,
|
||||||
AzureAppConfigurationConnectionListItemSchema,
|
AzureAppConfigurationConnectionListItemSchema,
|
||||||
|
AzureDevOpsConnectionListItemSchema,
|
||||||
DatabricksConnectionListItemSchema,
|
DatabricksConnectionListItemSchema,
|
||||||
HumanitecConnectionListItemSchema,
|
HumanitecConnectionListItemSchema,
|
||||||
TerraformCloudConnectionListItemSchema,
|
TerraformCloudConnectionListItemSchema,
|
||||||
|
@@ -0,0 +1,49 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateAzureDevOpsConnectionSchema,
|
||||||
|
SanitizedAzureDevOpsConnectionSchema,
|
||||||
|
UpdateAzureDevOpsConnectionSchema
|
||||||
|
} from "@app/services/app-connection/azure-devops/azure-devops-schemas";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerAzureDevOpsConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.AzureDevOps,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedAzureDevOpsConnectionSchema,
|
||||||
|
createSchema: CreateAzureDevOpsConnectionSchema,
|
||||||
|
updateSchema: UpdateAzureDevOpsConnectionSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/projects`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
projects: z.object({ name: z.string(), id: z.string(), appId: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const projects = await server.services.appConnection.azureDevOps.listProjects(connectionId, req.permission);
|
||||||
|
|
||||||
|
return { projects };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -6,6 +6,7 @@ import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
|
|||||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||||
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
||||||
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
|
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
|
||||||
|
import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router";
|
||||||
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
||||||
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||||
@@ -34,6 +35,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
|||||||
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
|
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
|
||||||
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||||
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
|
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
|
||||||
|
[AppConnection.AzureDevOps]: registerAzureDevOpsConnectionRouter,
|
||||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
||||||
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
||||||
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
|
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
|
||||||
|
@@ -108,17 +108,21 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
|
.nullable()
|
||||||
.describe(KUBERNETES_AUTH.ATTACH.kubernetesHost)
|
.describe(KUBERNETES_AUTH.ATTACH.kubernetesHost)
|
||||||
.refine(
|
.refine(
|
||||||
(val) =>
|
(val) => {
|
||||||
characterValidator([
|
if (val === null) return true;
|
||||||
|
|
||||||
|
return characterValidator([
|
||||||
CharacterType.Alphabets,
|
CharacterType.Alphabets,
|
||||||
CharacterType.Numbers,
|
CharacterType.Numbers,
|
||||||
CharacterType.Colon,
|
CharacterType.Colon,
|
||||||
CharacterType.Period,
|
CharacterType.Period,
|
||||||
CharacterType.ForwardSlash,
|
CharacterType.ForwardSlash,
|
||||||
CharacterType.Hyphen
|
CharacterType.Hyphen
|
||||||
])(val),
|
])(val);
|
||||||
|
},
|
||||||
{
|
{
|
||||||
message:
|
message:
|
||||||
"Kubernetes host must only contain alphabets, numbers, colons, periods, hyphen, and forward slashes."
|
"Kubernetes host must only contain alphabets, numbers, colons, periods, hyphen, and forward slashes."
|
||||||
@@ -164,6 +168,13 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
.describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit)
|
.describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api && !data.kubernetesHost) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["kubernetesHost"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "When token review mode is set to API, a Kubernetes host must be provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) {
|
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
path: ["gatewayId"],
|
path: ["gatewayId"],
|
||||||
@@ -171,6 +182,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
message: "When token review mode is set to Gateway, a gateway must be selected"
|
message: "When token review mode is set to Gateway, a gateway must be selected"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.accessTokenTTL > data.accessTokenMaxTTL) {
|
if (data.accessTokenTTL > data.accessTokenMaxTTL) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
path: ["accessTokenTTL"],
|
path: ["accessTokenTTL"],
|
||||||
@@ -203,7 +215,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
type: EventType.ADD_IDENTITY_KUBERNETES_AUTH,
|
type: EventType.ADD_IDENTITY_KUBERNETES_AUTH,
|
||||||
metadata: {
|
metadata: {
|
||||||
identityId: identityKubernetesAuth.identityId,
|
identityId: identityKubernetesAuth.identityId,
|
||||||
kubernetesHost: identityKubernetesAuth.kubernetesHost,
|
kubernetesHost: identityKubernetesAuth.kubernetesHost ?? "",
|
||||||
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
||||||
allowedNames: identityKubernetesAuth.allowedNames,
|
allowedNames: identityKubernetesAuth.allowedNames,
|
||||||
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
||||||
@@ -243,6 +255,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
|
.nullable()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(KUBERNETES_AUTH.UPDATE.kubernetesHost)
|
.describe(KUBERNETES_AUTH.UPDATE.kubernetesHost)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -345,7 +358,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH,
|
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH,
|
||||||
metadata: {
|
metadata: {
|
||||||
identityId: identityKubernetesAuth.identityId,
|
identityId: identityKubernetesAuth.identityId,
|
||||||
kubernetesHost: identityKubernetesAuth.kubernetesHost,
|
kubernetesHost: identityKubernetesAuth.kubernetesHost ?? "",
|
||||||
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
||||||
allowedNames: identityKubernetesAuth.allowedNames,
|
allowedNames: identityKubernetesAuth.allowedNames,
|
||||||
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
AzureDevOpsSyncSchema,
|
||||||
|
CreateAzureDevOpsSyncSchema,
|
||||||
|
UpdateAzureDevOpsSyncSchema
|
||||||
|
} from "@app/services/secret-sync/azure-devops";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||||
|
|
||||||
|
export const registerAzureDevOpsSyncRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSyncSecretsEndpoints({
|
||||||
|
destination: SecretSync.AzureDevOps,
|
||||||
|
server,
|
||||||
|
responseSchema: AzureDevOpsSyncSchema,
|
||||||
|
createSchema: CreateAzureDevOpsSyncSchema,
|
||||||
|
updateSchema: UpdateAzureDevOpsSyncSchema
|
||||||
|
});
|
@@ -5,6 +5,7 @@ import { registerOnePassSyncRouter } from "./1password-sync-router";
|
|||||||
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
||||||
import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router";
|
import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router";
|
||||||
import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router";
|
import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router";
|
||||||
|
import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router";
|
||||||
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
|
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
|
||||||
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
||||||
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||||
@@ -26,6 +27,7 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
|||||||
[SecretSync.GCPSecretManager]: registerGcpSyncRouter,
|
[SecretSync.GCPSecretManager]: registerGcpSyncRouter,
|
||||||
[SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter,
|
[SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter,
|
||||||
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
|
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
|
||||||
|
[SecretSync.AzureDevOps]: registerAzureDevOpsSyncRouter,
|
||||||
[SecretSync.Databricks]: registerDatabricksSyncRouter,
|
[SecretSync.Databricks]: registerDatabricksSyncRouter,
|
||||||
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
||||||
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
||||||
|
@@ -19,6 +19,7 @@ import {
|
|||||||
AzureAppConfigurationSyncListItemSchema,
|
AzureAppConfigurationSyncListItemSchema,
|
||||||
AzureAppConfigurationSyncSchema
|
AzureAppConfigurationSyncSchema
|
||||||
} from "@app/services/secret-sync/azure-app-configuration";
|
} from "@app/services/secret-sync/azure-app-configuration";
|
||||||
|
import { AzureDevOpsSyncListItemSchema, AzureDevOpsSyncSchema } from "@app/services/secret-sync/azure-devops";
|
||||||
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
|
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
|
||||||
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
||||||
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
|
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
|
||||||
@@ -38,6 +39,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
|||||||
GcpSyncSchema,
|
GcpSyncSchema,
|
||||||
AzureKeyVaultSyncSchema,
|
AzureKeyVaultSyncSchema,
|
||||||
AzureAppConfigurationSyncSchema,
|
AzureAppConfigurationSyncSchema,
|
||||||
|
AzureDevOpsSyncSchema,
|
||||||
DatabricksSyncSchema,
|
DatabricksSyncSchema,
|
||||||
HumanitecSyncSchema,
|
HumanitecSyncSchema,
|
||||||
TerraformCloudSyncSchema,
|
TerraformCloudSyncSchema,
|
||||||
@@ -57,6 +59,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
|||||||
GcpSyncListItemSchema,
|
GcpSyncListItemSchema,
|
||||||
AzureKeyVaultSyncListItemSchema,
|
AzureKeyVaultSyncListItemSchema,
|
||||||
AzureAppConfigurationSyncListItemSchema,
|
AzureAppConfigurationSyncListItemSchema,
|
||||||
|
AzureDevOpsSyncListItemSchema,
|
||||||
DatabricksSyncListItemSchema,
|
DatabricksSyncListItemSchema,
|
||||||
HumanitecSyncListItemSchema,
|
HumanitecSyncListItemSchema,
|
||||||
TerraformCloudSyncListItemSchema,
|
TerraformCloudSyncListItemSchema,
|
||||||
|
@@ -7,6 +7,7 @@ export enum AppConnection {
|
|||||||
AzureKeyVault = "azure-key-vault",
|
AzureKeyVault = "azure-key-vault",
|
||||||
AzureAppConfiguration = "azure-app-configuration",
|
AzureAppConfiguration = "azure-app-configuration",
|
||||||
AzureClientSecrets = "azure-client-secrets",
|
AzureClientSecrets = "azure-client-secrets",
|
||||||
|
AzureDevOps = "azure-devops",
|
||||||
Humanitec = "humanitec",
|
Humanitec = "humanitec",
|
||||||
TerraformCloud = "terraform-cloud",
|
TerraformCloud = "terraform-cloud",
|
||||||
Vercel = "vercel",
|
Vercel = "vercel",
|
||||||
|
@@ -39,6 +39,11 @@ import {
|
|||||||
getAzureClientSecretsConnectionListItem,
|
getAzureClientSecretsConnectionListItem,
|
||||||
validateAzureClientSecretsConnectionCredentials
|
validateAzureClientSecretsConnectionCredentials
|
||||||
} from "./azure-client-secrets";
|
} from "./azure-client-secrets";
|
||||||
|
import { AzureDevOpsConnectionMethod } from "./azure-devops/azure-devops-enums";
|
||||||
|
import {
|
||||||
|
getAzureDevopsConnectionListItem,
|
||||||
|
validateAzureDevOpsConnectionCredentials
|
||||||
|
} from "./azure-devops/azure-devops-fns";
|
||||||
import {
|
import {
|
||||||
AzureKeyVaultConnectionMethod,
|
AzureKeyVaultConnectionMethod,
|
||||||
getAzureKeyVaultConnectionListItem,
|
getAzureKeyVaultConnectionListItem,
|
||||||
@@ -98,6 +103,7 @@ export const listAppConnectionOptions = () => {
|
|||||||
getGcpConnectionListItem(),
|
getGcpConnectionListItem(),
|
||||||
getAzureKeyVaultConnectionListItem(),
|
getAzureKeyVaultConnectionListItem(),
|
||||||
getAzureAppConfigurationConnectionListItem(),
|
getAzureAppConfigurationConnectionListItem(),
|
||||||
|
getAzureDevopsConnectionListItem(),
|
||||||
getDatabricksConnectionListItem(),
|
getDatabricksConnectionListItem(),
|
||||||
getHumanitecConnectionListItem(),
|
getHumanitecConnectionListItem(),
|
||||||
getTerraformCloudConnectionListItem(),
|
getTerraformCloudConnectionListItem(),
|
||||||
@@ -173,6 +179,7 @@ export const validateAppConnectionCredentials = async (
|
|||||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.AzureClientSecrets]:
|
[AppConnection.AzureClientSecrets]:
|
||||||
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
|
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.AzureDevOps]: validateAzureDevOpsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
@@ -201,6 +208,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
case GitHubConnectionMethod.OAuth:
|
case GitHubConnectionMethod.OAuth:
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
return "OAuth";
|
return "OAuth";
|
||||||
case AwsConnectionMethod.AccessKey:
|
case AwsConnectionMethod.AccessKey:
|
||||||
case OCIConnectionMethod.AccessKey:
|
case OCIConnectionMethod.AccessKey:
|
||||||
@@ -225,6 +233,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case WindmillConnectionMethod.AccessToken:
|
case WindmillConnectionMethod.AccessToken:
|
||||||
case HCVaultConnectionMethod.AccessToken:
|
case HCVaultConnectionMethod.AccessToken:
|
||||||
case TeamCityConnectionMethod.AccessToken:
|
case TeamCityConnectionMethod.AccessToken:
|
||||||
|
case AzureDevOpsConnectionMethod.AccessToken:
|
||||||
return "Access Token";
|
return "Access Token";
|
||||||
case Auth0ConnectionMethod.ClientCredentials:
|
case Auth0ConnectionMethod.ClientCredentials:
|
||||||
return "Client Credentials";
|
return "Client Credentials";
|
||||||
@@ -270,6 +279,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.GCP]: platformManagedCredentialsNotSupported,
|
[AppConnection.GCP]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
|
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.AzureDevOps]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||||
|
@@ -8,6 +8,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.AzureKeyVault]: "Azure Key Vault",
|
[AppConnection.AzureKeyVault]: "Azure Key Vault",
|
||||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||||
[AppConnection.AzureClientSecrets]: "Azure Client Secrets",
|
[AppConnection.AzureClientSecrets]: "Azure Client Secrets",
|
||||||
|
[AppConnection.AzureDevOps]: "Azure DevOps",
|
||||||
[AppConnection.Databricks]: "Databricks",
|
[AppConnection.Databricks]: "Databricks",
|
||||||
[AppConnection.Humanitec]: "Humanitec",
|
[AppConnection.Humanitec]: "Humanitec",
|
||||||
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
||||||
@@ -33,6 +34,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
|||||||
[AppConnection.AzureKeyVault]: AppConnectionPlanType.Regular,
|
[AppConnection.AzureKeyVault]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular,
|
[AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular,
|
[AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular,
|
||||||
|
[AppConnection.AzureDevOps]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Databricks]: AppConnectionPlanType.Regular,
|
[AppConnection.Databricks]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Humanitec]: AppConnectionPlanType.Regular,
|
[AppConnection.Humanitec]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.TerraformCloud]: AppConnectionPlanType.Regular,
|
[AppConnection.TerraformCloud]: AppConnectionPlanType.Regular,
|
||||||
|
@@ -41,6 +41,8 @@ import { awsConnectionService } from "./aws/aws-connection-service";
|
|||||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||||
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
|
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
|
||||||
import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service";
|
import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service";
|
||||||
|
import { ValidateAzureDevOpsConnectionCredentialsSchema } from "./azure-devops/azure-devops-schemas";
|
||||||
|
import { azureDevOpsConnectionService } from "./azure-devops/azure-devops-service";
|
||||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||||
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||||
@@ -84,6 +86,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
|||||||
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
|
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
|
||||||
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
|
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
|
||||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||||
|
[AppConnection.AzureDevOps]: ValidateAzureDevOpsConnectionCredentialsSchema,
|
||||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
||||||
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
||||||
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
|
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
|
||||||
@@ -498,6 +501,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
vercel: vercelConnectionService(connectAppConnectionById),
|
vercel: vercelConnectionService(connectAppConnectionById),
|
||||||
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
|
azureDevOps: azureDevOpsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
hcvault: hcVaultConnectionService(connectAppConnectionById),
|
hcvault: hcVaultConnectionService(connectAppConnectionById),
|
||||||
windmill: windmillConnectionService(connectAppConnectionById),
|
windmill: windmillConnectionService(connectAppConnectionById),
|
||||||
|
@@ -39,6 +39,12 @@ import {
|
|||||||
TAzureClientSecretsConnectionInput,
|
TAzureClientSecretsConnectionInput,
|
||||||
TValidateAzureClientSecretsConnectionCredentialsSchema
|
TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||||
} from "./azure-client-secrets";
|
} from "./azure-client-secrets";
|
||||||
|
import {
|
||||||
|
TAzureDevOpsConnection,
|
||||||
|
TAzureDevOpsConnectionConfig,
|
||||||
|
TAzureDevOpsConnectionInput,
|
||||||
|
TValidateAzureDevOpsConnectionCredentialsSchema
|
||||||
|
} from "./azure-devops/azure-devops-types";
|
||||||
import {
|
import {
|
||||||
TAzureKeyVaultConnection,
|
TAzureKeyVaultConnection,
|
||||||
TAzureKeyVaultConnectionConfig,
|
TAzureKeyVaultConnectionConfig,
|
||||||
@@ -132,6 +138,7 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TGcpConnection
|
| TGcpConnection
|
||||||
| TAzureKeyVaultConnection
|
| TAzureKeyVaultConnection
|
||||||
| TAzureAppConfigurationConnection
|
| TAzureAppConfigurationConnection
|
||||||
|
| TAzureDevOpsConnection
|
||||||
| TDatabricksConnection
|
| TDatabricksConnection
|
||||||
| THumanitecConnection
|
| THumanitecConnection
|
||||||
| TTerraformCloudConnection
|
| TTerraformCloudConnection
|
||||||
@@ -161,6 +168,7 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TGcpConnectionInput
|
| TGcpConnectionInput
|
||||||
| TAzureKeyVaultConnectionInput
|
| TAzureKeyVaultConnectionInput
|
||||||
| TAzureAppConfigurationConnectionInput
|
| TAzureAppConfigurationConnectionInput
|
||||||
|
| TAzureDevOpsConnectionInput
|
||||||
| TDatabricksConnectionInput
|
| TDatabricksConnectionInput
|
||||||
| THumanitecConnectionInput
|
| THumanitecConnectionInput
|
||||||
| TTerraformCloudConnectionInput
|
| TTerraformCloudConnectionInput
|
||||||
@@ -197,6 +205,7 @@ export type TAppConnectionConfig =
|
|||||||
| TGcpConnectionConfig
|
| TGcpConnectionConfig
|
||||||
| TAzureKeyVaultConnectionConfig
|
| TAzureKeyVaultConnectionConfig
|
||||||
| TAzureAppConfigurationConnectionConfig
|
| TAzureAppConfigurationConnectionConfig
|
||||||
|
| TAzureDevOpsConnectionConfig
|
||||||
| TAzureClientSecretsConnectionConfig
|
| TAzureClientSecretsConnectionConfig
|
||||||
| TDatabricksConnectionConfig
|
| TDatabricksConnectionConfig
|
||||||
| THumanitecConnectionConfig
|
| THumanitecConnectionConfig
|
||||||
@@ -220,6 +229,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateAzureKeyVaultConnectionCredentialsSchema
|
| TValidateAzureKeyVaultConnectionCredentialsSchema
|
||||||
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||||
| TValidateAzureClientSecretsConnectionCredentialsSchema
|
| TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||||
|
| TValidateAzureDevOpsConnectionCredentialsSchema
|
||||||
| TValidateDatabricksConnectionCredentialsSchema
|
| TValidateDatabricksConnectionCredentialsSchema
|
||||||
| TValidateHumanitecConnectionCredentialsSchema
|
| TValidateHumanitecConnectionCredentialsSchema
|
||||||
| TValidatePostgresConnectionCredentialsSchema
|
| TValidatePostgresConnectionCredentialsSchema
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
export enum AzureDevOpsConnectionMethod {
|
||||||
|
OAuth = "oauth",
|
||||||
|
AccessToken = "access-token"
|
||||||
|
}
|
@@ -0,0 +1,269 @@
|
|||||||
|
/* eslint-disable no-case-declarations */
|
||||||
|
import { AxiosError, AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import {
|
||||||
|
decryptAppConnectionCredentials,
|
||||||
|
encryptAppConnectionCredentials,
|
||||||
|
getAppConnectionMethodName
|
||||||
|
} from "@app/services/app-connection/app-connection-fns";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { TAppConnectionDALFactory } from "../app-connection-dal";
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
|
||||||
|
import {
|
||||||
|
ExchangeCodeAzureResponse,
|
||||||
|
TAzureDevOpsConnectionConfig,
|
||||||
|
TAzureDevOpsConnectionCredentials
|
||||||
|
} from "./azure-devops-types";
|
||||||
|
|
||||||
|
export const getAzureDevopsConnectionListItem = () => {
|
||||||
|
const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "Azure DevOps" as const,
|
||||||
|
app: AppConnection.AzureDevOps as const,
|
||||||
|
methods: Object.values(AzureDevOpsConnectionMethod) as [
|
||||||
|
AzureDevOpsConnectionMethod.OAuth,
|
||||||
|
AzureDevOpsConnectionMethod.AccessToken
|
||||||
|
],
|
||||||
|
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAzureDevopsConnection = async (
|
||||||
|
connectionId: string,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
) => {
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) {
|
||||||
|
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appConnection.app !== AppConnection.AzureDevOps) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Connection with ID '${connectionId}' is not an Azure DevOps connection`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = (await decryptAppConnectionCredentials({
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService,
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials
|
||||||
|
})) as TAzureDevOpsConnectionCredentials;
|
||||||
|
|
||||||
|
// Handle different connection methods
|
||||||
|
switch (appConnection.method) {
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Azure environment variables have not been configured`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("refreshToken" in credentials)) {
|
||||||
|
throw new BadRequestError({ message: "Invalid OAuth credentials" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { refreshToken, tenantId } = credentials;
|
||||||
|
const currentTime = Date.now();
|
||||||
|
|
||||||
|
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
scope: `https://app.vssps.visualstudio.com/.default`,
|
||||||
|
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||||
|
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||||
|
refresh_token: refreshToken
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedCredentials = {
|
||||||
|
...credentials,
|
||||||
|
accessToken: data.access_token,
|
||||||
|
expiresAt: currentTime + data.expires_in * 1000,
|
||||||
|
refreshToken: data.refresh_token
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: updatedCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||||
|
|
||||||
|
return data.access_token;
|
||||||
|
|
||||||
|
case AzureDevOpsConnectionMethod.AccessToken:
|
||||||
|
if (!("accessToken" in credentials)) {
|
||||||
|
throw new BadRequestError({ message: "Invalid API token credentials" });
|
||||||
|
}
|
||||||
|
// For access token, return the basic auth token directly
|
||||||
|
return credentials.accessToken;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: `Unsupported connection method` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDevOpsConnectionConfig) => {
|
||||||
|
const { credentials: inputCredentials, method } = config;
|
||||||
|
|
||||||
|
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
|
if (!SITE_URL) {
|
||||||
|
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||||
|
let tokenError: AxiosError | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const oauthCredentials = inputCredentials as { code: string; tenantId: string };
|
||||||
|
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", oauthCredentials.tenantId || "common"),
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: oauthCredentials.code,
|
||||||
|
scope: `https://app.vssps.visualstudio.com/.default`,
|
||||||
|
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||||
|
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||||
|
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof AxiosError) {
|
||||||
|
tokenError = e;
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate connection: verify credentials`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenError) {
|
||||||
|
if (tokenError instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to get access token: ${
|
||||||
|
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: "Failed to get access token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenResp) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Failed to get access token: Token was empty with no error`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauthCredentials = inputCredentials as { code: string; tenantId: string; orgName: string };
|
||||||
|
return {
|
||||||
|
tenantId: oauthCredentials.tenantId,
|
||||||
|
orgName: oauthCredentials.orgName,
|
||||||
|
accessToken: tokenResp.data.access_token,
|
||||||
|
refreshToken: tokenResp.data.refresh_token,
|
||||||
|
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
case AzureDevOpsConnectionMethod.AccessToken:
|
||||||
|
const accessTokenCredentials = inputCredentials as { accessToken: string; orgName?: string };
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (accessTokenCredentials.orgName) {
|
||||||
|
// Validate against specific organization
|
||||||
|
const response = await request.get(
|
||||||
|
`${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(accessTokenCredentials.orgName)}/_apis/projects?api-version=7.2-preview.2&$top=1`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${Buffer.from(`:${accessTokenCredentials.accessToken}`).toString("base64")}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to validate connection: ${response.status}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: accessTokenCredentials.accessToken,
|
||||||
|
orgName: accessTokenCredentials.orgName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Validate via profile and discover organizations
|
||||||
|
const profileResponse = await request.get<{ displayName: string }>(
|
||||||
|
`https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.1`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${Buffer.from(`:${accessTokenCredentials.accessToken}`).toString("base64")}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let organizations: Array<{ accountId: string; accountName: string; accountUri: string }> = [];
|
||||||
|
try {
|
||||||
|
const orgsResponse = await request.get<{
|
||||||
|
value: Array<{ accountId: string; accountName: string; accountUri: string }>;
|
||||||
|
}>(`https://app.vssps.visualstudio.com/_apis/accounts?api-version=7.1`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${Buffer.from(`:${accessTokenCredentials.accessToken}`).toString("base64")}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
organizations = orgsResponse.data.value || [];
|
||||||
|
} catch (orgError) {
|
||||||
|
logger.warn(orgError, "Could not fetch organizations automatically:");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: accessTokenCredentials.accessToken,
|
||||||
|
userDisplayName: profileResponse.data.displayName,
|
||||||
|
organizations: organizations.map((org) => ({
|
||||||
|
accountId: org.accountId,
|
||||||
|
accountName: org.accountName,
|
||||||
|
accountUri: org.accountUri
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
const errorMessage = accessTokenCredentials.orgName
|
||||||
|
? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
`Failed to validate access token for organization '${accessTokenCredentials.orgName}': ${error.response?.data?.message || error.message}`
|
||||||
|
: `Invalid Azure DevOps Personal Access Token: ${error.response?.status === 401 ? "Token is invalid or expired" : error.message}`;
|
||||||
|
|
||||||
|
throw new BadRequestError({ message: errorMessage });
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate Azure DevOps token`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Unhandled Azure connection method: ${method as AzureDevOpsConnectionMethod}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,112 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
BaseAppConnectionSchema,
|
||||||
|
GenericCreateAppConnectionFieldsSchema,
|
||||||
|
GenericUpdateAppConnectionFieldsSchema
|
||||||
|
} from "@app/services/app-connection/app-connection-schemas";
|
||||||
|
|
||||||
|
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionOAuthInputCredentialsSchema = z.object({
|
||||||
|
code: z.string().trim().min(1, "OAuth code required").describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.code),
|
||||||
|
tenantId: z.string().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 AzureDevOpsConnectionOAuthOutputCredentialsSchema = z.object({
|
||||||
|
tenantId: z.string(),
|
||||||
|
orgName: z.string(),
|
||||||
|
accessToken: z.string(),
|
||||||
|
refreshToken: z.string(),
|
||||||
|
expiresAt: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionAccessTokenInputCredentialsSchema = z.object({
|
||||||
|
accessToken: z.string().trim().min(1, "Access Token required"),
|
||||||
|
orgName: z.string().trim().min(1, "Organization name required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionAccessTokenOutputCredentialsSchema = z.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
orgName: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ValidateAzureDevOpsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(AzureDevOpsConnectionMethod.OAuth)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.AzureDevOps).method),
|
||||||
|
credentials: AzureDevOpsConnectionOAuthInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.AzureDevOps).credentials
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(AzureDevOpsConnectionMethod.AccessToken)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.AzureDevOps).method),
|
||||||
|
credentials: AzureDevOpsConnectionAccessTokenInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.AzureDevOps).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateAzureDevOpsConnectionSchema = ValidateAzureDevOpsConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.AzureDevOps)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateAzureDevOpsConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: z
|
||||||
|
.union([AzureDevOpsConnectionOAuthInputCredentialsSchema, AzureDevOpsConnectionAccessTokenInputCredentialsSchema])
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.UPDATE(AppConnection.AzureDevOps).credentials)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureDevOps));
|
||||||
|
|
||||||
|
const BaseAzureDevOpsConnectionSchema = BaseAppConnectionSchema.extend({
|
||||||
|
app: z.literal(AppConnection.AzureDevOps)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionSchema = z.intersection(
|
||||||
|
BaseAzureDevOpsConnectionSchema,
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AzureDevOpsConnectionMethod.OAuth),
|
||||||
|
credentials: AzureDevOpsConnectionOAuthOutputCredentialsSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AzureDevOpsConnectionMethod.AccessToken),
|
||||||
|
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SanitizedAzureDevOpsConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseAzureDevOpsConnectionSchema.extend({
|
||||||
|
method: z.literal(AzureDevOpsConnectionMethod.OAuth),
|
||||||
|
credentials: AzureDevOpsConnectionOAuthOutputCredentialsSchema.pick({
|
||||||
|
tenantId: true,
|
||||||
|
orgName: true
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
BaseAzureDevOpsConnectionSchema.extend({
|
||||||
|
method: z.literal(AzureDevOpsConnectionMethod.AccessToken),
|
||||||
|
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema.pick({
|
||||||
|
orgName: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const AzureDevOpsConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("Azure DevOps"),
|
||||||
|
app: z.literal(AppConnection.AzureDevOps),
|
||||||
|
methods: z.nativeEnum(AzureDevOpsConnectionMethod).array(),
|
||||||
|
oauthClientId: z.string().optional()
|
||||||
|
});
|
@@ -0,0 +1,127 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable no-case-declarations */
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
|
||||||
|
import { getAzureDevopsConnection } from "./azure-devops-fns";
|
||||||
|
import { TAzureDevOpsConnection } from "./azure-devops-types";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TAzureDevOpsConnection>;
|
||||||
|
|
||||||
|
type TAzureDevOpsProject = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
state?: string;
|
||||||
|
visibility?: string;
|
||||||
|
lastUpdateTime?: string;
|
||||||
|
revision?: number;
|
||||||
|
abbreviation?: string;
|
||||||
|
defaultTeamImageUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TAzureDevOpsProjectsResponse = {
|
||||||
|
count: number;
|
||||||
|
value: TAzureDevOpsProject[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAuthHeaders = (appConnection: TAzureDevOpsConnection, accessToken: string) => {
|
||||||
|
switch (appConnection.method) {
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
};
|
||||||
|
case AzureDevOpsConnectionMethod.AccessToken:
|
||||||
|
// For access token, create Basic auth header
|
||||||
|
const basicAuthToken = Buffer.from(`user:${accessToken}`).toString("base64");
|
||||||
|
return {
|
||||||
|
Authorization: `Basic ${basicAuthToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: "Unsupported connection method" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const listAzureDevOpsProjects = async (
|
||||||
|
appConnection: TAzureDevOpsConnection,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
): Promise<TAzureDevOpsProject[]> => {
|
||||||
|
const accessToken = await getAzureDevopsConnection(appConnection.id, appConnectionDAL, kmsService);
|
||||||
|
|
||||||
|
// Both OAuth and access Token methods use organization name from credentials
|
||||||
|
const credentials = appConnection.credentials as { orgName: string };
|
||||||
|
const { orgName } = credentials;
|
||||||
|
|
||||||
|
// Use the standard Azure DevOps Projects API endpoint
|
||||||
|
// This endpoint returns only projects that the authenticated user has access to
|
||||||
|
const devOpsEndpoint = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/projects?api-version=7.1`;
|
||||||
|
try {
|
||||||
|
const { data } = await request.get<TAzureDevOpsProjectsResponse>(devOpsEndpoint, {
|
||||||
|
headers: getAuthHeaders(appConnection, accessToken)
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.value || [];
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
// Provide more specific error messages based on the response
|
||||||
|
if (error?.response?.status === 401) {
|
||||||
|
throw new Error(
|
||||||
|
`Authentication failed for Azure DevOps organization: ${orgName}. Please check your credentials and ensure the token has the required scopes (vso.project or vso.profile).`
|
||||||
|
);
|
||||||
|
} else if (error?.response?.status === 403) {
|
||||||
|
throw new Error(
|
||||||
|
`Access denied to Azure DevOps organization: ${orgName}. Please ensure the user has access to the organization.`
|
||||||
|
);
|
||||||
|
} else if (error?.response?.status === 404) {
|
||||||
|
throw new Error(`Azure DevOps organization not found: ${orgName}. Please verify the organization name.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const azureDevOpsConnectionService = (
|
||||||
|
getAppConnection: TGetAppConnectionFunc,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
) => {
|
||||||
|
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.AzureDevOps, connectionId, actor);
|
||||||
|
|
||||||
|
const projects = await listAzureDevOpsProjects(appConnection, appConnectionDAL, kmsService);
|
||||||
|
|
||||||
|
return projects.map((project) => ({
|
||||||
|
id: project.id,
|
||||||
|
name: project.name,
|
||||||
|
appId: project.id,
|
||||||
|
description: project.description,
|
||||||
|
url: project.url,
|
||||||
|
state: project.state,
|
||||||
|
visibility: project.visibility,
|
||||||
|
lastUpdateTime: project.lastUpdateTime,
|
||||||
|
revision: project.revision,
|
||||||
|
abbreviation: project.abbreviation,
|
||||||
|
defaultTeamImageUrl: project.defaultTeamImageUrl
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listProjects
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,54 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import {
|
||||||
|
AzureDevOpsConnectionOAuthOutputCredentialsSchema,
|
||||||
|
AzureDevOpsConnectionSchema,
|
||||||
|
CreateAzureDevOpsConnectionSchema,
|
||||||
|
ValidateAzureDevOpsConnectionCredentialsSchema
|
||||||
|
} from "./azure-devops-schemas";
|
||||||
|
|
||||||
|
export type TAzureDevOpsConnection = z.infer<typeof AzureDevOpsConnectionSchema>;
|
||||||
|
|
||||||
|
export type TAzureDevOpsConnectionInput = z.infer<typeof CreateAzureDevOpsConnectionSchema> & {
|
||||||
|
app: AppConnection.AzureDevOps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateAzureDevOpsConnectionCredentialsSchema = typeof ValidateAzureDevOpsConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TAzureDevOpsConnectionConfig = DiscriminativePick<
|
||||||
|
TAzureDevOpsConnectionInput,
|
||||||
|
"method" | "app" | "credentials"
|
||||||
|
> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAzureDevOpsConnectionCredentials = z.infer<typeof AzureDevOpsConnectionOAuthOutputCredentialsSchema>;
|
||||||
|
|
||||||
|
export interface ExchangeCodeAzureResponse {
|
||||||
|
token_type: string;
|
||||||
|
scope: string;
|
||||||
|
expires_in: number;
|
||||||
|
ext_expires_in: number;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
id_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TAzureRegisteredApp {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
displayName: string;
|
||||||
|
description?: string;
|
||||||
|
createdDateTime: string;
|
||||||
|
identifierUris?: string[];
|
||||||
|
signInAudience?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TAzureListRegisteredAppsResponse {
|
||||||
|
"@odata.context": string;
|
||||||
|
"@odata.nextLink"?: string;
|
||||||
|
value: TAzureRegisteredApp[];
|
||||||
|
}
|
@@ -120,7 +120,7 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
return docs.map((doc) => {
|
return docs.map((doc) => {
|
||||||
// Determine if this is a secret or folder change based on populated fields
|
// Determine if this is a secret or folder change based on populated fields
|
||||||
if (doc.secretKey && doc.secretVersion && doc.secretId) {
|
if (doc.secretKey && doc.secretVersion !== null && doc.secretId) {
|
||||||
return {
|
return {
|
||||||
...doc,
|
...doc,
|
||||||
resourceType: "secret",
|
resourceType: "secret",
|
||||||
@@ -168,7 +168,7 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return docs
|
return docs
|
||||||
.filter((doc) => doc.secretKey && doc.secretVersion && doc.secretId)
|
.filter((doc) => doc.secretKey && doc.secretVersion !== null && doc.secretId)
|
||||||
.map(
|
.map(
|
||||||
(doc): SecretCommitChange => ({
|
(doc): SecretCommitChange => ({
|
||||||
...doc,
|
...doc,
|
||||||
@@ -209,7 +209,7 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return docs
|
return docs
|
||||||
.filter((doc) => doc.folderName && doc.folderVersion && doc.folderChangeId)
|
.filter((doc) => doc.folderName && doc.folderVersion !== null && doc.folderChangeId)
|
||||||
.map(
|
.map(
|
||||||
(doc): FolderCommitChange => ({
|
(doc): FolderCommitChange => ({
|
||||||
...doc,
|
...doc,
|
||||||
|
@@ -815,7 +815,7 @@ export const folderCommitServiceFactory = ({
|
|||||||
encryptedComment: version1.encryptedComment
|
encryptedComment: version1.encryptedComment
|
||||||
? secretManagerDecryptor({ cipherTextBlob: version1.encryptedComment }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: version1.encryptedComment }).toString()
|
||||||
: "",
|
: "",
|
||||||
metadata: version1.metadata as { key: string; value: string }[],
|
metadata: Array.isArray(version1.metadata) ? (version1.metadata as { key: string; value: string }[]) : [],
|
||||||
tags: version1.tags.map((tag) => tag.id)
|
tags: version1.tags.map((tag) => tag.id)
|
||||||
};
|
};
|
||||||
const version2Reshaped = {
|
const version2Reshaped = {
|
||||||
@@ -826,7 +826,7 @@ export const folderCommitServiceFactory = ({
|
|||||||
encryptedComment: version2.encryptedComment
|
encryptedComment: version2.encryptedComment
|
||||||
? secretManagerDecryptor({ cipherTextBlob: version2.encryptedComment }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: version2.encryptedComment }).toString()
|
||||||
: "",
|
: "",
|
||||||
metadata: version2.metadata as { key: string; value: string }[],
|
metadata: Array.isArray(version2.metadata) ? (version2.metadata as { key: string; value: string }[]) : [],
|
||||||
tags: version2.tags.map((tag) => tag.id)
|
tags: version2.tags.map((tag) => tag.id)
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
@@ -72,8 +72,8 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
const $gatewayProxyWrapper = async <T>(
|
const $gatewayProxyWrapper = async <T>(
|
||||||
inputs: {
|
inputs: {
|
||||||
gatewayId: string;
|
gatewayId: string;
|
||||||
targetHost: string;
|
targetHost?: string;
|
||||||
targetPort: number;
|
targetPort?: number;
|
||||||
caCert?: string;
|
caCert?: string;
|
||||||
reviewTokenThroughGateway: boolean;
|
reviewTokenThroughGateway: boolean;
|
||||||
},
|
},
|
||||||
@@ -104,11 +104,15 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
cert: relayDetails.certificate,
|
cert: relayDetails.certificate,
|
||||||
key: relayDetails.privateKey.toString()
|
key: relayDetails.privateKey.toString()
|
||||||
},
|
},
|
||||||
// we always pass this, because its needed for both tcp and http protocol
|
// only needed for TCP protocol, because the gateway as reviewer will use the pod's CA cert for auth directly
|
||||||
httpsAgent: new https.Agent({
|
...(!inputs.reviewTokenThroughGateway
|
||||||
ca: inputs.caCert,
|
? {
|
||||||
rejectUnauthorized: Boolean(inputs.caCert)
|
httpsAgent: new https.Agent({
|
||||||
})
|
ca: inputs.caCert,
|
||||||
|
rejectUnauthorized: Boolean(inputs.caCert)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
: {})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -142,8 +146,15 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
|
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenReviewCallbackRaw = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => {
|
const tokenReviewCallbackRaw = async (host = identityKubernetesAuth.kubernetesHost, port?: number) => {
|
||||||
logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API");
|
logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API");
|
||||||
|
|
||||||
|
if (!host || !identityKubernetesAuth.kubernetesHost) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Kubernetes host is required when token review mode is set to API"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let tokenReviewerJwt = "";
|
let tokenReviewerJwt = "";
|
||||||
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
|
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
|
||||||
tokenReviewerJwt = decryptor({
|
tokenReviewerJwt = decryptor({
|
||||||
@@ -211,11 +222,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenReviewCallbackThroughGateway = async (
|
const tokenReviewCallbackThroughGateway = async (host: string, port?: number) => {
|
||||||
host: string = identityKubernetesAuth.kubernetesHost,
|
|
||||||
port?: number,
|
|
||||||
httpsAgent?: https.Agent
|
|
||||||
) => {
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
host,
|
host,
|
||||||
@@ -224,11 +231,9 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
"tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway"
|
"tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway"
|
||||||
);
|
);
|
||||||
|
|
||||||
const baseUrl = port ? `${host}:${port}` : host;
|
|
||||||
|
|
||||||
const res = await axios
|
const res = await axios
|
||||||
.post<TCreateTokenReviewResponse>(
|
.post<TCreateTokenReviewResponse>(
|
||||||
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
|
`${host}:${port}/apis/authentication.k8s.io/v1/tokenreviews`,
|
||||||
{
|
{
|
||||||
apiVersion: "authentication.k8s.io/v1",
|
apiVersion: "authentication.k8s.io/v1",
|
||||||
kind: "TokenReview",
|
kind: "TokenReview",
|
||||||
@@ -240,11 +245,10 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken
|
"x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(10000),
|
signal: AbortSignal.timeout(10000),
|
||||||
timeout: 10000,
|
timeout: 10000
|
||||||
...(httpsAgent ? { httpsAgent } : {})
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -273,29 +277,6 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
let data: TCreateTokenReviewResponse | undefined;
|
let data: TCreateTokenReviewResponse | undefined;
|
||||||
|
|
||||||
if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
|
if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
|
||||||
const { kubernetesHost } = identityKubernetesAuth;
|
|
||||||
|
|
||||||
let urlString = kubernetesHost;
|
|
||||||
if (!kubernetesHost.startsWith("http://") && !kubernetesHost.startsWith("https://")) {
|
|
||||||
urlString = `https://${kubernetesHost}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(urlString);
|
|
||||||
let { port: k8sPort } = url;
|
|
||||||
const { protocol, hostname: k8sHost } = url;
|
|
||||||
|
|
||||||
const cleanedProtocol = new RE2(/[^a-zA-Z0-9]/g).replace(protocol, "").toLowerCase();
|
|
||||||
|
|
||||||
if (!["https", "http"].includes(cleanedProtocol)) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Invalid Kubernetes host URL, must start with http:// or https://"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!k8sPort) {
|
|
||||||
k8sPort = cleanedProtocol === "https" ? "443" : "80";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!identityKubernetesAuth.gatewayId) {
|
if (!identityKubernetesAuth.gatewayId) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Gateway ID is required when token review mode is set to Gateway"
|
message: "Gateway ID is required when token review mode is set to Gateway"
|
||||||
@@ -305,14 +286,17 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
data = await $gatewayProxyWrapper(
|
data = await $gatewayProxyWrapper(
|
||||||
{
|
{
|
||||||
gatewayId: identityKubernetesAuth.gatewayId,
|
gatewayId: identityKubernetesAuth.gatewayId,
|
||||||
targetHost: `${cleanedProtocol}://${k8sHost}`, // note(daniel): must include the protocol (https|http)
|
|
||||||
targetPort: k8sPort ? Number(k8sPort) : 443,
|
|
||||||
caCert,
|
|
||||||
reviewTokenThroughGateway: true
|
reviewTokenThroughGateway: true
|
||||||
},
|
},
|
||||||
tokenReviewCallbackThroughGateway
|
tokenReviewCallbackThroughGateway
|
||||||
);
|
);
|
||||||
} else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) {
|
} else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) {
|
||||||
|
if (!identityKubernetesAuth.kubernetesHost) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Kubernetes host is required when token review mode is set to API"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let { kubernetesHost } = identityKubernetesAuth;
|
let { kubernetesHost } = identityKubernetesAuth;
|
||||||
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
|
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
|
||||||
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
|
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
|
||||||
|
@@ -12,7 +12,7 @@ export enum IdentityKubernetesAuthTokenReviewMode {
|
|||||||
|
|
||||||
export type TAttachKubernetesAuthDTO = {
|
export type TAttachKubernetesAuthDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
kubernetesHost: string;
|
kubernetesHost: string | null;
|
||||||
caCert: string;
|
caCert: string;
|
||||||
tokenReviewerJwt?: string;
|
tokenReviewerJwt?: string;
|
||||||
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
|
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
|
||||||
@@ -29,7 +29,7 @@ export type TAttachKubernetesAuthDTO = {
|
|||||||
|
|
||||||
export type TUpdateKubernetesAuthDTO = {
|
export type TUpdateKubernetesAuthDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
kubernetesHost?: string;
|
kubernetesHost?: string | null;
|
||||||
caCert?: string;
|
caCert?: string;
|
||||||
tokenReviewerJwt?: string | null;
|
tokenReviewerJwt?: string | null;
|
||||||
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;
|
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
export const AZURE_DEVOPS_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||||
|
name: "Azure DevOps",
|
||||||
|
destination: SecretSync.AzureDevOps,
|
||||||
|
connection: AppConnection.AzureDevOps,
|
||||||
|
canImportSecrets: false
|
||||||
|
};
|
@@ -0,0 +1,233 @@
|
|||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { AzureDevOpsConnectionMethod } from "@app/services/app-connection/azure-devops/azure-devops-enums";
|
||||||
|
import { getAzureDevopsConnection } from "@app/services/app-connection/azure-devops/azure-devops-fns";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { TAzureDevOpsSyncWithCredentials } from "./azure-devops-sync-types";
|
||||||
|
|
||||||
|
type TAzureDevOpsSyncFactoryDeps = {
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AzureDevOpsVariableGroup {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: string;
|
||||||
|
variables: Record<string, { value: string; isSecret: boolean }>;
|
||||||
|
variableGroupProjectReferences: Array<{
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
|
projectReference: { id: string; name: string };
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AzureDevOpsVariableGroupList {
|
||||||
|
count: number;
|
||||||
|
value: AzureDevOpsVariableGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const azureDevOpsSyncFactory = ({ kmsService, appConnectionDAL }: TAzureDevOpsSyncFactoryDeps) => {
|
||||||
|
const getConnectionAuth = async (secretSync: TAzureDevOpsSyncWithCredentials) => {
|
||||||
|
const { credentials } = secretSync.connection;
|
||||||
|
const isOAuth = secretSync.connection.method === AzureDevOpsConnectionMethod.OAuth;
|
||||||
|
|
||||||
|
const { orgName } = credentials;
|
||||||
|
if (!orgName) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Azure DevOps: organization name is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = await getAzureDevopsConnection(secretSync.connectionId, appConnectionDAL, kmsService);
|
||||||
|
|
||||||
|
return { accessToken, orgName, isOAuth };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAuthHeader = (accessToken: string, isOAuth: boolean) => {
|
||||||
|
if (isOAuth) {
|
||||||
|
return `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
const basicAuth = Buffer.from(`:${accessToken}`).toString("base64");
|
||||||
|
return `Basic ${basicAuth}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const $getEnvGroupId = async (
|
||||||
|
accessToken: string,
|
||||||
|
orgName: string,
|
||||||
|
projectId: string,
|
||||||
|
environmentName: string,
|
||||||
|
isOAuth: boolean
|
||||||
|
) => {
|
||||||
|
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/${encodeURIComponent(projectId)}/_apis/distributedtask/variablegroups?api-version=7.1`;
|
||||||
|
const response = await request.get<AzureDevOpsVariableGroupList>(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: getAuthHeader(accessToken, isOAuth)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const group of response.data.value) {
|
||||||
|
if (group.name === environmentName) {
|
||||||
|
return { groupId: group.id.toString(), groupName: group.name };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { groupId: "", groupName: "" };
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncSecrets = async (secretSync: TAzureDevOpsSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
|
if (!secretSync.destinationConfig.devopsProjectId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Azure DevOps: project ID is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secretSync.environment?.name) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Azure DevOps: environment name is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken, orgName, isOAuth } = await getConnectionAuth(secretSync);
|
||||||
|
|
||||||
|
const { groupId, groupName } = await $getEnvGroupId(
|
||||||
|
accessToken,
|
||||||
|
orgName,
|
||||||
|
secretSync.destinationConfig.devopsProjectId,
|
||||||
|
secretSync.environment.name,
|
||||||
|
isOAuth
|
||||||
|
);
|
||||||
|
|
||||||
|
const variables: Record<string, { value: string; isSecret: boolean }> = {};
|
||||||
|
for (const [key, secret] of Object.entries(secretMap)) {
|
||||||
|
if (secret?.value !== undefined) {
|
||||||
|
variables[key] = { value: secret.value, isSecret: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupId) {
|
||||||
|
// Create new variable group - API endpoint is organization-level
|
||||||
|
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/distributedtask/variablegroups?api-version=7.1`;
|
||||||
|
|
||||||
|
await request.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
name: secretSync.environment.name,
|
||||||
|
description: secretSync.environment.name,
|
||||||
|
type: "Vsts",
|
||||||
|
variables,
|
||||||
|
variableGroupProjectReferences: [
|
||||||
|
{
|
||||||
|
description: secretSync.environment.name,
|
||||||
|
name: secretSync.environment.name,
|
||||||
|
projectReference: {
|
||||||
|
id: secretSync.destinationConfig.devopsProjectId,
|
||||||
|
name: secretSync.destinationConfig.devopsProjectId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: getAuthHeader(accessToken, isOAuth),
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.1`;
|
||||||
|
|
||||||
|
await request.put(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
name: groupName,
|
||||||
|
description: groupName,
|
||||||
|
type: "Vsts",
|
||||||
|
variables,
|
||||||
|
variableGroupProjectReferences: [
|
||||||
|
{
|
||||||
|
description: groupName,
|
||||||
|
name: groupName,
|
||||||
|
projectReference: {
|
||||||
|
id: secretSync.destinationConfig.devopsProjectId,
|
||||||
|
name: secretSync.destinationConfig.devopsProjectId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: getAuthHeader(accessToken, isOAuth),
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSecrets = async (secretSync: TAzureDevOpsSyncWithCredentials) => {
|
||||||
|
const { accessToken, orgName, isOAuth } = await getConnectionAuth(secretSync);
|
||||||
|
|
||||||
|
const { groupId } = await $getEnvGroupId(
|
||||||
|
accessToken,
|
||||||
|
orgName,
|
||||||
|
secretSync.destinationConfig.devopsProjectId,
|
||||||
|
secretSync.environment?.name || "",
|
||||||
|
isOAuth
|
||||||
|
);
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
// Delete the variable group entirely using the DELETE API
|
||||||
|
const deleteUrl = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/distributedtask/variablegroups/${groupId}?projectIds=${secretSync.destinationConfig.devopsProjectId}&api-version=7.1`;
|
||||||
|
|
||||||
|
await request.delete(deleteUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: getAuthHeader(accessToken, isOAuth)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecrets = async (secretSync: TAzureDevOpsSyncWithCredentials) => {
|
||||||
|
const { accessToken, orgName, isOAuth } = await getConnectionAuth(secretSync);
|
||||||
|
|
||||||
|
const { groupId } = await $getEnvGroupId(
|
||||||
|
accessToken,
|
||||||
|
orgName,
|
||||||
|
secretSync.destinationConfig.devopsProjectId,
|
||||||
|
secretSync.environment?.name || "",
|
||||||
|
isOAuth
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretMap: TSecretMap = {};
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.1`;
|
||||||
|
const response = await request.get<AzureDevOpsVariableGroup>(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: getAuthHeader(accessToken, isOAuth)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.data?.variables) {
|
||||||
|
Object.entries(response.data.variables).forEach(([key, variable]) => {
|
||||||
|
secretMap[key] = {
|
||||||
|
value: variable.value || ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return secretMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
syncSecrets,
|
||||||
|
removeSecrets,
|
||||||
|
getSecrets
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,50 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretSyncs } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import {
|
||||||
|
BaseSecretSyncSchema,
|
||||||
|
GenericCreateSecretSyncFieldsSchema,
|
||||||
|
GenericUpdateSecretSyncFieldsSchema
|
||||||
|
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||||
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncDestinationConfigSchema = z.object({
|
||||||
|
devopsProjectId: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Project ID required")
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectId || "Azure DevOps Project ID"),
|
||||||
|
devopsProjectName: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Project name required")
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectName || "Azure DevOps Project Name")
|
||||||
|
});
|
||||||
|
|
||||||
|
const AzureDevOpsSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncSchema = BaseSecretSyncSchema(SecretSync.AzureDevOps, AzureDevOpsSyncOptionsConfig).extend({
|
||||||
|
destination: z.literal(SecretSync.AzureDevOps),
|
||||||
|
destinationConfig: AzureDevOpsSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateAzureDevOpsSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.AzureDevOps,
|
||||||
|
AzureDevOpsSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: AzureDevOpsSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateAzureDevOpsSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.AzureDevOps,
|
||||||
|
AzureDevOpsSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: AzureDevOpsSyncDestinationConfigSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncListItemSchema = z.object({
|
||||||
|
name: z.literal("Azure DevOps"),
|
||||||
|
connection: z.literal(AppConnection.AzureDevOps),
|
||||||
|
destination: z.literal(SecretSync.AzureDevOps),
|
||||||
|
canImportSecrets: z.literal(false)
|
||||||
|
});
|
@@ -0,0 +1,22 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TAzureDevOpsConnection } from "@app/services/app-connection/azure-devops/azure-devops-types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AzureDevOpsSyncDestinationConfigSchema,
|
||||||
|
AzureDevOpsSyncListItemSchema,
|
||||||
|
AzureDevOpsSyncSchema,
|
||||||
|
CreateAzureDevOpsSyncSchema
|
||||||
|
} from "./azure-devops-sync-schemas";
|
||||||
|
|
||||||
|
export type TAzureDevOpsSync = z.infer<typeof AzureDevOpsSyncSchema>;
|
||||||
|
|
||||||
|
export type TAzureDevOpsSyncInput = z.infer<typeof CreateAzureDevOpsSyncSchema>;
|
||||||
|
|
||||||
|
export type TAzureDevOpsSyncListItem = z.infer<typeof AzureDevOpsSyncListItemSchema>;
|
||||||
|
|
||||||
|
export type TAzureDevOpsSyncDestinationConfig = z.infer<typeof AzureDevOpsSyncDestinationConfigSchema>;
|
||||||
|
|
||||||
|
export type TAzureDevOpsSyncWithCredentials = TAzureDevOpsSync & {
|
||||||
|
connection: TAzureDevOpsConnection;
|
||||||
|
};
|
4
backend/src/services/secret-sync/azure-devops/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./azure-devops-sync-constants";
|
||||||
|
export * from "./azure-devops-sync-fns";
|
||||||
|
export * from "./azure-devops-sync-schemas";
|
||||||
|
export * from "./azure-devops-sync-types";
|
@@ -5,6 +5,7 @@ export enum SecretSync {
|
|||||||
GCPSecretManager = "gcp-secret-manager",
|
GCPSecretManager = "gcp-secret-manager",
|
||||||
AzureKeyVault = "azure-key-vault",
|
AzureKeyVault = "azure-key-vault",
|
||||||
AzureAppConfiguration = "azure-app-configuration",
|
AzureAppConfiguration = "azure-app-configuration",
|
||||||
|
AzureDevOps = "azure-devops",
|
||||||
Databricks = "databricks",
|
Databricks = "databricks",
|
||||||
Humanitec = "humanitec",
|
Humanitec = "humanitec",
|
||||||
TerraformCloud = "terraform-cloud",
|
TerraformCloud = "terraform-cloud",
|
||||||
|
@@ -26,6 +26,7 @@ import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
|||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { ONEPASS_SYNC_LIST_OPTION, OnePassSyncFns } from "./1password";
|
import { ONEPASS_SYNC_LIST_OPTION, OnePassSyncFns } from "./1password";
|
||||||
import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration";
|
import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration";
|
||||||
|
import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops";
|
||||||
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
|
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
|
||||||
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
||||||
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||||
@@ -45,6 +46,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
|||||||
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION,
|
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION,
|
||||||
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION,
|
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION,
|
||||||
[SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION,
|
[SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION,
|
||||||
|
[SecretSync.AzureDevOps]: AZURE_DEVOPS_SYNC_LIST_OPTION,
|
||||||
[SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION,
|
[SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION,
|
[SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
|
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
|
||||||
@@ -182,6 +184,11 @@ export const SecretSyncFns = {
|
|||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService
|
||||||
}).syncSecrets(secretSync, schemaSecretMap);
|
}).syncSecrets(secretSync, schemaSecretMap);
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
return azureDevOpsSyncFactory({
|
||||||
|
appConnectionDAL,
|
||||||
|
kmsService
|
||||||
|
}).syncSecrets(secretSync, schemaSecretMap);
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
return databricksSyncFactory({
|
return databricksSyncFactory({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
@@ -244,6 +251,12 @@ export const SecretSyncFns = {
|
|||||||
kmsService
|
kmsService
|
||||||
}).getSecrets(secretSync);
|
}).getSecrets(secretSync);
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
secretMap = await azureDevOpsSyncFactory({
|
||||||
|
appConnectionDAL,
|
||||||
|
kmsService
|
||||||
|
}).getSecrets(secretSync);
|
||||||
|
break;
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
return databricksSyncFactory({
|
return databricksSyncFactory({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
@@ -315,6 +328,11 @@ export const SecretSyncFns = {
|
|||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService
|
||||||
}).removeSecrets(secretSync, schemaSecretMap);
|
}).removeSecrets(secretSync, schemaSecretMap);
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
return azureDevOpsSyncFactory({
|
||||||
|
appConnectionDAL,
|
||||||
|
kmsService
|
||||||
|
}).removeSecrets(secretSync);
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
return databricksSyncFactory({
|
return databricksSyncFactory({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
|
@@ -8,6 +8,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
|||||||
[SecretSync.GCPSecretManager]: "GCP Secret Manager",
|
[SecretSync.GCPSecretManager]: "GCP Secret Manager",
|
||||||
[SecretSync.AzureKeyVault]: "Azure Key Vault",
|
[SecretSync.AzureKeyVault]: "Azure Key Vault",
|
||||||
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
|
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
|
||||||
|
[SecretSync.AzureDevOps]: "Azure DevOps",
|
||||||
[SecretSync.Databricks]: "Databricks",
|
[SecretSync.Databricks]: "Databricks",
|
||||||
[SecretSync.Humanitec]: "Humanitec",
|
[SecretSync.Humanitec]: "Humanitec",
|
||||||
[SecretSync.TerraformCloud]: "Terraform Cloud",
|
[SecretSync.TerraformCloud]: "Terraform Cloud",
|
||||||
@@ -27,6 +28,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
|||||||
[SecretSync.GCPSecretManager]: AppConnection.GCP,
|
[SecretSync.GCPSecretManager]: AppConnection.GCP,
|
||||||
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||||
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
||||||
|
[SecretSync.AzureDevOps]: AppConnection.AzureDevOps,
|
||||||
[SecretSync.Databricks]: AppConnection.Databricks,
|
[SecretSync.Databricks]: AppConnection.Databricks,
|
||||||
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
||||||
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
||||||
@@ -46,6 +48,7 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
|||||||
[SecretSync.GCPSecretManager]: SecretSyncPlanType.Regular,
|
[SecretSync.GCPSecretManager]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.AzureKeyVault]: SecretSyncPlanType.Regular,
|
[SecretSync.AzureKeyVault]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.AzureAppConfiguration]: SecretSyncPlanType.Regular,
|
[SecretSync.AzureAppConfiguration]: SecretSyncPlanType.Regular,
|
||||||
|
[SecretSync.AzureDevOps]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.Databricks]: SecretSyncPlanType.Regular,
|
[SecretSync.Databricks]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.Humanitec]: SecretSyncPlanType.Regular,
|
[SecretSync.Humanitec]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.TerraformCloud]: SecretSyncPlanType.Regular,
|
[SecretSync.TerraformCloud]: SecretSyncPlanType.Regular,
|
||||||
|
@@ -60,6 +60,12 @@ import {
|
|||||||
TAzureAppConfigurationSyncListItem,
|
TAzureAppConfigurationSyncListItem,
|
||||||
TAzureAppConfigurationSyncWithCredentials
|
TAzureAppConfigurationSyncWithCredentials
|
||||||
} from "./azure-app-configuration";
|
} from "./azure-app-configuration";
|
||||||
|
import {
|
||||||
|
TAzureDevOpsSync,
|
||||||
|
TAzureDevOpsSyncInput,
|
||||||
|
TAzureDevOpsSyncListItem,
|
||||||
|
TAzureDevOpsSyncWithCredentials
|
||||||
|
} from "./azure-devops";
|
||||||
import {
|
import {
|
||||||
TAzureKeyVaultSync,
|
TAzureKeyVaultSync,
|
||||||
TAzureKeyVaultSyncInput,
|
TAzureKeyVaultSyncInput,
|
||||||
@@ -100,6 +106,7 @@ export type TSecretSync =
|
|||||||
| TGcpSync
|
| TGcpSync
|
||||||
| TAzureKeyVaultSync
|
| TAzureKeyVaultSync
|
||||||
| TAzureAppConfigurationSync
|
| TAzureAppConfigurationSync
|
||||||
|
| TAzureDevOpsSync
|
||||||
| TDatabricksSync
|
| TDatabricksSync
|
||||||
| THumanitecSync
|
| THumanitecSync
|
||||||
| TTerraformCloudSync
|
| TTerraformCloudSync
|
||||||
@@ -118,6 +125,7 @@ export type TSecretSyncWithCredentials =
|
|||||||
| TGcpSyncWithCredentials
|
| TGcpSyncWithCredentials
|
||||||
| TAzureKeyVaultSyncWithCredentials
|
| TAzureKeyVaultSyncWithCredentials
|
||||||
| TAzureAppConfigurationSyncWithCredentials
|
| TAzureAppConfigurationSyncWithCredentials
|
||||||
|
| TAzureDevOpsSyncWithCredentials
|
||||||
| TDatabricksSyncWithCredentials
|
| TDatabricksSyncWithCredentials
|
||||||
| THumanitecSyncWithCredentials
|
| THumanitecSyncWithCredentials
|
||||||
| TTerraformCloudSyncWithCredentials
|
| TTerraformCloudSyncWithCredentials
|
||||||
@@ -136,6 +144,7 @@ export type TSecretSyncInput =
|
|||||||
| TGcpSyncInput
|
| TGcpSyncInput
|
||||||
| TAzureKeyVaultSyncInput
|
| TAzureKeyVaultSyncInput
|
||||||
| TAzureAppConfigurationSyncInput
|
| TAzureAppConfigurationSyncInput
|
||||||
|
| TAzureDevOpsSyncInput
|
||||||
| TDatabricksSyncInput
|
| TDatabricksSyncInput
|
||||||
| THumanitecSyncInput
|
| THumanitecSyncInput
|
||||||
| TTerraformCloudSyncInput
|
| TTerraformCloudSyncInput
|
||||||
@@ -154,6 +163,7 @@ export type TSecretSyncListItem =
|
|||||||
| TGcpSyncListItem
|
| TGcpSyncListItem
|
||||||
| TAzureKeyVaultSyncListItem
|
| TAzureKeyVaultSyncListItem
|
||||||
| TAzureAppConfigurationSyncListItem
|
| TAzureAppConfigurationSyncListItem
|
||||||
|
| TAzureDevOpsSyncListItem
|
||||||
| TDatabricksSyncListItem
|
| TDatabricksSyncListItem
|
||||||
| THumanitecSyncListItem
|
| THumanitecSyncListItem
|
||||||
| TTerraformCloudSyncListItem
|
| TTerraformCloudSyncListItem
|
||||||
|
@@ -14,7 +14,7 @@ require (
|
|||||||
github.com/fatih/semgroup v1.2.0
|
github.com/fatih/semgroup v1.2.0
|
||||||
github.com/gitleaks/go-gitdiff v0.9.1
|
github.com/gitleaks/go-gitdiff v0.9.1
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
github.com/infisical/go-sdk v0.5.95
|
github.com/infisical/go-sdk v0.5.96
|
||||||
github.com/infisical/infisical-kmip v0.3.5
|
github.com/infisical/infisical-kmip v0.3.5
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
||||||
@@ -25,6 +25,7 @@ require (
|
|||||||
github.com/pion/logging v0.2.3
|
github.com/pion/logging v0.2.3
|
||||||
github.com/pion/turn/v4 v4.0.0
|
github.com/pion/turn/v4 v4.0.0
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
|
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
|
||||||
github.com/quic-go/quic-go v0.50.0
|
github.com/quic-go/quic-go v0.50.0
|
||||||
github.com/rs/cors v1.11.0
|
github.com/rs/cors v1.11.0
|
||||||
@@ -106,7 +107,6 @@ require (
|
|||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
@@ -292,12 +292,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
|||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/infisical/go-sdk v0.5.92 h1:PoCnVndrd6Dbkipuxl9fFiwlD5vCKsabtQo09mo8lUE=
|
github.com/infisical/go-sdk v0.5.96 h1:huky6bQ1Y3oRdPb5MO3Ru868qZaPHUxZ7kP7FPNRn48=
|
||||||
github.com/infisical/go-sdk v0.5.92/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
github.com/infisical/go-sdk v0.5.96/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
||||||
github.com/infisical/go-sdk v0.5.94 h1:wKBj+KpJEe+ZzOJ7koXQZDR0dLL9bt0Kqgf/1q+7tG4=
|
|
||||||
github.com/infisical/go-sdk v0.5.94/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
|
||||||
github.com/infisical/go-sdk v0.5.95 h1:so0YwPofbT7j6Ao8Xcxee/o3ia33meuEVDU2vWr9yfs=
|
|
||||||
github.com/infisical/go-sdk v0.5.95/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
|
||||||
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
|
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
|
||||||
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
|
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
|
||||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||||
|
@@ -232,13 +232,26 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for Kubernetes dynamic secrets only
|
||||||
|
kubernetesNamespace, err := cmd.Flags().GetString("kubernetesNamespace")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := map[string]any{}
|
||||||
|
if kubernetesNamespace != "" {
|
||||||
|
config["namespace"] = kubernetesNamespace
|
||||||
|
}
|
||||||
|
|
||||||
leaseCredentials, _, leaseDetails, err := infisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{
|
leaseCredentials, _, leaseDetails, err := infisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{
|
||||||
DynamicSecretName: dynamicSecretRootCredential.Name,
|
DynamicSecretName: dynamicSecretRootCredential.Name,
|
||||||
ProjectSlug: projectDetails.Slug,
|
ProjectSlug: projectDetails.Slug,
|
||||||
TTL: ttl,
|
TTL: ttl,
|
||||||
SecretPath: secretsPath,
|
SecretPath: secretsPath,
|
||||||
EnvironmentSlug: environmentName,
|
EnvironmentSlug: environmentName,
|
||||||
|
Config: config,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "To lease dynamic secret")
|
util.HandleError(err, "To lease dynamic secret")
|
||||||
}
|
}
|
||||||
@@ -585,6 +598,10 @@ func init() {
|
|||||||
dynamicSecretLeaseCreateCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
dynamicSecretLeaseCreateCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||||
dynamicSecretLeaseCreateCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
|
dynamicSecretLeaseCreateCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
|
||||||
dynamicSecretLeaseCreateCmd.Flags().Bool("plain", false, "Print leased credentials without formatting, one per line")
|
dynamicSecretLeaseCreateCmd.Flags().Bool("plain", false, "Print leased credentials without formatting, one per line")
|
||||||
|
|
||||||
|
// Kubernetes specific flags
|
||||||
|
dynamicSecretLeaseCreateCmd.Flags().String("kubernetesNamespace", "", "The namespace to create the lease in. Only used for Kubernetes dynamic secrets.")
|
||||||
|
|
||||||
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseCreateCmd)
|
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseCreateCmd)
|
||||||
|
|
||||||
dynamicSecretLeaseListCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
dynamicSecretLeaseListCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||||
|
@@ -108,17 +108,17 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
|
|||||||
return
|
return
|
||||||
|
|
||||||
case "FORWARD-HTTP":
|
case "FORWARD-HTTP":
|
||||||
|
targetURL := ""
|
||||||
argParts := bytes.Split(args, []byte(" "))
|
argParts := bytes.Split(args, []byte(" "))
|
||||||
if len(argParts) == 0 {
|
|
||||||
log.Error().Msg("FORWARD-HTTP requires target URL")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
targetURL := string(argParts[0])
|
if len(argParts) == 0 || len(argParts[0]) == 0 {
|
||||||
|
log.Warn().Msg("FORWARD-HTTP used without a target URL.")
|
||||||
if !isValidURL(targetURL) {
|
} else {
|
||||||
log.Error().Msgf("Invalid target URL: %s", targetURL)
|
targetURL = string(argParts[0])
|
||||||
return
|
if !isValidURL(targetURL) {
|
||||||
|
log.Error().Msgf("Invalid target URL: %s", targetURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse optional parameters
|
// Parse optional parameters
|
||||||
@@ -183,11 +183,6 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string,
|
|||||||
transport.TLSClientConfig = tlsConfig
|
transport.TLSClientConfig = tlsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop to handle multiple HTTP requests on the same stream
|
// Loop to handle multiple HTTP requests on the same stream
|
||||||
for {
|
for {
|
||||||
req, err := http.ReadRequest(reader)
|
req, err := http.ReadRequest(reader)
|
||||||
@@ -201,18 +196,53 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string,
|
|||||||
}
|
}
|
||||||
log.Info().Msgf("Received HTTP request: %s", req.URL.Path)
|
log.Info().Msgf("Received HTTP request: %s", req.URL.Path)
|
||||||
|
|
||||||
actionHeader := req.Header.Get("x-infisical-action")
|
actionHeader := HttpProxyAction(req.Header.Get(INFISICAL_HTTP_PROXY_ACTION_HEADER))
|
||||||
if actionHeader != "" {
|
if actionHeader != "" {
|
||||||
if actionHeader == "inject-k8s-sa-auth-token" {
|
if actionHeader == HttpProxyActionInjectGatewayK8sServiceAccountToken {
|
||||||
token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
token, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
|
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
|
||||||
continue // Continue to next request instead of returning
|
continue // Continue to next request instead of returning
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
|
||||||
log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL)
|
log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL)
|
||||||
|
} else if actionHeader == HttpProxyActionUseGatewayK8sServiceAccount { // will work without a target URL set
|
||||||
|
// set the ca cert to the pod's k8s service account ca cert:
|
||||||
|
caCert, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH)
|
||||||
|
if err != nil {
|
||||||
|
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa ca cert")))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
|
||||||
|
stream.Write([]byte(buildHttpInternalServerError("failed to parse k8s sa ca cert")))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
transport.TLSClientConfig = &tls.Config{
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// set authorization header to the pod's k8s service account token:
|
||||||
|
token, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH)
|
||||||
|
if err != nil {
|
||||||
|
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
|
||||||
|
|
||||||
|
// update the target URL to point to the kubernetes API server:
|
||||||
|
kubernetesServiceHost := os.Getenv(KUBERNETES_SERVICE_HOST_ENV_NAME)
|
||||||
|
kubernetesServicePort := os.Getenv(KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME)
|
||||||
|
|
||||||
|
fullBaseUrl := fmt.Sprintf("https://%s:%s", kubernetesServiceHost, kubernetesServicePort)
|
||||||
|
targetURL = fullBaseUrl
|
||||||
|
|
||||||
|
log.Info().Msgf("Redirected request to Kubernetes API server: %s", targetURL)
|
||||||
}
|
}
|
||||||
req.Header.Del("x-infisical-action")
|
|
||||||
|
req.Header.Del(INFISICAL_HTTP_PROXY_ACTION_HEADER)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build full target URL
|
// Build full target URL
|
||||||
@@ -242,6 +272,11 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string,
|
|||||||
|
|
||||||
log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL)
|
log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL)
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.Do(proxyReq)
|
resp, err := client.Do(proxyReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msgf("Failed to reach target: %v", err)
|
log.Error().Msgf("Failed to reach target: %v", err)
|
||||||
|
17
cli/packages/gateway/constants.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
const (
|
||||||
|
KUBERNETES_SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST"
|
||||||
|
KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME = "KUBERNETES_SERVICE_PORT_HTTPS"
|
||||||
|
KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||||
|
KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||||
|
|
||||||
|
INFISICAL_HTTP_PROXY_ACTION_HEADER = "x-infisical-action"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpProxyAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
HttpProxyActionInjectGatewayK8sServiceAccountToken HttpProxyAction = "inject-k8s-sa-auth-token"
|
||||||
|
HttpProxyActionUseGatewayK8sServiceAccount HttpProxyAction = "use-k8s-sa"
|
||||||
|
)
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Available"
|
||||||
|
openapi: "GET /api/v1/app-connections/azure-devops/available"
|
||||||
|
---
|
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/app-connections/azure-devops"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Azure DevOps Connections must be created through the Infisical UI if you are using OAuth.
|
||||||
|
Check out the configuration docs for [Azure DevOps Connections](/integrations/app-connections/azure-devops) for a step-by-step
|
||||||
|
guide.
|
||||||
|
</Note>
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/app-connections/azure-devops/{connectionId}"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/app-connections/azure-devops/{connectionId}"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v1/app-connections/azure-devops/connection-name/{connectionName}"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/app-connections/azure-devops"
|
||||||
|
---
|
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/app-connections/azure-devops/{connectionId}"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Azure DevOps Connections must be updated through the Infisical UI if you are using OAuth.
|
||||||
|
Check out the configuration docs for [Azure DevOps Connections](/integrations/app-connections/azure-devops) for a step-by-step
|
||||||
|
guide.
|
||||||
|
</Note>
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create Kubernetes Lease"
|
||||||
|
openapi: "POST /api/v1/dynamic-secrets/leases/kubernetes"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/azure-devops"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/secret-syncs/azure-devops/{syncId}"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/azure-devops/{syncId}"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/azure-devops/sync-name/{syncName}"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Import Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/azure-devops/{syncId}/import-secrets"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/azure-devops"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Remove Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/azure-devops/{syncId}/remove-secrets"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Sync Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/azure-devops/{syncId}/sync-secrets"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/secret-syncs/azure-devops/{syncId}"
|
||||||
|
---
|
@@ -148,6 +148,22 @@ infisical dynamic-secrets lease create <dynamic-secret-name> --ttl=<ttl>
|
|||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
### Provider-specific flags
|
||||||
|
|
||||||
|
The following flags are specific to certain providers or integrations:
|
||||||
|
|
||||||
|
<Accordion title="Kubernetes">
|
||||||
|
<Accordion title="--kubernetesNamespace">
|
||||||
|
The namespace to create the lease in. Only used for Kubernetes dynamic secrets.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease create <dynamic-secret-name> --kubernetesNamespace=<namespace>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="infisical dynamic-secrets lease list">
|
<Accordion title="infisical dynamic-secrets lease list">
|
||||||
This command is used to list leases for a dynamic secret.
|
This command is used to list leases for a dynamic secret.
|
||||||
|
@@ -120,6 +120,12 @@ The CLI is designed for a variety of secret management applications ranging from
|
|||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Starting with CLI version v0.4.0, you can now choose to log in via Infisical Cloud (US/EU) or your own self-hosted instance by simply running `infisical login` and following the on-screen instructions — no need to manually set the `INFISICAL_API_URL` environment variable.
|
||||||
|
|
||||||
|
For versions prior to v0.4.0, the CLI defaults to the US Cloud. To connect to the EU Cloud or a self-hosted instance, set the `INFISICAL_API_URL` environment variable to `https://eu.infisical.com` or your custom URL.
|
||||||
|
</Note>
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
## Custom Request Headers
|
## Custom Request Headers
|
||||||
|
|
||||||
|
@@ -32,7 +32,8 @@ Infisical needs an initial AWS IAM user with the required permissions to create
|
|||||||
"iam:ListUserPolicies",
|
"iam:ListUserPolicies",
|
||||||
"iam:PutUserPolicy",
|
"iam:PutUserPolicy",
|
||||||
"iam:AddUserToGroup",
|
"iam:AddUserToGroup",
|
||||||
"iam:RemoveUserFromGroup"
|
"iam:RemoveUserFromGroup",
|
||||||
|
"iam:TagUser"
|
||||||
],
|
],
|
||||||
"Resource": ["*"]
|
"Resource": ["*"]
|
||||||
}
|
}
|
||||||
|
@@ -162,6 +162,12 @@ This feature is ideal for scenarios where you need to:
|
|||||||
tokens for the target service account.
|
tokens for the target service account.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
When using Gateway authentication, the Gateway will access the Kubernetes API server
|
||||||
|
using its internal cluster URL (typically https://kubernetes.default.svc) and TLS configuration.
|
||||||
|
You don't need to specify these values separately in the dynamic secret configuration.
|
||||||
|
</Note>
|
||||||
|
|
||||||
1. Deploy the Infisical Gateway in your cluster
|
1. Deploy the Infisical Gateway in your cluster
|
||||||
2. Set up RBAC permissions for the Gateway's service account:
|
2. Set up RBAC permissions for the Gateway's service account:
|
||||||
```yaml rbac.yaml
|
```yaml rbac.yaml
|
||||||
@@ -206,6 +212,7 @@ This feature is ideal for scenarios where you need to:
|
|||||||
- Automatically clean up service accounts after token expiration
|
- Automatically clean up service accounts after token expiration
|
||||||
- Assign different roles to different users or applications
|
- Assign different roles to different users or applications
|
||||||
- Maintain strict control over service account permissions
|
- Maintain strict control over service account permissions
|
||||||
|
- Support multiple namespaces with a single dynamic secret configuration
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
@@ -213,6 +220,16 @@ This feature is ideal for scenarios where you need to:
|
|||||||
- Cluster access token with permissions to create service accounts and manage RBAC
|
- Cluster access token with permissions to create service accounts and manage RBAC
|
||||||
- (Optional) [Gateway](/documentation/platform/gateways/overview) for private cluster access
|
- (Optional) [Gateway](/documentation/platform/gateways/overview) for private cluster access
|
||||||
|
|
||||||
|
### Namespace Support
|
||||||
|
|
||||||
|
When configuring a dynamic secret, you can specify multiple allowed namespaces as a comma-separated list. During lease creation, you can then specify which namespace to use from this allowed list. This provides flexibility while maintaining security by:
|
||||||
|
|
||||||
|
- Allowing a single dynamic secret configuration to support multiple namespaces
|
||||||
|
- Restricting service account creation to only the specified allowed namespaces
|
||||||
|
- Enabling fine-grained control over which namespaces can be used for each lease
|
||||||
|
|
||||||
|
For example, if you configure a dynamic secret with allowed namespaces "default,kube-system,monitoring", you can create leases that use any of these namespaces while preventing access to other namespaces in your cluster.
|
||||||
|
|
||||||
### Authentication Setup
|
### Authentication Setup
|
||||||
|
|
||||||
Choose your authentication method:
|
Choose your authentication method:
|
||||||
@@ -318,6 +335,12 @@ This feature is ideal for scenarios where you need to:
|
|||||||
manage service accounts, their tokens, and RBAC resources.
|
manage service accounts, their tokens, and RBAC resources.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
When using Gateway authentication, the Gateway will access the Kubernetes API server
|
||||||
|
using its internal cluster URL (typically https://kubernetes.default.svc) and TLS configuration.
|
||||||
|
You don't need to specify these values separately in the dynamic secret configuration.
|
||||||
|
</Note>
|
||||||
|
|
||||||
1. Deploy the Infisical Gateway in your cluster
|
1. Deploy the Infisical Gateway in your cluster
|
||||||
2. Set up RBAC permissions for the Gateway's service account:
|
2. Set up RBAC permissions for the Gateway's service account:
|
||||||
```yaml rbac.yaml
|
```yaml rbac.yaml
|
||||||
@@ -401,13 +424,13 @@ This feature is ideal for scenarios where you need to:
|
|||||||
Select a gateway for private cluster access. If not specified, the Internet Gateway will be used.
|
Select a gateway for private cluster access. If not specified, the Internet Gateway will be used.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
<ParamField path="Cluster URL" type="string" required>
|
<ParamField path="Cluster URL" type="string" required>
|
||||||
Kubernetes API server URL (e.g., https://kubernetes.default.svc)
|
Kubernetes API server URL (e.g., https://kubernetes.default.svc). Not required when using Gateway authentication as the Gateway will use its internal cluster URL.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
<ParamField path="Enable SSL" type="boolean">
|
<ParamField path="Enable SSL" type="boolean">
|
||||||
Whether to enable SSL verification for the Kubernetes API server connection.
|
Whether to enable SSL verification for the Kubernetes API server connection. Not required when using Gateway authentication as the Gateway will use its internal TLS configuration.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
<ParamField path="CA" type="string">
|
<ParamField path="CA" type="string">
|
||||||
Custom CA certificate for the Kubernetes API server. Leave blank to use the system/public CA.
|
Custom CA certificate for the Kubernetes API server. Leave blank to use the system/public CA. Not required when using Gateway authentication as the Gateway will use its internal TLS configuration.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
<ParamField path="Auth Method" type="string" required>
|
<ParamField path="Auth Method" type="string" required>
|
||||||
Choose between Token (API) or Gateway authentication. If using Gateway, the Gateway must be deployed in your Kubernetes cluster.
|
Choose between Token (API) or Gateway authentication. If using Gateway, the Gateway must be deployed in your Kubernetes cluster.
|
||||||
@@ -418,18 +441,30 @@ This feature is ideal for scenarios where you need to:
|
|||||||
<ParamField path="Credential Type" type="string" required>
|
<ParamField path="Credential Type" type="string" required>
|
||||||
Choose between Static (predefined service account) or Dynamic (temporary service accounts with role assignments)
|
Choose between Static (predefined service account) or Dynamic (temporary service accounts with role assignments)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
<ParamField path="Service Account Name" type="string" required>
|
|
||||||
Name of the service account to generate tokens for (required for Static credentials)
|
<Tabs>
|
||||||
</ParamField>
|
<Tab title="Static Credentials Parameters">
|
||||||
<ParamField path="Namespace" type="string" required>
|
<ParamField path="Service Account Name" type="string" required>
|
||||||
Kubernetes namespace where the service account exists or will be created
|
Name of the service account to generate tokens for
|
||||||
</ParamField>
|
</ParamField>
|
||||||
<ParamField path="Role Type" type="string" required>
|
<ParamField path="Namespace" type="string" required>
|
||||||
Type of role to assign (ClusterRole or Role) (required for Dynamic credentials)
|
Kubernetes namespace where the service account exists
|
||||||
</ParamField>
|
</ParamField>
|
||||||
<ParamField path="Role" type="string" required>
|
</Tab>
|
||||||
Name of the role to assign to the temporary service account (required for Dynamic credentials)
|
|
||||||
</ParamField>
|
<Tab title="Dynamic Credentials Parameters">
|
||||||
|
<ParamField path="Allowed Namespaces" type="string" required>
|
||||||
|
Kubernetes namespace(s) where the service accounts will be created. You can specify multiple namespaces as a comma-separated list (e.g., "default,kube-system"). During lease creation, you can specify which namespace to use from this allowed list.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="Role Type" type="string" required>
|
||||||
|
Type of role to assign (ClusterRole or Role)
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="Role" type="string" required>
|
||||||
|
Name of the role to assign to the temporary service account
|
||||||
|
</ParamField>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
<ParamField path="Audiences" type="array">
|
<ParamField path="Audiences" type="array">
|
||||||
Optional list of audiences to include in the generated token
|
Optional list of audiences to include in the generated token
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
BIN
docs/images/app-connections/azure/devops/devops-connection.png
Normal file
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 2.1 MiB |
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/images/app-connections/azure/devops/select-connection.png
Normal file
After Width: | Height: | Size: 2.3 MiB |
BIN
docs/images/integrations/azure-devops/app-api-permissions.png
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/images/secret-syncs/azure-devops/devops-destination.png
Normal file
After Width: | Height: | Size: 202 KiB |
BIN
docs/images/secret-syncs/azure-devops/devops-details.png
Normal file
After Width: | Height: | Size: 202 KiB |
BIN
docs/images/secret-syncs/azure-devops/devops-options.png
Normal file
After Width: | Height: | Size: 220 KiB |
BIN
docs/images/secret-syncs/azure-devops/devops-review.png
Normal file
After Width: | Height: | Size: 214 KiB |
BIN
docs/images/secret-syncs/azure-devops/devops-source.png
Normal file
After Width: | Height: | Size: 195 KiB |
BIN
docs/images/secret-syncs/azure-devops/devops-synced.png
Normal file
After Width: | Height: | Size: 294 KiB |
After Width: | Height: | Size: 235 KiB |
137
docs/integrations/app-connections/azure-devops.mdx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|
<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
|
||||||
|
and registering your instance with it.
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
|
||||||
|
- Set up Azure.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create an application in Azure">
|
||||||
|
Navigate to Azure Active Directory > App registrations to create a new application.
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Azure Active Directory is now Microsoft Entra ID.
|
||||||
|
</Info>
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/azure/oauth/callback`.
|
||||||
|
<Tip>
|
||||||
|
The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Assign API permissions to the application">
|
||||||
|
|
||||||
|
For the Azure Connection to work with DevOps Pipelines, you need to assign the following permission to the application.
|
||||||
|
|
||||||
|
#### Azure DevOps permissions
|
||||||
|
|
||||||
|
Set the API permissions of the Azure application to include the following permissions:
|
||||||
|
- Azure DevOps
|
||||||
|
- `user_impersonation`
|
||||||
|
- `vso.project_write`
|
||||||
|
- `vso.variablegroups_manage`
|
||||||
|
- `vso.variablegroups_write`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Add your application credentials to Infisical">
|
||||||
|
Obtain the **Application (Client) ID** and **Directory (Tenant) ID** (this will be used later in the Infisical connection) in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Back in your Infisical instance, add two new environment variables for the credentials of your Azure application.
|
||||||
|
|
||||||
|
- `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application.
|
||||||
|
- `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application.
|
||||||
|
|
||||||
|
Once added, restart your Infisical instance and use the Azure Client Secrets connection.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="Azure DevOps personal access token (PAT)">
|
||||||
|
#### Create a new Azure DevOps personal access token (PAT)
|
||||||
|
When using the Azure DevOps <b>Access Token connection</b> you'll need to create a new personal access token (PAT) in order to authenticate Infisical with Azure DevOps.
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to Azure DevOps">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Create a new token">
|
||||||
|
Make sure the newly created token has Read/Write access to the Release scope.
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Please make sure that the token has access to the following scopes: Variable Groups _(read, create, & manage)_, Release _(read/write)_, Project and Team _(read)_, Service Connections _(read & query)_
|
||||||
|
</Note>
|
||||||
|
</Step>
|
||||||
|
<Step title="Copy the new access token">
|
||||||
|
Copy the newly created token as this will be used to authenticate Infisical with Azure DevOps.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
## Setup Azure Connection in Infisical
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to App Connections">
|
||||||
|
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="Create Connection">
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="OAuth">
|
||||||
|
<Steps>
|
||||||
|
<Step title="Fill in Connection Details">
|
||||||
|
Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous [step](#azure-oauth-on-a-self-hosted-instance). Also fill in the organization name of the Azure DevOps organization you want to connect to.
|
||||||
|

|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
You can find the **Organization Name** on https://dev.azure.com/
|
||||||
|
</Tip>
|
||||||
|
</Step>
|
||||||
|
<Step title="Grant Access">
|
||||||
|
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
|
||||||
|
you will be redirected back to Infisical's App Connections page. 
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Access Token">
|
||||||
|
<Steps>
|
||||||
|
<Step title="Fill in Connection Details">
|
||||||
|
Fill in the **Access Token** field with the Access Token you obtained in the previous step. And the organization name of the Azure DevOps organization you want to connect to.
|
||||||
|

|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
You can find the **Organization Name** on https://dev.azure.com/
|
||||||
|
</Tip>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Step>
|
||||||
|
<Step title="Connection Created">
|
||||||
|
Your **Azure DevOps Connection** is now available for use. 
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
143
docs/integrations/secret-syncs/azure-devops.mdx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
title: "Azure DevOps Sync"
|
||||||
|
description: "Learn how to configure a Azure DevOps Sync for Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
|
||||||
|
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
- Create an [Azure DevOps Connection](/integrations/app-connections/azure-devops)
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Infisical UI">
|
||||||
|
1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
|
||||||
|

|
||||||
|
|
||||||
|
2. Select the **Azure DevOps** option.
|
||||||
|

|
||||||
|
|
||||||
|
3. Configure the **Source** from where secrets should be retrieved, then click **Next**.
|
||||||
|

|
||||||
|
|
||||||
|
- **Environment**: The project environment to retrieve secrets from.
|
||||||
|
- **Secret Path**: The folder path to retrieve secrets from.
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
4. Configure the **Destination** to where secrets should be deployed, then click **Next**.
|
||||||
|

|
||||||
|
|
||||||
|
- **Azure DevOps Connection**: The Azure DevOps Connection to authenticate with.
|
||||||
|
- **Project**: The Azure DevOps project to deploy secrets to.
|
||||||
|
<p class="height:1px"/>
|
||||||
|
|
||||||
|
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||||
|

|
||||||
|
|
||||||
|
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
|
||||||
|
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||||
|
<Note>
|
||||||
|
Azure Devops does not support importing secrets.
|
||||||
|
</Note>
|
||||||
|
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
|
||||||
|
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||||
|
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||||
|
|
||||||
|
6. Configure the **Details** of your Azure DevOps Sync, then click **Next**.
|
||||||
|

|
||||||
|
|
||||||
|
- **Name**: The name of your sync. Must be slug-friendly.
|
||||||
|
- **Description**: An optional description for your sync.
|
||||||
|
|
||||||
|
7. Review your Azure DevOps Sync configuration, then click **Create Sync**.
|
||||||
|

|
||||||
|
|
||||||
|
8. If enabled, your Azure DevOps Sync will begin syncing your secrets to the destination endpoint.
|
||||||
|

|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab title="API">
|
||||||
|
To create a **Azure DevOps Sync**, make an API request to the [Create Azure DevOps Sync](/api-reference/endpoints/secret-syncs/azure-devops/create) API endpoint.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --request POST \
|
||||||
|
--url https://app.infisical.com/api/v1/secret-syncs/azure-devops \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"name": "my-devops-sync",
|
||||||
|
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||||
|
"description": "an example sync",
|
||||||
|
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||||
|
"environment": "dev",
|
||||||
|
"secretPath": "/my-secrets",
|
||||||
|
"isEnabled": true,
|
||||||
|
"syncOptions": {
|
||||||
|
"initialSyncBehavior": "overwrite-destination",
|
||||||
|
"disableSecretDeletion": true
|
||||||
|
},
|
||||||
|
"destinationConfig": {
|
||||||
|
"devopsProjectId": "12345678-90ab-cdef-1234-567890abcdef",
|
||||||
|
"devopsProjectName": "example-project"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```json Response
|
||||||
|
{
|
||||||
|
"secretSync": {
|
||||||
|
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||||
|
"name": "my-devops-sync",
|
||||||
|
"description": "an example sync",
|
||||||
|
"isEnabled": true,
|
||||||
|
"version": 1,
|
||||||
|
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||||
|
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||||
|
"createdAt": "2023-11-07T05:31:56Z",
|
||||||
|
"updatedAt": "2023-11-07T05:31:56Z",
|
||||||
|
"syncStatus": "succeeded",
|
||||||
|
"lastSyncJobId": "123",
|
||||||
|
"lastSyncMessage": null,
|
||||||
|
"lastSyncedAt": "2023-11-07T05:31:56Z",
|
||||||
|
"importStatus": null,
|
||||||
|
"lastImportJobId": null,
|
||||||
|
"lastImportMessage": null,
|
||||||
|
"lastImportedAt": null,
|
||||||
|
"removeStatus": null,
|
||||||
|
"lastRemoveJobId": null,
|
||||||
|
"lastRemoveMessage": null,
|
||||||
|
"lastRemovedAt": null,
|
||||||
|
"syncOptions": {
|
||||||
|
"initialSyncBehavior": "overwrite-destination",
|
||||||
|
"keySchema": "PIPELINE_${secretKey}",
|
||||||
|
"disableSecretDeletion": true
|
||||||
|
},
|
||||||
|
"connection": {
|
||||||
|
"app": "azure-devops",
|
||||||
|
"name": "Production DevOps Organization",
|
||||||
|
"id": "8b92f5cc-3g77-5e80-6666-6ff57069385d"
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"slug": "production",
|
||||||
|
"name": "Production Environment",
|
||||||
|
"id": "4f16j9gg-7k11-9i23-2222-2jj91403729h"
|
||||||
|
},
|
||||||
|
"folder": {
|
||||||
|
"id": "5a71e8dd-2f66-4d70-7777-7cc46958274c",
|
||||||
|
"path": "/devops/pipeline-secrets"
|
||||||
|
},
|
||||||
|
"destination": "azure-devops",
|
||||||
|
"destinationConfig": {
|
||||||
|
"devopsProjectId": "12345678-90ab-cdef-1234-567890abcdef",
|
||||||
|
"devopsProjectName": "example-project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
@@ -495,6 +495,7 @@
|
|||||||
"integrations/app-connections/aws",
|
"integrations/app-connections/aws",
|
||||||
"integrations/app-connections/azure-app-configuration",
|
"integrations/app-connections/azure-app-configuration",
|
||||||
"integrations/app-connections/azure-client-secrets",
|
"integrations/app-connections/azure-client-secrets",
|
||||||
|
"integrations/app-connections/azure-devops",
|
||||||
"integrations/app-connections/azure-key-vault",
|
"integrations/app-connections/azure-key-vault",
|
||||||
"integrations/app-connections/camunda",
|
"integrations/app-connections/camunda",
|
||||||
"integrations/app-connections/databricks",
|
"integrations/app-connections/databricks",
|
||||||
@@ -527,6 +528,7 @@
|
|||||||
"integrations/secret-syncs/aws-parameter-store",
|
"integrations/secret-syncs/aws-parameter-store",
|
||||||
"integrations/secret-syncs/aws-secrets-manager",
|
"integrations/secret-syncs/aws-secrets-manager",
|
||||||
"integrations/secret-syncs/azure-app-configuration",
|
"integrations/secret-syncs/azure-app-configuration",
|
||||||
|
"integrations/secret-syncs/azure-devops",
|
||||||
"integrations/secret-syncs/azure-key-vault",
|
"integrations/secret-syncs/azure-key-vault",
|
||||||
"integrations/secret-syncs/camunda",
|
"integrations/secret-syncs/camunda",
|
||||||
"integrations/secret-syncs/databricks",
|
"integrations/secret-syncs/databricks",
|
||||||
@@ -926,6 +928,12 @@
|
|||||||
{
|
{
|
||||||
"group": "Dynamic Secrets",
|
"group": "Dynamic Secrets",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
{
|
||||||
|
"group": "Kubernetes",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"
|
||||||
|
]
|
||||||
|
},
|
||||||
"api-reference/endpoints/dynamic-secrets/create",
|
"api-reference/endpoints/dynamic-secrets/create",
|
||||||
"api-reference/endpoints/dynamic-secrets/update",
|
"api-reference/endpoints/dynamic-secrets/update",
|
||||||
"api-reference/endpoints/dynamic-secrets/delete",
|
"api-reference/endpoints/dynamic-secrets/delete",
|
||||||
@@ -1178,6 +1186,18 @@
|
|||||||
"api-reference/endpoints/app-connections/azure-client-secret/delete"
|
"api-reference/endpoints/app-connections/azure-client-secret/delete"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Azure DevOps",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/app-connections/azure-devops/list",
|
||||||
|
"api-reference/endpoints/app-connections/azure-devops/available",
|
||||||
|
"api-reference/endpoints/app-connections/azure-devops/get-by-id",
|
||||||
|
"api-reference/endpoints/app-connections/azure-devops/get-by-name",
|
||||||
|
"api-reference/endpoints/app-connections/azure-devops/create",
|
||||||
|
"api-reference/endpoints/app-connections/azure-devops/update",
|
||||||
|
"api-reference/endpoints/app-connections/azure-devops/delete"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Azure Key Vault",
|
"group": "Azure Key Vault",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -1445,6 +1465,20 @@
|
|||||||
"api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets"
|
"api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Azure DevOps",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/secret-syncs/azure-devops/list",
|
||||||
|
"api-reference/endpoints/secret-syncs/azure-devops/get-by-id",
|
||||||
|
"api-reference/endpoints/secret-syncs/azure-devops/get-by-name",
|
||||||
|
"api-reference/endpoints/secret-syncs/azure-devops/create",
|
||||||
|
"api-reference/endpoints/secret-syncs/azure-devops/update",
|
||||||
|
"api-reference/endpoints/secret-syncs/azure-devops/delete",
|
||||||
|
"api-reference/endpoints/secret-syncs/azure-devops/sync-secrets",
|
||||||
|
"api-reference/endpoints/secret-syncs/azure-devops/import-secrets",
|
||||||
|
"api-reference/endpoints/secret-syncs/azure-devops/remove-secrets"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Azure Key Vault",
|
"group": "Azure Key Vault",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
@@ -29,7 +29,7 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete
|
|||||||
infisical:
|
infisical:
|
||||||
image:
|
image:
|
||||||
repository: infisical/infisical
|
repository: infisical/infisical
|
||||||
tag: "v0.46.2-postgres" #<-- update
|
tag: "<>" #<-- select tag from Dockerhub from the above link
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
```
|
```
|
||||||
<Warning>
|
<Warning>
|
||||||
|
@@ -0,0 +1,76 @@
|
|||||||
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
import { SingleValue } from "react-select";
|
||||||
|
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||||
|
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
|
||||||
|
import { useGetAzureDevOpsProjects } from "@app/hooks/api/appConnections/azure";
|
||||||
|
import { AzureDevOpsProject } from "@app/hooks/api/appConnections/azure/types";
|
||||||
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
import { TSecretSyncForm } from "../schemas";
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncFields = () => {
|
||||||
|
const { control, setValue } = useFormContext<
|
||||||
|
TSecretSyncForm & { destination: SecretSync.AzureDevOps }
|
||||||
|
>();
|
||||||
|
|
||||||
|
const connectionId = useWatch({ name: "connection.id", control });
|
||||||
|
|
||||||
|
const { data: { projects } = { projects: [] }, isLoading: isProjectsLoading } =
|
||||||
|
useGetAzureDevOpsProjects(connectionId, {
|
||||||
|
enabled: Boolean(connectionId)
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SecretSyncConnectionField
|
||||||
|
onChange={() => {
|
||||||
|
setValue("destinationConfig.devopsProjectId", "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="destinationConfig.devopsProjectId"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Project"
|
||||||
|
helperText={
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-md"
|
||||||
|
content="Ensure the project exists in the connection's Azure DevOps instance URL."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span>Don't see the project you're looking for?</span>{" "}
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
menuPlacement="top"
|
||||||
|
isLoading={isProjectsLoading && Boolean(connectionId)}
|
||||||
|
isDisabled={!connectionId}
|
||||||
|
value={projects?.find((v) => v.appId === value) ?? null}
|
||||||
|
onChange={(option) => {
|
||||||
|
onChange((option as SingleValue<AzureDevOpsProject>)?.appId ?? null);
|
||||||
|
setValue(
|
||||||
|
"destinationConfig.devopsProjectName",
|
||||||
|
(option as SingleValue<AzureDevOpsProject>)?.name ?? ""
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={projects}
|
||||||
|
placeholder="Select a project..."
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@@ -7,6 +7,7 @@ import { OnePassSyncFields } from "./1PasswordSyncFields";
|
|||||||
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
|
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
|
||||||
import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields";
|
import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields";
|
||||||
import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields";
|
import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields";
|
||||||
|
import { AzureDevOpsSyncFields } from "./AzureDevOpsSyncFields";
|
||||||
import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
|
import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
|
||||||
import { CamundaSyncFields } from "./CamundaSyncFields";
|
import { CamundaSyncFields } from "./CamundaSyncFields";
|
||||||
import { DatabricksSyncFields } from "./DatabricksSyncFields";
|
import { DatabricksSyncFields } from "./DatabricksSyncFields";
|
||||||
@@ -38,6 +39,8 @@ export const SecretSyncDestinationFields = () => {
|
|||||||
return <AzureKeyVaultSyncFields />;
|
return <AzureKeyVaultSyncFields />;
|
||||||
case SecretSync.AzureAppConfiguration:
|
case SecretSync.AzureAppConfiguration:
|
||||||
return <AzureAppConfigurationSyncFields />;
|
return <AzureAppConfigurationSyncFields />;
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
return <AzureDevOpsSyncFields />;
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
return <DatabricksSyncFields />;
|
return <DatabricksSyncFields />;
|
||||||
case SecretSync.Humanitec:
|
case SecretSync.Humanitec:
|
||||||
|
@@ -41,6 +41,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
|||||||
case SecretSync.GCPSecretManager:
|
case SecretSync.GCPSecretManager:
|
||||||
case SecretSync.AzureKeyVault:
|
case SecretSync.AzureKeyVault:
|
||||||
case SecretSync.AzureAppConfiguration:
|
case SecretSync.AzureAppConfiguration:
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
case SecretSync.Humanitec:
|
case SecretSync.Humanitec:
|
||||||
case SecretSync.TerraformCloud:
|
case SecretSync.TerraformCloud:
|
||||||
|
@@ -0,0 +1,18 @@
|
|||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||||
|
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||||
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncReviewFields = () => {
|
||||||
|
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.AzureDevOps }>();
|
||||||
|
const devopsProjectId = watch("destinationConfig.devopsProjectId");
|
||||||
|
const devopsProjectName = watch("destinationConfig.devopsProjectName");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GenericFieldLabel label="Project">{devopsProjectName}</GenericFieldLabel>
|
||||||
|
<GenericFieldLabel label="Project ID">{devopsProjectId}</GenericFieldLabel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@@ -16,6 +16,7 @@ import {
|
|||||||
AwsSecretsManagerSyncReviewFields
|
AwsSecretsManagerSyncReviewFields
|
||||||
} from "./AwsSecretsManagerSyncReviewFields";
|
} from "./AwsSecretsManagerSyncReviewFields";
|
||||||
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
|
||||||
|
import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields";
|
||||||
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
||||||
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
|
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
|
||||||
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
||||||
@@ -70,6 +71,9 @@ export const SecretSyncReviewFields = () => {
|
|||||||
case SecretSync.AzureAppConfiguration:
|
case SecretSync.AzureAppConfiguration:
|
||||||
DestinationFieldsComponent = <AzureAppConfigurationSyncReviewFields />;
|
DestinationFieldsComponent = <AzureAppConfigurationSyncReviewFields />;
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.AzureDevOps:
|
||||||
|
DestinationFieldsComponent = <AzureDevOpsSyncReviewFields />;
|
||||||
|
break;
|
||||||
case SecretSync.Databricks:
|
case SecretSync.Databricks:
|
||||||
DestinationFieldsComponent = <DatabricksSyncReviewFields />;
|
DestinationFieldsComponent = <DatabricksSyncReviewFields />;
|
||||||
break;
|
break;
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
export const AzureDevOpsSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||||
|
z.object({
|
||||||
|
destination: z.literal(SecretSync.AzureDevOps),
|
||||||
|
destinationConfig: z.object({
|
||||||
|
devopsProjectId: z.string().trim().min(1, { message: "Azure DevOps Project ID is required" }),
|
||||||
|
devopsProjectName: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: "Azure DevOps Project Name is required" })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
@@ -4,6 +4,7 @@ import { OnePassSyncDestinationSchema } from "./1password-sync-destination-schem
|
|||||||
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
|
||||||
import { AwsSecretsManagerSyncDestinationSchema } from "./aws-secrets-manager-sync-destination-schema";
|
import { AwsSecretsManagerSyncDestinationSchema } from "./aws-secrets-manager-sync-destination-schema";
|
||||||
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
|
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
|
||||||
|
import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destination-schema";
|
||||||
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
||||||
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
|
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
|
||||||
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
|
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
|
||||||
@@ -24,6 +25,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
|||||||
GcpSyncDestinationSchema,
|
GcpSyncDestinationSchema,
|
||||||
AzureKeyVaultSyncDestinationSchema,
|
AzureKeyVaultSyncDestinationSchema,
|
||||||
AzureAppConfigurationSyncDestinationSchema,
|
AzureAppConfigurationSyncDestinationSchema,
|
||||||
|
AzureDevOpsSyncDestinationSchema,
|
||||||
DatabricksSyncDestinationSchema,
|
DatabricksSyncDestinationSchema,
|
||||||
HumanitecSyncDestinationSchema,
|
HumanitecSyncDestinationSchema,
|
||||||
TerraformCloudSyncDestinationSchema,
|
TerraformCloudSyncDestinationSchema,
|
||||||
|
@@ -15,6 +15,7 @@ import {
|
|||||||
AwsConnectionMethod,
|
AwsConnectionMethod,
|
||||||
AzureAppConfigurationConnectionMethod,
|
AzureAppConfigurationConnectionMethod,
|
||||||
AzureClientSecretsConnectionMethod,
|
AzureClientSecretsConnectionMethod,
|
||||||
|
AzureDevOpsConnectionMethod,
|
||||||
AzureKeyVaultConnectionMethod,
|
AzureKeyVaultConnectionMethod,
|
||||||
CamundaConnectionMethod,
|
CamundaConnectionMethod,
|
||||||
DatabricksConnectionMethod,
|
DatabricksConnectionMethod,
|
||||||
@@ -60,6 +61,7 @@ export const APP_CONNECTION_MAP: Record<
|
|||||||
name: "Azure Client Secrets",
|
name: "Azure Client Secrets",
|
||||||
image: "Microsoft Azure.png"
|
image: "Microsoft Azure.png"
|
||||||
},
|
},
|
||||||
|
[AppConnection.AzureDevOps]: { name: "Azure DevOps", image: "Microsoft Azure.png" },
|
||||||
[AppConnection.Databricks]: { name: "Databricks", image: "Databricks.png" },
|
[AppConnection.Databricks]: { name: "Databricks", image: "Databricks.png" },
|
||||||
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" },
|
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" },
|
||||||
[AppConnection.TerraformCloud]: { name: "Terraform Cloud", image: "Terraform Cloud.png" },
|
[AppConnection.TerraformCloud]: { name: "Terraform Cloud", image: "Terraform Cloud.png" },
|
||||||
@@ -85,6 +87,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
|||||||
case AzureKeyVaultConnectionMethod.OAuth:
|
case AzureKeyVaultConnectionMethod.OAuth:
|
||||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
case AzureDevOpsConnectionMethod.OAuth:
|
||||||
case GitHubConnectionMethod.OAuth:
|
case GitHubConnectionMethod.OAuth:
|
||||||
return { name: "OAuth", icon: faPassport };
|
return { name: "OAuth", icon: faPassport };
|
||||||
case AwsConnectionMethod.AccessKey:
|
case AwsConnectionMethod.AccessKey:
|
||||||
@@ -109,6 +112,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
|||||||
return { name: "Username & Password", icon: faLock };
|
return { name: "Username & Password", icon: faLock };
|
||||||
case HCVaultConnectionMethod.AccessToken:
|
case HCVaultConnectionMethod.AccessToken:
|
||||||
case TeamCityConnectionMethod.AccessToken:
|
case TeamCityConnectionMethod.AccessToken:
|
||||||
|
case AzureDevOpsConnectionMethod.AccessToken:
|
||||||
case WindmillConnectionMethod.AccessToken:
|
case WindmillConnectionMethod.AccessToken:
|
||||||
return { name: "Access Token", icon: faKey };
|
return { name: "Access Token", icon: faKey };
|
||||||
case Auth0ConnectionMethod.ClientCredentials:
|
case Auth0ConnectionMethod.ClientCredentials:
|
||||||
|
@@ -17,6 +17,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
|
|||||||
name: "Azure App Configuration",
|
name: "Azure App Configuration",
|
||||||
image: "Microsoft Azure.png"
|
image: "Microsoft Azure.png"
|
||||||
},
|
},
|
||||||
|
[SecretSync.AzureDevOps]: {
|
||||||
|
name: "Azure DevOps",
|
||||||
|
image: "Microsoft Azure.png"
|
||||||
|
},
|
||||||
[SecretSync.Databricks]: {
|
[SecretSync.Databricks]: {
|
||||||
name: "Databricks",
|
name: "Databricks",
|
||||||
image: "Databricks.png"
|
image: "Databricks.png"
|
||||||
@@ -66,6 +70,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
|||||||
[SecretSync.GCPSecretManager]: AppConnection.GCP,
|
[SecretSync.GCPSecretManager]: AppConnection.GCP,
|
||||||
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||||
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
||||||
|
[SecretSync.AzureDevOps]: AppConnection.AzureDevOps,
|
||||||
[SecretSync.Databricks]: AppConnection.Databricks,
|
[SecretSync.Databricks]: AppConnection.Databricks,
|
||||||
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
||||||
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
||||||
|
@@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { appConnectionKeys } from "../queries";
|
import { appConnectionKeys } from "../queries";
|
||||||
import { TAzureClient } from "./types";
|
import { AzureDevOpsProjectsResponse, TAzureClient } from "./types";
|
||||||
|
|
||||||
const azureConnectionKeys = {
|
const azureConnectionKeys = {
|
||||||
all: [...appConnectionKeys.all, "azure"] as const,
|
all: [...appConnectionKeys.all, "azure"] as const,
|
||||||
listClients: (connectionId: string) =>
|
listClients: (connectionId: string) =>
|
||||||
[...azureConnectionKeys.all, "clients", connectionId] as const
|
[...azureConnectionKeys.all, "clients", connectionId] as const,
|
||||||
|
listDevopsProjects: (connectionId: string) =>
|
||||||
|
[...azureConnectionKeys.all, "devops-projects", connectionId] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAzureConnectionListClients = (
|
export const useAzureConnectionListClients = (
|
||||||
@@ -35,3 +37,29 @@ export const useAzureConnectionListClients = (
|
|||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchAzureDevOpsProjects = async (
|
||||||
|
connectionId: string
|
||||||
|
): Promise<AzureDevOpsProjectsResponse> => {
|
||||||
|
if (!connectionId) {
|
||||||
|
throw new Error("Connection ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await apiRequest.get<AzureDevOpsProjectsResponse>(
|
||||||
|
`/api/v1/app-connections/azure-devops/${connectionId}/projects`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetAzureDevOpsProjects = (
|
||||||
|
connectionId: string,
|
||||||
|
options?: Omit<UseQueryOptions<AzureDevOpsProjectsResponse, Error>, "queryKey" | "queryFn">
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: azureConnectionKeys.listDevopsProjects(connectionId),
|
||||||
|
queryFn: () => fetchAzureDevOpsProjects(connectionId),
|
||||||
|
enabled: Boolean(connectionId),
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@@ -3,3 +3,13 @@ export type TAzureClient = {
|
|||||||
appId: string;
|
appId: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface AzureDevOpsProject {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
appId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AzureDevOpsProjectsResponse {
|
||||||
|
projects: AzureDevOpsProject[];
|
||||||
|
}
|
||||||
|