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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ gradle-app.setting
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath

# IntelliJ IDEA
.idea/**
25 changes: 18 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,10 @@ dependencies {
implementation 'org.springframework:spring-context'

testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'org.junit.jupiter:junit-jupiter-api:+'

testImplementation 'org.testcontainers:testcontainers-bom:+'
testImplementation 'org.testcontainers:testcontainers:+'
testImplementation 'org.testcontainers:junit-jupiter:+'

testImplementation 'org.mockito:mockito-core:+'
testImplementation 'org.mockito:mockito-junit-jupiter:+'
testImplementation 'org.testcontainers:testcontainers-bom:1.19.8'
testImplementation 'org.testcontainers:testcontainers:1.19.8'
testImplementation 'org.testcontainers:junit-jupiter:1.19.8'

api group: 'com.styra', name: 'opa', version: '1.8.0'

Expand Down Expand Up @@ -100,11 +96,26 @@ task lint {

test {
useJUnitPlatform()
exclude 'com/styra/opa/springboot/properties/ModifiedSystemEnvOPAPropertiesTest.class'
testLogging {
// uncomment for more verbose output during development
//events "passed", "skipped", "failed", "standard_out", "standard_error"
}
}
tasks.register("testModifiedSystemEnvProperties", Test) {
useJUnitPlatform()
group = "verification"
include 'com/styra/opa/springboot/properties/ModifiedSystemEnvOPAPropertiesTest.class'
doFirst {
systemProperty 'opa.url', 'http://localhost:8183'
environment 'OPA_PATH', 'tickets/main2'
}
doLast {
systemProperties.remove('opa.url')
environment.remove('OPA_PATH')
}
}
test.finalizedBy(testModifiedSystemEnvProperties)

gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
Expand Down
65 changes: 23 additions & 42 deletions src/main/java/com/styra/opa/springboot/OPAAuthorizationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,29 +38,20 @@ public class OPAAuthorizationManager
OPAAuthorizationManager.class
);

private static final String SubjectType = "java_authentication";
private static final String RequestResourceType = "endpoint";
private static final String RequestContextType = "http";

// If opaPath is null, then we assume the user wants to use the default path.
private String opaPath;

private String reasonKey;

private ContextDataProvider ctxProvider;

private OPAClient opa;

private OPAProperties opaProperties;

/**
* The authorization manager will internally instantiate an OPAClient
* instance with default settings. The OPA URL may be overridden using the
* OPA_URL environment variable. All OPA requests will be sent to the
* default path defined by the OPA configuration.
*/
public OPAAuthorizationManager() {
this.opa = defaultOPAClient();
this.opaPath = null;
this.reasonKey = "en";
this(null, null, null);
}

/**
Expand All @@ -71,9 +62,7 @@ public OPAAuthorizationManager() {
* @param opa
*/
public OPAAuthorizationManager(OPAClient opa) {
this.opa = opa;
this.opaPath = null;
this.reasonKey = "en";
this(opa, null, null);
}

/**
Expand All @@ -86,9 +75,7 @@ public OPAAuthorizationManager(OPAClient opa) {
* @param newOpaPath
*/
public OPAAuthorizationManager(OPAClient opa, String newOpaPath) {
this.opa = opa;
this.opaPath = newOpaPath;
this.reasonKey = "en";
this(opa, newOpaPath, null);
}

/**
Expand All @@ -99,9 +86,7 @@ public OPAAuthorizationManager(OPAClient opa, String newOpaPath) {
* @param newOpaPath
*/
public OPAAuthorizationManager(String newOpaPath) {
this.opa = defaultOPAClient();
this.opaPath = newOpaPath;
this.reasonKey = "en";
this(null, newOpaPath, null);
}

/**
Expand All @@ -114,9 +99,7 @@ public OPAAuthorizationManager(String newOpaPath) {
* @param newProvider
*/
public OPAAuthorizationManager(OPAClient opa, ContextDataProvider newProvider) {
this.opa = opa;
this.ctxProvider = newProvider;
this.reasonKey = "en";
this(opa, null, newProvider);
}

