diff --git a/.env.example b/.env.example index 82b401928..23a0b8be0 100644 --- a/.env.example +++ b/.env.example @@ -26,7 +26,8 @@ SITE_URL=http://localhost:8080 # Mail/SMTP SMTP_HOST= SMTP_PORT= -SMTP_NAME= +SMTP_FROM_ADDRESS= +SMTP_FROM_NAME= SMTP_USERNAME= SMTP_PASSWORD= @@ -107,4 +108,4 @@ INF_APP_CONNECTION_GITHUB_APP_SLUG= INF_APP_CONNECTION_GITHUB_APP_ID= #gcp app -INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= \ No newline at end of file +INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts index 9199c21f1..414f8534c 100644 --- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -79,44 +79,44 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().trim().describe(AWS_AUTH.ATTACH.identityId) }), - body: z.object({ - stsEndpoint: z - .string() - .trim() - .min(1) - .default("https://sts.amazonaws.com/") - .describe(AWS_AUTH.ATTACH.stsEndpoint), - allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.ATTACH.allowedPrincipalArns), - allowedAccountIds: validateAccountIds.describe(AWS_AUTH.ATTACH.allowedAccountIds), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(AWS_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(AWS_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(AWS_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AWS_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + stsEndpoint: z + .string() + .trim() + .min(1) + .default("https://sts.amazonaws.com/") + .describe(AWS_AUTH.ATTACH.stsEndpoint), + allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.ATTACH.allowedPrincipalArns), + allowedAccountIds: validateAccountIds.describe(AWS_AUTH.ATTACH.allowedAccountIds), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(AWS_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(AWS_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(1) + .max(315360000) + .default(2592000) + .describe(AWS_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AWS_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityAwsAuth: IdentityAwsAuthsSchema @@ -172,30 +172,33 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().describe(AWS_AUTH.UPDATE.identityId) }), - body: z.object({ - stsEndpoint: z.string().trim().min(1).optional().describe(AWS_AUTH.UPDATE.stsEndpoint), - allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.UPDATE.allowedPrincipalArns), - allowedAccountIds: validateAccountIds.describe(AWS_AUTH.UPDATE.allowedAccountIds), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(AWS_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AWS_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AWS_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(AWS_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + stsEndpoint: z.string().trim().min(1).optional().describe(AWS_AUTH.UPDATE.stsEndpoint), + allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.UPDATE.allowedPrincipalArns), + allowedAccountIds: validateAccountIds.describe(AWS_AUTH.UPDATE.allowedAccountIds), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(AWS_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AWS_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AWS_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .min(0) + .optional() + .describe(AWS_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityAwsAuth: IdentityAwsAuthsSchema diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts index 6aee4504f..f46fb57ca 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -76,39 +76,44 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider params: z.object({ identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId) }), - body: z.object({ - tenantId: z.string().trim().describe(AZURE_AUTH.ATTACH.tenantId), - resource: z.string().trim().describe(AZURE_AUTH.ATTACH.resource), - allowedServicePrincipalIds: validateAzureAuthField.describe(AZURE_AUTH.ATTACH.allowedServicePrincipalIds), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(AZURE_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(AZURE_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(AZURE_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AZURE_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + tenantId: z.string().trim().describe(AZURE_AUTH.ATTACH.tenantId), + resource: z.string().trim().describe(AZURE_AUTH.ATTACH.resource), + allowedServicePrincipalIds: validateAzureAuthField.describe(AZURE_AUTH.ATTACH.allowedServicePrincipalIds), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(AZURE_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(AZURE_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(AZURE_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(AZURE_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityAzureAuth: IdentityAzureAuthsSchema @@ -163,32 +168,40 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider params: z.object({ identityId: z.string().trim().describe(AZURE_AUTH.UPDATE.identityId) }), - body: z.object({ - tenantId: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.tenantId), - resource: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.resource), - allowedServicePrincipalIds: validateAzureAuthField - .optional() - .describe(AZURE_AUTH.UPDATE.allowedServicePrincipalIds), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(AZURE_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AZURE_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AZURE_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(AZURE_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + tenantId: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.tenantId), + resource: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.resource), + allowedServicePrincipalIds: validateAzureAuthField + .optional() + .describe(AZURE_AUTH.UPDATE.allowedServicePrincipalIds), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(AZURE_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AZURE_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(AZURE_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .min(0) + .optional() + .describe(AZURE_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityAzureAuth: IdentityAzureAuthsSchema diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts index 88c5af45f..057458bb2 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -74,40 +74,40 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().trim().describe(GCP_AUTH.ATTACH.identityId) }), - body: z.object({ - type: z.enum(["iam", "gce"]), - allowedServiceAccounts: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedServiceAccounts), - allowedProjects: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedProjects), - allowedZones: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedZones), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(GCP_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(GCP_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(GCP_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(GCP_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + type: z.enum(["iam", "gce"]), + allowedServiceAccounts: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedServiceAccounts), + allowedProjects: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedProjects), + allowedZones: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedZones), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(GCP_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(GCP_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(GCP_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(GCP_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityGcpAuth: IdentityGcpAuthsSchema @@ -164,31 +164,34 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().trim().describe(GCP_AUTH.UPDATE.identityId) }), - body: z.object({ - type: z.enum(["iam", "gce"]).optional(), - allowedServiceAccounts: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedServiceAccounts), - allowedProjects: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedProjects), - allowedZones: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedZones), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(GCP_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(GCP_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(GCP_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(GCP_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + type: z.enum(["iam", "gce"]).optional(), + allowedServiceAccounts: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedServiceAccounts), + allowedProjects: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedProjects), + allowedZones: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedZones), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(GCP_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(GCP_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(GCP_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(GCP_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityGcpAuth: IdentityGcpAuthsSchema diff --git a/backend/src/server/routes/v1/identity-jwt-auth-router.ts b/backend/src/server/routes/v1/identity-jwt-auth-router.ts index d60bb969d..2950fc72d 100644 --- a/backend/src/server/routes/v1/identity-jwt-auth-router.ts +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -34,23 +34,12 @@ const CreateBaseSchema = z.object({ .min(1) .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) .describe(JWT_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(JWT_AUTH.ATTACH.accessTokenTTL), + accessTokenTTL: z.number().int().min(0).max(315360000).default(2592000).describe(JWT_AUTH.ATTACH.accessTokenTTL), accessTokenMaxTTL: z .number() .int() + .min(0) .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) .default(2592000) .describe(JWT_AUTH.ATTACH.accessTokenMaxTTL), accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.ATTACH.accessTokenNumUsesLimit) @@ -70,23 +59,12 @@ const UpdateBaseSchema = z .min(1) .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) .describe(JWT_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(JWT_AUTH.UPDATE.accessTokenTTL), + accessTokenTTL: z.number().int().min(0).max(315360000).default(2592000).describe(JWT_AUTH.UPDATE.accessTokenTTL), accessTokenMaxTTL: z .number() .int() + .min(0) .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) .default(2592000) .describe(JWT_AUTH.UPDATE.accessTokenMaxTTL), accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.UPDATE.accessTokenNumUsesLimit) diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 3a71ba7a2..3b3025179 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -87,47 +87,47 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide params: z.object({ identityId: z.string().trim().describe(KUBERNETES_AUTH.ATTACH.identityId) }), - body: z.object({ - kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost), - caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert), - tokenReviewerJwt: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt), - allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation - allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames), - allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(KUBERNETES_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(KUBERNETES_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(KUBERNETES_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z - .number() - .int() - .min(0) - .default(0) - .describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost), + caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert), + tokenReviewerJwt: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt), + allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation + allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames), + allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityKubernetesAuth: IdentityKubernetesAuthResponseSchema @@ -183,44 +183,47 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide params: z.object({ identityId: z.string().describe(KUBERNETES_AUTH.UPDATE.identityId) }), - body: z.object({ - kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost), - caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert), - tokenReviewerJwt: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt), - allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation - allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames), - allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(KUBERNETES_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(0) - .max(315360000) - .optional() - .describe(KUBERNETES_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z - .number() - .int() - .min(0) - .optional() - .describe(KUBERNETES_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(KUBERNETES_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost), + caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert), + tokenReviewerJwt: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt), + allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation + allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames), + allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityKubernetesAuth: IdentityKubernetesAuthResponseSchema diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index 280dbc5d5..431ed3f4f 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -87,42 +87,42 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().trim().describe(OIDC_AUTH.ATTACH.identityId) }), - body: z.object({ - oidcDiscoveryUrl: z.string().url().min(1).describe(OIDC_AUTH.ATTACH.oidcDiscoveryUrl), - caCert: z.string().trim().default("").describe(OIDC_AUTH.ATTACH.caCert), - boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer), - boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences), - boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims), - boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(OIDC_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(OIDC_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(OIDC_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + oidcDiscoveryUrl: z.string().url().min(1).describe(OIDC_AUTH.ATTACH.oidcDiscoveryUrl), + caCert: z.string().trim().default("").describe(OIDC_AUTH.ATTACH.caCert), + boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer), + boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences), + boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims), + boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(OIDC_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(OIDC_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(OIDC_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityOidcAuth: IdentityOidcAuthResponseSchema @@ -202,26 +202,24 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) accessTokenTTL: z .number() .int() - .min(1) + .min(0) .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) .default(2592000) .describe(OIDC_AUTH.UPDATE.accessTokenTTL), accessTokenMaxTTL: z .number() .int() + .min(0) .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) .default(2592000) .describe(OIDC_AUTH.UPDATE.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.UPDATE.accessTokenNumUsesLimit) }) - .partial(), + .partial() + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityOidcAuth: IdentityOidcAuthResponseSchema diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index f367e6033..3d331403a 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -26,36 +26,41 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider params: z.object({ identityId: z.string().trim().describe(TOKEN_AUTH.ATTACH.identityId) }), - body: z.object({ - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(TOKEN_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(TOKEN_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(TOKEN_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(TOKEN_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(TOKEN_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(TOKEN_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(TOKEN_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(TOKEN_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityTokenAuth: IdentityTokenAuthsSchema @@ -110,27 +115,35 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider params: z.object({ identityId: z.string().trim().describe(TOKEN_AUTH.UPDATE.identityId) }), - body: z.object({ - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(TOKEN_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(TOKEN_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(TOKEN_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(TOKEN_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(TOKEN_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(TOKEN_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(TOKEN_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(TOKEN_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityTokenAuth: IdentityTokenAuthsSchema diff --git a/backend/src/server/routes/v1/identity-universal-auth-router.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts index f103a39e0..e48e1f442 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -86,49 +86,49 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { params: z.object({ identityId: z.string().trim().describe(UNIVERSAL_AUTH.ATTACH.identityId) }), - body: z.object({ - clientSecretTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(UNIVERSAL_AUTH.ATTACH.clientSecretTrustedIps), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTTL), // 30 days - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(UNIVERSAL_AUTH.ATTACH.accessTokenMaxTTL), // 30 days - accessTokenNumUsesLimit: z - .number() - .int() - .min(0) - .default(0) - .describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + clientSecretTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(UNIVERSAL_AUTH.ATTACH.clientSecretTrustedIps), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTTL), // 30 days + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenMaxTTL), // 30 days + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityUniversalAuth: IdentityUniversalAuthsSchema @@ -181,46 +181,49 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { params: z.object({ identityId: z.string().describe(UNIVERSAL_AUTH.UPDATE.identityId) }), - body: z.object({ - clientSecretTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(UNIVERSAL_AUTH.UPDATE.clientSecretTrustedIps), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(0) - .max(315360000) - .optional() - .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z - .number() - .int() - .min(0) - .optional() - .describe(UNIVERSAL_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + clientSecretTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.clientSecretTrustedIps), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityUniversalAuth: IdentityUniversalAuthsSchema diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index 9f791fd74..ff202f225 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -126,12 +126,12 @@ export const identityAwsAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityAwsAuth, identityAccessToken, identityMembershipOrg }; diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 6275aa0fa..01d013734 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -99,12 +99,12 @@ export const identityAzureAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityAzureAuth, identityAccessToken, identityMembershipOrg }; diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index a81b0cd01..5e404ca20 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -138,12 +138,12 @@ export const identityGcpAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityGcpAuth, identityAccessToken, identityMembershipOrg }; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index 5f8fc5ff6..6757b0b84 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -212,12 +212,12 @@ export const identityJwtAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg }; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index b62f3e8f5..4508a255d 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -229,12 +229,12 @@ export const identityKubernetesAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityKubernetesAuth, identityAccessToken, identityMembershipOrg }; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index dc3b1baa3..a1dbed46b 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -194,12 +194,12 @@ export const identityOidcAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg }; diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index 847030d76..bf38c5fa1 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -328,12 +328,12 @@ export const identityTokenAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg }; diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index b456c1647..b9837265a 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -129,12 +129,12 @@ export const identityUaServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg }; diff --git a/docker-swarm/.env-example b/docker-swarm/.env-example index 03d05a08e..a30e3bba6 100644 --- a/docker-swarm/.env-example +++ b/docker-swarm/.env-example @@ -20,7 +20,8 @@ SITE_URL=http://localhost:8080 # Mail/SMTP SMTP_HOST= SMTP_PORT= -SMTP_NAME= +SMTP_FROM_ADDRESS= +SMTP_FROM_NAME= SMTP_USERNAME= SMTP_PASSWORD= diff --git a/docs/documentation/guides/node.mdx b/docs/documentation/guides/node.mdx index d1b8fe5e8..9da99441a 100644 --- a/docs/documentation/guides/node.mdx +++ b/docs/documentation/guides/node.mdx @@ -5,7 +5,7 @@ title: "Node" This guide demonstrates how to use Infisical to manage secrets for your Node stack from local development to production. It uses: - Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets. -- The [@infisical/sdk](https://github.com/Infisical/sdk/tree/main/languages/node) Node.js client SDK to fetch secrets back to your Node application on demand. +- The [@infisical/sdk](https://github.com/Infisical/node-sdk-v2) Node.js client SDK to fetch secrets back to your Node application on demand. ## Project Setup @@ -46,43 +46,57 @@ Finally, create an index.js file containing the application code. ```js const express = require('express'); -const { InfisicalClient } = require("@infisical/sdk"); +const { InfisicalSDK } = require("@infisical/sdk"); + const app = express(); const PORT = 3000; -const client = new InfisicalClient({ - auth: { - universalAuth: { - clientId: "YOUR_CLIENT_ID", - clientSecret: "YOUR_CLIENT_SECRET", - } - } -}); +let client; + +const setupClient = () => { + + if (client) { + return; + } + + const infisicalSdk = new InfisicalSDK({ + siteUrl: "your-infisical-instance.com" // Optional, defaults to https://app.infisical.com + }); + + await infisicalSdk.auth().universalAuth.login({ + clientId: "", + clientSecret: "" + }); + + // If authentication was successful, assign the client + client = infisicalSdk; +} + + app.get("/", async (req, res) => { - // access value + - const name = await client.getSecret({ - environment: "dev", - projectId: "PROJECT_ID", - path: "/", - type: "shared", - secretName: "NAME" + const name = await client.secrets().getSecret({ + environment: "dev", // dev, staging, prod, etc. + projectId: "", + secretPath: "/", + secretName: "NAME" }); - + res.send(`Hello! My name is: ${name.secretValue}`); }); app.listen(PORT, async () => { - // initialize client - - console.log(`App listening on port ${PORT}`); + // initialize http server and Infisical + await setupClient(); + console.log(`Server listening on port ${PORT}`); }); ``` -Here, we initialized a `client` instance of the Infisical Node SDK with the Infisical Token +Here, we initialized a `client` instance of the Infisical Node SDK with the [Machine Identity](/documentation/platform/identities/overview) that we created earlier, giving access to the secrets in the development environment of the project in Infisical that we created earlier. @@ -94,16 +108,12 @@ node index.js The client fetched the secret with the key `NAME` from Infisical that we returned in the response of the endpoint. -At this stage, you know how to fetch secrets from Infisical back to your Node application. By using Infisical Tokens scoped to different environments, you can easily manage secrets across various stages of your project in Infisical, from local development to production. +At this stage, you know how to fetch secrets from Infisical back to your Node application. +By using Machine Identities scoped to different projects and environments, you can easily manage secrets across various stages of your project in Infisical, from local development to production. ## FAQ - - The client SDK caches every secret and implements a 5-minute waiting period before - re-requesting it. The waiting period can be controlled by setting the `cacheTTL` parameter at - the time of initializing the client. - The SDK caches every secret and falls back to the cached value if a request fails. If no cached value ever-existed, the SDK falls back to whatever value is on `process.env`. @@ -124,4 +134,4 @@ At this stage, you know how to fetch secrets from Infisical back to your Node ap See also: -- Explore the [Node SDK](https://github.com/Infisical/sdk/tree/main/languages/node) +- Explore the [Node SDK](https://github.com/Infisical/node-sdk-v2) diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx index df0730d87..32385e66e 100644 --- a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -26,15 +26,15 @@ spec: name: namespace: - managedSecretReference: - secretName: managed-secret - secretNamespace: default - creationPolicy: "Orphan" - template: - includeAllSecrets: true - data: - NEW_KEY_NAME: "{{ .KEY.SecretPath }} {{ .KEY.Value }}" - KEY_WITH_BINARY_VALUE: "{{ .KEY.SecretPath }} {{ .KEY.Value }}" + managedKubeSecretReferences: + - secretName: managed-secret + secretNamespace: default + creationPolicy: "Orphan" + template: + includeAllSecrets: true + data: + NEW_KEY_NAME: "{{ .KEY.SecretPath }} {{ .KEY.Value }}" + KEY_WITH_BINARY_VALUE: "{{ .KEY.SecretPath }} {{ .KEY.Value }}" ``` ## CRD properties @@ -541,18 +541,32 @@ The managed secret properties specify where to store the secrets retrieved from This includes defining the name and namespace of the Kubernetes secret that will hold these secrets. The Infisical operator will automatically create the Kubernetes secret in the specified name/namespace and ensure it stays up-to-date. - + + +The `managedSecretReference` field is deprecated and will be removed in a future release. +Replace it with `managedKubeSecretReferences`, which now accepts an array of references to support multiple managed secrets in a single InfisicalSecret CRD. + +Example: +```yaml + managedKubeSecretReferences: + - secretName: managed-secret + secretNamespace: default + creationPolicy: "Orphan" +``` + + + - + The name of the managed Kubernetes secret to be created - + The namespace of the managed Kubernetes secret to be created. - + Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. - + Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. @@ -573,18 +587,18 @@ This is useful for tools such as ArgoCD, where every resource requires an owner Fetching secrets from Infisical as is via the operator may not be enough. This is where templating functionality may be helpful. Using Go templates, you can format, combine, and create new key-value pairs from secrets fetched from Infisical before storing them as Kubernetes Secrets. - + - + This property controls what secrets are included in your managed secret when using templates. When set to `true`, all secrets fetched from your Infisical project will be added into your managed Kubernetes secret resource. **Use this option when you would like to sync all secrets from Infisical to Kubernetes but want to template a subset of them.** -When set to `false`, only secrets defined in the `managedSecretReference.template.data` field of the template will be included in the managed secret. +When set to `false`, only secrets defined in the `managedKubeSecretReferences[].template.data` field of the template will be included in the managed secret. Use this option when you would like to sync **only** a subset of secrets from Infisical to Kubernetes. - + Define secret keys and their corresponding templates. Each data value uses a Golang template with access to all secrets retrieved from the specified scope. @@ -600,16 +614,16 @@ type TemplateSecret struct { #### Example template configuration: ```yaml -managedSecretReference: - secretName: managed-secret - secretNamespace: default - template: - includeAllSecrets: true - data: - # Create new secret key that doesn't exist in your Infisical project using values of other secrets - NEW_KEY: "{{ .DB_PASSWORD.Value }}" - # Override an existing secret key in Infisical project with a new value using values of other secrets - API_URL: "https://api.{{.COMPANY_NAME.Value}}.{{.REGION.Value}}.com" +managedKubeSecretReferences: + - secretName: managed-secret + secretNamespace: default + template: + includeAllSecrets: true + data: + # Create new secret key that doesn't exist in your Infisical project using values of other secrets + NEW_KEY: "{{ .DB_PASSWORD.Value }}" + # Override an existing secret key in Infisical project with a new value using values of other secrets + API_URL: "https://api.{{.COMPANY_NAME.Value}}.{{.REGION.Value}}.com" ``` For this example, let's assume the following secrets exist in your Infisical project: @@ -652,13 +666,13 @@ The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a bas The resulting managed secret will contain the decoded value of `BINARY_KEY_BASE64`. ```yaml -managedSecretReference: -secretName: managed-secret -secretNamespace: default -template: - includeAllSecrets: true - data: - BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}" + managedKubeSecretReferences: + secretName: managed-secret + secretNamespace: default + template: + includeAllSecrets: true + data: + BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}" ``` @@ -913,7 +927,7 @@ spec: .. authentication: ... - managedSecretReference: + managedKubeSecretReferences: ... ``` @@ -934,4 +948,4 @@ metadata: type: Opaque ``` - + \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index b4ad1f3d1..7a0952c2c 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -301,7 +301,8 @@ "group": "Reference architectures", "pages": [ "self-hosting/reference-architectures/aws-ecs", - "self-hosting/reference-architectures/linux-deployment-ha" + "self-hosting/reference-architectures/linux-deployment-ha", + "self-hosting/reference-architectures/on-prem-k8s-ha" ] }, "self-hosting/ee", diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx index a58be0688..4a047d4e2 100644 --- a/docs/sdks/overview.mdx +++ b/docs/sdks/overview.mdx @@ -34,12 +34,6 @@ From local development to production, Infisical SDKs provide the easiest way for ## FAQ - - The client SDK caches every secret and implements a 5-minute waiting period before re-requesting it. The waiting period can be controlled by - setting the `cacheTTL` parameter at the time of initializing the client. - - Note: The exact parameter name may differ depending on the language. - The SDK caches every secret and falls back to the cached value if a request fails. If no cached value ever-existed, the SDK falls back to whatever value is on the process environment. diff --git a/docs/self-hosting/reference-architectures/on-prem-k8s-ha.mdx b/docs/self-hosting/reference-architectures/on-prem-k8s-ha.mdx new file mode 100644 index 000000000..6dc8ce574 --- /dev/null +++ b/docs/self-hosting/reference-architectures/on-prem-k8s-ha.mdx @@ -0,0 +1,231 @@ +--- +title: "Kubernetes (HA)" +description: "Reference architecture for self-hosting Infisical on Kubernetes (HA)" +--- +Deploying Infisical on-premise with high availability requires expertise in networking, container orchestration, and database management. +This guide serves as a reference architecture and a starting point. Actual deployments may vary depending on your organization's existing infrastructure and capabilities. + + +## Architecture Overview +{/* ![On premise architecture](/images/self-hosting/reference-architectures/on-premise-architecture.png) */} +```mermaid +flowchart TB + subgraph GLB["Global LB (HAProxy/NGINX)"] + end + + subgraph OS["Object Storage"] + direction LR + store["S3/MinIO/Enterprise Storage"] + subgraph store_contents["Storage Contents"] + wal["PostgreSQL WAL"] + pgbackup["PostgreSQL Backups"] + redisbackup["Redis Backups"] + end + end + + subgraph DC1["Active Data Center"] + direction TB + subgraph k8s1["Kubernetes Cluster"] + ing1["Ingress Controller"] + app1["Infisical Deployment"] + + subgraph db1["CloudNativePG"] + pg1p["PostgreSQL Primary"] + pg1r["PostgreSQL Replicas"] + end + + subgraph red1["Redis (Bitnami)"] + rp1["Redis Primary"] + end + end + end + + subgraph DC2["Passive Data Center"] + direction TB + subgraph k8s2["Kubernetes Cluster"] + ing2["Ingress Controller"] + app2["Infisical Deployment"] + + subgraph db2["CloudNativePG"] + pg2["PostgreSQL Replicas"] + end + + subgraph red2["Redis (Bitnami)"] + r2["Redis Standby"] + end + end + end + + %% Connections + GLB --> ing1 + GLB -.-> ing2 + + %% Database connections + pg1p --> store + store --> pg2 + + %% Redis backup flow + rp1 --> store + store -.-> r2 + + %% Intra-DC connections + ing1 --> app1 + app1 --> db1 + app1 --> red1 + + ing2 --> app2 + app2 --> db2 + app2 --> red2 + + classDef primary fill:#f96,stroke:#333 + classDef replica fill:#69f,stroke:#333 + classDef storage fill:#9c6,stroke:#333 + classDef lb fill:#c9f,stroke:#333 + + class pg1p,rp1 primary + class pg1r,pg2,r2 replica + class store,wal,pgbackup,redisbackup storage + class GLB,ing1,ing2 lb +``` +The architecture above makes use of Kubernetes for orchestrating both stateless and stateful components. +The architecture spans multiple data centers for increased redundancy, availability and disaster recovery capabilities using an active-passive configuration. + +### Stateful vs stateless workloads +While managing databases within Kubernetes has typically been complex, modern operators like [CloudNativePG](https://cloudnative-pg.io/) simplify this process by handling storage provisioning, persistent volume management, and backup/recovery processes. +However, if you lack deep expertise in Kubernetes operators or database management, we recommend a hybrid approach where the database is on a managed service for production deployments. + + + Managing stateful components like databases can be challenging without deep expertise or a dedicated in-house database management team. + To simplify operations and reduce complexity, we recommend offloading databases to managed services from AWS/GCP. + These managed services automatically handle provisioning, scaling, failover, backups and rollbacks. + + +## Core Components +### Kubernetes Cluster +Infisical is deployed on a Kubernetes cluster, which allows for container management, auto-scaling, and self-healing capabilities. +A load balancer sits in front of the Kubernetes cluster, directing traffic and making sure there is an even load distribution across the application nodes. +This is the entry point where all other services will interact with Infisical. + +### Object Storage +The architecture requires S3-compatible object storage for database backups and cross-datacenter replication. This can be provided by: +- Existing enterprise object storage solution +- Dedicated MinIO deployment +- In-cluster MinIO deployment if neither option above is available + +The object storage must be accessible from all Kubernetes clusters and provides: +- Storage for PostgreSQL WAL archiving and backups +- Storage for Redis backups + +### CloudNativePG for High Availability PostgreSQL +The database layer is powered by PostgreSQL, managed by CloudNativePG operator for high availability: +- **Redundancy:** CloudNativePG manages a primary-replica setup where the primary handles write operations and replicas handle read operations +- **Failover:** The operator automatically handles failover within a cluster by promoting a replica to primary when needed +- **Backup and Recovery:** Built-in support for backup to S3-compatible storage with point-in-time recovery capabilities + +### Redis High Availability +Redis is deployed using the [Bitnami Helm chart](https://github.com/bitnami/charts/tree/main/bitnami/redis) in a simple primary configuration: +- Single Redis instance per cluster without streaming replication +- Regular backups to object storage +- Restore from backup during failover + + +Infisical does not support Redis cluster mode, and since this is an active-passive setup, we use a simple Redis deployment with backup/restore for failover. + + +#### PostgreSQL Backup and Restore +PostgreSQL is the single source of truth for nearly all application data on Infisical. + +CloudNativePG provides well defined backup and restore capabilities: +- **Continuous Backup:** The operator continuously archives WAL files to object storage +- **Point-in-Time Recovery:** Supports restoring to any point in time using WAL archiving +- **Regular Testing:** Periodically test backup restoration to exercise the full lifecycle of this process + +#### Redis Backup and Restore +Each Redis instance is backed up through a Kubernetes CronJob that: +1. Executes the Redis `SAVE` command +2. Copies the resulting `dump.rdb` to object storage +3. Manages backup retention + + + ```yaml + apiVersion: batch/v1 + kind: CronJob + metadata: + name: redis-backup + spec: + schedule: "0 * * * *" # Every hour + jobTemplate: + spec: + template: + spec: + containers: + - name: redis-backup + image: bitnami/redis + command: + - /bin/sh + - -c + - | + redis-cli -a $REDIS_PASSWORD save + mc cp /data/dump.rdb object-store/redis-backups/ + volumes: + - name: redis-data + persistentVolumeClaim: + claimName: redis-data + ``` + + +During failover, the latest Redis backup is restored from object storage to the passive data center. This process is manual and requires operator intervention. + +## Multi Data Center Deployment +Infisical can be deployed across multiple data centers in an active-passive configuration for disaster recovery. In this setup, one data center serves as the active site while others remain as passive standbys. + +### Active Data Center +The active data center contains: +- The primary PostgreSQL cluster managed by CloudNativePG handling all write operations +- The active Redis instance handling all traffic +- The active Infisical deployment serving all user traffic + +### Passive Data Centers +Passive data centers act as disaster recovery sites. Each contains: +- A replica PostgreSQL cluster that replicates from the active site's primary cluster +- A standby Redis instance (not receiving traffic) +- A standby Infisical deployment (not receiving traffic) + +### Traffic Management and Failover +Traffic routing between data centers requires: +1. A global load balancer for traffic management. For on-premises deployments, this can be implemented using: + - HAProxy or NGINX configured as a global load balancer + - Any enterprise network routing solutions you may already have in place +2. Each data center should have its own ingress or load balancer + +The global load balancer should be deployed in a highly available configuration across multiple locations to avoid it becoming a single point of failure. + +During normal operation: +- The global load balancer routes all traffic to the active data center +- Replica PostgreSQL clusters continuously replicate from the primary cluster +- Redis backups are regularly created and stored in object storage + +During failover: +- A human operator must initiate the failover process +- The operator promotes a replica PostgreSQL cluster in the target passive data center to become primary using CloudNativePG's promotion process +- The latest Redis backup is restored from object storage to the passive data center's Redis instance +- Once database failover is complete, the global load balancer is updated to direct traffic to the new active data center + + +This is an active-passive setup where failover must be initiated manually by an operator. Automatic failover between data centers is not recommended as it can lead to split-brain scenarios. The operator should verify the state of both data centers before initiating failover. + + +## Data Replication Across Data Centers + +### PostgreSQL Replication +CloudNativePG manages replication across data centers: +- **Replica Clusters:** Each data center runs a replica cluster that replicates from the primary cluster +- **WAL Shipping:** Changes are replicated via WAL shipping to object storage +- **Failover:** The operator can promote a replica cluster to primary during planned switchovers or failures + +### Object Storage Configuration +If using MinIO for object storage, ensure: +- High availability deployment if running dedicated MinIO cluster +- Proper access controls and encryption for data at rest +- Regular monitoring of storage capacity and performance +- Backup of object storage data itself if running your own MinIO deployment \ No newline at end of file diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 42de92e71..3e38d9067 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -923,7 +923,7 @@ export const useAddIdentityTokenAuth = () => { }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ - queryKey: identitiesKeys.getIdentityUniversalAuth(identityId) + queryKey: identitiesKeys.getIdentityTokenAuth(identityId) }); } }); @@ -959,7 +959,7 @@ export const useUpdateIdentityTokenAuth = () => { }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ - queryKey: identitiesKeys.getIdentityUniversalAuth(identityId) + queryKey: identitiesKeys.getIdentityTokenAuth(identityId) }); } }); diff --git a/frontend/src/hooks/api/reactQuery.tsx b/frontend/src/hooks/api/reactQuery.tsx index d6ffc9378..f6ae0ca76 100644 --- a/frontend/src/hooks/api/reactQuery.tsx +++ b/frontend/src/hooks/api/reactQuery.tsx @@ -182,7 +182,7 @@ export const queryClient = new QueryClient({ createNotification({ title: "Bad Request", type: "error", - text: `${serverResponse.message}${serverResponse.message.endsWith(".") ? "" : "."}`, + text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`, copyActions: [ { value: serverResponse.reqId, diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx index 70128fa1a..a5f2357dd 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx @@ -7,10 +7,10 @@ import { UsePopUpState } from "@app/hooks/usePopUp"; import { IdentityAuthMethodModalContent } from "./IdentityAuthMethodModalContent"; type Props = { - popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>; + popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>, state?: boolean ) => void; }; @@ -34,7 +34,7 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog title={ isSelectedAuthAlreadyConfigured ? `Edit ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}` - : `Create new ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}` + : `Add ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}` } > ; + popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>, state?: boolean ) => void; @@ -56,13 +34,7 @@ type Props = { setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void; }; -type TRevokeOptions = { - identityId: string; - organizationId: string; -}; - type TRevokeMethods = { - revokeMethod: (revokeOptions: TRevokeOptions) => Promise; render: () => JSX.Element; }; @@ -96,18 +68,6 @@ export const IdentityAuthMethodModalContent = ({ initialAuthMethod, setSelectedAuthMethod }: Props) => { - const { currentOrg } = useOrganization(); - const orgId = currentOrg?.id || ""; - - const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth(); - const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth(); - const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth(); - const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth(); - const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth(); - const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth(); - const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth(); - const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth(); - const { control, watch } = useForm({ resolver: zodResolver(schema), defaultValues: async () => { @@ -149,10 +109,9 @@ export const IdentityAuthMethodModalContent = ({ const methodMap: Record = { [IdentityAuthMethod.UNIVERSAL_AUTH]: { - revokeMethod: revokeUniversalAuth, render: () => ( @@ -160,10 +119,9 @@ export const IdentityAuthMethodModalContent = ({ }, [IdentityAuthMethod.OIDC_AUTH]: { - revokeMethod: revokeOidcAuth, render: () => ( @@ -171,10 +129,9 @@ export const IdentityAuthMethodModalContent = ({ }, [IdentityAuthMethod.TOKEN_AUTH]: { - revokeMethod: revokeTokenAuth, render: () => ( @@ -182,10 +139,9 @@ export const IdentityAuthMethodModalContent = ({ }, [IdentityAuthMethod.AZURE_AUTH]: { - revokeMethod: revokeAzureAuth, render: () => ( @@ -193,10 +149,9 @@ export const IdentityAuthMethodModalContent = ({ }, [IdentityAuthMethod.GCP_AUTH]: { - revokeMethod: revokeGcpAuth, render: () => ( @@ -204,10 +159,9 @@ export const IdentityAuthMethodModalContent = ({ }, [IdentityAuthMethod.KUBERNETES_AUTH]: { - revokeMethod: revokeKubernetesAuth, render: () => ( @@ -215,10 +169,9 @@ export const IdentityAuthMethodModalContent = ({ }, [IdentityAuthMethod.AWS_AUTH]: { - revokeMethod: revokeAwsAuth, render: () => ( @@ -226,10 +179,9 @@ export const IdentityAuthMethodModalContent = ({ }, [IdentityAuthMethod.JWT_AUTH]: { - revokeMethod: revokeJwtAuth, render: () => ( @@ -294,42 +246,6 @@ export const IdentityAuthMethodModalContent = ({ onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} text="You can use IP allowlisting if you switch to Infisical's Pro plan." /> - handlePopUpToggle("revokeAuthMethod", isOpen)} - deleteKey="confirm" - buttonText="Remove" - onDeleteApproved={async () => { - if (!identityAuthMethodData.authMethod || !orgId || !selectedMethodItem) { - return; - } - - try { - await selectedMethodItem.revokeMethod({ - identityId: identityAuthMethodData.identityId, - organizationId: orgId - }); - - createNotification({ - text: "Successfully removed auth method", - type: "success" - }); - - handlePopUpToggle("revokeAuthMethod", false); - handlePopUpToggle("identityAuthMethod", false); - } catch { - createNotification({ - text: "Failed to remove auth method", - type: "error" - }); - } - }} - /> ); }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx index 59528e3a7..ddcc7b2d1 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -6,17 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, IconButton, Input } from "@app/components/v2"; +import { + Button, + FormControl, + IconButton, + Input, + Tab, + TabList, + TabPanel, + Tabs +} from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityAwsAuth, useGetIdentityAwsAuth, useUpdateIdentityAwsAuth } from "@app/hooks/api"; -import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { IdentityFormTab } from "./types"; + const schema = z .object({ stsEndpoint: z.string(), @@ -49,21 +59,18 @@ export type FormData = z.infer; type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, state?: boolean ) => void; - identityAuthMethodData: { - identityId: string; - name: string; - configuredAuthMethods?: IdentityAuthMethod[]; - authMethod?: IdentityAuthMethod; - }; + identityId?: string; + isUpdate?: boolean; }; export const IdentityAwsAuthForm = ({ handlePopUpOpen, handlePopUpToggle, - identityAuthMethodData + identityId, + isUpdate }: Props) => { const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; @@ -71,11 +78,9 @@ export const IdentityAwsAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth(); + const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); - const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes( - identityAuthMethodData.authMethod! || "" - ); - const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", { + const { data } = useGetIdentityAwsAuth(identityId ?? "", { enabled: isUpdate }); @@ -143,7 +148,7 @@ export const IdentityAwsAuthForm = ({ accessTokenTrustedIps }: FormData) => { try { - if (!identityAuthMethodData) return; + if (!identityId) return; if (data) { await updateMutateAsync({ @@ -151,7 +156,7 @@ export const IdentityAwsAuthForm = ({ stsEndpoint, allowedPrincipalArns, allowedAccountIds, - identityId: identityAuthMethodData.identityId, + identityId, accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), @@ -160,7 +165,7 @@ export const IdentityAwsAuthForm = ({ } else { await addMutateAsync({ organizationId: orgId, - identityId: identityAuthMethodData.identityId, + identityId, stsEndpoint: stsEndpoint || "", allowedPrincipalArns: allowedPrincipalArns || "", allowedAccountIds: allowedAccountIds || "", @@ -188,189 +193,194 @@ export const IdentityAwsAuthForm = ({ }; return ( -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - {accessTokenTrustedIpsFields.map(({ id }, index) => ( -
+ { + setTabValue( + ["accessTokenTrustedIps"].includes(Object.keys(fields)[0]) + ? IdentityFormTab.Advanced + : IdentityFormTab.Configuration + ); + })} + > + setTabValue(value as IdentityFormTab)}> + + Configuration + Advanced + + { - return ( - - { - if (subscription?.ipAllowlisting) { - field.onChange(e); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - placeholder="123.456.789.0" - /> - - ); - }} + defaultValue="2592000" + name="allowedPrincipalArns" + render={({ field, fieldState: { error } }) => ( + + + + )} /> - { - if (subscription?.ipAllowlisting) { - removeAccessTokenTrustedIp(index); - return; - } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + + + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } - handlePopUpOpen("upgradePlan"); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
+ handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeAccessTokenTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+
+
+
-
-
-
- - -
- {isUpdate && ( - - )} +
); diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx index 7af879888..cd32ca9dc 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -6,17 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, IconButton, Input } from "@app/components/v2"; +import { + Button, + FormControl, + IconButton, + Input, + Tab, + TabList, + TabPanel, + Tabs +} from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityAzureAuth, useGetIdentityAzureAuth, useUpdateIdentityAzureAuth } from "@app/hooks/api"; -import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { IdentityFormTab } from "./types"; + const schema = z .object({ tenantId: z.string().min(1), @@ -44,21 +54,18 @@ export type FormData = z.infer; type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, state?: boolean ) => void; - identityAuthMethodData: { - identityId: string; - name: string; - configuredAuthMethods?: IdentityAuthMethod[]; - authMethod?: IdentityAuthMethod; - }; + identityId?: string; + isUpdate?: boolean; }; export const IdentityAzureAuthForm = ({ handlePopUpOpen, handlePopUpToggle, - identityAuthMethodData + identityId, + isUpdate }: Props) => { const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; @@ -66,11 +73,9 @@ export const IdentityAzureAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth(); + const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); - const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes( - identityAuthMethodData.authMethod! || "" - ); - const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", { + const { data } = useGetIdentityAzureAuth(identityId ?? "", { enabled: isUpdate }); @@ -139,12 +144,12 @@ export const IdentityAzureAuthForm = ({ accessTokenTrustedIps }: FormData) => { try { - if (!identityAuthMethodData) return; + if (!identityId) return; if (data) { await updateMutateAsync({ organizationId: orgId, - identityId: identityAuthMethodData.identityId, + identityId, tenantId, resource, allowedServicePrincipalIds, @@ -156,7 +161,7 @@ export const IdentityAzureAuthForm = ({ } else { await addMutateAsync({ organizationId: orgId, - identityId: identityAuthMethodData.identityId, + identityId, tenantId: tenantId || "", resource: resource || "", allowedServicePrincipalIds: allowedServicePrincipalIds || "", @@ -184,189 +189,194 @@ export const IdentityAzureAuthForm = ({ }; return ( -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - {accessTokenTrustedIpsFields.map(({ id }, index) => ( -
+ { + setTabValue( + ["accessTokenTrustedIps"].includes(Object.keys(fields)[0]) + ? IdentityFormTab.Advanced + : IdentityFormTab.Configuration + ); + })} + > + setTabValue(value as IdentityFormTab)}> + + Configuration + Advanced + + { - return ( - - { - if (subscription?.ipAllowlisting) { - field.onChange(e); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - placeholder="123.456.789.0" - /> - - ); - }} + defaultValue="2592000" + name="tenantId" + render={({ field, fieldState: { error } }) => ( + + + + )} /> - { - if (subscription?.ipAllowlisting) { - removeAccessTokenTrustedIp(index); - return; - } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + + + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } - handlePopUpOpen("upgradePlan"); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
+ handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeAccessTokenTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+
+
+
-
-
-
- - -
- {isUpdate && ( - - )} +
); diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx index 0cccb8213..760bb6ed4 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -6,17 +6,29 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2"; +import { + Button, + FormControl, + IconButton, + Input, + Select, + SelectItem, + Tab, + TabList, + TabPanel, + Tabs +} from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityGcpAuth, useGetIdentityGcpAuth, useUpdateIdentityGcpAuth } from "@app/hooks/api"; -import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { IdentityFormTab } from "./types"; + const schema = z .object({ type: z.enum(["iam", "gce"]), @@ -45,21 +57,18 @@ export type FormData = z.infer; type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, state?: boolean ) => void; - identityAuthMethodData: { - identityId: string; - name: string; - configuredAuthMethods?: IdentityAuthMethod[]; - authMethod?: IdentityAuthMethod; - }; + identityId?: string; + isUpdate?: boolean; }; export const IdentityGcpAuthForm = ({ handlePopUpOpen, handlePopUpToggle, - identityAuthMethodData + identityId, + isUpdate }: Props) => { const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; @@ -67,11 +76,9 @@ export const IdentityGcpAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth(); + const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); - const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes( - identityAuthMethodData.authMethod! || "" - ); - const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", { + const { data } = useGetIdentityGcpAuth(identityId ?? "", { enabled: isUpdate }); @@ -146,11 +153,11 @@ export const IdentityGcpAuthForm = ({ accessTokenTrustedIps }: FormData) => { try { - if (!identityAuthMethodData) return; + if (!identityId) return; if (data) { await updateMutateAsync({ - identityId: identityAuthMethodData.identityId, + identityId, organizationId: orgId, type, allowedServiceAccounts, @@ -163,7 +170,7 @@ export const IdentityGcpAuthForm = ({ }); } else { await addMutateAsync({ - identityId: identityAuthMethodData.identityId, + identityId, organizationId: orgId, type, allowedServiceAccounts: allowedServiceAccounts || "", @@ -193,213 +200,222 @@ export const IdentityGcpAuthForm = ({ }; return ( -
- ( - - - - )} - /> - ( - - - - )} - /> - {watchedType === "gce" && ( - ( - - - - )} - /> - )} - {watchedType === "gce" && ( - ( - - - - )} - /> - )} - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - {accessTokenTrustedIpsFields.map(({ id }, index) => ( -
+ { + setTabValue( + ["accessTokenTrustedIps"].includes(Object.keys(fields)[0]) + ? IdentityFormTab.Advanced + : IdentityFormTab.Configuration + ); + })} + > + setTabValue(value as IdentityFormTab)}> + + Configuration + Advanced + + { - return ( + name="type" + render={({ field: { onChange, ...field }, fieldState: { error } }) => ( + + + + )} + /> + ( + + + + )} + /> + {watchedType === "gce" && ( + ( - { - if (subscription?.ipAllowlisting) { - field.onChange(e); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - placeholder="123.456.789.0" - /> + - ); - }} + )} + /> + )} + {watchedType === "gce" && ( + ( + + + + )} + /> + )} + ( + + + + )} /> - { - if (subscription?.ipAllowlisting) { - removeAccessTokenTrustedIp(index); - return; - } + ( + + + + )} + /> + ( + + + + )} + /> + + + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } - handlePopUpOpen("upgradePlan"); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
+ handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeAccessTokenTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+
+
+
-
-
-
- - -
- {isUpdate && ( - - )} +
); diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx index e975b272d..c9d8accca 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; @@ -14,17 +14,22 @@ import { Input, Select, SelectItem, + Tab, + TabList, + TabPanel, + Tabs, TextArea, Tooltip } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityJwtAuth, useUpdateIdentityJwtAuth } from "@app/hooks/api"; -import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums"; import { useGetIdentityJwtAuth } from "@app/hooks/api/identities/queries"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { IdentityFormTab } from "./types"; + const commonSchema = z.object({ accessTokenTrustedIps: z .array( @@ -85,21 +90,18 @@ export type FormData = z.infer; type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, state?: boolean ) => void; - identityAuthMethodData: { - identityId: string; - name: string; - configuredAuthMethods?: IdentityAuthMethod[]; - authMethod?: IdentityAuthMethod; - }; + identityId?: string; + isUpdate?: boolean; }; export const IdentityJwtAuthForm = ({ handlePopUpOpen, handlePopUpToggle, - identityAuthMethodData + identityId, + isUpdate }: Props) => { const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; @@ -107,11 +109,9 @@ export const IdentityJwtAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityJwtAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityJwtAuth(); + const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); - const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes( - identityAuthMethodData.authMethod! || "" - ); - const { data } = useGetIdentityJwtAuth(identityAuthMethodData?.identityId ?? "", { + const { data } = useGetIdentityJwtAuth(identityId ?? "", { enabled: isUpdate }); @@ -218,13 +218,13 @@ export const IdentityJwtAuthForm = ({ boundSubject }: FormData) => { try { - if (!identityAuthMethodData) { + if (!identityId) { return; } if (data) { await updateMutateAsync({ - identityId: identityAuthMethodData.identityId, + identityId, organizationId: orgId, configurationType, jwksUrl, @@ -241,7 +241,7 @@ export const IdentityJwtAuthForm = ({ }); } else { await addMutateAsync({ - identityId: identityAuthMethodData.identityId, + identityId, configurationType, jwksUrl, jwksCaCert, @@ -275,56 +275,179 @@ export const IdentityJwtAuthForm = ({ }; return ( -
- ( - - - - )} - /> - {selectedConfigurationType === IdentityJwtConfigurationType.JWKS && ( - <> + { + setTabValue( + ["accessTokenTrustedIps"].includes(Object.keys(fields)[0]) + ? IdentityFormTab.Advanced + : IdentityFormTab.Configuration + ); + })} + > + setTabValue(value as IdentityFormTab)}> + + Configuration + Advanced + + ( + name="configurationType" + render={({ field: { onChange, ...field }, fieldState: { error } }) => ( + + + )} + /> + {selectedConfigurationType === IdentityJwtConfigurationType.JWKS && ( + <> + ( + + + + )} + /> + ( + +