Skip to content

Commit

Permalink
Native java klient for PDP / ABAC (#1163)
Browse files Browse the repository at this point in the history
  • Loading branch information
jolarsen authored Aug 11, 2022
1 parent 862db49 commit 18c0a5f
Show file tree
Hide file tree
Showing 26 changed files with 176 additions and 1,440 deletions.
14 changes: 0 additions & 14 deletions felles/abac/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,6 @@
<groupId>jakarta.interceptor</groupId>
<artifactId>jakarta.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.json</artifactId>
<version>${jakarta.json-api.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Default;
import javax.inject.Inject;
import javax.inject.Named;

import no.nav.foreldrepenger.konfig.KonfigVerdi;

Expand All @@ -24,7 +23,6 @@ public class PepImpl implements Pep {
private final static String PIP = "pip.tjeneste.kan.kun.kalles.av.pdp.servicebruker";

private PdpKlient pdpKlient;
private PdpKlient pdp2Klient;
private PdpRequestBuilder builder;

private Set<String> pipUsers;
Expand All @@ -35,22 +33,12 @@ public PepImpl() {
}

@Inject
public PepImpl(@Named("pdp1") PdpKlient pdpKlient,
public PepImpl(PdpKlient pdpKlient,
TokenProvider tokenProvider,
PdpRequestBuilder pdpRequestBuilder,
AbacAuditlogger auditlogger,
@KonfigVerdi(value = "pip.users", required = false) String pipUsers) {
this(pdpKlient, null, tokenProvider, pdpRequestBuilder, auditlogger, pipUsers);
}

public PepImpl(PdpKlient pdpKlient,
PdpKlient pdp2Klient,
TokenProvider tokenProvider,
PdpRequestBuilder pdpRequestBuilder,
AbacAuditlogger auditlogger,
String pipUsers) {
this.pdpKlient = pdpKlient;
this.pdp2Klient = pdp2Klient;
this.builder = pdpRequestBuilder;
this.tokenProvider = tokenProvider;
this.auditlogger = auditlogger;
Expand All @@ -71,11 +59,7 @@ public Tilgangsbeslutning vurderTilgang(AbacAttributtSamling attributter) {
if (PIP.equals(attributter.getResource())) {
return vurderTilgangTilPipTjeneste(pdpRequest, attributter);
}
if (pdp2Klient != null) {
return pdp2Klient.forespørTilgang(pdpRequest);
} else {
return pdpKlient.forespørTilgang(pdpRequest);
}
return pdpKlient.forespørTilgang(pdpRequest);
}

protected Tilgangsbeslutning vurderTilgangTilPipTjeneste(PdpRequest pdpRequest, AbacAttributtSamling attributter) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package no.nav.vedtak.sikkerhet.pdp;

import no.nav.vedtak.sikkerhet.pdp.xacml.XacmlRequestBuilder;
import no.nav.vedtak.sikkerhet.pdp.xacml.XacmlResponseWrapper;
import no.nav.vedtak.sikkerhet.pdp.xacml.XacmlResponse;

public interface PdpConsumer {
XacmlResponseWrapper evaluate(XacmlRequestBuilder request);
XacmlResponse evaluate(XacmlRequestBuilder request);
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,30 @@
package no.nav.vedtak.sikkerhet.pdp;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.time.Duration;
import java.util.Base64;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonValue;

import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectReader;

import no.nav.foreldrepenger.konfig.KonfigVerdi;
import no.nav.vedtak.exception.TekniskException;
import no.nav.vedtak.log.mdc.MDCOperations;
import no.nav.vedtak.mapper.json.DefaultJsonMapper;
import no.nav.vedtak.sikkerhet.pdp.xacml.XacmlRequestBuilder;
import no.nav.vedtak.sikkerhet.pdp.xacml.XacmlResponseWrapper;
import no.nav.vedtak.sikkerhet.pdp.xacml.XacmlResponse;

@ApplicationScoped
public class PdpConsumerImpl implements PdpConsumer {
Expand All @@ -53,178 +33,64 @@ public class PdpConsumerImpl implements PdpConsumer {
private static final String PDP_ENDPOINT_URL_KEY = "abac.pdp.endpoint.url";
private static final String SYSTEMBRUKER_USERNAME = "systembruker.username";
private static final String SYSTEMBRUKER_PASSWORD = "systembruker.password"; // NOSONAR
private static final int MAX_TOTAL_CONNECTIONS_PER_ROUTE = 20;
private static final String MEDIA_TYPE = "application/xacml+json";
private static final Logger LOG = LoggerFactory.getLogger(PdpConsumerImpl.class);

private String pdpUrl;
private String brukernavn;
private String passord;
private HttpHost target;
private HttpClient client;
private ObjectReader reader;

private record Respons(StatusLine status, JsonObject res) {

}
private record CacheConfig(CloseableHttpClient client, AuthCache cache) {

}

private CacheConfig activeConfiguration;
private URI pdpUrl;
private String brukernavn;
private String basicCredentials;

PdpConsumerImpl() {
} // CDI

@Inject
public PdpConsumerImpl(@KonfigVerdi(value = PDP_ENDPOINT_URL_KEY, defaultVerdi = DEFAULT_ABAC_URL) String pdpUrl,
@KonfigVerdi(SYSTEMBRUKER_USERNAME) String brukernavn,
@KonfigVerdi(SYSTEMBRUKER_PASSWORD) String passord) {
this.pdpUrl = pdpUrl;
@KonfigVerdi(SYSTEMBRUKER_USERNAME) String brukernavn,
@KonfigVerdi(SYSTEMBRUKER_PASSWORD) String passord) {
this.pdpUrl = URI.create(pdpUrl);
this.brukernavn = brukernavn;
this.passord = passord;
target = HttpHost.create(getSchemaAndHostFromURL(pdpUrl));
activeConfiguration = buildClient();
}

private CacheConfig buildClient() {
var cm = new PoolingHttpClientConnectionManager();
cm.setDefaultMaxPerRoute(MAX_TOTAL_CONNECTIONS_PER_ROUTE);
cm.setMaxTotal(3 * MAX_TOTAL_CONNECTIONS_PER_ROUTE); // i tilfelle redirects

var credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(new AuthScope(target.getHostName(), target.getPort()), new UsernamePasswordCredentials(brukernavn, passord));

var requestConfig = RequestConfig.custom()
.setCookieSpec(CookieSpecs.IGNORE_COOKIES)
.setAuthenticationEnabled(true)
.build();

var client = HttpClients.custom()
.setConnectionManager(cm)
.setDefaultCredentialsProvider(credsProvider)
.setDefaultRequestConfig(requestConfig)
.setRetryHandler(new StandardHttpRequestRetryHandler()) // idempotente HTTP kall kan gjentas
.setKeepAliveStrategy(createKeepAliveStrategy(30)) // keep max 30 sek keepalive på connections hvis ikke angitt av server
.build();

AuthCache authCache = new BasicAuthCache();
authCache.put(target, new BasicScheme());

return new CacheConfig(client, authCache);
this.basicCredentials = basicCredentials(brukernavn, passord);
// TODO - vurder om bør settes static final?
this.client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
this.reader = DefaultJsonMapper.getObjectMapper().readerFor(XacmlResponse.class);
}

@Override
public XacmlResponseWrapper evaluate(XacmlRequestBuilder request) {
return new XacmlResponseWrapper(execute(request.build()));
}

JsonObject execute(JsonObject request) {
HttpPost post = new HttpPost(pdpUrl);
post.setHeader("Content-type", MEDIA_TYPE);
post.setEntity(new StringEntity(request.toString(), java.nio.charset.StandardCharsets.UTF_8));
public XacmlResponse evaluate(XacmlRequestBuilder xacmlRequest) {
// TODO : hvilke headere trenger abac egentlig - utenom Auth og Content-type
var request = HttpRequest.newBuilder()
.header("Authorization", basicCredentials)
.header("Nav-Consumer-Id", brukernavn)
.header("Nav-Call-Id", MDCOperations.getCallId())
.header("Nav-Callid", MDCOperations.getCallId())
.header("Content-type", MEDIA_TYPE)
.timeout(Duration.ofSeconds(5))
.uri(pdpUrl)
.POST(HttpRequest.BodyPublishers.ofString(DefaultJsonMapper.toJson(xacmlRequest.build()), UTF_8))
.build();

LOG.trace("PDP-request: {}", request);

CacheConfig active = activeConfiguration;

Respons response = call(active, post);
int statusCode = response.status().getStatusCode();
if (HttpStatus.SC_OK == statusCode) {
return response.res();
}
if (HttpStatus.SC_UNAUTHORIZED == statusCode) {
synchronized (this) {
if (active == activeConfiguration) {
// reset client bør sjelden skje, skal ikke havne i situasjonen at vi spør etter
// ting de ikke er authorized til, men
// det skjer at PDP server f.eks. er resatt og eneste vi kan gjøre er å resette
// vår egen tilstand.
activeConfiguration = buildClient();
LOG.warn("Feilet autentisering mot PDP, reinstansierer hele klienten for å fjerne all state");
}
}
active = activeConfiguration;

response = call(active, post);
statusCode = response.status().getStatusCode();
if (HttpStatus.SC_OK == statusCode) {
return response.res();
}
if (HttpStatus.SC_UNAUTHORIZED == statusCode) {
throw new TekniskException("F-867412",
String.format("Feilet autentisering mot PDP, reinstansiering av klienten hjalp ikke. Tiltak: Drep pod '%s'",
System.getenv("HOSTNAME")));
}
}
throw new TekniskException("F-815365",
String.format("Mottok HTTP error fra PDP: HTTP %s - %s", statusCode, response.status().getReasonPhrase()));

}

private Respons call(CacheConfig active, HttpPost post) {
final CloseableHttpClient client = active.client();
final AuthCache authCache = active.cache();

int retries = 2;
StatusLine statusLine = null;
while (--retries >= 0) {
HttpClientContext context = HttpClientContext.create();
context.setAuthCache(authCache);
try (CloseableHttpResponse response = client.execute(target, post, context)) {
statusLine = response.getStatusLine();
if (HttpStatus.SC_OK == statusLine.getStatusCode()) {
final HttpEntity entity = response.getEntity();
try (JsonReader reader = Json.createReader(entity.getContent())) {
JsonObject jsonResponse = reader.readObject();
LOG.trace("PDP-response: {}", jsonResponse);
return new Respons(statusLine, jsonResponse);
}
}
break;
} catch (IOException e) {
if (retries > 0) {
// ved IO feil gjør et forsøk til (server kan ha stengt conn)
// logg kun første gang vi treffer, kast exception andre gang
LOG.trace("Fikk IOException - PDP feil, prøver en gang til", e);
} else {
throw new TekniskException("F-091324", "Uventet IO-exception mot PDP", e);
}
} finally {
post.releaseConnection();
}
}

return new Respons(statusLine, JsonValue.EMPTY_JSON_OBJECT);
}

private String getSchemaAndHostFromURL(String pdpUrl) {
try {
URI uri = new URI(pdpUrl);
return uri.getScheme() + "://" + uri.getHost() + (uri.getPort() > -1 ? ":" + uri.getPort() : "");
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString(UTF_8));
if (response == null || response.statusCode() == HttpURLConnection.HTTP_UNAUTHORIZED || response.body() == null) {
LOG.info("ingen response fra PDP status = {}", response == null ? "null" : response.statusCode());
throw new TekniskException("F-157385", "Kunne ikke hente svar fra ABAC");
}
return reader.readValue(response.body(), XacmlResponse.class);
} catch (JsonProcessingException e) {
throw new TekniskException("F-208314", "Kunne ikke deserialisere objekt til JSON", e);
} catch (IOException e) {
throw new TekniskException("F-091324", "Uventet IO-exception mot PDP", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new TekniskException("F-432938", "InterruptedException ved kall mot PDP", e);
}
}

/**
* Sørger for å droppe og starte nye connections innimellom også om server ikke
* sender keepalive header.
*/
private static ConnectionKeepAliveStrategy createKeepAliveStrategy(int seconds) {
return new ConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
return Long.parseLong(value) * 1000L;
}
}
return seconds * 1000L;
}
};
private static String basicCredentials(String username, String password) {
return "Basic " + Base64.getEncoder().encodeToString(String.format("%s:%s", username, password).getBytes(UTF_8));
}

}
Loading

0 comments on commit 18c0a5f

Please sign in to comment.