Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

acl by apikey email #1620

Merged
merged 34 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
db9d8ad
API Key authenticate
maggarwal13 Dec 24, 2024
f7243bb
Fix tests
maggarwal13 Dec 24, 2024
a0d1b42
refactor
maggarwal13 Dec 24, 2024
c3cf5a9
add loggings
maggarwal13 Dec 27, 2024
4c5232f
fix exception
maggarwal13 Dec 27, 2024
abcd1fa
change logging
maggarwal13 Dec 27, 2024
6e52f60
refactor
maggarwal13 Dec 27, 2024
b21c4ee
Add more logging
maggarwal13 Dec 27, 2024
5063c18
add objectMapper
maggarwal13 Dec 27, 2024
88d7673
remove expiration
maggarwal13 Dec 27, 2024
117bd97
correct scope formatting
maggarwal13 Dec 27, 2024
74fd6d8
fix scope issue
maggarwal13 Dec 27, 2024
7a28d79
refactor
maggarwal13 Dec 27, 2024
632d27b
remove logging
maggarwal13 Dec 30, 2024
b945733
use Header for bearer token
maggarwal13 Dec 31, 2024
fe5a1e3
refactor
maggarwal13 Dec 31, 2024
bec395f
test for notification email
maggarwal13 Dec 31, 2024
8c5e29d
refactor
maggarwal13 Dec 31, 2024
eaded85
Add tests
maggarwal13 Dec 31, 2024
feb4715
Add JWT verification
maggarwal13 Jan 2, 2025
2104def
enable caching on public key
maggarwal13 Jan 2, 2025
db70263
change logging to error
maggarwal13 Jan 2, 2025
fe546c6
add tests
maggarwal13 Jan 2, 2025
2e73133
Add Tests for updates
maggarwal13 Jan 3, 2025
193fbca
add domain tests
maggarwal13 Jan 3, 2025
092d108
acl by apikey email
maggarwal13 Jan 6, 2025
f7f1fd1
add caching for ApiKey oAuth
maggarwal13 Jan 6, 2025
d747f70
Merge branch 'master' into apikey_autheniticate
maggarwal13 Jan 6, 2025
e492dbd
refcator
maggarwal13 Jan 7, 2025
fd63bbf
Merge branch 'apikey_autheniticate' into acl_accounting_api_key
maggarwal13 Jan 7, 2025
e0d758c
Merge branch 'master' into acl_accounting_api_key
maggarwal13 Jan 8, 2025
a99f8f6
fix tests after merging
maggarwal13 Jan 9, 2025
f8cf87f
fix tests
maggarwal13 Jan 9, 2025
d8472fb
Merge branch 'master' into acl_accounting_api_key
maggarwal13 Jan 9, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package net.ripe.db.whois.api.apiKey;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider;
import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;
import com.google.common.collect.Lists;
import com.nimbusds.jose.util.JSONObjectUtils;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import com.nimbusds.jose.jwk.RSAKey;
import jakarta.ws.rs.core.MediaType;
import org.apache.commons.lang.StringUtils;
import org.glassfish.jersey.client.ClientProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

import javax.annotation.Nullable;
import java.text.ParseException;
import java.util.Collections;
import java.util.List;
import java.util.Map;

@Component
public class ApiPublicKeyLoader {

private static final Logger LOGGER = LoggerFactory.getLogger(ApiPublicKeyLoader.class);

private static final int CLIENT_CONNECT_TIMEOUT = 10_000;
private static final int CLIENT_READ_TIMEOUT = 60_000;

private final Client client;
private final String restUrl;

@Autowired
public ApiPublicKeyLoader(
@Value("${api.public.key.url:}") final String restUrl) {
this.restUrl = restUrl;

final ObjectMapper objectMapper = JsonMapper.builder()
.enable(SerializationFeature.INDENT_OUTPUT)
.build();
objectMapper.setAnnotationIntrospector(
new AnnotationIntrospectorPair(
new JacksonAnnotationIntrospector(),
new JakartaXmlBindAnnotationIntrospector(TypeFactory.defaultInstance())));
objectMapper.registerModule(new JavaTimeModule());
final JacksonJsonProvider jsonProvider = (new JacksonJsonProvider())
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, false)
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
jsonProvider.setMapper(objectMapper);
this.client = (ClientBuilder.newBuilder()
.register(jsonProvider))
.property(ClientProperties.CONNECT_TIMEOUT, CLIENT_CONNECT_TIMEOUT)
.property(ClientProperties.READ_TIMEOUT, CLIENT_READ_TIMEOUT)
.build();
}

