Skip to content

Commit

Permalink
feat: check audit logs access via jimm
Browse files Browse the repository at this point in the history
  • Loading branch information
huwshimi committed Feb 19, 2025
1 parent 342f39b commit 8412417
Show file tree
Hide file tree
Showing 9 changed files with 393 additions and 205 deletions.
119 changes: 2 additions & 117 deletions src/pages/EntityDetails/EntityDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";

Expand All @@ -11,19 +11,14 @@ import {
configFactory,
} from "testing/factories/general";
import { modelListInfoFactory } from "testing/factories/juju/juju";
import {
controllerFactory,
modelFeaturesStateFactory,
modelFeaturesFactory,
} from "testing/factories/juju/juju";
import { controllerFactory } from "testing/factories/juju/juju";
import {
applicationInfoFactory,
modelWatcherModelDataFactory,
modelWatcherModelInfoFactory,
} from "testing/factories/juju/model-watcher";
import { renderComponent } from "testing/utils";
import urls from "urls";
import { ModelTab } from "urls";

import EntityDetails from "./EntityDetails";
import { Label } from "./types";
Expand Down Expand Up @@ -118,94 +113,6 @@ describe("Entity Details Container", () => {
).toBeInTheDocument();
});

it("lists the correct tabs", () => {
renderComponent(<EntityDetails />, { path, url, state });
expect(screen.getByTestId("view-selector")).toHaveTextContent(
/^ApplicationsIntegrationsLogsMachines$/,
);
});

it("lists the correct tabs for kubernetes", () => {
state.juju.modelWatcherData = {
abc123: modelWatcherModelDataFactory.build({
applications: {
"ceph-mon": applicationInfoFactory.build(),
},
model: modelWatcherModelInfoFactory.build({
name: "enterprise",
owner: "kirk@external",
type: "kubernetes",
}),
}),
};
renderComponent(<EntityDetails />, { path, url, state });
expect(screen.getByTestId("view-selector")).toHaveTextContent(
/^ApplicationsIntegrationsLogs$/,
);
});

it("lists the secrets tab", () => {
state.juju.modelFeatures = modelFeaturesStateFactory.build({
abc123: modelFeaturesFactory.build({
listSecrets: true,
}),
});
renderComponent(<EntityDetails />, { path, url, state });
expect(screen.getByTestId("view-selector")).toHaveTextContent(
/^ApplicationsIntegrationsLogsSecretsMachines$/,
);
});

it("clicking the tabs changes the visible section", async () => {
state.juju.modelFeatures = modelFeaturesStateFactory.build({
abc123: modelFeaturesFactory.build({
listSecrets: true,
}),
});
const { router } = renderComponent(<EntityDetails />, { path, url, state });
const viewSelector = screen.getByTestId("view-selector");
const sections = [
{
text: "Applications",
query: ModelTab.APPS,
},
{
text: "Integrations",
query: ModelTab.INTEGRATIONS,
},
{
text: "Logs",
query: ModelTab.LOGS,
},
{
text: "Machines",
query: ModelTab.MACHINES,
},
{
text: "Secrets",
query: ModelTab.SECRETS,
},
];
sections.forEach((section) => {
const scrollIntoView = vi.fn();
fireEvent.click(within(viewSelector).getByText(section.text), {
target: {
scrollIntoView,
},
});
expect(scrollIntoView.mock.calls[0]).toEqual([
{
behavior: "smooth",
block: "end",
inline: "nearest",
},
]);
expect(router.state.location.search).toEqual(
`?activeView=${section.query}`,
);
});
});

