AuthorizationRulesConfigurer.java
package com.taxonomy.security.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.stereotype.Component;
/**
* Shared authorization rules used by both the form-login and Keycloak
* security configurations. Rules are ordered from most specific to least
* specific so that state-changing endpoints are never accidentally covered by
* a generic authenticated-user fallback.
*/
@Component
public class AuthorizationRulesConfigurer {
@Value("${taxonomy.security.swagger-public:true}")
private boolean swaggerPublic;
public void configure(
AuthorizeHttpRequestsConfigurer<org.springframework.security.config.annotation.web.builders.HttpSecurity>
.AuthorizationManagerRequestMatcherRegistry auth) {
auth.requestMatchers("/login", "/error", "/css/**", "/js/**", "/images/**", "/webjars/**")
.permitAll();
auth.requestMatchers("/login/oauth2/**", "/oauth2/**").permitAll();
auth.requestMatchers("/change-password").authenticated();
auth.requestMatchers("/actuator/health", "/actuator/health/**", "/actuator/info").permitAll();
if (swaggerPublic) {
auth.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll();
} else {
auth.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").authenticated();
}
auth.requestMatchers("/api/admin/status", "/api/admin/verify").authenticated();
auth.requestMatchers("/admin/**", "/api/admin/**", "/api/preferences/**",
"/api/diagnostics", "/api/prompts/**")
.hasRole("ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/relations/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.PUT, "/api/relations/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.DELETE, "/api/relations/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/proposals/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/architecture/editor/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.GET, "/api/integrations/**", "/integrations", "/oslc/**").authenticated();
auth.requestMatchers(HttpMethod.HEAD, "/api/integrations/**", "/integrations", "/oslc/**").authenticated();
auth.requestMatchers(HttpMethod.OPTIONS, "/api/integrations/**").permitAll();
auth.requestMatchers(HttpMethod.OPTIONS, "/oslc/**").authenticated();
auth.requestMatchers(HttpMethod.POST, "/api/integrations", "/api/integrations/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers("/oslc/**", "/api/integrations/**").denyAll();
auth.requestMatchers(HttpMethod.PUT, "/api/proposals/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.DELETE, "/api/proposals/**").hasAnyRole("ARCHITECT", "ADMIN");
// Git-authoritative architecture decisions use identity-based endpoints
// outside the historic /api/relations and /api/proposals paths. Keep the
// same global role gate here; repository/workspace authority remains an
// additional controller-level check.
auth.requestMatchers(
HttpMethod.POST,
"/api/architecture/relations/**",
"/api/architecture/proposals/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(
HttpMethod.PUT,
"/api/architecture/relations/**",
"/api/architecture/proposals/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(
HttpMethod.DELETE,
"/api/architecture/relations/**",
"/api/architecture/proposals/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/dsl/parse", "/api/dsl/validate", "/api/dsl/format")
.authenticated();
auth.requestMatchers(HttpMethod.POST, "/api/dsl/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.PUT, "/api/dsl/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.DELETE, "/api/dsl/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/git/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.PUT, "/api/git/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.DELETE, "/api/git/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.GET, "/api/context/**").authenticated();
auth.requestMatchers(HttpMethod.POST, "/api/context/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.GET, "/api/workspace/**").authenticated();
// Provisioning creates only the authenticated user's isolated working
// copy. It must remain available to every product role and is ordered
// before the privileged catch-all for workspace administration.
auth.requestMatchers(HttpMethod.POST, "/api/workspace/provision")
.hasAnyRole("USER", "ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST,
"/api/workspace/sync-from-shared",
"/api/workspace/publish",
"/api/workspace/resolve-diverged")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/workspace/**").hasRole("ADMIN");
auth.requestMatchers(HttpMethod.PUT, "/api/workspace/**").hasRole("ADMIN");
auth.requestMatchers(HttpMethod.DELETE, "/api/workspace/**").hasRole("ADMIN");
auth.requestMatchers(HttpMethod.GET, "/api/analysis-drafts/**").authenticated();
auth.requestMatchers(HttpMethod.PUT, "/api/analysis-drafts/**")
.hasAnyRole("USER", "ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/analysis-drafts/**")
.hasAnyRole("USER", "ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.DELETE, "/api/analysis-drafts/**")
.hasAnyRole("USER", "ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.GET, "/api/repositories/**").authenticated();
auth.requestMatchers(HttpMethod.POST, "/api/repositories")
.hasRole("ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/repositories/*/workspaces")
.hasAnyRole("USER", "ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/repositories/*/forks")
.hasAnyRole("ARCHITECT", "ADMIN");
// Global authentication is only the outer gate. Repository OWNER authority is
// enforced by ArchitectureRepositoryController/RepositoryMembershipService.
auth.requestMatchers(HttpMethod.PUT, "/api/repositories/*/members/*")
.authenticated();
auth.requestMatchers(HttpMethod.DELETE, "/api/repositories/*/members/*")
.authenticated();
auth.requestMatchers(HttpMethod.POST, "/api/import/preview/**")
.hasAnyRole("USER", "ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/import/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/documents/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/provenance/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.PUT, "/api/provenance/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.DELETE, "/api/provenance/**").hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/coverage/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.DELETE, "/api/coverage/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/architecture/metadata/recompute")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST,
"/api/projects/*/analyses",
"/api/projects/*/requirements/*/analyses",
"/api/projects/*/analysis-jobs/*/retry-failed")
.hasAnyRole("USER", "ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/projects/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.PATCH, "/api/projects/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.PUT, "/api/projects/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.DELETE, "/api/projects/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/solutions/**", "/api/products/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.PATCH, "/api/solutions/**", "/api/products/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.PUT, "/api/solutions/**", "/api/products/**")
.hasAnyRole("ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.DELETE, "/api/solutions/**", "/api/products/**")
.hasAnyRole("ARCHITECT", "ADMIN");
// End-user calculations and file transformations use POST bodies but do
// not persist architecture decisions. They are explicitly enumerated so
// a newly introduced POST endpoint cannot inherit the same permission.
auth.requestMatchers(HttpMethod.POST,
"/api/recommend",
"/api/gap/**",
"/api/patterns/**",
"/api/explain/**",
"/api/graph/**",
"/api/diagram/**",
"/api/scores/**",
"/api/export/**",
"/api/report/**",
"/api/decision-report/**")
.hasAnyRole("USER", "ARCHITECT", "ADMIN");
auth.requestMatchers(HttpMethod.POST, "/api/account/change-password").authenticated();
auth.requestMatchers(HttpMethod.POST, "/api/analyze").authenticated();
auth.requestMatchers(HttpMethod.POST, "/api/justify-leaf").authenticated();
auth.requestMatchers(HttpMethod.GET, "/api/**").authenticated();
auth.requestMatchers(HttpMethod.HEAD, "/api/**").authenticated();
auth.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll();
auth.requestMatchers("/api/**").denyAll();
auth.requestMatchers("/**").authenticated();
}
}