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: