From 208703f48a388549f12c782dbe97486ef40c4ea6 Mon Sep 17 00:00:00 2001
From: Marco Massenzio <1153951+massenz@users.noreply.github.com>
Date: Fri, 18 Nov 2022 20:53:22 -0800
Subject: [PATCH] [#36] Adding HTTP Headers to OPA Auth request (#45)
* [#36] Adding Headers to OPA request
---
docker-compose.yml | 40 ++++++++++
.../JwtSecurityConfiguration.java | 1 -
.../KeyMaterialConfiguration.java | 3 -
.../configuration/OpaServerConfiguration.java | 14 +++-
.../configuration/OpaServerProperties.java | 27 ++++++-
.../OpaReactiveAuthorizationManager.java | 30 +++++++-
.../TokenBasedAuthorizationRequest.java | 4 +-
.../aws/AwsSecretsManagerResolver.java | 1 -
.../OpaServerPropertiesTest.java | 25 ++++--
.../OpaReactiveAuthorizationManagerTest.java | 76 +++++++++++++------
.../TokenBasedAuthorizationRequestTest.java | 10 ++-
.../src/test/resources/application-test.yaml | 4 +-
.../{jwt_auth.rego => test_policy.rego} | 12 ++-
run-example.sh | 15 +---
.../opademo/ExampleConfiguration.java | 1 +
webapp-example/src/main/rego/jwt_auth.rego | 38 +++++-----
.../src/main/resources/application.yaml | 21 ++++-
17 files changed, 235 insertions(+), 87 deletions(-)
create mode 100644 docker-compose.yml
rename jwt-opa/src/test/resources/{jwt_auth.rego => test_policy.rego} (86%)
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..effc199
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,40 @@
+# Copyright AlertAvert.com (c) 2022. All rights reserved.
+
+version: '3.2'
+
+services:
+ opa:
+ container_name: opa
+ hostname: opa
+ image: openpolicyagent/opa:0.42.2
+ command: run --server --addr :8181
+ ports:
+ - "8181:8181"
+ networks:
+ - backend
+
+ mongo:
+ container_name: "mongo"
+ image: "mongo:4"
+ hostname: mongo
+ ports:
+ - "27017:27017"
+ networks:
+ - backend
+ volumes:
+ - mongo_data:/data
+
+### INFRASTRUCTURE
+
+volumes:
+ mongo_data:
+
+# To connect to the servers in this stack, from a container run
+# via Docker, use `--network docker_backend`.
+# The hosts listed above will then be reachable at the given names,
+# on whatever ports are exposed.
+networks:
+ backend:
+ ipam:
+ config:
+ - subnet: 172.1.2.0/24
diff --git a/jwt-opa/src/main/java/com/alertavert/opa/configuration/JwtSecurityConfiguration.java b/jwt-opa/src/main/java/com/alertavert/opa/configuration/JwtSecurityConfiguration.java
index 4d6952d..0ffadd5 100644
--- a/jwt-opa/src/main/java/com/alertavert/opa/configuration/JwtSecurityConfiguration.java
+++ b/jwt-opa/src/main/java/com/alertavert/opa/configuration/JwtSecurityConfiguration.java
@@ -53,7 +53,6 @@ public class JwtSecurityConfiguration {
JwtAuthenticationWebFilter jwtAuthenticationWebFilter;
-
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
diff --git a/jwt-opa/src/main/java/com/alertavert/opa/configuration/KeyMaterialConfiguration.java b/jwt-opa/src/main/java/com/alertavert/opa/configuration/KeyMaterialConfiguration.java
index c9b7c80..607605a 100644
--- a/jwt-opa/src/main/java/com/alertavert/opa/configuration/KeyMaterialConfiguration.java
+++ b/jwt-opa/src/main/java/com/alertavert/opa/configuration/KeyMaterialConfiguration.java
@@ -18,7 +18,6 @@
package com.alertavert.opa.configuration;
-import com.alertavert.opa.security.crypto.KeypairFileReader;
import com.alertavert.opa.security.crypto.KeypairReader;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
@@ -29,14 +28,12 @@
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
-import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import static com.alertavert.opa.Constants.ELLIPTIC_CURVE;
import static com.alertavert.opa.Constants.PASSPHRASE;
-import static com.alertavert.opa.Constants.UNDEFINED_KEYPAIR;
@Slf4j
@Configuration
diff --git a/jwt-opa/src/main/java/com/alertavert/opa/configuration/OpaServerConfiguration.java b/jwt-opa/src/main/java/com/alertavert/opa/configuration/OpaServerConfiguration.java
index 3c87e6f..600cf11 100644
--- a/jwt-opa/src/main/java/com/alertavert/opa/configuration/OpaServerConfiguration.java
+++ b/jwt-opa/src/main/java/com/alertavert/opa/configuration/OpaServerConfiguration.java
@@ -26,6 +26,8 @@
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
+import java.util.List;
+
/**
*
OpaServerConfiguration
*
@@ -41,8 +43,7 @@ public class OpaServerConfiguration {
public OpaServerConfiguration(
OpaServerProperties opaServerProperties,
- RoutesConfiguration configuration
- ) {
+ RoutesConfiguration configuration) {
this.opaServerProperties = opaServerProperties;
this.configuration = configuration;
}
@@ -73,9 +74,14 @@ public WebClient client() {
.build();
}
+ @Bean
+ public List requiredHeaders() {
+ return opaServerProperties.getHeaders();
+ }
@Bean
- public OpaReactiveAuthorizationManager authorizationManager(WebClient client) {
- return new OpaReactiveAuthorizationManager(client, configuration);
+ public OpaReactiveAuthorizationManager authorizationManager() {
+ return new OpaReactiveAuthorizationManager(client(), configuration,
+ opaServerProperties.getHeaders());
}
}
diff --git a/jwt-opa/src/main/java/com/alertavert/opa/configuration/OpaServerProperties.java b/jwt-opa/src/main/java/com/alertavert/opa/configuration/OpaServerProperties.java
index 86e0976..8b01af7 100644
--- a/jwt-opa/src/main/java/com/alertavert/opa/configuration/OpaServerProperties.java
+++ b/jwt-opa/src/main/java/com/alertavert/opa/configuration/OpaServerProperties.java
@@ -20,7 +20,14 @@
import com.alertavert.opa.Constants;
import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.http.HttpHeaders;
+
+import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
/**
@@ -45,15 +52,33 @@
* @see OpaServerConfiguration
* @author M. Massenzio, 2020-11-22
*/
-@Data
+@Data @Slf4j
@ConfigurationProperties(prefix = "opa")
public class OpaServerProperties {
+ public static final Collection DEFAULT_HEADERS = List.of(
+ HttpHeaders.HOST,
+ HttpHeaders.USER_AGENT);
+
Boolean secure = false;
String server;
String policy;
String rule;
+ /**
+ * The list of headers to be sent to OPA to evaluate for authorization.
+ *
+ * {@link #DEFAULT_HEADERS default headers} are always sent
+ */
+ List headers = new ArrayList<>();
+
+
+ @PostConstruct
+ public void log() {
+ headers.addAll(DEFAULT_HEADERS);
+ log.info("Headers configured: headers = {}", headers);
+ }
+
protected String versionedApi(String api) {
return String.format("/%s/%s", Constants.OPA_VERSION, api);
}
diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java b/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java
index 93ecca9..80f4670 100644
--- a/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java
+++ b/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java
@@ -24,6 +24,7 @@
import com.alertavert.opa.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -38,7 +39,10 @@
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
+import javax.annotation.PostConstruct;
+import javax.sound.midi.Soundbank;
import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -61,16 +65,20 @@
*
* @author M. Massenzio, 2020-11-22
*/
-@Component
-@Slf4j
-@RequiredArgsConstructor
+@Slf4j @RequiredArgsConstructor
public class OpaReactiveAuthorizationManager
implements ReactiveAuthorizationManager {
private final WebClient client;
private final RoutesConfiguration configuration;
+ private final List requiredHeaders;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
+ @PostConstruct
+ private void info() {
+ log.info("Configured Headers, headers = {}", requiredHeaders);
+ }
+
/**
* Determines if access is granted for a specific request, given a user's credentials (API
* token).
@@ -134,12 +142,26 @@ private TokenBasedAuthorizationRequest makeRequestBody(
Object credentials,
ServerHttpRequest request
) {
+ Map authnHeaders = new HashMap<>();
+ HttpHeaders requestHeaders = request.getHeaders();
+ log.debug("Adding headers, request = {}, required = {}", requestHeaders,
+ requiredHeaders);
+ if (requestHeaders != null) {
+ requiredHeaders.forEach(key -> {
+ var value = requestHeaders.getFirst(key);
+ if (value != null) {
+ authnHeaders.put(key, value);
+ }
+ });
+ }
+
String token = Objects.requireNonNull(credentials).toString();
return TokenBasedAuthorizationRequest.builder()
.input(new TokenBasedAuthorizationRequest.AuthRequestBody(token,
new TokenBasedAuthorizationRequest.Resource(
request.getMethodValue(),
- request.getPath().toString()
+ request.getPath().toString(),
+ authnHeaders
)
)
)
diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/TokenBasedAuthorizationRequest.java b/jwt-opa/src/main/java/com/alertavert/opa/security/TokenBasedAuthorizationRequest.java
index 248c3f3..4541905 100644
--- a/jwt-opa/src/main/java/com/alertavert/opa/security/TokenBasedAuthorizationRequest.java
+++ b/jwt-opa/src/main/java/com/alertavert/opa/security/TokenBasedAuthorizationRequest.java
@@ -24,6 +24,8 @@
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
+import java.util.Map;
+
import static com.alertavert.opa.Constants.MAPPER;
import static com.alertavert.opa.Constants.MAX_TOKEN_LEN_LOG;
@@ -62,7 +64,7 @@
@Jacksonized
public class TokenBasedAuthorizationRequest {
- public record Resource(String method, String path) {
+ public record Resource(String method, String path, Map headers) {
}
public record AuthRequestBody(@JsonProperty("api_token") String token, Resource resource) {
diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolver.java b/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolver.java
index 912c21c..52c33ad 100644
--- a/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolver.java
+++ b/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolver.java
@@ -5,7 +5,6 @@
package com.alertavert.opa.security.aws;
-import com.alertavert.opa.ExcludeFromCoverageGenerated;
import com.alertavert.opa.security.crypto.KeyLoadException;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
diff --git a/jwt-opa/src/test/java/com/alertavert/opa/configuration/OpaServerPropertiesTest.java b/jwt-opa/src/test/java/com/alertavert/opa/configuration/OpaServerPropertiesTest.java
index 114a448..7c46e99 100644
--- a/jwt-opa/src/test/java/com/alertavert/opa/configuration/OpaServerPropertiesTest.java
+++ b/jwt-opa/src/test/java/com/alertavert/opa/configuration/OpaServerPropertiesTest.java
@@ -21,6 +21,8 @@
import com.alertavert.opa.AbstractTestBase;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
import static org.assertj.core.api.Assertions.assertThat;
@@ -29,27 +31,40 @@ class OpaServerPropertiesTest extends AbstractTestBase {
@Autowired
OpaServerProperties opaServerProperties;
+ @Value("${opa.policy}")
+ String policy;
+ @Value("${opa.rule}")
+ String rule;
+
@Test
public void endpoint() {
- assertThat(opaServerProperties.endpoint("foo"))
- .isEqualTo("http://localhost:8181/v1/foo/com.alertavert.policies");
+ String api = "foo";
+ assertThat(opaServerProperties.endpoint(api))
+ .isEqualTo(String.format("http://localhost:8181/v1/%s/%s", api, policy));
}
@Test
public void policy() {
assertThat(opaServerProperties.policyEndpoint())
- .isEqualTo("http://localhost:8181/v1/policies/com.alertavert.policies");
+ .isEqualTo(String.format("http://localhost:8181/v1/policies/%s", policy));
}
@Test
public void data() {
assertThat(opaServerProperties.dataEndpoint())
- .isEqualTo("http://localhost:8181/v1/data/com.alertavert.policies");
+ .isEqualTo(String.format("http://localhost:8181/v1/data/%s", policy));
}
@Test
public void authorizationEndpoint() {
assertThat(opaServerProperties.authorization())
- .isEqualTo("http://localhost:8181/v1/data/com.alertavert.policies/allow");
+ .isEqualTo(String.format("http://localhost:8181/v1/data/%s/%s",policy, rule));
+ }
+
+ @Test
+ public void testHeaders() {
+ assertThat(opaServerProperties.getHeaders()).containsExactlyInAnyOrder(
+ HttpHeaders.HOST, HttpHeaders.USER_AGENT, "x-test-header"
+ );
}
}
diff --git a/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java b/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java
index 031782c..03a4a36 100644
--- a/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java
+++ b/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java
@@ -27,6 +27,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -37,15 +38,15 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.util.FileCopyUtils;
-import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
-import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
+import java.util.List;
+import java.util.Map;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
@@ -70,7 +71,7 @@ class OpaReactiveAuthorizationManagerTest extends AbstractTestBaseWithOpaContain
@Autowired
String policyEndpoint;
- @Value("classpath:jwt_auth.rego")
+ @Value("classpath:test_policy.rego")
private Resource resource;
@BeforeEach
@@ -114,32 +115,42 @@ public void checkUnauthorizedFails() {
assertThat(auth.isAuthenticated()).isTrue();
AuthorizationContext context = getAuthorizationContext(HttpMethod.POST, "/users");
- AuthorizationDecision decision = opaReactiveAuthorizationManager.check(
- Mono.just(auth), context).block();
- assertThat(decision).isNotNull();
- assertThat(decision.isGranted()).isFalse();
+ assertThat(opaReactiveAuthorizationManager.check(
+ Mono.just(auth), context)
+ .map(AuthorizationDecision::isGranted)
+ .block()).isFalse();
}
@Test
public void checkUnauthenticatedFails() {
Authentication auth = new UsernamePasswordAuthenticationToken("bob", "pass");
- AuthorizationContext context = getAuthorizationContext(HttpMethod.GET, "/whocares");
- opaReactiveAuthorizationManager.check(Mono.just(auth), context)
- .doOnNext(decision -> assertThat(decision.isGranted()).isFalse())
- .block();
+ // As this endpoint is not mapped in `routes` (application-test.yaml) it expects by default
+ // a JWT Authorization Bearer token; finding a Username/Password credentials will deny access.
+ AuthorizationContext context = getAuthorizationContext(HttpMethod.GET, "/whocares");
+ assertThat(opaReactiveAuthorizationManager.check(Mono.just(auth), context).block()).isNull();
}
private AuthorizationContext getAuthorizationContext(
HttpMethod method, String path
+ ) {
+ return getAuthorizationContextWithHeaders(method, path, Map.of());
+ }
+
+ private AuthorizationContext getAuthorizationContextWithHeaders(
+ HttpMethod method, String path, Map headers
) {
ServerHttpRequest request = mock(ServerHttpRequest.class);
RequestPath requestPath = mock(RequestPath.class);
- when(request.getMethod()).thenReturn(method);
+ when(request.getMethodValue()).thenReturn(method.name());
when(request.getPath()).thenReturn(requestPath);
when(requestPath.toString()).thenReturn(path);
+ HttpHeaders mockHeaders = new HttpHeaders();
+ headers.forEach((k,v) -> mockHeaders.put(k, List.of(v)));
+ when(request.getHeaders()).thenReturn(mockHeaders);
+
ServerWebExchange exchange = mock(ServerWebExchange.class);
when(exchange.getRequest()).thenReturn(request);
@@ -151,13 +162,12 @@ private AuthorizationContext getAuthorizationContext(
@Test
public void authenticatedEndpointBypassesOpa() {
AuthorizationContext context = getAuthorizationContext(HttpMethod.GET, "/testauth");
- opaReactiveAuthorizationManager.check(
+ assertThat(opaReactiveAuthorizationManager.check(
factory.createAuthentication(
provider.createToken("alice", Lists.list("USER"))
), context)
.map(AuthorizationDecision::isGranted)
- .doOnNext(b -> assertThat(b).isTrue())
- .subscribe();
+ .block()).isTrue();
}
@Test
@@ -167,31 +177,49 @@ public void authenticatedEndpointMatches() {
// Here we test that an authenticated user gains access to them without needing authorization.
AuthorizationContext context = getAuthorizationContext(HttpMethod.GET, "/match/one/this");
- opaReactiveAuthorizationManager.check(
+ assertThat(opaReactiveAuthorizationManager.check(
factory.createAuthentication(
provider.createToken("alice", Lists.list("USER"))
), context)
.map(AuthorizationDecision::isGranted)
- .doOnNext(b -> assertThat(b).isTrue())
- .subscribe();
+ .block()).isTrue();
// This should NOT match
context = getAuthorizationContext(HttpMethod.GET, "/match/one/two/this.html");
- opaReactiveAuthorizationManager.check(
+ assertThat(opaReactiveAuthorizationManager.check(
factory.createAuthentication(
provider.createToken("alice", Lists.list("USER"))
), context)
.map(AuthorizationDecision::isGranted)
- .doOnNext(b -> assertThat(b).isFalse())
- .subscribe();
+ .block()).isFalse();
context = getAuthorizationContext(HttpMethod.GET, "/match/any/this/that.html");
- opaReactiveAuthorizationManager.check(
+ assertThat(opaReactiveAuthorizationManager.check(
factory.createAuthentication(
provider.createToken("alice", Lists.list("USER"))
), context)
.map(AuthorizationDecision::isGranted)
- .doOnNext(b -> assertThat(b).isTrue())
- .subscribe();
+ .block()).isTrue();
+ }
+
+ @Test
+ public void testHeaders() {
+ AuthorizationContext context = getAuthorizationContextWithHeaders(HttpMethod.GET, "/whatever",
+ Map.of("x-test-header", "test-value", HttpHeaders.USER_AGENT, "TestAgent"));
+ assertThat(opaReactiveAuthorizationManager.check(
+ factory.createAuthentication(
+ provider.createToken("alice", List.of("USER"))), context
+ )
+ .map(AuthorizationDecision::isGranted)
+ .block()).isTrue();
+
+ context = getAuthorizationContextWithHeaders(HttpMethod.GET, "/whatever",
+ Map.of("x-test-header", "wrong-value", HttpHeaders.USER_AGENT, "TestAgent"));
+ assertThat(opaReactiveAuthorizationManager.check(
+ factory.createAuthentication(
+ provider.createToken("alice", List.of("USER"))), context
+ )
+ .map(AuthorizationDecision::isGranted)
+ .block()).isFalse();
}
}
diff --git a/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java b/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java
index 92b2007..34ded7f 100644
--- a/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java
+++ b/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java
@@ -22,6 +22,8 @@
import com.alertavert.opa.security.TokenBasedAuthorizationRequest.AuthRequestBody;
import org.junit.jupiter.api.Test;
+import java.util.Map;
+
import static com.alertavert.opa.Constants.MAPPER;
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -30,10 +32,10 @@
class TokenBasedAuthorizationRequestTest {
@Test
- void serialize() throws JsonProcessingException {
+ void serialize() throws Exception {
TokenBasedAuthorizationRequest request = TokenBasedAuthorizationRequest.builder()
.input(new AuthRequestBody("tokenAAjwtDEF123456.anothertoken.yetanothertoken",
- new TokenBasedAuthorizationRequest.Resource("POST", "/foo/bar"))
+ new TokenBasedAuthorizationRequest.Resource("POST", "/foo/bar", Map.of()))
)
.build();
String json = MAPPER.writeValueAsString(request);
@@ -45,10 +47,10 @@ void serialize() throws JsonProcessingException {
}
@Test
- void obfuscatesJwt() throws JsonProcessingException {
+ void obfuscatesJwt() {
TokenBasedAuthorizationRequest request = TokenBasedAuthorizationRequest.builder()
.input(new AuthRequestBody("tokenAAjwtDEF123456.anothertoken.yetanothertoken",
- new TokenBasedAuthorizationRequest.Resource("POST", "/foo/bar"))
+ new TokenBasedAuthorizationRequest.Resource("POST", "/foo/bar", Map.of()))
)
.build();
String json = request.toString();
diff --git a/jwt-opa/src/test/resources/application-test.yaml b/jwt-opa/src/test/resources/application-test.yaml
index a7957ff..90d0605 100644
--- a/jwt-opa/src/test/resources/application-test.yaml
+++ b/jwt-opa/src/test/resources/application-test.yaml
@@ -14,9 +14,11 @@ tokens:
# http://localhost:8181/v1/data/com.alertavert.policies/allow
#
opa:
- policy: com.alertavert.policies
+ policy: com/alertavert/test
rule: allow
server: "localhost:8181"
+ headers:
+ - "x-test-header"
# Add an endpoint which won't trigger OPA Authorization
routes:
diff --git a/jwt-opa/src/test/resources/jwt_auth.rego b/jwt-opa/src/test/resources/test_policy.rego
similarity index 86%
rename from jwt-opa/src/test/resources/jwt_auth.rego
rename to jwt-opa/src/test/resources/test_policy.rego
index e62892a..dcc68f9 100644
--- a/jwt-opa/src/test/resources/jwt_auth.rego
+++ b/jwt-opa/src/test/resources/test_policy.rego
@@ -3,7 +3,7 @@
#
# This should be loaded to the OPA Policy Server via a PUT request to the /v1/policies endpoint.
-package com.alertavert.policies
+package com.alertavert.test
default allow = false
@@ -54,6 +54,10 @@ segments = split_path(input.resource.path)
entity := segments[0]
entity_id := segments[1]
+headers := h {
+ h = input.resource.headers
+}
+
# System accounts are allowed to make all API calls.
allow {
is_sysadmin
@@ -76,3 +80,9 @@ allow {
entity == "users"
input.resource.method == allowed_methods[_]
}
+
+# To test that a custom header is passed, and evaluated correctly
+allow {
+ headers["x-test-header"] == "test-value"
+ headers["User-Agent"] == "TestAgent"
+}
diff --git a/run-example.sh b/run-example.sh
index aa3ac17..859c052 100755
--- a/run-example.sh
+++ b/run-example.sh
@@ -25,21 +25,10 @@ OPA_PORT=8181
OPA_SERVER=http://localhost:${OPA_PORT}
POLICY_API=${OPA_SERVER}/v1/policies/userauth
-if [[ -z $(docker ps -q --filter name=mongo) ]]; then
- echo "Starting MongoDB container"
- docker run --rm -d -p 27017:27017 --name mongo mongo:4.0
-fi
-if [[ -z $(docker ps -q --filter name=opa) ]]; then
- echo "Starting Open Policy Agent (OPA) container"
- docker run --rm -d -p ${OPA_PORT}:${OPA_PORT} --name opa openpolicyagent/opa:0.25.2 run --server
-fi
-
-echo "curl -s ${POLICY_API}"
-curl -s ${POLICY_API}| jq .result.id
-
+docker-compose up -d
if [[ $(curl -s ${POLICY_API} | jq .result.id) != "userauth" ]]; then
echo "Uploading userauth Policy"
- curl -T "${WORKDIR}/jwt-opa/src/test/resources/jwt_auth.rego" -X PUT ${POLICY_API}
+ curl -T "${WORKDIR}/webapp-example/src/main/rego/jwt_auth.rego" -X PUT ${POLICY_API}
fi
export SPRING_PROFILES_ACTIVE="debug"
diff --git a/webapp-example/src/main/java/com/alertavert/opademo/ExampleConfiguration.java b/webapp-example/src/main/java/com/alertavert/opademo/ExampleConfiguration.java
index c223281..230bc3e 100644
--- a/webapp-example/src/main/java/com/alertavert/opademo/ExampleConfiguration.java
+++ b/webapp-example/src/main/java/com/alertavert/opademo/ExampleConfiguration.java
@@ -28,6 +28,7 @@
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
+import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;
diff --git a/webapp-example/src/main/rego/jwt_auth.rego b/webapp-example/src/main/rego/jwt_auth.rego
index 4928a1e..95748a7 100644
--- a/webapp-example/src/main/rego/jwt_auth.rego
+++ b/webapp-example/src/main/rego/jwt_auth.rego
@@ -1,46 +1,44 @@
# JWT Authorization Policy
-# Created M. Massenzio, 2020-11-22
+# Created M. Massenzio, 2022-11-15
#
# This should be loaded to the OPA Policy Server via a PUT request to the /v1/policies endpoint.
-package kapsules
-
-default allow = false
+package com.alertavert.userauth
+import future.keywords.in
# The JWT carries the username and roles, which will be used
# to authorize access to the endpoint (`input.resource.path`)
-# TODO: roles should be an array
-token := t {
+token := t[1] {
t := io.jwt.decode(input.api_token)
}
user := u {
- some i
- token[i].iss == "demo"
- u = token[i].sub
+ token.iss == "demo-issuer"
+ u = token.sub
}
-role := r {
- some i
- token[i].iss == "demo"
- r = token[i].role
+roles := r {
+ r = token.roles
}
-# System administrators can modify all entities
-is_sysadmin {
- role == "SYSTEM"
+# SYSTEM roles (typically, only bots) are allowed to make any
+# API calls, with whatever HTTP Method.
+is_system {
+ some i, "SYSTEM" in roles
}
# Admin users can only create/modify a subset
-# of entities
+# of entities, but is still a powerful role, ASSIGN WITH CARE.
is_admin {
- role == "ADMIN"
+ some i, "ADMIN" in roles
}
# Users can only modify self, and entities associated
# with the users themselves.
+# We assume that the user is valid if it could obtain a valid JWT and
+# has at least one Role.
is_user {
- role == "USER"
+ count(roles) > 0
}
split_path(path) = s {
@@ -55,7 +53,7 @@ entity_id := segments[1]
# System accounts are allowed to make all API calls.
allow {
- is_sysadmin
+ is_system
}
# User is allowed to view/modify self
diff --git a/webapp-example/src/main/resources/application.yaml b/webapp-example/src/main/resources/application.yaml
index 5028fc6..e5b58b9 100644
--- a/webapp-example/src/main/resources/application.yaml
+++ b/webapp-example/src/main/resources/application.yaml
@@ -14,16 +14,28 @@ management:
# This is used to build the Rule validation endpoint:
-# /v1/data/kapsules/allow
+# /v1/data/com/alertavert/userauth/allow
+#
+# Note the package in the Rego source (jwt_auth.rego) uses dotted notation:
+# package com.alertavert.userauth
+#
opa:
- policy: kapsules
+ policy: com/alertavert/userauth
rule: allow
+# List of request headers that will be submitted to OPA for policy evaluation.
+# `Host` and `User-Agent` are always included.
+#
+# NOTE: this is a quirk of Spring Boot: configure this with a comma-separated list
+# NOT a YAML array, as that will NOT work as expected.
+ headers: x-demoapp-auth, Accept-Encoding
+
db:
server: localhost
port: 27017
name: opa-demo-db
+ # Obviously, well, DON'T DO THIS for a real server.
admin:
username: admin
password: 8535b9c4-a
@@ -69,8 +81,8 @@ keys:
# Relative paths to a pair of private/public keys
# used to sign/verify the API Token (JWTs).
# See README.md for more information on how to generate them.
- priv: "private/ec-key.pem"
- pub: "private/ec-key-pub.pem"
+ priv: "../private/ec-key.pem"
+ pub: "../private/ec-key-pub.pem"
logging:
level:
@@ -93,6 +105,7 @@ management:
opa:
server: "localhost:8181"
+
routes:
# These endpoints will be accessible without authentication
allowed: