google-site-verification: google61fe8ba583a51912.html
Open Policy Agent (OPA) in Java: Complete Integration Guide

Open Policy Agent (OPA) is a cloud-native, general-purpose policy engine that enables unified, context-aware policy enforcement across your stack. Instead of embedding policy logic in your application code, you externalize it to OPA and query it via APIs.


OPA Core Concepts

  • Rego: OPA's purpose-built policy language
  • Data: JSON documents that policies evaluate against
  • Input: JSON document provided during policy evaluation
  • Query: Request to evaluate policies against input/data

Architecture Overview

Java Application → HTTP/REST → OPA (Policy Decision) → HTTP/REST → Java Application ↑ Policies + Data

Implementation Approaches

1. Using OPA as a Sidecar (Recommended for Production)

Run OPA as a separate process/container and communicate via HTTP

2. OPA Go Libraries (Not directly in Java)

Embed OPA in Go applications (not applicable for Java)

3. OPA REST API (Most Common in Java)

Make HTTP requests to OPA's evaluation endpoints


Java Implementation Guide

Let's build a complete authorization system using OPA with Spring Boot.

1. Project Dependencies (pom.xml)

<dependencies> <!-- Spring Boot Starter Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- Validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- Cache --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> </dependencies>

2. OPA Policy (Rego) - policies/authz.rego

package authz # Default deny default allow = false # Allow if user has required role for the resource allow { # User roles from input user_roles := input.user.roles # Resource being accessed required_role := input.resource.required_role # Check if user has the required role user_has_role(user_roles, required_role) } # Check if user has a specific role user_has_role(roles, required_role) { roles[_] == required_role } # More specific rules for different resources allow { input.resource.type == "public" } # Role hierarchy user_has_role(roles, required_role) { role_hierarchy[required_role] == higher_role user_has_role(roles, higher_role) } # Role hierarchy mapping role_hierarchy = { "viewer": "editor", "editor": "admin", "admin": "superadmin" } # Get user permissions for fine-grained access user_permissions[permission] { role_permissions[role] := permissions input.user.roles[_] == role permissions[_] == permission }

3. OPA Client Service

@Service public class OpaClientService { private final WebClient webClient; private final String opaBaseUrl; public OpaClientService(@Value("${opa.base-url:http://localhost:8181}") String opaBaseUrl) { this.opaBaseUrl = opaBaseUrl; this.webClient = WebClient.builder() .baseUrl(opaBaseUrl) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); } public boolean checkPermission(OpaRequest request) { try { OpaResponse response = webClient.post() .uri("/v1/data/authz") .bodyValue(request) .retrieve() .bodyToMono(OpaResponse.class) .block(); return response != null && response.getResult() != null && response.getResult().isAllow(); } catch (Exception e) { throw new OpaEvaluationException("Failed to evaluate OPA policy", e); } } public OpaResponse queryPolicy(String packagePath, Object input) { Map<String, Object> request = Map.of("input", input); return webClient.post() .uri("/v1/data/{package}", packagePath) .bodyValue(request) .retrieve() .bodyToMono(OpaResponse.class) .block(); } // Load policy into OPA public void loadPolicy(String policyId, String policy) { webClient.put() .uri("/v1/policies/{id}", policyId) .bodyValue(Map.of("raw", policy)) .retrieve() .bodyToMono(String.class) .block(); } }

4. Data Transfer Objects

