google-site-verification: google61fe8ba583a51912.html
Policy Enforcement with OPA in Java

Overview

Open Policy Agent (OPA) is a general-purpose policy engine that enables unified, context-aware policy enforcement across the entire stack. This guide covers comprehensive OPA integration in Java applications for authorization, validation, and governance.

Architecture Components

1. OPA Client Implementation

@Component public class OpaClient { private final RestTemplate restTemplate; private final String opaBaseUrl; private final ObjectMapper objectMapper; @Value("${opa.base-url:http://localhost:8181}") private String defaultOpaBaseUrl; public OpaClient(RestTemplate restTemplate, ObjectMapper objectMapper) { this.restTemplate = restTemplate; this.objectMapper = objectMapper; this.opaBaseUrl = defaultOpaBaseUrl; } public OpaClient(RestTemplate restTemplate, ObjectMapper objectMapper, String opaBaseUrl) { this.restTemplate = restTemplate; this.objectMapper = objectMapper; this.opaBaseUrl = opaBaseUrl; } public <T> OpaResponse<T> evaluatePolicy(String policyPath, Object input, Class<T> resultType) throws OpaException { try { OpaRequest request = new OpaRequest(input); String url = String.format("%s/v1/data/%s", opaBaseUrl, policyPath); ResponseEntity<String> response = restTemplate.postForEntity( url, request, String.class); if (!response.getStatusCode().is2xxSuccessful()) { throw new OpaException("OPA request failed with status: " + response.getStatusCode()); } JsonNode rootNode = objectMapper.readTree(response.getBody()); JsonNode resultNode = rootNode.path("result"); if (resultNode.isMissingNode()) { throw new OpaException("No result in OPA response"); } T result = objectMapper.treeToValue(resultNode, resultType); return new OpaResponse<>(result, rootNode); } catch (Exception e) { throw new OpaException("Failed to evaluate OPA policy: " + policyPath, e); } } public boolean checkPolicy(String policyPath, Object input) throws OpaException { OpaResponse<Boolean> response = evaluatePolicy(policyPath, input, Boolean.class); return Boolean.TRUE.equals(response.getResult()); } public void healthCheck() throws OpaException { try { String url = String.format("%s/health", opaBaseUrl); ResponseEntity<String> response = restTemplate.getForEntity(url, String.class); if (!response.getStatusCode().is2xxSuccessful()) { throw new OpaException("OPA health check failed"); } } catch (Exception e) { throw new OpaException("OPA health check failed", e); } } public List<String> listPolicies() throws OpaException { try { String url = String.format("%s/v1/policies", opaBaseUrl); ResponseEntity<String> response = restTemplate.getForEntity(url, String.class); JsonNode rootNode = objectMapper.readTree(response.getBody()); JsonNode policiesNode = rootNode.path("result"); List<String> policies = new ArrayList<>(); if (policiesNode.isArray()) { for (JsonNode policyNode : policiesNode) { policies.add(policyNode.path("id").asText()); } } return policies; } catch (Exception e) { throw new OpaException("Failed to list OPA policies", e); } } } // Request/Response classes public class OpaRequest { private final Object input; public OpaRequest(Object input) { this.input = input; } public Object getInput() { return input; } } public class OpaResponse<T> { private final T result; private final JsonNode rawResponse; public OpaResponse(T result, JsonNode rawResponse) { this.result = result; this.rawResponse = rawResponse; } public T getResult() { return result; } public JsonNode getRawResponse() { return rawResponse; } public <U> U getAdditionalField(String fieldName, Class<U> type) { try { JsonNode fieldNode = rawResponse.path(fieldName); if (!fieldNode.isMissingNode()) { return objectMapper.treeToValue(fieldNode, type); } return null; } catch (Exception e) { return null; } } } public class OpaException extends Exception { public OpaException(String message) { super(message); } public OpaException(String message, Throwable cause) { super(message, cause); } }

2. Policy Management Service

