mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
misc: added dynamic credential support and gateway auth
This commit is contained in:
@@ -1,13 +1,21 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import handlebars from "handlebars";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
|
|
||||||
import { InternalServerError } from "@app/lib/errors";
|
import { InternalServerError } from "@app/lib/errors";
|
||||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||||
|
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 { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
import { DynamicSecretKubernetesSchema, TDynamicProviderFns } from "./models";
|
import {
|
||||||
|
DynamicSecretKubernetesSchema,
|
||||||
|
KubernetesAuthMethod,
|
||||||
|
KubernetesCredentialType,
|
||||||
|
KubernetesRoleType,
|
||||||
|
TDynamicProviderFns
|
||||||
|
} from "./models";
|
||||||
|
|
||||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
@@ -15,6 +23,16 @@ type TKubernetesProviderDTO = {
|
|||||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateUsername = (usernameTemplate?: string | null) => {
|
||||||
|
const randomUsername = `dynamic-secret-sa-${alphaNumericNanoId(10).toLowerCase()}`;
|
||||||
|
if (!usernameTemplate) return randomUsername;
|
||||||
|
|
||||||
|
return handlebars.compile(usernameTemplate)({
|
||||||
|
randomUsername,
|
||||||
|
unixTimestamp: Math.floor(Date.now() / 100)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
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);
|
||||||
@@ -30,20 +48,27 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
gatewayId: string;
|
gatewayId: string;
|
||||||
targetHost: string;
|
targetHost: string;
|
||||||
targetPort: number;
|
targetPort: number;
|
||||||
|
caCert?: string;
|
||||||
|
reviewTokenThroughGateway: boolean;
|
||||||
|
enableSsl: boolean;
|
||||||
},
|
},
|
||||||
gatewayCallback: (host: string, port: number) => Promise<T>
|
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
|
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
|
||||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||||
|
|
||||||
const callbackResult = await withGatewayProxy(
|
const callbackResult = await withGatewayProxy(
|
||||||
async (port) => {
|
async (port, httpsAgent) => {
|
||||||
// Needs to be https protocol or the kubernetes API server will fail with "Client sent an HTTP request to an HTTPS server"
|
// Needs to be https protocol or the kubernetes API server will fail with "Client sent an HTTP request to an HTTPS server"
|
||||||
const res = await gatewayCallback("https://localhost", port);
|
const res = await gatewayCallback(
|
||||||
|
inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost",
|
||||||
|
port,
|
||||||
|
httpsAgent
|
||||||
|
);
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: GatewayProxyProtocol.Tcp,
|
protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp,
|
||||||
targetHost: inputs.targetHost,
|
targetHost: inputs.targetHost,
|
||||||
targetPort: inputs.targetPort,
|
targetPort: inputs.targetPort,
|
||||||
relayHost,
|
relayHost,
|
||||||
@@ -54,7 +79,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
ca: relayDetails.certChain,
|
ca: relayDetails.certChain,
|
||||||
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
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
ca: inputs.caCert,
|
||||||
|
rejectUnauthorized: inputs.enableSsl
|
||||||
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -64,7 +94,169 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
const serviceAccountGetCallback = async (host: string, port: number) => {
|
const serviceAccountDynamicCallback = async (host: string, port: number) => {
|
||||||
|
if (providerInputs.credentialType !== KubernetesCredentialType.Dynamic) {
|
||||||
|
throw new Error("invalid callback");
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
|
const serviceAccountName = generateUsername();
|
||||||
|
const roleBindingName = `${serviceAccountName}-role-binding`;
|
||||||
|
|
||||||
|
// 1. Create a test service account
|
||||||
|
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: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Create a test role binding
|
||||||
|
const roleBindingUrl =
|
||||||
|
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
||||||
|
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
||||||
|
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings`;
|
||||||
|
|
||||||
|
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",
|
||||||
|
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: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Request a token for the test service account
|
||||||
|
await axios.post(
|
||||||
|
`${baseUrl}/api/v1/namespaces/${providerInputs.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.InjectGatewayK8sServiceAccountToken }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
|
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Cleanup: delete role binding and service account
|
||||||
|
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
||||||
|
await axios.delete(
|
||||||
|
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings/${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: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} 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: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.delete(
|
||||||
|
`${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: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const serviceAccountStaticCallback = async (host: string, port: number) => {
|
||||||
|
if (providerInputs.credentialType !== KubernetesCredentialType.Static) {
|
||||||
|
throw new Error("invalid callback");
|
||||||
|
}
|
||||||
|
|
||||||
const baseUrl = port ? `${host}:${port}` : host;
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
|
|
||||||
await axios.get(
|
await axios.get(
|
||||||
@@ -72,7 +264,9 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${providerInputs.clusterToken}`
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||||
@@ -85,23 +279,45 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
};
|
};
|
||||||
|
|
||||||
const url = new URL(providerInputs.url);
|
const url = new URL(providerInputs.url);
|
||||||
|
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}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (providerInputs.gatewayId) {
|
if (providerInputs.gatewayId) {
|
||||||
const k8sHost = url.hostname;
|
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||||
|
await $gatewayProxyWrapper(
|
||||||
await $gatewayProxyWrapper(
|
{
|
||||||
{
|
gatewayId: providerInputs.gatewayId,
|
||||||
gatewayId: providerInputs.gatewayId,
|
targetHost: k8sHost,
|
||||||
targetHost: k8sHost,
|
targetPort: k8sPort,
|
||||||
targetPort: k8sPort
|
enableSsl: providerInputs.sslEnabled,
|
||||||
},
|
caCert: providerInputs.ca,
|
||||||
serviceAccountGetCallback
|
reviewTokenThroughGateway: true
|
||||||
);
|
},
|
||||||
|
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||||
|
? serviceAccountStaticCallback
|
||||||
|
: serviceAccountDynamicCallback
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await $gatewayProxyWrapper(
|
||||||
|
{
|
||||||
|
gatewayId: providerInputs.gatewayId,
|
||||||
|
targetHost: k8sGatewayHost,
|
||||||
|
targetPort: k8sPort,
|
||||||
|
enableSsl: providerInputs.sslEnabled,
|
||||||
|
caCert: providerInputs.ca,
|
||||||
|
reviewTokenThroughGateway: false
|
||||||
|
},
|
||||||
|
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||||
|
? serviceAccountStaticCallback
|
||||||
|
: serviceAccountDynamicCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (providerInputs.credentialType === KubernetesCredentialType.Static) {
|
||||||
|
await serviceAccountStaticCallback(k8sHost, k8sPort);
|
||||||
} else {
|
} else {
|
||||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
await serviceAccountDynamicCallback(k8sHost, k8sPort);
|
||||||
await serviceAccountGetCallback(k8sHost, k8sPort);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -117,10 +333,128 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const create = async ({ inputs, expireAt }: { inputs: unknown; expireAt: number }) => {
|
const create = async ({
|
||||||
|
inputs,
|
||||||
|
expireAt,
|
||||||
|
usernameTemplate
|
||||||
|
}: {
|
||||||
|
inputs: unknown;
|
||||||
|
expireAt: number;
|
||||||
|
usernameTemplate?: string | null;
|
||||||
|
}) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
const tokenRequestCallback = async (host: string, port: number) => {
|
const serviceAccountDynamicCallback = async (host: string, port: number) => {
|
||||||
|
if (providerInputs.credentialType !== KubernetesCredentialType.Dynamic) {
|
||||||
|
throw new Error("invalid callback");
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
|
const serviceAccountName = generateUsername(usernameTemplate);
|
||||||
|
const roleBindingName = `${serviceAccountName}-role-binding`;
|
||||||
|
|
||||||
|
// 1. Create the service account
|
||||||
|
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: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Create the role binding
|
||||||
|
const roleBindingUrl =
|
||||||
|
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
||||||
|
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
||||||
|
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings`;
|
||||||
|
|
||||||
|
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",
|
||||||
|
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: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Request a token for the service account
|
||||||
|
const res = await axios.post<TKubernetesTokenRequest>(
|
||||||
|
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}/token`,
|
||||||
|
{
|
||||||
|
spec: {
|
||||||
|
expirationSeconds: Math.floor((expireAt - Date.now()) / 1000),
|
||||||
|
...(providerInputs.audiences?.length ? { audiences: providerInputs.audiences } : {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...res.data, serviceAccountName };
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenRequestStaticCallback = async (host: string, port: number) => {
|
||||||
|
if (providerInputs.credentialType !== KubernetesCredentialType.Static) {
|
||||||
|
throw new Error("invalid callback");
|
||||||
|
}
|
||||||
|
|
||||||
const baseUrl = port ? `${host}:${port}` : host;
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
|
|
||||||
const res = await axios.post<TKubernetesTokenRequest>(
|
const res = await axios.post<TKubernetesTokenRequest>(
|
||||||
@@ -134,7 +468,9 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${providerInputs.clusterToken}`
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||||
@@ -145,7 +481,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.data;
|
return { ...res.data, serviceAccountName: providerInputs.serviceAccountName };
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = new URL(providerInputs.url);
|
const url = new URL(providerInputs.url);
|
||||||
@@ -154,19 +490,46 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const k8sPort = url.port ? Number(url.port) : 443;
|
const k8sPort = url.port ? Number(url.port) : 443;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenData = providerInputs.gatewayId
|
let tokenData;
|
||||||
? await $gatewayProxyWrapper(
|
if (providerInputs.gatewayId) {
|
||||||
|
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||||
|
tokenData = await $gatewayProxyWrapper(
|
||||||
|
{
|
||||||
|
gatewayId: providerInputs.gatewayId,
|
||||||
|
targetHost: k8sHost,
|
||||||
|
targetPort: k8sPort,
|
||||||
|
enableSsl: providerInputs.sslEnabled,
|
||||||
|
caCert: providerInputs.ca,
|
||||||
|
reviewTokenThroughGateway: true
|
||||||
|
},
|
||||||
|
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||||
|
? tokenRequestStaticCallback
|
||||||
|
: serviceAccountDynamicCallback
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tokenData = await $gatewayProxyWrapper(
|
||||||
{
|
{
|
||||||
gatewayId: providerInputs.gatewayId,
|
gatewayId: providerInputs.gatewayId,
|
||||||
targetHost: k8sGatewayHost,
|
targetHost: k8sGatewayHost,
|
||||||
targetPort: k8sPort
|
targetPort: k8sPort,
|
||||||
|
enableSsl: providerInputs.sslEnabled,
|
||||||
|
caCert: providerInputs.ca,
|
||||||
|
reviewTokenThroughGateway: false
|
||||||
},
|
},
|
||||||
tokenRequestCallback
|
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||||
)
|
? tokenRequestStaticCallback
|
||||||
: await tokenRequestCallback(k8sHost, k8sPort);
|
: serviceAccountDynamicCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tokenData =
|
||||||
|
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||||
|
? await tokenRequestStaticCallback(k8sHost, k8sPort)
|
||||||
|
: await serviceAccountDynamicCallback(k8sHost, k8sPort);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entityId: providerInputs.serviceAccountName,
|
entityId: tokenData.serviceAccountName,
|
||||||
data: { TOKEN: tokenData.status.token }
|
data: { TOKEN: tokenData.status.token }
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -181,7 +544,106 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const revoke = async (_inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
|
const serviceAccountDynamicCallback = async (host: string, port: number) => {
|
||||||
|
if (providerInputs.credentialType !== KubernetesCredentialType.Dynamic) {
|
||||||
|
throw new Error("invalid callback");
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
|
const roleBindingName = `${entityId}-role-binding`;
|
||||||
|
|
||||||
|
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
||||||
|
await axios.delete(
|
||||||
|
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings/${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: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} 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: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the service account
|
||||||
|
await axios.delete(`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${entityId}`, {
|
||||||
|
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: new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: providerInputs.sslEnabled
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) {
|
||||||
|
const url = new URL(providerInputs.url);
|
||||||
|
const k8sGatewayHost = url.hostname;
|
||||||
|
const k8sPort = url.port ? Number(url.port) : 443;
|
||||||
|
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||||
|
|
||||||
|
if (providerInputs.gatewayId) {
|
||||||
|
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||||
|
await $gatewayProxyWrapper(
|
||||||
|
{
|
||||||
|
gatewayId: providerInputs.gatewayId,
|
||||||
|
targetHost: k8sHost,
|
||||||
|
targetPort: k8sPort,
|
||||||
|
enableSsl: providerInputs.sslEnabled,
|
||||||
|
caCert: providerInputs.ca,
|
||||||
|
reviewTokenThroughGateway: true
|
||||||
|
},
|
||||||
|
serviceAccountDynamicCallback
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await $gatewayProxyWrapper(
|
||||||
|
{
|
||||||
|
gatewayId: providerInputs.gatewayId,
|
||||||
|
targetHost: k8sGatewayHost,
|
||||||
|
targetPort: k8sPort,
|
||||||
|
enableSsl: providerInputs.sslEnabled,
|
||||||
|
caCert: providerInputs.ca,
|
||||||
|
reviewTokenThroughGateway: false
|
||||||
|
},
|
||||||
|
serviceAccountDynamicCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await serviceAccountDynamicCallback(k8sHost, k8sPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,18 @@ export enum LdapCredentialType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum KubernetesCredentialType {
|
export enum KubernetesCredentialType {
|
||||||
Static = "static"
|
Static = "static",
|
||||||
|
Dynamic = "dynamic"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum KubernetesRoleType {
|
||||||
|
ClusterRole = "cluster-role",
|
||||||
|
Role = "role"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum KubernetesAuthMethod {
|
||||||
|
Gateway = "gateway",
|
||||||
|
Api = "api"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TotpConfigType {
|
export enum TotpConfigType {
|
||||||
@@ -282,17 +293,50 @@ export const LdapSchema = z.union([
|
|||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const DynamicSecretKubernetesSchema = z.object({
|
export const DynamicSecretKubernetesSchema = z
|
||||||
url: z.string().url().trim().min(1),
|
.discriminatedUnion("credentialType", [
|
||||||
gatewayId: z.string().nullable().optional(),
|
z.object({
|
||||||
sslEnabled: z.boolean().default(true),
|
url: z.string().url().trim().min(1),
|
||||||
clusterToken: z.string().trim().min(1),
|
clusterToken: z.string().trim().optional(),
|
||||||
ca: z.string().optional(),
|
ca: z.string().optional(),
|
||||||
serviceAccountName: z.string().trim().min(1),
|
sslEnabled: z.boolean().default(false),
|
||||||
credentialType: z.literal(KubernetesCredentialType.Static),
|
credentialType: z.literal(KubernetesCredentialType.Static),
|
||||||
namespace: z.string().trim().min(1),
|
serviceAccountName: z.string().trim().min(1),
|
||||||
audiences: z.array(z.string().trim().min(1))
|
namespace: z.string().trim().min(1),
|
||||||
});
|
gatewayId: z.string().optional(),
|
||||||
|
audiences: z.array(z.string().trim().min(1)),
|
||||||
|
authMethod: z.nativeEnum(KubernetesAuthMethod).default(KubernetesAuthMethod.Api)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
url: z.string().url().trim().min(1),
|
||||||
|
clusterToken: z.string().trim().optional(),
|
||||||
|
ca: z.string().optional(),
|
||||||
|
sslEnabled: z.boolean().default(false),
|
||||||
|
credentialType: z.literal(KubernetesCredentialType.Dynamic),
|
||||||
|
namespace: z.string().trim().min(1),
|
||||||
|
gatewayId: z.string().optional(),
|
||||||
|
audiences: z.array(z.string().trim().min(1)),
|
||||||
|
roleType: z.nativeEnum(KubernetesRoleType),
|
||||||
|
role: z.string().trim().min(1),
|
||||||
|
authMethod: z.nativeEnum(KubernetesAuthMethod).default(KubernetesAuthMethod.Api)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.authMethod === KubernetesAuthMethod.Gateway && !data.gatewayId) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["gatewayId"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "When auth method is set to Gateway, a gateway must be selected"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ((data.authMethod === KubernetesAuthMethod.Api || !data.authMethod) && !data.clusterToken) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["clusterToken"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "When auth method is set to Manual Token, a cluster token must be provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const DynamicSecretVerticaSchema = z.object({
|
export const DynamicSecretVerticaSchema = z.object({
|
||||||
host: z.string().trim().toLowerCase(),
|
host: z.string().trim().toLowerCase(),
|
||||||
|
|||||||
@@ -267,17 +267,32 @@ export type TDynamicSecretProvider =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: DynamicSecretProviders.Kubernetes;
|
type: DynamicSecretProviders.Kubernetes;
|
||||||
inputs: {
|
inputs:
|
||||||
url: string;
|
| {
|
||||||
clusterToken: string;
|
url: string;
|
||||||
ca?: string;
|
clusterToken?: string;
|
||||||
serviceAccountName: string;
|
ca?: string;
|
||||||
credentialType: "dynamic" | "static";
|
serviceAccountName: string;
|
||||||
namespace: string;
|
credentialType: "static";
|
||||||
gatewayId?: string;
|
namespace: string;
|
||||||
sslEnabled: boolean;
|
gatewayId?: string;
|
||||||
audiences: string[];
|
sslEnabled: boolean;
|
||||||
};
|
audiences: string[];
|
||||||
|
authMethod: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
url: string;
|
||||||
|
clusterToken?: string;
|
||||||
|
ca?: string;
|
||||||
|
credentialType: "dynamic";
|
||||||
|
namespace: string;
|
||||||
|
gatewayId?: string;
|
||||||
|
sslEnabled: boolean;
|
||||||
|
audiences: string[];
|
||||||
|
roleType: string;
|
||||||
|
role: string;
|
||||||
|
authMethod: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: DynamicSecretProviders.Vertica;
|
type: DynamicSecretProviders.Vertica;
|
||||||
|
|||||||
@@ -38,46 +38,94 @@ enum CredentialType {
|
|||||||
Static = "static"
|
Static = "static"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RoleType {
|
||||||
|
ClusterRole = "cluster-role",
|
||||||
|
Role = "role"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AuthMethod {
|
||||||
|
Api = "api",
|
||||||
|
Gateway = "gateway"
|
||||||
|
}
|
||||||
|
|
||||||
const credentialTypes = [
|
const credentialTypes = [
|
||||||
{
|
{
|
||||||
label: "Static",
|
label: "Static",
|
||||||
value: CredentialType.Static
|
value: CredentialType.Static
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Dynamic",
|
||||||
|
value: CredentialType.Dynamic
|
||||||
}
|
}
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z
|
||||||
provider: z.object({
|
.object({
|
||||||
url: z.string().url().trim().min(1),
|
provider: z.discriminatedUnion("credentialType", [
|
||||||
clusterToken: z.string().trim().min(1),
|
z.object({
|
||||||
ca: z.string().optional(),
|
url: z.string().url().trim().min(1),
|
||||||
sslEnabled: z.boolean().default(false),
|
clusterToken: z.string().trim().optional(),
|
||||||
credentialType: z.literal(CredentialType.Static),
|
ca: z.string().optional(),
|
||||||
serviceAccountName: z.string().trim().min(1),
|
sslEnabled: z.boolean().default(false),
|
||||||
namespace: z.string().trim().min(1),
|
credentialType: z.literal(CredentialType.Static),
|
||||||
gatewayId: z.string().optional(),
|
serviceAccountName: z.string().trim().min(1),
|
||||||
audiences: z.array(z.string().trim().min(1))
|
namespace: z.string().trim().min(1),
|
||||||
}),
|
gatewayId: z.string().optional(),
|
||||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
audiences: z.array(z.string().trim().min(1)),
|
||||||
const valMs = ms(val);
|
authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api)
|
||||||
if (valMs < 60 * 1000)
|
}),
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
z.object({
|
||||||
if (valMs > 24 * 60 * 60 * 1000)
|
url: z.string().url().trim().min(1),
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
clusterToken: z.string().trim().optional(),
|
||||||
}),
|
ca: z.string().optional(),
|
||||||
maxTTL: z
|
sslEnabled: z.boolean().default(false),
|
||||||
.string()
|
credentialType: z.literal(CredentialType.Dynamic),
|
||||||
.optional()
|
namespace: z.string().trim().min(1),
|
||||||
.superRefine((val, ctx) => {
|
gatewayId: z.string().optional(),
|
||||||
if (!val) return;
|
audiences: z.array(z.string().trim().min(1)),
|
||||||
|
roleType: z.nativeEnum(RoleType),
|
||||||
|
role: z.string().trim().min(1),
|
||||||
|
authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api)
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||||
const valMs = ms(val);
|
const valMs = ms(val);
|
||||||
if (valMs < 60 * 1000)
|
if (valMs < 60 * 1000)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
if (valMs > 24 * 60 * 60 * 1000)
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
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" });
|
||||||
}),
|
}),
|
||||||
name: slugSchema(),
|
maxTTL: z
|
||||||
environment: z.object({ name: z.string(), slug: z.string() })
|
.string()
|
||||||
});
|
.optional()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
name: slugSchema(),
|
||||||
|
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
usernameTemplate: z.string().trim().optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.provider.authMethod === AuthMethod.Gateway && !data.provider.gatewayId) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["provider.gatewayId"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "When auth method is set to Gateway, a gateway must be selected"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data.provider.authMethod === AuthMethod.Api && !data.provider.clusterToken) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["provider.clusterToken"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "When auth method is set to Manual Token, a cluster token must be provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
type TForm = z.infer<typeof formSchema> & FieldValues;
|
type TForm = z.infer<typeof formSchema> & FieldValues;
|
||||||
|
|
||||||
@@ -115,8 +163,9 @@ export const KubernetesInputForm = ({
|
|||||||
namespace: "",
|
namespace: "",
|
||||||
credentialType: CredentialType.Static,
|
credentialType: CredentialType.Static,
|
||||||
gatewayId: undefined,
|
gatewayId: undefined,
|
||||||
audiences: []
|
audiences: [],
|
||||||
},
|
authMethod: AuthMethod.Api
|
||||||
|
} as const,
|
||||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -130,12 +179,16 @@ export const KubernetesInputForm = ({
|
|||||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
const sslEnabled = watch("provider.sslEnabled");
|
const sslEnabled = watch("provider.sslEnabled");
|
||||||
|
const credentialType = watch("provider.credentialType");
|
||||||
|
const authMethod = watch("provider.authMethod");
|
||||||
|
|
||||||
const handleCreateDynamicSecret = async (formData: TForm) => {
|
const handleCreateDynamicSecret = async (formData: TForm) => {
|
||||||
const { provider, ...rest } = formData;
|
const { provider, usernameTemplate, ...rest } = formData;
|
||||||
// wait till previous request is finished
|
// wait till previous request is finished
|
||||||
if (createDynamicSecret.isPending) return;
|
if (createDynamicSecret.isPending) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||||
await createDynamicSecret.mutateAsync({
|
await createDynamicSecret.mutateAsync({
|
||||||
provider: { type: DynamicSecretProviders.Kubernetes, inputs: provider },
|
provider: { type: DynamicSecretProviders.Kubernetes, inputs: provider },
|
||||||
maxTTL: rest.maxTTL,
|
maxTTL: rest.maxTTL,
|
||||||
@@ -143,7 +196,9 @@ export const KubernetesInputForm = ({
|
|||||||
path: secretPath,
|
path: secretPath,
|
||||||
defaultTTL: rest.defaultTTL,
|
defaultTTL: rest.defaultTTL,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
environmentSlug: rest.environment.slug
|
environmentSlug: rest.environment.slug,
|
||||||
|
usernameTemplate:
|
||||||
|
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||||
});
|
});
|
||||||
|
|
||||||
onCompleted();
|
onCompleted();
|
||||||
@@ -343,20 +398,44 @@ export const KubernetesInputForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="provider.clusterToken"
|
name="provider.authMethod"
|
||||||
|
defaultValue={AuthMethod.Api}
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Cluster Token"
|
label="Auth Method"
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
<Input {...field} type="password" autoComplete="new-password" />
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
>
|
||||||
|
<SelectItem value={AuthMethod.Api}>Manual Token (API)</SelectItem>
|
||||||
|
<SelectItem value={AuthMethod.Gateway}>Gateway</SelectItem>
|
||||||
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{authMethod === AuthMethod.Api && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.clusterToken"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Cluster Token"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="password" autoComplete="new-password" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="provider.credentialType"
|
name="provider.credentialType"
|
||||||
@@ -373,12 +452,9 @@ export const KubernetesInputForm = ({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(e) => field.onChange(e)}
|
onValueChange={(e) => field.onChange(e)}
|
||||||
>
|
>
|
||||||
{credentialTypes.map((credentialType) => (
|
{credentialTypes.map((ct) => (
|
||||||
<SelectItem
|
<SelectItem value={ct.value} key={`credential-type-${ct.value}`}>
|
||||||
value={credentialType.value}
|
{ct.label}
|
||||||
key={`credential-type-${credentialType.value}`}
|
|
||||||
>
|
|
||||||
{credentialType.label}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -386,21 +462,45 @@ export const KubernetesInputForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex-1">
|
{credentialType === CredentialType.Static && (
|
||||||
<Controller
|
<div className="flex-1">
|
||||||
control={control}
|
<Controller
|
||||||
name="provider.serviceAccountName"
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => (
|
name="provider.serviceAccountName"
|
||||||
<FormControl
|
render={({ field, fieldState: { error } }) => (
|
||||||
label="Service Account Name"
|
<FormControl
|
||||||
isError={Boolean(error?.message)}
|
label="Service Account Name"
|
||||||
errorText={error?.message}
|
isError={Boolean(error?.message)}
|
||||||
>
|
errorText={error?.message}
|
||||||
<Input {...field} autoComplete="new-password" />
|
>
|
||||||
</FormControl>
|
<Input {...field} autoComplete="new-password" />
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
)}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{credentialType === CredentialType.Dynamic && (
|
||||||
|
<div className="flex-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="usernameTemplate"
|
||||||
|
defaultValue="{{randomUsername}}"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Username Template"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || undefined}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -417,6 +517,56 @@ export const KubernetesInputForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{credentialType === CredentialType.Dynamic && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.roleType"
|
||||||
|
defaultValue={RoleType.ClusterRole}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Role Type"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={RoleType.ClusterRole}
|
||||||
|
key={`role-type-${RoleType.ClusterRole}`}
|
||||||
|
>
|
||||||
|
Cluster Role
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={RoleType.Role} key={`role-type-${RoleType.Role}`}>
|
||||||
|
Role
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.role"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Role"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="mt-2 w-1/2">
|
<div className="mt-2 w-1/2">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -36,45 +36,93 @@ enum CredentialType {
|
|||||||
Static = "static"
|
Static = "static"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RoleType {
|
||||||
|
ClusterRole = "cluster-role",
|
||||||
|
Role = "role"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthMethod {
|
||||||
|
Api = "api",
|
||||||
|
Gateway = "gateway"
|
||||||
|
}
|
||||||
|
|
||||||
const credentialTypes = [
|
const credentialTypes = [
|
||||||
{
|
{
|
||||||
label: "Static",
|
label: "Static",
|
||||||
value: CredentialType.Static
|
value: CredentialType.Static
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Dynamic",
|
||||||
|
value: CredentialType.Dynamic
|
||||||
}
|
}
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z
|
||||||
inputs: z.object({
|
.object({
|
||||||
url: z.string().url().trim().min(1),
|
inputs: z.discriminatedUnion("credentialType", [
|
||||||
clusterToken: z.string().trim().min(1),
|
z.object({
|
||||||
ca: z.string().optional(),
|
url: z.string().url().trim().min(1),
|
||||||
sslEnabled: z.boolean().default(false),
|
clusterToken: z.string().trim().optional(),
|
||||||
credentialType: z.literal(CredentialType.Static),
|
ca: z.string().optional(),
|
||||||
serviceAccountName: z.string().trim().min(1),
|
sslEnabled: z.boolean().default(false),
|
||||||
namespace: z.string().trim().min(1),
|
credentialType: z.literal(CredentialType.Static),
|
||||||
gatewayId: z.string().optional(),
|
serviceAccountName: z.string().trim().min(1),
|
||||||
audiences: z.array(z.string().trim().min(1))
|
namespace: z.string().trim().min(1),
|
||||||
}),
|
gatewayId: z.string().optional(),
|
||||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
audiences: z.array(z.string().trim().min(1)),
|
||||||
const valMs = ms(val);
|
authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api)
|
||||||
if (valMs < 60 * 1000)
|
}),
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
z.object({
|
||||||
if (valMs > 24 * 60 * 60 * 1000)
|
url: z.string().url().trim().min(1),
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
clusterToken: z.string().trim().optional(),
|
||||||
}),
|
ca: z.string().optional(),
|
||||||
maxTTL: z
|
sslEnabled: z.boolean().default(false),
|
||||||
.string()
|
credentialType: z.literal(CredentialType.Dynamic),
|
||||||
.optional()
|
namespace: z.string().trim().min(1),
|
||||||
.superRefine((val, ctx) => {
|
gatewayId: z.string().optional(),
|
||||||
if (!val) return;
|
audiences: z.array(z.string().trim().min(1)),
|
||||||
|
roleType: z.nativeEnum(RoleType),
|
||||||
|
role: z.string().trim().min(1),
|
||||||
|
authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api)
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||||
const valMs = ms(val);
|
const valMs = ms(val);
|
||||||
if (valMs < 60 * 1000)
|
if (valMs < 60 * 1000)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
if (valMs > 24 * 60 * 60 * 1000)
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
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" });
|
||||||
}),
|
}),
|
||||||
newName: slugSchema().optional()
|
maxTTL: z
|
||||||
});
|
.string()
|
||||||
|
.optional()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
newName: slugSchema().optional(),
|
||||||
|
usernameTemplate: z.string().trim().optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.inputs.authMethod === AuthMethod.Gateway && !data.inputs.gatewayId) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["inputs.gatewayId"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "When auth method is set to Gateway, a gateway must be selected"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data.inputs.authMethod === AuthMethod.Api && !data.inputs.clusterToken) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["inputs.clusterToken"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "When auth method is set to Manual Token, a cluster token must be provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
type TForm = z.infer<typeof formSchema> & FieldValues;
|
type TForm = z.infer<typeof formSchema> & FieldValues;
|
||||||
|
|
||||||
@@ -103,6 +151,7 @@ export const EditDynamicSecretKubernetesForm = ({
|
|||||||
values: {
|
values: {
|
||||||
newName: dynamicSecret.name,
|
newName: dynamicSecret.name,
|
||||||
defaultTTL: dynamicSecret.defaultTTL,
|
defaultTTL: dynamicSecret.defaultTTL,
|
||||||
|
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||||
maxTTL: dynamicSecret.maxTTL,
|
maxTTL: dynamicSecret.maxTTL,
|
||||||
inputs: dynamicSecret.inputs as TForm["inputs"]
|
inputs: dynamicSecret.inputs as TForm["inputs"]
|
||||||
}
|
}
|
||||||
@@ -110,17 +159,20 @@ export const EditDynamicSecretKubernetesForm = ({
|
|||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: "inputs.audiences" as const
|
name: "inputs.audiences"
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
const sslEnabled = watch("inputs.sslEnabled");
|
const sslEnabled = watch("inputs.sslEnabled");
|
||||||
|
const credentialType = watch("inputs.credentialType");
|
||||||
|
const authMethod = watch("inputs.authMethod");
|
||||||
|
|
||||||
const handleUpdateDynamicSecret = async (formData: TForm) => {
|
const handleUpdateDynamicSecret = async (formData: TForm) => {
|
||||||
// wait till previous request is finished
|
// wait till previous request is finished
|
||||||
if (updateDynamicSecret.isPending) return;
|
if (updateDynamicSecret.isPending) return;
|
||||||
|
const isDefaultUsernameTemplate = formData.usernameTemplate === "{{randomUsername}}";
|
||||||
try {
|
try {
|
||||||
await updateDynamicSecret.mutateAsync({
|
await updateDynamicSecret.mutateAsync({
|
||||||
name: dynamicSecret.name,
|
name: dynamicSecret.name,
|
||||||
@@ -131,9 +183,14 @@ export const EditDynamicSecretKubernetesForm = ({
|
|||||||
inputs: formData.inputs,
|
inputs: formData.inputs,
|
||||||
newName: formData.newName === dynamicSecret.name ? undefined : formData.newName,
|
newName: formData.newName === dynamicSecret.name ? undefined : formData.newName,
|
||||||
defaultTTL: formData.defaultTTL,
|
defaultTTL: formData.defaultTTL,
|
||||||
maxTTL: formData.maxTTL
|
maxTTL: formData.maxTTL,
|
||||||
|
usernameTemplate:
|
||||||
|
!formData.usernameTemplate || isDefaultUsernameTemplate
|
||||||
|
? null
|
||||||
|
: formData.usernameTemplate
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
@@ -339,17 +396,42 @@ export const EditDynamicSecretKubernetesForm = ({
|
|||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="inputs.clusterToken"
|
name="inputs.authMethod"
|
||||||
|
defaultValue={AuthMethod.Api}
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Cluster Token"
|
label="Auth Method"
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
<Input {...field} type="password" autoComplete="new-password" />
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
>
|
||||||
|
<SelectItem value={AuthMethod.Api}>Manual Token (API)</SelectItem>
|
||||||
|
<SelectItem value={AuthMethod.Gateway}>Gateway</SelectItem>
|
||||||
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{authMethod === AuthMethod.Api && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.clusterToken"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Cluster Token"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="password" autoComplete="new-password" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="inputs.credentialType"
|
name="inputs.credentialType"
|
||||||
@@ -366,12 +448,9 @@ export const EditDynamicSecretKubernetesForm = ({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(e) => field.onChange(e)}
|
onValueChange={(e) => field.onChange(e)}
|
||||||
>
|
>
|
||||||
{credentialTypes.map((credentialType) => (
|
{credentialTypes.map((ct) => (
|
||||||
<SelectItem
|
<SelectItem value={ct.value} key={`credential-type-${ct.value}`}>
|
||||||
value={credentialType.value}
|
{ct.label}
|
||||||
key={`credential-type-${credentialType.value}`}
|
|
||||||
>
|
|
||||||
{credentialType.label}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -379,21 +458,44 @@ export const EditDynamicSecretKubernetesForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex-1">
|
{credentialType === CredentialType.Static && (
|
||||||
<Controller
|
<div className="flex-1">
|
||||||
control={control}
|
<Controller
|
||||||
name="inputs.serviceAccountName"
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => (
|
name="inputs.serviceAccountName"
|
||||||
<FormControl
|
render={({ field, fieldState: { error } }) => (
|
||||||
label="Service Account Name"
|
<FormControl
|
||||||
isError={Boolean(error?.message)}
|
label="Service Account Name"
|
||||||
errorText={error?.message}
|
isError={Boolean(error?.message)}
|
||||||
>
|
errorText={error?.message}
|
||||||
<Input {...field} autoComplete="new-password" />
|
>
|
||||||
</FormControl>
|
<Input {...field} autoComplete="new-password" />
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
)}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{credentialType === CredentialType.Dynamic && (
|
||||||
|
<div className="flex-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="usernameTemplate"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Username Template"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || undefined}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -410,6 +512,58 @@ export const EditDynamicSecretKubernetesForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{credentialType === CredentialType.Dynamic && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.roleType"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Role Type"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={RoleType.ClusterRole}
|
||||||
|
key={`role-type-${RoleType.ClusterRole}`}
|
||||||
|
>
|
||||||
|
Cluster Role
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value={RoleType.Role}
|
||||||
|
key={`role-type-${RoleType.Role}`}
|
||||||
|
>
|
||||||
|
Role
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.role"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Role"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 w-1/2">
|
<div className="mt-2 w-1/2">
|
||||||
|
|||||||
Reference in New Issue
Block a user