// OPA Request public class OpaRequest { private OpaInput input; public OpaRequest(OpaInput input) { this.input = input; } // Getters and Setters public OpaInput getInput() { return input; } public void setInput(OpaInput input) { this.input = input; } } // OPA Input public class OpaInput { private User user; private Resource resource; private Action action; private Map<String, Object> context; // Constructors public OpaInput() {} public OpaInput(User user, Resource resource, Action action) { this.user = user; this.resource = resource; this.action = action; this.context = new HashMap<>(); } // Getters and Setters public User getUser() { return user; } public void setUser(User user) { this.user = user; } public Resource getResource() { return resource; } public void setResource(Resource resource) { this.resource = resource; } public Action getAction() { return action; } public void setAction(Action action) { this.action = action; } public Map<String, Object> getContext() { return context; } public void setContext(Map<String, Object> context) { this.context = context; } public void addContext(String key, Object value) { if (this.context == null) { this.context = new HashMap<>(); } this.context.put(key, value); } } // User entity public class User { private String id; private String username; private Set<String> roles; private Map<String, Object> attributes; // Constructors, Getters, and Setters public User() { this.roles = new HashSet<>(); this.attributes = new HashMap<>(); } public User(String id, String username, Set<String> roles) { this(); this.id = id; this.username = username; this.roles = roles != null ? roles : new HashSet<>(); } // Getters and Setters public String getId() { return id; } public void setId(String id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Set<String> getRoles() { return roles; } public void setRoles(Set<String> roles) { this.roles = roles; } public Map<String, Object> getAttributes() { return attributes; } public void setAttributes(Map<String, Object> attributes) { this.attributes = attributes; } public void addRole(String role) { this.roles.add(role); } public void addAttribute(String key, Object value) { this.attributes.put(key, value); } } // Resource entity public class Resource { private String id; private String type; private String owner; private String requiredRole; private Map<String, Object> attributes; // Constructors, Getters, and Setters public Resource() { this.attributes = new HashMap<>(); } public Resource(String id, String type, String requiredRole) { this(); this.id = id; this.type = type; this.requiredRole = requiredRole; } // Getters and Setters... } // Action enum public enum Action { READ, WRITE, DELETE, CREATE, UPDATE, LIST, EXECUTE } // OPA Response public class OpaResponse { private OpaResult result; public OpaResponse() {} // Getters and Setters public OpaResult getResult() { return result; } public void setResult(OpaResult result) { this.result = result; } } // OPA Result public class OpaResult { private boolean allow; private List<String> reasons; private Map<String, Object> additionalData; // Constructors public OpaResult() { this.reasons = new ArrayList<>(); this.additionalData = new HashMap<>(); } // Getters and Setters public boolean isAllow() { return allow; } public void setAllow(boolean allow) { this.allow = allow; } public List<String> getReasons() { return reasons; } public void setReasons(List<String> reasons) { this.reasons = reasons; } public Map<String, Object> getAdditionalData() { return additionalData; } public void setAdditionalData(Map<String, Object> additionalData) { this.additionalData = additionalData; } }

5. Spring Security Integration

@Component public class OpaAuthorizationManager { private final OpaClientService opaClient; public OpaAuthorizationManager(OpaClientService opaClient) { this.opaClient = opaClient; } public boolean checkAccess(User user, Resource resource, Action action) { OpaInput input = new OpaInput(user, resource, action); OpaRequest request = new OpaRequest(input); return opaClient.checkPermission(request); } } // Custom Authorization Manager for Spring Security @Component public class OpaAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> { private final OpaClientService opaClient; public OpaAuthorizationManager(OpaClientService opaClient) { this.opaClient = opaClient; } @Override public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) { // Extract user from authentication User user = extractUserFromAuthentication(authentication.get()); // Build resource from HTTP request Resource resource = buildResourceFromRequest(context.getRequest()); // Build action from HTTP method Action action = buildActionFromHttpMethod(context.getRequest().getMethod()); // Check authorization via OPA boolean granted = opaClient.checkPermission( new OpaRequest(new OpaInput(user, resource, action)) ); return new AuthorizationDecision(granted); } private User extractUserFromAuthentication(Authentication authentication) { if (authentication == null || !authentication.isAuthenticated()) { return new User("anonymous", "anonymous", Set.of("anonymous")); } // Extract user details from Spring Security Authentication // This is a simplified example - adapt to your user model String username = authentication.getName(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); Set<String> roles = authorities.stream() .map(GrantedAuthority::getAuthority) .map(role -> role.replace("ROLE_", "")) .collect(Collectors.toSet()); return new User(username, username, roles); } private Resource buildResourceFromRequest(HttpServletRequest request) { String path = request.getRequestURI(); String method = request.getMethod(); return new Resource(path, "http_endpoint", determineRequiredRole(method, path)); } private Action buildActionFromHttpMethod(String httpMethod) { return switch (httpMethod.toUpperCase()) { case "GET" -> Action.READ; case "POST" -> Action.CREATE; case "PUT", "PATCH" -> Action.UPDATE; case "DELETE" -> Action.DELETE; default -> Action.READ; }; } private String determineRequiredRole(String method, String path) { // Implement logic to map endpoints to required roles if (path.startsWith("/admin")) return "admin"; if (path.startsWith("/api/secure")) return "user"; return "viewer"; } }