@Service public class PolicyManagementService { private final OpaClient opaClient; private final PolicyRepository policyRepository; private final ObjectMapper objectMapper; public PolicyManagementService(OpaClient opaClient, PolicyRepository policyRepository, ObjectMapper objectMapper) { this.opaClient = opaClient; this.policyRepository = policyRepository; this.objectMapper = objectMapper; } @Async public CompletableFuture<PolicyDeploymentResult> deployPolicy(PolicyDefinition policy) { return CompletableFuture.supplyAsync(() -> { try { // Validate policy syntax validatePolicySyntax(policy.getRegoCode()); // Deploy to OPA deployToOpa(policy); // Store in repository policyRepository.save(policy); return new PolicyDeploymentResult(true, "Policy deployed successfully", policy.getId()); } catch (Exception e) { return new PolicyDeploymentResult(false, e.getMessage(), policy.getId()); } }); } public PolicyValidationResult validatePolicy(String regoCode) { try { // Basic syntax validation validatePolicySyntax(regoCode); // Test with sample inputs if available Map<String, Object> sampleInput = createSampleInput(); OpaRequest request = new OpaRequest(sampleInput); // This would require a temporary deployment for validation return new PolicyValidationResult(true, "Policy validation successful"); } catch (Exception e) { return new PolicyValidationResult(false, e.getMessage()); } } public List<PolicyDefinition> getActivePolicies() { return policyRepository.findByStatus(PolicyStatus.ACTIVE); } public PolicyDefinition getPolicy(String policyId) { return policyRepository.findById(policyId) .orElseThrow(() -> new PolicyNotFoundException("Policy not found: " + policyId)); } public void activatePolicy(String policyId) { PolicyDefinition policy = getPolicy(policyId); policy.setStatus(PolicyStatus.ACTIVE); policyRepository.save(policy); // Redeploy to OPA deployToOpa(policy); } public void deactivatePolicy(String policyId) { PolicyDefinition policy = getPolicy(policyId); policy.setStatus(PolicyStatus.INACTIVE); policyRepository.save(policy); // Remove from OPA (implementation depends on OPA API) removeFromOpa(policy); } public PolicyEvaluationResult evaluatePolicy(String policyId, Object input) { try { PolicyDefinition policy = getPolicy(policyId); String policyPath = buildPolicyPath(policy); OpaResponse<Map<String, Object>> response = opaClient.evaluatePolicy( policyPath, input, Map.class); return new PolicyEvaluationResult(true, response.getResult(), null); } catch (OpaException e) { return new PolicyEvaluationResult(false, null, e.getMessage()); } } private void validatePolicySyntax(String regoCode) throws PolicyValidationException { // Basic Rego syntax validation if (regoCode == null || regoCode.trim().isEmpty()) { throw new PolicyValidationException("Policy code cannot be empty"); } // Check for package declaration if (!regoCode.contains("package ")) { throw new PolicyValidationException("Policy must contain package declaration"); } // Add more sophisticated validation as needed } private void deployToOpa(PolicyDefinition policy) throws OpaException { // Implement OPA policy deployment via REST API // This is a simplified version String policyUrl = String.format("%s/v1/policies/%s", opaClient.getBaseUrl(), policy.getId()); Map<String, String> policyPayload = Map.of( "policy", policy.getRegoCode() ); // restTemplate.put(policyUrl, policyPayload); } private void removeFromOpa(PolicyDefinition policy) { // Implement OPA policy removal } private String buildPolicyPath(PolicyDefinition policy) { return policy.getPackageName().replace(".", "/"); } private Map<String, Object> createSampleInput() { // Create sample input based on policy type return Map.of( "user", Map.of( "id", "user123", "roles", List.of("user"), "department", "engineering" ), "resource", Map.of( "type", "document", "owner", "user123", "sensitivity", "confidential" ), "action", "read" ); } } // Policy-related classes public class PolicyDefinition { private String id; private String name; private String description; private String packageName; private String regoCode; private PolicyType type; private PolicyStatus status; private Instant createdAt; private Instant updatedAt; private Map<String, Object> metadata; // Constructors, getters, setters public PolicyDefinition() { this.id = UUID.randomUUID().toString(); this.createdAt = Instant.now(); this.updatedAt = Instant.now(); this.status = PolicyStatus.DRAFT; this.metadata = new HashMap<>(); } // Getters and setters } public enum PolicyType { AUTHORIZATION, VALIDATION, COMPLIANCE, GOVERNANCE, CUSTOM } public enum PolicyStatus { DRAFT, ACTIVE, INACTIVE, DEPRECATED } public class PolicyDeploymentResult { private final boolean success; private final String message; private final String policyId; public PolicyDeploymentResult(boolean success, String message, String policyId) { this.success = success; this.message = message; this.policyId = policyId; } // Getters } public class PolicyValidationResult { private final boolean valid; private final String message; public PolicyValidationResult(boolean valid, String message) { this.valid = valid; this.message = message; } // Getters } public class PolicyEvaluationResult { private final boolean allowed; private final Map<String, Object> decision; private final String error; public PolicyEvaluationResult(boolean allowed, Map<String, Object> decision, String error) { this.allowed = allowed; this.decision = decision; this.error = error; } // Getters }

Authorization Policies

1. RBAC (Role-Based Access Control) Policy Service

@Service public class RbacPolicyService { private final OpaClient opaClient; private final UserService userService; private final ResourceService resourceService; private static final String RBAC_POLICY_PATH = "authz/rbac"; public RbacPolicyService(OpaClient opaClient, UserService userService, ResourceService resourceService) { this.opaClient = opaClient; this.userService = userService; this.resourceService = resourceService; } public AuthorizationResult checkPermission(String userId, String action, String resourceType, String resourceId) { try { // Build authorization input AuthzInput input = buildAuthzInput(userId, action, resourceType, resourceId); // Evaluate policy OpaResponse<AuthzResult> response = opaClient.evaluatePolicy( RBAC_POLICY_PATH, input, AuthzResult.class); AuthzResult result = response.getResult(); return new AuthorizationResult( result.isAllowed(), result.getReason(), result.getAdditionalConstraints() ); } catch (OpaException e) { return AuthorizationResult.denied("Policy evaluation failed: " + e.getMessage()); } } public boolean isAllowed(String userId, String action, String resourceType, String resourceId) { AuthorizationResult result = checkPermission(userId, action, resourceType, resourceId); return result.isAllowed(); } public List<String> getAllowedActions(String userId, String resourceType, String resourceId) { try { AuthzInput input = buildAuthzInput(userId, null, resourceType, resourceId); input.setQueryAllowedActions(true); OpaResponse<AllowedActionsResult> response = opaClient.evaluatePolicy( RBAC_POLICY_PATH + "/allowed_actions", input, AllowedActionsResult.class); return response.getResult().getAllowedActions(); } catch (OpaException e) { return Collections.emptyList(); } } private AuthzInput buildAuthzInput(String userId, String action, String resourceType, String resourceId) { User user = userService.getUser(userId); Resource resource = resourceService.getResource(resourceType, resourceId); return new AuthzInput(user, action, resource); } // Authorization input/result classes public static class AuthzInput { private User user; private String action; private Resource resource; private boolean queryAllowedActions; public AuthzInput(User user, String action, Resource resource) { this.user = user; this.action = action; this.resource = resource; } // Getters and setters } public static class AuthzResult { private boolean allowed; private String reason; private Map<String, Object> additionalConstraints; // Getters and setters } public static class AllowedActionsResult { private List<String> allowedActions; // Getters and setters } } public class AuthorizationResult { private final boolean allowed; private final String reason; private final Map<String, Object> constraints; public AuthorizationResult(boolean allowed, String reason, Map<String, Object> constraints) { this.allowed = allowed; this.reason = reason; this.constraints = constraints != null ? constraints : Collections.emptyMap(); } public static AuthorizationResult allowed(String reason) { return new AuthorizationResult(true, reason, Collections.emptyMap()); } public static AuthorizationResult denied(String reason) { return new AuthorizationResult(false, reason, Collections.emptyMap()); } public static AuthorizationResult allowedWithConstraints(String reason, Map<String, Object> constraints) { return new AuthorizationResult(true, reason, constraints); } // Getters public boolean isAllowed() { return allowed; } public String getReason() { return reason; } public Map<String, Object> getConstraints() { return constraints; } public <T> T getConstraint(String key, Class<T> type) { Object value = constraints.get(key); return type.isInstance(value) ? type.cast(value) : null; } }

2. ABAC (Attribute-Based Access Control) Policy Service

@Service public class AbacPolicyService { private final OpaClient opaClient; private final AttributeService attributeService; private static final String ABAC_POLICY_PATH = "authz/abac"; public AbacPolicyService(OpaClient opaClient, AttributeService attributeService) { this.opaClient = opaClient; this.attributeService = attributeService; } public AuthorizationResult checkAccess(AccessRequest request) { try { // Enrich with attributes AccessContext context = enrichWithAttributes(request); // Evaluate ABAC policy OpaResponse<AccessDecision> response = opaClient.evaluatePolicy( ABAC_POLICY_PATH, context, AccessDecision.class); AccessDecision decision = response.getResult(); return new AuthorizationResult( decision.isGranted(), decision.getReason(), decision.getObligations() ); } catch (OpaException e) { return AuthorizationResult.denied("ABAC policy evaluation failed: " + e.getMessage()); } } public boolean canAccess(AccessRequest request) { AuthorizationResult result = checkAccess(request); return result.isAllowed(); } public List<AccessRequest> filterResources(String userId, String action, List<AccessRequest> resources) { return resources.stream() .filter(resource -> { AccessRequest request = new AccessRequest(userId, action, resource.getResource()); return canAccess(request); }) .collect(Collectors.toList()); } private AccessContext enrichWithAttributes(AccessRequest request) { Map<String, Object> userAttributes = attributeService.getUserAttributes(request.getUserId()); Map<String, Object> resourceAttributes = attributeService.getResourceAttributes( request.getResource().getType(), request.getResource().getId()); Map<String, Object> environmentAttributes = attributeService.getEnvironmentAttributes(); return new AccessContext( request.getUserId(), request.getAction(), request.getResource(), userAttributes, resourceAttributes, environmentAttributes, Instant.now() ); } // ABAC-specific classes public static class AccessRequest { private final String userId; private final String action; private final Resource resource; public AccessRequest(String userId, String action, Resource resource) { this.userId = userId; this.action = action; this.resource = resource; } // Getters } public static class AccessContext { private final String userId; private final String action; private final Resource resource; private final Map<String, Object> userAttributes; private final Map<String, Object> resourceAttributes; private final Map<String, Object> environmentAttributes; private final Instant accessTime; public AccessContext(String userId, String action, Resource resource, Map<String, Object> userAttributes, Map<String, Object> resourceAttributes, Map<String, Object> environmentAttributes, Instant accessTime) { this.userId = userId; this.action = action; this.resource = resource; this.userAttributes = userAttributes; this.resourceAttributes = resourceAttributes; this.environmentAttributes = environmentAttributes; this.accessTime = accessTime; } // Getters } public static class AccessDecision { private boolean granted; private String reason; private Map<String, Object> obligations; // Getters and setters } }

Spring Security Integration

1. OPA-Based Method Security

@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class OpaMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration { @Autowired private OpaClient opaClient; @Autowired private UserService userService; @Override protected MethodSecurityExpressionHandler createExpressionHandler() { OpaMethodSecurityExpressionHandler expressionHandler = new OpaMethodSecurityExpressionHandler(); expressionHandler.setOpaClient(opaClient); expressionHandler.setUserService(userService); return expressionHandler; } } public class OpaMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler { private OpaClient opaClient; private UserService userService; @Override protected MethodSecurityExpressionOperations createSecurityExpressionRoot( Authentication authentication, MethodInvocation invocation) { OpaMethodSecurityExpressionRoot root = new OpaMethodSecurityExpressionRoot(authentication); root.setOpaClient(opaClient); root.setUserService(userService); root.setPermissionEvaluator(getPermissionEvaluator()); root.setTrustResolver(getTrustResolver()); root.setRoleHierarchy(getRoleHierarchy()); return root; } // Setters public void setOpaClient(OpaClient opaClient) { this.opaClient = opaClient; } public void setUserService(UserService userService) { this.userService = userService; } } public class OpaMethodSecurityExpressionRoot extends SecurityExpressionRoot { private OpaClient opaClient; private UserService userService; public OpaMethodSecurityExpressionRoot(Authentication authentication) { super(authentication); } public boolean checkOpaPolicy(String policyPath, Object resource, String action) { try { String userId = getCurrentUserId(); Map<String, Object> input = Map.of( "user", Map.of("id", userId), "resource", resource, "action", action ); return opaClient.checkPolicy(policyPath, input); } catch (OpaException e) { return false; } } public boolean hasOpaPermission(String resourceType, String resourceId, String action) { String policyPath = String.format("authz/%s", resourceType); Map<String, Object> resource = Map.of("type", resourceType, "id", resourceId); return checkOpaPolicy(policyPath, resource, action); } public boolean isInDepartment(String department) { String userId = getCurrentUserId(); User user = userService.getUser(userId); return department.equals(user.getDepartment()); } private String getCurrentUserId() { if (authentication.getPrincipal() instanceof UserDetails) { return ((UserDetails) authentication.getPrincipal()).getUsername(); } return authentication.getPrincipal().toString(); } // Setters public void setOpaClient(OpaClient opaClient) { this.opaClient = opaClient; } public void setUserService(UserService userService) { this.userService = userService; } }

2. Spring Security OPA Filter

@Component public class OpaAuthorizationFilter extends OncePerRequestFilter { private final OpaClient opaClient; private final UserService userService; private final List<String> excludePaths; public OpaAuthorizationFilter(OpaClient opaClient, UserService userService) { this.opaClient = opaClient; this.userService = userService; this.excludePaths = Arrays.asList("/public/", "/health", "/error"); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Skip OPA check for excluded paths if (shouldSkipOpaCheck(request)) { filterChain.doFilter(request, response); return; } // Build OPA input from request OpaHttpInput input = buildOpaInput(request); try { // Evaluate HTTP API policy OpaResponse<HttpAuthzResult> opaResponse = opaClient.evaluatePolicy( "httpapi/authz", input, HttpAuthzResult.class); HttpAuthzResult result = opaResponse.getResult(); if (result.isAllowed()) { // Add OPA result to request for downstream use request.setAttribute("OPA_RESULT", result); filterChain.doFilter(request, response); } else { sendForbiddenResponse(response, result); } } catch (OpaException e) { sendErrorResponse(response, "Policy evaluation failed"); } } private boolean shouldSkipOpaCheck(HttpServletRequest request) { String path = request.getRequestURI(); return excludePaths.stream().anyMatch(path::startsWith); } private OpaHttpInput buildOpaInput(HttpServletRequest request) { String userId = extractUserId(request); User user = userService.getUser(userId); return new OpaHttpInput( user, request.getMethod(), request.getRequestURI(), request.getParameterMap(), extractHeaders(request), extractJwtClaims(request) ); } private String extractUserId(HttpServletRequest request) { // Extract user ID from JWT or session Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated()) { return auth.getName(); } return "anonymous"; } private Map<String, String> extractHeaders(HttpServletRequest request) { Map<String, String> headers = new HashMap<>(); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); headers.put(headerName, request.getHeader(headerName)); } return headers; } private Map<String, Object> extractJwtClaims(HttpServletRequest request) { // Extract JWT claims if available String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { // Parse JWT and extract claims } return Collections.emptyMap(); } private void sendForbiddenResponse(HttpServletResponse response, HttpAuthzResult result) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); response.setContentType("application/json"); Map<String, Object> errorResponse = Map.of( "error", "Forbidden", "message", result.getReason(), "timestamp", Instant.now() ); response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse)); } private void sendErrorResponse(HttpServletResponse response, String message) throws IOException { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json"); Map<String, Object> errorResponse = Map.of( "error", "Internal Server Error", "message", message, "timestamp", Instant.now() ); response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse)); } } // HTTP-specific OPA classes public class OpaHttpInput { private final User user; private final String method; private final String path; private final Map<String, String[]> queryParams; private final Map<String, String> headers; private final Map<String, Object> jwtClaims; public OpaHttpInput(User user, String method, String path, Map<String, String[]> queryParams, Map<String, String> headers, Map<String, Object> jwtClaims) { this.user = user; this.method = method; this.path = path; this.queryParams = queryParams; this.headers = headers; this.jwtClaims = jwtClaims; } // Getters } public class HttpAuthzResult { private boolean allowed; private String reason; private Map<String, Object> context; private List<HttpHeader> additionalHeaders; // Getters and setters } public class HttpHeader { private String name; private String value; // Getters and setters }

Data Validation Policies

1. Input Validation Service

@Service public class OpaValidationService { private final OpaClient opaClient; public OpaValidationService(OpaClient opaClient) { this.opaClient = opaClient; } public <T> ValidationResult validate(String policyPath, T data) { try { OpaResponse<ValidationOutput> response = opaClient.evaluatePolicy( policyPath, data, ValidationOutput.class); ValidationOutput output = response.getResult(); return new ValidationResult(output.isValid(), output.getViolations()); } catch (OpaException e) { return ValidationResult.invalid( List.of(new Violation("system", "Validation failed: " + e.getMessage()))); } } public <T> void validateAndThrow(String policyPath, T data) throws ValidationException { ValidationResult result = validate(policyPath, data); if (!result.isValid()) { throw new ValidationException(result.getViolations()); } } public <T> boolean isValid(String policyPath, T data) { ValidationResult result = validate(policyPath, data); return result.isValid(); } // Specific validation methods public ValidationResult validateUserRegistration(UserRegistrationRequest request) { return validate("validation/user_registration", request); } public ValidationResult validateOrder(Order order) { return validate("validation/order", order); } public ValidationResult validateConfig(ConfigUpdate config) { return validate("validation/config", config); } } // Validation-related classes public class ValidationResult { private final boolean valid; private final List<Violation> violations; public ValidationResult(boolean valid, List<Violation> violations) { this.valid = valid; this.violations = violations != null ? violations : Collections.emptyList(); } public static ValidationResult valid() { return new ValidationResult(true, Collections.emptyList()); } public static ValidationResult invalid(List<Violation> violations) { return new ValidationResult(false, violations); } public static ValidationResult invalid(String field, String message) { return new ValidationResult(false, List.of(new Violation(field, message))); } // Getters public boolean isValid() { return valid; } public List<Violation> getViolations() { return violations; } public String getErrorMessage() { return violations.stream() .map(Violation::getMessage) .collect(Collectors.joining("; ")); } } public class Violation { private final String field; private final String message; private final String code; public Violation(String field, String message) { this(field, message, null); } public Violation(String field, String message, String code) { this.field = field; this.message = message; this.code = code; } // Getters } public class ValidationOutput { private boolean valid; private List<Violation> violations; // Getters and setters } public class ValidationException extends Exception { private final List<Violation> violations; public ValidationException(List<Violation> violations) { super(buildMessage(violations)); this.violations = violations; } public ValidationException(String field, String message) { this(List.of(new Violation(field, message))); } private static String buildMessage(List<Violation> violations) { return violations.stream() .map(v -> v.getField() + ": " + v.getMessage()) .collect(Collectors.joining("; ")); } public List<Violation> getViolations() { return violations; } }

2. Spring Validation Integration

@Target({ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = OpaValidator.class) public @interface OpaValid { String policyPath(); String message() default "Validation failed"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } public class OpaValidator implements ConstraintValidator<OpaValid, Object> { @Autowired private OpaValidationService validationService; private String policyPath; @Override public void initialize(OpaValid constraintAnnotation) { this.policyPath = constraintAnnotation.policyPath(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { if (value == null) { return true; } ValidationResult result = validationService.validate(policyPath, value); if (!result.isValid()) { context.disableDefaultConstraintViolation(); for (Violation violation : result.getViolations()) { context.buildConstraintViolationWithTemplate(violation.getMessage()) .addPropertyNode(violation.getField()) .addConstraintViolation(); } return false; } return true; } } // Usage in controllers @RestController @RequestMapping("/api/users") @Validated public class UserController { @PostMapping public ResponseEntity<User> createUser( @RequestBody @OpaValid(policyPath = "validation/user_registration") UserRegistrationRequest request) { // Process valid request return ResponseEntity.ok(userService.createUser(request)); } @PutMapping("/{userId}") public ResponseEntity<User> updateUser( @PathVariable String userId, @RequestBody @OpaValid(policyPath = "validation/user_update") UserUpdateRequest request) { // Process valid request return ResponseEntity.ok(userService.updateUser(userId, request)); } }

Policy Testing Framework

1. OPA Policy Testing

@SpringBootTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class OpaPolicyTest { @Autowired private OpaClient opaClient; @Autowired private PolicyManagementService policyService; @Test public void testRbacPolicy() throws OpaException { // Test data Map<String, Object> input = Map.of( "user", Map.of( "id", "alice", "roles", List.of("admin", "user") ), "action", "delete", "resource", Map.of( "type", "user", "owner", "bob" ) ); OpaResponse<Map<String, Object>> response = opaClient.evaluatePolicy( "authz/rbac", input, Map.class); assertThat(response.getResult()).isNotNull(); assertThat(response.getResult().get("allowed")).isEqualTo(true); } @Test public void testValidationPolicy() throws OpaException { Map<String, Object> userInput = Map.of( "username", "john_doe", "email", "invalid-email", "age", 15 ); OpaResponse<Map<String, Object>> response = opaClient.evaluatePolicy( "validation/user", userInput, Map.class); assertThat(response.getResult()).isNotNull(); assertThat(response.getResult().get("valid")).isEqualTo(false); @SuppressWarnings("unchecked") List<Map<String, String>> violations = (List<Map<String, String>>) response.getResult().get("violations"); assertThat(violations).isNotEmpty(); assertThat(violations.get(0).get("field")).isEqualTo("email"); } @ParameterizedTest @MethodSource("provideAuthorizationTestCases") public void testAuthorizationScenarios(String userRole, String action, String resourceType, boolean expected) throws OpaException { Map<String, Object> input = Map.of( "user", Map.of("roles", List.of(userRole)), "action", action, "resource", Map.of("type", resourceType) ); boolean result = opaClient.checkPolicy("authz/rbac", input); assertThat(result).isEqualTo(expected); } private static Stream<Arguments> provideAuthorizationTestCases() { return Stream.of( Arguments.of("admin", "delete", "user", true), Arguments.of("user", "delete", "user", false), Arguments.of("user", "read", "document", true), Arguments.of("guest", "write", "document", false) ); } } @Component public class PolicyTestRunner { private final OpaClient opaClient; private final ObjectMapper objectMapper; public PolicyTestRunner(OpaClient opaClient, ObjectMapper objectMapper) { this.opaClient = opaClient; this.objectMapper = objectMapper; } public PolicyTestResult runPolicyTests(String policyPath, List<PolicyTestCase> testCases) { List<PolicyTestResult.TestCaseResult> results = new ArrayList<>(); for (PolicyTestCase testCase : testCases) { PolicyTestResult.TestCaseResult result = runTestCase(policyPath, testCase); results.add(result); } long passed = results.stream().filter(PolicyTestResult.TestCaseResult::isPassed).count(); return new PolicyTestResult(policyPath, results, passed, testCases.size()); } private PolicyTestResult.TestCaseResult runTestCase(String policyPath, PolicyTestCase testCase) { try { OpaResponse<Map<String, Object>> response = opaClient.evaluatePolicy( policyPath, testCase.getInput(), Map.class); boolean passed = Objects.equals(response.getResult(), testCase.getExpectedOutput()); String message = passed ? "Test passed" : "Test failed"; return new PolicyTestResult.TestCaseResult( testCase.getName(), passed, message, testCase.getInput(), response.getResult()); } catch (OpaException e) { return new PolicyTestResult.TestCaseResult( testCase.getName(), false, "Test error: " + e.getMessage(), testCase.getInput(), null); } } } public class PolicyTestCase { private String name; private String description; private Map<String, Object> input; private Map<String, Object> expectedOutput; // Constructors, getters, setters } public class PolicyTestResult { private final String policyPath; private final List<TestCaseResult> testResults; private final long passedCount; private final long totalCount; public PolicyTestResult(String policyPath, List<TestCaseResult> testResults, long passedCount, long totalCount) { this.policyPath = policyPath; this.testResults = testResults; this.passedCount = passedCount; this.totalCount = totalCount; } public boolean isAllPassed() { return passedCount == totalCount; } public double getSuccessRate() { return totalCount > 0 ? (double) passedCount / totalCount : 0.0; } public static class TestCaseResult { private final String testName; private final boolean passed; private final String message; private final Map<String, Object> input; private final Map<String, Object> actualOutput; public TestCaseResult(String testName, boolean passed, String message, Map<String, Object> input, Map<String, Object> actualOutput) { this.testName = testName; this.passed = passed; this.message = message; this.input = input; this.actualOutput = actualOutput; } // Getters public boolean isPassed() { return passed; } } }

Monitoring and Observability

1. OPA Metrics and Monitoring

@Component public class OpaMetrics { private final MeterRegistry meterRegistry; private final Counter policyEvaluationCounter; private final Timer policyEvaluationTimer; private final Counter policyEvaluationErrorCounter; public OpaMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.policyEvaluationCounter = Counter.builder("opa.policy.evaluations") .description("Number of OPA policy evaluations") .tag("type", "total") .register(meterRegistry); this.policyEvaluationTimer = Timer.builder("opa.policy.evaluation.duration") .description("Time taken for OPA policy evaluation") .register(meterRegistry); this.policyEvaluationErrorCounter = Counter.builder("opa.policy.evaluation.errors") .description("Number of OPA policy evaluation errors") .register(meterRegistry); } public <T> T recordPolicyEvaluation(String policyPath, Supplier<T> evaluation) { policyEvaluationCounter.increment(); return policyEvaluationTimer.record(() -> { try { return evaluation.get(); } catch (Exception e) { policyEvaluationErrorCounter.increment(); throw e; } }); } public void recordPolicyEvaluationResult(String policyPath, boolean allowed, long duration) { Counter.builder("opa.policy.evaluation.results") .tag("policy", policyPath) .tag("result", allowed ? "allowed" : "denied") .register(meterRegistry) .increment(); Timer.builder("opa.policy.evaluation.duration.by_policy") .tag("policy", policyPath) .register(meterRegistry) .record(Duration.ofMillis(duration)); } } @Aspect @Component public class OpaMetricsAspect { private final OpaMetrics opaMetrics; public OpaMetricsAspect(OpaMetrics opaMetrics) { this.opaMetrics = opaMetrics; } @Around("execution(* com.example.opa..*Service.*(..)) && @annotation(recordMetrics)") public Object recordOpaMetrics(ProceedingJoinPoint joinPoint, RecordMetrics recordMetrics) throws Throwable { String policyPath = recordMetrics.policyPath(); long startTime = System.currentTimeMillis(); try { Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - startTime; // Extract allowed/denied from result if possible boolean allowed = extractAllowedFromResult(result); opaMetrics.recordPolicyEvaluationResult(policyPath, allowed, duration); return result; } catch (Exception e) { opaMetrics.recordPolicyEvaluationResult(policyPath, false, System.currentTimeMillis() - startTime); throw e; } } private boolean extractAllowedFromResult(Object result) { if (result instanceof AuthorizationResult) { return ((AuthorizationResult) result).isAllowed(); } else if (result instanceof Boolean) { return (Boolean) result; } else if (result instanceof Map) { Object allowed = ((Map<?, ?>) result).get("allowed"); return Boolean.TRUE.equals(allowed); } return false; } } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RecordMetrics { String policyPath(); }

Configuration and Deployment

1. Spring Boot Configuration

@Configuration @EnableConfigurationProperties(OpaProperties.class) public class OpaAutoConfiguration { @Bean @ConditionalOnMissingBean public RestTemplate opaRestTemplate(OpaProperties properties) { RestTemplate restTemplate = new RestTemplate(); // Configure timeouts HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); factory.setConnectTimeout(properties.getConnectTimeout()); factory.setReadTimeout(properties.getReadTimeout()); restTemplate.setRequestFactory(factory); return restTemplate; } @Bean @ConditionalOnMissingBean public OpaClient opaClient(RestTemplate opaRestTemplate, OpaProperties properties) { return new OpaClient(opaRestTemplate, new ObjectMapper(), properties.getBaseUrl()); } @Bean @ConditionalOnMissingBean public PolicyManagementService policyManagementService(OpaClient opaClient) { return new PolicyManagementService(opaClient, new InMemoryPolicyRepository(), new ObjectMapper()); } @Bean @ConditionalOnMissingBean public RbacPolicyService rbacPolicyService(OpaClient opaClient) { return new RbacPolicyService(opaClient, new DefaultUserService(), new DefaultResourceService()); } @Bean @ConditionalOnMissingBean public OpaValidationService opaValidationService(OpaClient opaClient) { return new OpaValidationService(opaClient); } @Bean @ConditionalOnMissingBean public OpaMetrics opaMetrics(MeterRegistry meterRegistry) { return new OpaMetrics(meterRegistry); } } @ConfigurationProperties(prefix = "opa") public class OpaProperties { private String baseUrl = "http://localhost:8181"; private int connectTimeout = 5000; private int readTimeout = 10000; private boolean enabled = true; private Cache cache = new Cache(); private Security security = new Security(); // Getters and setters public static class Cache { private boolean enabled = true; private int maxSize = 1000; private Duration ttl = Duration.ofMinutes(5); // Getters and setters } public static class Security { private String apiKey; private boolean sslVerification = true; // Getters and setters } }

2. Application Configuration

opa: base-url: ${OPA_URL:http://localhost:8181} connect-timeout: 5000 read-timeout: 10000 enabled: true cache: enabled: true max-size: 1000 ttl: 5m security: api-key: ${OPA_API_KEY:} ssl-verification: true management: endpoints: web: exposure: include: "opa,metrics" endpoint: opa: enabled: true logging: level: com.example.opa: DEBUG

Conclusion

Policy Enforcement with OPA in Java provides:

  1. Unified Policy Management: Centralized policy definition and enforcement
  2. Flexible Authorization: Support for RBAC, ABAC, and custom policies
  3. Data Validation: Comprehensive input and business rule validation
  4. Spring Integration: Seamless integration with Spring Security and validation
  5. Observability: Comprehensive metrics and monitoring

Key benefits:

  • Decoupling: Separate policy logic from application code
  • Reusability: Share policies across multiple services
  • Auditability: Track policy decisions and changes
  • Agility: Update policies without code deployment
  • Compliance: Enforce regulatory and security requirements

This comprehensive approach enables organizations to implement robust, maintainable, and scalable policy enforcement across their Java applications.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper