Skip to content

Commit

Permalink
add syncupdate support for apikey (#1626)
Browse files Browse the repository at this point in the history
* add syncupdate support for apikey

* refactor

* fix build

* add tests

---------

Co-authored-by: Ed Shryane <[email protected]>
  • Loading branch information
maggarwal13 and eshryane authored Jan 15, 2025
1 parent 85a6e83 commit fcd55cc
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import net.ripe.db.whois.common.Latin1Conversion;
import net.ripe.db.whois.update.domain.APIKeyCredential;
import net.ripe.db.whois.update.domain.ClientCertificateCredential;
import net.ripe.db.whois.update.domain.ContentWithCredentials;
import net.ripe.db.whois.update.domain.Credential;
Expand Down Expand Up @@ -63,6 +64,10 @@ public List<Paragraph> createParagraphs(final ContentWithCredentials contentWith
baseCredentials.add(SsoCredential.createOfferedCredential(updateContext.getUserSession()));
}

if (updateContext.getOAuthSession() != null) {
baseCredentials.add(APIKeyCredential.createOfferedCredential(updateContext.getOAuthSession()));
}

if (updateContext.getClientCertificates() != null) {
for (X509CertificateWrapper clientCertificate : updateContext.getClientCertificates()) {
baseCredentials.add(ClientCertificateCredential.createOfferedCredential(clientCertificate));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import net.ripe.db.whois.api.UpdatesParser;
import net.ripe.db.whois.api.apiKey.BearerTokenExtractor;
import net.ripe.db.whois.common.DateTimeProvider;
import net.ripe.db.whois.common.Message;
import net.ripe.db.whois.common.Messages;
import net.ripe.db.whois.common.apiKey.ApiKeyUtils;
import net.ripe.db.whois.common.conversion.PasswordFilter;
import net.ripe.db.whois.common.domain.CIString;
import net.ripe.db.whois.common.source.SourceContext;
Expand Down Expand Up @@ -70,20 +72,23 @@ public class SyncUpdatesService {
private final LoggerContext loggerContext;
private final SourceContext sourceContext;
private final SsoTokenTranslator ssoTokenTranslator;
private final BearerTokenExtractor bearerTokenExtractor;

@Autowired
public SyncUpdatesService(final DateTimeProvider dateTimeProvider,
final UpdateRequestHandler updateRequestHandler,
final UpdatesParser updatesParser,
final LoggerContext loggerContext,
final SourceContext sourceContext,
final BearerTokenExtractor bearerTokenExtractor,
final SsoTokenTranslator ssoTokenTranslator) {
this.dateTimeProvider = dateTimeProvider;
this.updateRequestHandler = updateRequestHandler;
this.updatesParser = updatesParser;
this.loggerContext = loggerContext;
this.sourceContext = sourceContext;
this.ssoTokenTranslator = ssoTokenTranslator;
this.bearerTokenExtractor = bearerTokenExtractor;
}

@GET
Expand All @@ -98,6 +103,7 @@ public Response doGet(
@QueryParam(Command.DIFF) final String diff,
@QueryParam(Command.REDIRECT) final String redirect,
@HeaderParam(HttpHeaders.CONTENT_TYPE) final String contentType,
@QueryParam(ApiKeyUtils.APIKEY_ACCESS_QUERY_PARAM) final String apiKeyId,
@CookieParam(AuthServiceClient.TOKEN_KEY) final String crowdTokenKey) {
final Request request = new Request.RequestBuilder()
.setData(decode(data, getCharset(contentType)))
Expand All @@ -107,6 +113,7 @@ public Response doGet(
.setDiff(diff)
.setRemoteAddress(httpServletRequest.getRemoteAddr())
.setSource(source)
.setApiKeyId(apiKeyId)
.setSsoToken(crowdTokenKey)
.build();
return doSyncUpdate(httpServletRequest, request, getCharset(contentType));
Expand All @@ -125,6 +132,7 @@ public Response doUrlEncodedPost(
@FormParam(Command.DIFF) final String diff,
@FormParam(Command.REDIRECT) final String redirect,
@HeaderParam(HttpHeaders.CONTENT_TYPE) final String contentType,
@QueryParam(ApiKeyUtils.APIKEY_ACCESS_QUERY_PARAM) final String apiKeyId,
@CookieParam(AuthServiceClient.TOKEN_KEY) final String crowdTokenKey) {
final Request request = new Request.RequestBuilder()
.setData(data)
Expand All @@ -134,6 +142,7 @@ public Response doUrlEncodedPost(
.setDiff(diff)
.setRemoteAddress(httpServletRequest.getRemoteAddr())
.setSource(source)
.setApiKeyId(apiKeyId)
.setSsoToken(crowdTokenKey)
.build();
return doSyncUpdate(httpServletRequest, request, getCharset(contentType));
Expand All @@ -151,6 +160,7 @@ public Response doMultipartPost(
@FormDataParam(Command.NEW) final String nnew,
@FormDataParam(Command.DIFF) final String diff,
@FormDataParam(Command.REDIRECT) final String redirect,
@QueryParam(ApiKeyUtils.APIKEY_ACCESS_QUERY_PARAM) final String apiKeyId,
@HeaderParam(HttpHeaders.CONTENT_TYPE) final String contentType,
@CookieParam(AuthServiceClient.TOKEN_KEY) final String crowdTokenKey) {
final Request request = new Request.RequestBuilder()
Expand All @@ -161,6 +171,7 @@ public Response doMultipartPost(
.setDiff(diff)
.setRemoteAddress(httpServletRequest.getRemoteAddr())
.setSource(source)
.setApiKeyId(apiKeyId)
.setSsoToken(crowdTokenKey)
.build();
return doSyncUpdate(httpServletRequest, request, getCharset(contentType));
Expand Down Expand Up @@ -208,6 +219,7 @@ private Response doSyncUpdate(final HttpServletRequest httpServletRequest, final

setSsoSessionToContext(updateContext, request.getSsoToken());
setClientCertificates(updateContext, httpServletRequest);
updateContext.setOAuthSession(bearerTokenExtractor.extractBearerToken(httpServletRequest, request.getApiKeyId()));

final String content = request.hasParam("DATA") ? request.getParam("DATA") : "";

Expand Down Expand Up @@ -356,12 +368,14 @@ static class Request {
private final String remoteAddress;
private final String source;
private final String ssoToken;
private final String apiKeyId;

private Request(final RequestBuilder requestBuilder) {
this.params = requestBuilder.params;
this.remoteAddress = requestBuilder.remoteAddress;
this.source = requestBuilder.source;
this.ssoToken = requestBuilder.ssoToken;
this.apiKeyId = requestBuilder.apiKeyId;
}

public String getRemoteAddress() {
Expand All @@ -376,6 +390,10 @@ public String getSsoToken() {
return ssoToken;
}

public String getApiKeyId() {
return apiKeyId;
}

public boolean hasParam(final String key) {
final String value = params.get(key);
return value != null && value.length() > 0;
Expand Down Expand Up @@ -422,6 +440,7 @@ static class RequestBuilder {
private String remoteAddress;
private String source;
private String ssoToken;
private String apiKeyId;

public RequestBuilder setData(final String data) {
params.put(Command.DATA, data);
Expand Down Expand Up @@ -463,6 +482,11 @@ public RequestBuilder setSsoToken(final String ssoToken) {
return this;
}

public RequestBuilder setApiKeyId(final String apiKeyId) {
this.apiKeyId = apiKeyId;
return this;
}

public Request build() {
return new Request(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.core.Response;
import net.ripe.db.whois.api.UpdatesParser;
import net.ripe.db.whois.api.apiKey.BearerTokenExtractor;
import net.ripe.db.whois.common.DateTimeProvider;
import net.ripe.db.whois.common.domain.IpRanges;
import net.ripe.db.whois.common.source.Source;
Expand Down Expand Up @@ -57,6 +58,8 @@ public class SyncUpdatesServiceTest {
@Mock LoggerContext loggerContext;
@Mock SourceContext sourceContext;
@Mock SsoTokenTranslator ssoTokenTranslator;
@Mock
BearerTokenExtractor bearerTokenExtractor;

@InjectMocks SyncUpdatesService subject;

Expand All @@ -80,7 +83,7 @@ public void handle_no_parameters() {
final String contentType = "UTF-8";
final String ssoToken = null;

final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);

assertThat(response.getStatus(), is(HttpURLConnection.HTTP_OK));
assertThat(response.getEntity().toString(), is("OK"));
Expand All @@ -97,7 +100,7 @@ public void handle_only_new_parameter() {
final String contentType = "UTF-8";
final String ssoToken = null;

final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);

assertThat(response.getStatus(), is(HttpURLConnection.HTTP_BAD_REQUEST));
assertThat(response.getEntity().toString(), is("DATA parameter is missing"));
Expand All @@ -114,7 +117,7 @@ public void handle_only_diff_parameter() {
final String contentType = "UTF-8";
final String ssoToken = null;

final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);

assertThat(response.getStatus(), is(HttpURLConnection.HTTP_BAD_REQUEST));
assertThat(response.getEntity().toString(), is("the DIFF method is not actually supported by the Syncupdates interface"));
Expand All @@ -133,7 +136,7 @@ public void handle_only_data_parameter() {
final String contentType = "UTF-8";
final String ssoToken = null;

final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);

assertThat(response.getStatus(), is(HttpURLConnection.HTTP_OK));
assertThat(response.getEntity().toString(), is("OK"));
Expand All @@ -151,7 +154,7 @@ public void handle_unauthorised() {
final String ssoToken = null;

when(messageHandler.handle(any(UpdateRequest.class), any(UpdateContext.class))).thenReturn(new UpdateResponse(UpdateStatus.FAILED_AUTHENTICATION, "FAILED"));
final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);

assertThat(response.getStatus(), is(HttpURLConnection.HTTP_UNAUTHORIZED));
assertThat(response.getEntity().toString(), is("FAILED"));
Expand All @@ -168,7 +171,7 @@ public void handle_diff_and_data_parameters() {
final String contentType = "UTF-8";
final String ssoToken = null;

final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);

assertThat(response.getStatus(), is(HttpURLConnection.HTTP_BAD_REQUEST));
assertThat(response.getEntity().toString(), is("the DIFF method is not actually supported by the Syncupdates interface"));
Expand All @@ -189,7 +192,7 @@ public void throw_illegal_argument_exception() {
doThrow(new IllegalArgumentException("some message")).
when(messageHandler).handle(any(UpdateRequest.class), any(UpdateContext.class));

subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);
fail();
} catch (RuntimeException e) {
assertThat(e.getMessage(), is("some message"));
Expand All @@ -211,7 +214,7 @@ public void throw_runtime_exception() {
doThrow(new RuntimeException("some message", new IllegalStateException("some message"))).
when(messageHandler).handle(any(UpdateRequest.class), any(UpdateContext.class));

subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);
fail();
} catch (RuntimeException e) {
assertThat(e.getMessage(), is("some message"));
Expand All @@ -231,7 +234,7 @@ public void handle_invalid_encoding() {
final String contentType = "text/plain; charset=RGRFE";
final String ssoToken = null;

final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);

assertThat(response.getStatus(), is(HttpURLConnection.HTTP_OK));
assertThat(response.getEntity().toString(), is("OK"));
Expand All @@ -250,7 +253,7 @@ public void handle_invalid_content_type() {
final String contentType = "invalid";
final String ssoToken = null;

final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);

assertThat(response.getStatus(), is(HttpURLConnection.HTTP_OK));
assertThat(response.getEntity().toString(), is("OK"));
Expand All @@ -269,7 +272,7 @@ public void handle_redirect_allowed() {
final String contentType = "UTF-8";
final String ssoToken = null;

final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);

assertThat(response.getStatus(), is(HttpURLConnection.HTTP_OK));
assertThat(response.getEntity().toString(), is("OK"));
Expand All @@ -288,7 +291,7 @@ public void handle_redirect_is_ignored() {
final String contentType = "UTF-8";
final String ssoToken = null;

final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
final Response response = subject.doGet(request, source, data, help, nnew, diff, redirect, contentType, null, ssoToken);

assertThat(response.getStatus(), is(HttpURLConnection.HTTP_OK));
assertThat(response.getEntity().toString(), is("OK"));
Expand Down Expand Up @@ -316,7 +319,7 @@ public void handle_multipart_post() {
final String ssoToken = "valid-token";
final String contentType = "charset=\"latin1\"";

subject.doMultipartPost(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
subject.doMultipartPost(request, source, data, help, nnew, diff, redirect, null, contentType, ssoToken);

verify(messageHandler).handle(
argThat(updateRequest -> {
Expand Down Expand Up @@ -351,7 +354,7 @@ public void handle_multipart_post_invalid_sso_token() {
final String ssoToken = "invalid-token";
final String contentType = "charset=\"latin1\"";

subject.doMultipartPost(request, source, data, help, nnew, diff, redirect, contentType, ssoToken);
subject.doMultipartPost(request, source, data, help, nnew, diff, redirect, null, contentType, ssoToken);

verify(messageHandler).handle(
argThat(updateRequest -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import net.ripe.db.whois.api.rest.domain.WhoisResources;
import net.ripe.db.whois.api.rest.mapper.FormattedClientAttributeMapper;
import net.ripe.db.whois.api.rest.mapper.WhoisObjectMapper;
import net.ripe.db.whois.api.syncupdate.SyncUpdateUtils;
import net.ripe.db.whois.common.apiKey.ApiKeyUtils;
import net.ripe.db.whois.common.rpsl.AttributeType;
import net.ripe.db.whois.common.rpsl.RpslAttribute;
Expand Down Expand Up @@ -193,6 +194,64 @@ public void request_failed_with_basic_auth_api_key_illegal_bearer_header() {
assertThat(response.getStatus(), is(HttpStatus.BAD_REQUEST_400));
}

@Test
public void update_selfrefencing_maintainer_only_data_parameter_with_api_key() {
final String mntner =
"mntner: SSO-MNT\n" +
"descr: description\n" +
"admin-c: TP1-TEST\n" +
"upd-to: [email protected]\n" +
"auth: SSO [email protected]\n" +
"mnt-by: SSO-MNT\n" +
"source: TEST";
databaseHelper.addObject(mntner);

final String response = SecureRestTest.target(getSecurePort(), "whois/syncupdates/test?" +
"DATA=" + SyncUpdateUtils.encode(mntner + "\nremarks: updated"))
.request()
.header(HttpHeaders.AUTHORIZATION, getBasicAuthHeader(BASIC_AUTH_PERSON_NO_MNT))
.get(String.class);

assertThat(response, containsString("Modify SUCCEEDED: [mntner] SSO-MNT"));
}

@Test
public void create_mntner_only_data_parameter_with_apiKey() {
final String mntner =
"mntner: SSO-MNT\n" +
"descr: description\n" +
"admin-c: TP1-TEST\n" +
"upd-to: [email protected]\n" +
"auth: SSO [email protected]\n" +
"mnt-by: SSO-MNT\n" +
"source: TEST";

final String response = SecureRestTest.target(getSecurePort(), "whois/syncupdates/test?" + "DATA=" + SyncUpdateUtils.encode(mntner))
.request()
.header(HttpHeaders.AUTHORIZATION, getBasicAuthHeader(BASIC_AUTH_PERSON_NO_MNT))
.get(String.class);

assertThat(response, containsString("Create SUCCEEDED: [mntner] SSO-MNT"));
}

@Test
public void create_mntner_only_data_parameter_with_apiKey_fails_no_sso() {
final String mntner =
"mntner: SSO-MNT\n" +
"descr: description\n" +
"admin-c: TP1-TEST\n" +
"upd-to: [email protected]\n" +
"auth: SSO [email protected]\n" +
"mnt-by: SSO-MNT\n" +
"source: TEST";

final String response = SecureRestTest.target(getSecurePort(), "whois/syncupdates/test?" + "DATA=" + SyncUpdateUtils.encode(mntner))
.request()
.header(HttpHeaders.AUTHORIZATION, getBasicAuthHeader(BASIC_AUTH_TEST_TEST_MNT))
.get(String.class);

assertThat(response, containsString("Create FAILED: [mntner] SSO-MNT"));
}

@Test
public void lookup_correct_api_key_with_sso_and_unfiltered() {
Expand Down Expand Up @@ -772,7 +831,7 @@ private WhoisResources map(final RpslObject ... rpslObjects) {
return whoisObjectMapper.mapRpslObjects(FormattedClientAttributeMapper.class, rpslObjects);
}

private static String getBasicAuthHeader(final String basicAuth) {
public static String getBasicAuthHeader(final String basicAuth) {
return StringUtils.joinWith(" ","Basic ", basicAuth);
}

Expand Down

0 comments on commit fcd55cc

Please sign in to comment.