Skip to content

Commit

Permalink
minor: add permissions guard for unlinking projects
Browse files Browse the repository at this point in the history
  • Loading branch information
ciyer committed Oct 25, 2024
1 parent a0e50bc commit 0d23c30
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 51 deletions.
157 changes: 107 additions & 50 deletions client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ import {
Row,
} from "reactstrap";

import { Loader } from "../../../components/Loader";
import { ButtonWithMenuV2 } from "../../../components/buttons/Button";
import { RtkOrNotebooksError } from "../../../components/errors/RtkErrorAlert";
import { Loader } from "../../../components/Loader";
import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants";
import useAppDispatch from "../../../utils/customHooks/useAppDispatch.hook";

import useGroupPermissions from "../../groupsV2/utils/useGroupPermissions.hook";
import PermissionsGuard from "../../permissionsV2/PermissionsGuard";
import useProjectPermissions from "../../ProjectPageV2/utils/useProjectPermissions.hook";
import { projectV2Api } from "../../projectsV2/api/projectV2.enhanced-api";

import type {
Expand All @@ -52,6 +54,7 @@ import {

import DataConnectorCredentialsModal from "./DataConnectorCredentialsModal";
import DataConnectorModal from "./DataConnectorModal";
import { useGetProjectsByNamespaceAndSlugQuery } from "../../projectsV2/api/projectV2.api";

interface DataConnectorRemoveModalProps {
dataConnector: DataConnectorRead;
Expand Down Expand Up @@ -194,7 +197,7 @@ function DataConnectorRemoveDeleteModal({
<Button
color="danger"
className={cx("float-right", "ms-2")}
disabled={typedName !== dataConnector.slug.trim()}
disabled={isLoading || typedName !== dataConnector.slug.trim()}
data-cy="delete-data-connector-modal-button"
type="submit"
onClick={onDeleteDataCollector}
Expand Down Expand Up @@ -224,20 +227,30 @@ function DataConnectorRemoveDeleteModal({
interface DataConnectorRemoveUnlinkModalProps
extends Omit<DataConnectorRemoveModalProps, "dataConnectorLink"> {
dataConnectorLink: DataConnectorToProjectLink;
projectId: string;
projectNamespace: string;
projectSlug: string;
}

function DataConnectorRemoveUnlinkModal({
dataConnector,
dataConnectorLink,
projectId,
onDelete,
projectNamespace,
projectSlug,
toggleModal,
isOpen,
}: DataConnectorRemoveUnlinkModalProps) {
const dispatch = useAppDispatch();
const [unlinkDataConnector, { isLoading: isLoadingUnlink, isSuccess }] =
useDeleteDataConnectorsByDataConnectorIdProjectLinksAndLinkIdMutation();
const [
unlinkDataConnector,
{ isLoading: isLoadingUnlink, isSuccess, error },
] = useDeleteDataConnectorsByDataConnectorIdProjectLinksAndLinkIdMutation();
const { data: project, isLoading: isLoadingProject } =
useGetProjectsByNamespaceAndSlugQuery({
namespace: projectNamespace,
slug: projectSlug,
});
const permissions = useProjectPermissions({ projectId: project?.id ?? "" });

const linkId = dataConnectorLink.id;

Expand All @@ -262,48 +275,92 @@ function DataConnectorRemoveUnlinkModal({
<ModalHeader className="text-danger" toggle={toggleModal}>
Unlink data connector
</ModalHeader>
<ModalBody>
<Row>
<Col>
<p>
Are you sure you want to unlink the data connector{" "}
<strong>{dataConnector.slug}</strong> from the project{" "}
<strong>{projectId}</strong>?
</p>
<p>
The data from the data connector will no longer be available in
sessions.
</p>
</Col>
</Row>
</ModalBody>
<ModalFooter>
<div className="d-flex justify-content-end">
<Button color="outline-danger" onClick={toggleModal}>
<XLg className={cx("bi", "me-1")} />
Cancel
</Button>
<Button
color="danger"
className={cx("float-right", "ms-2")}
data-cy="delete-data-connector-modal-button"
type="submit"
onClick={onDeleteDataCollector}
>
{isLoadingUnlink ? (
<>
<Loader className="me-1" inline size={16} />
Unlinking data connector
</>
) : (
<>
<NodeMinus className={cx("bi", "me-1")} />
Unlink data connector
</>
)}
</Button>
</div>
</ModalFooter>
{isLoadingProject ? (
<>
<ModalBody>
<Loader />
</ModalBody>
<ModalFooter></ModalFooter>
</>
) : (
<>
<ModalBody>
<Row>
<Col>
{permissions == null || permissions["write"] != true ? (
<p>
You do not have the required permissions to unlink this data
connector.
</p>
) : (
<>
<p>
Are you sure you want to unlink the data connector{" "}
<strong>{dataConnector.slug}</strong> from the project{" "}
<strong>
{projectNamespace}/{projectSlug}
</strong>
?
</p>
<p>
The data from the data connector will no longer be
available in sessions.
</p>
</>
)}
</Col>
</Row>
</ModalBody>
<ModalFooter>
{error && <RtkOrNotebooksError error={error} />}
<div className="d-flex justify-content-end">
<Button color="outline-danger" onClick={toggleModal}>
<XLg className={cx("bi", "me-1")} />
Cancel
</Button>
<PermissionsGuard
disabled={
<Button
color="danger"
className={cx("float-right", "ms-2")}
disabled={true}
data-cy="delete-data-connector-modal-button"
onClick={toggleModal}
>
<NodeMinus className={cx("bi", "me-1")} />
Unlink data connector
</Button>
}
enabled={
<Button
color="danger"
className={cx("float-right", "ms-2")}
disabled={isLoadingUnlink}
data-cy="delete-data-connector-modal-button"
type="submit"
onClick={onDeleteDataCollector}
>
{isLoadingUnlink ? (
<>
<Loader className="me-1" inline size={16} />
Unlinking data connector
</>
) : (
<>
<NodeMinus className={cx("bi", "me-1")} />
Unlink data connector
</>
)}
</Button>
}
requestedPermission="write"
userPermissions={permissions}
/>
</div>
</ModalFooter>
</>
)}
;
</Modal>
);
}
Expand All @@ -324,7 +381,6 @@ export default function DataConnectorActions({
);
const namespace = pathMatch?.params?.namespace;
const slug = pathMatch?.params?.slug;
const projectId = `${namespace}/${slug}`;
const removeMode =
pathMatch === null ||
namespace == null ||
Expand Down Expand Up @@ -377,7 +433,8 @@ export default function DataConnectorActions({
dataConnectorLink={dataConnectorLink!}
isOpen={isDeleteOpen}
onDelete={onDelete}
projectId={projectId}
projectNamespace={namespace!}
projectSlug={slug!}
toggleModal={toggleDelete}
/>
);
Expand Down
25 changes: 24 additions & 1 deletion tests/cypress/e2e/projectV2setup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ describe("Set up data connectors", () => {
).should("be.visible");
});

it("delete a data connector", () => {
it("unlink a data connector", () => {
fixtures
.readProjectV2({ fixture: "projectV2/read-projectV2-empty.json" })
.listProjectDataConnectors()
Expand All @@ -289,6 +289,7 @@ describe("Set up data connectors", () => {
cy.contains("example storage").should("be.visible").click();
openDataConnectorMenu();
cy.getDataCy("data-connector-delete").should("be.visible").click();
cy.wait("@getProjectV2Permissions");
cy.contains("Are you sure you want to unlink the data connector").should(
"be.visible"
);
Expand All @@ -298,4 +299,26 @@ describe("Set up data connectors", () => {
cy.wait("@deleteDataConnectorProjectLink");
cy.wait("@listProjectDataConnectors");
});

it("unlink data connector not allowed", () => {
fixtures
.readProjectV2({ fixture: "projectV2/read-projectV2-empty.json" })
.listProjectDataConnectors()
.getDataConnector()
.getProjectV2Permissions({
fixture: "projectV2/projectV2-permissions-viewer.json",
})
.deleteDataConnectorProjectLinkNotAllowed();
cy.visit("/v2/projects/user1-uuid/test-2-v2-project");
cy.wait("@readProjectV2");
cy.wait("@listProjectDataConnectors");

cy.contains("example storage").should("be.visible").click();
openDataConnectorMenu();
cy.getDataCy("data-connector-delete").should("be.visible").click();
cy.contains(
"You do not have the required permissions to unlink this data connector."
).should("be.visible");
cy.getDataCy("delete-data-connector-modal-button").should("be.disabled");
});
});
27 changes: 27 additions & 0 deletions tests/cypress/support/renkulab-fixtures/dataConnectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface DataConnectorIdentifierArgs extends SimpleFixture {

interface DeleteDataConnectorProjectLinkArgs extends DataConnectorIdArgs {
linkId?: string;
projectId?: string;
}

interface PatchDataConnectorSecretsArgs extends DataConnectorIdArgs {
Expand Down Expand Up @@ -104,6 +105,32 @@ export function DataConnector<T extends FixturesConstructor>(Parent: T) {
return this;
}

deleteDataConnectorProjectLinkNotAllowed(
args?: DeleteDataConnectorProjectLinkArgs
) {
const {
name = "deleteDataConnectorProjectLinkNotAllowed",
dataConnectorId = "ULID-1",
linkId = "LINK-ULID-1",
projectId = "THEPROJECTULID26CHARACTERS",
} = args ?? {};
const response = {
body: {
error: {
code: 1404,
message: `The user with ID 'userId' cannot perform operation delete_link on the data connector to project link with ID ${projectId} or the resource does not exist.`,
},
},
statusCode: 404,
};
cy.intercept(
"DELETE",
`/ui-server/api/data/data_connectors/${dataConnectorId}/project_links/${linkId}`,
response
).as(name);
return this;
}

deleteDataConnectorSecrets(args?: DataConnectorIdArgs) {
const {
name = "deleteDataConnectorSecrets",
Expand Down

0 comments on commit 0d23c30

Please sign in to comment.