Add an HTTP header to response from Nomulus after successful login (#879)

* Add a logged-in response header

* small fixes

* Refactor EPP test cases to check for headers

* small change
This commit is contained in:
sarahcaseybot 2020-12-01 19:24:56 -05:00 committed by GitHub
parent f92819243d
commit b0fad6c87b
7 changed files with 96 additions and 12 deletions

View file

@ -74,6 +74,14 @@ public class EppRequestHandler {
&& eppOutput.getResponse().getResult().getCode() == SUCCESS_AND_CLOSE) { && eppOutput.getResponse().getResult().getCode() == SUCCESS_AND_CLOSE) {
response.setHeader("Epp-Session", "close"); response.setHeader("Epp-Session", "close");
} }
// If a login request returns a success, a logged-in header is added to the response to inform
// the proxy that it is no longer necessary to send the full client certificate to the backend
// for this connection.
if (eppOutput.isResponse()
&& eppOutput.getResponse().isLoginResponse()
&& eppOutput.isSuccess()) {
response.setHeader("Logged-In", "true");
}
} catch (Exception e) { } catch (Exception e) {
logger.atWarning().withCause(e).log("handleEppCommand general exception"); logger.atWarning().withCause(e).log("handleEppCommand general exception");
response.setStatus(SC_BAD_REQUEST); response.setStatus(SC_BAD_REQUEST);

View file

@ -141,7 +141,7 @@ public class LoginFlow implements Flow {
sessionMetadata.resetFailedLoginAttempts(); sessionMetadata.resetFailedLoginAttempts();
sessionMetadata.setClientId(login.getClientId()); sessionMetadata.setClientId(login.getClientId());
sessionMetadata.setServiceExtensionUris(serviceExtensionUrisBuilder.build()); sessionMetadata.setServiceExtensionUris(serviceExtensionUrisBuilder.build());
return responseBuilder.build(); return responseBuilder.setIsLoginResponse().build();
} }
/** Registrar with this client ID could not be found. */ /** Registrar with this client ID could not be found. */

View file

@ -65,6 +65,7 @@ import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementRef; import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlElementRefs; import javax.xml.bind.annotation.XmlElementRefs;
import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType; import javax.xml.bind.annotation.XmlType;
/** /**
@ -87,6 +88,9 @@ public class EppResponse extends ImmutableObject implements ResponseOrGreeting {
/** The command result. The RFC allows multiple failure results, but we always return one. */ /** The command result. The RFC allows multiple failure results, but we always return one. */
Result result; Result result;
/** Indicates if this response is for a login request. */
@XmlTransient boolean isLoginResponse = false;
/** /**
* Information about messages queued for retrieval. This may appear in response to any EPP message * Information about messages queued for retrieval. This may appear in response to any EPP message
* (if messages are queued), but in practice this will only be set in response to a poll request. * (if messages are queued), but in practice this will only be set in response to a poll request.
@ -178,6 +182,10 @@ public class EppResponse extends ImmutableObject implements ResponseOrGreeting {
return result; return result;
} }
public boolean isLoginResponse() {
return isLoginResponse;
}
/** Marker interface for types that can go in the {@link #resData} field. */ /** Marker interface for types that can go in the {@link #resData} field. */
public interface ResponseData {} public interface ResponseData {}
@ -222,5 +230,10 @@ public class EppResponse extends ImmutableObject implements ResponseOrGreeting {
getInstance().extensions = forceEmptyToNull(extensions); getInstance().extensions = forceEmptyToNull(extensions);
return this; return this;
} }
public Builder setIsLoginResponse() {
getInstance().isLoginResponse = true;
return this;
}
} }
} }

View file

@ -501,7 +501,7 @@ class EppLifecycleDomainTest extends EppTestCase {
@Test @Test
void testEapDomainDeletion_withinAddGracePeriod_eapFeeIsNotRefunded() throws Exception { void testEapDomainDeletion_withinAddGracePeriod_eapFeeIsNotRefunded() throws Exception {
assertThatCommand("login_valid_fee_extension.xml").hasResponse("generic_success_response.xml"); assertThatCommand("login_valid_fee_extension.xml").hasSuccessfulLogin();
createContacts(DateTime.parse("2000-06-01T00:00:00Z")); createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Set the EAP schedule. // Set the EAP schedule.
@ -718,7 +718,7 @@ class EppLifecycleDomainTest extends EppTestCase {
START_OF_TIME, PREDELEGATION, START_OF_TIME, PREDELEGATION,
gaDate, GENERAL_AVAILABILITY)); gaDate, GENERAL_AVAILABILITY));
assertThatCommand("login_valid_fee_extension.xml").hasResponse("generic_success_response.xml"); assertThatCommand("login_valid_fee_extension.xml").hasSuccessfulLogin();
assertThatCommand("domain_check_fee_premium.xml") assertThatCommand("domain_check_fee_premium.xml")
.atTime(gaDate.plusDays(1)) .atTime(gaDate.plusDays(1))
@ -1196,7 +1196,7 @@ class EppLifecycleDomainTest extends EppTestCase {
assertThatLogin("NewRegistrar", "foo-BAR2") assertThatLogin("NewRegistrar", "foo-BAR2")
.atTime(sunriseDate.minusDays(3)) .atTime(sunriseDate.minusDays(3))
.hasResponse("generic_success_response.xml"); .hasSuccessfulLogin();
createContactsAndHosts(); createContactsAndHosts();
@ -1292,7 +1292,7 @@ class EppLifecycleDomainTest extends EppTestCase {
assertThatLogin("NewRegistrar", "foo-BAR2") assertThatLogin("NewRegistrar", "foo-BAR2")
.atTime(sunriseDate.minusDays(3)) .atTime(sunriseDate.minusDays(3))
.hasResponse("generic_success_response.xml"); .hasSuccessfulLogin();
createContactsAndHosts(); createContactsAndHosts();

View file

@ -121,6 +121,10 @@ public class EppTestCase {
return assertCommandAndResponse( return assertCommandAndResponse(
inputFilename, inputSubstitutions, outputFilename, outputSubstitutions, now); inputFilename, inputSubstitutions, outputFilename, outputSubstitutions, now);
} }
public String hasSuccessfulLogin() throws Exception {
return assertLoginCommandAndResponse(inputFilename, inputSubstitutions, null, now);
}
} }
protected CommandAsserter assertThatCommand(String inputFilename) { protected CommandAsserter assertThatCommand(String inputFilename) {
@ -137,13 +141,33 @@ public class EppTestCase {
} }
protected void assertThatLoginSucceeds(String clientId, String password) throws Exception { protected void assertThatLoginSucceeds(String clientId, String password) throws Exception {
assertThatLogin(clientId, password).hasResponse("generic_success_response.xml"); assertThatLogin(clientId, password).hasSuccessfulLogin();
} }
protected void assertThatLogoutSucceeds() throws Exception { protected void assertThatLogoutSucceeds() throws Exception {
assertThatCommand("logout.xml").hasResponse("logout_response.xml"); assertThatCommand("logout.xml").hasResponse("logout_response.xml");
} }
private String assertLoginCommandAndResponse(
String inputFilename,
@Nullable Map<String, String> inputSubstitutions,
@Nullable Map<String, String> outputSubstitutions,
DateTime now)
throws Exception {
String outputFilename = "generic_success_response.xml";
clock.setTo(now);
String input = loadFile(EppTestCase.class, inputFilename, inputSubstitutions);
String expectedOutput = loadFile(EppTestCase.class, outputFilename, outputSubstitutions);
setUpSession();
FakeResponse response = executeXmlCommand(input);
// Check that the logged-in header was added to the response
assertThat(response.getHeaders()).isEqualTo(ImmutableMap.of("Logged-In", "true"));
return verifyAndReturnOutput(
response.getPayload(), expectedOutput, inputFilename, outputFilename);
}
private String assertCommandAndResponse( private String assertCommandAndResponse(
String inputFilename, String inputFilename,
@Nullable Map<String, String> inputSubstitutions, @Nullable Map<String, String> inputSubstitutions,
@ -154,6 +178,18 @@ public class EppTestCase {
clock.setTo(now); clock.setTo(now);
String input = loadFile(EppTestCase.class, inputFilename, inputSubstitutions); String input = loadFile(EppTestCase.class, inputFilename, inputSubstitutions);
String expectedOutput = loadFile(EppTestCase.class, outputFilename, outputSubstitutions); String expectedOutput = loadFile(EppTestCase.class, outputFilename, outputSubstitutions);
setUpSession();
FakeResponse response = executeXmlCommand(input);
// Checks that the Logged-In header is not in the response. If testing the login command, use
// assertLoginCommandAndResponse instead of this method.
assertThat(response.getHeaders()).doesNotContainEntry("Logged-In", "true");
return verifyAndReturnOutput(
response.getPayload(), expectedOutput, inputFilename, outputFilename);
}
private void setUpSession() {
if (sessionMetadata == null) { if (sessionMetadata == null) {
sessionMetadata = sessionMetadata =
new HttpSessionMetadata(new FakeHttpSession()) { new HttpSessionMetadata(new FakeHttpSession()) {
@ -165,7 +201,13 @@ public class EppTestCase {
} }
}; };
} }
String actualOutput = executeXmlCommand(input); }
private String verifyAndReturnOutput(
String actualOutput, String expectedOutput, String inputFilename, String outputFilename)
throws Exception {
// Run the resulting xml through the unmarshaller to verify that it was valid.
EppXmlTransformer.validateOutput(actualOutput);
assertXmlEqualsWithMessage( assertXmlEqualsWithMessage(
expectedOutput, expectedOutput,
actualOutput, actualOutput,
@ -176,7 +218,7 @@ public class EppTestCase {
return actualOutput; return actualOutput;
} }
private String executeXmlCommand(String inputXml) throws Exception { private FakeResponse executeXmlCommand(String inputXml) throws Exception {
EppRequestHandler handler = new EppRequestHandler(); EppRequestHandler handler = new EppRequestHandler();
FakeResponse response = new FakeResponse(); FakeResponse response = new FakeResponse();
handler.response = response; handler.response = response;
@ -195,10 +237,7 @@ public class EppTestCase {
inputXml.getBytes(UTF_8)); inputXml.getBytes(UTF_8));
assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getContentType()).isEqualTo(APPLICATION_EPP_XML_UTF8); assertThat(response.getContentType()).isEqualTo(APPLICATION_EPP_XML_UTF8);
String result = response.getPayload(); return response;
// Run the resulting xml through the unmarshaller to verify that it was valid.
EppXmlTransformer.validateOutput(result);
return result;
} }
EppMetric getRecordedEppMetric() { EppMetric getRecordedEppMetric() {

View file

@ -14,6 +14,7 @@
package google.registry.flows.session; package google.registry.flows.session;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.deleteResource; import static google.registry.testing.DatabaseHelper.deleteResource;
import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistResource;
@ -32,6 +33,7 @@ import google.registry.flows.session.LoginFlow.PasswordChangesNotSupportedExcept
import google.registry.flows.session.LoginFlow.RegistrarAccountNotActiveException; import google.registry.flows.session.LoginFlow.RegistrarAccountNotActiveException;
import google.registry.flows.session.LoginFlow.TooManyFailedLoginsException; import google.registry.flows.session.LoginFlow.TooManyFailedLoginsException;
import google.registry.flows.session.LoginFlow.UnsupportedLanguageException; import google.registry.flows.session.LoginFlow.UnsupportedLanguageException;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.registrar.Registrar; import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State; import google.registry.model.registrar.Registrar.State;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -74,6 +76,14 @@ public abstract class LoginFlowTestCase extends FlowTestCase<LoginFlow> {
doSuccessfulTest("login_valid.xml"); doSuccessfulTest("login_valid.xml");
} }
@Test
void testSuccess_setsIsLoginResponse() throws Exception {
setEppInput("login_valid.xml");
assertTransactionalFlow(false);
EppOutput output = runFlow();
assertThat(output.getResponse().isLoginResponse()).isTrue();
}
@Test @Test
void testSuccess_suspendedRegistrar() throws Exception { void testSuccess_suspendedRegistrar() throws Exception {
persistResource(getRegistrarBuilder().setState(State.SUSPENDED).build()); persistResource(getRegistrarBuilder().setState(State.SUSPENDED).build());

View file

@ -249,6 +249,20 @@ class EppServiceHandlerTest {
assertThat(channel.isActive()).isFalse(); assertThat(channel.isActive()).isFalse();
} }
@Test
void sendResponseToNextHandler_unrecognizedHeader() throws Exception {
setHandshakeSuccess();
String content = "<epp>stuff</epp>";
HttpResponse response = makeEppHttpResponse(content, HttpResponseStatus.OK);
response.headers().set("unrecognized-header", "test");
channel.writeOutbound(response);
ByteBuf expectedResponse = channel.readOutbound();
assertThat(Unpooled.wrappedBuffer(content.getBytes(UTF_8))).isEqualTo(expectedResponse);
// Nothing further to pass to the next handler.
assertThat((Object) channel.readOutbound()).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test @Test
void testFailure_disconnectOnNonOKResponseStatus() throws Exception { void testFailure_disconnectOnNonOKResponseStatus() throws Exception {
setHandshakeSuccess(); setHandshakeSuccess();