@Cacheable(cacheNames = "JWTpublicKeyDetails")
public List<RSAKey> loadPublicKey() throws ParseException {
if(StringUtils.isEmpty(restUrl)) {
LOGGER.warn("Skipping JWT verification as url is null");
return Collections.emptyList();
}

LOGGER.debug("Loading public key from {}", restUrl);
try {
return getListOfKeys(client.target(restUrl)
.request(MediaType.APPLICATION_JSON_TYPE)
.get(String.class));

} catch (Exception e) {
LOGGER.error("Failed to load RSA public key apikey due to {}:{}", e.getClass().getName(), e.getMessage());
throw e;
}
}

protected List<RSAKey> getListOfKeys(final String publicKeys) throws ParseException {
try {
final Map<String, Object> content = JSONObjectUtils.parse(publicKeys);
final List<RSAKey> rsaKeys = Lists.newArrayList();

for (final Map<String, Object> key : JSONObjectUtils.getJSONObjectArray(content, "keys")) {
rsaKeys.add(RSAKey.parse(key));
}

return rsaKeys;
} catch ( Exception e ) {
LOGGER.error("Failed to parse public key", e);
throw e;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package net.ripe.db.whois.api.apiKey;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.net.HttpHeaders;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.SignedJWT;
import jakarta.servlet.http.HttpServletRequest;
import net.ripe.db.whois.common.apiKey.OAuthSession;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Nullable;
import java.util.List;

@Component
public class BearerTokenExtractor {

private static final Logger LOGGER = LoggerFactory.getLogger(BearerTokenExtractor.class);

private final ApiPublicKeyLoader apiPublicKeyLoader;
private final boolean enabled;

@Autowired
public BearerTokenExtractor(final ApiPublicKeyLoader apiPublicKeyLoader,
@Value("${apikey.authenticate.enabled:false}") final boolean enabled) {
this.apiPublicKeyLoader = apiPublicKeyLoader;
this.enabled = enabled;
}

@Nullable
public OAuthSession extractBearerToken(final HttpServletRequest request, final String accessKey) {
if(!enabled || StringUtils.isEmpty(accessKey)) {
return null;
}

final String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
return getOAuthSession(bearerToken, accessKey);
}

private OAuthSession getOAuthSession(final String bearerToken, final String accessKey) {
if(StringUtils.isEmpty(bearerToken)) {
return new OAuthSession(accessKey);
}

try {
final SignedJWT signedJWT = SignedJWT.parse(StringUtils.substringAfter(bearerToken, "Bearer "));

if(!verifyJWTSignature(signedJWT)) {
LOGGER.debug("JWT signature verification failed for {}", accessKey);
return new OAuthSession(accessKey);
}

//TODO[MA]: remove when accessKey is available from api registry call
return OAuthSession.from(new ObjectMapper().readValue(signedJWT.getPayload().toString(), OAuthSession.class), accessKey);
} catch (JsonProcessingException e) {
LOGGER.error("Failed to serialize OAuthSession, this should never have happened", e);
return new OAuthSession(accessKey);
} catch (Exception e) {
LOGGER.error("Failed to read OAuthSession, this should never have happened", e);
return new OAuthSession(accessKey);
}
}

private boolean verifyJWTSignature(final SignedJWT signedJWT) {
try {
final List<RSAKey> rsaKeys = apiPublicKeyLoader.loadPublicKey();

if(rsaKeys.isEmpty()) {
LOGGER.warn("Skipping JWT verification as url is null");
return true;
}

final RSAKey publicKey = rsaKeys.stream().filter( rsaKey -> rsaKey.getKeyID().equals(signedJWT.getHeader().getKeyID())).findFirst().get();
final JWSVerifier verifier = new RSASSAVerifier(publicKey);

return signedJWT.verify(verifier);
} catch (Exception ex) {
LOGGER.debug("failed to verify signature {}", ex.getMessage());
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,6 @@ protected void account(final RpslObject rpslObject) {
}

private AccountingIdentifier getAccountingIdentifier() {
return new AccountingIdentifier(remoteAddress, ssoToken);
return accessControlListManager.getAccountingIdentifier(remoteAddress, ssoToken, null );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public Response update(final WhoisResources whoisResources,

try {
final Origin origin = updatePerformer.createOrigin(request);
final UpdateContext updateContext = updatePerformer.initContext(origin, crowdTokenKey, request);
final UpdateContext updateContext = updatePerformer.initContext(origin, crowdTokenKey, null, request);
updateContext.setBatchUpdate();

if(isQueryParamSet(dryRun)) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import net.ripe.db.whois.api.rest.domain.WhoisResources;
import net.ripe.db.whois.api.rest.mapper.FormattedServerAttributeMapper;
import net.ripe.db.whois.api.rest.mapper.WhoisObjectMapper;
import net.ripe.db.whois.common.apiKey.ApiKeyUtils;
import net.ripe.db.whois.common.rpsl.ObjectType;
import net.ripe.db.whois.common.rpsl.RpslObject;
import net.ripe.db.whois.common.sso.AuthServiceClient;
Expand Down Expand Up @@ -84,6 +85,7 @@ public Response create(
@Context final HttpServletRequest request,
@PathParam("source") final String sourceParam,
@QueryParam("password") final List<String> passwords,
@QueryParam(ApiKeyUtils.APIKEY_ACCESS_QUERY_PARAM) final String accessKey,
@CookieParam(AuthServiceClient.TOKEN_KEY) final String crowdTokenKey) {

if (resources == null || resources.getWhoisObjects().size() == 0) {
Expand All @@ -93,7 +95,7 @@ public Response create(
try {
final Origin origin = updatePerformer.createOrigin(request);

final UpdateContext updateContext = updatePerformer.initContext(origin, crowdTokenKey, request);
final UpdateContext updateContext = updatePerformer.initContext(origin, crowdTokenKey, accessKey, request);
updateContext.setBatchUpdate();

auditlogRequest(request);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package net.ripe.db.whois.api.rest;

import com.google.common.net.HttpHeaders;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.core.UriBuilder;
import net.ripe.db.whois.common.apiKey.ApiKeyAuthServiceClient;
import net.ripe.db.whois.common.apiKey.ApiKeyUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Objects;

@Component
public class HttpsAPIKeyAuthCustomizer implements Filter {

private static final Logger LOGGER = LoggerFactory.getLogger(HttpsAPIKeyAuthCustomizer.class);

final private boolean isEnabled;
final ApiKeyAuthServiceClient apiKeyAuthServiceClient;

@Autowired
public HttpsAPIKeyAuthCustomizer(@Value("${apikey.authenticate.enabled:false}") final boolean isEnabled,
final ApiKeyAuthServiceClient apiKeyAuthServiceClient) {
this.apiKeyAuthServiceClient = apiKeyAuthServiceClient;
this.isEnabled = isEnabled;
}

@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {

if( !(request instanceof HttpServletRequest)) {
chain.doFilter(request, response);
}

final HttpServletRequest httpRequest = (HttpServletRequest) request;

if(isNotValidRequest(httpRequest)) {
final HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpStatus.BAD_REQUEST_400, "Bad Request");
return;
}

if(!isAPIKeyRequest(httpRequest)) {
chain.doFilter(request, response);
return;
}

LOGGER.debug("It is a api key request");

if (RestServiceHelper.isHttpProtocol(httpRequest)){
final HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpStatus.UPGRADE_REQUIRED_426, "HTTPS required for Basic authorization");
return;
}

try {
final String accessKey = ApiKeyUtils.getAccessKey(httpRequest.getHeader(HttpHeaders.AUTHORIZATION));

final String bearerToken = apiKeyAuthServiceClient.validateApiKey(httpRequest.getHeader(HttpHeaders.AUTHORIZATION), accessKey);
chain.doFilter(new HttpApiAuthRequestWrapper((HttpServletRequest) request, accessKey, bearerToken), response);

} catch (Exception ex) {
final HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "Api Key request failed");
}
}

private static boolean isNotValidRequest(HttpServletRequest httpRequest) {
return (!StringUtils.isEmpty(httpRequest.getQueryString()) && httpRequest.getQueryString().contains(ApiKeyUtils.APIKEY_ACCESS_QUERY_PARAM)) ||
(httpRequest.getHeader(HttpHeaders.AUTHORIZATION) != null && httpRequest.getHeader(HttpHeaders.AUTHORIZATION).startsWith("Bearer"));
}

private boolean isAPIKeyRequest(final HttpServletRequest httpRequest) {
if(!isEnabled) {
return false;
}

//TODO: Remove this logic when basic auth support is deprecated
return ApiKeyUtils.isAPIKeyRequest(httpRequest.getHeader(HttpHeaders.AUTHORIZATION));
}

@Override
public void destroy() {
// do nothing
}

private static final class HttpApiAuthRequestWrapper extends HttpServletRequestWrapper {

final private String bearerToken;
final private String accessKey;

private HttpApiAuthRequestWrapper(final HttpServletRequest request, final String accessKey , final String bearerToken) {
super(request);
this.bearerToken = bearerToken;
this.accessKey = accessKey;
}

@Override
public String getQueryString() {
final UriBuilder builder = UriBuilder.newInstance();
builder.replaceQuery(super.getQueryString());
builder.queryParam(ApiKeyUtils.APIKEY_ACCESS_QUERY_PARAM, accessKey);
return builder.build().getQuery();
}

@Override
public String getHeader(final String name) {
if(Objects.equals(name, HttpHeaders.AUTHORIZATION)) {
return "Bearer " + bearerToken;
}

return super.getHeader(name);
}
}
}
Loading