diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index 53fceb7d5..ff29df376 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -1,5 +1,6 @@ import { IdentityAuthMethod } from "./enums"; export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { - [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth" + [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth", + [IdentityAuthMethod.GCP_IAM_AUTH]: "GCP IAM Auth" }; diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx index 524c3a20c..7991ed7db 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -1,3 +1,4 @@ export enum IdentityAuthMethod { - UNIVERSAL_AUTH = "universal-auth" + UNIVERSAL_AUTH = "universal-auth", + GCP_IAM_AUTH = "gcp-iam-auth" } diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index 684a00b6f..dba67af40 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -1,12 +1,16 @@ export { identityAuthToNameMap } from "./constants"; export { IdentityAuthMethod } from "./enums"; export { + useAddIdentityGcpIamAuth, useAddIdentityUniversalAuth, useCreateIdentity, useCreateIdentityUniversalAuthClientSecret, useDeleteIdentity, useRevokeIdentityUniversalAuthClientSecret, useUpdateIdentity, - useUpdateIdentityUniversalAuth -} from "./mutations"; -export { useGetIdentityUniversalAuth, useGetIdentityUniversalAuthClientSecrets } from "./queries"; + useUpdateIdentityGcpIamAuth, + useUpdateIdentityUniversalAuth} from "./mutations"; +export { + useGetIdentityGcpIamAuth, + useGetIdentityUniversalAuth, + useGetIdentityUniversalAuthClientSecrets} from "./queries"; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index ec418659d..2f573f8d7 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request"; import { organizationKeys } from "../organization/queries"; import { identitiesKeys } from "./queries"; import { + AddIdentityGcpIamAuthDTO, AddIdentityUniversalAuthDTO, ClientSecretData, CreateIdentityDTO, @@ -13,10 +14,11 @@ import { DeleteIdentityDTO, DeleteIdentityUniversalAuthClientSecretDTO, Identity, + IdentityGcpIamAuth, IdentityUniversalAuth, UpdateIdentityDTO, - UpdateIdentityUniversalAuthDTO -} from "./types"; + UpdateIdentityGcpIamAuthDTO, + UpdateIdentityUniversalAuthDTO} from "./types"; export const useCreateIdentity = () => { const queryClient = useQueryClient(); @@ -169,3 +171,70 @@ export const useRevokeIdentityUniversalAuthClientSecret = () => { } }); }; + +export const useAddIdentityGcpIamAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + allowedServiceAccounts, + allowedProjects, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityGcpIamAuth } + } = await apiRequest.post<{ identityGcpIamAuth: IdentityGcpIamAuth }>( + `/api/v1/auth/gcp-iam-auth/identities/${identityId}`, + { + allowedServiceAccounts, + allowedProjects, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityGcpIamAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; + +export const useUpdateIdentityGcpIamAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + allowedServiceAccounts, + allowedProjects, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityGcpIamAuth } + } = await apiRequest.patch<{ identityGcpIamAuth: IdentityGcpIamAuth }>( + `/api/v1/auth/gcp-iam-auth/identities/${identityId}`, + { + allowedServiceAccounts, + allowedProjects, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + return identityGcpIamAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index 92d2a432f..fee3b7f0e 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -2,13 +2,14 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { ClientSecretData, IdentityUniversalAuth } from "./types"; +import { ClientSecretData, IdentityGcpIamAuth,IdentityUniversalAuth } from "./types"; export const identitiesKeys = { getIdentityUniversalAuth: (identityId: string) => [{ identityId }, "identity-universal-auth"] as const, getIdentityUniversalAuthClientSecrets: (identityId: string) => - [{ identityId }, "identity-universal-auth-client-secrets"] as const + [{ identityId }, "identity-universal-auth-client-secrets"] as const, + getIdentityGcpIamAuth: (identityId: string) => [{ identityId }, "identity-gcp-iam-auth"] as const }; export const useGetIdentityUniversalAuth = (identityId: string) => { @@ -44,3 +45,18 @@ export const useGetIdentityUniversalAuthClientSecrets = (identityId: string) => } }); }; + +export const useGetIdentityGcpIamAuth = (identityId: string) => { + return useQuery({ + enabled: Boolean(identityId), + queryKey: identitiesKeys.getIdentityGcpIamAuth(identityId), + queryFn: async () => { + const { + data: { identityGcpIamAuth } + } = await apiRequest.get<{ identityGcpIamAuth: IdentityGcpIamAuth }>( + `/api/v1/auth/gcp-iam-auth/identities/${identityId}` + ); + return identityGcpIamAuth; + } + }); +}; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 4ac19c351..2222fdf56 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -38,19 +38,19 @@ export type IdentityMembership = { customRoleSlug: string; } & ( | { - isTemporary: false; - temporaryRange: null; - temporaryMode: null; - temporaryAccessEndTime: null; - temporaryAccessStartTime: null; - } + isTemporary: false; + temporaryRange: null; + temporaryMode: null; + temporaryAccessEndTime: null; + temporaryAccessStartTime: null; + } | { - isTemporary: true; - temporaryRange: string; - temporaryMode: string; - temporaryAccessEndTime: string; - temporaryAccessStartTime: string; - } + isTemporary: true; + temporaryRange: string; + temporaryMode: string; + temporaryAccessEndTime: string; + temporaryAccessStartTime: string; + } ) >; createdAt: string; @@ -113,6 +113,42 @@ export type UpdateIdentityUniversalAuthDTO = { }[]; }; +export type IdentityGcpIamAuth = { + identityId: string; + allowedServiceAccounts: string; + allowedProjects: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +}; + +export type AddIdentityGcpIamAuthDTO = { + organizationId: string; + identityId: string; + allowedServiceAccounts: string; + allowedProjects: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +}; + +export type UpdateIdentityGcpIamAuthDTO = { + organizationId: string; + identityId: string; + allowedServiceAccounts?: string; + allowedProjects?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +}; + export type CreateIdentityUniversalAuthClientSecretDTO = { identityId: string; description?: string; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpIamAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpIamAuthForm.tsx new file mode 100644 index 000000000..2c36e60cc --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpIamAuthForm.tsx @@ -0,0 +1,331 @@ +import { useEffect } 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"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, IconButton, Input } from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { + useAddIdentityGcpIamAuth, + useGetIdentityGcpIamAuth, + useUpdateIdentityGcpIamAuth +} 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"; + +const schema = yup + .object({ + allowedServiceAccounts: yup.string(), + allowedProjects: yup.string(), + accessTokenTTL: yup.string().required("Access Token TTL is required"), + accessTokenMaxTTL: yup.string().required("Access Max Token TTL is required"), + accessTokenNumUsesLimit: yup.string().required("Access Token Max Number of Uses is required"), + accessTokenTrustedIps: yup + .array( + yup.object({ + ipAddress: yup.string().max(50).required().label("IP Address") + }) + ) + .min(1) + .required() + .label("Access Token Trusted IP") + }) + .required(); + +export type FormData = yup.InferType; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + state?: boolean + ) => void; + identityAuthMethodData: { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + }; +}; + +export const IdentityAwsIamAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityAuthMethodData +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityGcpIamAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpIamAuth(); + + const { data } = useGetIdentityGcpIamAuth(identityAuthMethodData?.identityId ?? ""); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + allowedServiceAccounts: "", + allowedProjects: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { + reset({ + allowedServiceAccounts: data.allowedServiceAccounts, + allowedProjects: data.allowedProjects, + accessTokenTTL: String(data.accessTokenTTL), + accessTokenMaxTTL: String(data.accessTokenMaxTTL), + accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit), + accessTokenTrustedIps: data.accessTokenTrustedIps.map( + ({ ipAddress, prefix }: IdentityTrustedIp) => { + return { + ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` + }; + } + ) + }); + } else { + reset({ + allowedServiceAccounts: "", + allowedProjects: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + allowedServiceAccounts, + allowedProjects, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityAuthMethodData) return; + + if (data) { + await updateMutateAsync({ + organizationId: orgId, + allowedServiceAccounts, + allowedProjects, + identityId: identityAuthMethodData.identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + organizationId: orgId, + identityId: identityAuthMethodData.identityId, + allowedServiceAccounts: allowedServiceAccounts || "", + allowedProjects: allowedProjects || "", + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${ + identityAuthMethodData?.authMethod ? "updated" : "configured" + } auth method`, + type: "success" + }); + + reset(); + } catch (err) { + createNotification({ + text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } + + 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" + > + + +
+ ))} +
+ +
+
+ + +
+ + ); +};