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:
Kira Pilot
2022-08-11 09:34:45 -04:00
committed by GitHub
parent 4e6645af50
commit 6122df6f1f
7 changed files with 174 additions and 136 deletions

View File

@ -88,6 +88,7 @@ var (
// Should be able to read all template details, even in orgs they
// are not in.
ResourceTemplate: {ActionRead},
ResourceAuditLog: {ActionRead},
}),
}
},

View File

@ -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

View File

@ -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>
)
}

View File

@ -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}
/>
)
}

View File

@ -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()
})

View File

@ -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} />}

View File

@ -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>