it("shows the supplied child", async () => {
const children = "Hello I am a child!";
renderComponent(<EntityDetails />, {
Expand Down Expand Up @@ -420,26 +327,4 @@ describe("Entity Details Container", () => {

window.location = location;
});

it("should navigate correctly when pressing Action Logs tab under Juju", () => {
state.general.config = configFactory.build({
isJuju: true,
});
const { router } = renderComponent(<EntityDetails />, { path, url, state });
const viewSelector = screen.getByTestId("view-selector");
const scrollIntoView = vi.fn();
fireEvent.click(within(viewSelector).getByText("Action Logs"), {
target: {
scrollIntoView,
},
});
expect(scrollIntoView.mock.calls[0]).toEqual([
{
behavior: "smooth",
block: "end",
inline: "nearest",
},
]);
expect(router.state.location.search).toEqual("?activeView=logs");
});
});
90 changes: 5 additions & 85 deletions src/pages/EntityDetails/EntityDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button, Notification, Strip, Tabs } from "@canonical/react-components";
import { Button, Notification, Strip } from "@canonical/react-components";
import classNames from "classnames";
import type { ReactNode, MouseEvent } from "react";
import type { ReactNode } from "react";
import { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { useParams, Link, Outlet } from "react-router";
Expand All @@ -11,24 +11,21 @@ import NotFound from "components/NotFound";
import type { EntityDetailsRoute } from "components/Routes";
import WebCLI from "components/WebCLI";
import { useEntityDetailsParams } from "components/hooks";
import { useQueryParams } from "hooks/useQueryParams";
import useWindowTitle from "hooks/useWindowTitle";
import BaseLayout from "layout/BaseLayout/BaseLayout";
import { getIsJuju, getUserPass } from "store/general/selectors";
import {
getCanListSecrets,
getControllerDataByUUID,
getModelInfo,
getModelListLoaded,
getModelUUIDFromList,
isKubernetesModel,
} from "store/juju/selectors";
import { useAppSelector } from "store/store";
import urls from "urls";
import { ModelTab } from "urls";
import { getMajorMinorVersion } from "utils";

import "./_entity-details.scss";
import ModelTabs from "./Model/ModelTabs";
import { Label, TestId } from "./types";

type Props = {
Expand All @@ -52,20 +49,10 @@ const EntityDetails = ({ modelWatcherError }: Props) => {
const modelsLoaded = useAppSelector(getModelListLoaded);
const modelUUID = useSelector(getModelUUIDFromList(modelName, userName));
const modelInfo = useSelector(getModelInfo(modelUUID));
const isK8s = useAppSelector((state) => isKubernetesModel(state, modelUUID));
const { isNestedEntityPage } = useEntityDetailsParams();

const isJuju = useSelector(getIsJuju);

const [query] = useQueryParams({
panel: null,
entity: null,
activeView: "apps",
filterQuery: "",
});

const { activeView } = query;

const [showWebCLI, setShowWebCLI] = useState(false);

// In a JAAS environment the controllerUUID will be the sub controller not
Expand All @@ -79,9 +66,6 @@ const EntityDetails = ({ modelWatcherError }: Props) => {
getControllerDataByUUID(controllerUUID),
);
const entityType = getEntityType(routeParams);
const canListSecrets = useAppSelector((state) =>
getCanListSecrets(state, modelUUID),
);
const credentials = useAppSelector((state) =>
getUserPass(state, primaryControllerData?.[0]),
);
Expand All @@ -92,14 +76,6 @@ const EntityDetails = ({ modelWatcherError }: Props) => {
.replace("/api", "") || null;
const wsProtocol = primaryControllerData?.[0].split("://")[0];

const handleNavClick = (e: MouseEvent) => {
(e.target as HTMLAnchorElement)?.scrollIntoView({
behavior: "smooth",
block: "end",
inline: "nearest",
});
};

useEffect(() => {
if (isJuju && getMajorMinorVersion(modelInfo?.version) >= 2.9) {
// The Web CLI is only available in Juju controller versions 2.9 and
Expand All @@ -112,57 +88,6 @@ const EntityDetails = ({ modelWatcherError }: Props) => {

useWindowTitle(modelInfo?.name ? `Model: ${modelInfo?.name}` : "...");

const generateTabItems = () => {
if (!userName || !modelName) {
return [];
}
const items = [
{
active: activeView === ModelTab.APPS,
label: "Applications",
onClick: (e: MouseEvent) => handleNavClick(e),
to: urls.model.tab({ userName, modelName, tab: ModelTab.APPS }),
component: Link,
},
{
active: activeView === ModelTab.INTEGRATIONS,
label: "Integrations",
onClick: (e: MouseEvent) => handleNavClick(e),
to: urls.model.tab({ userName, modelName, tab: ModelTab.INTEGRATIONS }),
component: Link,
},
{
active: activeView === "logs",
label: isJuju ? "Action Logs" : "Logs",
onClick: (e: MouseEvent) => handleNavClick(e),
to: urls.model.tab({ userName, modelName, tab: ModelTab.LOGS }),
component: Link,
},
];

if (canListSecrets) {
items.push({
active: activeView === "secrets",
label: "Secrets",
onClick: (e: MouseEvent) => handleNavClick(e),
to: urls.model.tab({ userName, modelName, tab: ModelTab.SECRETS }),
component: Link,
});
}

if (!isK8s) {
items.push({
active: activeView === ModelTab.MACHINES,
label: "Machines",
onClick: (e: MouseEvent) => handleNavClick(e),
to: urls.model.tab({ userName, modelName, tab: ModelTab.MACHINES }),
component: Link,
});
}

return items;
};

let content: ReactNode;
if (modelInfo) {
content = (
Expand Down Expand Up @@ -215,13 +140,8 @@ const EntityDetails = ({ modelWatcherError }: Props) => {
title={
<>
<Breadcrumb />
<div
className="entity-details__view-selector"
data-testid="view-selector"
>
{modelInfo && entityType === "model" && (
<Tabs links={generateTabItems()} />
)}
<div className="entity-details__view-selector">
{modelInfo && entityType === "model" ? <ModelTabs /> : null}
</div>
</>
}
Expand Down
50 changes: 48 additions & 2 deletions src/pages/EntityDetails/Model/Logs/Logs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@ import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { AuditLogsTableActionsLabel } from "components/AuditLogsTable/AuditLogsTableActions";
import { rootStateFactory } from "testing/factories";
import { configFactory, generalStateFactory } from "testing/factories/general";
import { JIMMRelation, JIMMTarget } from "juju/jimm/JIMMV4";
import { rootStateFactory, jujuStateFactory } from "testing/factories";
import {
configFactory,
generalStateFactory,
controllerFeaturesFactory,
controllerFeaturesStateFactory,
authUserInfoFactory,
} from "testing/factories/general";
import { rebacRelationFactory } from "testing/factories/juju/juju";
import { renderComponent } from "testing/utils";

import Logs from "./Logs";
Expand All @@ -28,9 +36,47 @@ describe("Logs", () => {
});

it("can display the audit logs tab", async () => {
const state = rootStateFactory.withGeneralConfig().build({
general: generalStateFactory.build({
config: configFactory.build({
controllerAPIEndpoint: "wss://jimm.jujucharms.com/api",
}),
controllerFeatures: controllerFeaturesStateFactory.build({
"wss://jimm.jujucharms.com/api": controllerFeaturesFactory.build({
auditLogs: true,
}),
}),
controllerConnections: {
"wss://jimm.jujucharms.com/api": {
user: authUserInfoFactory.build(),
},
},
}),
juju: jujuStateFactory.build({
rebacRelations: [
rebacRelationFactory.build({
tuple: {
object: "user-eggman@external",
relation: JIMMRelation.AUDIT_LOG_VIEWER,
target_object: JIMMTarget.JIMM_CONTROLLER,
},
allowed: true,
}),
rebacRelationFactory.build({
tuple: {
object: "user-eggman@external",
relation: JIMMRelation.ADMINISTRATOR,
target_object: JIMMTarget.JIMM_CONTROLLER,
},
allowed: true,
}),
],
}),
});
renderComponent(<Logs />, {
url: `${url}?activeView=logs&tableView=audit-logs`,
path,
state,
});

expect(screen.getByRole("tab", { name: "Audit logs" })).toHaveAttribute(
Expand Down
4 changes: 3 additions & 1 deletion src/pages/EntityDetails/Model/Logs/Logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ActionBar from "components/ActionBar";
import AuditLogsTableActions from "components/AuditLogsTable/AuditLogsTableActions";
import SegmentedControl from "components/SegmentedControl";
import { useQueryParams } from "hooks/useQueryParams";
import { useAuditLogsPermitted } from "juju/api-hooks/permissions";
import { getIsJuju } from "store/general/selectors";

import "./_logs.scss";
Expand All @@ -18,6 +19,7 @@ const BUTTON_DETAILS = [

const Logs = () => {
const isJuju = useSelector(getIsJuju);
const { permitted: auditLogsAllowed } = useAuditLogsPermitted();
const [{ tableView }, setQueryParams] = useQueryParams<{
activeView: string | null;
panel: string | null;
Expand Down Expand Up @@ -45,7 +47,7 @@ const Logs = () => {
</ActionBar>
)}
{tableView === "action-logs" && <ActionLogs />}
{!isJuju && tableView === "audit-logs" && <AuditLogs />}
{auditLogsAllowed && tableView === "audit-logs" && <AuditLogs />}
</div>
);
};
Expand Down
Loading

0 comments on commit 8412417

Please sign in to comment.