Skip to content

Commit

Permalink
Merge "add KeycloakPermissionFilter"
Browse files Browse the repository at this point in the history
  • Loading branch information
Fiete Ostkamp authored and Gerrit Code Review committed Dec 17, 2024
2 parents d4f1e51 + c5fab8a commit 11fa772
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 92 deletions.
21 changes: 0 additions & 21 deletions app/src/main/resources/application-access-control.yml

This file was deleted.

4 changes: 3 additions & 1 deletion app/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ bff:
preferences-url: ${PREFERENCES_URL}
history-url: ${HISTORY_URL}
keycloak-url: ${KEYCLOAK_URL}
keycloak-client-id: ${KEYCLOAK_CLIENT_ID}
endpoints:
unauthenticated: /api-docs.html, /api.yaml, /webjars/**, /actuator/**

rbac:
endpoints-excluded: /actuator/**, **/actuator/**, */actuator/**, /**/actuator/**, /*/actuator/**
endpoints-excluded: ${RBAC_EXCLUDED_ENDPOINTS}:-/api-docs.html, /api.yaml, /webjars/**, /actuator/**, /users**, /roles**, /preferences**, /actions**}

14 changes: 14 additions & 0 deletions app/src/test/java/org/onap/portalng/bff/BaseIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,20 @@ public void mockAuth() {
.put("session_state", UUID.randomUUID().toString())
.put("scope", "email profile")
.toString())));

/*
* MockAuth for new RBAC permission via keycloak
*/
WireMock.stubFor(
WireMock.post(
WireMock.urlMatching(
String.format("/realms/%s/protocol/openid-connect/token", realm)))
.withRequestBody(
WireMock.containing("grant_type=urn:ietf:params:oauth:grant-type:uma-ticket"))
.willReturn(
WireMock.aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(objectMapper.createObjectNode().put("result", "true").toString())));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
*
* Copyright (c) 2024. Deutsche Telekom AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
*
*/

package org.onap.portalng.bff.rbac;

import com.github.tomakehurst.wiremock.client.WireMock;
import io.restassured.http.Header;
import org.junit.jupiter.api.Test;
import org.onap.portalng.bff.BaseIntegrationTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

public class RoleBaseAccessIntegrationTest extends BaseIntegrationTest {

@Test
void thatRoleIsNotSufficient() {

WireMock.stubFor(
WireMock.post(
WireMock.urlMatching(
String.format("/realms/%s/protocol/openid-connect/token", realm)))
.withRequestBody(
WireMock.containing("grant_type=urn:ietf:params:oauth:grant-type:uma-ticket"))
.willReturn(
WireMock.aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withStatus(HttpStatus.FORBIDDEN.value())));

requestSpecification()
.given()
.accept(MediaType.APPLICATION_JSON_VALUE)
.header(new Header("X-Request-Id", "addf6005-3075-4c80-b7bc-2c70b7d42b57"))
.when()
.get("/roles")
.then()
.statusCode(HttpStatus.FORBIDDEN.value());
}

@Test
void thatResourceIsNotAvailable() {

WireMock.stubFor(
WireMock.post(
WireMock.urlMatching(
String.format("/realms/%s/protocol/openid-connect/token", realm)))
.withRequestBody(
WireMock.containing("grant_type=urn:ietf:params:oauth:grant-type:uma-ticket"))
.willReturn(
WireMock.aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withStatus(HttpStatus.BAD_REQUEST.value())));

requestSpecification()
.given()
.accept(MediaType.APPLICATION_JSON_VALUE)
.header(new Header("X-Request-Id", "addf6005-3075-4c80-b7bc-2c70b7d42b57"))
.when()
.get("/roles")
.then()
.statusCode(HttpStatus.FORBIDDEN.value());
}

@Test
void thatRoleBaseCheckIsMalformed() {

WireMock.stubFor(
WireMock.post(
WireMock.urlMatching(
String.format("/realms/%s/protocol/openid-connect/token", realm)))
.withRequestBody(
WireMock.containing("grant_type=urn:ietf:params:oauth:grant-type:uma-ticket"))
.willReturn(
WireMock.aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(objectMapper.createObjectNode().put("result", "false").toString())));

requestSpecification()
.given()
.accept(MediaType.APPLICATION_JSON_VALUE)
.header(new Header("X-Request-Id", "addf6005-3075-4c80-b7bc-2c70b7d42b57"))
.when()
.get("/roles")
.then()
.statusCode(HttpStatus.FORBIDDEN.value());
}
}
5 changes: 5 additions & 0 deletions app/src/test/resources/application-development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@ bff:
preferences-url: http://localhost:${wiremock.server.port}
history-url: http://localhost:${wiremock.server.port}
keycloak-url: http://localhost:${wiremock.server.port}
keycloak-client-id: test
endpoints:
unauthenticated: /api-docs.html, /api.yaml, /webjars/**, /actuator/**
rbac:
endpoints-excluded: /api-docs.html, /api.yaml, /webjars/**, /actuator/**
4 changes: 2 additions & 2 deletions app/src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ bff:
preferences-url: http://localhost:${wiremock.server.port}
history-url: http://localhost:${wiremock.server.port}
keycloak-url: http://localhost:${wiremock.server.port}
keycloak-client-id: test
endpoints:
unauthenticated: /api-docs.html, /api.yaml, /webjars/**, /actuator/**
rbac:
endpoints-excluded: /actuator/**, **/actuator/**, */actuator/**, /**/actuator/**, /*/actuator/**

endpoints-excluded: /api-docs.html, /api.yaml, /webjars/**, /actuator/**
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class BffConfig {
@NotBlank private final String preferencesUrl;
@NotBlank private final String historyUrl;
@NotBlank private final String keycloakUrl;
@NotBlank private final String keycloakClientId;

@NotNull private final Map<String, Set<String>> accessControl;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
*
* Copyright (c) 2024. Deutsche Telekom AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
*
*/