/**
Expand All @@ -129,10 +112,11 @@ public OPAAuthorizationManager(OPAClient opa, ContextDataProvider newProvider) {
* @param newProvider
*/
public OPAAuthorizationManager(OPAClient opa, String newOpaPath, ContextDataProvider newProvider) {
this.opa = opa;
this.opaPath = newOpaPath;
// If newOpaPath is null, then we assume the user wants to use the default path.
opaProperties = new OPAProperties();
opaProperties.setPath(newOpaPath);
this.opa = opa != null ? opa : defaultOPAClient(opaProperties);
this.ctxProvider = newProvider;
this.reasonKey = "en";
}

/**
Expand All @@ -143,14 +127,11 @@ public OPAAuthorizationManager(OPAClient opa, String newOpaPath, ContextDataProv
* @param newProvider
*/
public OPAAuthorizationManager(String newOpaPath, ContextDataProvider newProvider) {
this.opa = defaultOPAClient();
this.opaPath = newOpaPath;
this.ctxProvider = newProvider;
this.reasonKey = "en";
this(null, newOpaPath, newProvider);
}

private static OPAClient defaultOPAClient() {
String opaURL = "http://localhost:8181";
private static OPAClient defaultOPAClient(OPAProperties opaProperties) {
String opaURL = opaProperties.getUrl();
String opaURLEnv = System.getenv("OPA_URL");
if (opaURLEnv != null) {
opaURL = opaURLEnv;
Expand All @@ -160,7 +141,7 @@ private static OPAClient defaultOPAClient() {
}

public String getReasonKey() {
return this.reasonKey;
return opaProperties.getReasonKey();
}

/**
Expand All @@ -172,7 +153,7 @@ public String getReasonKey() {
* @param newReasonKey
*/
public void setReasonKey(String newReasonKey) {
this.reasonKey = newReasonKey;
opaProperties.setReasonKey(newReasonKey);
}

/**
Expand Down Expand Up @@ -222,13 +203,13 @@ private Map<String, Object> makeRequestInput(
Integer contextRemotePort = request.getRemotePort();

Map<String, Object> ctx = new HashMap<String, Object>();
nullablePut(ctx, "type", RequestContextType);
nullablePut(ctx, "type", opaProperties.getRequest().getContext().getType());
nullablePut(ctx, "host", contextRemoteHost);
nullablePut(ctx, "ip", contextRemoteAddr);
nullablePut(ctx, "port", contextRemotePort);

Map<String, Object> subjectInfo = new HashMap<String, Object>();
nullablePut(subjectInfo, "type", SubjectType);
nullablePut(subjectInfo, "type", opaProperties.getRequest().getSubject().getType());
nullablePut(subjectInfo, "id", subjectId);
nullablePut(subjectInfo, "details", subjectDetails);
nullablePut(subjectInfo, "authorities", subjectAuthorities);
Expand All @@ -243,7 +224,7 @@ private Map<String, Object> makeRequestInput(
entry(
"resource",
java.util.Map.ofEntries(
entry("type", RequestResourceType),
entry("type", opaProperties.getRequest().getResource().getType()),
entry("id", resourceId)
)
),
Expand Down Expand Up @@ -276,10 +257,10 @@ public OPAResponse opaRequest(
logger.trace("OPA input for request: {}", iMap);
OPAResponse resp = null;
try {
if (this.opaPath != null) {
logger.trace("OPA path is {}", this.opaPath);
if (opaProperties.getPath() != null) {
logger.trace("OPA path is {}", opaProperties.getPath());
resp = opa.evaluate(
this.opaPath,
opaProperties.getPath(),
iMap,
new TypeReference<OPAResponse>() {}
);
Expand Down Expand Up @@ -319,7 +300,7 @@ public void verify(
}

boolean allow = resp.getDecision();
String reason = resp.getReasonForDecision(this.reasonKey);
String reason = resp.getReasonForDecision(opaProperties.getReasonKey());
if (reason == null) {
reason = "access denied by policy";
}
Expand Down
57 changes: 57 additions & 0 deletions src/main/java/com/styra/opa/springboot/OPAProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.styra.opa.springboot;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "opa")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OPAProperties {
public static final String DEFAULT_URL = "http://localhost:8181";
public static final String DEFAULT_REASON_KEY = "en";

private String url = DEFAULT_URL;
private String path;
private String reasonKey = DEFAULT_REASON_KEY;
private Request request = new Request();

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Request {

private Resource resource = new Resource();
private Context context = new Context();
private Subject subject = new Subject();

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Resource {
public static final String DEFAULT_TYPE = "endpoint";

private String type = DEFAULT_TYPE;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Context {
public static final String DEFAULT_TYPE = "http";

private String type = DEFAULT_TYPE;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Subject {
public static final String DEFAULT_TYPE = "java_authentication";

private String type = DEFAULT_TYPE;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
Expand All @@ -19,6 +20,7 @@
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
Expand Down Expand Up @@ -93,7 +95,8 @@ class OPAAuthorizationManagerTest {
)
.withExposedPorts(opaPort, altPort)
.withFileSystemBind("./testdata/simple", "/policy", BindMode.READ_ONLY)
.withCommand("run -s --authentication=token --authorization=basic --bundle /policy");
.withCommand("run -s --authentication=token --authorization=basic --bundle /policy --addr=0.0.0.0:" + opaPort)
.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(OPAAuthorizationManagerTest.class)));
//CHECKSTYLE:ON

private Authentication createMockAuthentication() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.styra.opa.springboot.properties;

import com.styra.opa.springboot.OPAProperties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

@EnableConfigurationProperties(value = OPAProperties.class)
@ExtendWith(SpringExtension.class)
public class DefaultOPAPropertiesTest {

@Autowired
private OPAProperties opaProperties;

@Test
public void test() {
assertEquals(OPAProperties.DEFAULT_URL, opaProperties.getUrl());
assertNull(opaProperties.getPath());
assertEquals(OPAProperties.DEFAULT_REASON_KEY, opaProperties.getReasonKey());
assertNotNull(opaProperties.getRequest());
assertEquals(OPAProperties.Request.Resource.DEFAULT_TYPE, opaProperties.getRequest().getResource().getType());
assertNotNull(opaProperties.getRequest().getContext());
assertEquals(OPAProperties.Request.Context.DEFAULT_TYPE, opaProperties.getRequest().getContext().getType());
assertEquals(OPAProperties.Request.Subject.DEFAULT_TYPE,
opaProperties.getRequest().getSubject().getType());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.styra.opa.springboot.properties;

import com.styra.opa.springboot.OPAProperties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;

@TestPropertySource(properties = {
"opa.url=http://localhost:8182",
"opa.path=tickets/main",
"opa.reason-key=de",
"opa.request.resource.type=stomp_endpoint",
"opa.request.context.type=websocket",
"opa.request.subject.type=oauth2_resource_owner"
})
@EnableConfigurationProperties(value = OPAProperties.class)
@ExtendWith(SpringExtension.class)
public class ModifiedOPAPropertiesTest {

@Autowired
private OPAProperties opaProperties;

@Test
public void test() {
assertEquals("http://localhost:8182", opaProperties.getUrl());
assertEquals("tickets/main", opaProperties.getPath());
assertEquals("de", opaProperties.getReasonKey());
assertEquals("stomp_endpoint", opaProperties.getRequest().getResource().getType());
assertEquals("websocket", opaProperties.getRequest().getContext().getType());
assertEquals("oauth2_resource_owner", opaProperties.getRequest().getSubject().getType());
}
}

Loading
Loading