diff --git a/coderd/rbac/builtin.go b/coderd/rbac/builtin.go index cd51d88361636..61e5a8d544712 100644 --- a/coderd/rbac/builtin.go +++ b/coderd/rbac/builtin.go @@ -88,6 +88,7 @@ var ( // Should be able to read all template details, even in orgs they // are not in. ResourceTemplate: {ActionRead}, + ResourceAuditLog: {ActionRead}, }), } }, diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 6106dd8079015..88f342d286e41 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -22,6 +22,12 @@ var ( Type: "workspace", } + // ResourceAuditLog + // read = access audit log + ResourceAuditLog = Object{ + Type: "audit_log", + } + // ResourceTemplate CRUD. Org owner only. // create/delete = Make or delete a new template // update = Update the template, make new template versions diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index b7b0d0e6b9cf0..332f283505989 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -1,5 +1,8 @@ -import { FC, lazy, Suspense } from "react" +import { useSelector } from "@xstate/react" +import { FC, lazy, Suspense, useContext } from "react" import { Navigate, Route, Routes } from "react-router-dom" +import { selectPermissions } from "xServices/auth/authSelectors" +import { XServiceContext } from "xServices/StateContext" import { AuthAndFrame } from "./components/AuthAndFrame/AuthAndFrame" import { RequireAuth } from "./components/RequireAuth/RequireAuth" import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout" @@ -27,167 +30,172 @@ const WorkspacesPage = lazy(() => import("./pages/WorkspacesPage/WorkspacesPage" const CreateWorkspacePage = lazy(() => import("./pages/CreateWorkspacePage/CreateWorkspacePage")) const AuditPage = lazy(() => import("./pages/AuditPage/AuditPage")) -export const AppRouter: FC = () => ( - }> - - - - - } - /> +export const AppRouter: FC = () => { + const xServices = useContext(XServiceContext) + const permissions = useSelector(xServices.authXService, selectPermissions) - } /> - } /> - - - - } - /> - - + return ( + }> + - - + + + } /> - - + } /> + } /> - - + + + } /> - + - + } /> - - - - } - /> - - - - - - - } - /> - - - - } - /> - - {/* REMARK: Route under construction - Eventually, we should gate this page - with permissions and licensing */} - - - ) : ( + + - + - ) - } - > - + } + /> - }> - } /> - } /> - } /> - + + + + + } + /> + + + + } + /> + + - - + - + } /> - + } /> + + {/* REMARK: Route under construction + Eventually, we should gate this page + with permissions and licensing */} + - - + process.env.NODE_ENV === "production" || !permissions?.viewAuditLog ? ( + + ) : ( + + + + ) } - /> + > + + + }> + } /> + } /> + } /> + - + + - + } /> - + + + + } + /> - - - - } - /> + + + + } + /> + + + + + + } + /> + + + + + + } + /> + - - {/* Using path="*"" means "match anything", so this route + {/* Using path="*"" means "match anything", so this route acts like a catch-all for URLs that we don't have explicit routes for. */} - } /> - - -) + } /> + + + ) +} diff --git a/site/src/components/Navbar/Navbar.tsx b/site/src/components/Navbar/Navbar.tsx index 0ac64ef7d1269..cbfdfd949dd19 100644 --- a/site/src/components/Navbar/Navbar.tsx +++ b/site/src/components/Navbar/Navbar.tsx @@ -6,8 +6,14 @@ import { NavbarView } from "../NavbarView/NavbarView" export const Navbar: React.FC = () => { const xServices = useContext(XServiceContext) const [authState, authSend] = useActor(xServices.authXService) - const { me } = authState.context + const { me, permissions } = authState.context const onSignOut = () => authSend("SIGN_OUT") - return + return ( + + ) } diff --git a/site/src/components/NavbarView/NavbarView.test.tsx b/site/src/components/NavbarView/NavbarView.test.tsx index c0755dbda196e..a3c3c8861bfdd 100644 --- a/site/src/components/NavbarView/NavbarView.test.tsx +++ b/site/src/components/NavbarView/NavbarView.test.tsx @@ -1,5 +1,5 @@ import { screen } from "@testing-library/react" -import { MockUser } from "../../testHelpers/entities" +import { MockUser, MockUser2 } from "../../testHelpers/entities" import { render } from "../../testHelpers/renderHelpers" import { Language as navLanguage, NavbarView } from "./NavbarView" @@ -22,26 +22,26 @@ describe("NavbarView", () => { it("renders content", async () => { // When - render() + render() // Then await screen.findAllByText("Coder", { exact: false }) }) it("workspaces nav link has the correct href", async () => { - render() + render() const workspacesLink = await screen.findByText(navLanguage.workspaces) expect((workspacesLink as HTMLAnchorElement).href).toContain("/workspaces") }) it("templates nav link has the correct href", async () => { - render() + render() const templatesLink = await screen.findByText(navLanguage.templates) expect((templatesLink as HTMLAnchorElement).href).toContain("/templates") }) it("users nav link has the correct href", async () => { - render() + render() const userLink = await screen.findByText(navLanguage.users) expect((userLink as HTMLAnchorElement).href).toContain("/users") }) @@ -54,7 +54,7 @@ describe("NavbarView", () => { } // When - render() + render() // Then // There should be a 'B' avatar! @@ -63,7 +63,7 @@ describe("NavbarView", () => { }) it("audit nav link has the correct href", async () => { - render() + render() const auditLink = await screen.findByText(navLanguage.audit) expect((auditLink as HTMLAnchorElement).href).toContain("/audit") }) @@ -74,7 +74,13 @@ describe("NavbarView", () => { NODE_ENV: "production", } - render() + render() + const auditLink = screen.queryByText(navLanguage.audit) + expect(auditLink).not.toBeInTheDocument() + }) + + it("audit nav link is hidden for members", async () => { + render() const auditLink = screen.queryByText(navLanguage.audit) expect(auditLink).not.toBeInTheDocument() }) diff --git a/site/src/components/NavbarView/NavbarView.tsx b/site/src/components/NavbarView/NavbarView.tsx index 98dc8dd985e44..0e26ea9a21b94 100644 --- a/site/src/components/NavbarView/NavbarView.tsx +++ b/site/src/components/NavbarView/NavbarView.tsx @@ -15,6 +15,7 @@ import { UserDropdown } from "../UserDropdown/UsersDropdown" export interface NavbarViewProps { user?: TypesGen.User onSignOut: () => void + canViewAuditLog: boolean } export const Language = { @@ -24,7 +25,10 @@ export const Language = { audit: "Audit", } -const NavItems: React.FC<{ className?: string; linkClassName?: string }> = ({ className }) => { +const NavItems: React.FC<{ className?: string; canViewAuditLog: boolean }> = ({ + className, + canViewAuditLog, +}) => { const styles = useStyles() const location = useLocation() @@ -49,7 +53,7 @@ const NavItems: React.FC<{ className?: string; linkClassName?: string }> = ({ cl {/* REMARK: the below link is under-construction */} - {process.env.NODE_ENV !== "production" && ( + {process.env.NODE_ENV !== "production" && canViewAuditLog && ( {Language.audit} @@ -60,7 +64,7 @@ const NavItems: React.FC<{ className?: string; linkClassName?: string }> = ({ cl ) } -export const NavbarView: React.FC = ({ user, onSignOut }) => { +export const NavbarView: React.FC = ({ user, onSignOut, canViewAuditLog }) => { const styles = useStyles() const [isDrawerOpen, setIsDrawerOpen] = useState(false) @@ -81,7 +85,7 @@ export const NavbarView: React.FC = ({ user, onSignOut }) => {
- + @@ -89,7 +93,7 @@ export const NavbarView: React.FC = ({ user, onSignOut }) => {
- +
{user && } diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index cf0a9432ea33a..f07660885b275 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -14,6 +14,7 @@ export const checks = { updateUsers: "updateUsers", createUser: "createUser", createTemplates: "createTemplates", + viewAuditLog: "viewAuditLog", } as const export const permissionsToCheck = { @@ -41,6 +42,12 @@ export const permissionsToCheck = { }, action: "write", }, + [checks.viewAuditLog]: { + object: { + resource_type: "audit_log", + }, + action: "read", + }, } as const type Permissions = Record