Compare commits

...

4 Commits

Author SHA1 Message Date
anys 718e7f9d76 - Add role detection
- Restrict APIs to personnels
- Show 403 for unauthorized access"
2026-01-06 19:05:59 +01:00
anys 7d7cdd1c9a Add 403 error page and Polytech access control. 2026-01-06 18:56:10 +01:00
anys cb89a45743 Check if user is allowed to access 2026-01-06 10:32:52 +01:00
anys 5856eea5f3 Update Dockerfile 2026-01-05 21:37:19 +01:00
5 changed files with 82 additions and 14 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ FROM denoland/deno:alpine
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN deno cache main.ts --allow-import flag RUN deno cache --allow-import main.ts
RUN deno task build RUN deno task build
USER deno USER deno
+3
View File
@@ -3,11 +3,14 @@ import { AsyncRoute } from "$fresh/src/server/types.ts";
interface AuthenticatedState { interface AuthenticatedState {
isAuthenticated: true; isAuthenticated: true;
isFromPolytech: boolean;
role: "etudiants" | "personnels" | "autres";
session: CasContent; session: CasContent;
} }
interface UnauthenticatedState { interface UnauthenticatedState {
isAuthenticated: false; isAuthenticated: false;
isFromPolytech: false;
session: undefined; session: undefined;
} }
+2
View File
@@ -20,6 +20,7 @@ import * as $_apps_students_partials_admin_consult from "./routes/(apps)/student
import * as $_apps_students_partials_admin_upload from "./routes/(apps)/students/partials/(admin)/upload.tsx"; import * as $_apps_students_partials_admin_upload from "./routes/(apps)/students/partials/(admin)/upload.tsx";
import * as $_apps_students_partials_index from "./routes/(apps)/students/partials/index.tsx"; import * as $_apps_students_partials_index from "./routes/(apps)/students/partials/index.tsx";
import * as $_apps_students_partials_overview from "./routes/(apps)/students/partials/overview.tsx"; import * as $_apps_students_partials_overview from "./routes/(apps)/students/partials/overview.tsx";
import * as $_403 from "./routes/_403.tsx";
import * as $_404 from "./routes/_404.tsx"; import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx"; import * as $_app from "./routes/_app.tsx";
import * as $_middleware from "./routes/_middleware.ts"; import * as $_middleware from "./routes/_middleware.ts";
@@ -67,6 +68,7 @@ const manifest = {
$_apps_students_partials_index, $_apps_students_partials_index,
"./routes/(apps)/students/partials/overview.tsx": "./routes/(apps)/students/partials/overview.tsx":
$_apps_students_partials_overview, $_apps_students_partials_overview,
"./routes/_403.tsx": $_403,
"./routes/_404.tsx": $_404, "./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app, "./routes/_app.tsx": $_app,
"./routes/_middleware.ts": $_middleware, "./routes/_middleware.ts": $_middleware,
+12
View File
@@ -0,0 +1,12 @@
import { Head } from "$fresh/runtime.ts";
export default function Error403() {
return (
<>
<Head>
<title>403 - Forbidden</title>
</Head>
<p>403</p>
</>
);
}
+63 -12
View File
@@ -21,10 +21,19 @@ const deleteKey = (user: string) => delete jwtKeyCache[user];
* @returns `true` if the route is public, `false` otherwise. * @returns `true` if the route is public, `false` otherwise.
*/ */
function isRoutePublic(route: string): boolean { function isRoutePublic(route: string): boolean {
return PUBLIC_ROUTES.includes(route) || return (
!!(route.match(/\..+$/)?.[0] ?? false); PUBLIC_ROUTES.includes(route) || !!(route.match(/\..+$/)?.[0] ?? false)
);
} }
/**
* Checks if the given route is an API route.
* @param route The route to check.
* @returns `true` if the route is an API route, `false` otherwise.
*/
function isRouteAnAPI(route: string): boolean {
return route.includes("/api/");
}
/** /**
* Get the given user's key, creating it if not already existing. * Get the given user's key, creating it if not already existing.
* @param user The key's user. * @param user The key's user.
@@ -44,6 +53,7 @@ export function getKey(user: string): string {
export const handler: MiddlewareHandler<State>[] = [ export const handler: MiddlewareHandler<State>[] = [
/** /**
* Check if user is authenticated and add session to context accordingly. * Check if user is authenticated and add session to context accordingly.
* Only authenticated users who are members of Polytech are allowed.
* @param request The HTTP incomming request. * @param request The HTTP incomming request.
* @param context The Fresh context object with custom `State`. * @param context The Fresh context object with custom `State`.
* @returns The response from the next middleware. * @returns The response from the next middleware.
@@ -55,6 +65,7 @@ export const handler: MiddlewareHandler<State>[] = [
const cookies = getCookies(request.headers); const cookies = getCookies(request.headers);
if (!cookies["sessionToken"]) { if (!cookies["sessionToken"]) {
context.state.isAuthenticated = false; context.state.isAuthenticated = false;
context.state.isFromPolytech = false;
return await context.next(); return await context.next();
} }
@@ -67,9 +78,32 @@ export const handler: MiddlewareHandler<State>[] = [
); );
if (context.state.isAuthenticated) { if (context.state.isAuthenticated) {
const session: CasContent = const session: CasContent = (
(getJwtPayload(cookies["sessionToken"]) as LoginJWT).user; getJwtPayload(cookies["sessionToken"]) as LoginJWT
context.state.session = session; ).user;
const isFromPolytech = Object.values(session.memberOf).some(
(value) =>
typeof value === "string" && value.includes("cn=amu:ufr:polytech"),
);
context.state.isFromPolytech = isFromPolytech;
if (isFromPolytech) {
context.state.session = session;
if (Object.values(session.memberOf).some(
(value) => typeof value === "string" && value.includes("cn=amu:ufr:polytech:personnels")
)) {
context.state.role = "personnels";
} else if (Object.values(session.memberOf).some(
(value) => typeof value === "string" && value.includes("cn=amu:ufr:polytech:etudiants")
)) {
context.state.role = "etudiants";
} else {
context.state.role = "autres";
}
}
} }
return await context.next(); return await context.next();
@@ -87,13 +121,30 @@ export const handler: MiddlewareHandler<State>[] = [
): Promise<Response> { ): Promise<Response> {
const url = new URL(request.url); const url = new URL(request.url);
if (!isRoutePublic(url.pathname) && !context.state.isAuthenticated) { if (!isRoutePublic(url.pathname)) {
return new Response(null, { if (!context.state.isAuthenticated) {
status: 302, return new Response(null, {
headers: { status: 302,
Location: "/login", headers: {
}, Location: "/login",
}); },
});
}
if (!context.state.isFromPolytech) {
return new Response(null, {
status: 403,
headers: {
Location: "/403",
},
});
}
if (isRouteAnAPI(url.pathname) && !(context.state.role == "personnels")) {
return new Response(null, {
status: 403,
});
}
} }
return await context.next(); return await context.next();