6. Security Configuration

@Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { private final OpaAuthorizationManager opaAuthorizationManager; public SecurityConfig(OpaAuthorizationManager opaAuthorizationManager) { this.opaAuthorizationManager = opaAuthorizationManager; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/public/**").permitAll() .anyRequest().access(opaAuthorizationManager) ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(Customizer.withDefaults()) ) .csrf(csrf -> csrf.disable()); // Enable in production with proper configuration return http.build(); } }

7. REST Controller with OPA Protection

@RestController @RequestMapping("/api") public class DocumentController { private final OpaAuthorizationManager opaAuthManager; public DocumentController(OpaAuthorizationManager opaAuthManager) { this.opaAuthManager = opaAuthManager; } @GetMapping("/documents/{id}") public ResponseEntity<Document> getDocument(@PathVariable String id, Authentication authentication) { User user = extractUser(authentication); Resource resource = new Resource(id, "document", "viewer"); if (!opaAuthManager.checkAccess(user, resource, Action.READ)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } // Fetch and return document Document document = fetchDocument(id); return ResponseEntity.ok(document); } @PostMapping("/documents") public ResponseEntity<Document> createDocument(@RequestBody Document document, Authentication authentication) { User user = extractUser(authentication); Resource resource = new Resource("documents", "collection", "editor"); if (!opaAuthManager.checkAccess(user, resource, Action.CREATE)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } // Create document Document created = createDocument(document); return ResponseEntity.status(HttpStatus.CREATED).body(created); } private User extractUser(Authentication authentication) { // Implementation to extract user from authentication return new User("user123", "john.doe", Set.of("editor")); } private Document fetchDocument(String id) { // Implementation to fetch document return new Document(id, "Sample Document"); } private Document createDocument(Document document) { // Implementation to create document return document; } }

8. Application Configuration (application.yml)

# OPA Configuration opa: base-url: http://localhost:8181 policy-package: authz # Server Configuration server: port: 8080 # Logging logging: level: com.example.opa: DEBUG

9. Running OPA

Docker Compose (docker-compose.yml):

version: '3.8' services: opa: image: openpolicyagent/opa:latest ports: - "8181:8181" command: - "run" - "--server" - "--addr=:8181" - "--set=decision_logs.console=true" volumes: - ./policies:/policies working_dir: /policies

Manual Startup:

# Start OPA with policies opa run --server ./policies

Advanced Features

1. Caching for Performance

@Service @CacheConfig(cacheNames = "opaDecisions") public class CachedOpaService { private final OpaClientService opaClient; public CachedOpaService(OpaClientService opaClient) { this.opaClient = opaClient; } @Cacheable(key = "{#user.id, #resource.id, #action.name}") public boolean checkAccessCached(User user, Resource resource, Action action) { return opaClient.checkPermission(new OpaRequest(new OpaInput(user, resource, action))); } }

2. Bulk Policy Evaluation

public List<OpaResponse> bulkEvaluate(List<OpaRequest> requests) { return requests.parallelStream() .map(opaClient::checkPermission) .collect(Collectors.toList()); }

3. Health Check

@Component public class OpaHealthIndicator implements HealthIndicator { private final OpaClientService opaClient; public OpaHealthIndicator(OpaClientService opaClient) { this.opaClient = opaClient; } @Override public Health health() { try { // Simple policy evaluation to test OPA connectivity OpaRequest request = new OpaRequest(new OpaInput( new User("health-check", "health-check", Set.of("admin")), new Resource("health", "system", "admin"), Action.READ )); opaClient.checkPermission(request); return Health.up().withDetail("opa", "reachable").build(); } catch (Exception e) { return Health.down().withDetail("opa", "unreachable").withException(e).build(); } } }

Best Practices

  1. Policy Organization: Structure policies by domain (authz, validation, quota)
  2. Input Validation: Validate inputs before sending to OPA
  3. Caching: Cache policy decisions for performance
  4. Error Handling: Implement fallback mechanisms for OPA unavailability
  5. Testing: Write unit tests for both Java code and Rego policies
  6. Monitoring: Log policy decisions and monitor OPA performance

This implementation provides a robust foundation for integrating OPA with Java applications, enabling externalized, context-aware policy enforcement across your system.

Leave a Reply

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


Macro Nepal Helper