mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feature: add access requests to single env view, with general UI improvements
This commit is contained in:
@@ -40,7 +40,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: false,
|
secretApproval: true,
|
||||||
secretRotation: false,
|
secretRotation: false,
|
||||||
caCrl: false,
|
caCrl: false,
|
||||||
instanceUserManagement: false,
|
instanceUserManagement: false,
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export type SubscriptionPlan = {
|
|||||||
rbac: boolean;
|
rbac: boolean;
|
||||||
secretVersioning: boolean;
|
secretVersioning: boolean;
|
||||||
slug: string;
|
slug: string;
|
||||||
secretApproval: string;
|
secretApproval: boolean;
|
||||||
secretRotation: string;
|
secretRotation: boolean;
|
||||||
tier: number;
|
tier: number;
|
||||||
workspaceLimit: number;
|
workspaceLimit: number;
|
||||||
workspacesUsed: number;
|
workspacesUsed: number;
|
||||||
|
|||||||
62
frontend/src/hooks/usePathAccessPolicies.tsx
Normal file
62
frontend/src/hooks/usePathAccessPolicies.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { useSubscription, useWorkspace } from "@app/context";
|
||||||
|
import { useGetAccessApprovalPolicies } from "@app/hooks/api";
|
||||||
|
|
||||||
|
const matchesPath = (folderPath: string, pattern: string) => {
|
||||||
|
const normalizedPath = folderPath === "/" ? "/" : folderPath.replace(/\/$/, "");
|
||||||
|
const normalizedPattern = pattern === "/" ? "/" : pattern.replace(/\/$/, "");
|
||||||
|
|
||||||
|
console.log(normalizedPath, normalizedPattern);
|
||||||
|
|
||||||
|
if (normalizedPath === normalizedPattern) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedPattern.endsWith("/**")) {
|
||||||
|
const basePattern = normalizedPattern.slice(0, -3); // Remove "/**"
|
||||||
|
|
||||||
|
// Handle root wildcard "/**"
|
||||||
|
if (basePattern === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if path starts with the base pattern
|
||||||
|
if (normalizedPath === basePattern) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if path is a subdirectory of the base pattern
|
||||||
|
return normalizedPath.startsWith(`${basePattern}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePathAccessPolicies = ({ secretPath, environment }: Params) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const { data: policies } = useGetAccessApprovalPolicies({
|
||||||
|
projectSlug: currentWorkspace.slug,
|
||||||
|
options: {
|
||||||
|
enabled: subscription.secretApproval
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const pathPolicies = policies?.filter(
|
||||||
|
(policy) =>
|
||||||
|
policy.environment.slug === environment && matchesPath(secretPath, policy.secretPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasPathPolicies: subscription.secretApproval && Boolean(pathPolicies?.length),
|
||||||
|
pathPolicies
|
||||||
|
};
|
||||||
|
}, [secretPath, environment, policies, subscription.secretApproval]);
|
||||||
|
};
|
||||||
@@ -79,10 +79,14 @@ type TSecretPermissionForm = z.infer<typeof secretPermissionSchema>;
|
|||||||
export const SpecificPrivilegeSecretForm = ({
|
export const SpecificPrivilegeSecretForm = ({
|
||||||
privilege,
|
privilege,
|
||||||
policies,
|
policies,
|
||||||
onClose
|
onClose,
|
||||||
|
selectedActions = [],
|
||||||
|
secretPath: initialSecretPath
|
||||||
}: {
|
}: {
|
||||||
privilege?: TProjectUserPrivilege;
|
privilege?: TProjectUserPrivilege;
|
||||||
policies?: TAccessApprovalPolicy[];
|
policies?: TAccessApprovalPolicy[];
|
||||||
|
selectedActions?: ProjectPermissionActions[];
|
||||||
|
secretPath?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
@@ -126,10 +130,11 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
environmentSlug: currentWorkspace.environments?.[0]?.slug,
|
environmentSlug: currentWorkspace.environments?.[0]?.slug,
|
||||||
read: false,
|
secretPath: initialSecretPath,
|
||||||
edit: false,
|
read: selectedActions.includes(ProjectPermissionActions.Read),
|
||||||
create: false,
|
edit: selectedActions.includes(ProjectPermissionActions.Edit),
|
||||||
delete: false,
|
create: selectedActions.includes(ProjectPermissionActions.Create),
|
||||||
|
delete: selectedActions.includes(ProjectPermissionActions.Delete),
|
||||||
temporaryAccess: {
|
temporaryAccess: {
|
||||||
isTemporary: false
|
isTemporary: false
|
||||||
}
|
}
|
||||||
@@ -281,6 +286,8 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
isDisabled={isMemberEditDisabled}
|
isDisabled={isMemberEditDisabled}
|
||||||
className="w-full bg-mineshaft-900 hover:bg-mineshaft-800"
|
className="w-full bg-mineshaft-900 hover:bg-mineshaft-800"
|
||||||
onValueChange={(e) => onChange(e)}
|
onValueChange={(e) => onChange(e)}
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
>
|
>
|
||||||
{currentWorkspace?.environments?.map(({ slug, id, name }) => (
|
{currentWorkspace?.environments?.map(({ slug, id, name }) => (
|
||||||
<SelectItem value={slug} key={id}>
|
<SelectItem value={slug} key={id}>
|
||||||
@@ -309,6 +316,8 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
className="w-full hover:bg-mineshaft-800"
|
className="w-full hover:bg-mineshaft-800"
|
||||||
placeholder="Select a secret path"
|
placeholder="Select a secret path"
|
||||||
onValueChange={(e) => field.onChange(e)}
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
>
|
>
|
||||||
{selectablePaths.map((path) => (
|
{selectablePaths.map((path) => (
|
||||||
<SelectItem value={path} key={path}>
|
<SelectItem value={path} key={path}>
|
||||||
@@ -636,6 +645,7 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
{!!policies && (
|
{!!policies && (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="outline_bg"
|
||||||
isLoading={privilegeForm.formState.isSubmitting || requestAccess.isPending}
|
isLoading={privilegeForm.formState.isSubmitting || requestAccess.isPending}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
isMemberEditDisabled ||
|
isMemberEditDisabled ||
|
||||||
@@ -647,7 +657,7 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
className="mt-4"
|
className="mt-4"
|
||||||
leftIcon={<FontAwesomeIcon icon={faLockOpen} />}
|
leftIcon={<FontAwesomeIcon icon={faLockOpen} />}
|
||||||
>
|
>
|
||||||
Request access
|
Request Access
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { Modal, ModalContent } from "@app/components/v2";
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
|
||||||
|
import { ProjectPermissionActions } from "@app/context";
|
||||||
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
||||||
import { SpecificPrivilegeSecretForm } from "@app/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection";
|
import { SpecificPrivilegeSecretForm } from "@app/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection";
|
||||||
|
|
||||||
export const RequestAccessModal = ({
|
export const RequestAccessModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
policies
|
policies,
|
||||||
|
shouldShowBanner,
|
||||||
|
...props
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
policies: TAccessApprovalPolicy[];
|
policies: TAccessApprovalPolicy[];
|
||||||
|
selectedActions?: ProjectPermissionActions[];
|
||||||
|
secretPath?: string;
|
||||||
|
shouldShowBanner?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
@@ -18,7 +25,21 @@ export const RequestAccessModal = ({
|
|||||||
title="Request Access"
|
title="Request Access"
|
||||||
subTitle="Request access to any secrets and resources based on the predefined policies."
|
subTitle="Request access to any secrets and resources based on the predefined policies."
|
||||||
>
|
>
|
||||||
<SpecificPrivilegeSecretForm onClose={() => onOpenChange(false)} policies={policies} />
|
{shouldShowBanner && (
|
||||||
|
<NoticeBannerV2
|
||||||
|
className="mb-3"
|
||||||
|
title="You do not have permission to perform this action"
|
||||||
|
>
|
||||||
|
<p className="text-sm text-mineshaft-300">
|
||||||
|
Request access to gain access to this action.
|
||||||
|
</p>
|
||||||
|
</NoticeBannerV2>
|
||||||
|
)}
|
||||||
|
<SpecificPrivilegeSecretForm
|
||||||
|
onClose={() => onOpenChange(false)}
|
||||||
|
policies={policies}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faArrowDown, faArrowUp } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowDown, faArrowUp, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
@@ -10,10 +10,12 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
import { PermissionDeniedBanner } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
ContentLoader,
|
ContentLoader,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
|
PageHeader,
|
||||||
Pagination,
|
Pagination,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
@@ -46,7 +48,9 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
|
|||||||
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||||
import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits";
|
import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
|
||||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||||
|
import { RequestAccessModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/RequestAccessModal";
|
||||||
import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView";
|
import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView";
|
||||||
|
|
||||||
import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount";
|
import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount";
|
||||||
@@ -114,7 +118,10 @@ const Page = () => {
|
|||||||
|
|
||||||
const [snapshotId, setSnapshotId] = useState<string | null>(null);
|
const [snapshotId, setSnapshotId] = useState<string | null>(null);
|
||||||
const isRollbackMode = Boolean(snapshotId);
|
const isRollbackMode = Boolean(snapshotId);
|
||||||
const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["snapshots"] as const);
|
const { popUp, handlePopUpClose, handlePopUpToggle, handlePopUpOpen } = usePopUp([
|
||||||
|
"snapshots",
|
||||||
|
"requestAccess"
|
||||||
|
] as const);
|
||||||
|
|
||||||
// env slug
|
// env slug
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
@@ -132,6 +139,26 @@ const Page = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canEditSecrets = permission.can(
|
||||||
|
ProjectPermissionSecretActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: "*",
|
||||||
|
secretTags: ["*"]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const canDeleteSecrets = permission.can(
|
||||||
|
ProjectPermissionSecretActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: "*",
|
||||||
|
secretTags: ["*"]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
|
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
|
||||||
permission,
|
permission,
|
||||||
ProjectPermissionSecretActions.ReadValue,
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
@@ -257,6 +284,8 @@ const Page = () => {
|
|||||||
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? workspaceId : ""
|
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? workspaceId : ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { pathPolicies, hasPathPolicies } = usePathAccessPolicies({ secretPath, environment });
|
||||||
|
|
||||||
const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({
|
const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -476,8 +505,55 @@ const Page = () => {
|
|||||||
setFilter(defaultFilterState);
|
setFilter(defaultFilterState);
|
||||||
setDebouncedSearchFilter("");
|
setDebouncedSearchFilter("");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
|
<PageHeader
|
||||||
|
title={
|
||||||
|
currentWorkspace.environments.find((env) => env.slug === environment)?.name ?? environment
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<p className="text-md text-bunker-300">
|
||||||
|
Inject your secrets using
|
||||||
|
<a
|
||||||
|
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||||
|
href="https://infisical.com/docs/cli/overview"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Infisical CLI
|
||||||
|
</a>
|
||||||
|
,
|
||||||
|
<a
|
||||||
|
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||||
|
href="https://infisical.com/docs/documentation/getting-started/api"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Infisical API
|
||||||
|
</a>
|
||||||
|
,
|
||||||
|
<a
|
||||||
|
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||||
|
href="https://infisical.com/docs/sdks/overview"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Infisical SDKs
|
||||||
|
</a>
|
||||||
|
, and
|
||||||
|
<a
|
||||||
|
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||||
|
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
more
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<SecretV2MigrationSection />
|
<SecretV2MigrationSection />
|
||||||
{!isRollbackMode ? (
|
{!isRollbackMode ? (
|
||||||
<>
|
<>
|
||||||
@@ -500,8 +576,15 @@ const Page = () => {
|
|||||||
importedBy={importedBy}
|
importedBy={importedBy}
|
||||||
usedBySecretSyncs={usedBySecretSyncs}
|
usedBySecretSyncs={usedBySecretSyncs}
|
||||||
isPITEnabled={isPITEnabled}
|
isPITEnabled={isPITEnabled}
|
||||||
|
hasPathPolicies={hasPathPolicies}
|
||||||
|
onRequestAccess={(params) => handlePopUpOpen("requestAccess", params)}
|
||||||
/>
|
/>
|
||||||
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md bg-mineshaft-800 text-left text-sm text-bunker-300",
|
||||||
|
isNotEmpty && "rounded-b-none"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex flex-col" id="dashboard">
|
<div className="flex flex-col" id="dashboard">
|
||||||
{isNotEmpty && (
|
{isNotEmpty && (
|
||||||
<div
|
<div
|
||||||
@@ -548,6 +631,68 @@ const Page = () => {
|
|||||||
<div className="flex-grow px-4 py-2">Value</div>
|
<div className="flex-grow px-4 py-2">Value</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{hasPathPolicies &&
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
(!canReadSecret ? (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex border-l-2 border-l-primary bg-mineshaft-700 px-4 py-2",
|
||||||
|
isNotEmpty ? "border-b border-b-mineshaft-600" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center text-sm">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faInfoCircle}
|
||||||
|
className="ml-[0.15rem] mr-[1.65rem] text-primary"
|
||||||
|
/>
|
||||||
|
<span>You do not permission to read secrets for this path</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
className="ml-auto"
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("requestAccess", {
|
||||||
|
actions: [ProjectPermissionActions.Read],
|
||||||
|
shouldShowBanner: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Request Access
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : !canEditSecrets || !canDeleteSecrets ? (
|
||||||
|
<div className="flex border-b border-l-2 border-b-mineshaft-600 border-l-primary bg-mineshaft-700 px-4 py-2">
|
||||||
|
<div className="flex items-center text-sm">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faInfoCircle}
|
||||||
|
className="ml-[0.15rem] mr-[1.65rem] text-primary"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
You do not permission to {!canEditSecrets ? "edit" : ""}
|
||||||
|
{!canEditSecrets && !canDeleteSecrets ? " or " : ""}
|
||||||
|
{!canDeleteSecrets ? "delete" : ""} secrets for this path
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
className="ml-auto"
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("requestAccess", {
|
||||||
|
actions: [
|
||||||
|
...(!canEditSecrets ? [ProjectPermissionActions.Edit] : []),
|
||||||
|
...(!canDeleteSecrets ? [ProjectPermissionActions.Delete] : [])
|
||||||
|
],
|
||||||
|
shouldShowBanner: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Request Access
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null)}
|
||||||
|
|
||||||
{canReadSecretImports && Boolean(imports?.length) && (
|
{canReadSecretImports && Boolean(imports?.length) && (
|
||||||
<SecretImportListView
|
<SecretImportListView
|
||||||
searchTerm={debouncedSearchFilter}
|
searchTerm={debouncedSearchFilter}
|
||||||
@@ -636,6 +781,18 @@ const Page = () => {
|
|||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
{!!pathPolicies && (
|
||||||
|
<RequestAccessModal
|
||||||
|
policies={pathPolicies}
|
||||||
|
isOpen={popUp.requestAccess.isOpen}
|
||||||
|
onOpenChange={() => {
|
||||||
|
handlePopUpClose("requestAccess");
|
||||||
|
}}
|
||||||
|
selectedActions={popUp.requestAccess.data?.actions}
|
||||||
|
shouldShowBanner={popUp.requestAccess.data?.shouldShowBanner}
|
||||||
|
secretPath={pathPolicies?.[0]?.secretPath}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SecretDropzone
|
<SecretDropzone
|
||||||
environment={environment}
|
environment={environment}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
|
|||||||
@@ -53,11 +53,13 @@ import {
|
|||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
useSubscription,
|
useSubscription,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionCommitsActions,
|
ProjectPermissionCommitsActions,
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSecretRotationActions
|
ProjectPermissionSecretRotationActions
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
@@ -127,6 +129,11 @@ type Props = {
|
|||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
isPITEnabled: boolean;
|
isPITEnabled: boolean;
|
||||||
|
onRequestAccess: (params: {
|
||||||
|
actions: ProjectPermissionActions[];
|
||||||
|
shouldShowBanner: boolean;
|
||||||
|
}) => void;
|
||||||
|
hasPathPolicies: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActionBar = ({
|
export const ActionBar = ({
|
||||||
@@ -147,7 +154,9 @@ export const ActionBar = ({
|
|||||||
protectedBranchPolicyName,
|
protectedBranchPolicyName,
|
||||||
importedBy,
|
importedBy,
|
||||||
isPITEnabled = false,
|
isPITEnabled = false,
|
||||||
usedBySecretSyncs
|
usedBySecretSyncs,
|
||||||
|
onRequestAccess,
|
||||||
|
hasPathPolicies
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||||
"addFolder",
|
"addFolder",
|
||||||
@@ -180,6 +189,7 @@ export const ActionBar = ({
|
|||||||
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
|
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
|
||||||
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||||
try {
|
try {
|
||||||
@@ -807,27 +817,53 @@ export const ActionBar = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<ProjectPermissionCan
|
{hasPathPolicies ? (
|
||||||
I={ProjectPermissionActions.Create}
|
<Button
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
variant="outline_bg"
|
||||||
environment,
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
secretPath,
|
onClick={() =>
|
||||||
secretName: "*",
|
permission.can(
|
||||||
secretTags: ["*"]
|
ProjectPermissionSecretActions.Create,
|
||||||
})}
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
>
|
environment,
|
||||||
{(isAllowed) => (
|
secretPath,
|
||||||
<Button
|
secretName: "*",
|
||||||
variant="outline_bg"
|
secretTags: ["*"]
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
})
|
||||||
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
|
)
|
||||||
className="h-10 rounded-r-none"
|
? openPopUp(PopUpNames.CreateSecretForm)
|
||||||
isDisabled={!isAllowed}
|
: onRequestAccess({
|
||||||
>
|
actions: [ProjectPermissionActions.Create],
|
||||||
Add Secret
|
shouldShowBanner: true
|
||||||
</Button>
|
})
|
||||||
)}
|
}
|
||||||
</ProjectPermissionCan>
|
className="h-10 rounded-r-none"
|
||||||
|
>
|
||||||
|
Add Secret
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: "*",
|
||||||
|
secretTags: ["*"]
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
|
||||||
|
className="h-10 rounded-r-none"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Add Secret
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
open={popUp.misc.isOpen}
|
open={popUp.misc.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ export const SecretDropzone = ({
|
|||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 px-2 py-4 text-sm text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
|
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 px-2 py-4 text-sm text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
|
||||||
isDragActive && "opacity-100",
|
isDragActive && "opacity-100",
|
||||||
!isSmaller && "mx-auto w-full max-w-3xl flex-col space-y-4 py-20",
|
!isSmaller && "mx-auto mt-40 w-full max-w-3xl flex-col space-y-4 py-20",
|
||||||
isLoading && "bg-bunker-800"
|
isLoading && "bg-bunker-800"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user