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
- Policy Organization: Structure policies by domain (authz, validation, quota)
- Input Validation: Validate inputs before sending to OPA
- Caching: Cache policy decisions for performance
- Error Handling: Implement fallback mechanisms for OPA unavailability
- Testing: Write unit tests for both Java code and Rego policies
- 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.