Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Run setup
run: npm run setup
- name: Run tests
run: npm run test-unit

Expand Down
43 changes: 25 additions & 18 deletions scripts/generate-acl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@ function extractAclMapping() {

if (acl && Array.isArray(acl.admin)) {
aclMapping.acl.admin = [...aclMapping.acl.admin, ...acl.admin];
const recordIds = getAlgorithmRecordIds(provider.name);
// Assign each algorithm record id with the provider ACL
for (const recordId of recordIds) {
if (aclMapping.records[recordId]) {
aclMapping.records[recordId] = [
...aclMapping.records[recordId],
const scenarioIds = getBenchmarkScenarioIds(provider.name);
// Assign each benchmark scenario id with the provider ACL
for (const scenarioId of scenarioIds) {
if (aclMapping.records[scenarioId]) {
aclMapping.records[scenarioId] = [
...aclMapping.records[scenarioId],
...acl.admin,
];
} else {
aclMapping.records[recordId] = acl.admin;
aclMapping.records[scenarioId] = acl.admin;
}
}
} else {
Expand All @@ -76,32 +76,39 @@ function extractAclMapping() {
}
}

function getAlgorithmRecordIds(providerDir) {
function getBenchmarkScenarioIds(providerDir) {
try {
const targetDir = path.join(ALGORITHM_CATALOG_DIR, providerDir);

const records = fs
const scenarios = fs
.readdirSync(targetDir, { recursive: true })
.map((file) => file.toString())
.filter(
(file) =>
file.endsWith(".json") &&
(file.includes("/records/") || file.includes("\\records\\")), // support linux and windows based path
(file.includes("/benchmark_scenarios/") ||
file.includes("\\benchmark_scenarios\\")), // support linux and windows based path
);

const recordIds = [];
const scenarioIds = [];

for (const recordFile of records) {
const recordPath = path.join(targetDir, recordFile);
const recordContent = fs.readFileSync(recordPath, "utf-8");
const recordJson = JSON.parse(recordContent);
recordIds.push(recordJson.id);
for (const scenarioFile of scenarios) {
const scenarioPath = path.join(targetDir, scenarioFile);
const scenarioContent = fs.readFileSync(scenarioPath, "utf-8");
const scenarioJson = JSON.parse(scenarioContent);
if (Array.isArray(scenarioJson)) {
for (const scenario of scenarioJson) {
scenarioIds.push(scenario.id);
}
} else {
scenarioIds.push(scenarioJson.id);
}
}

return recordIds;
return scenarioIds;
} catch (error) {
console.error(
`Error reading records for provider ${providerDir}:`,
`Error reading benchmark scenarios for provider ${providerDir}:`,
error.message,
);
return [];
Expand Down
33 changes: 33 additions & 0 deletions src/components/react/LogOutButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { LogOut } from "lucide-react";
import { Button } from "./Button";
import { logOut as handleLogOut } from "@/lib/auth";

interface LogOutButtonProps {
variant?:
| "default"
| "destructive"
| "outline"
| "secondary"
| "ghost"
| "link";
size?: "default" | "sm" | "lg" | "icon";
className?: string;
}

export function LogOutButton({
variant = "outline",
size = "default",
className,
}: LogOutButtonProps) {
return (
<Button
variant={variant}
size={size}
className={className}
onClick={handleLogOut}
>
<LogOut className="mr-2 h-4 w-4" />
Sign out
</Button>
);
}
11 changes: 2 additions & 9 deletions src/components/react/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "./DropdownMenu";
import { logOut as handleLogOut } from "@/lib/auth";

interface UserMenuProps {
name?: string | null;
Expand All @@ -26,14 +27,6 @@ export function UserMenu({ name, username, email }: UserMenuProps) {
: email
? email[0].toUpperCase()
: "U";
const handleSignOut = async () => {
const callbackUrl = new URL(window.location.origin);
callbackUrl.searchParams.set("toastSuccess", "logout");
await window.signOut({
// @ts-ignore
callbackUrl: callbackUrl.href,
});
};

return (
<div className="flex">
Expand All @@ -57,7 +50,7 @@ export function UserMenu({ name, username, email }: UserMenuProps) {
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer" onClick={handleSignOut}>
<DropdownMenuItem className="cursor-pointer" onClick={handleLogOut}>
<LogOut className="mr-2 h-4 w-4" />
<span>Sign out</span>
</DropdownMenuItem>
Expand Down
1 change: 1 addition & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ declare namespace App {
username?: string;
email?: string | null;
roles?: string[];
emailDomain?: string | null;
};
}
}
8 changes: 8 additions & 0 deletions src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const logOut = async () => {
const callbackUrl = new URL(window.location.origin);
callbackUrl.searchParams.set("toastSuccess", "logout");
await window.signOut({
// @ts-ignore
callbackUrl: callbackUrl.href,
});
};
8 changes: 5 additions & 3 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { config as authConfig } from "../auth.config";
import { isFeatureEnabled } from "./lib/featureflag";
import aclMapping from "./acl-mapping.json";

const protectedPaths = ["/api/admin/services/benchmarks.json", "/dashboard"];
const protectedPaths = ["/api/admin/services/", "/dashboard"];

/**
* Check if the request is for an API endpoint
Expand Down Expand Up @@ -52,14 +52,16 @@ export const onRequest = defineMiddleware(async (context, next) => {
const session = await getSession(context.request, authConfig);

if (session?.user) {
const emailDomain = `@${session.user.email?.split("@").pop()}`;

context.locals.user = {
name: session.user.name,
username: session.user.username,
email: session.user.email,
roles: session.user.roles || [],
emailDomain,
};

const emailDomain = `@${context.locals.user.email?.split("@").pop()}`;
if (
context.locals.user.roles?.includes("administrator") ||
aclMapping.acl.admin.includes(emailDomain)
Expand All @@ -71,7 +73,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
return new Response("Forbidden", { status: 403 });
}

const notFoundUrl = new URL("/404", context.url);
const notFoundUrl = new URL("/403", context.url);
return Response.redirect(notFoundUrl.toString(), 302);
}

Expand Down
36 changes: 36 additions & 0 deletions src/pages/403.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
import Layout from "../layouts/Layout.astro";
import { LogOutButton } from "../components/react/LogOutButton";
const title = "403 Forbidden";

export const prerender = true;

Astro.response.status = 403;
---

<Layout title={`${title} | APEx`}>
<div class="my-14">
<main
class="w-full block sm:max-w-screen-sm md:max-w-screen-md lg:max-w-screen-lg xl:max-w-screen-xl mx-auto px-4"
>
<div class="max-w-screen-xl text-center mx-auto mb-10">
<h1
class="text-3xl md:text-5xl mb-8 pb-10 text-white after:content-[''] relative after:absolute after:w-16 after:h-[2px] after:bottom-0 after:left-1/2 after:transform after:-translate-x-1/2 after:bg-brand-teal-50"
>
{title}
</h1>
<p class="text-balance text-brand-gray-50">
You do not have permission to access this page.
</p>
<div class="mt-6">
<LogOutButton client:only="react" />
</div>
</div>
</main>
</div>
<script>
const { signOut } = await import("auth-astro/client");

window.signOut = signOut;
</script>
</Layout>
2 changes: 2 additions & 0 deletions src/pages/404.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Layout from "../layouts/Layout.astro";
const title = "404 Not Found";

export const prerender = true;

Astro.response.status = 404;
---

<Layout title={`${title} | APEx`}>
Expand Down
11 changes: 10 additions & 1 deletion src/pages/api/admin/services/[id]/benchmarks.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
PARQUET_MONTH_COVERAGE,
getUrlsFromRequest,
} from "@/lib/parquet-datasource";
import aclMapping from "@/acl-mapping.json";

/**
* @openapi
Expand Down Expand Up @@ -92,7 +93,7 @@ import {
* - Benchmark
* - Scenario
*/
export const GET: APIRoute = async ({ params, request }) => {
export const GET: APIRoute = async ({ params, request, locals }) => {
const scenario = params.id;

if (!scenario) {
Expand All @@ -102,6 +103,14 @@ export const GET: APIRoute = async ({ params, request }) => {
);
}

// @ts-expect-error
if (!aclMapping.records[scenario]?.includes(locals.user?.emailDomain)) {
return new Response(
JSON.stringify({ message: "Scenario not found." }),
{ status: 404, headers: { "Content-Type": "application/json" } },
);
}

try {
const urlResponse = await getUrlsFromRequest(request);
if (urlResponse instanceof Response) {
Expand Down
9 changes: 7 additions & 2 deletions src/pages/api/admin/services/benchmarks.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
PARQUET_MONTH_COVERAGE,
getUrlsFromRequest,
} from "@/lib/parquet-datasource";
import aclMapping from "@/acl-mapping.json";

/**
* @openapi
Expand Down Expand Up @@ -71,7 +72,7 @@ import {
* - Admin
* - Benchmark
*/
export const GET: APIRoute = async ({ request }) => {
export const GET: APIRoute = async ({ request, locals }) => {
try {
const urlResponse = await getUrlsFromRequest(request);
if (urlResponse instanceof Response) {
Expand Down Expand Up @@ -102,7 +103,11 @@ export const GET: APIRoute = async ({ request }) => {
ORDER BY "scenario_id";
`;

const data = (await executeQuery(query)) as BenchmarkSummary[];
let data = (await executeQuery(query)) as BenchmarkSummary[];
data = data.filter((benchmark) => {
// @ts-expect-error
return aclMapping.records[benchmark.scenario_id]?.includes(locals.user?.emailDomain)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Find a way to make this rule reusable

});

return Response.json(data);
} catch (error) {
Expand Down
Loading