package org.onap.portalng.bff.config;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class KeycloakPermissionFilter implements WebFilter {

private final WebClient webClient;

private final ObjectMapper objectMapper;
private final BffConfig bffConfig;

@Value("${bff.rbac.endpoints-excluded}")
private String[] EXCLUDED_PATHS;

public KeycloakPermissionFilter(
WebClient.Builder webClientBuilder, ObjectMapper objectMapper, BffConfig bffConfig) {
this.webClient = webClientBuilder.build();
this.objectMapper = objectMapper;
this.bffConfig = bffConfig;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String accessToken = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
String uri = exchange.getRequest().getURI().getPath();
String method = exchange.getRequest().getMethod().toString();

for (String excludedPath : EXCLUDED_PATHS) {
if (uri.matches(excludedPath.replace("**", ".*"))) {
return chain.filter(exchange);
}
}

String body =
new StringBuilder()
.append("grant_type=urn:ietf:params:oauth:grant-type:uma-ticket")
.append("&audience=")
.append(bffConfig.getKeycloakClientId())
.append("&permission_resource_format=uri")
.append("&permission_resource_matching_uri=true")
.append("&permission=")
.append(uri)
.append("#")
.append(method)
.append("&response_mode=decision")
.toString();

return webClient
.post()
.uri(
bffConfig.getKeycloakUrl()
+ "/realms/"
+ bffConfig.getRealm()
+ "/protocol/openid-connect/token")
.header(HttpHeaders.AUTHORIZATION, accessToken)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.bodyValue(body)
.retrieve()
.bodyToMono(String.class)
.flatMap(
response -> {
if (isPermissionGranted(response)) {
return chain.filter(exchange);
} else {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
})
.onErrorResume(
ex -> {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
});
}

private boolean isPermissionGranted(String response) {
try {
JsonNode jsonNode = objectMapper.readTree(response);
if (jsonNode.has("result") && jsonNode.get("result").asBoolean()) {
return true;
}
} catch (Exception e) {
log.error("Error parsing JSON response", e);
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@

import static org.springframework.security.config.Customizer.withDefaults;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
Expand All @@ -38,8 +40,11 @@

@Configuration
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final KeycloakPermissionFilter keycloakPermissionFilter;

@Value("${bff.endpoints.unauthenticated}")
private String[] unauthenticatedEndpoints;

Expand All @@ -59,6 +64,7 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
.authenticated())
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt)
.oauth2Client(withDefaults())
.addFilterAfter(keycloakPermissionFilter, SecurityWebFiltersOrder.AUTHORIZATION)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@
package org.onap.portalng.bff.controller;

import org.onap.portalng.bff.config.BffConfig;
import org.onap.portalng.bff.config.IdTokenExchangeFilterFunction;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public abstract class AbstractBffController {

Expand All @@ -33,14 +30,4 @@ public abstract class AbstractBffController {
protected AbstractBffController(BffConfig bffConfig) {
this.bffConfig = bffConfig;
}

public Mono<Void> checkRoleAccess(String method, ServerWebExchange exchange) {
return bffConfig
.getRoles(method)
.flatMap(
roles ->
roles.contains("*")
? Mono.empty()
: IdTokenExchangeFilterFunction.validateAccess(exchange, roles));
}
}
Loading

0 comments on commit 11fa772

Please sign in to comment.