From b4e9bba622cf5d089a6012e79eebe54740c36a0e Mon Sep 17 00:00:00 2001 From: Eric Bouchut Date: Mon, 13 Jul 2026 09:37:55 +0200 Subject: [PATCH] feat(security): Gate course, instructor, and admin URL prefixes by role Phase 1 of the course management by role plan: - SecurityConfig: /instructor/** requires ROLE_INSTRUCTOR, /admin/** requires ROLE_ADMIN, /courses/** requires authentication; method security enabled for upcoming service-level ownership rules; the ERROR dispatch is permitted so the error templates can render. - Styled error pages (403, 404, 500) on the shared layout with the RGAA wiring, replacing the Whitelabel fallback. - Role-aware navigation: Instructor and Admin entries appear only for their roles, with the aria-current wiring of the existing entries. - SecurityMatrixTest: access matrix per role and prefix (anonymous is redirected to login, wrong role gets 403, right role passes the gate); gate-pass asserts 404 until each controller phase lands. Verified in the browser: anonymous /courses redirects to login, a student gets the styled 403 on /instructor/courses, the Instructor nav entry appears once the role is granted, and axe (WCAG 2.1 A/AA and best-practice rules) reports zero violations on the 403 and 404 pages (the flagged brand mark is a logotype, exempt under WCAG 1.4.3). --- .../common/config/SecurityConfig.java | 10 +++ src/main/resources/templates/error/403.html | 17 ++++ src/main/resources/templates/error/404.html | 17 ++++ src/main/resources/templates/error/500.html | 17 ++++ .../resources/templates/fragments/layout.html | 9 +- .../common/config/SecurityMatrixTest.java | 89 +++++++++++++++++++ 6 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/templates/error/403.html create mode 100644 src/main/resources/templates/error/404.html create mode 100644 src/main/resources/templates/error/500.html create mode 100644 src/test/java/com/ericbouchut/learndev/common/config/SecurityMatrixTest.java diff --git a/src/main/java/com/ericbouchut/learndev/common/config/SecurityConfig.java b/src/main/java/com/ericbouchut/learndev/common/config/SecurityConfig.java index 46efbe9..4320970 100644 --- a/src/main/java/com/ericbouchut/learndev/common/config/SecurityConfig.java +++ b/src/main/java/com/ericbouchut/learndev/common/config/SecurityConfig.java @@ -1,7 +1,9 @@ package com.ericbouchut.learndev.common.config; +import jakarta.servlet.DispatcherType; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -10,6 +12,8 @@ @Configuration @EnableWebSecurity +// Enables @PreAuthorize for service-level rules (e.g. course ownership). +@EnableMethodSecurity public class SecurityConfig { @Bean @@ -21,7 +25,13 @@ public PasswordEncoder passwordEncoder() { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth + // The ERROR dispatch renders templates/error/*.html for a + // response already authorized (or denied) on its way in. + .dispatcherTypeMatchers(DispatcherType.ERROR).permitAll() .requestMatchers("/", "/privacy", "/auth/**", "/css/**", "/js/**", "/fonts/**").permitAll() + .requestMatchers("/instructor/**").hasRole("INSTRUCTOR") + .requestMatchers("/admin/**").hasRole("ADMIN") + .requestMatchers("/courses/**").authenticated() .anyRequest().authenticated()) .formLogin(form -> form .loginPage("/auth/login") // GET: show the login form diff --git a/src/main/resources/templates/error/403.html b/src/main/resources/templates/error/403.html new file mode 100644 index 0000000..c3f70ba --- /dev/null +++ b/src/main/resources/templates/error/403.html @@ -0,0 +1,17 @@ + + + + + + +
+

Access denied

+

You do not have permission to view this page.

+

+ Back to your dashboard +

+
+ + + + diff --git a/src/main/resources/templates/error/404.html b/src/main/resources/templates/error/404.html new file mode 100644 index 0000000..a040255 --- /dev/null +++ b/src/main/resources/templates/error/404.html @@ -0,0 +1,17 @@ + + + + + + +
+

Page not found

+

The page you are looking for does not exist or has moved.

+

+ Back to the home page +

+
+ + + + diff --git a/src/main/resources/templates/error/500.html b/src/main/resources/templates/error/500.html new file mode 100644 index 0000000..15b14f9 --- /dev/null +++ b/src/main/resources/templates/error/500.html @@ -0,0 +1,17 @@ + + + + + + +
+

Something went wrong

+

An unexpected error occurred on our side. Please try again in a moment.

+

+ Back to the home page +

+
+ + + + diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html index 3a97133..3dceece 100644 --- a/src/main/resources/templates/fragments/layout.html +++ b/src/main/resources/templates/fragments/layout.html @@ -4,7 +4,8 @@ - head(title): document head with the design stylesheets - header(current): skip link + banner + auth-aware navigation; `current` names the nav entry of the calling page ('home', 'login', - 'register', 'dashboard' or null) and drives aria-current="page" + 'register', 'dashboard', 'instructor', 'admin' or null) and drives + aria-current="page" - footer: site footer Each page keeps its own (privacy.html is lang="fr"). --> @@ -38,6 +39,12 @@