Skip to content

Commit

Permalink
Merge pull request #76 from mrc-ide/mrc-5367-setup-manage-acess
Browse files Browse the repository at this point in the history
Mrc-5367- Initial setup of manage access
  • Loading branch information
absternator committed May 23, 2024
2 parents b8a7133 + 8d74e9c commit 05d72bd
Show file tree
Hide file tree
Showing 29 changed files with 358 additions and 71 deletions.
11 changes: 3 additions & 8 deletions api/app/src/main/kotlin/packit/service/RoleService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface RoleService
{
fun getUsernameRole(username: String): Role
fun getAdminRole(): Role
fun getGrantedAuthorities(roles: List<Role>): MutableList<GrantedAuthority>
fun getGrantedAuthorities(roles: List<Role>): MutableSet<GrantedAuthority>
fun createRole(createRole: CreateRole): Role
fun deleteRole(roleName: String)
fun deleteUsernameRole(username: String)
Expand Down Expand Up @@ -152,15 +152,10 @@ class BaseRoleService(
return roleRepository.save(role)
}

/**
* Authorities constructed as combination of role names and permission names.
* This allows for more granular control over permissions.
*/
override fun getGrantedAuthorities(roles: List<Role>): MutableList<GrantedAuthority>
override fun getGrantedAuthorities(roles: List<Role>): MutableSet<GrantedAuthority>
{
val grantedAuthorities = mutableListOf<GrantedAuthority>()
val grantedAuthorities = mutableSetOf<GrantedAuthority>()
roles.forEach { role ->
grantedAuthorities.add(SimpleGrantedAuthority(role.name))
role.rolePermissions.forEach {
grantedAuthorities.add(SimpleGrantedAuthority(getPermissionScoped(it)))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,6 @@ class RoleControllerTest : IntegrationTest()
fun `users can get username roles with relationships `()
{
roleRepository.save(Role("test-username", isUsername = true))
val allUsernameRoles =
ObjectMapper().writeValueAsString(roleRepository.findAllByIsUsername(true).map { it.toDto() })
val result =
restTemplate.exchange(
"/role?isUsername=true",
Expand All @@ -235,9 +233,7 @@ class RoleControllerTest : IntegrationTest()
String::class.java
)

val roles = ObjectMapper().readValue(result.body, List::class.java)

assert(roles.containsAll(ObjectMapper().readValue(allUsernameRoles, List::class.java)))
assertSuccess(result)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class GithubAPILoginServiceTest
private val userPrincipal = UserPrincipal(
username,
displayName,
mutableListOf(SimpleGrantedAuthority("USER")),
mutableSetOf(SimpleGrantedAuthority("USER")),
mutableMapOf()
)
private val mockIssuer = mock<JwtIssuer> {
Expand All @@ -51,7 +51,7 @@ class GithubAPILoginServiceTest
on { getGithubUser() } doReturn fakeGHMyself
}
private val mockRoleService = mock<RoleService> {
on { getGrantedAuthorities(fakeUser.roles) } doReturn mutableListOf(SimpleGrantedAuthority("USER"))
on { getGrantedAuthorities(fakeUser.roles) } doReturn mutableSetOf(SimpleGrantedAuthority("USER"))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class RoleServiceTest
}

@Test
fun `getGrantedAuthorities returns authorities for roles and permissions`()
fun `getGrantedAuthorities returns authorities of permissions`()
{
val role1 =
createRoleWithPermission("role1", "permission1")
Expand All @@ -150,13 +150,11 @@ class RoleServiceTest

val result = roleService.getGrantedAuthorities(listOf(role1, role2))

assertEquals(4, result.size)
assertEquals(2, result.size)
assertTrue(
result.containsAll(
listOf(
SimpleGrantedAuthority("role1"),
SimpleGrantedAuthority("permission1"),
SimpleGrantedAuthority("role2"),
SimpleGrantedAuthority("permission2")
)
)
Expand Down
34 changes: 34 additions & 0 deletions app/src/app/components/contents/common/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NavLink, useLocation } from "react-router-dom";
import { buttonVariants } from "../../Base/Button";
import { cn } from "../../../../lib/cn";
import { SidebarItem } from "../../../../lib/types/SidebarItem";

interface SidebarProps {
sidebarItems: SidebarItem[];
children: React.ReactNode;
}
export const Sidebar = ({ sidebarItems, children }: SidebarProps) => {
const { pathname } = useLocation();
return (
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-10 lg:space-y-2">
<aside data-testid="sidebar" className="lg:w-1/5 pl-1 lg:pl-2">
<nav className="flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1">
{sidebarItems.map((item, index) => (
<NavLink
key={index}
to={item.to}
className={cn(
buttonVariants({ variant: "ghost" }),
pathname === item.to ? "bg-muted hover:bg-muted" : "hover:bg-transparent hover:underline",
"justify-start"
)}
>
{item.title}
</NavLink>
))}
</nav>
</aside>
<div className="flex-1 lg:max-w-6xl">{children}</div>
</div>
);
};
10 changes: 10 additions & 0 deletions app/src/app/components/contents/common/Unauthorized.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const Unauthorized = () => {
return (
<div className="container h-[800px] flex items-center justify-center m-auto">
<div className="flex flex-col space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight text-red-500">Unauthorized</h1>
<p>You do not have permission to access this page.</p>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Outlet } from "react-router-dom";
import { Sidebar } from "../common/Sidebar";
import { SidebarItem } from "../../../../lib/types/SidebarItem";
import { useUser } from "../../providers/UserProvider";
import { Unauthorized } from "../common/Unauthorized";

const sidebarItems: SidebarItem[] = [
{
to: "/manage-roles",
title: "Manage Roles"
},
{
to: "/manage-users",
title: "Manage Users"
}
];

export const ManageAccessLayout = () => {
const { user } = useUser();

if (!user?.authorities.includes("user.manage")) {
return <Unauthorized />;
}
return (
<Sidebar sidebarItems={sidebarItems}>
<Outlet />
</Sidebar>
);
};
3 changes: 3 additions & 0 deletions app/src/app/components/contents/manageAccess/ManageRoles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ManageRoles = () => {
return <div>manage roles stuffoes</div>;
};
3 changes: 3 additions & 0 deletions app/src/app/components/contents/manageAccess/ManageUsers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ManageUsers = () => {
return <div>manage users stuffies</div>;
};
5 changes: 5 additions & 0 deletions app/src/app/components/contents/manageAccess/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ManageAccessLayout } from "./ManageAccessLayout";
import { ManageRoles } from "./ManageRoles";
import { ManageUsers } from "./ManageUsers";

export { ManageAccessLayout, ManageRoles, ManageUsers };
7 changes: 7 additions & 0 deletions app/src/app/components/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { AccountHeaderDropdown } from "./AccountHeaderDropdown";

import { useUser } from "../providers/UserProvider";
import { ThemeToggleButton } from "./ThemeToggleButton";
import { cn } from "../../../lib/cn";
import { buttonVariants } from "../Base/Button";

export default function Header() {
const { user } = useUser();
Expand All @@ -27,6 +29,11 @@ export default function Header() {
{/* <NavigationLink to="/accessibility" className="mx-6 hidden md:flex">
Accessibility
</NavigationLink> */}
{user?.authorities.includes("user.manage") && (
<NavLink to="/manage-roles" className={cn(buttonVariants({ variant: "ghost" }), "justify-start")}>
Manage Access
</NavLink>
)}
<ThemeToggleButton />
{user && <AccountHeaderDropdown />}
</div>
Expand Down
3 changes: 2 additions & 1 deletion app/src/app/components/main/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChevronRight } from "lucide-react";
import { NavLink, Outlet, useLocation } from "react-router-dom";
import { kebabToSentenceCase } from "../../../lib/string";

export const Breadcrumb = () => {
const { pathname } = useLocation();
Expand All @@ -12,7 +13,7 @@ export const Breadcrumb = () => {
<div className="flex h-9 items-center px-4 justify-start space-x-1 text-sm">
{pathNames.map((path, index) => {
const routeTo = `${pathNames.slice(0, index + 1).join("/")}`;
const displayName = routeTo === "" ? "Home" : path;
const displayName = routeTo === "" ? "home" : kebabToSentenceCase(path);

return index === pathNames.length - 1 ? (
<div key={index}>{displayName}</div>
Expand Down
18 changes: 7 additions & 11 deletions app/src/app/components/main/PacketLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useParams } from "react-router-dom";
import { Sidebar } from "../contents/common/Sidebar";
import { PacketOutlet } from "./PacketOutlet";
import { PacketSidebarNav } from "./PacketSidebarNav";
import { SidebarItem } from "../../../lib/types/SidebarItem";

const getSideBarNavItems = (packetName = "", packetId = "") => [
const getSideBarNavItems = (packetName = "", packetId = ""): SidebarItem[] => [
{
to: `/${packetName}/${packetId}`,
title: "Summary"
Expand All @@ -23,16 +24,11 @@ const getSideBarNavItems = (packetName = "", packetId = "") => [

export const PacketLayout = () => {
const { packetId, packetName } = useParams();
const sidebarNavItems = getSideBarNavItems(packetName, packetId);
const sidebarItems = getSideBarNavItems(packetName, packetId);

return (
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-10 lg:space-y-2">
<aside data-testid="sidebar" className="lg:w-1/5 pl-1 lg:pl-2">
<PacketSidebarNav items={sidebarNavItems} />
</aside>
<div className="flex-1 lg:max-w-6xl">
<PacketOutlet packetId={packetId} />
</div>
</div>
<Sidebar sidebarItems={sidebarItems}>
<PacketOutlet packetId={packetId} />
</Sidebar>
);
};
34 changes: 0 additions & 34 deletions app/src/app/components/main/PacketSidebarNav.tsx

This file was deleted.

3 changes: 2 additions & 1 deletion app/src/app/components/providers/UserProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export const UserProvider = ({ children }: UserProviderProps) => {
token: jwt,
exp: jwtPayload.exp?.valueOf() ?? 0,
displayName: jwtPayload.displayName ?? "",
userName: jwtPayload.userName ?? ""
userName: jwtPayload.userName ?? "",
authorities: jwtPayload.au ?? []
};
setUserState(user);
localStorage.setItem(LocalStorageKeys.USER, JSON.stringify(user));
Expand Down
1 change: 1 addition & 0 deletions app/src/app/components/providers/types/UserTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export interface UserState {
exp: number;
displayName: string;
userName: string;
authorities: string[];
}
export interface UserProviderState {
user: UserState | null;
Expand Down
7 changes: 6 additions & 1 deletion app/src/app/components/routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PacketLayout } from "../main/PacketLayout";
import ProtectedRoute from "./ProtectedRoute";
import { UpdatePassword } from "../login";
import { AuthLayoutForm } from "../login";
import { ManageAccessLayout, ManageRoles, ManageUsers } from "../contents/manageAccess";

export function Router() {
return (
Expand All @@ -34,12 +35,16 @@ export function Router() {
{/* <Route path="run-workflow" element={<WorkflowRunner />} /> */}
{/* <Route path="documentation" element={<ProjectDocumentation />} /> */}
<Route path="/:packetName" element={<PacketGroup />} />
<Route element={<PacketLayout />} path="/:packetName/:packetId">
<Route element={<PacketLayout />}>
<Route path="/:packetName/:packetId" element={<PacketDetails />} />
<Route path="/:packetName/:packetId/metadata" element={<Metadata />} />
<Route path="/:packetName/:packetId/downloads" element={<Download />} />
{/* <Route path="/:packetName/:packetId/changelogs" element={<ChangeLogs />} /> */}
</Route>
<Route element={<ManageAccessLayout />}>
<Route path="manage-roles" element={<ManageRoles />} />
<Route path="manage-users" element={<ManageUsers />} />
</Route>
</Route>
</Route>
</Route>
Expand Down
7 changes: 7 additions & 0 deletions app/src/lib/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ export const getInitials = (name = "X X") => {

return `${firstName[0]}${lastName[0]}`;
};

export const kebabToSentenceCase = (input: string): string => {
if (input.includes("-")) {
return input.replace(/-/g, " ");
}
return input;
};
4 changes: 4 additions & 0 deletions app/src/lib/types/SidebarItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface SidebarItem {
to: string;
title: string;
}
29 changes: 29 additions & 0 deletions app/src/tests/components/contents/common/Sidebar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { Sidebar } from "../../../../app/components/contents/common/Sidebar";

describe("Sidebar", () => {
it("should render sidebar items with children and correct classes", () => {
const sidebarItems = [
{
to: "/manage-roles",
title: "Manage Roles"
},
{
to: "/manage-users",
title: "Manage Users"
}
];
render(
<MemoryRouter initialEntries={["/manage-roles"]}>
<Sidebar sidebarItems={sidebarItems}>
<div>main content</div>
</Sidebar>
</MemoryRouter>
);

expect(screen.getByRole("link", { name: "Manage Roles" })).toHaveClass("bg-muted hover:bg-muted active");
expect(screen.getByRole("link", { name: "Manage Users" })).toHaveClass("hover:bg-transparent hover:underline");
expect(screen.getByText("main content")).toBeVisible();
});
});
Loading

0 comments on commit 05d72bd

Please sign in to comment.