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 247d4ecd5..681bee73f 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 @@ -367,7 +367,8 @@ export const identityTokenAuthServiceFactory = ({ const tokens = await identityAccessTokenDAL.find( { - identityId + identityId, + authMethod: IdentityAuthMethod.TOKEN_AUTH }, { offset, limit, sort: [["updatedAt", "desc"]] } ); @@ -383,8 +384,12 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TUpdateTokenAuthTokenDTO) => { - const foundToken = await identityAccessTokenDAL.findById(tokenId); + const foundToken = await identityAccessTokenDAL.findOne({ + id: tokenId, + authMethod: IdentityAuthMethod.TOKEN_AUTH + }); if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` }); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: foundToken.identityId }); if (!identityMembershipOrg) { throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` }); @@ -418,6 +423,7 @@ export const identityTokenAuthServiceFactory = ({ const [token] = await identityAccessTokenDAL.update( { + authMethod: IdentityAuthMethod.TOKEN_AUTH, identityId: foundToken.identityId, id: tokenId }, @@ -438,7 +444,8 @@ export const identityTokenAuthServiceFactory = ({ }: TRevokeTokenAuthTokenDTO) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, - isAccessTokenRevoked: false + isAccessTokenRevoked: false, + authMethod: IdentityAuthMethod.TOKEN_AUTH }); if (!identityAccessToken) throw new NotFoundError({ @@ -462,9 +469,15 @@ export const identityTokenAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); - const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, { - isAccessTokenRevoked: true - }); + const [revokedToken] = await identityAccessTokenDAL.update( + { + id: identityAccessToken.id, + authMethod: IdentityAuthMethod.TOKEN_AUTH + }, + { + isAccessTokenRevoked: true + } + ); return { revokedToken }; }; diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts index 0d18aec5a..49cf4d119 100644 --- a/backend/src/services/identity/identity-fns.ts +++ b/backend/src/services/identity/identity-fns.ts @@ -18,12 +18,12 @@ export const buildAuthMethods = ({ tokenId?: string; }) => { return [ - ...(uaId ? [IdentityAuthMethod.UNIVERSAL_AUTH] : []), - ...(gcpId ? [IdentityAuthMethod.GCP_AUTH] : []), - ...(awsId ? [IdentityAuthMethod.AWS_AUTH] : []), - ...(kubernetesId ? [IdentityAuthMethod.KUBERNETES_AUTH] : []), - ...(oidcId ? [IdentityAuthMethod.OIDC_AUTH] : []), - ...(azureId ? [IdentityAuthMethod.AZURE_AUTH] : []), - ...(tokenId ? [IdentityAuthMethod.TOKEN_AUTH] : []) - ].filter((authMethod) => authMethod); + ...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null], + ...[gcpId ? IdentityAuthMethod.GCP_AUTH : null], + ...[awsId ? IdentityAuthMethod.AWS_AUTH : null], + ...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null], + ...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null], + ...[azureId ? IdentityAuthMethod.AZURE_AUTH : null], + ...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null] + ].filter((authMethod) => authMethod) as IdentityAuthMethod[]; }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx index a20bdd97a..70128fa1a 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx @@ -1,40 +1,10 @@ -import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; +import { useState } from "react"; -import { createNotification } from "@app/components/notifications"; -import { - Badge, - DeleteActionModal, - FormControl, - Modal, - ModalContent, - Select, - SelectItem, - Tooltip, - UpgradePlanModal -} from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { - useDeleteIdentityAwsAuth, - useDeleteIdentityAzureAuth, - useDeleteIdentityGcpAuth, - useDeleteIdentityKubernetesAuth, - useDeleteIdentityOidcAuth, - useDeleteIdentityTokenAuth, - useDeleteIdentityUniversalAuth -} from "@app/hooks/api"; +import { Modal, ModalContent } from "@app/components/v2"; import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; -import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; -import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm"; -import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm"; -import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm"; -import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm"; -import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm"; -import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; +import { IdentityAuthMethodModalContent } from "./IdentityAuthMethodModalContent"; type Props = { popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>; @@ -45,175 +15,13 @@ type Props = { ) => void; }; -type TRevokeOptions = { - identityId: string; - organizationId: string; -}; - -type TRevokeMethods = { - revokeMethod: (revokeOptions: TRevokeOptions) => Promise; - render: () => JSX.Element; -}; - -const identityAuthMethods = [ - { label: "Token Auth", value: IdentityAuthMethod.TOKEN_AUTH }, - { label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }, - { label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH }, - { label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH }, - { label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }, - { label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH }, - { label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH } -]; - -const schema = yup - .object({ - authMethod: yup - .mixed() - .oneOf(Object.values(IdentityAuthMethod)) - .required("Auth method is required") - }) - .required(); - -export type FormData = yup.InferType; - export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: 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 [selectedAuthMethod, setSelectedAuthMethod] = useState(null); const initialAuthMethod = popUp?.identityAuthMethod?.data?.authMethod; - const { control, watch, setValue, reset } = useForm({ - resolver: yupResolver(schema), - defaultValues: { - authMethod: initialAuthMethod - } - }); - - useEffect(() => { - if (popUp.identityAuthMethod.isOpen) { - reset({ authMethod: popUp?.identityAuthMethod?.data?.authMethod }); - } - }, [popUp.identityAuthMethod.isOpen]); - - const watchedAuthMethod = watch("authMethod"); - - const identityAuthMethodData = { - identityId: popUp?.identityAuthMethod.data?.identityId, - name: popUp?.identityAuthMethod?.data?.name, - authMethod: watch("authMethod"), - configuredAuthMethods: popUp?.identityAuthMethod?.data?.allAuthMethods - } as { - identityId: string; - name: string; - authMethod?: IdentityAuthMethod; - configuredAuthMethods?: IdentityAuthMethod[]; - }; - const isSelectedAuthAlreadyConfigured = - identityAuthMethodData?.configuredAuthMethods?.includes(watchedAuthMethod); - - useEffect(() => { - if (popUp?.identityAuthMethod?.data?.authMethod) { - setValue("authMethod", popUp?.identityAuthMethod?.data?.authMethod); - } else { - const firstAuthMethodNotConfiguredAuthMethod = identityAuthMethods.find( - ({ value }) => !identityAuthMethodData?.configuredAuthMethods?.includes(value) - ); - - if (firstAuthMethodNotConfiguredAuthMethod) { - setValue("authMethod", firstAuthMethodNotConfiguredAuthMethod.value); - } - } - }, [popUp.identityAuthMethod.isOpen]); - - const methodMap: Record = { - [IdentityAuthMethod.UNIVERSAL_AUTH]: { - revokeMethod: revokeUniversalAuth, - render: () => ( - - ) - }, - - [IdentityAuthMethod.OIDC_AUTH]: { - revokeMethod: revokeOidcAuth, - render: () => ( - - ) - }, - - [IdentityAuthMethod.TOKEN_AUTH]: { - revokeMethod: revokeTokenAuth, - render: () => ( - - ) - }, - - [IdentityAuthMethod.AZURE_AUTH]: { - revokeMethod: revokeAzureAuth, - render: () => ( - - ) - }, - - [IdentityAuthMethod.GCP_AUTH]: { - revokeMethod: revokeGcpAuth, - render: () => ( - - ) - }, - - [IdentityAuthMethod.KUBERNETES_AUTH]: { - revokeMethod: revokeKubernetesAuth, - render: () => ( - - ) - }, - - [IdentityAuthMethod.AWS_AUTH]: { - revokeMethod: revokeAwsAuth, - render: () => ( - - ) - } - }; - - const selectedMethodItem = methodMap[identityAuthMethodData.authMethod!]; + popUp?.identityAuthMethod?.data?.allAuthMethods?.includes(selectedAuthMethod); return ( - ( - - - - )} - /> - {selectedMethodItem?.render ? selectedMethodItem.render() :
} - 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 (err) { - createNotification({ - text: "Failed to remove auth method", - type: "error" - }); - } + diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx new file mode 100644 index 000000000..8852af872 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx @@ -0,0 +1,317 @@ +import { useCallback } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { createNotification } from "@app/components/notifications"; +import { + Badge, + DeleteActionModal, + FormControl, + Select, + SelectItem, + Tooltip, + UpgradePlanModal +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useDeleteIdentityAwsAuth, + useDeleteIdentityAzureAuth, + useDeleteIdentityGcpAuth, + useDeleteIdentityKubernetesAuth, + useDeleteIdentityOidcAuth, + useDeleteIdentityTokenAuth, + useDeleteIdentityUniversalAuth +} from "@app/hooks/api"; +import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; +import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm"; +import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm"; +import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm"; +import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm"; +import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm"; +import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; + +type Props = { + popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>, + state?: boolean + ) => void; + + identity: { + name: string; + id: string; + authMethods: IdentityAuthMethod[]; + }; + initialAuthMethod: IdentityAuthMethod; + setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void; +}; + +type TRevokeOptions = { + identityId: string; + organizationId: string; +}; + +type TRevokeMethods = { + revokeMethod: (revokeOptions: TRevokeOptions) => Promise; + render: () => JSX.Element; +}; + +const identityAuthMethods = [ + { label: "Token Auth", value: IdentityAuthMethod.TOKEN_AUTH }, + { label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }, + { label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH }, + { label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH }, + { label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }, + { label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH }, + { label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH } +]; + +const schema = yup + .object({ + authMethod: yup + .mixed() + .oneOf(Object.values(IdentityAuthMethod)) + .required("Auth method is required") + }) + .required(); + +export type FormData = yup.InferType; + +export const IdentityAuthMethodModalContent = ({ + popUp, + handlePopUpOpen, + handlePopUpToggle, + identity, + 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 { control, watch } = useForm({ + resolver: yupResolver(schema), + defaultValues: async () => { + let authMethod = initialAuthMethod; + + if (!authMethod) { + const firstAuthMethodNotConfiguredAuthMethod = identityAuthMethods.find( + ({ value }) => !identity?.authMethods?.includes(value) + ); + + if (firstAuthMethodNotConfiguredAuthMethod) { + authMethod = firstAuthMethodNotConfiguredAuthMethod.value; + } + } + + setSelectedAuthMethod(authMethod); + return { + authMethod + }; + } + }); + + const watchedAuthMethod = watch("authMethod"); + + const identityAuthMethodData = { + identityId: identity.id, + name: identity.name, + authMethod: watch("authMethod"), + configuredAuthMethods: identity.authMethods + } as { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + configuredAuthMethods?: IdentityAuthMethod[]; + }; + + const isSelectedAuthAlreadyConfigured = + identityAuthMethodData?.configuredAuthMethods?.includes(watchedAuthMethod); + + const methodMap: Record = { + [IdentityAuthMethod.UNIVERSAL_AUTH]: { + revokeMethod: revokeUniversalAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.OIDC_AUTH]: { + revokeMethod: revokeOidcAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.TOKEN_AUTH]: { + revokeMethod: revokeTokenAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.AZURE_AUTH]: { + revokeMethod: revokeAzureAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.GCP_AUTH]: { + revokeMethod: revokeGcpAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.KUBERNETES_AUTH]: { + revokeMethod: revokeKubernetesAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.AWS_AUTH]: { + revokeMethod: revokeAwsAuth, + render: () => ( + + ) + } + }; + + const isAlreadyConfigured = useCallback((method: IdentityAuthMethod) => { + return identityAuthMethodData?.configuredAuthMethods?.includes(method); + }, []); + + const selectedMethodItem = methodMap[identityAuthMethodData.authMethod!]; + + return ( + <> + ( + + + + )} + /> + {selectedMethodItem?.render ? selectedMethodItem.render() :
} + 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 (err) { + createNotification({ + text: "Failed to remove auth method", + type: "error" + }); + } + }} + /> + + ); +};