Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## v0.0.9 (unreleased)

* Add `OPAAutoConfiguration` to auto-configure `OPAClient` and `OPAAuthorizationManager` beans. When another
`OPAClient` and `OPAAuthorizationManager` is defined in Spring context, auto-configured beans will not be created.
* Add `OPAProperties` to organize properties, provide default values, and externalize them (modify them through
properties files, yaml files, environment variables, system properties, etc.).

## v0.0.8

Expand Down
56 changes: 32 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ You can use the Styra OPA Spring Boot SDK to connect [Open Policy Agent](https:/

## SDK Installation

This package is published on Maven Central as [`com.styra.opa/springboot`](https://central.sonatype.com/artifact/com.styra.opa/springboot). The Maven Central page includes up-to-date instructions to add it as a dependency to your Java project, tailored to a variety of build systems including Maven and Gradle.
This package is published on Maven Central as [`com.styra.opa:springboot`](https://central.sonatype.com/artifact/com.styra.opa/springboot). The Maven Central page includes up-to-date instructions to add it as a dependency to your Java project, tailored to a variety of build systems including Maven and Gradle.

If you wish to build from source and publish the SDK artifact to your local Maven repository (on your filesystem) then use the following command (after cloning the git repo locally):

Expand All @@ -23,11 +23,13 @@ On Linux/MacOS:
On Windows:

```shell
gradlew.bat publishToMavenLocal -Pskip.signing
gradlew.bat publishToMavenLocal -"Pskip.signing"
```

## SDK Example Usage (high-level)

Using `OPAAuthorizationManager`, HTTP requests could be authorized:

```java
// ...

Expand All @@ -39,38 +41,44 @@ import com.styra.opa.OPAClient;
public class SecurityConfig {

@Autowired
TicketRepository ticketRepository;

@Autowired
TenantRepository tenantRepository;

@Autowired
CustomerRepository customerRepository;
OPAAuthorizationManager opaAuthorizationManager;

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

String opaURL = "http://localhost:8181";
String opaURLEnv = System.getenv("OPA_URL");
if (opaURLEnv != null) {
opaURL = opaURLEnv;
}
OPAClient opa = new OPAClient(opaURL);

AuthorizationManager<RequestAuthorizationContext> am = new OPAAuthorizationManager(opa, "tickets/spring/main");

http.authorizeHttpRequests(authorize -> authorize.anyRequest().access(am));

http.authorizeHttpRequests(authorize -> authorize.anyRequest().access(opaAuthorizationManager));
// Other security configs
return http.build();
}

}

```
Auto-configuration will be done using `OPAAutoConfiguration`. If any customization would be needed, custom `OPAClient`
or `OPAAuthorizationManager` beans could be defined.

Configuration properties are defined in `OPAProperties` and can be set
[externally](https://docs.spring.io/spring-boot/reference/features/external-config.html), e.g. via
`application.properties`, `application.yaml`, system properties, or environment variables.

Example `application.yaml` to modify properties:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

```yaml
opa:
url: http://localhost:8182 # OPA server URL. Default is "http://localhost:8181".
path: foo/bar # Policy path in OPA. Default is null.
request:
resource:
type: stomp_endpoint # Type of the request's resource. Default is "endpoint".
context:
type: websocket # Type of the request's context. Default is "http".
subject:
type: oauth2_resource_owner # Type of the request's subject. Default is "java_authentication".
response:
context:
reason-key: de # Key to search for decision reasons in the response. Default is "en".
```

## Policy Input/Output Schema

Documentation for the required input and output schema of policies used by the OPA Spring Boot SDK can be found [here](https://docs.styra.com/sdk/springboot/reference/input-output-schema)
Documentation for the required input and output schema of policies used by the OPA Spring Boot SDK can be found [here](https://docs.styra.com/sdk/springboot/reference/input-output-schema).

## Build Instructions

Expand All @@ -80,7 +88,7 @@ Documentation for the required input and output schema of policies used by the O

**To run the unit tests**, you can use `./gradlew test`.

**To run the linter**, you can use `./gradlew lint`
**To run the linter**, you can use `./gradlew lint`.

## Community

Expand Down
8 changes: 5 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ repositories {
}

javadoc {
options.addBooleanOption("Xdoclint:-missing", true)
options.links += [
"https://styrainc.github.io/opa-java/javadoc/",
"https://docs.spring.io/spring-security/site/docs/current/api/",
"https://docs.spring.io/spring-boot/api/java/",
]
}

Expand Down Expand Up @@ -96,7 +98,7 @@ task lint {

test {
useJUnitPlatform()
exclude 'com/styra/opa/springboot/properties/ModifiedSystemEnvOPAPropertiesTest.class'
exclude 'com/styra/opa/springboot/autoconfigure/properties/ModifiedSystemEnvOPAPropertiesTest.class'
testLogging {
// uncomment for more verbose output during development
//events "passed", "skipped", "failed", "standard_out", "standard_error"
Expand All @@ -105,10 +107,10 @@ test {
tasks.register("testModifiedSystemEnvProperties", Test) {
useJUnitPlatform()
group = "verification"
include 'com/styra/opa/springboot/properties/ModifiedSystemEnvOPAPropertiesTest.class'
include 'com/styra/opa/springboot/autoconfigure/properties/ModifiedSystemEnvOPAPropertiesTest.class'
doFirst {
systemProperty 'opa.url', 'http://localhost:8183'
environment 'OPA_PATH', 'tickets/main2'
environment 'OPA_PATH', 'foo/bar2'
}
doLast {
systemProperties.remove('opa.url')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.core.type.TypeReference;
import com.styra.opa.OPAClient;
import com.styra.opa.OPAException;
import com.styra.opa.springboot.autoconfigure.OPAProperties;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -141,7 +142,7 @@ private static OPAClient defaultOPAClient(OPAProperties opaProperties) {
}

public String getReasonKey() {
return opaProperties.getReasonKey();
return opaProperties.getResponse().getContext().getReasonKey();
}

/**
Expand All @@ -153,7 +154,7 @@ public String getReasonKey() {
* @param newReasonKey
*/
public void setReasonKey(String newReasonKey) {
opaProperties.setReasonKey(newReasonKey);
opaProperties.getResponse().getContext().setReasonKey(newReasonKey);
}

/**
Expand Down Expand Up @@ -300,7 +301,7 @@ public void verify(
}

boolean allow = resp.getDecision();
String reason = resp.getReasonForDecision(opaProperties.getReasonKey());
String reason = resp.getReasonForDecision(opaProperties.getResponse().getContext().getReasonKey());
if (reason == null) {
reason = "access denied by policy";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.styra.opa.springboot.autoconfigure;

import com.styra.opa.OPAClient;
import com.styra.opa.springboot.OPAAuthorizationManager;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
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.context.annotation.Bean;

/**
* {@link EnableAutoConfiguration Auto-configuration} for OPA authorization support.
*/
@AutoConfiguration
@EnableConfigurationProperties(OPAProperties.class)
@AutoConfigureBefore(SecurityAutoConfiguration.class)
@ConditionalOnClass(OPAClient.class)
public class OPAAutoConfiguration {

/**
* Create an {@link OPAClient} bean using {@link OPAProperties#getUrl()}.
*/
@Bean
@ConditionalOnMissingBean(OPAClient.class)
public OPAClient opaClient(OPAProperties opaProperties) {
return new OPAClient(opaProperties.getUrl());
}

/**
* Create an {@link OPAAuthorizationManager} bean using {@link OPAClient} bean and {@link OPAProperties#getPath()}.
*/
@Bean
@ConditionalOnMissingBean(OPAAuthorizationManager.class)
public OPAAuthorizationManager opaAuthorizationManager(OPAClient opaClient, OPAProperties opaProperties) {
return new OPAAuthorizationManager(opaClient, opaProperties.getPath());
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
package com.styra.opa.springboot;
package com.styra.opa.springboot.autoconfigure;

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

/**
* Configuration properties for OPA authorization support.
*/
@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";

/**
* URL of the OPA server. Default is {@value DEFAULT_URL}.
*/
private String url = DEFAULT_URL;
/**
* Policy path in OPA. Default is null.
*/
private String path;
private String reasonKey = DEFAULT_REASON_KEY;
private Request request = new Request();
private Response response = new Response();

@Data
@NoArgsConstructor
Expand All @@ -33,6 +41,9 @@ public static class Request {
public static class Resource {
public static final String DEFAULT_TYPE = "endpoint";

/**
* Type of the resource. Default is {@value DEFAULT_TYPE}.
*/
private String type = DEFAULT_TYPE;
}

Expand All @@ -42,6 +53,9 @@ public static class Resource {
public static class Context {
public static final String DEFAULT_TYPE = "http";

/**
* Type of the context. Default is {@value DEFAULT_TYPE}.
*/
private String type = DEFAULT_TYPE;
}

Expand All @@ -51,7 +65,32 @@ public static class Context {
public static class Subject {
public static final String DEFAULT_TYPE = "java_authentication";

/**
* Type of the subject. Default is {@value DEFAULT_TYPE}.
*/
private String type = DEFAULT_TYPE;
}
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Response {

private Context context = new Context();

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Context {
public static final String DEFAULT_REASON_KEY = "en";

/**
* Key to search for decision reasons in the response. Default is {@value DEFAULT_REASON_KEY}.
*
* @see <a href="https://openid.github.io/authzen/#reason-field">AuthZEN Reason Field</a>
*/
private String reasonKey = DEFAULT_REASON_KEY;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.styra.opa.springboot.autoconfigure.OPAAutoConfiguration
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.styra.opa.springboot.autoconfigure;

import com.styra.opa.OPAClient;
import com.styra.opa.springboot.OPAAuthorizationManager;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import java.util.Map;

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

@SpringBootTest(classes = OPAAutoConfiguration.class)
public class OPAAutoConfigurationTest {

@Nested
public class DefaultOPAAutoConfigurationTest {

@Autowired(required = false)
private OPAProperties opaProperties;
@Autowired(required = false)
private OPAClient opaClient;
@Autowired(required = false)
private OPAAuthorizationManager opaAuthorizationManager;

@Test
public void test() {
assertNotNull(opaProperties);
assertNotNull(opaClient);
assertNotNull(opaAuthorizationManager);
}
}

@Import(OPAAutoConfigurationTestWithCustomOPAClient.CustomOPAClientConfiguration.class)
@Nested
public class OPAAutoConfigurationTestWithCustomOPAClient {

@Autowired(required = false)
private Map<String, OPAClient> opaClients;

@Test
public void test() {
assertNotNull(opaClients);
assertEquals(1, opaClients.size());
assertNotNull(opaClients.get("customOPAClient"));
}

@Configuration
public static class CustomOPAClientConfiguration {

@Bean
public OPAClient customOPAClient() {
return new OPAClient("http://localhost:8182");
}
}
}

@Import(OPAAutoConfigurationTestWithCustomOPAAuthorizationManager.CustomOPAAuthorizationManagerConfiguration.class)
@Nested
public class OPAAutoConfigurationTestWithCustomOPAAuthorizationManager {

@Autowired(required = false)
private Map<String, OPAAuthorizationManager> opaAuthorizationManagers;

@Test
public void test() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it convention to name the tests test, or could we use something more descriptive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first I used simple test name since there was 1 test method per class, but as you suggested better to use more descriptive names. Just updated. :)

assertNotNull(opaAuthorizationManagers);
assertEquals(1, opaAuthorizationManagers.size());
assertNotNull(opaAuthorizationManagers.get("customOPAAuthorizationManager"));
}

@Configuration
public static class CustomOPAAuthorizationManagerConfiguration {

@Bean
public OPAAuthorizationManager customOPAAuthorizationManager() {
return new OPAAuthorizationManager("foo/bar2");
}
}
}
}
Loading
Loading