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:
- Unified Policy Management: Centralized policy definition and enforcement
- Flexible Authorization: Support for RBAC, ABAC, and custom policies
- Data Validation: Comprehensive input and business rule validation
- Spring Integration: Seamless integration with Spring Security and validation
- 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.