mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feature: gate audit log by permissions (#3464)
* pairing * restricting audit route resolvees #3460 * updated tests * fixing lint * useSelector instead of useActor
This commit is contained in:
@ -88,6 +88,7 @@ var (
|
||||
// Should be able to read all template details, even in orgs they
|
||||
// are not in.
|
||||
ResourceTemplate: {ActionRead},
|
||||
ResourceAuditLog: {ActionRead},
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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,7 +30,11 @@ 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 (
|
||||
<Suspense fallback={<></>}>
|
||||
<Routes>
|
||||
<Route
|
||||
@ -117,7 +124,7 @@ export const AppRouter: FC = () => (
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
process.env.NODE_ENV === "production" ? (
|
||||
process.env.NODE_ENV === "production" || !permissions?.viewAuditLog ? (
|
||||
<Navigate to="/workspaces" />
|
||||
) : (
|
||||
<AuthAndFrame>
|
||||
@ -191,3 +198,4 @@ export const AppRouter: FC = () => (
|
||||
</Routes>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
@ -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 <NavbarView user={me} onSignOut={onSignOut} />
|
||||
return (
|
||||
<NavbarView
|
||||
user={me}
|
||||
onSignOut={onSignOut}
|
||||
canViewAuditLog={permissions?.viewAuditLog ?? false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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(<NavbarView user={MockUser} onSignOut={noop} />)
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} canViewAuditLog />)
|
||||
|
||||
// Then
|
||||
await screen.findAllByText("Coder", { exact: false })
|
||||
})
|
||||
|
||||
it("workspaces nav link has the correct href", async () => {
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} />)
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} canViewAuditLog />)
|
||||
const workspacesLink = await screen.findByText(navLanguage.workspaces)
|
||||
expect((workspacesLink as HTMLAnchorElement).href).toContain("/workspaces")
|
||||
})
|
||||
|
||||
it("templates nav link has the correct href", async () => {
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} />)
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} canViewAuditLog />)
|
||||
const templatesLink = await screen.findByText(navLanguage.templates)
|
||||
expect((templatesLink as HTMLAnchorElement).href).toContain("/templates")
|
||||
})
|
||||
|
||||
it("users nav link has the correct href", async () => {
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} />)
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} canViewAuditLog />)
|
||||
const userLink = await screen.findByText(navLanguage.users)
|
||||
expect((userLink as HTMLAnchorElement).href).toContain("/users")
|
||||
})
|
||||
@ -54,7 +54,7 @@ describe("NavbarView", () => {
|
||||
}
|
||||
|
||||
// When
|
||||
render(<NavbarView user={mockUser} onSignOut={noop} />)
|
||||
render(<NavbarView user={mockUser} onSignOut={noop} canViewAuditLog />)
|
||||
|
||||
// Then
|
||||
// There should be a 'B' avatar!
|
||||
@ -63,7 +63,7 @@ describe("NavbarView", () => {
|
||||
})
|
||||
|
||||
it("audit nav link has the correct href", async () => {
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} />)
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} canViewAuditLog />)
|
||||
const auditLink = await screen.findByText(navLanguage.audit)
|
||||
expect((auditLink as HTMLAnchorElement).href).toContain("/audit")
|
||||
})
|
||||
@ -74,7 +74,13 @@ describe("NavbarView", () => {
|
||||
NODE_ENV: "production",
|
||||
}
|
||||
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} />)
|
||||
render(<NavbarView user={MockUser} onSignOut={noop} canViewAuditLog />)
|
||||
const auditLink = screen.queryByText(navLanguage.audit)
|
||||
expect(auditLink).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("audit nav link is hidden for members", async () => {
|
||||
render(<NavbarView user={MockUser2} onSignOut={noop} canViewAuditLog={false} />)
|
||||
const auditLink = screen.queryByText(navLanguage.audit)
|
||||
expect(auditLink).not.toBeInTheDocument()
|
||||
})
|
||||
|
@ -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
|
||||
</NavLink>
|
||||
</ListItem>
|
||||
{/* REMARK: the below link is under-construction */}
|
||||
{process.env.NODE_ENV !== "production" && (
|
||||
{process.env.NODE_ENV !== "production" && canViewAuditLog && (
|
||||
<ListItem button className={styles.item}>
|
||||
<NavLink className={styles.link} to="/audit">
|
||||
{Language.audit}
|
||||
@ -60,7 +64,7 @@ const NavItems: React.FC<{ className?: string; linkClassName?: string }> = ({ cl
|
||||
)
|
||||
}
|
||||
|
||||
export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut }) => {
|
||||
export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut, canViewAuditLog }) => {
|
||||
const styles = useStyles()
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||
|
||||
@ -81,7 +85,7 @@ export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut }) => {
|
||||
<div className={styles.drawerHeader}>
|
||||
<Logo fill="white" opacity={1} width={125} />
|
||||
</div>
|
||||
<NavItems />
|
||||
<NavItems canViewAuditLog={canViewAuditLog} />
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
@ -89,7 +93,7 @@ export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut }) => {
|
||||
<Logo fill="white" opacity={1} width={125} />
|
||||
</NavLink>
|
||||
|
||||
<NavItems className={styles.desktopNavItems} />
|
||||
<NavItems className={styles.desktopNavItems} canViewAuditLog={canViewAuditLog} />
|
||||
|
||||
<div className={styles.profileButton}>
|
||||
{user && <UserDropdown user={user} onSignOut={onSignOut} />}
|
||||
|
@ -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<keyof typeof permissionsToCheck, boolean>
|
||||
|
Reference in New Issue
Block a user