I spent last week debugging a permission bug. It was in our main customer dashboard. A developer had added a new user role. The logic worked in one service but failed silently in another. We only caught it because a beta tester reported strange behavior. This kind of fragmentation is why I believe we need a better way to manage who can do what in our applications.
Have you ever faced a situation where a user could see data in one part of your app but not another, even though the rules should be the same? This inconsistency is a common symptom of a bigger problem: authorization logic scattered like broken glass across a codebase.
I want to show you a different approach. Instead of burying if statements in your controllers and services, we can centralize these decisions. This makes them consistent, testable, and auditable. The tool we’ll use for this is called Open Policy Agent, or OPA. When combined with Spring Security, it creates a powerful and clear system.
Let’s start with the basic setup. You’ll need a standard Spring Boot application. First, we add the necessary dependencies to our pom.xml file. This includes Spring Security, a web client for talking to OPA, and a caching library.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
Next, we run OPA. Think of OPA as a separate, dedicated service for making policy decisions. The easiest way is with Docker. Open your terminal and run this command.
docker run -d -p 8181:8181 openpolicyagent/opa run --server
This starts OPA on port 8181. It’s now waiting for us to ask it questions. But what questions do we ask? We ask about permissions. For example, “Can user X perform action Y on resource Z?”
How do we teach OPA the rules? We write them in a language called Rego. It’s purpose-built for defining policies. Let’s create a simple rule file. We’ll call it policy.rego.
package authz
default allow = false
allow {
input.subject.role == "admin"
}
allow {
input.subject.role == "editor"
input.action == "read"
}
This policy is simple. It says: deny by default. Allow if the user’s role is “admin”. Also allow if the user’s role is “editor” and the action is “read”. Notice how the logic is separate from our main Java application.
Now, we need to connect Spring Security to this OPA service. We create a custom authorization manager. This manager will take the current user and HTTP request, package it up, and send it to OPA for a decision.
@Component
public class OpaAuthorizationManager {
private final WebClient webClient;
public AuthorizationDecision check(Object principal, HttpServletRequest request) {
// Build the input JSON for OPA
Map<String, Object> input = Map.of(
"subject", getSubject(principal),
"resource", request.getRequestURI(),
"action", request.getMethod()
);
// Send the request
Boolean allowed = webClient.post()
.uri("/v1/data/authz/allow")
.bodyValue(Map.of("input", input))
.retrieve()
.bodyToMono(Response.class)
.map(response -> response.result)
.block();
return new AuthorizationDecision(Boolean.TRUE.equals(allowed));
}
}
We then wire this manager into our security configuration. This replaces the old @PreAuthorize annotations. Every request now flows through this central checkpoint.
But what about more complex rules? Simple roles are often not enough. What if access depends on the time of day, the user’s department, or the specific data they’re trying to view? This is where attribute-based checks shine.
We can expand our Rego policy to handle these scenarios. Imagine a rule where managers can only approve expenses from their own team.
allow {
input.action == "approve"
input.subject.role == "manager"
input.resource.owner_department == input.subject.department
}
This logic is now in one place. If the business rule changes, we update the Rego file. We don’t need to search through dozens of Java classes.
A major advantage of OPA is testing. You can test your policies in complete isolation. Create a policy_test.rego file. You feed it mock input and assert the expected output. This is far simpler than spinning up a whole Spring context to test security logic.
What happens when your application gets thousands of requests per second? Calling out to OPA for every single one could be slow. This is where the caching library we added comes in. We can store permission decisions for a short time.
@Cacheable(value = "opaDecisions", key = "{#principal.name, #requestURI, #method}")
public AuthorizationDecision checkCached(Object principal, String requestURI, String method) {
return check(principal, requestURI, method);
}
This cache might store that user “Alice” can GET /api/reports for the next 30 seconds. It reduces load on the OPA server and speeds up response times.
Another powerful concept is data filtering. Sometimes, you don’t just want a yes/no answer. You want OPA to tell you which records a user is allowed to see. You send a list of resources to OPA, and it returns a filtered subset.
This solves the row-level security problem elegantly. Your service code fetches data without worrying about permissions. Then, it asks OPA, “Here are 100 records, which ones can this user see?” OPA returns the safe list.
The journey from scattered if statements to a unified policy service is transformative. Your code becomes cleaner. Your security rules become visible and manageable. Auditing who can do what becomes a matter of reading the Rego files, not deciphering business logic.
I encourage you to start small. Take one service, one permission check, and try moving it to OPA. You might be surprised by the clarity it brings. What’s the most tangled permission rule in your current project? Could expressing it in a single, clear Rego statement help?
Building this system has given our team confidence. We know that a change to a policy is made in one place and applied everywhere. The days of hunting for hidden security checks are over. Our application is more secure because of it.
If you found this walk-through helpful, please share it with a colleague who might be facing similar challenges. Have you tried a similar approach? What was your experience? Let me know in the comments below
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva