mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: finished up oidc auth management
This commit is contained in:
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -41,6 +41,7 @@ import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/
|
||||
import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
|
||||
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
|
||||
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
|
||||
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
|
||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
||||
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||
@@ -132,6 +133,7 @@ declare module "fastify" {
|
||||
identityGcpAuth: TIdentityGcpAuthServiceFactory;
|
||||
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
||||
identityAzureAuth: TIdentityAzureAuthServiceFactory;
|
||||
identityOidcAuth: TIdentityOidcAuthServiceFactory;
|
||||
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
|
||||
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
|
||||
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||
|
||||
@@ -166,5 +166,6 @@ export enum IdentityAuthMethod {
|
||||
KUBERNETES_AUTH = "kubernetes-auth",
|
||||
GCP_AUTH = "gcp-auth",
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth"
|
||||
AZURE_AUTH = "azure-auth",
|
||||
OIDC_AUTH = "oidc-auth"
|
||||
}
|
||||
|
||||
@@ -102,6 +102,8 @@ import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/ident
|
||||
import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
|
||||
import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal";
|
||||
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
|
||||
import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal";
|
||||
import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
|
||||
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
|
||||
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||
@@ -238,6 +240,7 @@ export const registerRoutes = async (
|
||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
|
||||
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
|
||||
const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
|
||||
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
|
||||
|
||||
const auditLogDAL = auditLogDALFactory(db);
|
||||
@@ -874,6 +877,16 @@ export const registerRoutes = async (
|
||||
licenseService
|
||||
});
|
||||
|
||||
const identityOidcAuthService = identityOidcAuthServiceFactory({
|
||||
identityOidcAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
orgBotDAL
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders();
|
||||
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
||||
queueService,
|
||||
@@ -972,6 +985,7 @@ export const registerRoutes = async (
|
||||
identityGcpAuth: identityGcpAuthService,
|
||||
identityAwsAuth: identityAwsAuthService,
|
||||
identityAzureAuth: identityAzureAuthService,
|
||||
identityOidcAuth: identityOidcAuthService,
|
||||
accessApprovalPolicy: accessApprovalPolicyService,
|
||||
accessApprovalRequest: accessApprovalRequestService,
|
||||
secretApprovalPolicy: secretApprovalPolicyService,
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityOidcAuthsSchema } from "@app/db/schemas";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { validateOidcAuthAudiencesField } from "@app/services/identity-oidc-auth/identity-oidc-auth-validators";
|
||||
|
||||
const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.omit({
|
||||
encryptedCaCert: true,
|
||||
caCertIV: true,
|
||||
caCertTag: true
|
||||
}).extend({
|
||||
caCert: z.string()
|
||||
});
|
||||
|
||||
export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
@@ -53,14 +61,133 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
boundIssuer: z.string().min(1),
|
||||
boundAudiences: validateOidcAuthAudiencesField,
|
||||
boundClaims: z.record(z.string()),
|
||||
boundSubject: z.string().optional()
|
||||
boundSubject: z.string().optional().default("")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityOidcAuth: IdentityOidcAuthsSchema
|
||||
identityOidcAuth: IdentityOidcAuthResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async () => {}
|
||||
handler: async (req) => {
|
||||
const identityOidcAuth = await server.services.identityOidcAuth.attachOidcAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
|
||||
return {
|
||||
identityOidcAuth
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/oidc-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update OIDC Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenTTL must have a non zero number"
|
||||
})
|
||||
.default(2592000),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.refine((value) => value !== 0, {
|
||||
message: "accessTokenMaxTTL must have a non zero number"
|
||||
})
|
||||
.default(2592000),
|
||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0),
|
||||
oidcDiscoveryUrl: z.string().url().min(1),
|
||||
caCert: z.string().trim().default(""),
|
||||
boundIssuer: z.string().min(1),
|
||||
boundAudiences: validateOidcAuthAudiencesField,
|
||||
boundClaims: z.record(z.string()),
|
||||
boundSubject: z.string().optional().default("")
|
||||
})
|
||||
.partial(),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityOidcAuth: IdentityOidcAuthResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityOidcAuth = await server.services.identityOidcAuth.updateOidcAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
|
||||
return { identityOidcAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/oidc-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Retrieve OIDC Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityOidcAuth: IdentityOidcAuthResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityOidcAuth = await server.services.identityOidcAuth.getOidcAuth({
|
||||
identityId: req.params.identityId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { identityOidcAuth };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,11 +1,350 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { IdentityAuthMethod, SecretKeyEncoding, TIdentityOidcAuthsUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||
import {
|
||||
decryptSymmetric,
|
||||
encryptSymmetric,
|
||||
generateSymmetricKey,
|
||||
infisicalSymmetricDecrypt,
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||
import { TOrgBotDALFactory } from "../org/org-bot-dal";
|
||||
import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal";
|
||||
import { TAttachOidcAuthDTO, TGetOidcAuthDTO, TUpdateOidcAuthDTO } from "./identity-oidc-auth-types";
|
||||
|
||||
type TIdentityOidcAuthServiceFactoryDep = {
|
||||
identityOidcAuthDAL: TIdentityOidcAuthDALFactory;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
|
||||
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
|
||||
};
|
||||
|
||||
export type TIdentityOidcAuthServiceFactory = ReturnType<typeof identityOidcAuthServiceFactory>;
|
||||
|
||||
export const identityOidcAuthServiceFactory = ({ identityOidcAuthDAL }: TIdentityOidcAuthServiceFactoryDep) => {
|
||||
return {};
|
||||
export const identityOidcAuthServiceFactory = ({
|
||||
identityOidcAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
orgBotDAL
|
||||
}: TIdentityOidcAuthServiceFactoryDep) => {
|
||||
const attachOidcAuth = async ({
|
||||
identityId,
|
||||
oidcDiscoveryUrl,
|
||||
caCert,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TAttachOidcAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
throw new BadRequestError({ message: "Failed to find identity" });
|
||||
}
|
||||
if (identityMembershipOrg.identity.authMethod)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add OIDC Auth to already configured identity"
|
||||
});
|
||||
|
||||
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
if (
|
||||
!plan.ipAllowlisting &&
|
||||
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||
throw new BadRequestError({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||
});
|
||||
|
||||
const orgBot = await orgBotDAL.transaction(async (tx) => {
|
||||
const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx);
|
||||
if (doc) return doc;
|
||||
|
||||
const { privateKey, publicKey } = generateAsymmetricKeyPair();
|
||||
const key = generateSymmetricKey();
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: privateKeyIV,
|
||||
tag: privateKeyTag,
|
||||
encoding: privateKeyKeyEncoding,
|
||||
algorithm: privateKeyAlgorithm
|
||||
} = infisicalSymmetricEncypt(privateKey);
|
||||
const {
|
||||
ciphertext: encryptedSymmetricKey,
|
||||
iv: symmetricKeyIV,
|
||||
tag: symmetricKeyTag,
|
||||
encoding: symmetricKeyKeyEncoding,
|
||||
algorithm: symmetricKeyAlgorithm
|
||||
} = infisicalSymmetricEncypt(key);
|
||||
|
||||
return orgBotDAL.create(
|
||||
{
|
||||
name: "Infisical org bot",
|
||||
publicKey,
|
||||
privateKeyIV,
|
||||
encryptedPrivateKey,
|
||||
symmetricKeyIV,
|
||||
symmetricKeyTag,
|
||||
encryptedSymmetricKey,
|
||||
symmetricKeyAlgorithm,
|
||||
orgId: identityMembershipOrg.orgId,
|
||||
privateKeyTag,
|
||||
privateKeyAlgorithm,
|
||||
privateKeyKeyEncoding,
|
||||
symmetricKeyKeyEncoding
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
tag: orgBot.symmetricKeyTag,
|
||||
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||
});
|
||||
|
||||
const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
|
||||
|
||||
const identityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
|
||||
const doc = await identityOidcAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
oidcDiscoveryUrl,
|
||||
encryptedCaCert,
|
||||
caCertIV,
|
||||
caCertTag,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
},
|
||||
tx
|
||||
);
|
||||
await identityDAL.updateById(
|
||||
identityMembershipOrg.identityId,
|
||||
{
|
||||
authMethod: IdentityAuthMethod.OIDC_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert };
|
||||
};
|
||||
|
||||
const updateOidcAuth = async ({
|
||||
identityId,
|
||||
oidcDiscoveryUrl,
|
||||
caCert,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateOidcAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
throw new BadRequestError({ message: "Failed to find identity" });
|
||||
}
|
||||
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update OIDC Auth"
|
||||
});
|
||||
}
|
||||
|
||||
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
|
||||
|
||||
if (
|
||||
(accessTokenMaxTTL || identityOidcAuth.accessTokenMaxTTL) > 0 &&
|
||||
(accessTokenTTL || identityOidcAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityOidcAuth.accessTokenMaxTTL)
|
||||
) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
if (
|
||||
!plan.ipAllowlisting &&
|
||||
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||
throw new BadRequestError({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||
});
|
||||
|
||||
const updateQuery: TIdentityOidcAuthsUpdate = {
|
||||
oidcDiscoveryUrl,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
};
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
|
||||
if (!orgBot) {
|
||||
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||
}
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
tag: orgBot.symmetricKeyTag,
|
||||
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||
});
|
||||
|
||||
if (caCert !== undefined) {
|
||||
const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
|
||||
updateQuery.encryptedCaCert = encryptedCACert;
|
||||
updateQuery.caCertIV = caCertIV;
|
||||
updateQuery.caCertTag = caCertTag;
|
||||
}
|
||||
|
||||
const updatedOidcAuth = await identityOidcAuthDAL.updateById(identityOidcAuth.id, updateQuery);
|
||||
const updatedCACert =
|
||||
updatedOidcAuth.encryptedCaCert && updatedOidcAuth.caCertIV && updatedOidcAuth.caCertTag
|
||||
? decryptSymmetric({
|
||||
ciphertext: updatedOidcAuth.encryptedCaCert,
|
||||
iv: updatedOidcAuth.caCertIV,
|
||||
tag: updatedOidcAuth.caCertTag,
|
||||
key
|
||||
})
|
||||
: "";
|
||||
|
||||
return {
|
||||
...updatedOidcAuth,
|
||||
orgId: identityMembershipOrg.orgId,
|
||||
caCert: updatedCACert
|
||||
};
|
||||
};
|
||||
|
||||
const getOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOidcAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
throw new BadRequestError({ message: "Failed to find identity" });
|
||||
}
|
||||
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have OIDC Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
|
||||
if (!orgBot) {
|
||||
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||
}
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
tag: orgBot.symmetricKeyTag,
|
||||
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||
});
|
||||
|
||||
const caCert = decryptSymmetric({
|
||||
ciphertext: identityOidcAuth.encryptedCaCert,
|
||||
iv: identityOidcAuth.caCertIV,
|
||||
tag: identityOidcAuth.caCertTag,
|
||||
key
|
||||
});
|
||||
|
||||
return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert };
|
||||
};
|
||||
|
||||
return {
|
||||
attachOidcAuth,
|
||||
updateOidcAuth,
|
||||
getOidcAuth
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TAttachOidcAuthDTO = {
|
||||
identityId: string;
|
||||
oidcDiscoveryUrl: string;
|
||||
caCert: string;
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateOidcAuthDTO = {
|
||||
identityId: string;
|
||||
oidcDiscoveryUrl?: string;
|
||||
caCert?: string;
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetOidcAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -5,5 +5,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
|
||||
[IdentityAuthMethod.KUBERNETES_AUTH]: "Kubernetes Auth",
|
||||
[IdentityAuthMethod.GCP_AUTH]: "GCP Auth",
|
||||
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth",
|
||||
[IdentityAuthMethod.AZURE_AUTH]: "Azure Auth"
|
||||
[IdentityAuthMethod.AZURE_AUTH]: "Azure Auth",
|
||||
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth"
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
AddIdentityAzureAuthDTO,
|
||||
AddIdentityGcpAuthDTO,
|
||||
AddIdentityKubernetesAuthDTO,
|
||||
AddIdentityOidcAuthDTO,
|
||||
AddIdentityUniversalAuthDTO,
|
||||
ClientSecretData,
|
||||
CreateIdentityDTO,
|
||||
@@ -21,12 +22,14 @@ import {
|
||||
IdentityAzureAuth,
|
||||
IdentityGcpAuth,
|
||||
IdentityKubernetesAuth,
|
||||
IdentityOidcAuth,
|
||||
IdentityUniversalAuth,
|
||||
UpdateIdentityAwsAuthDTO,
|
||||
UpdateIdentityAzureAuthDTO,
|
||||
UpdateIdentityDTO,
|
||||
UpdateIdentityGcpAuthDTO,
|
||||
UpdateIdentityKubernetesAuthDTO,
|
||||
UpdateIdentityOidcAuthDTO,
|
||||
UpdateIdentityUniversalAuthDTO
|
||||
} from "./types";
|
||||
|
||||
@@ -330,6 +333,90 @@ export const useUpdateIdentityAwsAuth = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityOidcAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityOidcAuth, {}, UpdateIdentityOidcAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
oidcDiscoveryUrl,
|
||||
caCert,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject
|
||||
}) => {
|
||||
const {
|
||||
data: { identityOidcAuth }
|
||||
} = await apiRequest.patch<{ identityOidcAuth: IdentityOidcAuth }>(
|
||||
`/api/v1/auth/oidc-auth/identities/${identityId}`,
|
||||
{
|
||||
oidcDiscoveryUrl,
|
||||
caCert,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityOidcAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddIdentityOidcAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityOidcAuth, {}, AddIdentityOidcAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
oidcDiscoveryUrl,
|
||||
caCert,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const {
|
||||
data: { identityOidcAuth }
|
||||
} = await apiRequest.post<{ identityOidcAuth: IdentityOidcAuth }>(
|
||||
`/api/v1/auth/oidc-auth/identities/${identityId}`,
|
||||
{
|
||||
oidcDiscoveryUrl,
|
||||
caCert,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityOidcAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddIdentityAzureAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityAzureAuth, {}, AddIdentityAzureAuthDTO>({
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
IdentityAzureAuth,
|
||||
IdentityGcpAuth,
|
||||
IdentityKubernetesAuth,
|
||||
IdentityUniversalAuth} from "./types";
|
||||
IdentityOidcAuth,
|
||||
IdentityUniversalAuth
|
||||
} from "./types";
|
||||
|
||||
export const identitiesKeys = {
|
||||
getIdentityUniversalAuth: (identityId: string) =>
|
||||
@@ -18,6 +20,7 @@ export const identitiesKeys = {
|
||||
getIdentityKubernetesAuth: (identityId: string) =>
|
||||
[{ identityId }, "identity-kubernetes-auth"] as const,
|
||||
getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
|
||||
getIdentityOidcAuth: (identityId: string) => [{ identityId }, "identity-oidc-auth"] as const,
|
||||
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const,
|
||||
getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const
|
||||
};
|
||||
@@ -111,3 +114,18 @@ export const useGetIdentityKubernetesAuth = (identityId: string) => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityOidcAuth = (identityId: string) => {
|
||||
return useQuery({
|
||||
enabled: Boolean(identityId),
|
||||
queryKey: identitiesKeys.getIdentityOidcAuth(identityId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { identityOidcAuth }
|
||||
} = await apiRequest.get<{ identityOidcAuth: IdentityOidcAuth }>(
|
||||
`/api/v1/auth/oidc-auth/identities/${identityId}`
|
||||
);
|
||||
return identityOidcAuth;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -155,6 +155,54 @@ export type UpdateIdentityGcpAuthDTO = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export type IdentityOidcAuth = {
|
||||
identityId: string;
|
||||
oidcDiscoveryUrl: string;
|
||||
caCert: string;
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||
};
|
||||
|
||||
export type AddIdentityOidcAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
oidcDiscoveryUrl: string;
|
||||
caCert: string;
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type UpdateIdentityOidcAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
oidcDiscoveryUrl?: string;
|
||||
caCert?: string;
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type IdentityAwsAuth = {
|
||||
identityId: string;
|
||||
type: "iam";
|
||||
|
||||
@@ -6,9 +6,14 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import {
|
||||
useAddIdentityOidcAuth,
|
||||
useUpdateIdentityOidcAuth
|
||||
} from "@app/hooks/api/identities/mutations";
|
||||
import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -60,10 +65,10 @@ export const IdentityOidcAuthForm = ({
|
||||
const orgId = currentOrg?.id || "";
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
// const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
|
||||
// const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
|
||||
|
||||
// const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "");
|
||||
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "");
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -95,78 +100,93 @@ export const IdentityOidcAuthForm = ({
|
||||
remove: removeAccessTokenTrustedIp
|
||||
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
||||
|
||||
// useEffect(() => {
|
||||
// if (data) {
|
||||
// reset({
|
||||
// type: data.type,
|
||||
// allowedServiceAccounts: data.allowedServiceAccounts,
|
||||
// allowedProjects: data.allowedProjects,
|
||||
// allowedZones: data.allowedZones,
|
||||
// accessTokenTTL: String(data.accessTokenTTL),
|
||||
// accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||
// accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
|
||||
// accessTokenTrustedIps: data.accessTokenTrustedIps.map(
|
||||
// ({ ipAddress, prefix }: IdentityTrustedIp) => {
|
||||
// return {
|
||||
// ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||
// };
|
||||
// }
|
||||
// )
|
||||
// });
|
||||
// } else {
|
||||
// reset({
|
||||
// type: "iam",
|
||||
// allowedServiceAccounts: "",
|
||||
// allowedProjects: "",
|
||||
// allowedZones: "",
|
||||
// accessTokenTTL: "2592000",
|
||||
// accessTokenMaxTTL: "2592000",
|
||||
// accessTokenNumUsesLimit: "0",
|
||||
// accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
// });
|
||||
// }
|
||||
// }, [data]);
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
oidcDiscoveryUrl: data.oidcDiscoveryUrl,
|
||||
caCert: data.caCert,
|
||||
boundIssuer: data.boundIssuer,
|
||||
boundAudiences: data.boundAudiences,
|
||||
boundClaims: Object.entries(data.boundClaims).map(([key, value]) => ({
|
||||
key,
|
||||
value
|
||||
})),
|
||||
boundSubject: data.boundSubject,
|
||||
accessTokenTTL: String(data.accessTokenTTL),
|
||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
|
||||
({ ipAddress, prefix }: IdentityTrustedIp) => {
|
||||
return {
|
||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||
};
|
||||
}
|
||||
)
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
oidcDiscoveryUrl: "",
|
||||
caCert: "",
|
||||
boundIssuer: "",
|
||||
boundAudiences: "",
|
||||
boundClaims: [],
|
||||
boundSubject: "",
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
accessTokenTrustedIps,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
oidcDiscoveryUrl,
|
||||
caCert,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!identityAuthMethodData) return;
|
||||
if (!identityAuthMethodData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if (data) {
|
||||
// await updateMutateAsync({
|
||||
// identityId: identityAuthMethodData.identityId,
|
||||
// organizationId: orgId,
|
||||
// type,
|
||||
// allowedServiceAccounts,
|
||||
// allowedProjects,
|
||||
// allowedZones,
|
||||
// accessTokenTTL: Number(accessTokenTTL),
|
||||
// accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
// accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
// accessTokenTrustedIps
|
||||
// });
|
||||
// } else {
|
||||
// await addMutateAsync({
|
||||
// identityId: identityAuthMethodData.identityId,
|
||||
// organizationId: orgId,
|
||||
// type,
|
||||
// allowedServiceAccounts: allowedServiceAccounts || "",
|
||||
// allowedProjects: allowedProjects || "",
|
||||
// allowedZones: allowedZones || "",
|
||||
// accessTokenTTL: Number(accessTokenTTL),
|
||||
// accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
// accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
// accessTokenTrustedIps
|
||||
// });
|
||||
// }
|
||||
if (data) {
|
||||
await updateMutateAsync({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
organizationId: orgId,
|
||||
oidcDiscoveryUrl,
|
||||
caCert,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
|
||||
boundSubject,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
} else {
|
||||
await addMutateAsync({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
oidcDiscoveryUrl,
|
||||
caCert,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
|
||||
boundSubject,
|
||||
organizationId: orgId,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
@@ -198,11 +218,7 @@ export const IdentityOidcAuthForm = ({
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://accounts.google.com/.well-known/openid-configuration"
|
||||
type="text"
|
||||
/>
|
||||
<Input {...field} placeholder="https://accounts.google.com" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -216,7 +232,16 @@ export const IdentityOidcAuthForm = ({
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="text" />
|
||||
<Input {...field} type="text" placeholder="https://accounts.google.com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user