Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ plugins {
id 'maven-publish'
id 'signing'
id 'cl.franciscosolis.sonatype-central-upload' version '1.0.3'
id 'org.springframework.boot' version '3.3.5'
id 'io.spring.dependency-management' version '1.1.6'
id 'org.springframework.boot' version '4.0.3'
id("checkstyle")
}

apply plugin: 'io.spring.dependency-management'

version = "${version}"
group = "${groupId}"

Expand Down Expand Up @@ -68,23 +69,20 @@ javadoc {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.security:spring-security-web'
implementation 'org.springframework.security:spring-security-core'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework:spring-context'

testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.security:spring-security-config'
testImplementation 'org.testcontainers:testcontainers-bom:1.19.8'
testImplementation 'org.testcontainers:testcontainers:1.19.8'
testImplementation 'org.testcontainers:junit-jupiter:1.19.8'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
testImplementation 'org.testcontainers:testcontainers'
testImplementation 'org.testcontainers:testcontainers-junit-jupiter'

api group: 'io.github.open-policy-agent', name: 'opa', version: '2.1.1'

compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.34'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}

apply plugin: 'application'
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public ConstantContextDataProvider(Object data) {
}

@Override
public Object getContextData(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
public Object getContextData(Supplier<? extends Authentication> authentication,
RequestAuthorizationContext object) {
return data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
*/
@FunctionalInterface
public interface ContextDataProvider {
Object getContextData(Supplier<Authentication> authentication, RequestAuthorizationContext object);
Object getContextData(Supplier<? extends Authentication> authentication, RequestAuthorizationContext object);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.github.open_policy_agent.opa.springboot.input.OPAInputValidator;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Getter;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -54,7 +55,7 @@
* This class implements {@link AuthorizationManager} which wraps the
* <a href="https://github.com/open-policy-agent/opa-java">OPA Java SDK</a>.
* Authorization will be done in
* {@link #check(Supplier, RequestAuthorizationContext)} and
* {@link #authorize(Supplier, RequestAuthorizationContext)} and
* {@link #verify(Supplier, RequestAuthorizationContext)} by:
* <ol>
* <li>constructing an <a href=
Expand All @@ -75,24 +76,32 @@ public class OPAAuthorizationManager implements AuthorizationManager<RequestAuth

private static final Logger LOGGER = LoggerFactory.getLogger(OPAAuthorizationManager.class);

@Nullable
private final String opaPath;
@Getter
private String reasonKey = OPAProperties.Response.Context.DEFAULT_REASON_KEY;
@Nullable
private final ContextDataProvider contextDataProvider;
private final OPAClient opaClient;
@Autowired
private OPAProperties opaProperties;
@Autowired
@Nullable
@Autowired(required = false)
private OPAPathSelector opaPathSelector;
@Nullable
@Autowired(required = false)
private OPAInputSubjectCustomizer opaInputSubjectCustomizer;
@Nullable
@Autowired(required = false)
private OPAInputResourceCustomizer opaInputResourceCustomizer;
@Nullable
@Autowired(required = false)
private OPAInputActionCustomizer opaInputActionCustomizer;
@Nullable
@Autowired(required = false)
private OPAInputContextCustomizer opaInputContextCustomizer;
@Autowired
@Nullable
@Autowired(required = false)
private OPAInputValidator opaInputValidator;

public OPAAuthorizationManager() {
Expand All @@ -101,39 +110,39 @@ public OPAAuthorizationManager() {

/**
* @see OPAAuthorizationManager#OPAAuthorizationManager(OPAClient, String,
* ContextDataProvider)
* ContextDataProvider)
*/
public OPAAuthorizationManager(OPAClient opaClient) {
this(opaClient, null, null);
}

/**
* @see OPAAuthorizationManager#OPAAuthorizationManager(OPAClient, String,
* ContextDataProvider)
* ContextDataProvider)
*/
public OPAAuthorizationManager(String opaPath) {
this(null, opaPath, null);
}

/**
* @see OPAAuthorizationManager#OPAAuthorizationManager(OPAClient, String,
* ContextDataProvider)
* ContextDataProvider)
*/
public OPAAuthorizationManager(OPAClient opaClient, String opaPath) {
this(opaClient, opaPath, null);
}

/**
* @see OPAAuthorizationManager#OPAAuthorizationManager(OPAClient, String,
* ContextDataProvider)
* ContextDataProvider)
*/
public OPAAuthorizationManager(OPAClient opaClient, ContextDataProvider contextDataProvider) {
this(opaClient, null, contextDataProvider);
}

/**
* @see OPAAuthorizationManager#OPAAuthorizationManager(OPAClient, String,
* ContextDataProvider)
* ContextDataProvider)
*/
public OPAAuthorizationManager(String opaPath, ContextDataProvider contextDataProvider) {
this(null, opaPath, contextDataProvider);
Expand All @@ -152,7 +161,9 @@ public OPAAuthorizationManager(String opaPath, ContextDataProvider contextDataPr
* @param contextDataProvider helps providing additional context data in
* {@code input.context.data}.
*/
public OPAAuthorizationManager(OPAClient opaClient, String opaPath, ContextDataProvider contextDataProvider) {
public OPAAuthorizationManager(@Nullable OPAClient opaClient,
@Nullable String opaPath,
@Nullable ContextDataProvider contextDataProvider) {
opaProperties = new OPAProperties();
this.opaClient = opaClient != null ? opaClient : defaultOPAClient();
this.opaPath = opaPath;
Expand All @@ -169,7 +180,11 @@ private static OPAClient defaultOPAClient() {
}

@Override
public void verify(Supplier<Authentication> authenticationSupplier, RequestAuthorizationContext object) {
public void verify(Supplier<? extends Authentication> authenticationSupplier,
@Nullable RequestAuthorizationContext object) {
if (object == null) {
throw new OPAAccessDeniedException("no request context");
}
OPAResponse opaResponse = opaRequest(authenticationSupplier, object);
if (opaResponse == null) {
throw new OPAAccessDeniedException("null response from policy");
Expand All @@ -189,8 +204,12 @@ public void verify(Supplier<Authentication> authenticationSupplier, RequestAutho
}

@Override
public AuthorizationDecision check(Supplier<Authentication> authenticationSupplier,
RequestAuthorizationContext object) {
public AuthorizationDecision authorize(Supplier<? extends Authentication> authenticationSupplier,
@Nullable RequestAuthorizationContext object) {
if (object == null) {
LOGGER.trace("No request context, default-denying access");
return new OPAAuthorizationDecision(false, null);
}
OPAResponse opaResponse = opaRequest(authenticationSupplier, object);
if (opaResponse == null) {
LOGGER.trace("OPA provided a null response, default-denying access");
Expand All @@ -207,7 +226,8 @@ public AuthorizationDecision check(Supplier<Authentication> authenticationSuppli
* directly rather than using this method, as it should not be needed during
* normal use.
*/
public OPAResponse opaRequest(Supplier<Authentication> authenticationSupplier, RequestAuthorizationContext object) {
public @Nullable OPAResponse opaRequest(Supplier<? extends Authentication> authenticationSupplier,
RequestAuthorizationContext object) {
Map<String, Object> input = makeRequestInput(authenticationSupplier, object);
LOGGER.trace("OPA input (request body) is: {}", input);
try {
Expand All @@ -232,8 +252,8 @@ public OPAResponse opaRequest(Supplier<Authentication> authenticationSupplier, R
}
}

private Map<String, Object> makeRequestInput(Supplier<Authentication> authenticationSupplier,
RequestAuthorizationContext object) {
private Map<String, Object> makeRequestInput(Supplier<? extends @Nullable Authentication> authenticationSupplier,
RequestAuthorizationContext object) {
HttpServletRequest request = object.getRequest();

Object subjectId = null;
Expand Down Expand Up @@ -312,7 +332,7 @@ private Map<String, Object> makeRequestInput(Supplier<Authentication> authentica
* calls
* {@code map}.put({@code key}, {@code nullableValue}).
*/
private void nullablePut(Map<String, Object> map, String key, Object nullableValue) {
private void nullablePut(Map<String, Object> map, String key, @Nullable Object nullableValue) {
Optional.ofNullable(nullableValue).ifPresent(value -> map.put(key, value));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.jspecify.annotations.Nullable;

/**
* This class models the data to be returned from an OPA Spring Boot SDK policy.
Expand All @@ -19,6 +20,7 @@
public class OPAResponse {

private boolean decision;
@Nullable
private OPAResponseContext context;

public boolean getDecision() {
Expand All @@ -30,7 +32,7 @@ public boolean getDecision() {
* is omitted (which the spec
* permits), then it returns null.
*/
public String getReasonForDecision(String searchKey) {
public @Nullable String getReasonForDecision(String searchKey) {
if (context == null) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authorization.AuthorizationDecision;

import java.util.ArrayList;
Expand All @@ -28,6 +29,7 @@ public class OPAResponseContext {
@JsonProperty("reason_admin")
private Map<String, String> reasonAdmin;
@JsonProperty("reason_user")
@Nullable
private Map<String, String> reasonUser;
/**
* The extra {@code data} field allows for the OPA policy to pass back arbitrary
Expand All @@ -44,7 +46,7 @@ public class OPAResponseContext {
* lexicographically first from the {@code reasonUser}. It will not consider
* data in the {@code reasonAdmin}.
*/
public String getReasonForDecision(String searchKey) {
public @Nullable String getReasonForDecision(String searchKey) {
if (reasonUser == null) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.github.open_policy_agent.opa.springboot.OPAResponse;
import lombok.Getter;
import org.jspecify.annotations.Nullable;
import org.springframework.security.access.AccessDeniedException;


Expand All @@ -11,6 +12,7 @@
@Getter
public class OPAAccessDeniedException extends AccessDeniedException {

@Nullable
private OPAResponse opaResponse;

public OPAAccessDeniedException(String message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

import io.github.open_policy_agent.opa.springboot.OPAResponse;
import lombok.Getter;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authorization.AuthorizationDecision;

/**
* Extends {@link AuthorizationDecision} which conveys {@link OPAResponse}.
*/
@Getter
public class OPAAuthorizationDecision extends AuthorizationDecision {
@Nullable
private final OPAResponse opaResponse;

public OPAAuthorizationDecision(boolean granted, OPAResponse opaResponse) {
public OPAAuthorizationDecision(boolean granted, @Nullable OPAResponse opaResponse) {
super(granted);
this.opaResponse = opaResponse;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package io.github.open_policy_agent.opa.springboot.authorization;

import io.github.open_policy_agent.opa.springboot.autoconfigure.OPAProperties;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
import org.springframework.security.authorization.event.AuthorizationGrantedEvent;
import org.springframework.security.core.Authentication;
Expand All @@ -26,8 +27,8 @@
* </ul>
*
* @see <a href=
* "https://docs.spring.io/spring-security/reference/servlet/authorization/events.html">
* Authorization Events</a>
* "https://docs.spring.io/spring-security/reference/servlet/authorization/events.html">
* Authorization Events</a>
*/
public class OPAAuthorizationEventPublisher implements AuthorizationEventPublisher {
private static final Logger LOGGER = LoggerFactory.getLogger(OPAAuthorizationEventPublisher.class);
Expand All @@ -44,7 +45,7 @@ public OPAAuthorizationEventPublisher(ApplicationEventPublisher publisher, OPAPr

@Override
public <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object,
AuthorizationDecision decision) {
@Nullable AuthorizationResult decision) {
if (!(decision instanceof OPAAuthorizationDecision)) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Support classes for integration OPA decisions with Spring authorization framework.
*/
@NullMarked
package io.github.open_policy_agent.opa.springboot.authorization;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Auto-configuration for OPA.
*/
@NullMarked
package io.github.open_policy_agent.opa.springboot.autoconfigure;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.open_policy_agent.opa.springboot.input;

import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;

Expand Down Expand Up @@ -30,6 +31,6 @@ public interface OPAInputActionCustomizer {
* <li>{@value InputConstants#ACTION_NAME}</li>
* </ul>
*/
Map<String, Object> customize(Authentication authentication,
Map<String, Object> customize(@Nullable Authentication authentication,
RequestAuthorizationContext requestAuthorizationContext, Map<String, Object> action);
}
Loading
Loading