SecurityService.java
package com.smartsupplypro.inventory.service;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
/**
* Security context service for authorization checks.
*
* <p><b>Purpose</b>: Centralized authorization logic for demo mode and role-based access control.
* Abstracts Spring Security internals from controllers and @PreAuthorize expressions.</p>
*
* <p><b>Design Pattern</b>: Security service pattern allows complex authorization rules to be:
* <ul>
* <li>Tested independently from controllers</li>
* <li>Shared across multiple @PreAuthorize expressions</li>
* <li>Updated in one place (DRY principle)</li>
* <li>Works with both OAuth2 (runtime) and test authentication (UsernamePasswordAuthenticationToken)</li>
* </ul>
* </p>
*
* <p><b>Enterprise Use Cases</b>:
* <ul>
* <li><b>Demo Mode</b>: Read-only access for user evaluation (isDemo=true blocks mutations)</li>
* <li><b>Role-Based Access</b>: ADMIN vs. USER authorization checks</li>
* <li><b>Feature Flags</b>: Time-limited access during pilot programs</li>
* <li><b>Audit Trail</b>: Track who attempted restricted operations</li>
* </ul>
* </p>
*
* @see <a href="file:../../../../../../docs/architecture/patterns/security-patterns.md">Security Patterns</a>
*/
@Service
public class SecurityService {
/**
* Checks if current authenticated user is in demo mode (read-only).
*
* <p><b>ENTERPRISE CONTEXT</b>:
* Demo mode is a product feature that allows:
* - Sales team to share live app with prospects without granting write access
* - Training environments where users can explore but not modify data
* - A/B testing read-only vs. full-access user experiences
* </p>
*
* <p><b>Authorization Behavior</b>:
* <ul>
* <li>OAuth2 runtime (Google/Azure OAuth): Checks `isDemo` attribute set by authentication service</li>
* <li>Test authentication (@WithMockUser): Returns false (tests don't set isDemo, only test demo scenarios explicitly)</li>
* <li>Unauthenticated: Returns false (no security context)</li>
* </ul>
* </p>
*
* <p><b>Usage in @PreAuthorize</b>:
* <pre>
* @PreAuthorize("hasRole('ADMIN') and !@securityService.isDemo()")
* public ResponseEntity<ItemDTO> create(@RequestBody ItemDTO item) { ... }
* </pre>
* Denies ADMIN users in demo mode from creating items. Normal ADMIN users are allowed.
* </p>
*
* @return true if current user is authenticated AND has isDemo=true; false otherwise
* @throws java.lang.ClassCastException never thrown (safe type checking in place)
*/
public boolean isDemo() {
// Step 1: Retrieve current authentication from thread-local context
// SecurityContextHolder manages authentication for the current request thread
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Step 2: Handle unauthenticated requests (null or !isAuthenticated)
// In these cases, demo mode doesn't apply (no user to be in demo)
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
// Step 3: Extract principal object from authentication token
// This could be OAuth2User (runtime), UserDetails (standard auth), or other types
Object principal = authentication.getPrincipal();
// Step 4: Handle OAuth2 runtime authentication
// At runtime (with real OAuth2), the principal is an OAuth2User with attributes map
if (principal instanceof OAuth2User oauth2User) {
// OAuth2User stores custom attributes (like isDemo) in an attributes map
// Attribute key must match what CustomOAuth2UserService or CustomOidcUserService sets
Boolean isDemoAttribute = oauth2User.getAttribute("isDemo");
// Boolean.TRUE.equals() safely handles null (returns false) and type checking
// This prevents NullPointerException if isDemo attribute is missing
return Boolean.TRUE.equals(isDemoAttribute);
}
// Step 5: Handle test authentication (@WithMockUser)
// @WithMockUser creates UsernamePasswordAuthenticationToken, not OAuth2User
// Tests use explicit OAuth2 mocking only when testing demo scenarios
// Therefore, standard @WithMockUser tests default to isDemo=false
return false;
}
}