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 handlebars from "handlebars";
|
||||
import https from "https";
|
||||
|
||||
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 { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
||||
|
||||
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;
|
||||
|
||||
@@ -15,6 +23,16 @@ type TKubernetesProviderDTO = {
|
||||
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 => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
|
||||
@@ -30,20 +48,27 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
gatewayId: string;
|
||||
targetHost: string;
|
||||
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> => {
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
|
||||
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"
|
||||
const res = await gatewayCallback("https://localhost", port);
|
||||
const res = await gatewayCallback(
|
||||
inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost",
|
||||
port,
|
||||
httpsAgent
|
||||
);
|
||||
return res;
|
||||
},
|
||||
{
|
||||
protocol: GatewayProxyProtocol.Tcp,
|
||||
protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp,
|
||||
targetHost: inputs.targetHost,
|
||||
targetPort: inputs.targetPort,
|
||||
relayHost,
|
||||
@@ -54,7 +79,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
ca: relayDetails.certChain,
|
||||
cert: relayDetails.certificate,
|
||||
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 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;
|
||||
|
||||
await axios.get(
|
||||
@@ -72,7 +264,9 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
{
|
||||
headers: {
|
||||
"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),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
@@ -85,23 +279,45 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
};
|
||||
|
||||
const url = new URL(providerInputs.url);
|
||||
const k8sGatewayHost = url.hostname;
|
||||
const k8sPort = url.port ? Number(url.port) : 443;
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
|
||||
try {
|
||||
if (providerInputs.gatewayId) {
|
||||
const k8sHost = url.hostname;
|
||||
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort
|
||||
},
|
||||
serviceAccountGetCallback
|
||||
);
|
||||
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort,
|
||||
enableSsl: providerInputs.sslEnabled,
|
||||
caCert: providerInputs.ca,
|
||||
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 {
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
await serviceAccountGetCallback(k8sHost, k8sPort);
|
||||
await serviceAccountDynamicCallback(k8sHost, k8sPort);
|
||||
}
|
||||
|
||||
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 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 res = await axios.post<TKubernetesTokenRequest>(
|
||||
@@ -134,7 +468,9 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
{
|
||||
headers: {
|
||||
"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),
|
||||
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);
|
||||
@@ -154,19 +490,46 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
const k8sPort = url.port ? Number(url.port) : 443;
|
||||
|
||||
try {
|
||||
const tokenData = providerInputs.gatewayId
|
||||
? await $gatewayProxyWrapper(
|
||||
let tokenData;
|
||||
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,
|
||||
targetHost: k8sGatewayHost,
|
||||
targetPort: k8sPort
|
||||
targetPort: k8sPort,
|
||||
enableSsl: providerInputs.sslEnabled,
|
||||
caCert: providerInputs.ca,
|
||||
reviewTokenThroughGateway: false
|
||||
},
|
||||
tokenRequestCallback
|
||||
)
|
||||
: await tokenRequestCallback(k8sHost, k8sPort);
|
||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||
? tokenRequestStaticCallback
|
||||
: serviceAccountDynamicCallback
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tokenData =
|
||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||
? await tokenRequestStaticCallback(k8sHost, k8sPort)
|
||||
: await serviceAccountDynamicCallback(k8sHost, k8sPort);
|
||||
}
|
||||
|
||||
return {
|
||||
entityId: providerInputs.serviceAccountName,
|
||||
entityId: tokenData.serviceAccountName,
|
||||
data: { TOKEN: tokenData.status.token }
|
||||
};
|
||||
} 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 };
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,18 @@ export enum LdapCredentialType {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -282,17 +293,50 @@ export const LdapSchema = z.union([
|
||||
})
|
||||
]);
|
||||
|
||||
export const DynamicSecretKubernetesSchema = z.object({
|
||||
url: z.string().url().trim().min(1),
|
||||
gatewayId: z.string().nullable().optional(),
|
||||
sslEnabled: z.boolean().default(true),
|
||||
clusterToken: z.string().trim().min(1),
|
||||
ca: z.string().optional(),
|
||||
serviceAccountName: z.string().trim().min(1),
|
||||
credentialType: z.literal(KubernetesCredentialType.Static),
|
||||
namespace: z.string().trim().min(1),
|
||||
audiences: z.array(z.string().trim().min(1))
|
||||
});
|
||||
export const DynamicSecretKubernetesSchema = z
|
||||
.discriminatedUnion("credentialType", [
|
||||
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.Static),
|
||||
serviceAccountName: 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({
|
||||
host: z.string().trim().toLowerCase(),
|
||||
|
||||
@@ -267,17 +267,32 @@ export type TDynamicSecretProvider =
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.Kubernetes;
|
||||
inputs: {
|
||||
url: string;
|
||||
clusterToken: string;
|
||||
ca?: string;
|
||||
serviceAccountName: string;
|
||||
credentialType: "dynamic" | "static";
|
||||
namespace: string;
|
||||
gatewayId?: string;
|
||||
sslEnabled: boolean;
|
||||
audiences: string[];
|
||||
};
|
||||
inputs:
|
||||
| {
|
||||
url: string;
|
||||
clusterToken?: string;
|
||||
ca?: string;
|
||||
serviceAccountName: string;
|
||||
credentialType: "static";
|
||||
namespace: string;
|
||||
gatewayId?: 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;
|
||||
|
||||
@@ -38,46 +38,94 @@ enum CredentialType {
|
||||
Static = "static"
|
||||
}
|
||||
|
||||
enum RoleType {
|
||||
ClusterRole = "cluster-role",
|
||||
Role = "role"
|
||||
}
|
||||
|
||||
export enum AuthMethod {
|
||||
Api = "api",
|
||||
Gateway = "gateway"
|
||||
}
|
||||
|
||||
const credentialTypes = [
|
||||
{
|
||||
label: "Static",
|
||||
value: CredentialType.Static
|
||||
},
|
||||
{
|
||||
label: "Dynamic",
|
||||
value: CredentialType.Dynamic
|
||||
}
|
||||
] as const;
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
url: z.string().url().trim().min(1),
|
||||
clusterToken: z.string().trim().min(1),
|
||||
ca: z.string().optional(),
|
||||
sslEnabled: z.boolean().default(false),
|
||||
credentialType: z.literal(CredentialType.Static),
|
||||
serviceAccountName: z.string().trim().min(1),
|
||||
namespace: z.string().trim().min(1),
|
||||
gatewayId: z.string().optional(),
|
||||
audiences: z.array(z.string().trim().min(1))
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
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" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const formSchema = z
|
||||
.object({
|
||||
provider: z.discriminatedUnion("credentialType", [
|
||||
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(CredentialType.Static),
|
||||
serviceAccountName: 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(AuthMethod).default(AuthMethod.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(CredentialType.Dynamic),
|
||||
namespace: z.string().trim().min(1),
|
||||
gatewayId: z.string().optional(),
|
||||
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);
|
||||
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() })
|
||||
});
|
||||
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" });
|
||||
}),
|
||||
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;
|
||||
|
||||
@@ -115,8 +163,9 @@ export const KubernetesInputForm = ({
|
||||
namespace: "",
|
||||
credentialType: CredentialType.Static,
|
||||
gatewayId: undefined,
|
||||
audiences: []
|
||||
},
|
||||
audiences: [],
|
||||
authMethod: AuthMethod.Api
|
||||
} as const,
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
@@ -130,12 +179,16 @@ export const KubernetesInputForm = ({
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const sslEnabled = watch("provider.sslEnabled");
|
||||
const credentialType = watch("provider.credentialType");
|
||||
const authMethod = watch("provider.authMethod");
|
||||
|
||||
const handleCreateDynamicSecret = async (formData: TForm) => {
|
||||
const { provider, ...rest } = formData;
|
||||
const { provider, usernameTemplate, ...rest } = formData;
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
try {
|
||||
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.Kubernetes, inputs: provider },
|
||||
maxTTL: rest.maxTTL,
|
||||
@@ -143,7 +196,9 @@ export const KubernetesInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL: rest.defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: rest.environment.slug
|
||||
environmentSlug: rest.environment.slug,
|
||||
usernameTemplate:
|
||||
!usernameTemplate || isDefaultUsernameTemplate ? undefined : usernameTemplate
|
||||
});
|
||||
|
||||
onCompleted();
|
||||
@@ -343,20 +398,44 @@ export const KubernetesInputForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.clusterToken"
|
||||
name="provider.authMethod"
|
||||
defaultValue={AuthMethod.Api}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Cluster Token"
|
||||
label="Auth Method"
|
||||
isError={Boolean(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>
|
||||
)}
|
||||
/>
|
||||
{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
|
||||
control={control}
|
||||
name="provider.credentialType"
|
||||
@@ -373,12 +452,9 @@ export const KubernetesInputForm = ({
|
||||
className="w-full"
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
>
|
||||
{credentialTypes.map((credentialType) => (
|
||||
<SelectItem
|
||||
value={credentialType.value}
|
||||
key={`credential-type-${credentialType.value}`}
|
||||
>
|
||||
{credentialType.label}
|
||||
{credentialTypes.map((ct) => (
|
||||
<SelectItem value={ct.value} key={`credential-type-${ct.value}`}>
|
||||
{ct.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -386,21 +462,45 @@ export const KubernetesInputForm = ({
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.serviceAccountName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Service Account Name"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{credentialType === CredentialType.Static && (
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.serviceAccountName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Service Account Name"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</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">
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -417,6 +517,56 @@ export const KubernetesInputForm = ({
|
||||
/>
|
||||
</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">
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
@@ -36,45 +36,93 @@ enum CredentialType {
|
||||
Static = "static"
|
||||
}
|
||||
|
||||
enum RoleType {
|
||||
ClusterRole = "cluster-role",
|
||||
Role = "role"
|
||||
}
|
||||
|
||||
enum AuthMethod {
|
||||
Api = "api",
|
||||
Gateway = "gateway"
|
||||
}
|
||||
|
||||
const credentialTypes = [
|
||||
{
|
||||
label: "Static",
|
||||
value: CredentialType.Static
|
||||
},
|
||||
{
|
||||
label: "Dynamic",
|
||||
value: CredentialType.Dynamic
|
||||
}
|
||||
] as const;
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z.object({
|
||||
url: z.string().url().trim().min(1),
|
||||
clusterToken: z.string().trim().min(1),
|
||||
ca: z.string().optional(),
|
||||
sslEnabled: z.boolean().default(false),
|
||||
credentialType: z.literal(CredentialType.Static),
|
||||
serviceAccountName: z.string().trim().min(1),
|
||||
namespace: z.string().trim().min(1),
|
||||
gatewayId: z.string().optional(),
|
||||
audiences: z.array(z.string().trim().min(1))
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
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" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const formSchema = z
|
||||
.object({
|
||||
inputs: z.discriminatedUnion("credentialType", [
|
||||
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(CredentialType.Static),
|
||||
serviceAccountName: 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(AuthMethod).default(AuthMethod.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(CredentialType.Dynamic),
|
||||
namespace: z.string().trim().min(1),
|
||||
gatewayId: z.string().optional(),
|
||||
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);
|
||||
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()
|
||||
});
|
||||
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;
|
||||
|
||||
@@ -103,6 +151,7 @@ export const EditDynamicSecretKubernetesForm = ({
|
||||
values: {
|
||||
newName: dynamicSecret.name,
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
inputs: dynamicSecret.inputs as TForm["inputs"]
|
||||
}
|
||||
@@ -110,17 +159,20 @@ export const EditDynamicSecretKubernetesForm = ({
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "inputs.audiences" as const
|
||||
name: "inputs.audiences"
|
||||
});
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const sslEnabled = watch("inputs.sslEnabled");
|
||||
const credentialType = watch("inputs.credentialType");
|
||||
const authMethod = watch("inputs.authMethod");
|
||||
|
||||
const handleUpdateDynamicSecret = async (formData: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isPending) return;
|
||||
const isDefaultUsernameTemplate = formData.usernameTemplate === "{{randomUsername}}";
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
@@ -131,9 +183,14 @@ export const EditDynamicSecretKubernetesForm = ({
|
||||
inputs: formData.inputs,
|
||||
newName: formData.newName === dynamicSecret.name ? undefined : formData.newName,
|
||||
defaultTTL: formData.defaultTTL,
|
||||
maxTTL: formData.maxTTL
|
||||
maxTTL: formData.maxTTL,
|
||||
usernameTemplate:
|
||||
!formData.usernameTemplate || isDefaultUsernameTemplate
|
||||
? null
|
||||
: formData.usernameTemplate
|
||||
}
|
||||
});
|
||||
|
||||
onClose();
|
||||
createNotification({
|
||||
type: "success",
|
||||
@@ -339,17 +396,42 @@ export const EditDynamicSecretKubernetesForm = ({
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.clusterToken"
|
||||
name="inputs.authMethod"
|
||||
defaultValue={AuthMethod.Api}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Cluster Token"
|
||||
label="Auth Method"
|
||||
isError={Boolean(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>
|
||||
)}
|
||||
/>
|
||||
{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
|
||||
control={control}
|
||||
name="inputs.credentialType"
|
||||
@@ -366,12 +448,9 @@ export const EditDynamicSecretKubernetesForm = ({
|
||||
className="w-full"
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
>
|
||||
{credentialTypes.map((credentialType) => (
|
||||
<SelectItem
|
||||
value={credentialType.value}
|
||||
key={`credential-type-${credentialType.value}`}
|
||||
>
|
||||
{credentialType.label}
|
||||
{credentialTypes.map((ct) => (
|
||||
<SelectItem value={ct.value} key={`credential-type-${ct.value}`}>
|
||||
{ct.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -379,21 +458,44 @@ export const EditDynamicSecretKubernetesForm = ({
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.serviceAccountName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Service Account Name"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{credentialType === CredentialType.Static && (
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.serviceAccountName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Service Account Name"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</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">
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -410,6 +512,58 @@ export const EditDynamicSecretKubernetesForm = ({
|
||||
/>
|
||||
</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 className="mt-2 w-1/2">
|
||||
|
||||
Reference in New Issue
Block a user