Merge branch 'main' into ENG-3009

This commit is contained in:
x032205
2025-06-30 12:01:14 -04:00
92 changed files with 3019 additions and 739 deletions
@@ -58,6 +58,7 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop
}
onPointerDownOutside={(e) => e.preventDefault()}
className="max-w-2xl"
bodyClassName="overflow-visible"
subTitle={selectedSync ? undefined : "Select a third-party service to sync secrets to."}
>
<Content
@@ -1,10 +1,11 @@
import { faWrench } from "@fortawesome/free-solid-svg-icons";
import { useMemo } from "react";
import { faInfoCircle, faMagnifyingGlass, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Spinner, Tooltip } from "@app/components/v2";
import { EmptyState, Input, Pagination, Spinner, Tooltip } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { usePopUp } from "@app/hooks";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { SecretSync, useSecretSyncOptions } from "@app/hooks/api/secretSyncs";
import { UpgradePlanModal } from "../license/UpgradePlanModal";
@@ -19,6 +20,26 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
initPerPage: 16
});
const filteredOptions = useMemo(
() =>
secretSyncOptions?.filter(
({ name, destination }) =>
name?.toLowerCase().includes(search.trim().toLowerCase()) ||
destination.toLowerCase().includes(search.toLowerCase())
) ?? [],
[secretSyncOptions, search]
);
useResetPageHelper({
totalCount: filteredOptions.length,
offset,
setPage
});
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
@@ -29,75 +50,103 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
}
return (
<div className="grid grid-cols-4 gap-2">
{secretSyncOptions?.map(({ destination, enterprise }) => {
const { image, name } = SECRET_SYNC_MAP[destination];
return (
<button
type="button"
onClick={() =>
enterprise && !subscription.enterpriseSecretSyncs
? handlePopUpOpen("upgradePlan")
: onSelect(destination)
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<img
src={`/images/integrations/${image}`}
height={40}
width={40}
className="mt-auto"
alt={`${name} logo`}
/>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
<div className="flex flex-col gap-4">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search options..."
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
/>
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
{filteredOptions.slice(offset, perPage * page)?.map(({ destination, enterprise }) => {
const { image, name } = SECRET_SYNC_MAP[destination];
return (
<button
type="button"
onClick={() =>
enterprise && !subscription.enterpriseSecretSyncs
? handlePopUpOpen("upgradePlan")
: onSelect(destination)
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<img
src={`/images/integrations/${image}`}
height={40}
width={40}
className="mt-auto"
alt={`${name} logo`}
/>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
{!filteredOptions?.length && (
<EmptyState
className="col-span-full mt-40"
title="No Secret Syncs match search"
icon={faSearch}
/>
)}
</div>
{Boolean(filteredOptions.length) && (
<Pagination
startAdornment={
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more services.</p>
<p>
{`If you don't see the third-party
service you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
<span className="text-xs">
Don&#39;t see the third-party service you&#39;re looking for?
</span>
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
</div>
</Tooltip>
}
count={filteredOptions.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
perPageList={[16]}
/>
)}
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use every Secret Sync if you switch to Infisical's Enterprise plan."
/>
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more services.</p>
<p>
{`If you don't see the third-party
service you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4 hover:bg-mineshaft-900/50">
<FontAwesomeIcon className="mt-auto text-3xl" icon={faWrench} />
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
Coming Soon
</div>
</div>
</Tooltip>
</div>
);
};
@@ -30,7 +30,29 @@ export const AwsParameterStoreSyncFields = () => {
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Path">
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Path"
tooltipText={
<>
The path is required and will be prepended to the key schema. For example, if you
have a path of{" "}
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
/demo/path/
</code>{" "}
and a key schema of{" "}
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
INFISICAL_{"{{secretKey}}"}
</code>
, then the result will be{" "}
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
/demo/path/INFISICAL_{"{{secretKey}}"}
</code>
</>
}
tooltipClassName="max-w-lg"
>
<Input value={value} onChange={onChange} placeholder="Path..." />
</FormControl>
)}
@@ -76,7 +76,7 @@ export const GcpSyncFields = () => {
helperText={
<Tooltip
className="max-w-md"
content="Ensure that you've enabled the Secret Manager API and Cloud Resource Manager API on your GCP project. Additionally, make sure that the service account is assigned the appropriate GCP roles."
content="Ensure that you've enabled the Secret Manager API, Cloud Resource Manager API, and Service Usage API on your GCP project. Additionally, make sure that the service account is assigned the appropriate GCP roles."
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
@@ -1,4 +1,6 @@
import { ReactNode } from "react";
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
type Props = {
@@ -7,6 +9,7 @@ type Props = {
className?: string;
labelClassName?: string;
truncate?: boolean;
icon?: IconDefinition;
};
export const GenericFieldLabel = ({
@@ -14,11 +17,15 @@ export const GenericFieldLabel = ({
children,
className,
labelClassName,
truncate
truncate,
icon
}: Props) => {
return (
<div className={twMerge("min-w-0", className)}>
<p className={twMerge("text-xs font-medium text-mineshaft-400", labelClassName)}>{label}</p>
<div className="flex items-center gap-1.5">
{icon && <FontAwesomeIcon icon={icon} className="text-mineshaft-400" size="sm" />}
<p className={twMerge("text-xs font-medium text-mineshaft-400", labelClassName)}>{label}</p>
</div>
{children ? (
<p className={twMerge("text-sm text-mineshaft-100", truncate && "truncate")}>{children}</p>
) : (
@@ -163,7 +163,7 @@ const PasswordGeneratorModal = ({
<div className="mb-6 flex flex-row justify-between gap-2">
<Checkbox
id="useUppercase"
className="mr-2 data-[state=checked]:bg-primary"
className="mr-2"
isChecked={passwordOptions.useUppercase}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useUppercase: checked as boolean })
@@ -174,7 +174,7 @@ const PasswordGeneratorModal = ({
<Checkbox
id="useLowercase"
className="mr-2 data-[state=checked]:bg-primary"
className="mr-2"
isChecked={passwordOptions.useLowercase}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useLowercase: checked as boolean })
@@ -185,7 +185,7 @@ const PasswordGeneratorModal = ({
<Checkbox
id="useNumbers"
className="mr-2 data-[state=checked]:bg-primary"
className="mr-2"
isChecked={passwordOptions.useNumbers}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useNumbers: checked as boolean })
@@ -196,7 +196,7 @@ const PasswordGeneratorModal = ({
<Checkbox
id="useSpecialChars"
className="mr-2 data-[state=checked]:bg-primary"
className="mr-2"
isChecked={passwordOptions.useSpecialChars}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useSpecialChars: checked as boolean })
@@ -11,5 +11,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
[IdentityAuthMethod.OCI_AUTH]: "OCI Auth",
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth",
[IdentityAuthMethod.LDAP_AUTH]: "LDAP Auth",
[IdentityAuthMethod.JWT_AUTH]: "JWT Auth"
[IdentityAuthMethod.JWT_AUTH]: "JWT Auth",
[IdentityAuthMethod.TLS_CERT_AUTH]: "TLS Certificate Auth"
};
+2 -1
View File
@@ -9,7 +9,8 @@ export enum IdentityAuthMethod {
OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth",
LDAP_AUTH = "ldap-auth",
JWT_AUTH = "jwt-auth"
JWT_AUTH = "jwt-auth",
TLS_CERT_AUTH = "tls-cert-auth"
}
export enum IdentityJwtConfigurationType {
@@ -14,6 +14,7 @@ import {
AddIdentityLdapAuthDTO,
AddIdentityOciAuthDTO,
AddIdentityOidcAuthDTO,
AddIdentityTlsCertAuthDTO,
AddIdentityTokenAuthDTO,
AddIdentityUniversalAuthDTO,
ClientSecretData,
@@ -32,6 +33,7 @@ import {
DeleteIdentityLdapAuthDTO,
DeleteIdentityOciAuthDTO,
DeleteIdentityOidcAuthDTO,
DeleteIdentityTlsCertAuthDTO,
DeleteIdentityTokenAuthDTO,
DeleteIdentityUniversalAuthClientSecretDTO,
DeleteIdentityUniversalAuthDTO,
@@ -46,6 +48,7 @@ import {
IdentityLdapAuth,
IdentityOciAuth,
IdentityOidcAuth,
IdentityTlsCertAuth,
IdentityTokenAuth,
IdentityUniversalAuth,
RevokeTokenDTO,
@@ -60,6 +63,7 @@ import {
UpdateIdentityLdapAuthDTO,
UpdateIdentityOciAuthDTO,
UpdateIdentityOidcAuthDTO,
UpdateIdentityTlsCertAuthDTO,
UpdateIdentityTokenAuthDTO,
UpdateIdentityUniversalAuthDTO,
UpdateTokenIdentityTokenAuthDTO
@@ -655,6 +659,107 @@ export const useDeleteIdentityAliCloudAuth = () => {
});
};
export const useAddIdentityTlsCertAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityTlsCertAuth, object, AddIdentityTlsCertAuthDTO>({
mutationFn: async ({
identityId,
allowedCommonNames,
caCertificate,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}) => {
const {
data: { identityTlsCertAuth }
} = await apiRequest.post<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
`/api/v1/auth/tls-cert-auth/identities/${identityId}`,
{
allowedCommonNames,
caCertificate,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityTlsCertAuth;
},
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries({
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
});
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
queryClient.invalidateQueries({
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
});
}
});
};
export const useUpdateIdentityTlsCertAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityTlsCertAuth, object, UpdateIdentityTlsCertAuthDTO>({
mutationFn: async ({
identityId,
allowedCommonNames,
caCertificate,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}) => {
const {
data: { identityTlsCertAuth }
} = await apiRequest.patch<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
`/api/v1/auth/tls-cert-auth/identities/${identityId}`,
{
caCertificate,
allowedCommonNames,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityTlsCertAuth;
},
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries({
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
});
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
queryClient.invalidateQueries({
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
});
}
});
};
export const useDeleteIdentityTlsCertAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityTlsCertAuth, object, DeleteIdentityTlsCertAuthDTO>({
mutationFn: async ({ identityId }) => {
const {
data: { identityTlsCertAuth }
} = await apiRequest.delete(`/api/v1/auth/tls-cert-auth/identities/${identityId}`);
return identityTlsCertAuth;
},
onSuccess: (_, { organizationId, identityId }) => {
queryClient.invalidateQueries({
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
});
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
queryClient.invalidateQueries({
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
});
}
});
};
export const useUpdateIdentityOidcAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityOidcAuth, object, UpdateIdentityOidcAuthDTO>({
@@ -17,6 +17,7 @@ import {
IdentityMembershipOrg,
IdentityOciAuth,
IdentityOidcAuth,
IdentityTlsCertAuth,
IdentityTokenAuth,
IdentityUniversalAuth,
TSearchIdentitiesDTO
@@ -34,6 +35,8 @@ export const identitiesKeys = {
getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
getIdentityOidcAuth: (identityId: string) => [{ identityId }, "identity-oidc-auth"] as const,
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const,
getIdentityTlsCertAuth: (identityId: string) =>
[{ identityId }, "identity-tls-cert-auth"] as const,
getIdentityAliCloudAuth: (identityId: string) =>
[{ identityId }, "identity-alicloud-auth"] as const,
getIdentityOciAuth: (identityId: string) => [{ identityId }, "identity-oci-auth"] as const,
@@ -175,6 +178,27 @@ export const useGetIdentityAwsAuth = (
});
};
export const useGetIdentityTlsCertAuth = (
identityId: string,
options?: TReactQueryOptions["options"]
) => {
return useQuery({
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId),
queryFn: async () => {
const {
data: { identityTlsCertAuth }
} = await apiRequest.get<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
`/api/v1/auth/tls-cert-auth/identities/${identityId}`
);
return identityTlsCertAuth;
},
staleTime: 0,
gcTime: 0,
...options,
enabled: Boolean(identityId) && (options?.enabled ?? true)
});
};
export const useGetIdentityOciAuth = (
identityId: string,
options?: TReactQueryOptions["options"]
@@ -485,6 +485,47 @@ export type DeleteIdentityKubernetesAuthDTO = {
identityId: string;
};
export type IdentityTlsCertAuth = {
identityId: string;
caCertificate: string;
allowedCommonNames: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: IdentityTrustedIp[];
};
export type AddIdentityTlsCertAuthDTO = {
organizationId: string;
identityId: string;
caCertificate: string;
allowedCommonNames?: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: {
ipAddress: string;
}[];
};
export type UpdateIdentityTlsCertAuthDTO = {
organizationId: string;
identityId: string;
caCertificate: string;
allowedCommonNames?: string | null;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: {
ipAddress: string;
}[];
};
export type DeleteIdentityTlsCertAuthDTO = {
organizationId: string;
identityId: string;
};
export type CreateIdentityUniversalAuthClientSecretDTO = {
identityId: string;
description?: string;
@@ -446,7 +446,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isDisabled={Boolean(cert)}
isChecked={value[optionValue]}
onCheckedChange={(state) => {
@@ -481,7 +480,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isDisabled={Boolean(cert)}
isChecked={value[optionValue]}
onCheckedChange={(state) => {
@@ -405,7 +405,6 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
@@ -439,7 +438,6 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
@@ -408,7 +408,6 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
@@ -443,7 +442,6 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
@@ -477,12 +475,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
errorText={error?.message}
tooltipText="If enabled, a new certificate will be issued automatically X days before the current certificate expires."
>
<Checkbox
id="enableAutoRenewal"
isChecked={value}
onCheckedChange={onChange}
className="data-[state=checked]:bg-primary"
>
<Checkbox id="enableAutoRenewal" isChecked={value} onCheckedChange={onChange}>
Enable Certificate Auto Renewal
</Checkbox>
</FormControl>
@@ -333,7 +333,6 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
@@ -367,7 +366,6 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
@@ -145,7 +145,6 @@ const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => {
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
@@ -17,6 +17,7 @@ import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
import { IdentityLdapAuthForm } from "./IdentityLdapAuthForm";
import { IdentityOciAuthForm } from "./IdentityOciAuthForm";
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
import { IdentityTlsCertAuthForm } from "./IdentityTlsCertAuthForm";
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
@@ -52,6 +53,7 @@ const identityAuthMethods = [
{ label: "OCI Auth", value: IdentityAuthMethod.OCI_AUTH },
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH },
{ label: "LDAP Auth", value: IdentityAuthMethod.LDAP_AUTH },
{ label: "TLS Certificate Auth", value: IdentityAuthMethod.TLS_CERT_AUTH },
{
label: "JWT Auth",
value: IdentityAuthMethod.JWT_AUTH
@@ -123,6 +125,15 @@ export const IdentityAuthMethodModalContent = ({
/>
)
},
[IdentityAuthMethod.TLS_CERT_AUTH]: {
render: () => (
<IdentityTlsCertAuthForm
identityId={identityAuthMethodData.identityId}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.OIDC_AUTH]: {
render: () => (
@@ -0,0 +1,358 @@
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";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs,
TextArea
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useAddIdentityTlsCertAuth,
useGetIdentityTlsCertAuth,
useUpdateIdentityTlsCertAuth
} from "@app/hooks/api";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z.object({
allowedCommonNames: z.string().optional(),
caCertificate: z.string().min(1),
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
message: "Access Token TTL cannot be greater than 315360000"
}),
accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, {
message: "Access Token Max TTL cannot be greater than 315360000"
}),
accessTokenNumUsesLimit: z.string(),
accessTokenTrustedIps: z
.array(
z.object({
ipAddress: z.string().max(50)
})
)
.min(1)
});
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
identityId?: string;
isUpdate?: boolean;
};
export const IdentityTlsCertAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityId,
isUpdate
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { subscription } = useSubscription();
const { mutateAsync: addMutateAsync } = useAddIdentityTlsCertAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTlsCertAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data } = useGetIdentityTlsCertAuth(identityId ?? "", {
enabled: isUpdate
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
caCertificate: "",
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({
caCertificate: data.caCertificate,
allowedCommonNames: data.allowedCommonNames || undefined,
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({
caCertificate: "",
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
}
}, [data]);
const onFormSubmit = async ({
caCertificate,
allowedCommonNames,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}: FormData) => {
try {
if (!identityId) return;
if (data) {
await updateMutateAsync({
organizationId: orgId,
caCertificate,
allowedCommonNames: allowedCommonNames || null,
identityId,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
} else {
await addMutateAsync({
organizationId: orgId,
identityId,
caCertificate,
allowedCommonNames: allowedCommonNames || undefined,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
}
handlePopUpToggle("identityAuthMethod", false);
createNotification({
text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
type: "success"
});
reset();
} catch {
createNotification({
text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
<TabList>
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
<Controller
control={control}
name="caCertificate"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
tooltipText="A PEM-encoded CA certificate. This will be used to validate client certificate."
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="allowedCommonNames"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Common Names"
isError={Boolean(error)}
isOptional
errorText={error?.message}
tooltipText="Comma separated common names allowed to authenticate against the identity. Leave empty to allow any certificate."
>
<Input {...field} placeholder="" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
tooltipText="The lifetime for an access token in seconds. This value will be referenced at renewal time."
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
>
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Add"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
</form>
);
};
@@ -1,11 +1,12 @@
import { faWrench } from "@fortawesome/free-solid-svg-icons";
import { useMemo } from "react";
import { faInfoCircle, faMagnifyingGlass, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { Spinner, Tooltip } from "@app/components/v2";
import { EmptyState, Input, Pagination, Spinner, Tooltip } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { usePopUp } from "@app/hooks";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useAppConnectionOptions } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
@@ -19,6 +20,26 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
initPerPage: 16
});
const filteredOptions = useMemo(
() =>
appConnectionOptions?.filter(
({ name, app }) =>
name?.toLowerCase().includes(search.trim().toLowerCase()) ||
app.toLowerCase().includes(search.toLowerCase())
) ?? [],
[appConnectionOptions, search]
);
useResetPageHelper({
totalCount: filteredOptions.length,
offset,
setPage
});
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
@@ -29,86 +50,120 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
}
return (
<div className="grid grid-cols-4 gap-2">
{appConnectionOptions?.map((option) => {
const { image, name, size = 50, enterprise = false, icon } = APP_CONNECTION_MAP[option.app];
<div className="flex flex-col gap-4">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search options..."
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
/>
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
{filteredOptions.slice(offset, perPage * page)?.map((option) => {
const {
image,
name,
size = 50,
enterprise = false,
icon
} = APP_CONNECTION_MAP[option.app];
return (
<button
type="button"
onClick={() =>
enterprise && !subscription.enterpriseAppConnections
? handlePopUpOpen("upgradePlan")
: onSelect(option.app)
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<div className="relative">
<img
src={`/images/integrations/${image}`}
style={{
width: `${size}px`
}}
className="mt-auto"
alt={`${name} logo`}
/>
{icon && (
<FontAwesomeIcon
className="absolute -bottom-1.5 -right-1.5 text-primary-700"
size="xl"
icon={icon}
return (
<button
type="button"
onClick={() =>
enterprise && !subscription.enterpriseAppConnections
? handlePopUpOpen("upgradePlan")
: onSelect(option.app)
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<div className="relative">
<img
src={`/images/integrations/${image}`}
style={{
width: `${size}px`
}}
className="mt-auto"
alt={`${name} logo`}
/>
)}
</div>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
{icon && (
<FontAwesomeIcon
className="absolute -bottom-1.5 -right-1.5 text-primary-700"
size="xl"
icon={icon}
/>
)}
</div>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
{!filteredOptions?.length && (
<EmptyState
className="col-span-full mt-40"
title="No App Connections match search"
icon={faSearch}
/>
)}
</div>
{Boolean(filteredOptions.length) && (
<Pagination
startAdornment={
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more services.</p>
<p>
{`If you don't see the third-party
service you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
<span className="text-xs">
Don&#39;t see the third-party service you&#39;re looking for?
</span>
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
</div>
</Tooltip>
}
count={filteredOptions.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
perPageList={[16]}
/>
)}
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use every App Connection if you switch to Infisical's Enterprise plan."
/>
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more connections.</p>
<p>
{`If you don't see the third-party
app you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4">
<FontAwesomeIcon className="mt-auto text-xl" icon={faWrench} />
<div className="mt-auto max-w-xs text-center text-sm font-medium text-gray-300 duration-200 group-hover:text-gray-200">
Coming Soon
</div>
</div>
</Tooltip>
</div>
);
};
@@ -15,6 +15,7 @@ import {
useDeleteIdentityLdapAuth,
useDeleteIdentityOciAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTlsCertAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
@@ -29,6 +30,7 @@ import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthC
import { ViewIdentityLdapAuthContent } from "./ViewIdentityLdapAuthContent";
import { ViewIdentityOciAuthContent } from "./ViewIdentityOciAuthContent";
import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent";
import { ViewIdentityTlsCertAuthContent } from "./ViewIdentityTlsCertAuthContent";
import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent";
import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent";
@@ -63,6 +65,7 @@ export const Content = ({
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
const { mutateAsync: revokeTlsCertAuth } = useDeleteIdentityTlsCertAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeAliCloudAuth } = useDeleteIdentityAliCloudAuth();
@@ -93,6 +96,10 @@ export const Content = ({
revokeMethod = revokeGcpAuth;
Component = ViewIdentityGcpAuthContent;
break;
case IdentityAuthMethod.TLS_CERT_AUTH:
revokeMethod = revokeTlsCertAuth;
Component = ViewIdentityTlsCertAuthContent;
break;
case IdentityAuthMethod.AWS_AUTH:
revokeMethod = revokeAwsAuth;
Component = ViewIdentityAwsAuthContent;
@@ -0,0 +1,88 @@
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { useGetIdentityTlsCertAuth } from "@app/hooks/api";
import { IdentityTlsCertAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityTlsCertAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityTlsCertAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState
icon={faBan}
title="Could not find TLS Certificate Auth associated with this Identity."
/>
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityTlsCertAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="CA Certificate">
<Tooltip
side="right"
className="max-w-xl p-2"
content={<p className="break-words rounded bg-mineshaft-600 p-2">{data.caCertificate}</p>}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Common Names">
{data.allowedCommonNames
?.split(",")
.map((cn) => cn.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};
@@ -18,6 +18,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
@@ -39,6 +40,7 @@ import {
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { isCustomProjectRole } from "@app/helpers/roles";
import {
getUserTablePreference,
PreferenceKey,
@@ -53,7 +55,8 @@ import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/R
enum RolesOrderBy {
Name = "name",
Slug = "slug"
Slug = "slug",
Type = "type"
}
export const ProjectRoleList = () => {
@@ -98,7 +101,7 @@ export const ProjectRoleList = () => {
setPerPage,
setPage,
offset
} = usePagination<RolesOrderBy>(RolesOrderBy.Name, {
} = usePagination<RolesOrderBy>(RolesOrderBy.Type, {
initPerPage: getUserTablePreference("projectRolesTable", PreferenceKey.PerPage, 20)
});
@@ -125,6 +128,12 @@ export const ProjectRoleList = () => {
switch (orderBy) {
case RolesOrderBy.Slug:
return roleOne.slug.toLowerCase().localeCompare(roleTwo.slug.toLowerCase());
case RolesOrderBy.Type: {
const roleOneValue = isCustomProjectRole(roleOne.slug) ? -1 : 1;
const roleTwoValue = isCustomProjectRole(roleTwo.slug) ? -1 : 1;
return roleTwoValue - roleOneValue;
}
case RolesOrderBy.Name:
default:
return roleOne.name.toLowerCase().localeCompare(roleTwo.name.toLowerCase());
@@ -210,6 +219,19 @@ export const ProjectRoleList = () => {
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Type
<IconButton
variant="plain"
className={getClassName(RolesOrderBy.Type)}
ariaLabel="sort"
onClick={() => handleSort(RolesOrderBy.Type)}
>
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Type)} />
</IconButton>
</div>
</Th>
<Th aria-label="actions" className="w-5" />
</Tr>
</THead>
@@ -237,6 +259,11 @@ export const ProjectRoleList = () => {
>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>
<Badge className="w-min whitespace-nowrap bg-mineshaft-400/50 text-bunker-200">
{isCustomProjectRole(slug) ? "Custom" : "Default"}
</Badge>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
@@ -284,7 +284,6 @@ const ServiceTokenForm = () => {
<Checkbox
id={String(value[optionValue])}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
isDisabled={optionValue === "read"}
onCheckedChange={(state) => {
@@ -247,9 +247,7 @@ export const AccessApprovalRequest = ({
};
else if (userReviewStatus === ApprovalStatus.APPROVED) {
displayData = {
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
}`,
label: "Pending Additional Reviews",
type: "primary",
icon: faClipboardCheck
};
@@ -1,10 +1,9 @@
import { useCallback, useMemo, useState } from "react";
import { ReactNode, useCallback, useMemo, useState } from "react";
import {
faCheckCircle,
faCircle,
faTriangleExclamation,
faUsers,
faXmarkCircle
faBan,
faCheck,
faHourglass,
faTriangleExclamation
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import ms from "ms";
@@ -15,12 +14,10 @@ import {
Button,
Checkbox,
FormControl,
GenericFieldLabel,
Input,
Modal,
ModalContent,
Popover,
PopoverContent,
PopoverTrigger,
Tooltip
} from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge";
@@ -38,10 +35,22 @@ import { groupBy } from "@app/lib/fn/array";
const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
if (status === ApprovalStatus.APPROVED)
return <FontAwesomeIcon icon={faCheckCircle} size="xs" style={{ color: "#15803d" }} />;
return (
<Badge variant="success" className="flex h-4 items-center justify-center">
<FontAwesomeIcon icon={faCheck} size="xs" />
</Badge>
);
if (status === ApprovalStatus.REJECTED)
return <FontAwesomeIcon icon={faXmarkCircle} size="xs" style={{ color: "#b91c1c" }} />;
return <FontAwesomeIcon icon={faCircle} size="xs" style={{ color: "#c2410c" }} />;
return (
<Badge variant="danger" className="flex h-4 items-center justify-center">
<FontAwesomeIcon icon={faBan} size="xs" />
</Badge>
);
return (
<Badge variant="primary" className="flex h-4 items-center justify-center">
<FontAwesomeIcon icon={faHourglass} size="xs" />
</Badge>
);
};
export const ReviewAccessRequestModal = ({
@@ -267,139 +276,160 @@ export const ReviewAccessRequestModal = ({
</div>
<div className="">
<div className="mb-2 mt-4 text-mineshaft-200">
<div className="grid grid-cols-2 gap-4">
<div>
<div className="mb-1 text-xs font-semibold uppercase">Environment</div>
<div>{accessDetails.env || "-"}</div>
</div>
<div>
<div className="mb-1 text-xs font-semibold uppercase">Secret Path</div>
<div>{accessDetails.secretPath || "-"}</div>
</div>
<div>
<div className="mb-1 text-xs font-semibold uppercase">Access Type</div>
<div>{getAccessLabel()}</div>
</div>
<div>
<div className="mb-1 text-xs font-semibold uppercase">Permission</div>
<div>{requestedAccess}</div>
</div>
<div className="col-span-2">
<div className="mb-1 text-xs font-semibold uppercase">Note</div>
<div>{request.note || "-"}</div>
</div>
<div className="flex flex-wrap gap-8">
<GenericFieldLabel label="Environment">{accessDetails.env}</GenericFieldLabel>
<GenericFieldLabel truncate label="Secret Path">
{accessDetails.secretPath}
</GenericFieldLabel>
<GenericFieldLabel label="Access Type">{getAccessLabel()}</GenericFieldLabel>
<GenericFieldLabel label="Permission">{requestedAccess}</GenericFieldLabel>
{request.note && (
<GenericFieldLabel className="col-span-full" label="Note">
{request.note}
</GenericFieldLabel>
)}
</div>
</div>
<div className="mb-4 border-b-2 border-mineshaft-500 py-2 text-lg">Approvers</div>
<div className="thin-scrollbar max-h-64 overflow-y-auto rounded p-2">
{approverSequence?.approvers?.map((approver, index) => (
<div
key={`approval-list-${index + 1}`}
className={twMerge(
"relative mb-2 flex items-center rounded border border-mineshaft-500 bg-mineshaft-700 p-4",
approverSequence?.currentSequence !== approver.sequence &&
!hasApproved &&
"text-mineshaft-400"
)}
>
<div>
<div className="mt-4 flex items-center justify-between border-b-2 border-mineshaft-500 py-2">
<span>Approvers</span>
{approverSequence.isMyReviewInThisSequence &&
request.status === ApprovalStatus.PENDING && (
<Badge variant="primary" className="h-min">
Awaiting Your Review
</Badge>
)}
</div>
<div className="thin-scrollbar max-h-[40vh] overflow-y-auto rounded py-2">
{approverSequence?.approvers &&
approverSequence.approvers.map((approver, index) => {
const isInactive =
approverSequence?.currentSequence <
(approver.sequence ?? approverSequence.approvers!.length);
const isPending = approverSequence?.currentSequence === approver.sequence;
let StepComponent: ReactNode;
let BadgeComponent: ReactNode = null;
if (approver.hasRejected) {
StepComponent = (
<Badge
variant="danger"
className="flex h-6 min-w-6 items-center justify-center"
>
<FontAwesomeIcon icon={faBan} />
</Badge>
);
BadgeComponent = <Badge variant="danger">Rejected</Badge>;
} else if (approver.hasApproved) {
StepComponent = (
<Badge
variant="success"
className="flex h-6 min-w-6 items-center justify-center"
>
<FontAwesomeIcon icon={faCheck} />
</Badge>
);
BadgeComponent = <Badge variant="success">Approved</Badge>;
} else if (isPending) {
StepComponent = (
<Badge
variant="primary"
className="flex h-6 min-w-6 items-center justify-center"
>
<FontAwesomeIcon icon={faHourglass} />
</Badge>
);
BadgeComponent = <Badge variant="primary">Pending</Badge>;
} else {
StepComponent = (
<Badge
className={
isInactive
? "py-auto my-auto flex h-6 min-w-6 items-center justify-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-center text-bunker-200"
: ""
}
>
<span>{index + 1}</span>
</Badge>
);
}
return (
<div
className={twMerge(
"mr-8 flex h-8 w-8 items-center justify-center text-3xl font-medium",
approver.hasApproved && "border-green-400 text-green-400",
approver.hasRejected && "border-red-500 text-red-500"
key={`approval-list-${index + 1}`}
className={twMerge("flex", isInactive && "opacity-50")}
>
{approverSequence.approvers!.length > 1 && (
<div className="flex w-12 flex-col items-center gap-2 pr-4">
<div
className={twMerge(
"flex-grow border-mineshaft-600",
index !== 0 && "border-r"
)}
/>
{StepComponent}
<div
className={twMerge(
"flex-grow border-mineshaft-600",
index < approverSequence.approvers!.length - 1 && "border-r"
)}
/>
</div>
)}
>
{index + 1}
</div>
{index !== (approverSequence?.approvers?.length || 0) - 1 && (
<div
className={twMerge(
"absolute bottom-0 left-8 h-5 border-r-2 border-gray-400",
approver.hasApproved && "border-green-400",
approver.hasRejected && "border-red-500"
)}
/>
)}
{index !== 0 && (
<div
className={twMerge(
"absolute left-8 top-0 h-5 border-r-2 border-gray-400",
approver.hasApproved && "border-green-400",
approver.hasRejected && "border-red-500"
)}
/>
)}
</div>
<div className="grid flex-grow grid-cols-3">
<div>
<div className="mb-1 text-xs font-semibold uppercase">Users</div>
<div>
{approver?.user
?.map(
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
)
.join(",") || "-"}
</div>
</div>
<div>
<div className="mb-1 text-xs font-semibold uppercase">Groups</div>
<div>
{approver?.group
?.map(
(el) =>
approverSequence?.projectGroupsGroupById?.[el.id]?.[0]?.group?.name
)
.join(",") || "-"}
</div>
</div>
<div className="flex items-center">
<div>
<div className="mb-1 text-xs font-semibold uppercase">Approvals Required</div>
<div>{approver.approvals || "-"}</div>
</div>
<div className="ml-16">
<Popover>
<PopoverTrigger>
<FontAwesomeIcon icon={faUsers} />
</PopoverTrigger>
<PopoverContent hideCloseBtn className="pt-3">
<div>
<div className="mb-1 text-sm text-bunker-300">Reviewers</div>
<div className="thin-scrollbar flex max-h-64 flex-col gap-1 overflow-y-auto rounded">
{approver.reviewers.map((el, idx) => (
<div
key={`reviewer-${idx + 1}`}
className="flex items-center gap-2 bg-mineshaft-700 p-1 text-sm"
>
<div className="flex-grow">{el.username}</div>
<Tooltip
content={`Status: ${el?.status || ApprovalStatus.PENDING}`}
>
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
</Tooltip>
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
<GenericFieldLabel className="col-span-2" label="Users">
{approver?.user
?.map(
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
)
.join(", ")}
</GenericFieldLabel>
<GenericFieldLabel className="col-span-2" label="Groups">
{approver?.group
?.map(
(el) =>
approverSequence?.projectGroupsGroupById?.[el.id]?.[0]?.group?.name
)
.join(", ")}
</GenericFieldLabel>
<GenericFieldLabel label="Approvals Required">
<div className="flex items-center">
<span className="mr-2">{approver.approvals}</span>
{BadgeComponent && (
<Tooltip
className="max-w-lg"
content={
<div>
<div className="mb-1 text-sm text-bunker-300">Reviewers</div>
<div className="thin-scrollbar flex max-h-64 flex-col divide-y divide-mineshaft-500 overflow-y-auto rounded">
{approver.reviewers.map((el, idx) => (
<div
key={`reviewer-${idx + 1}`}
className="flex items-center gap-2 px-2 py-2 text-sm"
>
<div className="flex-1">{el.username}</div>
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
</div>
))}
</div>
</div>
))}
</div>
</div>
</PopoverContent>
</Popover>
}
>
<div>{BadgeComponent}</div>
</Tooltip>
)}
</div>
</GenericFieldLabel>
</div>
</div>
</div>
</div>
))}
);
})}
</div>
{approverSequence.isMyReviewInThisSequence &&
request.status === ApprovalStatus.PENDING && (
<div className="mb-4 rounded-r border-l-2 border-l-primary-400 bg-mineshaft-300/5 px-4 py-2.5 text-sm">
Awaiting review from you.
</div>
)}
{shouldBlockRequestActions ? (
<div
className={twMerge(
"mb-4 rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm",
"mt-2 rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm",
isReviewedByMe && "border-l-green-400",
!approverSequence.isMyReviewInThisSequence && "border-l-primary-400",
hasRejected && "border-l-red-500"
@@ -409,41 +439,6 @@ export const ReviewAccessRequestModal = ({
</div>
) : (
<>
<div className="space-x-2">
<Button
isLoading={isLoading === "approved"}
isDisabled={
Boolean(isLoading) ||
(!(
request.isApprover &&
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
) &&
!bypassApproval)
}
onClick={() => handleReview("approved")}
className="mt-4"
size="sm"
colorSchema={!request.isApprover && isSoftEnforcement ? "danger" : "primary"}
>
Approve Request
</Button>
<Button
isLoading={isLoading === "rejected"}
isDisabled={
!!isLoading ||
(!(
request.isApprover &&
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
) &&
!bypassApproval)
}
onClick={() => handleReview("rejected")}
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
size="sm"
>
Reject Request
</Button>
</div>
{isSoftEnforcement &&
request.isRequestedByCurrentUser &&
!(request.isApprover && request.isSelfApproveAllowed) &&
@@ -453,11 +448,7 @@ export const ReviewAccessRequestModal = ({
onCheckedChange={(checked) => setBypassApproval(checked === true)}
isChecked={bypassApproval}
id="byPassApproval"
checkIndicatorBg="text-white"
className={twMerge(
"mr-2",
bypassApproval ? "border-red bg-red hover:bg-red-600" : ""
)}
className={twMerge("mr-2", bypassApproval ? "border-red/30 bg-red/10" : "")}
>
<span className="text-xs text-red">
Approve without waiting for requirements to be met (bypass policy
@@ -481,6 +472,42 @@ export const ReviewAccessRequestModal = ({
)}
</div>
)}
<div className="space-x-2">
<Button
isLoading={isLoading === "approved"}
isDisabled={
Boolean(isLoading) ||
(!(
request.isApprover &&
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
) &&
!bypassApproval)
}
onClick={() => handleReview("approved")}
className="mt-4"
size="sm"
variant="outline_bg"
colorSchema={!request.isApprover && isSoftEnforcement ? "danger" : "primary"}
>
Approve Request
</Button>
<Button
isLoading={isLoading === "rejected"}
isDisabled={
!!isLoading ||
(!(
request.isApprover &&
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
) &&
!bypassApproval)
}
onClick={() => handleReview("rejected")}
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
size="sm"
>
Reject Request
</Button>
</div>
</>
)}
</div>
@@ -1,5 +1,12 @@
import { useMemo } from "react";
import { faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
import {
faClipboardCheck,
faEdit,
faEllipsisV,
faTrash,
faUser,
faUserGroup
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
@@ -179,26 +186,40 @@ export const ApprovalPolicyRow = ({
}`}
>
<div className="p-4">
<div className="mb-4 border-b-2 border-mineshaft-500 pb-2">Approvers</div>
<div className="border-b-2 border-mineshaft-500 pb-2">Approvers</div>
{labels?.map((el, index) => (
<div
key={`approval-list-${index + 1}`}
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-800 p-4"
>
<div className="my-auto mr-8 flex h-8 w-8 items-center justify-center rounded border border-mineshaft-400 bg-bunker-500/50 text-white">
<div>{index + 1}</div>
</div>
{index !== labels.length - 1 && (
<div className="absolute bottom-0 left-8 h-[1.25rem] border-r border-mineshaft-400" />
<div key={`approval-list-${index + 1}`} className="flex">
{labels.length > 1 && (
<div className="flex w-12 flex-col items-center gap-2 pr-4">
<div
className={twMerge(
"flex-grow border-mineshaft-600",
index !== 0 && "border-r"
)}
/>
{labels.length > 1 && (
<Badge className="my-auto flex h-5 w-min min-w-5 items-center justify-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-center text-bunker-200">
<span>{index + 1}</span>
</Badge>
)}
<div
className={twMerge(
"flex-grow border-mineshaft-600",
index < labels.length - 1 && "border-r"
)}
/>
</div>
)}
{index !== 0 && (
<div className="absolute left-8 top-0 h-[1.25rem] border-r border-mineshaft-400" />
)}
<div className="grid flex-grow grid-cols-3">
<GenericFieldLabel label="Users">{el.userLabels}</GenericFieldLabel>
<GenericFieldLabel label="Groups">{el.groupLabels}</GenericFieldLabel>
<GenericFieldLabel label="Approvals Required">{el.approvals}</GenericFieldLabel>
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
{el.userLabels}
</GenericFieldLabel>
<GenericFieldLabel className="col-span-2" icon={faUserGroup} label="Groups">
{el.groupLabels}
</GenericFieldLabel>
<GenericFieldLabel icon={faClipboardCheck} label="Approvals Required">
{el.approvals}
</GenericFieldLabel>
</div>
</div>
))}
@@ -133,7 +133,6 @@ const Folder: React.FC<FolderProps> = ({
{!isDisabled && (
<Checkbox
id="folder-root"
className="data-[state=indeterminate]:bg-secondary data-[state=checked]:bg-primary"
isChecked={allSelected || someSelected}
onCheckedChange={handleFolderSelect}
isIndeterminate={someSelected && !allSelected}
@@ -167,7 +166,6 @@ const Folder: React.FC<FolderProps> = ({
{!isDisabled && (
<Checkbox
id={`folder-${item.id}`}
className="data-[state=indeterminate]:bg-secondary data-[state=checked]:bg-primary"
isChecked={selectedItemIds.includes(item.id)}
onCheckedChange={(checked) => onItemSelect(item, !!checked)}
isDisabled={isDisabled}
@@ -42,7 +42,6 @@ export const AutoCapitalizationSection = () => {
{(isAllowed) => (
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isDisabled={!isAllowed}
isChecked={currentWorkspace?.autoCapitalization ?? false}
@@ -38,7 +38,6 @@ export const DeleteProjectProtection = () => {
{(isAllowed) => (
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="hasDeleteProtection"
isDisabled={!isAllowed}
isChecked={currentWorkspace?.hasDeleteProtection ?? false}
@@ -48,7 +48,6 @@ export const SecretSharingSection = () => {
{(isAllowed) => (
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="secretSharing"
isDisabled={!isAllowed || isLoading}
isChecked={currentWorkspace?.secretSharing ?? true}
@@ -48,7 +48,6 @@ export const SecretSnapshotsLegacySection = () => {
{(isAllowed) => (
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="showSnapshotsLegacy"
isDisabled={!isAllowed || isLoading}
isChecked={currentWorkspace?.showSnapshotsLegacy ?? false}