mv com/google/domain/registry google/registry

This change renames directories in preparation for the great package
rename. The repository is now in a broken state because the code
itself hasn't been updated. However this should ensure that git
correctly preserves history for each file.
This commit is contained in:
Justine Tunney 2016-05-13 18:55:08 -04:00
parent a41677aea1
commit 5012893c1d
2396 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,49 @@
package(
default_visibility = ["//java/com/google/domain/registry:registry_project"],
)
filegroup(
name = "flows_files",
srcs = glob([
"*.java",
"**/*.java",
]),
)
java_library(
name = "flows",
srcs = glob([
"*.java",
"**/*.java",
]),
deps = [
"//java/com/google/common/annotations",
"//java/com/google/common/base",
"//java/com/google/common/collect",
"//java/com/google/common/io",
"//java/com/google/common/net",
"//java/com/google/domain/registry/config",
"//java/com/google/domain/registry/dns",
"//java/com/google/domain/registry/mapreduce",
"//java/com/google/domain/registry/mapreduce/inputs",
"//java/com/google/domain/registry/model",
"//java/com/google/domain/registry/monitoring/whitebox",
"//java/com/google/domain/registry/request",
"//java/com/google/domain/registry/security:servlets",
"//java/com/google/domain/registry/tldconfig/idn",
"//java/com/google/domain/registry/tmch",
"//java/com/google/domain/registry/util",
"//java/com/google/domain/registry/xml",
"//third_party/java/appengine:appengine-api",
"//third_party/java/appengine_mapreduce2:appengine_mapreduce",
"//third_party/java/dagger",
"//third_party/java/joda_money",
"//third_party/java/joda_time",
"//third_party/java/jsr305_annotations",
"//third_party/java/jsr330_inject",
"//third_party/java/objectify:objectify-v4_1",
"//third_party/java/servlet/servlet_api",
],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,119 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.appengine.api.users.UserServiceFactory.getUserService;
import static com.google.common.base.Strings.nullToEmpty;
import static com.google.domain.registry.flows.EppServletUtils.handleEppCommandAndWriteResponse;
import static java.lang.System.identityHashCode;
import com.google.appengine.api.users.User;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteStreams;
import com.google.domain.registry.flows.EppException.AuthenticationErrorException;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.model.registrar.RegistrarContact;
import com.google.domain.registry.security.XsrfProtectedServlet;
import org.joda.time.Duration;
import java.io.IOException;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** The {@link EppConsoleServlet} runs EPP from the console. It requires GAE user authentication. */
public class EppConsoleServlet extends XsrfProtectedServlet {
/**
* Credentials provided by the GAE User service.
*
* @see com.google.appengine.api.users.UserService
*/
public static final class GaeUserCredentials implements TransportCredentials {
/** User is not logged in as a GAE user. */
public static class UserNotLoggedInException extends AuthenticationErrorException {
public UserNotLoggedInException() {
super("User is not logged in");
}
}
/** GAE user id is not allowed to login as requested registrar. */
public static class BadGaeUserIdException extends AuthenticationErrorException {
public BadGaeUserIdException(User user) {
super(
"User id is not allowed to login as requested registrar: "
+ (nullToEmpty(user.getEmail())));
}
}
final User gaeUser;
@VisibleForTesting
public GaeUserCredentials(@Nullable User gaeUser) {
this.gaeUser = gaeUser;
}
@Override
public boolean performsLoginCheck() {
return true;
}
@Override
public void validate(Registrar r) throws AuthenticationErrorException {
if (gaeUser == null) {
throw new UserNotLoggedInException();
}
// Allow admins to act as any registrar.
if (getUserService().isUserAdmin()) {
return;
}
// Check Registrar's contacts to see if any are associated with this gaeUserId.
final String gaeUserId = gaeUser.getUserId();
for (RegistrarContact rc : r.getContacts()) {
if (gaeUserId.equals(rc.getGaeUserId())) {
return;
}
}
throw new BadGaeUserIdException(gaeUser);
}
@Override
public String toString() {
return String.format("GaeUserCredentials@%s{gaeUser: %s}", identityHashCode(this), gaeUser);
}
}
/** Used by related UI servlets to generate matching XSRF tokens. */
public static final String XSRF_SCOPE = "console";
/** How long generated XSRF tokens for this scope remain valid. */
public static final Duration XSRF_LIFETIME = Duration.standardDays(1);
public EppConsoleServlet() {
super(XSRF_SCOPE, false);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
handleEppCommandAndWriteResponse(
ByteStreams.toByteArray(req.getInputStream()),
rsp,
new HttpSessionMetadata(
new GaeUserCredentials(getUserService().getCurrentUser()), req.getSession(true)));
}
}

View file

@ -0,0 +1,114 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.EppXmlTransformer.marshalWithLenientRetry;
import static com.google.domain.registry.flows.EppXmlTransformer.unmarshal;
import static com.google.domain.registry.flows.FlowRegistry.getFlowClass;
import com.google.apphosting.api.ApiProxy;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.flows.FlowRunner.CommitMode;
import com.google.domain.registry.flows.FlowRunner.UserPrivileges;
import com.google.domain.registry.model.eppcommon.Trid;
import com.google.domain.registry.model.eppinput.EppInput;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.eppoutput.Response;
import com.google.domain.registry.model.eppoutput.Result;
import com.google.domain.registry.model.eppoutput.Result.Code;
import com.google.domain.registry.monitoring.whitebox.EppMetrics;
import com.google.domain.registry.util.Clock;
import com.google.domain.registry.util.FormattingLogger;
import com.google.domain.registry.util.SystemClock;
/**
* The EppController class, which implements the state machine for the EPP command/response
* protocol.
*
* @see "http://tools.ietf.org/html/rfc5730"
*/
public final class EppController {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
private static final Clock clock = new SystemClock();
/**
* Read an EPP envelope from the client, find the matching flow, execute it, and return
* the response marshalled to a byte array.
*/
public static byte[] handleEppCommand(byte[] inputXmlBytes, SessionMetadata sessionMetadata) {
Trid trid = null;
EppMetrics metrics = new EppMetrics();
metrics.setRequestId(
ApiProxy.getCurrentEnvironment().getAttributes().get(
"com.google.appengine.runtime.request_log_id").toString());
try {
EppInput eppInput = unmarshal(inputXmlBytes);
trid = Trid.create(eppInput.getCommandWrapper().getClTrid());
ImmutableList<String> targetIds = eppInput.getTargetIds();
metrics.setCommandName(eppInput.getCommandName());
metrics.setClientId(sessionMetadata.getClientId());
metrics.setPrivilegeLevel(
sessionMetadata.isSuperuser()
? UserPrivileges.SUPERUSER.toString()
: UserPrivileges.NORMAL.toString());
if (!targetIds.isEmpty()) {
metrics.setEppTarget(Joiner.on(",").join(targetIds));
}
FlowRunner flowRunner = new FlowRunner(
getFlowClass(eppInput),
eppInput,
trid,
sessionMetadata,
inputXmlBytes,
metrics);
EppOutput eppOutput = flowRunner.run(
sessionMetadata.isDryRun() ? CommitMode.DRY_RUN : CommitMode.LIVE,
sessionMetadata.isSuperuser() ? UserPrivileges.SUPERUSER : UserPrivileges.NORMAL);
if (eppOutput.isResponse()) {
metrics.setEppStatus(eppOutput.getResponse().getResult().getCode());
}
return marshalWithLenientRetry(eppOutput);
} catch (EppException e) {
// The command failed. Send the client an error message.
metrics.setEppStatus(e.getResult().getCode());
return marshalWithLenientRetry(getErrorResponse(e.getResult(), trid));
} catch (Throwable e) {
// Something bad and unexpected happened. Send the client a generic error, and log it.
logger.severe(e, "Unexpected failure");
metrics.setEppStatus(Code.CommandFailed);
return marshalWithLenientRetry(getErrorResponse(Result.create(Code.CommandFailed), trid));
} finally {
metrics.export();
}
}
/** Create a response indicating an Epp failure. */
@VisibleForTesting
static EppOutput getErrorResponse(Result result, Trid trid) {
// Create TRID (without a clTRID) if one hasn't been created yet, as it's necessary to construct
// a valid response. This can happen if the error occurred before we could even parse out the
// clTRID (e.g. if a syntax error occurred parsing the supplied XML).
return EppOutput.create(new Response.Builder()
.setTrid(trid == null ? Trid.create(null) : trid)
.setResult(result)
.setExecutionTime(clock.nowUtc())
.build());
}
}

View file

@ -0,0 +1,240 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.google.common.base.Preconditions;
import com.google.domain.registry.model.annotations.ExternalMessagingName;
import com.google.domain.registry.model.eppinput.EppInput.InnerCommand;
import com.google.domain.registry.model.eppinput.ResourceCommand;
import com.google.domain.registry.model.eppoutput.Result;
import com.google.domain.registry.model.eppoutput.Result.Code;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/** Exception used to propagate all failures containing one or more EPP responses. */
public abstract class EppException extends Exception {
private final Result result;
/** Create an EppException with a custom message. */
private EppException(String message) {
super(message);
Code code = getClass().getAnnotation(EppResultCode.class).value();
Preconditions.checkState(!code.isSuccess());
this.result = Result.create(code, message);
}
/** Create an EppException with the default message for this code. */
private EppException() {
this(null);
}
public Result getResult() {
return result;
}
/** Annotation for associating an EPP Result.Code value with an EppException subclass. */
@Documented
@Inherited
@Retention(RUNTIME)
@Target(TYPE)
public @interface EppResultCode {
/** The Code value associated with this exception. */
Code value();
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.AuthenticationError)
public abstract static class AuthenticationErrorException extends EppException {
public AuthenticationErrorException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.AuthenticationErrorClosingConnection)
public abstract static class AuthenticationErrorClosingConnectionException extends EppException {
public AuthenticationErrorClosingConnectionException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.AuthorizationError)
public abstract static class AuthorizationErrorException extends EppException {
public AuthorizationErrorException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.InvalidAuthorizationInformationError)
public abstract static class InvalidAuthorizationInformationErrorException extends EppException {
public InvalidAuthorizationInformationErrorException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.CommandUseError)
public abstract static class CommandUseErrorException extends EppException {
public CommandUseErrorException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.ObjectExists)
public abstract static class ObjectAlreadyExistsException extends EppException {
public ObjectAlreadyExistsException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.ObjectDoesNotExist)
public abstract static class ObjectDoesNotExistException extends EppException {
public ObjectDoesNotExistException(Class<?> type, String id) {
super(
String.format(
"The %s with given ID (%s) doesn't exist.",
!type.isAnnotationPresent(ExternalMessagingName.class)
? "object"
: type.getAnnotation(ExternalMessagingName.class).value(),
id));
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.ObjectPendingTransfer)
public abstract static class ObjectPendingTransferException extends EppException {
public ObjectPendingTransferException(String id) {
super(String.format("Object with given ID (%s) already has a pending transfer.", id));
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.ObjectNotPendingTransfer)
public abstract static class ObjectNotPendingTransferException extends EppException {
public ObjectNotPendingTransferException(String id) {
super(String.format("Object with given ID (%s) does not have a pending transfer.", id));
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.AssociationProhibitsOperation)
public abstract static class AssociationProhibitsOperationException extends EppException {
public AssociationProhibitsOperationException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.ParameterValuePolicyError)
public abstract static class ParameterValuePolicyErrorException extends EppException {
public ParameterValuePolicyErrorException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.ParameterValueRangeError)
public abstract static class ParameterValueRangeErrorException extends EppException {
public ParameterValueRangeErrorException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.ParameterValueSyntaxError)
public abstract static class ParameterValueSyntaxErrorException extends EppException {
public ParameterValueSyntaxErrorException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.RequiredParameterMissing)
public abstract static class RequiredParameterMissingException extends EppException {
public RequiredParameterMissingException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.StatusProhibitsOperation)
public abstract static class StatusProhibitsOperationException extends EppException {
public StatusProhibitsOperationException(String message) {
super(message);
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.SyntaxError)
public abstract static class SyntaxErrorException extends EppException {
public SyntaxErrorException(String message) {
super(message);
}
}
/** Specified command is not implemented. */
@EppResultCode(Code.UnimplementedCommand)
public static class UnimplementedCommandException extends EppException {
public UnimplementedCommandException(InnerCommand command, ResourceCommand resourceCommand) {
super(String.format(
"No flow found for %s with extension %s",
command.getClass().getSimpleName(),
resourceCommand == null ? null : resourceCommand.getClass().getSimpleName()));
}
}
/** Abstract exception class. Do not throw this directly or catch in tests. */
@EppResultCode(Code.UnimplementedOption)
public abstract static class UnimplementedOptionException extends EppException {
public UnimplementedOptionException(String message) {
super(message);
}
}
/** Specified extension is not implemented. */
@EppResultCode(Code.UnimplementedExtension)
public static class UnimplementedExtensionException extends EppException {
public UnimplementedExtensionException() {
super("Specified extension is not implemented");
}
}
/** Specified object service is not implemented. */
@EppResultCode(Code.UnimplementedObjectService)
public static class UnimplementedObjectServiceException extends EppException {
public UnimplementedObjectServiceException() {
super("Specified object service is not implemented");
}
}
/** Specified protocol version is not implemented. */
@EppResultCode(Code.UnimplementedProtocolVersion)
public static class UnimplementedProtocolVersionException extends EppException {
public UnimplementedProtocolVersionException() {
super("Specified protocol version is not implemented");
}
}
}

View file

@ -0,0 +1,67 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.EppController.handleEppCommand;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.net.MediaType;
import com.google.domain.registry.util.FormattingLogger;
import java.io.IOException;
import java.io.OutputStream;
import javax.servlet.http.HttpServletResponse;
/** Utility methods for Epp servlet classes. */
public final class EppServletUtils {
public static final MediaType APPLICATION_EPP_XML_UTF8 =
MediaType.create("application", "epp+xml").withCharset(UTF_8);
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
/**
* Handle an EPP request and write out a servlet response.
*
* @throws IOException upon failure writing to {@code rsp}
*/
static void handleEppCommandAndWriteResponse(
byte[] inputXmlBytes, HttpServletResponse rsp, SessionMetadata sessionMetadata)
throws IOException {
byte[] response;
try {
response = handleEppCommand(inputXmlBytes, sessionMetadata);
} catch (Exception e) {
logger.warning(e, "handleEppCommand general exception");
rsp.setStatus(SC_BAD_REQUEST);
return;
}
// Note that we always return 200 (OK) even if the EppController returns an error response.
// This is because returning an non-OK HTTP status code will cause the proxy server to
// silently close the connection without returning any data. The only time we will ever return
// a non-OK status (400) is if we fail to muster even an EPP error response message. In that
// case it's better to close the connection than to return garbage.
rsp.setStatus(SC_OK);
rsp.setContentType(APPLICATION_EPP_XML_UTF8.toString());
try (OutputStream output = rsp.getOutputStream()) {
output.write(response);
}
}
private EppServletUtils() {}
}

View file

@ -0,0 +1,72 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.common.io.ByteStreams.toByteArray;
import static com.google.domain.registry.flows.EppServletUtils.handleEppCommandAndWriteResponse;
import com.google.domain.registry.util.FormattingLogger;
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* The {@link EppTlsServlet} class establishes a transport for EPP+TLS over* HTTP. All commands and
* responses are EPP XML according to RFC 5730. Commands must must requested via POST.
* <p>
* There are a number of expected headers to this endpoint:
* <dl>
* <dt>{@value #SSL_CLIENT_CERTIFICATE_HASH_FIELD}
* <dd>
* This field should contain a base64 encoded digest of the client's TLS certificate. It is
* validated during an EPP login command against a known good value that is transmitted out of
* band.
* <dt>{@value #FORWARDED_FOR_FIELD}
* <dd>
* This field should contain the host and port of the connecting client. It is validated during
* an EPP login command against an IP whitelist that is transmitted out of band.
* <dt>{@value #REQUESTED_SERVERNAME_VIA_SNI_FIELD}
* <dd>
* This field should contain the servername that the client requested during the TLS handshake.
* It is unused, but expected to be present in the GFE-proxied configuration.
* </dl>
*/
public class EppTlsServlet extends HttpServlet {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
static final String REQUESTED_SERVERNAME_VIA_SNI_FIELD = "X-GFE-Requested-Servername-SNI";
static final String FORWARDED_FOR_FIELD = "X-Forwarded-For";
static final String SSL_CLIENT_CERTIFICATE_HASH_FIELD = "X-GFE-SSL-Certificate";
@Override
public void doPost(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
// Check that SNI header is present. This is a signal that we're receiving traffic proxied by a
// GFE, which is the expectation of this servlet. The value is unused.
TlsCredentials tlsCredentials = new TlsCredentials(req);
if (!tlsCredentials.hasSni()) {
logger.warning("Request did not include required SNI header.");
}
SessionMetadata sessionMetadata = new HttpSessionMetadata(tlsCredentials, req.getSession(true));
// Note that we are using the raw input stream rather than the reader, which implies that we are
// ignoring the HTTP-specified charset (if any) in favor of whatever charset the XML declares.
// This is ok because this code is only called from the proxy, which can't specify a charset
// (it blindly copies bytes off a socket).
handleEppCommandAndWriteResponse(toByteArray(req.getInputStream()), rsp, sessionMetadata);
}
}

View file

@ -0,0 +1,53 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.EppServletUtils.handleEppCommandAndWriteResponse;
import com.google.common.io.ByteStreams;
import com.google.domain.registry.flows.SessionMetadata.SessionSource;
import com.google.domain.registry.model.eppcommon.ProtocolDefinition;
import com.google.domain.registry.security.XsrfProtectedServlet;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* This servlet runs EPP commands directly without logging in. It verifies an XSRF token that could
* only come from the tool.
*/
public class EppToolServlet extends XsrfProtectedServlet {
/** Used to verify XSRF tokens. */
public static final String XSRF_SCOPE = "admin";
public EppToolServlet() {
super(XSRF_SCOPE, true);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
byte[] xml = ByteStreams.toByteArray(req.getInputStream());
handleEppCommandAndWriteResponse(
xml, rsp, new StatelessRequestSessionMetadata(
req.getParameter("clientIdentifier"),
Boolean.parseBoolean(req.getParameter("superuser")),
Boolean.parseBoolean(req.getParameter("dryRun")),
ProtocolDefinition.getVisibleServiceExtensionUris(),
SessionSource.TOOL));
}
}

View file

@ -0,0 +1,170 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.common.base.Preconditions.checkState;
import static com.google.domain.registry.xml.ValidationMode.LENIENT;
import static com.google.domain.registry.xml.ValidationMode.STRICT;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.flows.EppException.ParameterValueRangeErrorException;
import com.google.domain.registry.flows.EppException.ParameterValueSyntaxErrorException;
import com.google.domain.registry.flows.EppException.SyntaxErrorException;
import com.google.domain.registry.flows.EppException.UnimplementedProtocolVersionException;
import com.google.domain.registry.model.EppResourceUtils.InvalidRepoIdException;
import com.google.domain.registry.model.ImmutableObject;
import com.google.domain.registry.model.eppinput.EppInput;
import com.google.domain.registry.model.eppinput.EppInput.WrongProtocolVersionException;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.host.InetAddressAdapter.IpVersionMismatchException;
import com.google.domain.registry.model.translators.CurrencyUnitAdapter.UnknownCurrencyException;
import com.google.domain.registry.util.FormattingLogger;
import com.google.domain.registry.xml.ValidationMode;
import com.google.domain.registry.xml.XmlException;
import com.google.domain.registry.xml.XmlTransformer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
/** {@link XmlTransformer} for marshalling to and from the Epp model classes. */
public class EppXmlTransformer {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
// Hardcoded XML schemas, ordered with respect to dependency.
private static final ImmutableList<String> SCHEMAS = ImmutableList.of(
"eppcom.xsd",
"epp.xsd",
"contact.xsd",
"host.xsd",
"domain.xsd",
"rgp.xsd",
"secdns.xsd",
"fee.xsd",
"metadata.xsd",
"mark.xsd",
"dsig.xsd",
"smd.xsd",
"launch.xsd",
"allocate.xsd");
private static final XmlTransformer INPUT_TRANSFORMER =
new XmlTransformer(SCHEMAS, EppInput.class);
private static final XmlTransformer OUTPUT_TRANSFORMER =
new XmlTransformer(SCHEMAS, EppOutput.class);
public static void validateOutput(String xml) throws XmlException {
OUTPUT_TRANSFORMER.validate(xml);
}
public static <T> T unmarshal(byte[] bytes) throws EppException {
try {
return INPUT_TRANSFORMER.unmarshal(new ByteArrayInputStream(bytes));
} catch (XmlException e) {
// If this XmlException is wrapping a known type find it. If not, it's a syntax error.
FluentIterable<Throwable> causalChain = FluentIterable.from(Throwables.getCausalChain(e));
if (!(causalChain.filter(IpVersionMismatchException.class).isEmpty())) {
throw new IpAddressVersionMismatchException();
}
if (!(causalChain.filter(WrongProtocolVersionException.class).isEmpty())) {
throw new UnimplementedProtocolVersionException();
}
if (!(causalChain.filter(InvalidRepoIdException.class).isEmpty())) {
throw new InvalidRepoIdEppException();
}
if (!(causalChain.filter(UnknownCurrencyException.class).isEmpty())) {
throw new UnknownCurrencyEppException();
}
throw new GenericSyntaxErrorException(e.getMessage());
}
}
private static byte[] marshal(
XmlTransformer transformer,
ImmutableObject root,
ValidationMode validation) throws XmlException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
transformer.marshal(root, byteArrayOutputStream, UTF_8, validation);
return byteArrayOutputStream.toByteArray();
}
public static byte[] marshal(EppOutput root, ValidationMode validation) throws XmlException {
return marshal(OUTPUT_TRANSFORMER, root, validation);
}
public static byte[] marshalWithLenientRetry(EppOutput eppOutput) {
checkState(eppOutput != null);
// We need to marshal to a string instead of writing the response directly to the servlet's
// response writer, so that partial results don't get written on failure.
try {
return EppXmlTransformer.marshal(eppOutput, STRICT);
} catch (XmlException e) {
// We failed to marshal with validation. This is very bad, but we can potentially still send
// back slightly invalid xml, so try again without validation.
try {
byte[] lenient = EppXmlTransformer.marshal(eppOutput, LENIENT);
// Marshaling worked even though the results didn't validate against the schema.
logger.severe(e, "Result marshaled but did not validate: " + new String(lenient, UTF_8));
return lenient;
} catch (XmlException e2) {
throw new RuntimeException(e2); // Failing to marshal at all is not recoverable.
}
}
}
@VisibleForTesting
public static byte[] marshalInput(EppInput root, ValidationMode validation) throws XmlException {
return marshal(INPUT_TRANSFORMER, root, validation);
}
@VisibleForTesting
public static void validateInput(String xml) throws XmlException {
INPUT_TRANSFORMER.validate(xml);
}
/** IP address version mismatch. */
public static class IpAddressVersionMismatchException extends ParameterValueRangeErrorException {
public IpAddressVersionMismatchException() {
super("IP adddress version mismatch");
}
}
/** Invalid format for repository id. */
public static class InvalidRepoIdEppException extends ParameterValueSyntaxErrorException {
public InvalidRepoIdEppException() {
super("Invalid format for repository id");
}
}
/** Unknown currency. */
static class UnknownCurrencyEppException extends ParameterValueRangeErrorException {
public UnknownCurrencyEppException() {
super("Unknown currency.");
}
}
/** Generic syntax error that can be thrown by any flow. */
static class GenericSyntaxErrorException extends SyntaxErrorException {
public GenericSyntaxErrorException(String message) {
super(message);
}
}
}

View file

@ -0,0 +1,130 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.model.eppcommon.Trid;
import com.google.domain.registry.model.eppinput.EppInput;
import com.google.domain.registry.model.eppinput.EppInput.CommandExtension;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.eppoutput.Response;
import com.google.domain.registry.model.eppoutput.Response.ResponseData;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
import com.google.domain.registry.model.eppoutput.Result;
import org.joda.time.DateTime;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* An abstract EPP flow.
* <p>
* This class also contains static methods for loading an appropriate flow based on model classes.
*/
public abstract class Flow {
protected EppInput eppInput;
protected SessionMetadata sessionMetadata;
protected Trid trid;
protected DateTime now;
protected byte[] inputXmlBytes;
/** Whether this flow is being run in a superuser mode that can skip some checks. */
protected boolean superuser;
/** The collection of allowed extensions for the flow. */
private Set<Class<? extends CommandExtension>> validExtensions = new HashSet<>();
/** Flows can override this for custom initialization. */
@SuppressWarnings("unused")
protected void initFlow() throws EppException {}
/** Execute the business logic for this flow. */
protected abstract EppOutput run() throws EppException;
/**
* Subclasses that create a resource should override this to return the repoId of the new
* resource.
*/
protected String getCreatedRepoId() {
return null;
}
protected String getClientId() {
return sessionMetadata.getClientId();
}
protected EppOutput createOutput(Result.Code code) {
return createOutput(code, null);
}
protected EppOutput createOutput(Result.Code code, ResponseData responseData) {
return createOutput(code, responseData, null);
}
protected EppOutput createOutput(
Result.Code code,
ResponseData responseData,
ImmutableList<? extends ResponseExtension> extensions) {
return EppOutput.create(new Response.Builder()
.setTrid(trid)
.setResult(Result.create(code))
.setExecutionTime(now)
.setCreatedRepoId(getCreatedRepoId())
.setResData(responseData == null ? null : ImmutableList.of(responseData))
.setExtensions(extensions)
.build());
}
/**
* Using an init function instead of a constructor avoids duplicating constructors across the
* entire hierarchy of flow classes
*/
public final Flow init(
EppInput eppInput,
Trid trid,
SessionMetadata sessionMetadata,
boolean superuser,
DateTime now,
byte[] inputXmlBytes) throws EppException {
this.eppInput = eppInput;
this.trid = trid;
this.sessionMetadata = sessionMetadata;
this.now = now;
this.superuser = superuser;
this.inputXmlBytes = inputXmlBytes;
initFlow();
validExtensions = ImmutableSet.copyOf(validExtensions);
return this;
}
/**
* Add an extension class as a valid extension for a flow.
* Must be called in the init series of methods, as the validExtensions
* becomes immutable once init is complete.
*/
@SafeVarargs
protected final void registerExtensions(Class<? extends CommandExtension>... extensions) {
Collections.addAll(validExtensions, extensions);
}
/** Get the legal command extension types for this flow. */
protected final Set<Class<? extends CommandExtension>> getValidRequestExtensions() {
return ImmutableSet.copyOf(validExtensions);
}
}

View file

@ -0,0 +1,334 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.model.domain.launch.LaunchCreateExtension.CreateType.APPLICATION;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Table;
import com.google.domain.registry.flows.EppException.SyntaxErrorException;
import com.google.domain.registry.flows.EppException.UnimplementedCommandException;
import com.google.domain.registry.flows.contact.ContactCheckFlow;
import com.google.domain.registry.flows.contact.ContactCreateFlow;
import com.google.domain.registry.flows.contact.ContactDeleteFlow;
import com.google.domain.registry.flows.contact.ContactInfoFlow;
import com.google.domain.registry.flows.contact.ContactTransferApproveFlow;
import com.google.domain.registry.flows.contact.ContactTransferCancelFlow;
import com.google.domain.registry.flows.contact.ContactTransferQueryFlow;
import com.google.domain.registry.flows.contact.ContactTransferRejectFlow;
import com.google.domain.registry.flows.contact.ContactTransferRequestFlow;
import com.google.domain.registry.flows.contact.ContactUpdateFlow;
import com.google.domain.registry.flows.domain.ClaimsCheckFlow;
import com.google.domain.registry.flows.domain.DomainAllocateFlow;
import com.google.domain.registry.flows.domain.DomainApplicationCreateFlow;
import com.google.domain.registry.flows.domain.DomainApplicationDeleteFlow;
import com.google.domain.registry.flows.domain.DomainApplicationInfoFlow;
import com.google.domain.registry.flows.domain.DomainApplicationUpdateFlow;
import com.google.domain.registry.flows.domain.DomainCheckFlow;
import com.google.domain.registry.flows.domain.DomainCreateFlow;
import com.google.domain.registry.flows.domain.DomainDeleteFlow;
import com.google.domain.registry.flows.domain.DomainInfoFlow;
import com.google.domain.registry.flows.domain.DomainRenewFlow;
import com.google.domain.registry.flows.domain.DomainRestoreRequestFlow;
import com.google.domain.registry.flows.domain.DomainTransferApproveFlow;
import com.google.domain.registry.flows.domain.DomainTransferCancelFlow;
import com.google.domain.registry.flows.domain.DomainTransferQueryFlow;
import com.google.domain.registry.flows.domain.DomainTransferRejectFlow;
import com.google.domain.registry.flows.domain.DomainTransferRequestFlow;
import com.google.domain.registry.flows.domain.DomainUpdateFlow;
import com.google.domain.registry.flows.host.HostCheckFlow;
import com.google.domain.registry.flows.host.HostCreateFlow;
import com.google.domain.registry.flows.host.HostDeleteFlow;
import com.google.domain.registry.flows.host.HostInfoFlow;
import com.google.domain.registry.flows.host.HostUpdateFlow;
import com.google.domain.registry.flows.poll.PollAckFlow;
import com.google.domain.registry.flows.poll.PollRequestFlow;
import com.google.domain.registry.flows.session.HelloFlow;
import com.google.domain.registry.flows.session.LoginFlow;
import com.google.domain.registry.flows.session.LogoutFlow;
import com.google.domain.registry.model.contact.ContactCommand;
import com.google.domain.registry.model.domain.DomainCommand;
import com.google.domain.registry.model.domain.allocate.AllocateCreateExtension;
import com.google.domain.registry.model.domain.launch.ApplicationIdTargetExtension;
import com.google.domain.registry.model.domain.launch.LaunchCheckExtension;
import com.google.domain.registry.model.domain.launch.LaunchCheckExtension.CheckType;
import com.google.domain.registry.model.domain.launch.LaunchCreateExtension;
import com.google.domain.registry.model.domain.launch.LaunchPhase;
import com.google.domain.registry.model.domain.rgp.RestoreCommand.RestoreOp;
import com.google.domain.registry.model.domain.rgp.RgpUpdateExtension;
import com.google.domain.registry.model.eppinput.EppInput;
import com.google.domain.registry.model.eppinput.EppInput.Hello;
import com.google.domain.registry.model.eppinput.EppInput.InnerCommand;
import com.google.domain.registry.model.eppinput.EppInput.Login;
import com.google.domain.registry.model.eppinput.EppInput.Logout;
import com.google.domain.registry.model.eppinput.EppInput.Poll;
import com.google.domain.registry.model.eppinput.EppInput.ResourceCommandWrapper;
import com.google.domain.registry.model.eppinput.EppInput.Transfer;
import com.google.domain.registry.model.eppinput.EppInput.Transfer.TransferOp;
import com.google.domain.registry.model.eppinput.ResourceCommand;
import com.google.domain.registry.model.host.HostCommand;
import java.util.Map;
import java.util.Set;
/** Registry that can select a flow to handle a given Epp command. */
public class FlowRegistry {
/** Marker class for unimplemented flows. */
private abstract static class UnimplementedFlow extends Flow {}
/** A function type that takes an {@link EppInput} and returns a {@link Flow} class. */
private abstract static class FlowProvider {
/** Get the flow associated with this {@link EppInput} or return null to signal no match. */
Class<? extends Flow> get(EppInput eppInput) {
InnerCommand innerCommand = eppInput.getCommandWrapper().getCommand();
return get(eppInput, innerCommand, (innerCommand instanceof ResourceCommandWrapper)
? ((ResourceCommandWrapper) innerCommand).getResourceCommand() : null);
}
/**
* Subclasses need to implement this to examine the parameters and choose a flow (or null if
* the subclass doesn't know of an appropriate flow.
*/
abstract Class<? extends Flow> get(
EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand);
}
/** The hello flow is keyed on a special {@code CommandWrapper} type. */
private static final FlowProvider HELLO_FLOW_PROVIDER = new FlowProvider() {
@Override
Class<? extends Flow> get(
EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) {
return eppInput.getCommandWrapper() instanceof Hello ? HelloFlow.class : null;
}};
/** Session flows like login and logout are keyed only on the {@link InnerCommand} type. */
private static final FlowProvider SESSION_FLOW_PROVIDER = new FlowProvider() {
private final Map<Class<?>, Class<? extends Flow>> commandFlows =
ImmutableMap.<Class<?>, Class<? extends Flow>>of(
Login.class, LoginFlow.class,
Logout.class, LogoutFlow.class);
@Override
Class<? extends Flow> get(
EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) {
return innerCommand == null ? null : commandFlows.get(innerCommand.getClass());
}};
/** Poll flows have an {@link InnerCommand} of type {@link Poll}. */
private static final FlowProvider POLL_FLOW_PROVIDER = new FlowProvider() {
@Override
Class<? extends Flow> get(
EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) {
if (!(innerCommand instanceof Poll)) {
return null;
}
switch (((Poll) innerCommand).getPollOp()) {
case ACK:
return PollAckFlow.class;
case REQUEST:
return PollRequestFlow.class;
default:
return UnimplementedFlow.class;
}
}};
/**
* The domain restore command is technically a domain {@literal <update>}, but logically a totally
* separate flow.
* <p>
* This provider must be tried before {@link #RESOURCE_CRUD_FLOW_PROVIDER}. Otherwise, the regular
* domain update flow will match first.
*/
private static final FlowProvider DOMAIN_RESTORE_FLOW_PROVIDER = new FlowProvider() {
@Override
Class<? extends Flow> get(
EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) {
if (!(resourceCommand instanceof DomainCommand.Update)) {
return null;
}
RgpUpdateExtension rgpUpdateExtension = eppInput.getSingleExtension(RgpUpdateExtension.class);
if (rgpUpdateExtension == null) {
return null;
}
// Restore command with an op of "report" is not currently supported.
return (rgpUpdateExtension.getRestoreCommand().getRestoreOp() == RestoreOp.REQUEST)
? DomainRestoreRequestFlow.class
: UnimplementedFlow.class;
}};
/**
* The claims check flow is keyed on the type of the {@link ResourceCommand} and on having the
* correct extension with a specific phase value.
*/
private static final FlowProvider DOMAIN_CHECK_FLOW_PROVIDER = new FlowProvider() {
@Override
Class<? extends Flow> get(
EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) {
if (!(resourceCommand instanceof DomainCommand.Check)) {
return null;
}
LaunchCheckExtension extension = eppInput.getSingleExtension(LaunchCheckExtension.class);
if (extension == null || CheckType.AVAILABILITY.equals(extension.getCheckType())) {
// We don't distinguish between registry phases for "avail", so don't bother checking phase.
return DomainCheckFlow.class;
}
if (CheckType.CLAIMS.equals(extension.getCheckType())
&& LaunchPhase.CLAIMS.equals(extension.getPhase())) {
return ClaimsCheckFlow.class;
}
return null;
}};
/** General resource CRUD flows are keyed on the type of their {@link ResourceCommand}. */
private static final FlowProvider RESOURCE_CRUD_FLOW_PROVIDER = new FlowProvider() {
private final Map<Class<?>, Class<? extends Flow>> resourceCrudFlows =
new ImmutableMap.Builder<Class<?>, Class<? extends Flow>>()
.put(ContactCommand.Check.class, ContactCheckFlow.class)
.put(ContactCommand.Create.class, ContactCreateFlow.class)
.put(ContactCommand.Delete.class, ContactDeleteFlow.class)
.put(ContactCommand.Info.class, ContactInfoFlow.class)
.put(ContactCommand.Update.class, ContactUpdateFlow.class)
.put(DomainCommand.Create.class, DomainCreateFlow.class)
.put(DomainCommand.Delete.class, DomainDeleteFlow.class)
.put(DomainCommand.Info.class, DomainInfoFlow.class)
.put(DomainCommand.Renew.class, DomainRenewFlow.class)
.put(DomainCommand.Update.class, DomainUpdateFlow.class)
.put(HostCommand.Check.class, HostCheckFlow.class)
.put(HostCommand.Create.class, HostCreateFlow.class)
.put(HostCommand.Delete.class, HostDeleteFlow.class)
.put(HostCommand.Info.class, HostInfoFlow.class)
.put(HostCommand.Update.class, HostUpdateFlow.class)
.build();
@Override
Class<? extends Flow> get(
EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) {
return resourceCommand == null ? null : resourceCrudFlows.get(resourceCommand.getClass());
}};
/** The domain allocate flow has a specific extension. */
private static final FlowProvider ALLOCATE_FLOW_PROVIDER = new FlowProvider() {
@Override
Class<? extends Flow> get(
EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) {
return (resourceCommand instanceof DomainCommand.Create
&& eppInput.getSingleExtension(AllocateCreateExtension.class) != null)
? DomainAllocateFlow.class : null;
}};
/**
* Application CRUD flows have an extension and are keyed on the type of their
* {@link ResourceCommand}.
*/
private static final FlowProvider APPLICATION_CRUD_FLOW_PROVIDER = new FlowProvider() {
private final Map<Class<? extends ResourceCommand>, Class<? extends Flow>> applicationFlows =
ImmutableMap.<Class<? extends ResourceCommand>, Class<? extends Flow>>of(
DomainCommand.Create.class, DomainApplicationCreateFlow.class,
DomainCommand.Delete.class, DomainApplicationDeleteFlow.class,
DomainCommand.Info.class, DomainApplicationInfoFlow.class,
DomainCommand.Update.class, DomainApplicationUpdateFlow.class);
private final Set<LaunchPhase> launchPhases = ImmutableSet.of(
LaunchPhase.SUNRISE, LaunchPhase.SUNRUSH, LaunchPhase.LANDRUSH);
@Override
Class<? extends Flow> get(
EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) {
if (eppInput.getSingleExtension(ApplicationIdTargetExtension.class) != null) {
return applicationFlows.get(resourceCommand.getClass());
}
LaunchCreateExtension createExtension =
eppInput.getSingleExtension(LaunchCreateExtension.class);
// Return a flow if the type is APPLICATION, or if it's null and we are in a launch phase.
// If the type is specified as REGISTRATION, return null.
if (createExtension != null) {
LaunchPhase launchPhase = createExtension.getPhase();
if (APPLICATION.equals(createExtension.getCreateType())
|| (createExtension.getCreateType() == null && launchPhases.contains(launchPhase))) {
return applicationFlows.get(resourceCommand.getClass());
}
}
return null;
}};
/** Transfer flows have an {@link InnerCommand} of type {@link Transfer}. */
private static final FlowProvider TRANSFER_FLOW_PROVIDER = new FlowProvider() {
private final Table<Class<?>, TransferOp, Class<? extends Flow>> transferFlows = ImmutableTable
.<Class<?>, TransferOp, Class<? extends Flow>>builder()
.put(ContactCommand.Transfer.class, TransferOp.APPROVE, ContactTransferApproveFlow.class)
.put(ContactCommand.Transfer.class, TransferOp.CANCEL, ContactTransferCancelFlow.class)
.put(ContactCommand.Transfer.class, TransferOp.QUERY, ContactTransferQueryFlow.class)
.put(ContactCommand.Transfer.class, TransferOp.REJECT, ContactTransferRejectFlow.class)
.put(ContactCommand.Transfer.class, TransferOp.REQUEST, ContactTransferRequestFlow.class)
.put(DomainCommand.Transfer.class, TransferOp.APPROVE, DomainTransferApproveFlow.class)
.put(DomainCommand.Transfer.class, TransferOp.CANCEL, DomainTransferCancelFlow.class)
.put(DomainCommand.Transfer.class, TransferOp.QUERY, DomainTransferQueryFlow.class)
.put(DomainCommand.Transfer.class, TransferOp.REJECT, DomainTransferRejectFlow.class)
.put(DomainCommand.Transfer.class, TransferOp.REQUEST, DomainTransferRequestFlow.class)
.build();
@Override
Class<? extends Flow> get(
EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) {
return resourceCommand != null && innerCommand instanceof Transfer
? transferFlows.get(resourceCommand.getClass(), ((Transfer) innerCommand).getTransferOp())
: null;
}};
/** Return the appropriate flow to handle this EPP command. */
public static Class<? extends Flow> getFlowClass(EppInput eppInput) throws EppException {
// Do some sanity checking on the input; anything but Hello must have a command type.
InnerCommand innerCommand = eppInput.getCommandWrapper().getCommand();
if (innerCommand == null && !(eppInput.getCommandWrapper() instanceof Hello)) {
throw new MissingCommandException();
}
// Try the FlowProviders until we find a match. The order matters because it's possible to
// match multiple FlowProviders and so more specific matches are tried first.
for (FlowProvider flowProvider : new FlowProvider[] {
HELLO_FLOW_PROVIDER,
SESSION_FLOW_PROVIDER,
POLL_FLOW_PROVIDER,
DOMAIN_RESTORE_FLOW_PROVIDER,
ALLOCATE_FLOW_PROVIDER,
APPLICATION_CRUD_FLOW_PROVIDER,
DOMAIN_CHECK_FLOW_PROVIDER,
RESOURCE_CRUD_FLOW_PROVIDER,
TRANSFER_FLOW_PROVIDER}) {
Class<? extends Flow> flowClass = flowProvider.get(eppInput);
if (flowClass == UnimplementedFlow.class) {
break; // We found it, but it's marked as not implemented.
}
if (flowClass != null) {
return flowClass; // We found it!
}
}
// Nothing usable was found, so throw an exception.
throw new UnimplementedCommandException(
innerCommand,
innerCommand instanceof ResourceCommandWrapper
? ((ResourceCommandWrapper) innerCommand).getResourceCommand() : null);
}
/** Command missing. */
static class MissingCommandException extends SyntaxErrorException {
public MissingCommandException() {
super("Command missing");
}
}
}

View file

@ -0,0 +1,191 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.common.base.Throwables.getStackTraceAsString;
import static com.google.common.base.Throwables.propagateIfInstanceOf;
import static com.google.common.io.BaseEncoding.base64;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.xml.XmlTransformer.prettyPrint;
import com.google.common.base.Strings;
import com.google.domain.registry.model.eppcommon.Trid;
import com.google.domain.registry.model.eppinput.EppInput;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.monitoring.whitebox.EppMetrics;
import com.google.domain.registry.util.Clock;
import com.google.domain.registry.util.FormattingLogger;
import com.google.domain.registry.util.NonFinalForTesting;
import com.google.domain.registry.util.SystemClock;
import com.google.domain.registry.util.TypeUtils;
import com.googlecode.objectify.Work;
import org.joda.time.DateTime;
/** Run a flow, either transactionally or not, with logging and retrying as needed. */
public class FlowRunner {
private static final String COMMAND_LOG_FORMAT = "EPP Command" + Strings.repeat("\n\t%s", 4);
/** Whether to actually write to the datastore or just simulate. */
public enum CommitMode { LIVE, DRY_RUN }
/** Whether to run in normal or superuser mode. */
public enum UserPrivileges { NORMAL, SUPERUSER }
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
@NonFinalForTesting
private static Clock clock = new SystemClock();
private final Class<? extends Flow> flowClass;
private final EppInput eppInput;
private final Trid trid;
private final SessionMetadata sessionMetadata;
private final byte[] inputXmlBytes;
private final EppMetrics metrics;
public FlowRunner(
Class<? extends Flow> flowClass,
EppInput eppInput,
Trid trid,
SessionMetadata sessionMetadata,
byte[] inputXmlBytes,
final EppMetrics metrics) {
this.flowClass = flowClass;
this.eppInput = eppInput;
this.trid = trid;
this.sessionMetadata = sessionMetadata;
this.inputXmlBytes = inputXmlBytes;
this.metrics = metrics;
}
public EppOutput run(
final CommitMode commitMode, final UserPrivileges userPrivileges) throws EppException {
String clientId = sessionMetadata.getClientId();
final boolean isSuperuser = UserPrivileges.SUPERUSER.equals(userPrivileges);
logger.infofmt(
COMMAND_LOG_FORMAT,
trid.getServerTransactionId(),
clientId,
sessionMetadata,
prettyPrint(inputXmlBytes).replaceAll("\n", "\n\t"));
if (!isTransactional()) {
if (metrics != null) {
metrics.incrementAttempts();
}
return createAndInitFlow(isSuperuser, clock.nowUtc()).run();
}
// We log the command in a structured format. Note that we do this before the transaction;
// if we did it after, we might miss a transaction that committed successfully but then crashed
// before it could log.
logger.info("EPP_Mutation " + new JsonLogStatement(trid)
.add("client", clientId)
.add("privileges", userPrivileges.toString())
.add("xmlBytes", base64().encode(inputXmlBytes)));
try {
EppOutput flowResult = ofy().transact(new Work<EppOutput>() {
@Override
public EppOutput run() {
if (metrics != null) {
metrics.incrementAttempts();
}
try {
EppOutput output = createAndInitFlow(isSuperuser, ofy().getTransactionTime()).run();
if (CommitMode.DRY_RUN.equals(commitMode)) {
throw new DryRunException(output);
}
return output;
} catch (EppException e) {
throw new RuntimeException(e);
}
}});
logger.info("EPP_Mutation_Committed " + new JsonLogStatement(trid)
.add("createdRepoId", flowResult.getResponse().getCreatedRepoId())
.add("executionTime", flowResult.getResponse().getExecutionTime().getMillis()));
return flowResult;
} catch (DryRunException e) {
return e.output;
} catch (RuntimeException e) {
logger.warning("EPP_Mutation_Failed " + new JsonLogStatement(trid));
logger.warning(getStackTraceAsString(e));
propagateIfInstanceOf(e.getCause(), EppException.class);
throw e;
}
}
private Flow createAndInitFlow(boolean superuser, DateTime now) throws EppException {
return TypeUtils.<Flow>instantiate(flowClass).init(
eppInput,
trid,
sessionMetadata,
superuser,
now,
inputXmlBytes);
}
public boolean isTransactional() {
return TransactionalFlow.class.isAssignableFrom(flowClass);
}
/**
* Helper for logging in json format.
* <p>
* This is needed because the usual json outputters perform normalizations that we don't want or
* need, since we know that our values never need to be escaped - there are only strings and
* numbers, and the strings are not allowed to contain quote characters.
* <p>
* An example output for an EPP_Mutation:
* {"trid":"abc-123", "client":"some_registrar", "tld":"com", "xmlBytes":"abc123DEF"}
* <p>
* An example output for an EPP_Mutation_Committed that doesn't create a new resource:
* {"trid":"abc-123", "executionTime":123456789}
* <p>
* An example output for an EPP_Mutation_Committed that creates a new resource:
* {"trid":"abc-123", "executionRepoId":123, "executionTime":123456789}
*/
private static class JsonLogStatement {
StringBuilder message;
JsonLogStatement(Trid trid) {
message =
new StringBuilder("{\"trid\":\"").append(trid.getServerTransactionId()).append('\"');
}
JsonLogStatement add(String key, Object value) {
if (value != null) {
String quote = value instanceof String ? "\"" : "";
message.append(String.format(", \"%s\":%s%s%s", key, quote, value, quote));
}
return this;
}
@Override
public String toString() {
return message + "}";
}
}
/** Exception for canceling a transaction while capturing what the output would have been. */
private class DryRunException extends RuntimeException {
final EppOutput output;
DryRunException(EppOutput output) {
this.output = output;
}
}
}

View file

@ -0,0 +1,61 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.common.base.Preconditions.checkState;
import javax.servlet.http.HttpSession;
/** A metadata class that is a wrapper around {@link HttpSession}. */
public class HttpSessionMetadata extends SessionMetadata {
private final HttpSession session;
private boolean isValid = true;
public HttpSessionMetadata(TransportCredentials credentials, HttpSession session) {
this.session = session;
setTransportCredentials(credentials);
}
@Override
protected void checkValid() {
checkState(isValid, "This session has been invalidated.");
}
@Override
public void invalidate() {
session.invalidate();
isValid = false;
}
@Override
protected void setProperty(String key, Object value) {
if (value == null) {
session.removeAttribute(key);
} else {
session.setAttribute(key, value);
}
}
@Override
protected Object getProperty(String key) {
return session.getAttribute(key);
}
@Override
public SessionSource getSessionSource() {
return SessionSource.HTTP;
}
}

View file

@ -0,0 +1,135 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.common.base.Verify.verifyNotNull;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.intersection;
import static com.google.domain.registry.model.registry.Registries.getTlds;
import static com.google.domain.registry.util.CollectionUtils.nullToEmpty;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.flows.EppException.CommandUseErrorException;
import com.google.domain.registry.flows.EppException.SyntaxErrorException;
import com.google.domain.registry.flows.EppException.UnimplementedExtensionException;
import com.google.domain.registry.model.eppcommon.ProtocolDefinition;
import com.google.domain.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
import com.google.domain.registry.model.eppinput.EppInput.CommandExtension;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.util.FormattingLogger;
import java.util.Set;
/** A flow that requires being logged in. */
public abstract class LoggedInFlow extends Flow {
static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
/**
* A blacklist of service extension URIs that will cause an error if they are used without being
* declared on login.
*/
private static final ImmutableSet<String> UNDECLARED_URIS_BLACKLIST =
ImmutableSet.of(ServiceExtension.FEE_0_6.getUri());
/**
* The TLDs on which the logged-in registrar is allowed access domains.
*/
private ImmutableSet<String> allowedTlds;
protected ImmutableSet<String> getAllowedTlds() {
return allowedTlds;
}
@Override
public final void initFlow() throws EppException {
if (getClientId() == null) {
throw new NotLoggedInException();
}
// Validate that the extensions in the input match what this flow expects.
ImmutableSet<Class<? extends CommandExtension>> extensionClasses = FluentIterable
.from(eppInput.getCommandWrapper().getExtensions())
.transform(new Function<CommandExtension, Class<? extends CommandExtension>>() {
@Override
public Class<? extends CommandExtension> apply(CommandExtension extension) {
return extension.getClass();
}})
.toSet();
if (extensionClasses.size() != eppInput.getCommandWrapper().getExtensions().size()) {
throw new UnsupportedRepeatedExtensionException();
}
// Validate that we did not receive any undeclared extensions.
ImmutableSet<String> extensionUris = FluentIterable
.from(extensionClasses)
.transform(new Function<Class<? extends CommandExtension>, String>() {
@Override
public String apply(Class<? extends CommandExtension> clazz) {
return ProtocolDefinition.ServiceExtension.getCommandExtensionUri(clazz);
}})
.toSet();
Set<String> undeclaredUris = difference(
extensionUris, nullToEmpty(sessionMetadata.getServiceExtensionUris()));
if (!undeclaredUris.isEmpty()) {
Set<String> undeclaredUrisThatError = intersection(undeclaredUris, UNDECLARED_URIS_BLACKLIST);
if (!undeclaredUrisThatError.isEmpty()) {
throw new UndeclaredServiceExtensionException(undeclaredUrisThatError);
} else {
logger.warningfmt(
"Client (%s) is attempting to run flow (%s) without declaring URIs %s on login",
getClientId(), getClass().getSimpleName(), undeclaredUris);
}
}
if (sessionMetadata.isSuperuser()) {
allowedTlds = getTlds();
} else {
Registrar registrar = verifyNotNull(
Registrar.loadByClientId(sessionMetadata.getClientId()),
"Could not load registrar %s", sessionMetadata.getClientId());
allowedTlds = registrar.getAllowedTlds();
}
initLoggedInFlow();
if (!difference(extensionClasses, getValidRequestExtensions()).isEmpty()) {
throw new UnimplementedExtensionException();
}
}
@SuppressWarnings("unused")
protected void initLoggedInFlow() throws EppException {}
/** Registrar is not logged in. */
public static class NotLoggedInException extends CommandUseErrorException {
public NotLoggedInException() {
super("Registrar is not logged in.");
}
}
/** Unsupported repetition of an extension. */
static class UnsupportedRepeatedExtensionException extends SyntaxErrorException {
public UnsupportedRepeatedExtensionException() {
super("Unsupported repetition of an extension");
}
}
/** Service extension(s) must be declared at login. */
public static class UndeclaredServiceExtensionException extends CommandUseErrorException {
public UndeclaredServiceExtensionException(Set<String> undeclaredUris) {
super(String.format("Service extension(s) must be declared at login: %s",
Joiner.on(", ").join(undeclaredUris)));
}
}
}

View file

@ -0,0 +1,43 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
/**
* An EPP flow that mutates a single stored resource that is owned by the current registrar.
*
* @param <R> the resource type being changed
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class OwnedResourceMutateFlow
<R extends EppResource, C extends SingleResourceCommand>
extends ResourceMutateFlow<R, C> {
/** Fail if the object doesn't exist or was deleted. */
@Override
protected final void verifyMutationAllowed() throws EppException {
if (!superuser) {
verifyResourceOwnership(getClientId(), existingResource);
}
verifyMutationOnOwnedResourceAllowed();
}
/** Check invariants before allowing the command to proceed. */
@SuppressWarnings("unused")
protected void verifyMutationOnOwnedResourceAllowed() throws EppException {}
}

View file

@ -0,0 +1,43 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.EppResource.Builder;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
/**
* An EPP flow that acts on an owned resource with a pending transfer on it.
*
* @param <R> the resource type being manipulated
* @param <B> a builder for the resource
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class OwnedResourceMutatePendingTransferFlow
<R extends EppResource, B extends Builder<R, ?>, C extends SingleResourceCommand>
extends ResourceMutatePendingTransferFlow<R, B, C> {
/** Fail if this command isn't coming from the registrar that currently owns the resource. */
@Override
protected final void verifyPendingTransferMutationAllowed() throws EppException {
verifyResourceOwnership(getClientId(), existingResource);
verifyOwnedResourcePendingTransferMutationAllowed();
}
@SuppressWarnings("unused")
protected void verifyOwnedResourcePendingTransferMutationAllowed() throws EppException {}
}

View file

@ -0,0 +1,85 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.model.eppoutput.Result.Code.SuccessWithActionPending;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.domain.registry.flows.EppException.AssociationProhibitsOperationException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.EppResource.Builder;
import com.google.domain.registry.model.domain.ReferenceUnion;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.eppoutput.Result.Code;
import com.google.domain.registry.model.index.ForeignKeyIndex;
import com.googlecode.objectify.Work;
/**
* An EPP flow that deletes a resource asynchronously (i.e. via mapreduce).
*
* @param <R> the resource type being changed
* @param <B> a builder for the resource
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceAsyncDeleteFlow
<R extends EppResource, B extends Builder<R, ?>, C extends SingleResourceCommand>
extends ResourceDeleteFlow<R, C> {
@Override
public void failfast() throws ResourceToDeleteIsReferencedException {
// Enter a transactionless context briefly.
boolean isLinked = ofy().doTransactionless(new Work<Boolean>() {
@Override
public Boolean run() {
ForeignKeyIndex<R> fki = ForeignKeyIndex.load(resourceClass, targetId, now);
if (fki == null) {
// Don't failfast on non-existence. We could, but that would duplicate code paths in a way
// that would be hard to reason about, and there's no real gain in doing so.
return false;
}
return isLinkedForFailfast(ReferenceUnion.create(fki.getReference()));
}
});
if (isLinked) {
throw new ResourceToDeleteIsReferencedException();
}
}
/** Subclasses must override this to check if the supplied reference has incoming links. */
protected abstract boolean isLinkedForFailfast(ReferenceUnion<R> ref);
@Override
protected final R createOrMutateResource() {
@SuppressWarnings("unchecked")
B builder = (B) existingResource.asBuilder().addStatusValue(StatusValue.PENDING_DELETE);
return builder.build();
}
/** Subclasses can override this to return a different success result code. */
@Override
protected Code getDeleteResultCode() {
return SuccessWithActionPending;
}
/** Resource to be deleted has active incoming references. */
public static class ResourceToDeleteIsReferencedException
extends AssociationProhibitsOperationException {
public ResourceToDeleteIsReferencedException() {
super("Resource to be deleted has active incoming references");
}
}
}

View file

@ -0,0 +1,83 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.flows.EppException.ParameterValuePolicyErrorException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.eppinput.ResourceCommand.ResourceCheck;
import com.google.domain.registry.model.eppoutput.CheckData;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
import java.util.List;
/**
* An EPP flow that checks whether resources can be provisioned.
*
* @param <R> the resource type being manipulated
* @param <C> the overall command type doing the manipulation.
*/
public abstract class ResourceCheckFlow<R extends EppResource, C extends ResourceCheck>
extends ResourceFlow<R, C> {
protected List<String> targetIds;
@Override
protected final void initResourceFlow() throws EppException {
this.targetIds = command.getTargetIds();
initCheckResourceFlow();
}
@Override
protected final EppOutput runResourceFlow() throws EppException {
return createOutput(
Success,
getCheckData(),
getResponseExtensions());
}
@Override
protected final void verifyIsAllowed() throws EppException {
if (targetIds.size() > RegistryEnvironment.get().config().getMaxChecks()) {
throw new TooManyResourceChecksException();
}
}
@SuppressWarnings("unused")
protected void initCheckResourceFlow() throws EppException {}
/** Subclasses must implement this to return the check data. */
protected abstract CheckData getCheckData();
/** Subclasses may override this to return extensions. */
@SuppressWarnings("unused")
protected ImmutableList<? extends ResponseExtension> getResponseExtensions() throws EppException {
return null;
}
/** Too many resource checks requested in one check command. */
public static class TooManyResourceChecksException extends ParameterValuePolicyErrorException {
public TooManyResourceChecksException() {
super(String.format(
"No more than %s resources may be checked at a time",
RegistryEnvironment.get().config().getMaxChecks()));
}
}
}

View file

@ -0,0 +1,129 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.common.annotations.VisibleForTesting;
import com.google.domain.registry.flows.EppException.ObjectAlreadyExistsException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.EppResource.Builder;
import com.google.domain.registry.model.EppResource.ForeignKeyedEppResource;
import com.google.domain.registry.model.domain.DomainApplication;
import com.google.domain.registry.model.eppinput.ResourceCommand.ResourceCreateOrChange;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.index.DomainApplicationIndex;
import com.google.domain.registry.model.index.EppResourceIndex;
import com.google.domain.registry.model.index.ForeignKeyIndex;
import com.google.domain.registry.util.TypeUtils.TypeInstantiator;
import com.googlecode.objectify.Key;
import javax.annotation.Nullable;
/**
* An EPP flow that creates a storable resource.
*
* @param <R> the resource type being changed
* @param <B> a builder for the resource
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceCreateFlow
<R extends EppResource,
B extends Builder<R, ?>,
C extends ResourceCreateOrChange<? super B> & SingleResourceCommand>
extends ResourceCreateOrMutateFlow<R, C> {
@Override
protected void initRepoId() {
repoId = createFlowRepoId();
}
@Nullable
protected abstract String createFlowRepoId();
@Override
protected final void verifyIsAllowed() throws EppException {
if (existingResource != null) {
throw new ResourceAlreadyExistsException(targetId);
}
verifyCreateIsAllowed();
}
@Override
protected final R createOrMutateResource() throws EppException {
B builder = new TypeInstantiator<B>(getClass()){}.instantiate();
command.applyTo(builder);
builder
.setCreationClientId(getClientId())
.setCurrentSponsorClientId(getClientId())
.setRepoId(getResourceKey().getName());
setCreateProperties(builder);
return builder.build();
}
/**
* Save a new or updated {@link ForeignKeyIndex} and {@link EppResourceIndex} pointing to what we
* created.
*/
@Override
protected final void modifyRelatedResources() {
if (newResource instanceof ForeignKeyedEppResource) {
ofy().save().entity(ForeignKeyIndex.create(newResource, newResource.getDeletionTime()));
} else if (newResource instanceof DomainApplication) {
ofy().save().entity(
DomainApplicationIndex.createUpdatedInstance((DomainApplication) newResource));
}
ofy().save().entity(EppResourceIndex.create(Key.create(newResource)));
modifyCreateRelatedResources();
}
@Override
protected final String getCreatedRepoId() {
return newResource.getRepoId();
}
/** Modify any other resources that need to be informed of this create. */
protected void modifyCreateRelatedResources() {}
/** Check resource-specific invariants before allowing the create to proceed. */
@SuppressWarnings("unused")
protected void verifyCreateIsAllowed() throws EppException {}
/** Set any resource-specific properties before creating. */
@SuppressWarnings("unused")
protected void setCreateProperties(B builder) throws EppException {}
/** Resource with this id already exists. */
public static class ResourceAlreadyExistsException extends ObjectAlreadyExistsException {
/** Whether this was thrown from a "failfast" context. Useful for testing. */
final boolean failfast;
public ResourceAlreadyExistsException(String resourceId, boolean failfast) {
super(String.format("Object with given ID (%s) already exists", resourceId));
this.failfast = failfast;
}
public ResourceAlreadyExistsException(String resourceId) {
this(resourceId, false);
}
@VisibleForTesting
public boolean isFailfast() {
return failfast;
}
}
}

View file

@ -0,0 +1,168 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.common.base.Preconditions.checkState;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.domain.registry.flows.EppException.AuthorizationErrorException;
import com.google.domain.registry.flows.SessionMetadata.SessionSource;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.domain.Period;
import com.google.domain.registry.model.domain.metadata.MetadataExtension;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.util.TypeUtils.TypeInstantiator;
import com.googlecode.objectify.Key;
/**
* An EPP flow that creates or mutates a single stored resource.
*
* @param <R> the resource type being changed
* @param <C> the command type, marshalled directly from the epp xml
*
* @error {@link OnlyToolCanPassMetadataException}
*/
public abstract class ResourceCreateOrMutateFlow
<R extends EppResource, C extends SingleResourceCommand> extends SingleResourceFlow<R, C>
implements TransactionalFlow {
String repoId;
protected R newResource;
protected HistoryEntry historyEntry;
protected MetadataExtension metadataExtension;
@Override
protected final void initSingleResourceFlow() throws EppException {
metadataExtension = eppInput.getSingleExtension(MetadataExtension.class);
initRepoId();
initHistoryEntry();
initResourceCreateOrMutateFlow();
}
/** Subclasses can optionally override this for further initialization. */
@SuppressWarnings("unused")
protected void initResourceCreateOrMutateFlow() throws EppException {}
/**
* Initializes the repoId on the flow. For mutate flows, the repoId is the same as that of the
* existing resource. For create flows, a new repoId is allocated for the appropriate class.
*/
protected abstract void initRepoId();
/**
* Create the history entry associated with this resource create or mutate flow.
*/
private void initHistoryEntry() {
// Don't try to create a historyEntry for mutate flows that are failing because the
// existingResource doesn't actually exist.
historyEntry = (repoId == null) ? null : new HistoryEntry.Builder()
.setType(getHistoryEntryType())
.setPeriod(getCommandPeriod())
.setClientId(getClientId())
.setTrid(trid)
.setModificationTime(now)
.setXmlBytes(storeXmlInHistoryEntry() ? inputXmlBytes : null)
.setBySuperuser(superuser)
.setReason(getHistoryEntryReason())
.setRequestedByRegistrar(getHistoryEntryRequestedByRegistrar())
.setParent(getResourceKey())
.build();
}
/**
* Returns a Key pointing to this resource, even if this resource hasn't been initialized or
* persisted yet.
*/
protected Key<EppResource> getResourceKey() {
checkState(repoId != null,
"RepoId hasn't been initialized yet; getResourceKey() called too early");
Class<R> resourceClazz = new TypeInstantiator<R>(getClass()){}.getExactType();
return Key.<EppResource>create(null, resourceClazz, repoId);
}
@Override
protected final EppOutput runResourceFlow() throws EppException {
newResource = createOrMutateResource();
verifyNewStateIsAllowed();
validateMetadataExtension();
modifyRelatedResources();
enqueueTasks();
ofy().save().<Object>entities(newResource, historyEntry);
return getOutput();
}
/** Execute the inner core of the command and returned the created or mutated resource. */
protected abstract R createOrMutateResource() throws EppException;
/** Check the new state before writing it. */
@SuppressWarnings("unused")
protected void verifyNewStateIsAllowed() throws EppException {}
/** Kick off any tasks that need to happen asynchronously. */
@SuppressWarnings("unused")
protected void enqueueTasks() throws EppException {}
/** Modify any other resources that need to be informed of this change. */
@SuppressWarnings("unused")
protected void modifyRelatedResources() throws EppException {}
/** Ensure that, if a metadata command exists, it is being passed from a tool-created session. */
void validateMetadataExtension() throws EppException {
if (!(metadataExtension == null
|| sessionMetadata.getSessionSource().equals(SessionSource.TOOL))) {
throw new OnlyToolCanPassMetadataException();
}
}
/** Subclasses must override this to specify the type set on the history entry. */
protected abstract HistoryEntry.Type getHistoryEntryType();
/** Subclasses may override this if they do not wish to store the XML of a command. */
protected boolean storeXmlInHistoryEntry() { return true; }
/** Retrieve the reason for the history entry. */
protected String getHistoryEntryReason() {
return metadataExtension != null
? metadataExtension.getReason()
: null;
}
/** Retrieve the requested by registrar flag for the history entry. */
protected Boolean getHistoryEntryRequestedByRegistrar() {
return metadataExtension != null
? metadataExtension.getRequestedByRegistrar()
: null;
}
/**
* Subclasses that have a specified period for their command should override this to so that the
* history entry contains the correct data.
*/
protected Period getCommandPeriod() { return null; }
/** Get the {@link EppOutput} to return. */
protected abstract EppOutput getOutput() throws EppException;
/** Only a tool can pass a metadata extension. */
public static class OnlyToolCanPassMetadataException extends AuthorizationErrorException {
public OnlyToolCanPassMetadataException() {
super("Metadata extensions can only be passed by tools.");
}
}
}

View file

@ -0,0 +1,65 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
import com.google.domain.registry.model.eppoutput.Result.Code;
import java.util.Set;
/**
* An EPP flow that deletes an {@link EppResource}.
*
* @param <R> the resource type being changed
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceDeleteFlow<R extends EppResource, C extends SingleResourceCommand>
extends OwnedResourceMutateFlow<R, C> {
private static final Set<StatusValue> DELETE_DISALLOWED_STATUSES = ImmutableSet.of(
StatusValue.LINKED,
StatusValue.CLIENT_DELETE_PROHIBITED,
StatusValue.PENDING_DELETE,
StatusValue.SERVER_DELETE_PROHIBITED);
/** This is intentionally non-final so that subclasses can override the disallowed statuses. */
@Override
protected Set<StatusValue> getDisallowedStatuses() {
return DELETE_DISALLOWED_STATUSES;
}
@Override
protected final EppOutput getOutput() {
return createOutput(getDeleteResultCode(), null, getDeleteResponseExtensions());
}
/** Subclasses can override this to return a different success result code. */
protected Code getDeleteResultCode() {
return Success;
}
/** Subclasses can override this to return response extensions. */
protected ImmutableList<? extends ResponseExtension> getDeleteResponseExtensions() {
return null;
}
}

View file

@ -0,0 +1,103 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.domain.registry.flows.EppException.CommandUseErrorException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.eppinput.EppInput.ResourceCommandWrapper;
import com.google.domain.registry.model.eppinput.ResourceCommand;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.registry.Registry.TldState;
import com.google.domain.registry.util.TypeUtils.TypeInstantiator;
/**
* An EPP flow that addresses a stored resource.
*
* @param <R> the resource type being manipulated
* @param <C> the command type doing the manipulation.
*/
public abstract class ResourceFlow<R extends EppResource, C extends ResourceCommand>
extends LoggedInFlow {
protected C command;
protected Class<R> resourceClass;
@Override
@SuppressWarnings("unchecked")
protected final void initLoggedInFlow() throws EppException {
this.command = (C) ((ResourceCommandWrapper) eppInput.getCommandWrapper().getCommand())
.getResourceCommand();
this.resourceClass = new TypeInstantiator<R>(getClass()){}.getExactType();
initResourceFlow();
}
/** Resource flows can override this for custom initialization.*/
protected abstract void initResourceFlow() throws EppException;
/**
* Loads the target resource and performs authorization and state allowance checks on it before
* delegating to {@link #runResourceFlow()}.
*
* @throws EppException If an error occurred while manipulating the resource.
*/
@Override
public final EppOutput run() throws EppException {
verifyIsAllowed();
return runResourceFlow();
}
/**
* Check that the current action operating within the scope of a single TLD (i.e. an operation on
* a domain) is allowed in the registry phase for the specified TLD that the resource is in.
*/
protected void checkRegistryStateForTld(String tld) throws BadCommandForRegistryPhaseException {
if (!superuser && getDisallowedTldStates().contains(Registry.get(tld).getTldState(now))) {
throw new BadCommandForRegistryPhaseException();
}
}
/**
* Get the TLD states during which this command is disallowed. By default all commands can be run
* in any state (except predelegation); Flow subclasses must override this method to disallow any
* further states.
*/
protected ImmutableSet<TldState> getDisallowedTldStates() {
return Sets.immutableEnumSet(TldState.PREDELEGATION);
}
/**
* Verifies that the command is allowed on the target resource.
*
* @throws EppException If the command is not allowed on this resource.
*/
protected abstract void verifyIsAllowed() throws EppException;
/**
* Run the flow.
*
* @throws EppException If something fails while manipulating the resource.
*/
protected abstract EppOutput runResourceFlow() throws EppException;
/** Command is not allowed in the current registry phase. */
public static class BadCommandForRegistryPhaseException extends CommandUseErrorException {
public BadCommandForRegistryPhaseException() {
super("Command is not allowed in the current registry phase");
}
}
}

View file

@ -0,0 +1,194 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.common.base.Preconditions.checkState;
import static com.google.domain.registry.model.domain.DomainResource.extendRegistrationWithCap;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.domain.registry.flows.EppException.AuthorizationErrorException;
import com.google.domain.registry.flows.EppException.InvalidAuthorizationInformationErrorException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.EppResource.Builder;
import com.google.domain.registry.model.EppResource.ForeignKeyedEppResource;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.eppcommon.AuthInfo;
import com.google.domain.registry.model.eppcommon.AuthInfo.BadAuthInfoException;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.eppcommon.Trid;
import com.google.domain.registry.model.index.ForeignKeyIndex;
import com.google.domain.registry.model.poll.PendingActionNotificationResponse;
import com.google.domain.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
import com.google.domain.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.model.transfer.TransferData;
import com.google.domain.registry.model.transfer.TransferResponse;
import com.google.domain.registry.model.transfer.TransferResponse.ContactTransferResponse;
import com.google.domain.registry.model.transfer.TransferResponse.DomainTransferResponse;
import com.google.domain.registry.model.transfer.TransferStatus;
import org.joda.time.DateTime;
/** Static utility functions for resource transfer flows. */
public class ResourceFlowUtils {
/** Statuses for which an exDate should be added to transfer responses. */
private static final ImmutableSet<TransferStatus> ADD_EXDATE_STATUSES = Sets.immutableEnumSet(
TransferStatus.PENDING, TransferStatus.CLIENT_APPROVED, TransferStatus.SERVER_APPROVED);
/**
* Create a transfer response using the id and type of this resource and the specified
* {@link TransferData}.
*/
public static TransferResponse createTransferResponse(
EppResource eppResource, TransferData transferData, DateTime now) {
assertIsContactOrDomain(eppResource);
TransferResponse.Builder<? extends TransferResponse, ?> builder;
if (eppResource instanceof ContactResource) {
builder = new ContactTransferResponse.Builder().setContactId(eppResource.getForeignKey());
} else {
DomainResource domain = (DomainResource) eppResource;
builder = new DomainTransferResponse.Builder()
.setFullyQualifiedDomainNameName(eppResource.getForeignKey())
.setExtendedRegistrationExpirationTime(
ADD_EXDATE_STATUSES.contains(transferData.getTransferStatus())
? extendRegistrationWithCap(
now,
domain.getRegistrationExpirationTime(),
transferData.getExtendedRegistrationYears())
: null);
}
builder.setGainingClientId(transferData.getGainingClientId())
.setLosingClientId(transferData.getLosingClientId())
.setPendingTransferExpirationTime(transferData.getPendingTransferExpirationTime())
.setTransferRequestTime(transferData.getTransferRequestTime())
.setTransferStatus(transferData.getTransferStatus());
return builder.build();
}
/**
* Create a pending action notification response indicating the resolution of a transfer.
* <p>
* The returned object will use the id and type of this resource, the trid of the resource's last
* transfer request, and the specified status and date.
*/
public static PendingActionNotificationResponse createPendingTransferNotificationResponse(
EppResource eppResource,
Trid transferRequestTrid,
boolean actionResult,
DateTime processedDate) {
assertIsContactOrDomain(eppResource);
return eppResource instanceof ContactResource
? ContactPendingActionNotificationResponse.create(
eppResource.getForeignKey(), actionResult, transferRequestTrid, processedDate)
: DomainPendingActionNotificationResponse.create(
eppResource.getForeignKey(), actionResult, transferRequestTrid, processedDate);
}
private static void assertIsContactOrDomain(EppResource eppResource) {
checkState(eppResource instanceof ContactResource || eppResource instanceof DomainResource);
}
/** Check that the given clientId corresponds to the owner of given resource. */
public static void verifyResourceOwnership(String myClientId, EppResource resource)
throws EppException {
if (!myClientId.equals(resource.getCurrentSponsorClientId())) {
throw new ResourceNotOwnedException();
}
}
/**
* Performs common deletion operations on an EPP resource and returns a builder for further
* modifications. This is broken out into ResourceFlowUtils in order to expose the functionality
* to async flows (i.e. mapreduces).
*/
@SuppressWarnings("unchecked")
public static <R extends EppResource> Builder<R, ? extends Builder<R, ?>>
prepareDeletedResourceAsBuilder(R existingResource, DateTime now) {
Builder<R, ? extends Builder<R, ?>> builder =
(Builder<R, ? extends Builder<R, ?>>) existingResource.asBuilder()
.setDeletionTime(now)
.setStatusValues(null)
.setTransferData(
existingResource.getStatusValues().contains(StatusValue.PENDING_TRANSFER)
? existingResource.getTransferData().asBuilder()
.setTransferStatus(TransferStatus.SERVER_CANCELLED)
.setServerApproveEntities(null)
.setServerApproveBillingEvent(null)
.setServerApproveAutorenewEvent(null)
.setServerApproveAutorenewPollMessage(null)
.setPendingTransferExpirationTime(null)
.build()
: existingResource.getTransferData())
.wipeOut();
return builder;
}
/** Update the relevant {@link ForeignKeyIndex} to cache the new deletion time. */
public static <R extends EppResource> void updateForeignKeyIndexDeletionTime(R resource) {
if (resource instanceof ForeignKeyedEppResource) {
ofy().save().entity(ForeignKeyIndex.create(resource, resource.getDeletionTime()));
}
}
/** If there is a transfer out, delete the server-approve entities and enqueue a poll message. */
public static <R extends EppResource> void handlePendingTransferOnDelete(
R existingResource, R newResource, DateTime now, HistoryEntry historyEntry) {
if (existingResource.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) {
TransferData oldTransferData = existingResource.getTransferData();
ofy().delete().keys(oldTransferData.getServerApproveEntities());
ofy().save().entity(new PollMessage.OneTime.Builder()
.setClientId(oldTransferData.getGainingClientId())
.setEventTime(now)
.setMsg(TransferStatus.SERVER_CANCELLED.getMessage())
.setResponseData(ImmutableList.of(
createTransferResponse(newResource, newResource.getTransferData(), now),
createPendingTransferNotificationResponse(
existingResource, oldTransferData.getTransferRequestTrid(), false, now)))
.setParent(historyEntry)
.build());
}
}
/** The specified resource belongs to another client. */
public static class ResourceNotOwnedException extends AuthorizationErrorException {
public ResourceNotOwnedException() {
super("The specified resource belongs to another client");
}
}
/** Check that the given AuthInfo is valid for the given resource. */
public static void verifyAuthInfoForResource(AuthInfo authInfo, EppResource resource)
throws EppException {
try {
authInfo.verifyAuthorizedFor(resource);
} catch (BadAuthInfoException e) {
throw new BadAuthInfoForResourceException();
}
}
/** Authorization information for accessing resource is invalid. */
public static class BadAuthInfoForResourceException
extends InvalidAuthorizationInformationErrorException {
public BadAuthInfoForResourceException() {
super("Authorization information for accessing resource is invalid");
}
}
}

View file

@ -0,0 +1,49 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.model.EppResourceUtils.cloneResourceWithLinkedStatus;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.eppoutput.Response.ResponseData;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
/**
* An EPP flow that reads a storable resource.
*
* @param <R> the resource type being manipulated
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceInfoFlow<R extends EppResource, C extends SingleResourceCommand>
extends ResourceQueryFlow<R, C> {
@Override
public EppOutput runResourceFlow() throws EppException {
return createOutput(Success, getResourceInfo(), getResponseExtensions());
}
@SuppressWarnings("unused")
protected ResponseData getResourceInfo() throws EppException {
return cloneResourceWithLinkedStatus(existingResource, now);
}
@SuppressWarnings("unused")
protected ImmutableList<? extends ResponseExtension> getResponseExtensions() throws EppException {
return null;
}
}

View file

@ -0,0 +1,64 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource;
import com.google.domain.registry.flows.EppException.ObjectDoesNotExistException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.util.TypeUtils.TypeInstantiator;
/**
* An EPP flow that mutates a single stored resource.
*
* @param <R> the resource type being changed
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceMutateFlow<R extends EppResource, C extends SingleResourceCommand>
extends ResourceCreateOrMutateFlow<R, C> {
@Override
protected void initRepoId() {
// existingResource could be null here if the flow is being called to mutate a resource that
// does not exist, in which case don't throw NPE here and allow the non-existence to be handled
// later.
repoId = (existingResource == null) ? null : existingResource.getRepoId();
}
/** Fail if the object doesn't exist or was deleted. */
@Override
protected final void verifyIsAllowed() throws EppException {
if (existingResource == null) {
throw new ResourceToMutateDoesNotExistException(
new TypeInstantiator<R>(getClass()){}.getExactType(), targetId);
}
if (command.getAuthInfo() != null) {
verifyAuthInfoForResource(command.getAuthInfo(), existingResource);
}
verifyMutationAllowed();
}
/** Check invariants before allowing the command to proceed. */
@SuppressWarnings("unused")
protected void verifyMutationAllowed() throws EppException {}
/** Resource with this id does not exist. */
public static class ResourceToMutateDoesNotExistException extends ObjectDoesNotExistException {
public ResourceToMutateDoesNotExistException(Class<?> type, String targetId) {
super(type, targetId);
}
}
}

View file

@ -0,0 +1,104 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.ResourceFlowUtils.createTransferResponse;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.domain.registry.flows.EppException.ObjectNotPendingTransferException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.EppResource.Builder;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.transfer.TransferData;
import com.google.domain.registry.model.transfer.TransferStatus;
/**
* An EPP flow that acts on a resource with a pending transfer on it.
*
* @param <R> the resource type being changed
* @param <B> a builder for the resource
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceMutatePendingTransferFlow
<R extends EppResource, B extends Builder<R, ?>, C extends SingleResourceCommand>
extends ResourceTransferFlow<R, C> {
/** Fail if object doesn't have a pending transfer, or if authinfo doesn't match. */
@Override
protected final void verifyMutationAllowed() throws EppException {
if (existingResource.getTransferData().getTransferStatus() != TransferStatus.PENDING) {
throw new NotPendingTransferException(targetId);
}
verifyPendingTransferMutationAllowed();
}
@SuppressWarnings("unused")
protected void verifyPendingTransferMutationAllowed() throws EppException {}
@Override
@SuppressWarnings("unchecked")
protected final R createOrMutateResource() {
TransferData transferData = existingResource.getTransferData();
B builder = (B) existingResource.asBuilder()
.removeStatusValue(StatusValue.PENDING_TRANSFER)
.setTransferData(transferData.asBuilder()
.setTransferStatus(getTransferStatus())
.setPendingTransferExpirationTime(now)
.setExtendedRegistrationYears(null)
.setServerApproveEntities(null)
.setServerApproveBillingEvent(null)
.setServerApproveAutorenewEvent(null)
.setServerApproveAutorenewPollMessage(null)
.build());
setTransferMutateProperties(builder);
return builder.build();
}
/** Get the new transfer status to set on the resource (and subordinates) after the flow. */
protected abstract TransferStatus getTransferStatus();
/** Set any resource-specific properties for the pending-transfer mutation. */
protected void setTransferMutateProperties(@SuppressWarnings("unused") B builder) {}
/**
* Delete the billing event and poll messages that were written in case the transfer would have
* been implicitly server approved.
*/
@Override
protected final void modifyRelatedResources() throws EppException {
modifyRelatedResourcesForMutateTransfer();
ofy().delete().keys(existingResource.getTransferData().getServerApproveEntities());
}
/** Subclasses can override this to make any other model changes that are implied by this flow. */
@SuppressWarnings("unused")
protected void modifyRelatedResourcesForMutateTransfer() throws EppException {}
@Override
protected final EppOutput getOutput() throws EppException {
return createOutput(
Success, createTransferResponse(newResource, newResource.getTransferData(), now));
}
/** The resource does not have a pending transfer. */
public static class NotPendingTransferException extends ObjectNotPendingTransferException {
public NotPendingTransferException(String objectId) {
super(objectId);
}
}
}

View file

@ -0,0 +1,55 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource;
import com.google.domain.registry.flows.EppException.ObjectDoesNotExistException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.util.TypeUtils.TypeInstantiator;
/**
* An EPP flow that queries a storable resource.
*
* @param <R> the resource type being queried
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceQueryFlow<R extends EppResource, C extends SingleResourceCommand>
extends SingleResourceFlow<R, C> {
/** Fail if the object doesn't exist or was deleted. */
@Override
protected final void verifyIsAllowed() throws EppException {
if (existingResource == null) {
throw new ResourceToQueryDoesNotExistException(
new TypeInstantiator<R>(getClass()){}.getExactType(), targetId);
}
if (command.getAuthInfo() != null) {
verifyAuthInfoForResource(command.getAuthInfo(), existingResource);
}
verifyQueryIsAllowed();
}
/** Check command- and resource-specific invariants before allowing the query to proceed. */
@SuppressWarnings("unused")
protected void verifyQueryIsAllowed() throws EppException {}
/** Resource with this id does not exist. */
public static class ResourceToQueryDoesNotExistException extends ObjectDoesNotExistException {
public ResourceToQueryDoesNotExistException(Class<?> type, String targetId) {
super(type, targetId);
}
}
}

View file

@ -0,0 +1,59 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.ResourceFlowUtils.handlePendingTransferOnDelete;
import static com.google.domain.registry.flows.ResourceFlowUtils.prepareDeletedResourceAsBuilder;
import static com.google.domain.registry.flows.ResourceFlowUtils.updateForeignKeyIndexDeletionTime;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.EppResource.Builder;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.index.ForeignKeyIndex;
/**
* An EPP flow that deletes a resource synchronously.
*
* @param <R> the resource type being changed
* @param <B> a builder for the resource
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceSyncDeleteFlow
<R extends EppResource, B extends Builder<R, ?>, C extends SingleResourceCommand>
extends ResourceDeleteFlow<R, C> {
@Override
@SuppressWarnings("unchecked")
protected final R createOrMutateResource() {
B builder = (B) prepareDeletedResourceAsBuilder(existingResource, now);
setDeleteProperties(builder);
return builder.build();
}
/** Update the relevant {@link ForeignKeyIndex} to cache the new deletion time. */
@Override
protected final void modifyRelatedResources() throws EppException {
updateForeignKeyIndexDeletionTime(newResource);
handlePendingTransferOnDelete(existingResource, newResource, now, historyEntry);
modifySyncDeleteRelatedResources();
}
/** Set any resource-specific properties before deleting. */
@SuppressWarnings("unused")
protected void setDeleteProperties(B builder) {}
/** Modify any other resources that need to be informed of this delete. */
protected void modifySyncDeleteRelatedResources() {}
}

View file

@ -0,0 +1,73 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.ResourceFlowUtils.createPendingTransferNotificationResponse;
import static com.google.domain.registry.flows.ResourceFlowUtils.createTransferResponse;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.EppResource.Builder;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.transfer.TransferData;
import com.google.domain.registry.model.transfer.TransferStatus;
/**
* An EPP flow that approves a transfer on a resource.
*
* @param <R> the resource type being manipulated
* @param <B> a builder for the resource
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceTransferApproveFlow
<R extends EppResource, B extends Builder<R, ?>, C extends SingleResourceCommand>
extends OwnedResourceMutatePendingTransferFlow<R, B, C> {
@Override
protected final TransferStatus getTransferStatus() {
return TransferStatus.CLIENT_APPROVED;
}
@Override
protected final void setTransferMutateProperties(B builder) {
builder.setLastTransferTime(now)
.setCurrentSponsorClientId(existingResource.getTransferData().getGainingClientId());
setTransferApproveProperties(builder);
}
protected void setTransferApproveProperties(@SuppressWarnings("unused") B builder) {}
@Override
protected void modifyRelatedResourcesForMutateTransfer() throws EppException {
// Create a poll message for the gaining client.
TransferData oldTransferData = existingResource.getTransferData();
ofy().save().entity(new PollMessage.OneTime.Builder()
.setClientId(oldTransferData.getGainingClientId())
.setEventTime(now)
.setMsg(TransferStatus.CLIENT_APPROVED.getMessage())
.setResponseData(ImmutableList.of(
createTransferResponse(newResource, newResource.getTransferData(), now),
createPendingTransferNotificationResponse(
existingResource, oldTransferData.getTransferRequestTrid(), true, now)))
.setParent(historyEntry)
.build());
modifyRelatedResourcesForTransferApprove();
}
/** Subclasses can override this to modify other transfer-related resources. */
protected void modifyRelatedResourcesForTransferApprove() {}
}

View file

@ -0,0 +1,79 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.ResourceFlowUtils.createTransferResponse;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.flows.EppException.AuthorizationErrorException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.EppResource.Builder;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.transfer.TransferStatus;
/**
* An EPP flow that cancels a transfer on a resource.
*
* @param <R> the resource type being manipulated
* @param <B> a builder for the resource
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceTransferCancelFlow
<R extends EppResource, B extends Builder<R, ?>, C extends SingleResourceCommand>
extends ResourceMutatePendingTransferFlow<R, B, C> {
/** Verify that this is the correct client to cancel this pending transfer. */
@Override
protected final void verifyPendingTransferMutationAllowed() throws EppException {
// TODO(b/18997997): Determine if authInfo is necessary to cancel a transfer.
if (!getClientId().equals(existingResource.getTransferData().getGainingClientId())) {
throw new NotTransferInitiatorException();
}
verifyTransferCancelMutationAllowed();
}
@SuppressWarnings("unused")
protected void verifyTransferCancelMutationAllowed() throws EppException {}
@Override
protected void modifyRelatedResourcesForMutateTransfer() throws EppException {
ofy().save().entity(new PollMessage.OneTime.Builder()
.setClientId(existingResource.getTransferData().getLosingClientId())
.setEventTime(now)
.setMsg(TransferStatus.CLIENT_CANCELLED.getMessage())
.setResponseData(ImmutableList.of(
createTransferResponse(newResource, newResource.getTransferData(), now)))
.setParent(historyEntry)
.build());
modifyRelatedResourcesForTransferCancel();
}
/** Subclasses can override this to modify other cancellation-related resources. */
protected void modifyRelatedResourcesForTransferCancel() {}
@Override
protected final TransferStatus getTransferStatus() {
return TransferStatus.CLIENT_CANCELLED;
}
/** Registrar is not the initiator of this transfer. */
public static class NotTransferInitiatorException extends AuthorizationErrorException {
public NotTransferInitiatorException() {
super("Registrar is not the initiator of this transfer");
}
}
}

View file

@ -0,0 +1,27 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
/**
* An EPP flow that involves a transfer on a resource.
*
* @param <R> the resource type being manipulated
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceTransferFlow
<R extends EppResource, C extends SingleResourceCommand> extends ResourceMutateFlow<R, C> {}

View file

@ -0,0 +1,72 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.ResourceFlowUtils.createTransferResponse;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import com.google.domain.registry.flows.EppException.AuthorizationErrorException;
import com.google.domain.registry.flows.EppException.CommandUseErrorException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.eppoutput.EppOutput;
/**
* An EPP flow that queries the state of a pending transfer on a resource.
*
* @param <R> the resource type being manipulated
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceTransferQueryFlow<R extends EppResource,
C extends SingleResourceCommand> extends ResourceQueryFlow<R, C> {
@Override
protected final void verifyQueryIsAllowed() throws EppException {
// Most of the fields on the transfer response are required, so there's no way to return valid
// XML if the object has never been transferred (and hence the fields aren't populated).
if (existingResource.getTransferData().getTransferStatus() == null) {
throw new NoTransferHistoryToQueryException();
}
// Note that the authorization info on the command (if present) has already been verified by the
// parent class. If it's present, then the other checks are unnecessary.
if (command.getAuthInfo() == null &&
!getClientId().equals(existingResource.getTransferData().getGainingClientId()) &&
!getClientId().equals(existingResource.getTransferData().getLosingClientId())) {
throw new NotAuthorizedToViewTransferException();
}
}
@Override
public final EppOutput runResourceFlow() throws EppException {
return createOutput(
Success, createTransferResponse(existingResource, existingResource.getTransferData(), now));
}
/** Registrar is not authorized to view transfer status. */
public static class NotAuthorizedToViewTransferException
extends AuthorizationErrorException {
public NotAuthorizedToViewTransferException() {
super("Registrar is not authorized to view transfer status");
}
}
/** Object has no transfer history. */
public static class NoTransferHistoryToQueryException extends CommandUseErrorException {
public NoTransferHistoryToQueryException() {
super("Object has no transfer history");
}
}
}

View file

@ -0,0 +1,63 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.ResourceFlowUtils.createPendingTransferNotificationResponse;
import static com.google.domain.registry.flows.ResourceFlowUtils.createTransferResponse;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.EppResource.Builder;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.transfer.TransferData;
import com.google.domain.registry.model.transfer.TransferStatus;
/**
* An EPP flow that rejects a transfer on a resource.
*
* @param <R> the resource type being manipulated
* @param <B> a builder for the resource
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceTransferRejectFlow
<R extends EppResource, B extends Builder<R, ?>, C extends SingleResourceCommand>
extends OwnedResourceMutatePendingTransferFlow<R, B, C> {
@Override
protected final TransferStatus getTransferStatus() {
return TransferStatus.CLIENT_REJECTED;
}
@Override
protected void modifyRelatedResourcesForMutateTransfer() throws EppException {
TransferData oldTransferData = existingResource.getTransferData();
ofy().save().entity(new PollMessage.OneTime.Builder()
.setClientId(oldTransferData.getGainingClientId())
.setEventTime(now)
.setMsg(TransferStatus.CLIENT_REJECTED.getMessage())
.setResponseData(ImmutableList.of(
createTransferResponse(newResource, newResource.getTransferData(), now),
createPendingTransferNotificationResponse(
existingResource, oldTransferData.getTransferRequestTrid(), false, now)))
.setParent(historyEntry)
.build());
modifyRelatedResourcesForTransferReject();
}
/** Subclasses can override this to modify other rejection-related resources. */
protected void modifyRelatedResourcesForTransferReject() {}
}

View file

@ -0,0 +1,220 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.flows.ResourceFlowUtils.createPendingTransferNotificationResponse;
import static com.google.domain.registry.flows.ResourceFlowUtils.createTransferResponse;
import static com.google.domain.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource;
import static com.google.domain.registry.model.eppoutput.Result.Code.SuccessWithActionPending;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.CollectionUtils.union;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.flows.EppException.AuthorizationErrorException;
import com.google.domain.registry.flows.EppException.CommandUseErrorException;
import com.google.domain.registry.flows.EppException.ObjectPendingTransferException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.eppoutput.Response.ResponseData;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.transfer.TransferData;
import com.google.domain.registry.model.transfer.TransferData.TransferServerApproveEntity;
import com.google.domain.registry.model.transfer.TransferStatus;
import com.googlecode.objectify.Key;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import java.util.Set;
/**
* An EPP flow that requests a transfer on a resource.
*
* @param <R> the resource type being manipulated
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceTransferRequestFlow
<R extends EppResource, C extends SingleResourceCommand> extends ResourceTransferFlow<R, C> {
private static final Set<StatusValue> TRANSFER_DISALLOWED_STATUSES = ImmutableSet.of(
StatusValue.CLIENT_TRANSFER_PROHIBITED,
StatusValue.PENDING_DELETE,
StatusValue.SERVER_TRANSFER_PROHIBITED);
private DateTime transferExpirationTime;
/** Helper class to identify the two clients. */
protected abstract class Client {
public abstract String getId();
}
/** The gaining client. */
protected Client gainingClient = new Client() {
@Override
public String getId() {
return getClientId();
}};
/** The losing client. */
protected Client losingClient = new Client() {
@Override
public String getId() {
return existingResource.getCurrentSponsorClientId();
}};
@Override
protected final void initResourceCreateOrMutateFlow() {
initResourceTransferRequestFlow();
}
protected abstract Duration getAutomaticTransferLength();
@Override
protected final void verifyMutationAllowed() throws EppException {
// Verify that the resource does not already have a pending transfer.
if (TransferStatus.PENDING.equals(existingResource.getTransferData().getTransferStatus())) {
throw new AlreadyPendingTransferException(targetId);
}
// Verify that this client doesn't already sponsor this resource.
if (gainingClient.getId().equals(losingClient.getId())) {
throw new ObjectAlreadySponsoredException();
}
if (command.getAuthInfo() == null) {
throw new MissingTransferRequestAuthInfoException();
}
verifyAuthInfoForResource(command.getAuthInfo(), existingResource);
verifyTransferRequestIsAllowed();
}
private TransferData.Builder createTransferDataBuilder(TransferStatus transferStatus) {
TransferData.Builder builder = new TransferData.Builder()
.setGainingClientId(gainingClient.getId())
.setTransferRequestTime(now)
.setLosingClientId(losingClient.getId())
.setPendingTransferExpirationTime(transferExpirationTime)
.setTransferRequestTrid(trid)
.setTransferStatus(transferStatus);
setTransferDataProperties(builder);
return builder;
}
private PollMessage createPollMessage(
Client client, TransferStatus transferStatus, DateTime eventTime) {
ImmutableList.Builder<ResponseData> responseData = new ImmutableList.Builder<>();
responseData.add(createTransferResponse(
existingResource, createTransferDataBuilder(transferStatus).build(), now));
if (client.getId().equals(gainingClient.getId())) {
responseData.add(createPendingTransferNotificationResponse(
existingResource, trid, true, now));
}
return new PollMessage.OneTime.Builder()
.setClientId(client.getId())
.setEventTime(eventTime)
.setMsg(transferStatus.getMessage())
.setResponseData(responseData.build())
.setParent(historyEntry)
.build();
}
@Override
@SuppressWarnings("unchecked")
protected final R createOrMutateResource() {
// Figure out transfer expiration time once we've verified that the existingResource does in
// fact exist (otherwise we won't know which TLD to get this figure off of).
transferExpirationTime = now.plus(getAutomaticTransferLength());
// When a transfer is requested, a poll message is created to notify the losing registrar.
PollMessage requestPollMessage = createPollMessage(losingClient, TransferStatus.PENDING, now);
// If the transfer is server approved, this message will be sent to the gaining registrar. */
PollMessage serverApproveGainingPollMessage =
createPollMessage(gainingClient, TransferStatus.SERVER_APPROVED, transferExpirationTime);
// If the transfer is server approved, this message will be sent to the losing registrar. */
PollMessage serverApproveLosingPollMessage =
createPollMessage(losingClient, TransferStatus.SERVER_APPROVED, transferExpirationTime);
ofy().save().entities(
requestPollMessage, serverApproveGainingPollMessage, serverApproveLosingPollMessage);
return (R) existingResource.asBuilder()
.setTransferData(createTransferDataBuilder(TransferStatus.PENDING)
.setServerApproveEntities(union(
getTransferServerApproveEntities(),
Key.create(serverApproveGainingPollMessage),
Key.create(serverApproveLosingPollMessage)))
.build())
.addStatusValue(StatusValue.PENDING_TRANSFER)
.build();
}
/** Subclasses can override this to do further initialization. */
protected void initResourceTransferRequestFlow() {}
/**
* Subclasses can override this to return the keys of any entities that need to be deleted if the
* transfer ends in any state other than SERVER_APPROVED.
*/
protected Set<Key<? extends TransferServerApproveEntity>> getTransferServerApproveEntities() {
return ImmutableSet.of();
}
/** Check resource-specific invariants before allowing the transfer request to proceed. */
@SuppressWarnings("unused")
protected void verifyTransferRequestIsAllowed() throws EppException {}
/** Subclasses can override this to modify fields on the transfer data builder. */
protected void setTransferDataProperties(
@SuppressWarnings("unused") TransferData.Builder builder) {}
@Override
protected final EppOutput getOutput() throws EppException {
return createOutput(
SuccessWithActionPending,
createTransferResponse(newResource, newResource.getTransferData(), now),
getTransferResponseExtensions());
}
/** Subclasses can override this to return response extensions. */
protected ImmutableList<? extends ResponseExtension> getTransferResponseExtensions() {
return null;
}
@Override
protected final Set<StatusValue> getDisallowedStatuses() {
return TRANSFER_DISALLOWED_STATUSES;
}
/** Authorization info is required to request a transfer. */
public static class MissingTransferRequestAuthInfoException extends AuthorizationErrorException {
public MissingTransferRequestAuthInfoException() {
super("Authorization info is required to request a transfer");
}
}
/** Registrar already sponsors the object of this transfer request. */
public static class ObjectAlreadySponsoredException extends CommandUseErrorException {
public ObjectAlreadySponsoredException() {
super("Registrar already sponsors the object of this transfer request");
}
}
/** The resource is already pending transfer. */
public static class AlreadyPendingTransferException extends ObjectPendingTransferException {
public AlreadyPendingTransferException(String targetId) {
super(targetId);
}
}
}

View file

@ -0,0 +1,133 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.domain.registry.flows.EppException.ParameterValuePolicyErrorException;
import com.google.domain.registry.flows.EppException.ParameterValueRangeErrorException;
import com.google.domain.registry.flows.EppException.StatusProhibitsOperationException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.EppResource.Builder;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.eppinput.ResourceCommand.AddRemoveSameValueException;
import com.google.domain.registry.model.eppinput.ResourceCommand.ResourceUpdate;
import com.google.domain.registry.model.eppoutput.EppOutput;
import java.util.Set;
/**
* An EPP flow that mutates a single stored resource.
*
* @param <R> the resource type being changed
* @param <B> a builder for the resource
* @param <C> the command type, marshalled directly from the epp xml
*/
public abstract class ResourceUpdateFlow
<R extends EppResource, B extends Builder<R, ?>, C extends ResourceUpdate<?, ? super B, ?>>
extends OwnedResourceMutateFlow<R, C> {
/**
* Note that CLIENT_UPDATE_PROHIBITED is intentionally not in this list. This is because it
* requires special checking, since you must be able to clear the status off the object with an
* update.
*/
private static final Set<StatusValue> UPDATE_DISALLOWED_STATUSES = ImmutableSet.of(
StatusValue.PENDING_DELETE,
StatusValue.SERVER_UPDATE_PROHIBITED);
@Override
protected Set<StatusValue> getDisallowedStatuses() {
return UPDATE_DISALLOWED_STATUSES;
}
@Override
protected final void verifyMutationOnOwnedResourceAllowed() throws EppException {
for (StatusValue statusValue : Sets.union(
command.getInnerAdd().getStatusValues(),
command.getInnerRemove().getStatusValues())) {
if (!superuser && !statusValue.isClientSettable()) { // The superuser can set any status.
throw new StatusNotClientSettableException(statusValue.getXmlName());
}
}
verifyUpdateIsAllowed();
}
@Override
protected final R createOrMutateResource() throws EppException {
@SuppressWarnings("unchecked")
B builder = (B) existingResource.asBuilder();
try {
command.applyTo(builder);
} catch (AddRemoveSameValueException e) {
throw new AddRemoveSameValueEppException();
}
builder.setLastEppUpdateTime(now).setLastEppUpdateClientId(getClientId());
return setUpdateProperties(builder).build();
}
@Override
protected final void verifyNewStateIsAllowed() throws EppException {
// If the resource is marked with clientUpdateProhibited, and this update did not clear that
// status, then the update must be disallowed (unless a superuser is requesting the change).
if (!superuser
&& existingResource.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
&& newResource.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
throw new ResourceHasClientUpdateProhibitedException();
}
verifyNewUpdatedStateIsAllowed();
}
/** Subclasses may override this to do more specific checks on the new state after the update. */
@SuppressWarnings("unused")
protected void verifyNewUpdatedStateIsAllowed() throws EppException {}
@SuppressWarnings("unused")
protected void verifyUpdateIsAllowed() throws EppException {}
@SuppressWarnings("unused")
protected B setUpdateProperties(B builder) throws EppException {
return builder;
}
@Override
protected final EppOutput getOutput() {
return createOutput(Success);
}
/** The specified status value cannot be set by clients. */
public static class StatusNotClientSettableException extends ParameterValueRangeErrorException {
public StatusNotClientSettableException(String statusValue) {
super(String.format("Status value %s cannot be set by clients", statusValue));
}
}
/** This resource has clientUpdateProhibited on it, and the update does not clear that status. */
public static class ResourceHasClientUpdateProhibitedException
extends StatusProhibitsOperationException {
public ResourceHasClientUpdateProhibitedException() {
super("Operation disallowed by status: clientUpdateProhibited");
}
}
/** Cannot add and remove the same value. */
public static class AddRemoveSameValueEppException extends ParameterValuePolicyErrorException {
public AddRemoveSameValueEppException() {
super("Cannot add and remove the same value");
}
}
}

View file

@ -0,0 +1,162 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.domain.registry.util.CollectionUtils.nullToEmpty;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import java.util.Set;
/** Class to allow setting and retrieving session information in flows. */
public abstract class SessionMetadata {
/**
* An enum that identifies the origin of the session.
*/
public enum SessionSource {
/** e.g. {@code EppConsoleServlet} */
CONSOLE,
/** e.g. {@code EppTlsServlet} */
HTTP,
/** e.g. {@code EppToolServlet} */
TOOL,
/** e.g. {@code LoadTestAction} */
LOADTEST,
/** Direct flow runs (default for e.g. testing) */
NONE
}
private TransportCredentials credentials;
/** The key used for looking up the current client id on the session object. */
protected static final String CLIENT_ID_KEY = "CLIENT_ID";
/** The key used for looking up the superuser bit on the session object. */
protected static final String SUPERUSER_KEY = "SUPERUSER";
/** The key used for looking up the service extensions on the session object. */
protected static final String SERVICE_EXTENSIONS_KEY = "SERVICE_EXTENSIONS";
/** The key used for looking up the number of failed login attempts. */
protected static final String FAILED_LOGIN_ATTEMPTS_KEY = "FAILED_LOGIN_ATTEMPTS";
protected abstract void setProperty(String key, Object value);
protected abstract Object getProperty(String key);
/**
* Invalidates the session. A new instance must be created after this for future sessions.
* Attempts to invoke methods of this class after this method has been called will throw
* {@code IllegalStateException}.
*/
public abstract void invalidate();
/** Subclasses can override this to verify that this is a valid session. */
protected void checkValid() {}
protected void setPropertyChecked(String key, Object value) {
checkValid();
setProperty(key, value);
}
@SuppressWarnings("unchecked")
protected <T> T getPropertyChecked(String key) {
checkValid();
return (T) getProperty(key);
}
public TransportCredentials getTransportCredentials() {
checkValid();
return credentials;
}
public void setTransportCredentials(TransportCredentials credentials) {
checkValid();
this.credentials = credentials;
}
public String getClientId() {
return getPropertyChecked(CLIENT_ID_KEY);
}
public boolean isSuperuser() {
return Boolean.TRUE.equals(getPropertyChecked(SUPERUSER_KEY));
}
public Set<String> getServiceExtensionUris() {
return getPropertyChecked(SERVICE_EXTENSIONS_KEY);
}
public abstract SessionSource getSessionSource();
/**
* Subclasses can override if they present a need to change the session
* source at runtime (e.g. anonymous classes created for testing)
*/
public void setSessionSource(@SuppressWarnings("unused") SessionSource source) {
throw new UnsupportedOperationException();
}
public void setClientId(String clientId) {
setPropertyChecked(CLIENT_ID_KEY, clientId);
}
public void setSuperuser(boolean superuser) {
setPropertyChecked(SUPERUSER_KEY, superuser);
}
public void setServiceExtensionUris(Set<String> serviceExtensionUris) {
setPropertyChecked(SERVICE_EXTENSIONS_KEY, checkNotNull(serviceExtensionUris));
}
public int getFailedLoginAttempts() {
return ((Integer) Optional.fromNullable(getPropertyChecked(FAILED_LOGIN_ATTEMPTS_KEY)).or(0));
}
public void incrementFailedLoginAttempts() {
setPropertyChecked(FAILED_LOGIN_ATTEMPTS_KEY, getFailedLoginAttempts() + 1);
}
public void resetFailedLoginAttempts() {
setPropertyChecked(FAILED_LOGIN_ATTEMPTS_KEY, null);
}
// These three methods are here to allow special permissions if a derived class overrides them.
public boolean isDryRun() {
return false;
}
@Override
public String toString() {
return toStringHelper(getClass())
.add("system hash code", System.identityHashCode(this))
.add("clientId", getClientId())
.add("isSuperuser", isSuperuser())
.add("failedLoginAttempts", getFailedLoginAttempts())
.add("sessionSource", getSessionSource())
.add("serviceExtensionUris", Joiner.on('.').join(nullToEmpty(getServiceExtensionUris())))
.add("transportCredentials", getTransportCredentials())
.toString();
}
}

View file

@ -0,0 +1,104 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.domain.registry.model.EppResourceUtils.loadByUniqueId;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.domain.registry.flows.EppException.StatusProhibitsOperationException;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.domain.launch.ApplicationIdTargetExtension;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import java.util.Set;
/**
* An EPP flow that manipulates a single stored resource.
*
* @param <R> the resource type being manipulated
* @param <C> the command type doing the manipulation.
*/
public abstract class SingleResourceFlow<R extends EppResource, C extends SingleResourceCommand>
extends ResourceFlow<R, C> {
protected R existingResource;
protected String targetId;
@Override
protected final void initResourceFlow() throws EppException {
targetId = getTargetId();
// In a transactional flow, loading the resource will be expensive because it can't be cached.
// Allow flows to optionally fail fast here before loading.
failfast();
// Loads the target resource if it exists
// Some flows such as DomainApplicationInfoFlow have the id marked as optional in the schema.
// We require it by policy in the relevant flow, but here we need to make sure not to NPE when
// initializing the (obviously nonexistent) existing resource.
existingResource = (targetId == null || !tryToLoadExisting())
? null
: loadByUniqueId(resourceClass, targetId, now);
if (existingResource != null) {
Set<StatusValue> problems = Sets.intersection(
existingResource.getStatusValues(), getDisallowedStatuses());
if (!problems.isEmpty()) {
throw new ResourceStatusProhibitsOperationException(problems);
}
}
initSingleResourceFlow();
}
/**
* Returns whether the resource flow should attempt to load an existing resource with the
* matching targetId. Defaults to true, but overriding flows can set to false to bypass loading
* of existing resources.
*/
protected boolean tryToLoadExisting() {
return true;
}
/**
* Get the target id from {@link SingleResourceCommand}. If there is a launch extension present,
* it overrides that target id with its application id, so return that instead. There will never
* be more than one launch extension.
*/
protected final String getTargetId() {
ApplicationIdTargetExtension extension =
eppInput.getSingleExtension(ApplicationIdTargetExtension.class);
return extension == null ? command.getTargetId() : extension.getApplicationId();
}
/** Subclasses can optionally override this to fail before loading {@link #existingResource}. */
@SuppressWarnings("unused")
protected void failfast() throws EppException {}
/** Subclasses can optionally override this for further initialization. */
@SuppressWarnings("unused")
protected void initSingleResourceFlow() throws EppException {}
protected Set<StatusValue> getDisallowedStatuses() {
return ImmutableSet.of();
}
/** Resource status prohibits this operation. */
public static class ResourceStatusProhibitsOperationException
extends StatusProhibitsOperationException {
public ResourceStatusProhibitsOperationException(Set<StatusValue> status) {
super("Operation disallowed by status: " + Joiner.on(", ").join(status));
}
}
}

View file

@ -0,0 +1,87 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import java.util.Set;
/** A read-only {@link SessionMetadata} that doesn't support login/logout. */
public class StatelessRequestSessionMetadata extends SessionMetadata {
private final String clientId;
private final boolean isSuperuser;
private final boolean isDryRun;
private final Set<String> serviceExtensionUris;
private final SessionSource sessionSource;
public StatelessRequestSessionMetadata(
String clientId,
boolean isSuperuser,
boolean isDryRun,
Set<String> serviceExtensionUris,
SessionSource source) {
this.clientId = clientId;
this.isSuperuser = isSuperuser;
this.isDryRun = isDryRun;
this.serviceExtensionUris = serviceExtensionUris;
this.sessionSource = source;
}
@Override
public String getClientId() {
return clientId;
}
@Override
public boolean isSuperuser() {
return isSuperuser;
}
@Override
public boolean isDryRun() {
return isDryRun;
}
@Override
public Set<String> getServiceExtensionUris() {
return serviceExtensionUris;
}
@Override
public SessionSource getSessionSource() {
return sessionSource;
}
@Override
public void invalidate() {
throw new UnsupportedOperationException();
}
@Override
public void setTransportCredentials(TransportCredentials credentials) {
throw new UnsupportedOperationException();
}
@Override
protected void setProperty(String key, Object value) {
throw new UnsupportedOperationException();
}
@Override
protected Object getProperty(String key) {
// We've overridden the getters of all of the properties that we care about. Return null for
// everything else so that toString() continues to work.
return null;
}
}

View file

@ -0,0 +1,180 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Strings.isNullOrEmpty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.net.HostAndPort;
import com.google.common.net.InetAddresses;
import com.google.domain.registry.flows.EppException.AuthenticationErrorException;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.util.CidrAddressBlock;
import com.google.domain.registry.util.FormattingLogger;
import java.net.InetAddress;
import javax.servlet.http.HttpServletRequest;
/**
* Container and validation for TLS certificate and ip-whitelisting.
*/
public final class TlsCredentials implements TransportCredentials {
/** Registrar certificate does not match stored certificate. */
public static class BadRegistrarCertificateException extends AuthenticationErrorException {
public BadRegistrarCertificateException() {
super("Registrar certificate does not match stored certificate");
}
}
/** Registrar certificate not present. */
public static class MissingRegistrarCertificateException extends AuthenticationErrorException {
public MissingRegistrarCertificateException() {
super("Registrar certificate not present");
}
}
/** SNI header is required. */
public static class NoSniException extends AuthenticationErrorException {
public NoSniException() {
super("SNI header is required");
}
}
/** Registrar IP address is not in stored whitelist. */
public static class BadRegistrarIpAddressException extends AuthenticationErrorException {
public BadRegistrarIpAddressException() {
super("Registrar IP address is not in stored whitelist");
}
}
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
private final String clientCertificateHash;
private final InetAddress clientInetAddr;
private final String sni;
@VisibleForTesting
public TlsCredentials(String clientCertificateHash, InetAddress clientInetAddr, String sni) {
this.clientCertificateHash = clientCertificateHash;
this.clientInetAddr = clientInetAddr;
this.sni = sni;
}
/**
* Extracts the client TLS certificate and source internet address
* from the given HTTP request.
*/
TlsCredentials(HttpServletRequest req) {
this(req.getHeader(EppTlsServlet.SSL_CLIENT_CERTIFICATE_HASH_FIELD),
parseInetAddress(req.getHeader(EppTlsServlet.FORWARDED_FOR_FIELD)),
req.getHeader(EppTlsServlet.REQUESTED_SERVERNAME_VIA_SNI_FIELD));
}
static InetAddress parseInetAddress(String asciiAddr) {
try {
return InetAddresses.forString(HostAndPort.fromString(asciiAddr).getHostText());
} catch (IllegalArgumentException e) {
return null;
}
}
@Override
public boolean performsLoginCheck() {
return false;
}
/** Returns {@code true} if frontend passed us the requested server name. */
boolean hasSni() {
return !isNullOrEmpty(sni);
}
@Override
public void validate(Registrar registrar) throws AuthenticationErrorException {
validateIp(registrar);
validateCertificate(registrar);
}
/**
* Verifies {@link #clientInetAddr} is in CIDR whitelist associated with {@code registrar}.
*
* @throws BadRegistrarIpAddressException If IP address is not in the whitelist provided
*/
private void validateIp(Registrar registrar) throws AuthenticationErrorException {
ImmutableList<CidrAddressBlock> ipWhitelist = registrar.getIpAddressWhitelist();
if (ipWhitelist.isEmpty()) {
logger.infofmt("Skipping IP whitelist check because %s doesn't have an IP whitelist",
registrar.getClientIdentifier());
return;
}
for (CidrAddressBlock cidrAddressBlock : ipWhitelist) {
if (cidrAddressBlock.contains(clientInetAddr)) {
// IP address is in whitelist; return early.
return;
}
}
logger.infofmt("%s not in %s's CIDR whitelist: %s",
clientInetAddr, registrar.getClientIdentifier(), ipWhitelist);
throw new BadRegistrarIpAddressException();
}
/**
* Verifies client SSL certificate is permitted to issue commands as {@code registrar}.
*
* @throws NoSniException if frontend didn't send host or certificate hash headers
* @throws MissingRegistrarCertificateException if frontend didn't send certificate hash header
* @throws BadRegistrarCertificateException if registrar requires certificate and it didn't match
*/
private void validateCertificate(Registrar registrar) throws AuthenticationErrorException {
if (isNullOrEmpty(registrar.getClientCertificateHash())
&& isNullOrEmpty(registrar.getFailoverClientCertificateHash())) {
logger.infofmt(
"Skipping SSL certificate check because %s doesn't have any certificate hashes on file",
registrar.getClientIdentifier());
return;
}
if (isNullOrEmpty(clientCertificateHash)) {
// If there's no SNI header that's probably why we don't have a cert, so send a specific
// message. Otherwise, send a missing certificate message.
if (!hasSni()) {
throw new NoSniException();
}
logger.infofmt("Request did not include %s", EppTlsServlet.SSL_CLIENT_CERTIFICATE_HASH_FIELD);
throw new MissingRegistrarCertificateException();
}
if (!clientCertificateHash.equals(registrar.getClientCertificateHash())
&& !clientCertificateHash.equals(registrar.getFailoverClientCertificateHash())) {
logger.warningfmt("bad certificate hash (%s) for %s, wanted either %s or %s",
clientCertificateHash,
registrar.getClientIdentifier(),
registrar.getClientCertificateHash(),
registrar.getFailoverClientCertificateHash());
throw new BadRegistrarCertificateException();
}
}
@Override
public String toString() {
return toStringHelper(getClass())
.add("system hash code", System.identityHashCode(this))
.add("clientCertificateHash", clientCertificateHash)
.add("clientInetAddress", clientInetAddr)
.add("sni", sni)
.toString();
}
}

View file

@ -0,0 +1,23 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
/**
* Marker interface indicating that a {@link Flow} needs to be run transactionally.
* <p>
* Any flow that mutates the datastore should be tagged with this so that {@link FlowRunner} will
* know how to run it.
*/
public interface TransactionalFlow {}

View file

@ -0,0 +1,39 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows;
import com.google.domain.registry.flows.EppException.AuthenticationErrorException;
import com.google.domain.registry.model.registrar.Registrar;
/**
* A marker interface for objects containing registrar credentials provided via an EPP transport.
*/
public interface TransportCredentials {
/**
* Indicates whether the transport takes the place of EPP login checks, in which case LoginFlow
* will not check the password. Alternatively, if the password should be checked, it MUST match
* the user's and GAE's isUserAdmin should not be used to bypass this check as internal
* connections over RPC will have this property for all registrars.
*/
boolean performsLoginCheck();
/**
* Called by {@link com.google.domain.registry.flows.session.LoginFlow LoginFlow}
* to check the transport credentials against the stored registrar's credentials.
* If they do not match, throw an AuthenticationErrorException.
*/
void validate(Registrar r) throws AuthenticationErrorException;
}

View file

@ -0,0 +1,66 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.async;
import static com.google.domain.registry.request.Actions.getPathForAction;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.RetryOptions;
import com.google.appengine.api.taskqueue.TaskHandle;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.taskqueue.TaskOptions.Method;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.mapreduce.MapreduceAction;
import com.google.domain.registry.util.FormattingLogger;
import org.joda.time.Duration;
import java.util.Map.Entry;
/** Utility methods specific to async flows. */
public final class AsyncFlowUtils {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
@VisibleForTesting
public static final String ASYNC_FLOW_QUEUE_NAME = "flows-async"; // See queue.xml.
private AsyncFlowUtils() {}
/** Enqueues a mapreduce action to perform an async flow operation. */
public static TaskHandle enqueueMapreduceAction(
Class<? extends MapreduceAction> action,
ImmutableMap<String, String> params,
Duration executionDelay) {
Queue queue = QueueFactory.getQueue(ASYNC_FLOW_QUEUE_NAME);
String path = getPathForAction(action);
logger.infofmt("Enqueueing async mapreduce action with path %s and params %s", path, params);
// Aggressively back off if the task fails, to minimize flooding the logs.
RetryOptions retryOptions = RetryOptions.Builder.withMinBackoffSeconds(
RegistryEnvironment.get().config().getAsyncFlowFailureBackoff().getStandardSeconds());
TaskOptions options = TaskOptions.Builder
.withUrl(path)
.retryOptions(retryOptions)
.countdownMillis(executionDelay.getMillis())
.method(Method.GET);
for (Entry<String, String> entry : params.entrySet()) {
options.param(entry.getKey(), entry.getValue());
}
return queue.add(options);
}
}

View file

@ -0,0 +1,58 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.async;
import static com.google.domain.registry.flows.async.DeleteEppResourceAction.PARAM_IS_SUPERUSER;
import static com.google.domain.registry.flows.async.DeleteEppResourceAction.PARAM_REQUESTING_CLIENT_ID;
import static com.google.domain.registry.flows.async.DeleteEppResourceAction.PARAM_RESOURCE_KEY;
import static com.google.domain.registry.flows.async.DnsRefreshForHostRenameAction.PARAM_HOST_KEY;
import static com.google.domain.registry.request.RequestParameters.extractBooleanParameter;
import static com.google.domain.registry.request.RequestParameters.extractRequiredParameter;
import com.google.domain.registry.request.Parameter;
import dagger.Module;
import dagger.Provides;
import javax.servlet.http.HttpServletRequest;
/** Dagger module for the async flows package. */
@Module
public final class AsyncFlowsModule {
@Provides
@Parameter(PARAM_IS_SUPERUSER)
static boolean provideIsSuperuser(HttpServletRequest req) {
return extractBooleanParameter(req, PARAM_IS_SUPERUSER);
}
@Provides
@Parameter(PARAM_REQUESTING_CLIENT_ID)
static String provideRequestingClientId(HttpServletRequest req) {
return extractRequiredParameter(req, PARAM_REQUESTING_CLIENT_ID);
}
@Provides
@Parameter(PARAM_RESOURCE_KEY)
static String provideResourceKey(HttpServletRequest req) {
return extractRequiredParameter(req, PARAM_RESOURCE_KEY);
}
@Provides
@Parameter(PARAM_HOST_KEY)
static String provideHostKey(HttpServletRequest req) {
return extractRequiredParameter(req, PARAM_HOST_KEY);
}
}

View file

@ -0,0 +1,82 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.async;
import static com.google.domain.registry.flows.ResourceFlowUtils.handlePendingTransferOnDelete;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.domain.DomainBase;
import com.google.domain.registry.model.domain.ReferenceUnion;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.model.reporting.HistoryEntry.Type;
import com.google.domain.registry.request.Action;
import org.joda.time.DateTime;
import javax.inject.Inject;
/**
* A mapreduce to delete the specified ContactResource, but ONLY if it is not referred to by any
* existing DomainBase entity.
*/
@Action(path = "/_dr/task/deleteContactResource")
public class DeleteContactResourceAction extends DeleteEppResourceAction<ContactResource> {
@Inject
public DeleteContactResourceAction() {
super(
new DeleteContactResourceMapper(),
new DeleteContactResourceReducer());
}
/** An async deletion mapper for {@link ContactResource}. */
public static class DeleteContactResourceMapper extends DeleteEppResourceMapper<ContactResource> {
private static final long serialVersionUID = -5904009575877950342L;
@Override
protected boolean isLinked(
DomainBase domain, ReferenceUnion<ContactResource> targetResourceRef) {
return domain.getReferencedContacts().contains(targetResourceRef);
}
}
/** An async deletion reducer for {@link ContactResource}. */
public static class DeleteContactResourceReducer
extends DeleteEppResourceReducer<ContactResource> {
private static final long serialVersionUID = -7633644054441045215L;
@Override
protected Type getHistoryType(boolean successfulDelete) {
return successfulDelete
? HistoryEntry.Type.CONTACT_DELETE
: HistoryEntry.Type.CONTACT_DELETE_FAILURE;
}
@Override
protected void performDeleteTasks(
ContactResource targetResource,
ContactResource deletedResource,
DateTime deletionTime,
HistoryEntry historyEntryForDelete) {
handlePendingTransferOnDelete(
targetResource,
deletedResource,
deletionTime,
historyEntryForDelete);
}
}
}

View file

@ -0,0 +1,272 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.async;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.domain.registry.flows.ResourceFlowUtils.prepareDeletedResourceAsBuilder;
import static com.google.domain.registry.flows.ResourceFlowUtils.updateForeignKeyIndexDeletionTime;
import static com.google.domain.registry.model.EppResourceUtils.isActive;
import static com.google.domain.registry.model.EppResourceUtils.isDeleted;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.PipelineUtils.createJobPath;
import static com.google.domain.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.appengine.tools.mapreduce.Mapper;
import com.google.appengine.tools.mapreduce.Reducer;
import com.google.appengine.tools.mapreduce.ReducerInput;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.domain.registry.mapreduce.MapreduceAction;
import com.google.domain.registry.mapreduce.MapreduceRunner;
import com.google.domain.registry.mapreduce.inputs.EppResourceInputs;
import com.google.domain.registry.mapreduce.inputs.NullInput;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.model.annotations.ExternalMessagingName;
import com.google.domain.registry.model.domain.DomainBase;
import com.google.domain.registry.model.domain.ReferenceUnion;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.request.HttpException.BadRequestException;
import com.google.domain.registry.request.Parameter;
import com.google.domain.registry.request.Response;
import com.google.domain.registry.util.Clock;
import com.google.domain.registry.util.FormattingLogger;
import com.google.domain.registry.util.NonFinalForTesting;
import com.google.domain.registry.util.SystemClock;
import com.google.domain.registry.util.TypeUtils.TypeInstantiator;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Ref;
import com.googlecode.objectify.Work;
import org.joda.time.DateTime;
import javax.inject.Inject;
/**
* A mapreduce to delete the specified EPP resource, but ONLY if it is not referred to by any
* existing DomainBase entity.
*/
public abstract class DeleteEppResourceAction<T extends EppResource> implements MapreduceAction {
@NonFinalForTesting
static Clock clock = new SystemClock();
/** The HTTP parameter name used to specify the websafe key of the resource to delete. */
public static final String PARAM_RESOURCE_KEY = "resourceKey";
public static final String PARAM_REQUESTING_CLIENT_ID = "requestingClientId";
public static final String PARAM_IS_SUPERUSER = "superuser";
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
@Inject @Parameter(PARAM_RESOURCE_KEY) String resourceKeyString;
@Inject @Parameter(PARAM_REQUESTING_CLIENT_ID) String requestingClientId;
@Inject @Parameter(PARAM_IS_SUPERUSER) boolean isSuperuser;
@Inject MapreduceRunner mrRunner;
@Inject Response response;
DeleteEppResourceMapper<T> mapper;
DeleteEppResourceReducer<T> reducer;
protected DeleteEppResourceAction(
DeleteEppResourceMapper<T> mapper,
DeleteEppResourceReducer<T> reducer) {
this.mapper = mapper;
this.reducer = reducer;
}
@Override
public void run() {
Key<T> resourceKey = null;
T resource;
try {
resourceKey = Key.create(resourceKeyString);
resource = checkArgumentNotNull(ofy().load().key(resourceKey).now());
} catch (IllegalArgumentException e) {
throw new BadRequestException(resourceKey == null
? "Could not parse key string: " + resourceKeyString
: "Could not load resource for key: " + resourceKey);
}
checkArgument(
resource.getClass().equals(new TypeInstantiator<T>(getClass()){}.getExactType()),
String.format("Cannot delete a %s via this action.", resource.getClass().getSimpleName()));
checkState(
!isDeleted(resource, clock.nowUtc()),
"Resource %s is already deleted.", resource.getForeignKey());
checkState(
resource.getStatusValues().contains(StatusValue.PENDING_DELETE),
"Resource %s is not set as PENDING_DELETE", resource.getForeignKey());
mapper.setTargetResource(resourceKey);
reducer.setClient(requestingClientId, isSuperuser);
logger.infofmt("Executing Delete EPP resource mapreduce for %s", resourceKey);
response.sendJavaScriptRedirect(createJobPath(mrRunner
.setJobName("Check for EPP resource references and then delete")
.setModuleName("backend")
.runMapreduce(
mapper,
reducer,
ImmutableList.of(
// Add an extra shard that maps over a null domain. See the mapper code for why.
new NullInput<DomainBase>(),
EppResourceInputs.createEntityInput(DomainBase.class)))));
}
/**
* A mapper that iterates over all {@link DomainBase} entities.
*
* <p>It emits the target key and {@code true} for domains referencing the target resource. For
* the special input of {@code null} it emits the target key and {@code false}.
*/
public abstract static class DeleteEppResourceMapper<T extends EppResource>
extends Mapper<DomainBase, Key<T>, Boolean> {
private static final long serialVersionUID = -7355145176854995813L;
private DateTime targetResourceUpdateTimestamp;
private Key<T> targetEppResourceKey;
private void setTargetResource(Key<T> targetEppResourceKey) {
this.targetEppResourceKey = targetEppResourceKey;
this.targetResourceUpdateTimestamp =
ofy().load().key(targetEppResourceKey).now().getUpdateAutoTimestamp().getTimestamp();
}
/** Determine whether the target resource is a linked resource on the domain. */
protected abstract boolean isLinked(DomainBase domain, ReferenceUnion<T> targetResourceRef);
@Override
public void map(DomainBase domain) {
// The reducer only runs if at least one value is emitted. We add a null input to the
// mapreduce and always emit 'false' for it to force the reducer to run. We can then emit
// 'true' for linked domains and not emit anything for unlinked domains, which speeds up the
// reducer since it will only receive true keys, of which there will be few (usually none).
if (domain == null) {
emit(targetEppResourceKey, false);
return;
}
// The ReferenceUnion can't be a field on the Mapper, because when a Ref<?> is serialized
// (required for each MapShardTask), it uses the DeadRef version, which contains the Ref's
// value, which isn't serializable. Thankfully, this isn't expensive.
// See: https://github.com/objectify/objectify/blob/master/src/main/java/com/googlecode/objectify/impl/ref/DeadRef.java
if (isActive(domain, targetResourceUpdateTimestamp)
&& isLinked(domain, ReferenceUnion.create(Ref.create(targetEppResourceKey)))) {
emit(targetEppResourceKey, true);
}
}
}
/**
* A reducer that checks if the EPP resource to be deleted is referenced anywhere, and then
* deletes it if not and unmarks it for deletion if so.
*/
public abstract static class DeleteEppResourceReducer<T extends EppResource>
extends Reducer<Key<T>, Boolean, Void> {
private static final long serialVersionUID = 875017002097945151L;
private String requestingClientId;
private boolean isSuperuser;
private void setClient(String requestingClientId, boolean isSuperuser) {
this.requestingClientId = requestingClientId;
this.isSuperuser = isSuperuser;
}
/**
* Determine the proper history entry type for the delete operation, as a function of
* whether or not the delete was successful.
*/
protected abstract HistoryEntry.Type getHistoryType(boolean successfulDelete);
/** Perform any type-specific tasks on the resource to be deleted (and/or its dependencies). */
protected abstract void performDeleteTasks(
T targetResource,
T deletedResource,
DateTime deletionTime,
HistoryEntry historyEntryForDelete);
@Override
public void reduce(final Key<T> key, final ReducerInput<Boolean> values) {
final boolean hasNoActiveReferences = !Iterators.contains(values, true);
logger.infofmt("Processing delete request for %s", key.toString());
String pollMessageText = ofy().transactNew(new Work<String>() {
@Override
@SuppressWarnings("unchecked")
public String run() {
DateTime now = ofy().getTransactionTime();
T targetResource = (T) ofy().load().key(key).now().cloneProjectedAtTime(now);
String resourceName = targetResource.getForeignKey();
// Double-check that the resource is still active and in PENDING_DELETE within the
// transaction.
checkState(
!isDeleted(targetResource, now),
"Resource %s is already deleted.", resourceName);
checkState(
targetResource.getStatusValues().contains(StatusValue.PENDING_DELETE),
"Resource %s is not in PENDING_DELETE.", resourceName);
targetResource = (T) targetResource.asBuilder()
.removeStatusValue(StatusValue.PENDING_DELETE)
.build();
boolean requestedByCurrentOwner =
targetResource.getCurrentSponsorClientId().equals(requestingClientId);
boolean deleteAllowed = hasNoActiveReferences && (requestedByCurrentOwner || isSuperuser);
String resourceTypeName =
targetResource.getClass().getAnnotation(ExternalMessagingName.class).value();
HistoryEntry.Type historyType = getHistoryType(deleteAllowed);
String pollMessageText = deleteAllowed
? String.format("Deleted %s %s.", resourceTypeName, resourceName)
: String.format(
"Can't delete %s %s because %s.",
resourceTypeName,
resourceName,
requestedByCurrentOwner
? "it is referenced by a domain"
: "it was transferred prior to deletion");
HistoryEntry historyEntry = new HistoryEntry.Builder()
.setClientId(requestingClientId)
.setModificationTime(now)
.setType(historyType)
.setParent(key)
.build();
PollMessage.OneTime deleteResultMessage = new PollMessage.OneTime.Builder()
.setClientId(requestingClientId)
.setMsg(pollMessageText)
.setParent(historyEntry)
.setEventTime(now)
.build();
if (deleteAllowed) {
T deletedResource = prepareDeletedResourceAsBuilder(targetResource, now).build();
performDeleteTasks(targetResource, deletedResource, now, historyEntry);
updateForeignKeyIndexDeletionTime(deletedResource);
ofy().save().<Object>entities(deletedResource, historyEntry, deleteResultMessage);
} else {
ofy().save().<Object>entities(targetResource, historyEntry, deleteResultMessage);
}
return pollMessageText;
}
});
logger.infofmt(pollMessageText);
}
}
}

View file

@ -0,0 +1,84 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.async;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.domain.registry.dns.DnsQueue;
import com.google.domain.registry.model.domain.DomainBase;
import com.google.domain.registry.model.domain.ReferenceUnion;
import com.google.domain.registry.model.host.HostResource;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.model.reporting.HistoryEntry.Type;
import com.google.domain.registry.request.Action;
import org.joda.time.DateTime;
import javax.inject.Inject;
/**
* A mapreduce to delete the specified HostResource, but ONLY if it is not referred to by any
* existing DomainBase entity.
*/
@Action(path = "/_dr/task/deleteHostResource")
public class DeleteHostResourceAction extends DeleteEppResourceAction<HostResource> {
@Inject
public DeleteHostResourceAction() {
super(
new DeleteHostResourceMapper(),
new DeleteHostResourceReducer());
}
/** An async deletion mapper for {@link HostResource}. */
public static class DeleteHostResourceMapper extends DeleteEppResourceMapper<HostResource> {
private static final long serialVersionUID = 1941092742903217194L;
@Override
protected boolean isLinked(
DomainBase domain, ReferenceUnion<HostResource> targetResourceRef) {
return domain.getNameservers().contains(targetResourceRef);
}
}
/** An async deletion reducer for {@link HostResource}. */
public static class DeleteHostResourceReducer extends DeleteEppResourceReducer<HostResource> {
private static final long serialVersionUID = 555457935288867324L;
@Override
protected Type getHistoryType(boolean successfulDelete) {
return successfulDelete
? HistoryEntry.Type.HOST_DELETE
: HistoryEntry.Type.HOST_DELETE_FAILURE;
}
@Override
protected void performDeleteTasks(
HostResource targetResource,
HostResource deletedResource,
DateTime deletionTime,
HistoryEntry historyEntryForDelete) {
if (targetResource.getSuperordinateDomain() != null) {
DnsQueue.create().addHostRefreshTask(targetResource.getFullyQualifiedHostName());
ofy().save().entity(
targetResource.getSuperordinateDomain().get().asBuilder()
.removeSubordinateHost(targetResource.getFullyQualifiedHostName())
.build());
}
}
}
}

View file

@ -0,0 +1,110 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.async;
import static com.google.domain.registry.model.EppResourceUtils.isActive;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.PipelineUtils.createJobPath;
import static com.google.domain.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.appengine.tools.mapreduce.Mapper;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.dns.DnsQueue;
import com.google.domain.registry.mapreduce.MapreduceAction;
import com.google.domain.registry.mapreduce.MapreduceRunner;
import com.google.domain.registry.mapreduce.inputs.EppResourceInputs;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.ReferenceUnion;
import com.google.domain.registry.model.host.HostResource;
import com.google.domain.registry.request.Action;
import com.google.domain.registry.request.HttpException.BadRequestException;
import com.google.domain.registry.request.Parameter;
import com.google.domain.registry.request.Response;
import com.google.domain.registry.util.FormattingLogger;
import com.google.domain.registry.util.NonFinalForTesting;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Ref;
import org.joda.time.DateTime;
import javax.inject.Inject;
/**
* Enqueues DNS refreshes for applicable domains following a host rename.
*/
@Action(path = "/_dr/task/dnsRefreshForHostRename")
public class DnsRefreshForHostRenameAction implements MapreduceAction {
/** The HTTP parameter name used to specify the websafe key of the host to rename. */
public static final String PARAM_HOST_KEY = "hostKey";
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
@NonFinalForTesting
static DnsQueue dnsQueue = DnsQueue.create();
@Inject @Parameter(PARAM_HOST_KEY) String hostKeyString;
@Inject MapreduceRunner mrRunner;
@Inject Response response;
@Inject DnsRefreshForHostRenameAction() {}
@Override
public void run() {
Key<HostResource> resourceKey = null;
HostResource host;
try {
resourceKey = Key.create(hostKeyString);
host = checkArgumentNotNull(ofy().load().key(resourceKey).now());
} catch (IllegalArgumentException e) {
throw new BadRequestException(resourceKey == null
? "Could not parse key string: " + hostKeyString
: "Could not load resource for key: " + resourceKey);
}
response.sendJavaScriptRedirect(createJobPath(mrRunner
.setJobName("Enqueue DNS refreshes for domains following a host rename")
.setModuleName("backend")
.runMapOnly(
new DnsRefreshForHostRenameMapper(host),
ImmutableList.of(EppResourceInputs.createEntityInput(DomainResource.class)))));
}
/** Map over domains and refresh the dns of those that referenced this host. */
public static class DnsRefreshForHostRenameMapper extends Mapper<DomainResource, Void, Void> {
private static final long serialVersionUID = -4707015136971008447L;
private final DateTime hostUpdateTime;
private final Key<HostResource> targetHostKey;
DnsRefreshForHostRenameMapper(HostResource host) {
this.targetHostKey = Key.create(host);
this.hostUpdateTime = host.getUpdateAutoTimestamp().getTimestamp();
}
@Override
public final void map(DomainResource domain) {
if (isActive(domain, hostUpdateTime)
&& domain.getNameservers().contains(ReferenceUnion.create(Ref.create(targetHostKey)))) {
try {
dnsQueue.addDomainRefreshTask(domain.getFullyQualifiedDomainName());
logger.infofmt("Enqueued refresh for domain %s", domain.getFullyQualifiedDomainName());
} catch (Throwable t) {
logger.severefmt(t, "Error while refreshing DNS for host rename %s", targetHostKey);
}
}
}
}
}

View file

@ -0,0 +1,45 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.contact;
import static com.google.domain.registry.model.EppResourceUtils.checkResourcesExist;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.flows.ResourceCheckFlow;
import com.google.domain.registry.model.contact.ContactCommand.Check;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.eppoutput.CheckData;
import com.google.domain.registry.model.eppoutput.CheckData.ContactCheck;
import com.google.domain.registry.model.eppoutput.CheckData.ContactCheckData;
import java.util.Set;
/**
* An EPP flow that checks whether a contact can be provisioned.
*
* @error {@link com.google.domain.registry.flows.ResourceCheckFlow.TooManyResourceChecksException}
*/
public class ContactCheckFlow extends ResourceCheckFlow<ContactResource, Check> {
@Override
protected CheckData getCheckData() {
Set<String> existingIds = checkResourcesExist(resourceClass, targetIds, now);
ImmutableList.Builder<ContactCheck> checks = new ImmutableList.Builder<>();
for (String id : targetIds) {
boolean unused = !existingIds.contains(id);
checks.add(ContactCheck.create(unused, id, unused ? null : "In use"));
}
return ContactCheckData.create(checks.build());
}
}

View file

@ -0,0 +1,65 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.contact;
import static com.google.domain.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo;
import static com.google.domain.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy;
import static com.google.domain.registry.model.EppResourceUtils.createContactHostRoid;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.ResourceCreateFlow;
import com.google.domain.registry.model.contact.ContactCommand.Create;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.contact.ContactResource.Builder;
import com.google.domain.registry.model.eppoutput.CreateData.ContactCreateData;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.ofy.ObjectifyService;
import com.google.domain.registry.model.reporting.HistoryEntry;
/**
* An EPP flow that creates a new contact resource.
*
* @error {@link com.google.domain.registry.flows.ResourceCreateFlow.ResourceAlreadyExistsException}
* @error {@link ContactFlowUtils.BadInternationalizedPostalInfoException}
* @error {@link ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException}
*/
public class ContactCreateFlow extends ResourceCreateFlow<ContactResource, Builder, Create> {
@Override
protected EppOutput getOutput() {
return createOutput(Success, ContactCreateData.create(newResource.getContactId(), now));
}
@Override
protected String createFlowRepoId() {
return createContactHostRoid(ObjectifyService.allocateId());
}
@Override
protected void verifyNewStateIsAllowed() throws EppException {
validateAsciiPostalInfo(newResource.getInternationalizedPostalInfo());
validateContactAgainstPolicy(newResource);
}
@Override
protected boolean storeXmlInHistoryEntry() {
return false;
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.CONTACT_CREATE;
}
}

View file

@ -0,0 +1,87 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.contact;
import static com.google.domain.registry.model.EppResourceUtils.queryDomainsUsingResource;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.ResourceAsyncDeleteFlow;
import com.google.domain.registry.flows.async.AsyncFlowUtils;
import com.google.domain.registry.flows.async.DeleteContactResourceAction;
import com.google.domain.registry.flows.async.DeleteEppResourceAction;
import com.google.domain.registry.model.contact.ContactCommand.Delete;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.contact.ContactResource.Builder;
import com.google.domain.registry.model.domain.DomainBase;
import com.google.domain.registry.model.domain.ReferenceUnion;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.googlecode.objectify.Key;
/**
* An EPP flow that deletes a contact resource.
*
* @error {@link com.google.domain.registry.flows.ResourceAsyncDeleteFlow.ResourceToDeleteIsReferencedException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException}
*/
public class ContactDeleteFlow extends ResourceAsyncDeleteFlow<ContactResource, Builder, Delete> {
/** In {@link #isLinkedForFailfast}, check this (arbitrary) number of resources from the query. */
private static final int FAILFAST_CHECK_COUNT = 5;
@Override
protected boolean isLinkedForFailfast(final ReferenceUnion<ContactResource> ref) {
// Query for the first few linked domains, and if found, actually load them. The query is
// eventually consistent and so might be very stale, but the direct load will not be stale,
// just non-transactional. If we find at least one actual reference then we can reliably
// fail. If we don't find any, we can't trust the query and need to do the full mapreduce.
return Iterables.any(
ofy().load().keys(
queryDomainsUsingResource(
ContactResource.class, ref.getLinked(), now, FAILFAST_CHECK_COUNT)).values(),
new Predicate<DomainBase>() {
@Override
public boolean apply(DomainBase domain) {
return domain.getReferencedContacts().contains(ref);
}});
}
/** Enqueues a contact resource deletion on the mapreduce queue. */
@Override
protected final void enqueueTasks() throws EppException {
AsyncFlowUtils.enqueueMapreduceAction(
DeleteContactResourceAction.class,
ImmutableMap.of(
DeleteEppResourceAction.PARAM_RESOURCE_KEY,
Key.create(existingResource).getString(),
DeleteEppResourceAction.PARAM_REQUESTING_CLIENT_ID,
getClientId(),
DeleteEppResourceAction.PARAM_IS_SUPERUSER,
Boolean.toString(superuser)),
RegistryEnvironment.get().config().getAsyncDeleteFlowMapreduceDelay());
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.CONTACT_PENDING_DELETE;
}
}

View file

@ -0,0 +1,77 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.contact;
import static com.google.domain.registry.model.contact.PostalInfo.Type.INTERNATIONALIZED;
import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.ParameterValuePolicyErrorException;
import com.google.domain.registry.flows.EppException.ParameterValueSyntaxErrorException;
import com.google.domain.registry.model.contact.ContactAddress;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.contact.PostalInfo;
import java.util.Set;
import javax.annotation.Nullable;
/** Static utility functions for contact flows. */
public class ContactFlowUtils {
/** Check that an internationalized postal info has only ascii characters. */
static void validateAsciiPostalInfo(@Nullable PostalInfo internationalized) throws EppException {
if (internationalized != null) {
Preconditions.checkState(INTERNATIONALIZED.equals(internationalized.getType()));
ContactAddress address = internationalized.getAddress();
Set<String> fields = Sets.newHashSet(
internationalized.getName(),
internationalized.getOrg(),
address.getCity(),
address.getCountryCode(),
address.getState(),
address.getZip());
fields.addAll(address.getStreet());
for (String field : fields) {
if (field != null && !CharMatcher.ascii().matchesAllOf(field)) {
throw new BadInternationalizedPostalInfoException();
}
}
}
}
/** Check contact's state against server policy. */
static void validateContactAgainstPolicy(ContactResource contact) throws EppException {
if (contact.getDisclose() != null && !contact.getDisclose().getFlag()) {
throw new DeclineContactDisclosureFieldDisallowedPolicyException();
}
}
/** Declining contact disclosure is disallowed by server policy. */
static class DeclineContactDisclosureFieldDisallowedPolicyException
extends ParameterValuePolicyErrorException {
public DeclineContactDisclosureFieldDisallowedPolicyException() {
super("Declining contact disclosure is disallowed by server policy.");
}
}
/** Internationalized postal infos can only contain ASCII characters. */
static class BadInternationalizedPostalInfoException extends ParameterValueSyntaxErrorException {
public BadInternationalizedPostalInfoException() {
super("Internationalized postal infos can only contain ASCII characters");
}
}
}

View file

@ -0,0 +1,27 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.contact;
import com.google.domain.registry.flows.ResourceInfoFlow;
import com.google.domain.registry.model.contact.ContactCommand.Info;
import com.google.domain.registry.model.contact.ContactResource;
/**
* An EPP flow that reads a contact.
*
* @error {@link com.google.domain.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException}
*/
public class ContactInfoFlow extends ResourceInfoFlow<ContactResource, Info> {}

View file

@ -0,0 +1,37 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.contact;
import com.google.domain.registry.flows.ResourceTransferApproveFlow;
import com.google.domain.registry.model.contact.ContactCommand.Transfer;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.contact.ContactResource.Builder;
import com.google.domain.registry.model.reporting.HistoryEntry;
/**
* An EPP flow that approves a pending transfer on a {@link ContactResource}.
*
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException}
*/
public class ContactTransferApproveFlow
extends ResourceTransferApproveFlow<ContactResource, Builder, Transfer> {
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.CONTACT_TRANSFER_APPROVE;
}
}

View file

@ -0,0 +1,37 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.contact;
import com.google.domain.registry.flows.ResourceTransferCancelFlow;
import com.google.domain.registry.model.contact.ContactCommand.Transfer;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.contact.ContactResource.Builder;
import com.google.domain.registry.model.reporting.HistoryEntry;
/**
* An EPP flow that cancels a pending transfer on a {@link ContactResource}.
*
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException}
* @error {@link com.google.domain.registry.flows.ResourceTransferCancelFlow.NotTransferInitiatorException}
*/
public class ContactTransferCancelFlow
extends ResourceTransferCancelFlow<ContactResource, Builder, Transfer> {
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.CONTACT_TRANSFER_CANCEL;
}
}

View file

@ -0,0 +1,30 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.contact;
import com.google.domain.registry.flows.ResourceTransferQueryFlow;
import com.google.domain.registry.model.contact.ContactCommand.Transfer;
import com.google.domain.registry.model.contact.ContactResource;
/**
* An EPP flow that queries a pending transfer on a {@link ContactResource}.
*
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link com.google.domain.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceTransferQueryFlow.NoTransferHistoryToQueryException}
* @error {@link com.google.domain.registry.flows.ResourceTransferQueryFlow.NotAuthorizedToViewTransferException}
*/
public class ContactTransferQueryFlow extends ResourceTransferQueryFlow<ContactResource, Transfer> {
}

View file

@ -0,0 +1,37 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.contact;
import com.google.domain.registry.flows.ResourceTransferRejectFlow;
import com.google.domain.registry.model.contact.ContactCommand.Transfer;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.contact.ContactResource.Builder;
import com.google.domain.registry.model.reporting.HistoryEntry;
/**
* An EPP flow that rejects a pending transfer on a {@link ContactResource}.
*
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException}
*/
public class ContactTransferRejectFlow
extends ResourceTransferRejectFlow<ContactResource, Builder, Transfer> {
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.CONTACT_TRANSFER_REJECT;
}
}

View file

@ -0,0 +1,46 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.contact;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.flows.ResourceTransferRequestFlow;
import com.google.domain.registry.model.contact.ContactCommand.Transfer;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.reporting.HistoryEntry;
import org.joda.time.Duration;
/**
* An EPP flow that requests a transfer on a {@link ContactResource}.
*
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceTransferRequestFlow.AlreadyPendingTransferException}
* @error {@link com.google.domain.registry.flows.ResourceTransferRequestFlow.MissingTransferRequestAuthInfoException}
* @error {@link com.google.domain.registry.flows.ResourceTransferRequestFlow.ObjectAlreadySponsoredException}
*/
public class ContactTransferRequestFlow
extends ResourceTransferRequestFlow<ContactResource, Transfer> {
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.CONTACT_TRANSFER_REQUEST;
}
@Override
protected Duration getAutomaticTransferLength() {
return RegistryEnvironment.get().config().getContactAutomaticTransferLength();
}
}

View file

@ -0,0 +1,54 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.contact;
import static com.google.domain.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo;
import static com.google.domain.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.ResourceUpdateFlow;
import com.google.domain.registry.model.contact.ContactCommand.Update;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.contact.ContactResource.Builder;
import com.google.domain.registry.model.reporting.HistoryEntry;
/**
* An EPP flow that updates a contact resource.
*
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceUpdateFlow.ResourceHasClientUpdateProhibitedException}
* @error {@link com.google.domain.registry.flows.ResourceUpdateFlow.StatusNotClientSettableException}
* @error {@link com.google.domain.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException}
* @error {@link ContactFlowUtils.BadInternationalizedPostalInfoException}
* @error {@link ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException}
*/
public class ContactUpdateFlow extends ResourceUpdateFlow<ContactResource, Builder, Update> {
@Override
protected void verifyNewUpdatedStateIsAllowed() throws EppException {
validateAsciiPostalInfo(newResource.getInternationalizedPostalInfo());
validateContactAgainstPolicy(newResource);
}
@Override
protected boolean storeXmlInHistoryEntry() {
return false;
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.CONTACT_UPDATE;
}
}

View file

@ -0,0 +1,57 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateDomainName;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateDomainNameWithIdnTables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.ResourceCheckFlow;
import com.google.domain.registry.model.domain.DomainCommand.Check;
import com.google.domain.registry.model.domain.DomainResource;
import java.util.Map;
/** An EPP flow that checks whether a domain can be provisioned. */
public abstract class BaseDomainCheckFlow extends ResourceCheckFlow<DomainResource, Check> {
protected Map<String, InternetDomainName> domainNames;
@Override
protected final void initCheckResourceFlow() throws EppException {
ImmutableMap.Builder<String, InternetDomainName> domains = new ImmutableMap.Builder<>();
ImmutableSet.Builder<String> tlds = new ImmutableSet.Builder<>();
for (String targetId : ImmutableSet.copyOf(targetIds)) {
// This validation is moderately expensive, so cache the results for getCheckData to use too.
InternetDomainName domainName = validateDomainName(targetId);
tlds.add(domainName.parent().toString());
validateDomainNameWithIdnTables(domainName);
domains.put(targetId, domainName);
}
for (String tld : tlds.build()) {
checkAllowedAccessToTld(getAllowedTlds(), tld);
checkRegistryStateForTld(tld);
}
domainNames = domains.build();
initDomainCheckFlow();
}
@SuppressWarnings("unused")
protected void initDomainCheckFlow() throws EppException {}
}

View file

@ -0,0 +1,378 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.cloneAndLinkReferences;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateContactsHaveTypes;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateDomainName;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateDomainNameWithIdnTables;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateDsData;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateNameservers;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateNoDuplicateContacts;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateRegistrantAllowedOnTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateRequiredContactsPresent;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyLaunchPhase;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyNotReserved;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifySignedMarks;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears;
import static com.google.domain.registry.model.EppResourceUtils.createDomainRoid;
import static com.google.domain.registry.model.EppResourceUtils.loadByUniqueId;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.model.registry.Registries.findTldForName;
import static com.google.domain.registry.model.registry.label.ReservedList.matchesAnchorTenantReservation;
import com.google.common.base.Optional;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.ParameterValuePolicyErrorException;
import com.google.domain.registry.flows.EppException.ParameterValueRangeErrorException;
import com.google.domain.registry.flows.EppException.ParameterValueSyntaxErrorException;
import com.google.domain.registry.flows.EppException.StatusProhibitsOperationException;
import com.google.domain.registry.flows.EppException.UnimplementedOptionException;
import com.google.domain.registry.flows.ResourceCreateFlow;
import com.google.domain.registry.model.domain.DomainBase;
import com.google.domain.registry.model.domain.DomainBase.Builder;
import com.google.domain.registry.model.domain.DomainCommand.Create;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.fee.FeeCreateExtension;
import com.google.domain.registry.model.domain.launch.LaunchCreateExtension;
import com.google.domain.registry.model.domain.launch.LaunchNotice;
import com.google.domain.registry.model.domain.launch.LaunchNotice.InvalidChecksumException;
import com.google.domain.registry.model.domain.rgp.GracePeriodStatus;
import com.google.domain.registry.model.domain.secdns.SecDnsCreateExtension;
import com.google.domain.registry.model.ofy.ObjectifyService;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.registry.Registry.TldState;
import com.google.domain.registry.model.smd.SignedMark;
import com.google.domain.registry.model.tmch.ClaimsListShard;
import com.googlecode.objectify.Work;
import org.joda.money.Money;
import javax.annotation.Nullable;
/**
* An EPP flow that creates a new domain resource or application.
*
* @param <R> the resource type being created
* @param <B> a builder for the resource
*/
public abstract class BaseDomainCreateFlow<R extends DomainBase, B extends Builder<R, ?>>
extends ResourceCreateFlow<R, B, Create> {
private SecDnsCreateExtension secDnsCreate;
protected LaunchCreateExtension launchCreate;
protected String domainLabel;
protected InternetDomainName domainName;
protected String idnTableName;
protected FeeCreateExtension feeCreate;
protected Money createCost;
protected boolean hasSignedMarks;
protected SignedMark signedMark;
protected boolean isAnchorTenantViaReservation;
protected TldState tldState;
@Override
public final void initResourceCreateOrMutateFlow() throws EppException {
command = cloneAndLinkReferences(command, now);
registerExtensions(SecDnsCreateExtension.class);
secDnsCreate = eppInput.getSingleExtension(SecDnsCreateExtension.class);
launchCreate = eppInput.getSingleExtension(LaunchCreateExtension.class);
feeCreate = eppInput.getSingleExtension(FeeCreateExtension.class);
hasSignedMarks = launchCreate != null && !launchCreate.getSignedMarks().isEmpty();
initDomainCreateFlow();
}
@Override
@Nullable
protected String createFlowRepoId() {
// The domain name hasn't been validated yet, so if it's invalid, instead of throwing an error,
// simply leave the repoId blank (it won't be needed anyway as the flow will fail when
// validation fails later).
try {
Optional<InternetDomainName> tldParsed =
findTldForName(InternetDomainName.from(command.getFullyQualifiedDomainName()));
return tldParsed.isPresent()
? createDomainRoid(ObjectifyService.allocateId(), tldParsed.get().toString())
: null;
} catch (IllegalArgumentException e) {
return null;
}
}
/** Subclasses may override this to do more specific initialization. */
protected void initDomainCreateFlow() {}
/**
* Returns the tld of the domain being created.
*
* <p>Update/delete domain-related flows can simply grab the tld using existingResource.getTld(),
* but in the create flows, the resource doesn't exist yet. So we grab it off the domain name
* that the flow is attempting to create.
*
* <p>Note that it's not always safe to call this until after the domain name has been validated
* in verifyCreateIsAllowed().
*/
protected String getTld() {
return domainName.parent().toString();
}
/**
* Fail the domain or application create very fast if the domain is already registered.
* <p>
* Try to load the domain non-transactionally, since this can hit memcache. If we succeed, and the
* domain is not in the ADD grace period (the only state that allows instantaneous transition to
* being deleted), we can assume that the domain will not be deleted (and therefore won't be
* creatable) until its deletion time. For repeated failed creates this means we can avoid the
* datastore lookup, which is very expensive (and first-seen failed creates are no worse than they
* otherwise would be). This comes at the cost of the extra lookup for successful creates (or
* rather, those that don't fail due to the domain existing) and also for failed creates within
* the existing domain's ADD grace period.
*/
@Override
protected final void failfast() throws EppException {
// Enter a transactionless context briefly.
DomainResource domain = ofy().doTransactionless(new Work<DomainResource>() {
@Override
public DomainResource run() {
// This is cacheable because we are outside of a transaction.
return loadByUniqueId(DomainResource.class, targetId, now);
}});
// If the domain exists already and isn't in the ADD grace period then there is no way it will
// be suddenly deleted and therefore the create must fail.
if (domain != null
&& !domain.getGracePeriodStatuses().contains(GracePeriodStatus.ADD)) {
throw new ResourceAlreadyExistsException(targetId, true);
}
}
/** Fail if the create command is somehow invalid. */
@Override
protected final void verifyCreateIsAllowed() throws EppException {
// Validate that this is actually a legal domain name on a TLD that the registrar has access to.
domainName = validateDomainName(command.getFullyQualifiedDomainName());
idnTableName = validateDomainNameWithIdnTables(domainName);
String tld = getTld();
checkAllowedAccessToTld(getAllowedTlds(), tld);
Registry registry = Registry.get(tld);
tldState = registry.getTldState(now);
checkRegistryStateForTld(tld);
domainLabel = domainName.parts().get(0);
createCost = registry.getDomainCreateCost(targetId, command.getPeriod().getValue());
// The TLD should always be the parent of the requested domain name.
isAnchorTenantViaReservation = matchesAnchorTenantReservation(
domainLabel, tld, command.getAuthInfo().getPw().getValue());
// Superusers can create reserved domains, force creations on domains that require a claims
// notice without specifying a claims key, and override blocks on registering premium domains.
if (!superuser) {
boolean isSunriseApplication =
launchCreate != null && !launchCreate.getSignedMarks().isEmpty();
if (!isAnchorTenantViaReservation) {
verifyNotReserved(domainName, isSunriseApplication);
}
boolean isClaimsPeriod = now.isBefore(registry.getClaimsPeriodEnd());
boolean isClaimsCreate = launchCreate != null && launchCreate.getNotice() != null;
if (isClaimsPeriod) {
boolean labelOnClaimsList = ClaimsListShard.get().getClaimKey(domainLabel) != null;
if (labelOnClaimsList && !isSunriseApplication && !isClaimsCreate) {
throw new MissingClaimsNoticeException(domainName.toString());
}
if (!labelOnClaimsList && isClaimsCreate) {
throw new UnexpectedClaimsNoticeException(domainName.toString());
}
} else if (isClaimsCreate) {
throw new ClaimsPeriodEndedException(tld);
}
verifyPremiumNameIsNotBlocked(targetId, tld, getClientId());
}
verifyUnitIsYears(command.getPeriod());
verifyNotInPendingDelete(
command.getContacts(),
command.getRegistrant(),
command.getNameservers());
validateContactsHaveTypes(command.getContacts());
validateRegistrantAllowedOnTld(tld, command.getRegistrant());
validateNoDuplicateContacts(command.getContacts());
validateRequiredContactsPresent(command.getRegistrant(), command.getContacts());
validateNameservers(tld, command.getNameservers());
validateLaunchCreateExtension();
// If a signed mark was provided, then it must match the desired domain label.
// We do this after validating the launch create extension so that flows which don't allow any
// signed marks throw a more useful error message rather than complaining about specific issues
// with the signed marks.
if (hasSignedMarks) {
signedMark = verifySignedMarks(launchCreate.getSignedMarks(), domainLabel, now);
}
validateSecDnsExtension();
verifyDomainCreateIsAllowed();
}
/** Validate the secDNS extension, if present. */
private void validateSecDnsExtension() throws EppException {
if (secDnsCreate != null) {
if (secDnsCreate.getDsData() == null) {
throw new DsDataRequiredException();
}
if (secDnsCreate.getMaxSigLife() != null) {
throw new MaxSigLifeNotSupportedException();
}
validateDsData(secDnsCreate.getDsData());
}
}
/**
* If a launch create extension was given (always present for application creates, optional for
* domain creates) then validate it.
*/
private void validateLaunchCreateExtension() throws EppException {
if (launchCreate == null) {
return;
}
if (!superuser) { // Superusers can ignore the phase.
verifyLaunchPhase(getTld(), launchCreate, now);
}
if (launchCreate.hasCodeMarks()) {
throw new UnsupportedMarkTypeException();
}
validateDomainLaunchCreateExtension();
LaunchNotice notice = launchCreate.getNotice();
if (notice == null) {
return;
}
if (!notice.getNoticeId().getValidatorId().equals("tmch")) {
throw new InvalidTrademarkValidatorException();
}
// Superuser can force domain creations regardless of the current date.
if (!superuser) {
if (notice.getExpirationTime().isBefore(now)) {
throw new ExpiredClaimException();
}
// An acceptance within the past 48 hours is mandated by the TMCH Functional Spec.
if (notice.getAcceptedTime().isBefore(now.minusHours(48))) {
throw new AcceptedTooLongAgoException();
}
}
try {
notice.validate(domainLabel);
} catch (IllegalArgumentException e) {
throw new MalformedTcnIdException();
} catch (InvalidChecksumException e) {
throw new InvalidTcnIdChecksumException();
}
}
/** Subclasses may override this to do more specific checks. */
@SuppressWarnings("unused")
protected void verifyDomainCreateIsAllowed() throws EppException {}
/** Subclasses may override this to do more specific validation of the launchCreate extension. */
@SuppressWarnings("unused")
protected void validateDomainLaunchCreateExtension() throws EppException {}
/** Handle the secDNS extension */
@Override
protected final void setCreateProperties(B builder) throws EppException {
if (secDnsCreate != null) {
builder.setDsData(secDnsCreate.getDsData());
}
builder.setLaunchNotice(launchCreate == null ? null : launchCreate.getNotice());
setDomainCreateProperties(builder);
builder.setIdnTableName(idnTableName);
}
protected abstract void setDomainCreateProperties(B builder) throws EppException;
/** Requested domain requires a claims notice. */
static class MissingClaimsNoticeException extends StatusProhibitsOperationException {
public MissingClaimsNoticeException(String domainName) {
super(String.format("%s requires a claims notice", domainName));
}
}
/** Requested domain does not require a claims notice. */
static class UnexpectedClaimsNoticeException extends StatusProhibitsOperationException {
public UnexpectedClaimsNoticeException(String domainName) {
super(String.format("%s does not require a claims notice", domainName));
}
}
/** The claims period for this TLD has ended. */
static class ClaimsPeriodEndedException extends StatusProhibitsOperationException {
public ClaimsPeriodEndedException(String tld) {
super(String.format("The claims period for %s has ended", tld));
}
}
/** The specified trademark validator is not supported. */
static class InvalidTrademarkValidatorException extends ParameterValuePolicyErrorException {
public InvalidTrademarkValidatorException() {
super("The only supported validationID is 'tmch' for the ICANN Trademark Clearinghouse.");
}
}
/** At least one dsData is required when using the secDNS extension. */
static class DsDataRequiredException extends ParameterValuePolicyErrorException {
public DsDataRequiredException() {
super("At least one dsData is required when using the secDNS extension");
}
}
/** Only encoded signed marks are supported. */
static class UnsupportedMarkTypeException extends ParameterValuePolicyErrorException {
public UnsupportedMarkTypeException() {
super("Only encoded signed marks are supported");
}
}
/** The 'maxSigLife' setting is not supported. */
static class MaxSigLifeNotSupportedException extends UnimplementedOptionException {
public MaxSigLifeNotSupportedException() {
super("The 'maxSigLife' setting is not supported");
}
}
/** The expiration time specified in the claim notice has elapsed. */
static class ExpiredClaimException extends ParameterValueRangeErrorException {
public ExpiredClaimException() {
super("The expiration time specified in the claim notice has elapsed");
}
}
/** The acceptance time specified in the claim notice is more than 48 hours in the past. */
static class AcceptedTooLongAgoException extends ParameterValueRangeErrorException {
public AcceptedTooLongAgoException() {
super("The acceptance time specified in the claim notice is more than 48 hours in the past");
}
}
/** The specified TCNID is invalid. */
static class MalformedTcnIdException extends ParameterValueSyntaxErrorException {
public MalformedTcnIdException() {
super("The specified TCNID is malformed");
}
}
/** The checksum in the specified TCNID does not validate. */
static class InvalidTcnIdChecksumException extends ParameterValueRangeErrorException {
public InvalidTcnIdChecksumException() {
super("The checksum in the specified TCNID does not validate");
}
}
}

View file

@ -0,0 +1,51 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.util.CollectionUtils.forceEmptyToNull;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.ResourceInfoFlow;
import com.google.domain.registry.model.domain.DomainBase;
import com.google.domain.registry.model.domain.DomainBase.Builder;
import com.google.domain.registry.model.domain.DomainCommand;
import com.google.domain.registry.model.domain.secdns.SecDnsInfoExtension;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
/**
* An EPP flow that reads a domain resource or application.
*
* @param <R> the resource type being manipulated
* @param <B> a builder for the resource
*/
public abstract class BaseDomainInfoFlow<R extends DomainBase, B extends Builder<R, B>>
extends ResourceInfoFlow<R, DomainCommand.Info> {
@Override
protected final ImmutableList<ResponseExtension> getResponseExtensions() throws EppException {
ImmutableList.Builder<ResponseExtension> builder = new ImmutableList.Builder<>();
// According to RFC 5910 section 2, we should only return this if the client specified the
// "urn:ietf:params:xml:ns:secDNS-1.1" when logging in. However, this is a "SHOULD" not a "MUST"
// and we are going to ignore it; clients who don't care about secDNS can just ignore it.
if (!existingResource.getDsData().isEmpty()) {
builder.add(SecDnsInfoExtension.create(existingResource.getDsData()));
}
return forceEmptyToNull(builder.addAll(getDomainResponseExtensions()).build());
}
/** Subclasses should override this to add their extensions. */
protected abstract ImmutableList<? extends ResponseExtension> getDomainResponseExtensions()
throws EppException;
}

View file

@ -0,0 +1,156 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.union;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.cloneAndLinkReferences;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateContactsHaveTypes;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateDsData;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateNameservers;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateNoDuplicateContacts;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateRegistrantAllowedOnTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateRequiredContactsPresent;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.ParameterValuePolicyErrorException;
import com.google.domain.registry.flows.EppException.RequiredParameterMissingException;
import com.google.domain.registry.flows.EppException.UnimplementedOptionException;
import com.google.domain.registry.flows.ResourceUpdateFlow;
import com.google.domain.registry.model.domain.DomainBase;
import com.google.domain.registry.model.domain.DomainBase.Builder;
import com.google.domain.registry.model.domain.DomainCommand.Update;
import com.google.domain.registry.model.domain.secdns.DelegationSignerData;
import com.google.domain.registry.model.domain.secdns.SecDnsUpdateExtension;
import com.google.domain.registry.model.domain.secdns.SecDnsUpdateExtension.Add;
import com.google.domain.registry.model.domain.secdns.SecDnsUpdateExtension.Remove;
import java.util.Set;
/**
* An EPP flow that updates a domain application or resource.
*
* @param <R> the resource type being created
* @param <B> a builder for the resource
*/
public abstract class BaseDomainUpdateFlow<R extends DomainBase, B extends Builder<R, B>>
extends ResourceUpdateFlow<R, B, Update> {
@Override
public final void initResourceCreateOrMutateFlow() throws EppException {
command = cloneAndLinkReferences(command, now);
initDomainUpdateFlow();
}
@SuppressWarnings("unused")
protected void initDomainUpdateFlow() throws EppException {}
@Override
public final B setUpdateProperties(B builder) throws EppException {
// Handle the secDNS extension.
SecDnsUpdateExtension secDnsUpdate = eppInput.getSingleExtension(SecDnsUpdateExtension.class);
if (secDnsUpdate != null) {
// We don't support 'urgent' because we do everything as fast as we can anyways.
if (Boolean.TRUE.equals(secDnsUpdate.getUrgent())) { // We allow both false and null.
throw new UrgentAttributeNotSupportedException();
}
// There must be at least one of add/rem/chg, and chg isn't actually supported.
if (secDnsUpdate.getAdd() == null && secDnsUpdate.getRemove() == null) {
// The only thing you can change is maxSigLife, and we don't support that at all.
throw (secDnsUpdate.getChange() == null)
? new EmptySecDnsUpdateException()
: new MaxSigLifeChangeNotSupportedException();
}
Set<DelegationSignerData> newDsData = existingResource.getDsData();
// RFC 5901 specifies that removes are processed before adds.
Remove remove = secDnsUpdate.getRemove();
if (remove != null) {
if (Boolean.FALSE.equals(remove.getAll())) { // Explicit all=false is meaningless.
throw new SecDnsAllUsageException();
}
newDsData = (remove.getAll() == null)
? difference(existingResource.getDsData(), remove.getDsData())
: ImmutableSet.<DelegationSignerData>of();
}
Add add = secDnsUpdate.getAdd();
if (add != null) {
newDsData = union(newDsData, add.getDsData());
}
builder.setDsData(ImmutableSet.copyOf(newDsData));
}
return setDomainUpdateProperties(builder);
}
/** Subclasses can override this to do set more specific properties. */
protected B setDomainUpdateProperties(B builder) {
return builder;
}
@Override
protected final void verifyUpdateIsAllowed() throws EppException {
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld());
verifyDomainUpdateIsAllowed();
verifyNotInPendingDelete(
command.getInnerAdd().getContacts(),
command.getInnerChange().getRegistrant(),
command.getInnerAdd().getNameservers());
validateContactsHaveTypes(command.getInnerAdd().getContacts());
validateContactsHaveTypes(command.getInnerRemove().getContacts());
}
/** Subclasses can override this to do more specific verification. */
@SuppressWarnings("unused")
protected void verifyDomainUpdateIsAllowed() throws EppException {}
@Override
protected final void verifyNewUpdatedStateIsAllowed() throws EppException {
validateNoDuplicateContacts(newResource.getContacts());
validateRequiredContactsPresent(newResource.getRegistrant(), newResource.getContacts());
validateDsData(newResource.getDsData());
validateRegistrantAllowedOnTld(newResource.getTld(), newResource.getRegistrant());
validateNameservers(newResource.getTld(), newResource.getNameservers());
}
/** The secDNS:all element must have value 'true' if present. */
static class SecDnsAllUsageException extends ParameterValuePolicyErrorException {
public SecDnsAllUsageException() {
super("The secDNS:all element must have value 'true' if present");
}
}
/** At least one of 'add' or 'rem' is required on a secDNS update. */
static class EmptySecDnsUpdateException extends RequiredParameterMissingException {
public EmptySecDnsUpdateException() {
super("At least one of 'add' or 'rem' is required on a secDNS update");
}
}
/** The 'urgent' attribute is not supported. */
static class UrgentAttributeNotSupportedException extends UnimplementedOptionException {
public UrgentAttributeNotSupportedException() {
super("The 'urgent' attribute is not supported");
}
}
/** Changing 'maxSigLife' is not supported. */
static class MaxSigLifeChangeNotSupportedException extends UnimplementedOptionException {
public MaxSigLifeChangeNotSupportedException() {
super("Changing 'maxSigLife' is not supported");
}
}
}

View file

@ -0,0 +1,80 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.model.domain.launch.LaunchPhase.CLAIMS;
import static com.google.domain.registry.util.DateTimeUtils.isAtOrAfter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.model.domain.launch.LaunchCheckExtension;
import com.google.domain.registry.model.domain.launch.LaunchCheckResponseExtension;
import com.google.domain.registry.model.domain.launch.LaunchCheckResponseExtension.LaunchCheck;
import com.google.domain.registry.model.domain.launch.LaunchCheckResponseExtension.LaunchCheckName;
import com.google.domain.registry.model.eppoutput.CheckData;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.registry.Registry.TldState;
import com.google.domain.registry.model.tmch.ClaimsListShard;
import java.util.Map.Entry;
/**
* An EPP flow that checks whether strings are trademarked.
*
* @error {@link com.google.domain.registry.flows.ResourceCheckFlow.TooManyResourceChecksException}
* @error {@link com.google.domain.registry.flows.ResourceFlow.BadCommandForRegistryPhaseException}
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link DomainFlowUtils.TldDoesNotExistException}
*/
public class ClaimsCheckFlow extends BaseDomainCheckFlow {
public static final ImmutableSet<TldState> DISALLOWED_TLD_STATES = Sets.immutableEnumSet(
TldState.PREDELEGATION, TldState.SUNRISE);
@Override
protected void initDomainCheckFlow() throws EppException {
registerExtensions(LaunchCheckExtension.class);
}
@Override
protected CheckData getCheckData() {
return null;
}
@Override
protected ImmutableList<? extends ResponseExtension> getResponseExtensions() throws EppException {
ImmutableList.Builder<LaunchCheck> launchChecksBuilder = new ImmutableList.Builder<>();
for (Entry<String, InternetDomainName> entry : domainNames.entrySet()) {
InternetDomainName domainName = entry.getValue();
if (isAtOrAfter(now, Registry.get(domainName.parent().toString()).getClaimsPeriodEnd())) {
throw new BadCommandForRegistryPhaseException();
}
String claimKey = ClaimsListShard.get().getClaimKey(domainName.parts().get(0));
launchChecksBuilder.add(LaunchCheck.create(
LaunchCheckName.create(claimKey != null, entry.getKey()), claimKey));
}
return ImmutableList.of(
LaunchCheckResponseExtension.create(CLAIMS, launchChecksBuilder.build()));
}
@Override
protected final ImmutableSet<TldState> getDisallowedTldStates() {
return DISALLOWED_TLD_STATES;
}
}

View file

@ -0,0 +1,223 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.getReservationType;
import static com.google.domain.registry.model.EppResourceUtils.loadByUniqueId;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.CollectionUtils.isNullOrEmpty;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.AuthorizationErrorException;
import com.google.domain.registry.flows.EppException.ObjectDoesNotExistException;
import com.google.domain.registry.flows.EppException.RequiredParameterMissingException;
import com.google.domain.registry.flows.EppException.StatusProhibitsOperationException;
import com.google.domain.registry.model.billing.BillingEvent;
import com.google.domain.registry.model.billing.BillingEvent.Flag;
import com.google.domain.registry.model.billing.BillingEvent.Reason;
import com.google.domain.registry.model.domain.DomainApplication;
import com.google.domain.registry.model.domain.DomainResource.Builder;
import com.google.domain.registry.model.domain.GracePeriod;
import com.google.domain.registry.model.domain.allocate.AllocateCreateExtension;
import com.google.domain.registry.model.domain.launch.ApplicationStatus;
import com.google.domain.registry.model.domain.launch.LaunchInfoResponseExtension;
import com.google.domain.registry.model.domain.rgp.GracePeriodStatus;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.registry.label.ReservationType;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.tmch.LordnTask;
import com.googlecode.objectify.Ref;
/**
* An EPP flow that allocates a new domain resource from a domain application.
*
* @error {@link com.google.domain.registry.flows.EppException.UnimplementedExtensionException}
* @error {@link com.google.domain.registry.flows.ResourceCreateFlow.ResourceAlreadyExistsException}
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link DomainAllocateFlow.HasFinalStatusException}
* @error {@link DomainAllocateFlow.MissingAllocateCreateExtensionException}
* @error {@link DomainAllocateFlow.MissingApplicationException}
* @error {@link DomainAllocateFlow.OnlySuperuserCanAllocateException}
*/
public class DomainAllocateFlow extends DomainCreateOrAllocateFlow {
protected AllocateCreateExtension allocateCreate;
protected DomainApplication application;
@Override
protected final void initDomainCreateOrAllocateFlow() {
registerExtensions(AllocateCreateExtension.class);
allocateCreate = eppInput.getSingleExtension(AllocateCreateExtension.class);
}
@Override
protected final void verifyDomainCreateIsAllowed() throws EppException {
if (!superuser) {
throw new OnlySuperuserCanAllocateException();
}
if (allocateCreate == null) {
throw new MissingAllocateCreateExtensionException();
}
String applicationRoid = allocateCreate.getApplicationRoid();
application = loadByUniqueId(DomainApplication.class, applicationRoid, now);
if (application == null) {
throw new MissingApplicationException(applicationRoid);
}
if (application.getApplicationStatus().isFinalStatus()) {
throw new HasFinalStatusException();
}
}
@Override
protected final void setDomainCreateOrAllocateProperties(Builder builder) {
boolean sunrushAddGracePeriod = isNullOrEmpty(command.getNameservers());
Registry registry = Registry.get(getTld());
ImmutableSet.Builder<Flag> billingFlagsBuilder = ImmutableSet.builder();
if (!application.getEncodedSignedMarks().isEmpty()) {
billingFlagsBuilder.add(Flag.SUNRISE);
} else {
billingFlagsBuilder.add(Flag.LANDRUSH);
}
BillingEvent.OneTime billingEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.CREATE)
.setFlags(billingFlagsBuilder.add(Flag.ALLOCATION).build())
.setTargetId(targetId)
.setClientId(getClientId())
.setCost(registry.getDomainCreateCost(targetId, command.getPeriod().getValue()))
.setPeriodYears(command.getPeriod().getValue())
.setEventTime(now)
// If there are no nameservers on the domain, then they get the benefit of the sunrush add
// grace period, which is longer than the standard add grace period.
.setBillingTime(now.plus(sunrushAddGracePeriod
? registry.getSunrushAddGracePeriodLength()
: registry.getAddGracePeriodLength()))
.setParent(historyEntry)
.build();
ReservationType reservationType = getReservationType(domainName);
ofy().save().<Object>entities(
// Save the billing event
billingEvent,
// Update the application itself.
application.asBuilder()
.setApplicationStatus(ApplicationStatus.ALLOCATED)
.removeStatusValue(StatusValue.PENDING_CREATE)
.build(),
// Create a poll message informing the registrar that the application status was updated.
new PollMessage.OneTime.Builder()
.setClientId(application.getCurrentSponsorClientId())
.setEventTime(ofy().getTransactionTime())
.setMsg(reservationType == ReservationType.NAME_COLLISION
// Change the poll message to remind the registrar of the name collision policy.
? "Domain on the name collision list was allocated. "
+ "But by policy, the domain will not be delegated. "
+ "Please visit https://www.icann.org/namecollision "
+ "for more information on name collision."
: "Domain was allocated")
.setResponseData(ImmutableList.of(
DomainPendingActionNotificationResponse.create(
application.getFullyQualifiedDomainName(),
true,
// If the creation TRID is not present on the application (this can happen for
// older applications written before this field was added), then we must read
// the earliest history entry for the application to retrieve it.
application.getCreationTrid() == null
? checkNotNull(ofy()
.load()
.type(HistoryEntry.class)
.ancestor(application)
.order("modificationTime")
.first()
.now()
.getTrid())
: application.getCreationTrid(),
now)))
.setResponseExtensions(ImmutableList.of(
new LaunchInfoResponseExtension.Builder()
.setApplicationId(application.getForeignKey())
.setPhase(application.getPhase())
.setApplicationStatus(ApplicationStatus.ALLOCATED)
.build()))
.setParent(historyEntry)
.build(),
// Create a history entry (with no xml or trid) to record that we updated the application.
new HistoryEntry.Builder()
.setType(HistoryEntry.Type.DOMAIN_APPLICATION_STATUS_UPDATE)
.setParent(application)
.setModificationTime(now)
.setClientId(application.getCurrentSponsorClientId())
.setBySuperuser(true)
.build());
// Set the properties on the new domain.
builder
.addGracePeriod(GracePeriod.forBillingEvent(
sunrushAddGracePeriod ? GracePeriodStatus.SUNRUSH_ADD : GracePeriodStatus.ADD,
billingEvent))
.setApplicationTime(allocateCreate.getApplicationTime())
.setApplication(Ref.create(application))
.setSmdId(allocateCreate.getSmdId())
.setLaunchNotice(allocateCreate.getNotice());
// Names on the collision list will not be delegated. Set server hold.
if (ReservationType.NAME_COLLISION == reservationType) {
builder.addStatusValue(StatusValue.SERVER_HOLD);
}
}
@Override
protected void enqueueLordnTaskIfNeeded() {
if (allocateCreate.getSmdId() != null || allocateCreate.getNotice() != null) {
LordnTask.enqueueDomainResourceTask(newResource);
}
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_ALLOCATE;
}
/** The allocate create extension is required to allocate a domain. */
static class MissingAllocateCreateExtensionException extends RequiredParameterMissingException {
public MissingAllocateCreateExtensionException() {
super("The allocate create extension is required to allocate a domain");
}
}
/** Domain application with specific ROID does not exist. */
static class MissingApplicationException extends ObjectDoesNotExistException {
public MissingApplicationException(String applicationRoid) {
super(DomainApplication.class, applicationRoid);
}
}
/** Domain application already has a final status. */
static class HasFinalStatusException extends StatusProhibitsOperationException {
public HasFinalStatusException() {
super("Domain application already has a final status");
}
}
/** Only a superuser can allocate domains. */
static class OnlySuperuserCanAllocateException extends AuthorizationErrorException {
public OnlySuperuserCanAllocateException() {
super("Only a superuser can allocate domains");
}
}
}

View file

@ -0,0 +1,253 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.DISALLOWED_TLD_STATES_FOR_LAUNCH_FLOWS;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateFeeChallenge;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import static com.google.domain.registry.model.index.DomainApplicationIndex.loadActiveApplicationsByDomainName;
import static com.google.domain.registry.model.index.ForeignKeyIndex.loadAndGetReference;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.CommandUseErrorException;
import com.google.domain.registry.flows.EppException.ObjectAlreadyExistsException;
import com.google.domain.registry.flows.EppException.RequiredParameterMissingException;
import com.google.domain.registry.model.domain.DomainApplication;
import com.google.domain.registry.model.domain.DomainApplication.Builder;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.Period;
import com.google.domain.registry.model.domain.fee.Fee;
import com.google.domain.registry.model.domain.fee.FeeCreateExtension;
import com.google.domain.registry.model.domain.fee.FeeCreateResponseExtension;
import com.google.domain.registry.model.domain.launch.ApplicationStatus;
import com.google.domain.registry.model.domain.launch.LaunchCreateExtension;
import com.google.domain.registry.model.domain.launch.LaunchCreateResponseExtension;
import com.google.domain.registry.model.domain.launch.LaunchPhase;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.eppoutput.CreateData.DomainCreateData;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
import com.google.domain.registry.model.registry.Registry.TldState;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.model.smd.AbstractSignedMark;
import com.google.domain.registry.model.smd.EncodedSignedMark;
import java.util.List;
/**
* An EPP flow that creates a new application for a domain resource.
*
* @error {@link com.google.domain.registry.flows.EppException.UnimplementedExtensionException}
* @error {@link com.google.domain.registry.flows.ResourceFlow.BadCommandForRegistryPhaseException}
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link com.google.domain.registry.flows.ResourceCreateFlow.ResourceAlreadyExistsException}
* @error {@link BaseDomainCreateFlow.AcceptedTooLongAgoException}
* @error {@link BaseDomainCreateFlow.ClaimsPeriodEndedException}
* @error {@link BaseDomainCreateFlow.ExpiredClaimException}
* @error {@link BaseDomainCreateFlow.InvalidTcnIdChecksumException}
* @error {@link BaseDomainCreateFlow.InvalidTrademarkValidatorException}
* @error {@link BaseDomainCreateFlow.MalformedTcnIdException}
* @error {@link BaseDomainCreateFlow.MaxSigLifeNotSupportedException}
* @error {@link BaseDomainCreateFlow.MissingClaimsNoticeException}
* @error {@link BaseDomainCreateFlow.UnexpectedClaimsNoticeException}
* @error {@link BaseDomainCreateFlow.UnsupportedMarkTypeException}
* @error {@link DomainApplicationCreateFlow.LandrushApplicationDisallowedDuringSunriseException}
* @error {@link DomainApplicationCreateFlow.NoticeCannotBeUsedWithSignedMarkException}
* @error {@link DomainApplicationCreateFlow.SunriseApplicationDisallowedDuringLandrushException}
* @error {@link DomainApplicationCreateFlow.UncontestedSunriseApplicationBlockedInLandrushException}
* @error {@link DomainFlowUtils.BadDomainNameCharacterException}
* @error {@link DomainFlowUtils.BadDomainNamePartsCountException}
* @error {@link DomainFlowUtils.BadPeriodUnitException}
* @error {@link DomainFlowUtils.Base64RequiredForEncodedSignedMarksException}
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
* @error {@link DomainFlowUtils.CurrencyValueScaleException}
* @error {@link DomainFlowUtils.DashesInThirdAndFourthException}
* @error {@link DomainFlowUtils.DomainLabelTooLongException}
* @error {@link DomainFlowUtils.DomainReservedException}
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
* @error {@link DomainFlowUtils.EmptyDomainNamePartException}
* @error {@link DomainFlowUtils.FeesMismatchException}
* @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException}
* @error {@link DomainFlowUtils.InvalidIdnDomainLabelException}
* @error {@link DomainFlowUtils.InvalidPunycodeException}
* @error {@link DomainFlowUtils.LaunchPhaseMismatchException}
* @error {@link DomainFlowUtils.LeadingDashException}
* @error {@link DomainFlowUtils.LinkedResourceDoesNotExistException}
* @error {@link DomainFlowUtils.MissingContactTypeException}
* @error {@link DomainFlowUtils.NameserverNotAllowedException}
* @error {@link DomainFlowUtils.NoMarksFoundMatchingDomainException}
* @error {@link DomainFlowUtils.PremiumNameBlockedException}
* @error {@link DomainFlowUtils.RegistrantNotAllowedException}
* @error {@link DomainFlowUtils.SignedMarksMustBeEncodedException}
* @error {@link DomainFlowUtils.SignedMarkCertificateExpiredException}
* @error {@link DomainFlowUtils.SignedMarkCertificateInvalidException}
* @error {@link DomainFlowUtils.SignedMarkCertificateNotYetValidException}
* @error {@link DomainFlowUtils.SignedMarkCertificateRevokedException}
* @error {@link DomainFlowUtils.SignedMarkCertificateSignatureException}
* @error {@link DomainFlowUtils.SignedMarkEncodingErrorException}
* @error {@link DomainFlowUtils.SignedMarkParsingErrorException}
* @error {@link DomainFlowUtils.SignedMarkRevokedErrorException}
* @error {@link DomainFlowUtils.SignedMarkSignatureException}
* @error {@link DomainFlowUtils.TldDoesNotExistException}
* @error {@link DomainFlowUtils.TooManyDsRecordsException}
* @error {@link DomainFlowUtils.TooManyNameserversException}
* @error {@link DomainFlowUtils.TooManySignedMarksException}
* @error {@link DomainFlowUtils.TrailingDashException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
*/
public class DomainApplicationCreateFlow extends BaseDomainCreateFlow<DomainApplication, Builder> {
@Override
protected void initDomainCreateFlow() {
registerExtensions(FeeCreateExtension.class, LaunchCreateExtension.class);
}
@Override
protected void validateDomainLaunchCreateExtension() throws EppException {
if (launchCreate.getSignedMarks().isEmpty()) {
// During sunrise, a signed mark is required since only trademark holders are allowed to
// create an application. However, we found no marks (ie, this was a landrush application).
if (tldState == TldState.SUNRISE) {
throw new LandrushApplicationDisallowedDuringSunriseException();
}
} else {
if (launchCreate.getNotice() != null) { // Can't use a claims notice id with a signed mark.
throw new NoticeCannotBeUsedWithSignedMarkException();
}
if (tldState == TldState.LANDRUSH) {
throw new SunriseApplicationDisallowedDuringLandrushException();
}
}
}
@Override
protected void verifyDomainCreateIsAllowed() throws EppException {
validateFeeChallenge(targetId, getTld(), feeCreate, createCost);
if (tldState == TldState.LANDRUSH && !superuser) {
// Prohibit creating a landrush application in LANDRUSH (but not in SUNRUSH) if there is
// exactly one sunrise application for the same name.
List<DomainApplication> applications = FluentIterable
.from(loadActiveApplicationsByDomainName(targetId, now))
.limit(2)
.toList();
if (applications.size() == 1 && applications.get(0).getPhase().equals(LaunchPhase.SUNRISE)) {
throw new UncontestedSunriseApplicationBlockedInLandrushException();
}
}
// Fail if the domain is already registered (e.g. this is a landrush application but the domain
// was awarded at the end of sunrise).
if (loadAndGetReference(DomainResource.class, targetId, now) != null) {
throw new ResourceAlreadyExistsException(targetId);
}
}
@Override
protected void setDomainCreateProperties(Builder builder) {
builder
.setCreationTrid(trid)
.setPhase(launchCreate.getPhase())
.setApplicationStatus(ApplicationStatus.VALIDATED)
.addStatusValue(StatusValue.PENDING_CREATE);
if (!launchCreate.getSignedMarks().isEmpty()) {
builder.setEncodedSignedMarks(FluentIterable
.from(launchCreate.getSignedMarks())
.transform(new Function<AbstractSignedMark, EncodedSignedMark>() {
@Override
public EncodedSignedMark apply(AbstractSignedMark abstractSignedMark) {
// We verified that this is the case in verifyDomainCreateIsAllowed().
return (EncodedSignedMark) abstractSignedMark;
}})
.toList());
}
}
@Override
protected final ImmutableSet<TldState> getDisallowedTldStates() {
return DISALLOWED_TLD_STATES_FOR_LAUNCH_FLOWS;
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_APPLICATION_CREATE;
}
@Override
protected final Period getCommandPeriod() {
return command.getPeriod();
}
@Override
protected boolean tryToLoadExisting() {
// Multiple domain applications can be created for the same targetId (which is the fully
// qualified domain name), so don't try to load an existing resource with the same target id.
return false;
}
@Override
protected final EppOutput getOutput() {
ImmutableList.Builder<ResponseExtension> responseExtensionsBuilder =
new ImmutableList.Builder<>();
responseExtensionsBuilder.add(new LaunchCreateResponseExtension.Builder()
.setPhase(launchCreate.getPhase())
.setApplicationId(newResource.getForeignKey())
.build());
if (feeCreate != null) {
responseExtensionsBuilder.add(new FeeCreateResponseExtension.Builder()
.setCurrency(createCost.getCurrencyUnit())
.setFee(ImmutableList.of(Fee.create(createCost.getAmount(), "create")))
.build());
}
return createOutput(
Success,
DomainCreateData.create(newResource.getFullyQualifiedDomainName(), now, null),
responseExtensionsBuilder.build());
}
/** Landrush applications are disallowed during sunrise. */
static class LandrushApplicationDisallowedDuringSunriseException
extends RequiredParameterMissingException {
public LandrushApplicationDisallowedDuringSunriseException() {
super("Landrush applications are disallowed during sunrise");
}
}
/** A notice cannot be specified when using a signed mark. */
static class NoticeCannotBeUsedWithSignedMarkException extends CommandUseErrorException {
public NoticeCannotBeUsedWithSignedMarkException() {
super("A notice cannot be specified when using a signed mark");
}
}
/** Sunrise applications are disallowed during landrush. */
static class SunriseApplicationDisallowedDuringLandrushException
extends CommandUseErrorException {
public SunriseApplicationDisallowedDuringLandrushException() {
super("Sunrise applications are disallowed during landrush");
}
}
/** This name has already been claimed by a sunrise applicant. */
static class UncontestedSunriseApplicationBlockedInLandrushException
extends ObjectAlreadyExistsException {
public UncontestedSunriseApplicationBlockedInLandrushException() {
super("This name has already been claimed by a sunrise applicant");
}
}
}

View file

@ -0,0 +1,96 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.DISALLOWED_TLD_STATES_FOR_LAUNCH_FLOWS;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyLaunchApplicationIdMatchesDomain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyLaunchPhase;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.StatusProhibitsOperationException;
import com.google.domain.registry.flows.ResourceSyncDeleteFlow;
import com.google.domain.registry.model.domain.DomainApplication;
import com.google.domain.registry.model.domain.DomainApplication.Builder;
import com.google.domain.registry.model.domain.DomainCommand.Delete;
import com.google.domain.registry.model.domain.launch.LaunchDeleteExtension;
import com.google.domain.registry.model.domain.launch.LaunchPhase;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.registry.Registry.TldState;
import com.google.domain.registry.model.reporting.HistoryEntry;
import java.util.Set;
/**
* An EPP flow that deletes a domain application.
*
* @error {@link com.google.domain.registry.flows.EppException.UnimplementedExtensionException}
* @error {@link com.google.domain.registry.flows.ResourceFlow.BadCommandForRegistryPhaseException}
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link DomainApplicationDeleteFlow.SunriseApplicationCannotBeDeletedInLandrushException}
* @error {@link DomainFlowUtils.ApplicationDomainNameMismatchException}
* @error {@link DomainFlowUtils.LaunchPhaseMismatchException}
*/
public class DomainApplicationDeleteFlow
extends ResourceSyncDeleteFlow<DomainApplication, Builder, Delete> {
@Override
protected void initResourceCreateOrMutateFlow() throws EppException {
registerExtensions(LaunchDeleteExtension.class);
}
@Override
protected void verifyMutationOnOwnedResourceAllowed() throws EppException {
String tld = existingResource.getTld();
checkRegistryStateForTld(tld);
checkAllowedAccessToTld(getAllowedTlds(), tld);
verifyLaunchPhase(tld, eppInput.getSingleExtension(LaunchDeleteExtension.class), now);
verifyLaunchApplicationIdMatchesDomain(command, existingResource);
// Don't allow deleting a sunrise application during landrush.
if (existingResource.getPhase().equals(LaunchPhase.SUNRISE)
&& Registry.get(existingResource.getTld()).getTldState(now).equals(TldState.LANDRUSH)
&& !superuser) {
throw new SunriseApplicationCannotBeDeletedInLandrushException();
}
}
@Override
protected final ImmutableSet<TldState> getDisallowedTldStates() {
return DISALLOWED_TLD_STATES_FOR_LAUNCH_FLOWS;
}
/** Domain applications do not respect status values that prohibit various operations. */
@Override
protected Set<StatusValue> getDisallowedStatuses() {
return ImmutableSet.of();
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_APPLICATION_DELETE;
}
/** A sunrise application cannot be deleted during landrush. */
static class SunriseApplicationCannotBeDeletedInLandrushException
extends StatusProhibitsOperationException {
public SunriseApplicationCannotBeDeletedInLandrushException() {
super("A sunrise application cannot be deleted during landrush");
}
}
}

View file

@ -0,0 +1,117 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.EppXmlTransformer.unmarshal;
import static com.google.domain.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyLaunchApplicationIdMatchesDomain;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.ParameterValuePolicyErrorException;
import com.google.domain.registry.flows.EppException.RequiredParameterMissingException;
import com.google.domain.registry.model.domain.DomainApplication;
import com.google.domain.registry.model.domain.DomainApplication.Builder;
import com.google.domain.registry.model.domain.launch.LaunchInfoExtension;
import com.google.domain.registry.model.domain.launch.LaunchInfoResponseExtension;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
import com.google.domain.registry.model.mark.Mark;
import com.google.domain.registry.model.smd.EncodedSignedMark;
import com.google.domain.registry.model.smd.SignedMark;
/**
* An EPP flow that reads a domain application.
*
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException}
* @error {@link DomainFlowUtils.ApplicationDomainNameMismatchException}
* @error {@link DomainApplicationInfoFlow.ApplicationLaunchPhaseMismatchException}
* @error {@link DomainApplicationInfoFlow.MissingApplicationIdException}
*/
public class DomainApplicationInfoFlow extends BaseDomainInfoFlow<DomainApplication, Builder> {
private boolean includeMarks;
@Override
protected final void initSingleResourceFlow() throws EppException {
registerExtensions(LaunchInfoExtension.class);
// We need to do this in init rather than verify or we'll get the generic "object not found".
LaunchInfoExtension extension = eppInput.getSingleExtension(LaunchInfoExtension.class);
if (extension.getApplicationId() == null) {
throw new MissingApplicationIdException();
}
includeMarks = Boolean.TRUE.equals(extension.getIncludeMark()); // Default to false.
}
@Override
protected final void verifyQueryIsAllowed() throws EppException {
verifyLaunchApplicationIdMatchesDomain(command, existingResource);
if (!existingResource.getPhase().equals(
eppInput.getSingleExtension(LaunchInfoExtension.class).getPhase())) {
throw new ApplicationLaunchPhaseMismatchException();
}
}
@Override
protected final DomainApplication getResourceInfo() throws EppException {
// We don't support authInfo for applications, so if it's another registrar always fail.
verifyResourceOwnership(getClientId(), existingResource);
if (!command.getHostsRequest().requestDelegated()) {
// Delegated hosts are present by default, so clear them out if they aren't wanted.
// This requires overriding the implicit status values so that we don't get INACTIVE added due
// to the missing nameservers.
return existingResource.asBuilder()
.setNameservers(null)
.buildWithoutImplicitStatusValues();
}
return existingResource;
}
@Override
protected final ImmutableList<? extends ResponseExtension> getDomainResponseExtensions()
throws EppException {
ImmutableList.Builder<Mark> marksBuilder = new ImmutableList.Builder<>();
if (includeMarks) {
for (EncodedSignedMark encodedMark : existingResource.getEncodedSignedMarks()) {
try {
marksBuilder.add(((SignedMark) unmarshal(encodedMark.getBytes())).getMark());
} catch (EppException e) {
// This is a serious error; don't let the benign EppException propagate.
throw new IllegalStateException("Could not decode a stored encoded signed mark");
}
}
}
return ImmutableList.of(new LaunchInfoResponseExtension.Builder()
.setPhase(existingResource.getPhase())
.setApplicationId(existingResource.getForeignKey())
.setApplicationStatus(existingResource.getApplicationStatus())
.setMarks(marksBuilder.build())
.build());
}
/** Application id is required. */
static class MissingApplicationIdException extends RequiredParameterMissingException {
public MissingApplicationIdException() {
super("Application id is required");
}
}
/** Declared launch extension phase does not match phase of the application. */
static class ApplicationLaunchPhaseMismatchException extends ParameterValuePolicyErrorException {
public ApplicationLaunchPhaseMismatchException() {
super("Declared launch extension phase does not match the phase of the application");
}
}
}

View file

@ -0,0 +1,97 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.common.base.CaseFormat.LOWER_CAMEL;
import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.DISALLOWED_TLD_STATES_FOR_LAUNCH_FLOWS;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.StatusProhibitsOperationException;
import com.google.domain.registry.model.domain.DomainApplication;
import com.google.domain.registry.model.domain.DomainApplication.Builder;
import com.google.domain.registry.model.domain.launch.ApplicationStatus;
import com.google.domain.registry.model.domain.launch.LaunchUpdateExtension;
import com.google.domain.registry.model.domain.secdns.SecDnsUpdateExtension;
import com.google.domain.registry.model.registry.Registry.TldState;
import com.google.domain.registry.model.reporting.HistoryEntry;
/**
* An EPP flow that updates a domain resource.
*
* @error {@link com.google.domain.registry.flows.EppException.UnimplementedExtensionException}
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceUpdateFlow.AddRemoveSameValueEppException}
* @error {@link com.google.domain.registry.flows.ResourceUpdateFlow.ResourceHasClientUpdateProhibitedException}
* @error {@link com.google.domain.registry.flows.ResourceUpdateFlow.StatusNotClientSettableException}
* @error {@link com.google.domain.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException}
* @error {@link BaseDomainUpdateFlow.EmptySecDnsUpdateException}
* @error {@link BaseDomainUpdateFlow.MaxSigLifeChangeNotSupportedException}
* @error {@link BaseDomainUpdateFlow.SecDnsAllUsageException}
* @error {@link BaseDomainUpdateFlow.UrgentAttributeNotSupportedException}
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
* @error {@link DomainFlowUtils.LinkedResourceDoesNotExistException}
* @error {@link DomainFlowUtils.MissingAdminContactException}
* @error {@link DomainFlowUtils.MissingContactTypeException}
* @error {@link DomainFlowUtils.MissingTechnicalContactException}
* @error {@link DomainFlowUtils.NameserverNotAllowedException}
* @error {@link DomainFlowUtils.RegistrantNotAllowedException}
* @error {@link DomainFlowUtils.TooManyDsRecordsException}
* @error {@link DomainFlowUtils.TooManyNameserversException}
* @error {@link DomainApplicationUpdateFlow.ApplicationStatusProhibitsUpdateException}
*/
public class DomainApplicationUpdateFlow
extends BaseDomainUpdateFlow<DomainApplication, Builder> {
@Override
protected void initDomainUpdateFlow() throws EppException {
registerExtensions(LaunchUpdateExtension.class, SecDnsUpdateExtension.class);
}
@Override
protected final void verifyDomainUpdateIsAllowed() throws EppException {
switch (existingResource.getApplicationStatus()) {
case PENDING_ALLOCATION:
case PENDING_VALIDATION:
case VALIDATED:
return;
default:
throw new ApplicationStatusProhibitsUpdateException(
existingResource.getApplicationStatus());
}
}
@Override
protected final ImmutableSet<TldState> getDisallowedTldStates() {
return DISALLOWED_TLD_STATES_FOR_LAUNCH_FLOWS;
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_APPLICATION_UPDATE;
}
/** Application status prohibits this domain update. */
static class ApplicationStatusProhibitsUpdateException extends StatusProhibitsOperationException {
public ApplicationStatusProhibitsUpdateException(ApplicationStatus status) {
super(String.format(
"Applications in state %s can not be updated",
UPPER_UNDERSCORE.to(LOWER_CAMEL, status.name())));
}
}
}

View file

@ -0,0 +1,128 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.getReservationType;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.handleFeeRequest;
import static com.google.domain.registry.model.EppResourceUtils.checkResourcesExist;
import static com.google.domain.registry.model.registry.label.ReservationType.UNRESERVED;
import static com.google.domain.registry.util.CollectionUtils.nullToEmpty;
import static com.google.domain.registry.util.DomainNameUtils.getTldFromDomainName;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.ParameterValuePolicyErrorException;
import com.google.domain.registry.model.domain.fee.FeeCheckExtension;
import com.google.domain.registry.model.domain.fee.FeeCheckResponseExtension;
import com.google.domain.registry.model.domain.fee.FeeCheckResponseExtension.FeeCheck;
import com.google.domain.registry.model.domain.launch.LaunchCheckExtension;
import com.google.domain.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
import com.google.domain.registry.model.eppoutput.CheckData;
import com.google.domain.registry.model.eppoutput.CheckData.DomainCheck;
import com.google.domain.registry.model.eppoutput.CheckData.DomainCheckData;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.registry.label.ReservationType;
import java.util.Set;
/**
* An EPP flow that checks whether a domain can be provisioned.
*
* @error {@link com.google.domain.registry.flows.ResourceCheckFlow.TooManyResourceChecksException}
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link DomainFlowUtils.BadDomainNameCharacterException}
* @error {@link DomainFlowUtils.BadDomainNamePartsCountException}
* @error {@link DomainFlowUtils.BadPeriodUnitException}
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
* @error {@link DomainFlowUtils.DashesInThirdAndFourthException}
* @error {@link DomainFlowUtils.DomainLabelTooLongException}
* @error {@link DomainFlowUtils.EmptyDomainNamePartException}
* @error {@link DomainFlowUtils.FeeChecksDontSupportPhasesException}
* @error {@link DomainFlowUtils.InvalidIdnDomainLabelException}
* @error {@link DomainFlowUtils.InvalidPunycodeException}
* @error {@link DomainFlowUtils.LeadingDashException}
* @error {@link DomainFlowUtils.RestoresAreAlwaysForOneYearException}
* @error {@link DomainFlowUtils.TldDoesNotExistException}
* @error {@link DomainFlowUtils.TrailingDashException}
* @error {@link DomainFlowUtils.UnknownFeeCommandException}
* @error {@link DomainCheckFlow.OnlyCheckedNamesCanBeFeeCheckedException}
*/
public class DomainCheckFlow extends BaseDomainCheckFlow {
@Override
protected void initDomainCheckFlow() throws EppException {
registerExtensions(LaunchCheckExtension.class, FeeCheckExtension.class);
}
private String getMessageForCheck(String targetId, Set<String> existingIds) {
if (existingIds.contains(targetId)) {
return "In use";
}
InternetDomainName domainName = domainNames.get(targetId);
ReservationType reservationType = getReservationType(domainName);
Registry registry = Registry.get(domainName.parent().toString());
if (reservationType == UNRESERVED
&& registry.isPremiumName(domainName)
&& registry.getPremiumPriceAckRequired()
&& !nullToEmpty(sessionMetadata.getServiceExtensionUris()).contains(
ServiceExtension.FEE_0_6.getUri())) {
return "Premium names require EPP ext.";
}
return reservationType.getMessageForCheck();
}
@Override
protected CheckData getCheckData() {
Set<String> existingIds = checkResourcesExist(resourceClass, targetIds, now);
ImmutableList.Builder<DomainCheck> checks = new ImmutableList.Builder<>();
for (String id : targetIds) {
String message = getMessageForCheck(id, existingIds);
checks.add(DomainCheck.create(message == null, id, message));
}
return DomainCheckData.create(checks.build());
}
/** Handle the fee check extension. */
@Override
protected ImmutableList<? extends ResponseExtension> getResponseExtensions() throws EppException {
FeeCheckExtension feeCheck = eppInput.getSingleExtension(FeeCheckExtension.class);
if (feeCheck == null) {
return null; // No fee checks were requested.
}
ImmutableList.Builder<FeeCheck> feeChecksBuilder = new ImmutableList.Builder<>();
for (FeeCheckExtension.DomainCheck domainCheck : feeCheck.getDomains()) {
String domainName = domainCheck.getName();
if (!domainNames.containsKey(domainName)) {
// Although the fee extension explicitly says it's ok to fee check a domain name that you
// aren't also availability checking, we forbid it. This makes the experience simpler and
// also means we can assume any domain names in the fee checks have been validated.
throw new OnlyCheckedNamesCanBeFeeCheckedException();
}
FeeCheck.Builder builder = new FeeCheck.Builder();
handleFeeRequest(domainCheck, builder, domainName, getTldFromDomainName(domainName), now);
feeChecksBuilder.add(builder.setName(domainName).build());
}
return ImmutableList.of(FeeCheckResponseExtension.create(feeChecksBuilder.build()));
}
/** By server policy, fee check names must be listed in the availability check. */
static class OnlyCheckedNamesCanBeFeeCheckedException extends ParameterValuePolicyErrorException {
OnlyCheckedNamesCanBeFeeCheckedException() {
super("By server policy, fee check names must be listed in the availability check");
}
}
}

View file

@ -0,0 +1,199 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateFeeChallenge;
import static com.google.domain.registry.model.index.DomainApplicationIndex.loadActiveApplicationsByDomainName;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.CommandUseErrorException;
import com.google.domain.registry.flows.EppException.StatusProhibitsOperationException;
import com.google.domain.registry.model.billing.BillingEvent;
import com.google.domain.registry.model.billing.BillingEvent.Reason;
import com.google.domain.registry.model.domain.DomainApplication;
import com.google.domain.registry.model.domain.DomainResource.Builder;
import com.google.domain.registry.model.domain.GracePeriod;
import com.google.domain.registry.model.domain.fee.FeeCreateExtension;
import com.google.domain.registry.model.domain.launch.LaunchCreateExtension;
import com.google.domain.registry.model.domain.rgp.GracePeriodStatus;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.registry.Registry.TldState;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.tmch.LordnTask;
import java.util.Set;
/**
* An EPP flow that creates a new domain resource.
*
* @error {@link com.google.domain.registry.flows.EppException.UnimplementedExtensionException}
* @error {@link com.google.domain.registry.flows.LoggedInFlow.UndeclaredServiceExtensionException}
* @error {@link com.google.domain.registry.flows.ResourceCreateFlow.ResourceAlreadyExistsException}
* @error {@link com.google.domain.registry.flows.ResourceCreateOrMutateFlow.OnlyToolCanPassMetadataException}
* @error {@link com.google.domain.registry.flows.ResourceFlow.BadCommandForRegistryPhaseException}
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link BaseDomainCreateFlow.AcceptedTooLongAgoException}
* @error {@link BaseDomainCreateFlow.ClaimsPeriodEndedException}
* @error {@link BaseDomainCreateFlow.ExpiredClaimException}
* @error {@link BaseDomainCreateFlow.InvalidTcnIdChecksumException}
* @error {@link BaseDomainCreateFlow.InvalidTrademarkValidatorException}
* @error {@link BaseDomainCreateFlow.MalformedTcnIdException}
* @error {@link BaseDomainCreateFlow.MaxSigLifeNotSupportedException}
* @error {@link BaseDomainCreateFlow.MissingClaimsNoticeException}
* @error {@link BaseDomainCreateFlow.UnexpectedClaimsNoticeException}
* @error {@link BaseDomainCreateFlow.UnsupportedMarkTypeException}
* @error {@link DomainCreateFlow.SignedMarksNotAcceptedInCurrentPhaseException}
* @error {@link DomainFlowUtils.BadDomainNameCharacterException}
* @error {@link DomainFlowUtils.BadDomainNamePartsCountException}
* @error {@link DomainFlowUtils.BadPeriodUnitException}
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
* @error {@link DomainFlowUtils.CurrencyValueScaleException}
* @error {@link DomainFlowUtils.DashesInThirdAndFourthException}
* @error {@link DomainFlowUtils.DomainLabelTooLongException}
* @error {@link DomainFlowUtils.DomainReservedException}
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
* @error {@link DomainFlowUtils.EmptyDomainNamePartException}
* @error {@link DomainFlowUtils.FeesMismatchException}
* @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException}
* @error {@link DomainFlowUtils.InvalidIdnDomainLabelException}
* @error {@link DomainFlowUtils.InvalidPunycodeException}
* @error {@link DomainFlowUtils.LeadingDashException}
* @error {@link DomainFlowUtils.LinkedResourceDoesNotExistException}
* @error {@link DomainFlowUtils.LinkedResourceInPendingDeleteProhibitsOperationException}
* @error {@link DomainFlowUtils.MissingAdminContactException}
* @error {@link DomainFlowUtils.MissingContactTypeException}
* @error {@link DomainFlowUtils.MissingRegistrantException}
* @error {@link DomainFlowUtils.MissingTechnicalContactException}
* @error {@link DomainFlowUtils.NameserverNotAllowedException}
* @error {@link DomainFlowUtils.PremiumNameBlockedException}
* @error {@link DomainFlowUtils.RegistrantNotAllowedException}
* @error {@link DomainFlowUtils.TldDoesNotExistException}
* @error {@link DomainFlowUtils.TooManyDsRecordsException}
* @error {@link DomainFlowUtils.TooManyNameserversException}
* @error {@link DomainFlowUtils.TrailingDashException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link DomainCreateFlow.DomainHasOpenApplicationsException}
* @error {@link DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException}
*/
public class DomainCreateFlow extends DomainCreateOrAllocateFlow {
private static final Set<TldState> QLP_SMD_ALLOWED_STATES =
Sets.immutableEnumSet(TldState.SUNRISE, TldState.SUNRUSH);
private boolean isAnchorTenant() {
return isAnchorTenantViaReservation || isAnchorTenantViaExtension;
}
@Override
protected final void verifyDomainCreateIsAllowed() throws EppException {
String tld = getTld();
validateFeeChallenge(targetId, tld, feeCreate, createCost);
if (!superuser) {
// Prohibit creating a domain if there is an open application for the same name.
for (DomainApplication application : loadActiveApplicationsByDomainName(targetId, now)) {
if (!application.getApplicationStatus().isFinalStatus()) {
throw new DomainHasOpenApplicationsException();
}
}
// Prohibit registrations for non-qlp and non-superuser outside of GA.
if (!isAnchorTenant()
&& Registry.get(tld).getTldState(now) != TldState.GENERAL_AVAILABILITY) {
throw new NoGeneralRegistrationsInCurrentPhaseException();
}
}
}
@Override
protected final void initDomainCreateOrAllocateFlow() {
registerExtensions(FeeCreateExtension.class, LaunchCreateExtension.class);
}
@Override
protected final void validateDomainLaunchCreateExtension() throws EppException {
// We can assume launchCreate is not null here.
// Only QLP domains can have a signed mark on a domain create, and only in sunrise or sunrush.
if (hasSignedMarks) {
if (isAnchorTenant() && QLP_SMD_ALLOWED_STATES.contains(
Registry.get(getTld()).getTldState(now))) {
return;
}
throw new SignedMarksNotAcceptedInCurrentPhaseException();
}
}
@Override
protected final void setDomainCreateOrAllocateProperties(Builder builder) throws EppException {
Registry registry = Registry.get(getTld());
// Bill for the create.
BillingEvent.OneTime createEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.CREATE)
.setTargetId(targetId)
.setClientId(getClientId())
.setPeriodYears(command.getPeriod().getValue())
.setCost(checkNotNull(createCost))
.setEventTime(now)
.setBillingTime(now.plus(isAnchorTenant()
? registry.getAnchorTenantAddGracePeriodLength()
: registry.getAddGracePeriodLength()))
.setFlags(isAnchorTenant() ? ImmutableSet.of(BillingEvent.Flag.ANCHOR_TENANT) : null)
.setParent(historyEntry)
.build();
ofy().save().entity(createEvent);
builder.addGracePeriod(GracePeriod.forBillingEvent(GracePeriodStatus.ADD, createEvent));
if (launchCreate != null && (launchCreate.getNotice() != null || hasSignedMarks)) {
builder
.setLaunchNotice(launchCreate.getNotice())
.setSmdId(signedMark == null ? null : signedMark.getId());
}
}
@Override
protected void enqueueLordnTaskIfNeeded() {
if (launchCreate != null && (launchCreate.getNotice() != null || hasSignedMarks)) {
LordnTask.enqueueDomainResourceTask(newResource);
}
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_CREATE;
}
/** There is an open application for this domain. */
static class DomainHasOpenApplicationsException extends StatusProhibitsOperationException {
public DomainHasOpenApplicationsException() {
super("There is an open application for this domain");
}
}
/** Signed marks are not accepted in the current registry phase. */
static class SignedMarksNotAcceptedInCurrentPhaseException extends CommandUseErrorException {
public SignedMarksNotAcceptedInCurrentPhaseException() {
super("Signed marks are not accepted in the current registry phase");
}
}
/** The current registry phase does not allow for general registrations. */
static class NoGeneralRegistrationsInCurrentPhaseException extends CommandUseErrorException {
public NoGeneralRegistrationsInCurrentPhaseException() {
super("The current registry phase does not allow for general registrations");
}
}
}

View file

@ -0,0 +1,121 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.DateTimeUtils.END_OF_TIME;
import static com.google.domain.registry.util.DateTimeUtils.leapSafeAddYears;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.dns.DnsQueue;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.model.billing.BillingEvent;
import com.google.domain.registry.model.billing.BillingEvent.Flag;
import com.google.domain.registry.model.billing.BillingEvent.Reason;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.DomainResource.Builder;
import com.google.domain.registry.model.domain.Period;
import com.google.domain.registry.model.domain.fee.Fee;
import com.google.domain.registry.model.domain.fee.FeeCreateResponseExtension;
import com.google.domain.registry.model.domain.metadata.MetadataExtension;
import com.google.domain.registry.model.eppoutput.CreateData.DomainCreateData;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.eppoutput.Result;
import com.google.domain.registry.model.poll.PollMessage;
import com.googlecode.objectify.Ref;
import org.joda.time.DateTime;
/** An EPP flow that creates or allocates a new domain resource. */
public abstract class DomainCreateOrAllocateFlow
extends BaseDomainCreateFlow<DomainResource, Builder> {
protected boolean isAnchorTenantViaExtension;
@Override
protected final void initDomainCreateFlow() {
registerExtensions(MetadataExtension.class);
isAnchorTenantViaExtension =
(metadataExtension != null && metadataExtension.getIsAnchorTenant());
initDomainCreateOrAllocateFlow();
}
protected abstract void initDomainCreateOrAllocateFlow();
@Override
protected final void setDomainCreateProperties(Builder builder) throws EppException {
DateTime registrationExpirationTime = leapSafeAddYears(now, command.getPeriod().getValue());
// Create a new autorenew billing event and poll message starting at the expiration time.
BillingEvent.Recurring autorenewEvent = new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(targetId)
.setClientId(getClientId())
.setEventTime(registrationExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
PollMessage.Autorenew autorenewPollMessage = new PollMessage.Autorenew.Builder()
.setTargetId(targetId)
.setClientId(getClientId())
.setEventTime(registrationExpirationTime)
.setMsg("Domain was auto-renewed.")
.setParent(historyEntry)
.build();
ofy().save().<Object>entities(autorenewEvent, autorenewPollMessage);
builder
.setRegistrationExpirationTime(registrationExpirationTime)
.setAutorenewBillingEvent(Ref.create(autorenewEvent))
.setAutorenewPollMessage(Ref.create(autorenewPollMessage));
setDomainCreateOrAllocateProperties(builder);
}
/** Subclasses must override this to set more fields, like any grace period. */
protected abstract void setDomainCreateOrAllocateProperties(Builder builder) throws EppException;
@Override
protected final void enqueueTasks() {
if (newResource.shouldPublishToDns()) {
DnsQueue.create().addDomainRefreshTask(newResource.getFullyQualifiedDomainName());
}
enqueueLordnTaskIfNeeded();
}
/** Subclasses must override this to enqueue any additional tasks. */
protected abstract void enqueueLordnTaskIfNeeded();
@Override
protected final Period getCommandPeriod() {
return command.getPeriod();
}
@Override
protected final EppOutput getOutput() {
return createOutput(
Result.Code.Success,
DomainCreateData.create(
newResource.getFullyQualifiedDomainName(),
now,
newResource.getRegistrationExpirationTime()),
(feeCreate == null) ? null : ImmutableList.of(
new FeeCreateResponseExtension.Builder()
.setCurrency(createCost.getCurrencyUnit())
.setFee(ImmutableList.of(Fee.create(createCost.getAmount(), "create")))
.build()));
}
}

View file

@ -0,0 +1,197 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import static com.google.domain.registry.model.eppoutput.Result.Code.SuccessWithActionPending;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.CollectionUtils.nullToEmpty;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.dns.DnsQueue;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.AssociationProhibitsOperationException;
import com.google.domain.registry.flows.ResourceSyncDeleteFlow;
import com.google.domain.registry.model.billing.BillingEvent;
import com.google.domain.registry.model.common.TimeOfYear;
import com.google.domain.registry.model.domain.DomainCommand.Delete;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.DomainResource.Builder;
import com.google.domain.registry.model.domain.GracePeriod;
import com.google.domain.registry.model.domain.fee.Credit;
import com.google.domain.registry.model.domain.fee.FeeDeleteResponseExtension;
import com.google.domain.registry.model.domain.metadata.MetadataExtension;
import com.google.domain.registry.model.domain.rgp.GracePeriodStatus;
import com.google.domain.registry.model.domain.secdns.SecDnsUpdateExtension;
import com.google.domain.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
import com.google.domain.registry.model.eppoutput.Result.Code;
import com.google.domain.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.googlecode.objectify.Key;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
/**
* An EPP flow that deletes a domain resource.
*
* @error {@link com.google.domain.registry.flows.ResourceCreateOrMutateFlow.OnlyToolCanPassMetadataException}
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException}
* @error {@link DomainDeleteFlow.DomainToDeleteHasHostsException}
*/
public class DomainDeleteFlow extends ResourceSyncDeleteFlow<DomainResource, Builder, Delete> {
PollMessage.OneTime deletePollMessage;
CurrencyUnit creditsCurrencyUnit;
ImmutableList<Credit> credits;
@Override
protected void initResourceCreateOrMutateFlow() throws EppException {
registerExtensions(SecDnsUpdateExtension.class, MetadataExtension.class);
}
@Override
protected final void verifyMutationOnOwnedResourceAllowed() throws EppException {
checkRegistryStateForTld(existingResource.getTld());
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld());
if (!existingResource.getSubordinateHosts().isEmpty()) {
throw new DomainToDeleteHasHostsException();
}
}
@Override
protected final void setDeleteProperties(Builder builder) {
// Only set to PENDING_DELETE if this domain is not in the Add Grace Period. If domain is in Add
// Grace Period, we delete it immediately.
// The base class code already handles the immediate delete case, so we only have to handle the
// pending delete case here.
if (!existingResource.getGracePeriodStatuses().contains(GracePeriodStatus.ADD)) {
Registry registry = Registry.get(existingResource.getTld());
// By default, this should be 30 days of grace, and 5 days of pending delete. */
DateTime deletionTime = now
.plus(registry.getRedemptionGracePeriodLength())
.plus(registry.getPendingDeleteLength());
deletePollMessage = new PollMessage.OneTime.Builder()
.setClientId(existingResource.getCurrentSponsorClientId())
.setEventTime(deletionTime)
.setMsg("Domain deleted.")
.setResponseData(ImmutableList.of(DomainPendingActionNotificationResponse.create(
existingResource.getFullyQualifiedDomainName(), true, trid, deletionTime)))
.setParent(historyEntry)
.build();
builder.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.setDeletionTime(deletionTime)
// Clear out all old grace periods and add REDEMPTION, which does not include a ref
// to a billing event because there isn't one for a domain delete.
.setGracePeriods(ImmutableSet.of(GracePeriod.createWithoutBillingEvent(
GracePeriodStatus.REDEMPTION,
now.plus(registry.getRedemptionGracePeriodLength()),
getClientId())))
.setDeletePollMessage(Key.create(deletePollMessage));
}
}
@Override
protected final void enqueueTasks() {
DnsQueue.create().addDomainRefreshTask(existingResource.getFullyQualifiedDomainName());
}
@Override
protected final void modifySyncDeleteRelatedResources() {
// Cancel any grace periods that were still active.
ImmutableList.Builder<Credit> creditsBuilder = new ImmutableList.Builder<>();
for (GracePeriod gracePeriod : existingResource.getGracePeriods()) {
// No cancellation is written if the grace period was not for a billable event.
if (gracePeriod.hasBillingEvent()) {
ofy().save().entity(
BillingEvent.Cancellation.forGracePeriod(gracePeriod, historyEntry, targetId));
Money cost;
if (gracePeriod.getType() == GracePeriodStatus.AUTO_RENEW) {
TimeOfYear recurrenceTimeOfYear =
checkNotNull(gracePeriod.getRecurringBillingEvent()).get().getRecurrenceTimeOfYear();
DateTime autoRenewTime = recurrenceTimeOfYear.beforeOrAt(now);
cost = Registry.get(existingResource.getTld())
.getDomainRenewCost(targetId, 1, autoRenewTime);
} else {
cost = checkNotNull(gracePeriod.getOneTimeBillingEvent()).get().getCost();
}
creditsBuilder.add(Credit.create(
cost.negated().getAmount(),
String.format("%s credit", gracePeriod.getType().getXmlName())));
creditsCurrencyUnit = cost.getCurrencyUnit();
}
}
credits = creditsBuilder.build();
// If the delete isn't immediate, save the poll message for when the delete will happen.
if (deletePollMessage != null) {
ofy().save().entity(deletePollMessage);
}
// Close the autorenew billing event and poll message. This may delete the poll message.
updateAutorenewRecurrenceEndTime(existingResource, now);
// If there's a pending transfer, the gaining client's autorenew billing
// event and poll message will already have been deleted in
// ResourceDeleteFlow since it's listed in serverApproveEntities.
}
@Override
protected final Code getDeleteResultCode() {
return newResource.getDeletionTime().isAfter(now)
? SuccessWithActionPending : Success;
}
@Override
protected final ImmutableList<? extends ResponseExtension> getDeleteResponseExtensions() {
if (!credits.isEmpty()
&& nullToEmpty(sessionMetadata.getServiceExtensionUris()).contains(
ServiceExtension.FEE_0_6.getUri())) {
return ImmutableList.of(new FeeDeleteResponseExtension.Builder()
.setCurrency(checkNotNull(creditsCurrencyUnit))
.setCredits(credits)
.build());
} else {
return null;
}
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_DELETE;
}
/** Domain to be deleted has subordinate hosts. */
static class DomainToDeleteHasHostsException extends AssociationProhibitsOperationException {
public DomainToDeleteHasHostsException() {
super("Domain to be deleted has subordinate hosts");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,94 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.handleFeeRequest;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.DomainResource.Builder;
import com.google.domain.registry.model.domain.fee.FeeInfoExtension;
import com.google.domain.registry.model.domain.fee.FeeInfoResponseExtension;
import com.google.domain.registry.model.domain.rgp.GracePeriodStatus;
import com.google.domain.registry.model.domain.rgp.RgpInfoExtension;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
/**
* An EPP flow that reads a domain.
*
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link com.google.domain.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException}
* @error {@link DomainFlowUtils.BadPeriodUnitException}
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
* @error {@link DomainFlowUtils.FeeChecksDontSupportPhasesException}
* @error {@link DomainFlowUtils.RestoresAreAlwaysForOneYearException}
*/
public class DomainInfoFlow extends BaseDomainInfoFlow<DomainResource, Builder> {
@Override
protected void initSingleResourceFlow() throws EppException {
registerExtensions(FeeInfoExtension.class);
}
@Override
protected final DomainResource getResourceInfo() {
// If authInfo is non-null, then the caller is authorized to see the full information since we
// will have already verified the authInfo is valid in ResourceQueryFlow.verifyIsAllowed().
if (!getClientId().equals(existingResource.getCurrentSponsorClientId())
&& command.getAuthInfo() == null) {
// Registrars can only see a few fields on unauthorized domains.
// This is a policy decision that is left up to us by the rfcs.
return new DomainResource.Builder()
.setFullyQualifiedDomainName(existingResource.getFullyQualifiedDomainName())
.setRepoId(existingResource.getRepoId())
.setCurrentSponsorClientId(existingResource.getCurrentSponsorClientId())
// If we didn't do this, we'd get implicit status values.
.buildWithoutImplicitStatusValues();
}
Builder info = existingResource.asBuilder();
if (!command.getHostsRequest().requestSubordinate()) {
info.setSubordinateHosts(null);
}
if (!command.getHostsRequest().requestDelegated()) {
// Delegated hosts are present by default, so clear them out if they aren't wanted.
// This requires overriding the implicit status values so that we don't get INACTIVE added due
// to the missing nameservers.
return info.setNameservers(null).buildWithoutImplicitStatusValues();
}
return info.build();
}
@Override
protected final ImmutableList<ResponseExtension> getDomainResponseExtensions()
throws EppException {
ImmutableList.Builder<ResponseExtension> extensions = new ImmutableList.Builder<>();
// According to RFC 5910 section 2, we should only return this if the client specified the
// "urn:ietf:params:xml:ns:rgp-1.0" when logging in. However, this is a "SHOULD" not a "MUST"
// and we are going to ignore it; clients who don't care about rgp can just ignore it.
ImmutableSet<GracePeriodStatus> gracePeriodStatuses = existingResource.getGracePeriodStatuses();
if (!gracePeriodStatuses.isEmpty()) {
extensions.add(RgpInfoExtension.create(gracePeriodStatuses));
}
FeeInfoExtension feeInfo = eppInput.getSingleExtension(FeeInfoExtension.class);
if (feeInfo != null) { // Fee check was requested.
FeeInfoResponseExtension.Builder builder = new FeeInfoResponseExtension.Builder();
handleFeeRequest(feeInfo, builder, getTargetId(), existingResource.getTld(), now);
extensions.add(builder.build());
}
return extensions.build();
}
}

View file

@ -0,0 +1,206 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.newAutorenewBillingEvent;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.newAutorenewPollMessage;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateFeeChallenge;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears;
import static com.google.domain.registry.model.domain.DomainResource.MAX_REGISTRATION_YEARS;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.DateTimeUtils.leapSafeAddYears;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.ObjectPendingTransferException;
import com.google.domain.registry.flows.EppException.ParameterValueRangeErrorException;
import com.google.domain.registry.flows.OwnedResourceMutateFlow;
import com.google.domain.registry.model.billing.BillingEvent;
import com.google.domain.registry.model.billing.BillingEvent.Reason;
import com.google.domain.registry.model.domain.DomainCommand.Renew;
import com.google.domain.registry.model.domain.DomainRenewData;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.GracePeriod;
import com.google.domain.registry.model.domain.Period;
import com.google.domain.registry.model.domain.fee.Fee;
import com.google.domain.registry.model.domain.fee.FeeRenewExtension;
import com.google.domain.registry.model.domain.fee.FeeRenewResponseExtension;
import com.google.domain.registry.model.domain.rgp.GracePeriodStatus;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.model.transfer.TransferStatus;
import com.googlecode.objectify.Ref;
import org.joda.money.Money;
import org.joda.time.DateTime;
import java.util.Set;
/**
* An EPP flow that updates a domain resource.
*
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException}
* @error {@link DomainFlowUtils.BadPeriodUnitException}
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
* @error {@link DomainFlowUtils.CurrencyValueScaleException}
* @error {@link DomainFlowUtils.FeesMismatchException}
* @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link DomainRenewFlow.DomainHasPendingTransferException}
* @error {@link DomainRenewFlow.ExceedsMaxRegistrationYearsException}
* @error {@link DomainRenewFlow.IncorrectCurrentExpirationDateException}
*/
public class DomainRenewFlow extends OwnedResourceMutateFlow<DomainResource, Renew> {
private static final Set<StatusValue> RENEW_DISALLOWED_STATUSES = ImmutableSet.of(
StatusValue.CLIENT_RENEW_PROHIBITED,
StatusValue.PENDING_DELETE,
StatusValue.SERVER_RENEW_PROHIBITED);
protected FeeRenewExtension feeRenew;
protected Money renewCost;
@Override
protected Set<StatusValue> getDisallowedStatuses() {
return RENEW_DISALLOWED_STATUSES;
}
@Override
public final void initResourceCreateOrMutateFlow() throws EppException {
registerExtensions(FeeRenewExtension.class);
feeRenew = eppInput.getSingleExtension(FeeRenewExtension.class);
}
@Override
protected void verifyMutationOnOwnedResourceAllowed() throws EppException {
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld());
// Verify that the resource does not have a pending transfer on it.
if (existingResource.getTransferData().getTransferStatus() == TransferStatus.PENDING) {
throw new DomainHasPendingTransferException(targetId);
}
verifyUnitIsYears(command.getPeriod());
// If the date they specify doesn't match the expiration, fail. (This is an idempotence check).
if (!command.getCurrentExpirationDate().equals(
existingResource.getRegistrationExpirationTime().toLocalDate())) {
throw new IncorrectCurrentExpirationDateException();
}
renewCost = Registry.get(existingResource.getTld())
.getDomainRenewCost(targetId, command.getPeriod().getValue(), now);
validateFeeChallenge(targetId, existingResource.getTld(), feeRenew, renewCost);
}
@Override
protected DomainResource createOrMutateResource() {
DateTime newExpirationTime = leapSafeAddYears(
existingResource.getRegistrationExpirationTime(), command.getPeriod().getValue());
// Bill for this explicit renew itself.
BillingEvent.OneTime explicitRenewEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.RENEW)
.setTargetId(targetId)
.setClientId(getClientId())
.setPeriodYears(command.getPeriod().getValue())
.setCost(checkNotNull(renewCost))
.setEventTime(now)
.setBillingTime(
now.plus(Registry.get(existingResource.getTld()).getRenewGracePeriodLength()))
.setParent(historyEntry)
.build();
// End the old autorenew billing event and poll message now. This may delete the poll message.
updateAutorenewRecurrenceEndTime(existingResource, now);
// Create a new autorenew billing event and poll message starting at the new expiration time.
BillingEvent.Recurring newAutorenewEvent = newAutorenewBillingEvent(existingResource)
.setEventTime(newExpirationTime)
.setParent(historyEntry)
.build();
PollMessage.Autorenew newAutorenewPollMessage = newAutorenewPollMessage(existingResource)
.setEventTime(newExpirationTime)
.setParent(historyEntry)
.build();
ofy().save().<Object>entities(explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage);
return existingResource.asBuilder()
.setRegistrationExpirationTime(newExpirationTime)
.setAutorenewBillingEvent(Ref.create(newAutorenewEvent))
.setAutorenewPollMessage(Ref.create(newAutorenewPollMessage))
.addGracePeriod(GracePeriod.forBillingEvent(GracePeriodStatus.RENEW, explicitRenewEvent))
.build();
}
@Override
protected void verifyNewStateIsAllowed() throws EppException {
if (leapSafeAddYears(now, MAX_REGISTRATION_YEARS)
.isBefore(newResource.getRegistrationExpirationTime())) {
throw new ExceedsMaxRegistrationYearsException();
}
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_RENEW;
}
@Override
protected final Period getCommandPeriod() {
return command.getPeriod();
}
@Override
protected final EppOutput getOutput() {
return createOutput(
Success,
DomainRenewData.create(
newResource.getFullyQualifiedDomainName(),
newResource.getRegistrationExpirationTime()),
(feeRenew == null) ? null : ImmutableList.of(
new FeeRenewResponseExtension.Builder()
.setCurrency(renewCost.getCurrencyUnit())
.setFee(ImmutableList.of(Fee.create(renewCost.getAmount(), "renew")))
.build()));
}
/** The domain has a pending transfer on it and so can't be explicitly renewed. */
public static class DomainHasPendingTransferException extends ObjectPendingTransferException {
public DomainHasPendingTransferException(String targetId) {
super(targetId);
}
}
/** The current expiration date is incorrect. */
static class IncorrectCurrentExpirationDateException extends ParameterValueRangeErrorException {
public IncorrectCurrentExpirationDateException() {
super("The current expiration date is incorrect");
}
}
/** New registration period exceeds maximum number of years. */
static class ExceedsMaxRegistrationYearsException extends ParameterValueRangeErrorException {
public ExceedsMaxRegistrationYearsException() {
super(String.format(
"Registrations cannot extend for more than %d years into the future",
MAX_REGISTRATION_YEARS));
}
}
}

View file

@ -0,0 +1,208 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.newAutorenewBillingEvent;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.newAutorenewPollMessage;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateFeeChallenge;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyNotReserved;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.dns.DnsQueue;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.CommandUseErrorException;
import com.google.domain.registry.flows.EppException.StatusProhibitsOperationException;
import com.google.domain.registry.flows.OwnedResourceMutateFlow;
import com.google.domain.registry.model.billing.BillingEvent;
import com.google.domain.registry.model.billing.BillingEvent.Reason;
import com.google.domain.registry.model.domain.DomainCommand.Update;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.fee.Fee;
import com.google.domain.registry.model.domain.fee.FeeUpdateExtension;
import com.google.domain.registry.model.domain.fee.FeeUpdateResponseExtension;
import com.google.domain.registry.model.domain.rgp.GracePeriodStatus;
import com.google.domain.registry.model.domain.rgp.RgpUpdateExtension;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.index.ForeignKeyIndex;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.googlecode.objectify.Ref;
import org.joda.money.Money;
import org.joda.time.DateTime;
/**
* An EPP flow that requests that a deleted domain be restored.
*
* @error {@link com.google.domain.registry.flows.EppException.UnimplementedExtensionException}
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
* @error {@link DomainFlowUtils.CurrencyValueScaleException}
* @error {@link DomainFlowUtils.DomainReservedException}
* @error {@link DomainFlowUtils.FeesMismatchException}
* @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException}
* @error {@link DomainFlowUtils.PremiumNameBlockedException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link DomainRestoreRequestFlow.DomainNotEligibleForRestoreException}
* @error {@link DomainRestoreRequestFlow.RestoreCommandIncludesChangesException}
*/
public class DomainRestoreRequestFlow extends OwnedResourceMutateFlow<DomainResource, Update> {
protected FeeUpdateExtension feeUpdate;
protected Money restoreCost;
protected Money renewCost;
@Override
protected final void initResourceCreateOrMutateFlow() throws EppException {
registerExtensions(FeeUpdateExtension.class, RgpUpdateExtension.class);
}
@Override
protected final void verifyMutationOnOwnedResourceAllowed() throws EppException {
// No other changes can be specified on a restore request.
if (!command.noChangesPresent()) {
throw new RestoreCommandIncludesChangesException();
}
// Domain must be in pendingDelete and within the redemptionPeriod to be eligible for restore.
if (!existingResource.getStatusValues().contains(StatusValue.PENDING_DELETE)
|| !existingResource.getGracePeriodStatuses().contains(GracePeriodStatus.REDEMPTION)) {
throw new DomainNotEligibleForRestoreException();
}
String tld = existingResource.getTld();
checkAllowedAccessToTld(getAllowedTlds(), tld);
if (!superuser) {
verifyNotReserved(InternetDomainName.from(targetId), false);
verifyPremiumNameIsNotBlocked(targetId, tld, getClientId());
}
feeUpdate = eppInput.getSingleExtension(FeeUpdateExtension.class);
restoreCost = Registry.get(tld).getStandardRestoreCost();
renewCost = Registry.get(tld).getDomainRenewCost(targetId, 1, now);
validateFeeChallenge(targetId, tld, feeUpdate, restoreCost, renewCost);
}
@Override
protected final DomainResource createOrMutateResource() throws EppException {
// We don't preserve the original expiration time of the domain when we restore, since doing so
// would require us to know if they received a grace period refund when they deleted the domain,
// and to charge them for that again. Instead, we just say that all restores get a fresh year of
// registration and bill them for that accordingly.
DateTime newExpirationTime = now.plusYears(1);
// Bill for the restore.
BillingEvent.OneTime restoreEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.RESTORE)
.setTargetId(targetId)
.setClientId(getClientId())
.setCost(restoreCost)
.setEventTime(now)
.setBillingTime(now)
.setParent(historyEntry)
.build();
// Create a new autorenew billing event and poll message starting at the new expiration time.
BillingEvent.Recurring autorenewEvent = newAutorenewBillingEvent(existingResource)
.setEventTime(newExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
PollMessage.Autorenew autorenewPollMessage = newAutorenewPollMessage(existingResource)
.setEventTime(newExpirationTime)
.setAutorenewEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
// Also bill for the 1 year cost of a domain renew. This is to avoid registrants being able to
// game the system for premium names by renewing, deleting, and then restoring to get a free
// year. Note that this billing event has no grace period; it is effective immediately.
BillingEvent.OneTime renewEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.RENEW)
.setTargetId(targetId)
.setClientId(getClientId())
.setPeriodYears(1)
.setCost(renewCost)
.setEventTime(now)
.setBillingTime(now)
.setParent(historyEntry)
.build();
ofy().save().<Object>entities(restoreEvent, autorenewEvent, autorenewPollMessage, renewEvent);
return existingResource.asBuilder()
.setRegistrationExpirationTime(newExpirationTime)
.setDeletionTime(END_OF_TIME)
.setStatusValues(null)
.setGracePeriods(null)
.setDeletePollMessage(null)
.setAutorenewBillingEvent(Ref.create(autorenewEvent))
.setAutorenewPollMessage(Ref.create(autorenewPollMessage))
.build();
}
@Override
protected void modifyRelatedResources() {
// Update the relevant {@link ForeignKey} to cache the new deletion time.
ofy().save().entity(ForeignKeyIndex.create(newResource, newResource.getDeletionTime()));
ofy().delete().key(existingResource.getDeletePollMessage());
}
@Override
protected void enqueueTasks() {
DnsQueue.create().addDomainRefreshTask(existingResource.getFullyQualifiedDomainName());
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_RESTORE;
}
@Override
protected final EppOutput getOutput() {
return createOutput(
Success,
null,
(feeUpdate == null) ? null : ImmutableList.of(
new FeeUpdateResponseExtension.Builder()
.setCurrency(restoreCost.getCurrencyUnit())
.setFee(ImmutableList.of(
Fee.create(restoreCost.getAmount(), "restore"),
Fee.create(renewCost.getAmount(), "renew")))
.build()));
}
/** Restore command cannot have other changes specified. */
static class RestoreCommandIncludesChangesException extends CommandUseErrorException {
public RestoreCommandIncludesChangesException() {
super("Restore command cannot have other changes specified");
}
}
/** Domain is not eligible for restore. */
static class DomainNotEligibleForRestoreException extends StatusProhibitsOperationException {
public DomainNotEligibleForRestoreException() {
super("Domain is not eligible for restore");
}
}
}

View file

@ -0,0 +1,140 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static com.google.domain.registry.model.domain.DomainResource.extendRegistrationWithCap;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.ResourceTransferApproveFlow;
import com.google.domain.registry.model.billing.BillingEvent;
import com.google.domain.registry.model.billing.BillingEvent.Flag;
import com.google.domain.registry.model.billing.BillingEvent.Reason;
import com.google.domain.registry.model.domain.DomainCommand.Transfer;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.DomainResource.Builder;
import com.google.domain.registry.model.domain.GracePeriod;
import com.google.domain.registry.model.domain.rgp.GracePeriodStatus;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.model.transfer.TransferData;
import com.googlecode.objectify.Ref;
import org.joda.time.DateTime;
/**
* An EPP flow that approves a pending transfer on a {@link DomainResource}.
* <p>
* The logic in this flow, which handles client approvals, very closely parallels the logic in
* {@link DomainResource#cloneProjectedAtTime} which handles implicit server approvals.
*
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException}
*/
public class DomainTransferApproveFlow extends
ResourceTransferApproveFlow<DomainResource, Builder, Transfer> {
@Override
protected void verifyOwnedResourcePendingTransferMutationAllowed() throws EppException {
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld());
}
@Override
protected final void setTransferApproveProperties(Builder builder) {
TransferData transferData = existingResource.getTransferData();
String gainingClientId = transferData.getGainingClientId();
String tld = existingResource.getTld();
int extraYears = transferData.getExtendedRegistrationYears();
// Bill for the transfer.
BillingEvent.OneTime billingEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.TRANSFER)
.setTargetId(targetId)
.setClientId(gainingClientId)
.setPeriodYears(extraYears)
.setCost(Registry.get(tld).getDomainRenewCost(
targetId,
extraYears,
transferData.getTransferRequestTime()))
.setEventTime(now)
.setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength()))
.setParent(historyEntry)
.build();
ofy().save().entity(billingEvent);
// If we are within an autorenew grace period, cancel the autorenew billing event and reduce
// the number of years to extend the registration by one.
GracePeriod autorenewGrace = Iterables.getOnlyElement(FluentIterable
.from(existingResource.getGracePeriods())
.filter(new Predicate<GracePeriod>(){
@Override
public boolean apply(GracePeriod gracePeriod) {
return GracePeriodStatus.AUTO_RENEW.equals(gracePeriod.getType());
}}), null);
if (autorenewGrace != null) {
extraYears--;
ofy().save().entity(
BillingEvent.Cancellation.forGracePeriod(autorenewGrace, historyEntry, targetId));
}
// Close the old autorenew event and poll message at the transfer time (aka now). This may end
// up deleting the poll message.
updateAutorenewRecurrenceEndTime(existingResource, now);
DateTime newExpirationTime = extendRegistrationWithCap(
now, existingResource.getRegistrationExpirationTime(), extraYears);
// Create a new autorenew event starting at the expiration time.
BillingEvent.Recurring autorenewEvent = new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(targetId)
.setClientId(gainingClientId)
.setEventTime(newExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
ofy().save().entity(autorenewEvent);
// Create a new autorenew poll message.
PollMessage.Autorenew gainingClientAutorenewPollMessage = new PollMessage.Autorenew.Builder()
.setTargetId(targetId)
.setClientId(gainingClientId)
.setEventTime(newExpirationTime)
.setAutorenewEndTime(END_OF_TIME)
.setMsg("Domain was auto-renewed.")
.setParent(historyEntry)
.build();
ofy().save().entity(gainingClientAutorenewPollMessage);
builder
.setRegistrationExpirationTime(newExpirationTime)
.setAutorenewBillingEvent(Ref.create(autorenewEvent))
.setAutorenewPollMessage(Ref.create(gainingClientAutorenewPollMessage))
// Remove all the old grace periods and add a new one for the transfer.
.setGracePeriods(ImmutableSet.of(
GracePeriod.forBillingEvent(GracePeriodStatus.TRANSFER, billingEvent)));
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE;
}
}

View file

@ -0,0 +1,59 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static com.google.domain.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.ResourceTransferCancelFlow;
import com.google.domain.registry.model.domain.DomainCommand.Transfer;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.DomainResource.Builder;
import com.google.domain.registry.model.reporting.HistoryEntry;
/**
* An EPP flow that cancels a pending transfer on a {@link DomainResource}.
*
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException}
* @error {@link com.google.domain.registry.flows.ResourceTransferCancelFlow.NotTransferInitiatorException}
*/
public class DomainTransferCancelFlow
extends ResourceTransferCancelFlow<DomainResource, Builder, Transfer> {
/**
* Reopen the autorenew event and poll message that we closed for the implicit transfer.
* This may end up recreating the autorenew poll message if it was deleted when the transfer
* request was made.
*/
@Override
protected final void modifyRelatedResourcesForTransferCancel() {
updateAutorenewRecurrenceEndTime(existingResource, END_OF_TIME);
}
@Override
protected void verifyTransferCancelMutationAllowed() throws EppException {
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld());
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_TRANSFER_CANCEL;
}
}

View file

@ -0,0 +1,29 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import com.google.domain.registry.flows.ResourceTransferQueryFlow;
import com.google.domain.registry.model.domain.DomainCommand.Transfer;
import com.google.domain.registry.model.domain.DomainResource;
/**
* An EPP flow that queries a pending transfer on a {@link DomainResource}.
*
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link com.google.domain.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceTransferQueryFlow.NoTransferHistoryToQueryException}
* @error {@link com.google.domain.registry.flows.ResourceTransferQueryFlow.NotAuthorizedToViewTransferException}
*/
public class DomainTransferQueryFlow extends ResourceTransferQueryFlow<DomainResource, Transfer> {}

View file

@ -0,0 +1,58 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static com.google.domain.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.ResourceTransferRejectFlow;
import com.google.domain.registry.model.domain.DomainCommand.Transfer;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.DomainResource.Builder;
import com.google.domain.registry.model.reporting.HistoryEntry;
/**
* An EPP flow that rejects a pending transfer on a {@link DomainResource}.
*
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException}
*/
public class DomainTransferRejectFlow
extends ResourceTransferRejectFlow<DomainResource, Builder, Transfer> {
@Override
protected void verifyOwnedResourcePendingTransferMutationAllowed() throws EppException {
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld());
}
/**
* Reopen the autorenew event and poll message that we closed for the implicit transfer.
* This may end up recreating the poll message if it was deleted upon the transfer request.
*/
@Override
protected final void modifyRelatedResourcesForTransferReject() {
updateAutorenewRecurrenceEndTime(existingResource, END_OF_TIME);
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_TRANSFER_REJECT;
}
}

View file

@ -0,0 +1,242 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.validateFeeChallenge;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked;
import static com.google.domain.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears;
import static com.google.domain.registry.model.domain.DomainResource.extendRegistrationWithCap;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.ResourceTransferRequestFlow;
import com.google.domain.registry.model.billing.BillingEvent;
import com.google.domain.registry.model.billing.BillingEvent.Flag;
import com.google.domain.registry.model.billing.BillingEvent.Reason;
import com.google.domain.registry.model.domain.DomainCommand.Transfer;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.Period;
import com.google.domain.registry.model.domain.fee.Fee;
import com.google.domain.registry.model.domain.fee.FeeTransferExtension;
import com.google.domain.registry.model.domain.fee.FeeTransferResponseExtension;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.model.transfer.TransferData;
import com.google.domain.registry.model.transfer.TransferData.TransferServerApproveEntity;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Ref;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import java.util.HashSet;
import java.util.Set;
/**
* An EPP flow that requests a transfer on a {@link DomainResource}.
*
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceTransferRequestFlow.AlreadyPendingTransferException}
* @error {@link com.google.domain.registry.flows.ResourceTransferRequestFlow.MissingTransferRequestAuthInfoException}
* @error {@link com.google.domain.registry.flows.ResourceTransferRequestFlow.ObjectAlreadySponsoredException}
* @error {@link com.google.domain.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException}
* @error {@link DomainFlowUtils.BadPeriodUnitException}
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
* @error {@link DomainFlowUtils.CurrencyValueScaleException}
* @error {@link DomainFlowUtils.FeesMismatchException}
* @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException}
* @error {@link DomainFlowUtils.PremiumNameBlockedException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
*/
public class DomainTransferRequestFlow
extends ResourceTransferRequestFlow<DomainResource, Transfer> {
/** The time when the transfer will be server approved if no other action happens first. */
private DateTime automaticTransferTime;
/** A new one-time billing event for the renewal packaged as part of this transfer. */
private BillingEvent.OneTime transferBillingEvent;
/** A new autorenew billing event starting at the transfer time. */
private BillingEvent.Recurring gainingClientAutorenewEvent;
/** A new autorenew poll message starting at the transfer time. */
private PollMessage.Autorenew gainingClientAutorenewPollMessage;
/** The amount that this transfer will cost due to the implied renew. */
private Money renewCost;
/**
* An optional extension from the client specifying how much they think the transfer should cost.
*/
private FeeTransferExtension feeTransfer;
@Override
protected Duration getAutomaticTransferLength() {
return Registry.get(existingResource.getTld()).getAutomaticTransferLength();
}
@Override
protected final void initResourceTransferRequestFlow() {
registerExtensions(FeeTransferExtension.class);
feeTransfer = eppInput.getSingleExtension(FeeTransferExtension.class);
// The "existingResource" field is loaded before this function is called, but it may be null if
// the domain name specified is invalid or doesn't exist. If that's the case, simply exit
// early, and ResourceMutateFlow will later throw ResourceToMutateDoesNotExistException.
if (existingResource == null) {
return;
}
Registry registry = Registry.get(existingResource.getTld());
automaticTransferTime = now.plus(registry.getAutomaticTransferLength());
renewCost = registry.getDomainRenewCost(targetId, command.getPeriod().getValue(), now);
transferBillingEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.TRANSFER)
.setTargetId(targetId)
.setClientId(getClientId())
.setCost(renewCost)
.setPeriodYears(command.getPeriod().getValue())
.setEventTime(automaticTransferTime)
.setBillingTime(automaticTransferTime.plus(registry.getTransferGracePeriodLength()))
.setParent(historyEntry)
.build();
DateTime newExpirationTime = extendRegistrationWithCap(
automaticTransferTime,
existingResource.getRegistrationExpirationTime(),
command.getPeriod().getValue());
gainingClientAutorenewEvent = new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(targetId)
.setClientId(gainingClient.getId())
.setEventTime(newExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
.build();
gainingClientAutorenewPollMessage = new PollMessage.Autorenew.Builder()
.setTargetId(targetId)
.setClientId(gainingClient.getId())
.setEventTime(newExpirationTime)
.setAutorenewEndTime(END_OF_TIME)
.setMsg("Domain was auto-renewed.")
.setParent(historyEntry)
.build();
}
@Override
protected final void verifyTransferRequestIsAllowed() throws EppException {
verifyUnitIsYears(command.getPeriod());
if (!superuser) {
verifyPremiumNameIsNotBlocked(targetId, existingResource.getTld(), getClientId());
}
validateFeeChallenge(targetId, existingResource.getTld(), feeTransfer, renewCost);
checkAllowedAccessToTld(getAllowedTlds(), existingResource.getTld());
}
@Override
protected ImmutableList<? extends ResponseExtension> getTransferResponseExtensions() {
if (feeTransfer != null) {
return ImmutableList.of(new FeeTransferResponseExtension.Builder()
.setCurrency(renewCost.getCurrencyUnit())
.setFee(ImmutableList.of(Fee.create(renewCost.getAmount(), "renew")))
.build());
} else {
return null;
}
}
@Override
protected void setTransferDataProperties(TransferData.Builder builder) {
builder
.setServerApproveBillingEvent(Ref.create(transferBillingEvent))
.setServerApproveAutorenewEvent(Ref.create(gainingClientAutorenewEvent))
.setServerApproveAutorenewPollMessage(Ref.create(gainingClientAutorenewPollMessage))
.setExtendedRegistrationYears(command.getPeriod().getValue());
}
/**
* When a transfer is requested, schedule a billing event and poll message for the automatic
* approval case.
* <p>
* Note that the action time is AUTOMATIC_TRANSFER_DAYS in the future, matching the server policy
* on automated approval of transfers. There is no equivalent grace period added; if the transfer
* is implicitly approved, the resource will project a grace period on itself.
*/
@Override
protected Set<Key<? extends TransferServerApproveEntity>> getTransferServerApproveEntities() {
ofy().save().<Object>entities(
transferBillingEvent, gainingClientAutorenewEvent, gainingClientAutorenewPollMessage);
// If there will be an autorenew between now and the automatic transfer time, and if the
// autorenew grace period length is long enough that the domain will still be within it at the
// automatic transfer time, then the transfer will subsume the autorenew so we need to write out
// a cancellation for it.
Set<Key<? extends TransferServerApproveEntity>> serverApproveEntities = new HashSet<>();
DateTime expirationTime = existingResource.getRegistrationExpirationTime();
Registry registry = Registry.get(existingResource.getTld());
if (automaticTransferTime.isAfter(expirationTime) && automaticTransferTime.isBefore(
expirationTime.plus(registry.getAutoRenewGracePeriodLength()))) {
BillingEvent.Cancellation autorenewCancellation = new BillingEvent.Cancellation.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(targetId)
.setClientId(existingResource.getCurrentSponsorClientId())
.setEventTime(automaticTransferTime)
.setBillingTime(expirationTime.plus(registry.getAutoRenewGracePeriodLength()))
.setRecurringEventRef(existingResource.getAutorenewBillingEvent())
.setParent(historyEntry)
.build();
ofy().save().entity(autorenewCancellation);
serverApproveEntities.add(Key.create(autorenewCancellation));
}
serverApproveEntities.add(Key.create(transferBillingEvent));
serverApproveEntities.add(Key.create(gainingClientAutorenewEvent));
serverApproveEntities.add(Key.create(gainingClientAutorenewPollMessage));
return serverApproveEntities;
}
/** Close the old autorenew billing event and save a new one. */
@Override
protected final void modifyRelatedResources() throws EppException {
// End the old autorenew event and poll message at the implicit transfer time. This may delete
// the poll message if it has no events left.
//
// Note that this is still left on the domain as the autorenewBillingEvent because it is still
// the current autorenew event until the transfer happens. If you read the domain after the
// transfer occurs, then the logic in cloneProjectedAtTime() will move the
// serverApproveAutoRenewEvent into the autoRenewEvent field.
updateAutorenewRecurrenceEndTime(existingResource, automaticTransferTime);
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST;
}
@Override
protected final Period getCommandPeriod() {
return command.getPeriod();
}
}

View file

@ -0,0 +1,167 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.domain;
import static com.google.common.collect.Sets.symmetricDifference;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.DateTimeUtils.earliestOf;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.domain.registry.dns.DnsQueue;
import com.google.domain.registry.model.billing.BillingEvent;
import com.google.domain.registry.model.billing.BillingEvent.Reason;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.DomainResource.Builder;
import com.google.domain.registry.model.domain.GracePeriod;
import com.google.domain.registry.model.domain.metadata.MetadataExtension;
import com.google.domain.registry.model.domain.rgp.GracePeriodStatus;
import com.google.domain.registry.model.domain.secdns.SecDnsUpdateExtension;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.reporting.HistoryEntry;
import org.joda.time.DateTime;
import java.util.Set;
/**
* An EPP flow that updates a domain resource.
*
* @error {@link com.google.domain.registry.flows.EppException.UnimplementedExtensionException}
* @error {@link com.google.domain.registry.flows.ResourceCreateOrMutateFlow.OnlyToolCanPassMetadataException}
* @error {@link com.google.domain.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceUpdateFlow.AddRemoveSameValueEppException}
* @error {@link com.google.domain.registry.flows.ResourceUpdateFlow.ResourceHasClientUpdateProhibitedException}
* @error {@link com.google.domain.registry.flows.ResourceUpdateFlow.StatusNotClientSettableException}
* @error {@link com.google.domain.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException}
* @error {@link BaseDomainUpdateFlow.EmptySecDnsUpdateException}
* @error {@link BaseDomainUpdateFlow.MaxSigLifeChangeNotSupportedException}
* @error {@link BaseDomainUpdateFlow.SecDnsAllUsageException}
* @error {@link BaseDomainUpdateFlow.UrgentAttributeNotSupportedException}
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
* @error {@link DomainFlowUtils.LinkedResourceDoesNotExistException}
* @error {@link DomainFlowUtils.LinkedResourceInPendingDeleteProhibitsOperationException}
* @error {@link DomainFlowUtils.MissingAdminContactException}
* @error {@link DomainFlowUtils.MissingContactTypeException}
* @error {@link DomainFlowUtils.MissingTechnicalContactException}
* @error {@link DomainFlowUtils.NameserverNotAllowedException}
* @error {@link DomainFlowUtils.RegistrantNotAllowedException}
* @error {@link DomainFlowUtils.TooManyDsRecordsException}
* @error {@link DomainFlowUtils.TooManyNameserversException}
*/
public class DomainUpdateFlow extends BaseDomainUpdateFlow<DomainResource, Builder> {
@Override
protected void initDomainUpdateFlow() {
registerExtensions(SecDnsUpdateExtension.class, MetadataExtension.class);
}
@Override
protected Builder setDomainUpdateProperties(Builder builder) {
// Check if the domain is currently in the sunrush add grace period.
Optional<GracePeriod> sunrushAddGracePeriod = Iterables.tryFind(
existingResource.getGracePeriods(),
new Predicate<GracePeriod>() {
@Override
public boolean apply(GracePeriod gracePeriod) {
return gracePeriod.isSunrushAddGracePeriod();
}});
// If this domain is currently in the sunrush add grace period, and we're updating it in a way
// that will cause it to now get delegated (either by setting nameservers, or by removing a
// clientHold or serverHold), then that will remove the sunrush add grace period and convert
// that to a standard add grace period.
DomainResource updatedDomain = builder.build();
builder = updatedDomain.asBuilder();
if (sunrushAddGracePeriod.isPresent() && updatedDomain.shouldPublishToDns()) {
// Remove the sunrush grace period and write a billing event cancellation for it.
builder.removeGracePeriod(sunrushAddGracePeriod.get());
BillingEvent.Cancellation billingEventCancellation = BillingEvent.Cancellation
.forGracePeriod(sunrushAddGracePeriod.get(), historyEntry, targetId);
// Compute the expiration time of the add grace period. We will not allow it to be after the
// sunrush add grace period expiration time (i.e. you can't get extra add grace period by
// setting a nameserver).
DateTime addGracePeriodExpirationTime = earliestOf(
now.plus(Registry.get(existingResource.getTld()).getAddGracePeriodLength()),
sunrushAddGracePeriod.get().getExpirationTime());
// Create a new billing event for the add grace period. Note that we do this even if it would
// occur at the same time as the sunrush add grace period, as the event time will differ
// between them.
BillingEvent.OneTime originalAddEvent =
sunrushAddGracePeriod.get().getOneTimeBillingEvent().get();
BillingEvent.OneTime billingEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.CREATE)
.setTargetId(targetId)
.setFlags(originalAddEvent.getFlags())
.setClientId(sunrushAddGracePeriod.get().getClientId())
.setCost(originalAddEvent.getCost())
.setPeriodYears(originalAddEvent.getPeriodYears())
.setEventTime(now)
.setBillingTime(addGracePeriodExpirationTime)
.setParent(historyEntry)
.build();
// Set the add grace period on the domain.
builder.addGracePeriod(GracePeriod.forBillingEvent(GracePeriodStatus.ADD, billingEvent));
// Save the billing events.
ofy().save().entities(billingEvent, billingEventCancellation);
}
return builder;
}
@Override
protected final void modifyRelatedResources() {
// Determine the status changes, and filter to server statuses.
// If any of these statuses have been added or removed, bill once.
if (metadataExtension != null && metadataExtension.getRequestedByRegistrar()) {
Set<StatusValue> statusDifferences =
symmetricDifference(existingResource.getStatusValues(), newResource.getStatusValues());
if (Iterables.any(statusDifferences, new Predicate<StatusValue>() {
@Override
public boolean apply(StatusValue statusValue) {
return statusValue.isChargedStatus();
}})) {
BillingEvent.OneTime billingEvent = new BillingEvent.OneTime.Builder()
.setReason(Reason.SERVER_STATUS)
.setTargetId(targetId)
.setClientId(getClientId())
.setCost(Registry.get(existingResource.getTld()).getServerStatusChangeCost())
.setEventTime(now)
.setBillingTime(now)
.setParent(historyEntry)
.build();
ofy().save().entity(billingEvent);
}
}
}
@Override
protected void enqueueTasks() {
DnsQueue.create().addDomainRefreshTask(existingResource.getFullyQualifiedDomainName());
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.DOMAIN_UPDATE;
}
}

View file

@ -0,0 +1,45 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.host;
import static com.google.domain.registry.model.EppResourceUtils.checkResourcesExist;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.flows.ResourceCheckFlow;
import com.google.domain.registry.model.eppoutput.CheckData;
import com.google.domain.registry.model.eppoutput.CheckData.HostCheck;
import com.google.domain.registry.model.eppoutput.CheckData.HostCheckData;
import com.google.domain.registry.model.host.HostCommand.Check;
import com.google.domain.registry.model.host.HostResource;
import java.util.Set;
/**
* An EPP flow that checks whether a host can be provisioned.
*
* @error {@link com.google.domain.registry.flows.ResourceCheckFlow.TooManyResourceChecksException}
*/
public class HostCheckFlow extends ResourceCheckFlow<HostResource, Check> {
@Override
protected CheckData getCheckData() {
Set<String> existingIds = checkResourcesExist(resourceClass, targetIds, now);
ImmutableList.Builder<HostCheck> checks = new ImmutableList.Builder<>();
for (String id : targetIds) {
boolean unused = !existingIds.contains(id);
checks.add(HostCheck.create(unused, id, unused ? null : "In use"));
}
return HostCheckData.create(checks.build());
}
}

View file

@ -0,0 +1,143 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.host;
import static com.google.domain.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain;
import static com.google.domain.registry.flows.host.HostFlowUtils.validateHostName;
import static com.google.domain.registry.flows.host.HostFlowUtils.verifyDomainIsSameRegistrar;
import static com.google.domain.registry.model.EppResourceUtils.createContactHostRoid;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.CollectionUtils.isNullOrEmpty;
import com.google.common.base.Optional;
import com.google.domain.registry.dns.DnsQueue;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.ParameterValueRangeErrorException;
import com.google.domain.registry.flows.EppException.RequiredParameterMissingException;
import com.google.domain.registry.flows.ResourceCreateFlow;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.eppoutput.CreateData.HostCreateData;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.host.HostCommand.Create;
import com.google.domain.registry.model.host.HostResource;
import com.google.domain.registry.model.host.HostResource.Builder;
import com.google.domain.registry.model.ofy.ObjectifyService;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.googlecode.objectify.Ref;
/**
* An EPP flow that creates a new host resource.
*
* @error {@link com.google.domain.registry.flows.EppXmlTransformer.IpAddressVersionMismatchException}
* @error {@link com.google.domain.registry.flows.ResourceCreateFlow.ResourceAlreadyExistsException}
* @error {@link HostFlowUtils.HostNameTooLongException}
* @error {@link HostFlowUtils.HostNameTooShallowException}
* @error {@link HostFlowUtils.InvalidHostNameException}
* @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException}
* @error {@link SubordinateHostMustHaveIpException}
* @error {@link UnexpectedExternalHostIpException}
*/
public class HostCreateFlow extends ResourceCreateFlow<HostResource, Builder, Create> {
/**
* The superordinate domain of the host object if creating an in-bailiwick host, or null if
* creating an external host. This is looked up before we actually create the Host object so that
* we can detect error conditions earlier. By the time {@link #setCreateProperties} is called
* (where this reference is actually used), we no longer have the ability to return an
* {@link EppException}.
*
* <p>The general model of these classes is to do validation of parameters up front before we get
* to the actual object creation, which is why this class looks up and stores the superordinate
* domain ahead of time.
*/
private Optional<Ref<DomainResource>> superordinateDomain;
@Override
protected void initResourceCreateOrMutateFlow() throws EppException {
superordinateDomain = Optional.fromNullable(lookupSuperordinateDomain(
validateHostName(command.getFullyQualifiedHostName()), now));
}
@Override
protected String createFlowRepoId() {
return createContactHostRoid(ObjectifyService.allocateId());
}
@Override
protected void verifyCreateIsAllowed() throws EppException {
verifyDomainIsSameRegistrar(superordinateDomain.orNull(), getClientId());
boolean willBeSubordinate = superordinateDomain.isPresent();
boolean hasIpAddresses = !isNullOrEmpty(command.getInetAddresses());
if (willBeSubordinate != hasIpAddresses) {
// Subordinate hosts must have ip addresses and external hosts must not have them.
throw willBeSubordinate
? new SubordinateHostMustHaveIpException()
: new UnexpectedExternalHostIpException();
}
}
@Override
protected void setCreateProperties(Builder builder) {
if (superordinateDomain.isPresent()) {
builder.setSuperordinateDomain(superordinateDomain.get());
}
}
/** Modify any other resources that need to be informed of this create. */
@Override
protected void modifyCreateRelatedResources() {
if (superordinateDomain.isPresent()) {
ofy().save().entity(superordinateDomain.get().get().asBuilder()
.addSubordinateHost(command.getFullyQualifiedHostName())
.build());
}
}
@Override
protected void enqueueTasks() {
// Only update DNS if this is a subordinate host. External hosts have no glue to write, so they
// are only written as NS records from the referencing domain.
if (superordinateDomain.isPresent()) {
DnsQueue.create().addHostRefreshTask(newResource.getFullyQualifiedHostName());
}
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.HOST_CREATE;
}
@Override
protected EppOutput getOutput() {
return createOutput(Success,
HostCreateData.create(newResource.getFullyQualifiedHostName(), now));
}
/** Subordinate hosts must have an ip address. */
static class SubordinateHostMustHaveIpException extends RequiredParameterMissingException {
public SubordinateHostMustHaveIpException() {
super("Subordinate hosts must have an ip address");
}
}
/** External hosts must not have ip addresses. */
static class UnexpectedExternalHostIpException extends ParameterValueRangeErrorException {
public UnexpectedExternalHostIpException() {
super("External hosts must not have ip addresses");
}
}
}

View file

@ -0,0 +1,87 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.host;
import static com.google.domain.registry.model.EppResourceUtils.queryDomainsUsingResource;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.ResourceAsyncDeleteFlow;
import com.google.domain.registry.flows.async.AsyncFlowUtils;
import com.google.domain.registry.flows.async.DeleteEppResourceAction;
import com.google.domain.registry.flows.async.DeleteHostResourceAction;
import com.google.domain.registry.model.domain.DomainBase;
import com.google.domain.registry.model.domain.ReferenceUnion;
import com.google.domain.registry.model.host.HostCommand.Delete;
import com.google.domain.registry.model.host.HostResource;
import com.google.domain.registry.model.host.HostResource.Builder;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.googlecode.objectify.Key;
/**
* An EPP flow that deletes a host resource.
*
* @error {@link com.google.domain.registry.flows.ResourceAsyncDeleteFlow.ResourceToDeleteIsReferencedException}
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException}
*/
public class HostDeleteFlow extends ResourceAsyncDeleteFlow<HostResource, Builder, Delete> {
/** In {@link #isLinkedForFailfast}, check this (arbitrary) number of resources from the query. */
private static final int FAILFAST_CHECK_COUNT = 5;
@Override
protected boolean isLinkedForFailfast(final ReferenceUnion<HostResource> ref) {
// Query for the first few linked domains, and if found, actually load them. The query is
// eventually consistent and so might be very stale, but the direct load will not be stale,
// just non-transactional. If we find at least one actual reference then we can reliably
// fail. If we don't find any, we can't trust the query and need to do the full mapreduce.
return Iterables.any(
ofy().load().keys(
queryDomainsUsingResource(
HostResource.class, ref.getLinked(), now, FAILFAST_CHECK_COUNT)).values(),
new Predicate<DomainBase>() {
@Override
public boolean apply(DomainBase domain) {
return domain.getNameservers().contains(ref);
}});
}
/** Enqueues a host resource deletion on the mapreduce queue. */
@Override
protected final void enqueueTasks() throws EppException {
AsyncFlowUtils.enqueueMapreduceAction(
DeleteHostResourceAction.class,
ImmutableMap.of(
DeleteEppResourceAction.PARAM_RESOURCE_KEY,
Key.create(existingResource).getString(),
DeleteEppResourceAction.PARAM_REQUESTING_CLIENT_ID,
getClientId(),
DeleteEppResourceAction.PARAM_IS_SUPERUSER,
Boolean.toString(superuser)),
RegistryEnvironment.get().config().getAsyncDeleteFlowMapreduceDelay());
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.HOST_PENDING_DELETE;
}
}

View file

@ -0,0 +1,143 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.host;
import static com.google.domain.registry.model.EppResourceUtils.isActive;
import static com.google.domain.registry.model.EppResourceUtils.loadByUniqueId;
import static com.google.domain.registry.model.registry.Registries.findTldForName;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.Iterables;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.AuthorizationErrorException;
import com.google.domain.registry.flows.EppException.ObjectDoesNotExistException;
import com.google.domain.registry.flows.EppException.ParameterValuePolicyErrorException;
import com.google.domain.registry.flows.EppException.ParameterValueRangeErrorException;
import com.google.domain.registry.flows.EppException.ParameterValueSyntaxErrorException;
import com.google.domain.registry.model.domain.DomainResource;
import com.googlecode.objectify.Ref;
import org.joda.time.DateTime;
/** Static utility functions for host flows. */
public class HostFlowUtils {
/** Checks that a host name is valid. */
static InternetDomainName validateHostName(String name) throws EppException {
if (name == null) {
return null;
}
if (name.length() > 253) {
throw new HostNameTooLongException();
}
try {
InternetDomainName hostName = InternetDomainName.from(name);
// Checks whether a hostname is deep enough. Technically a host can be just one under a
// public suffix (e.g. example.com) but we require by policy that it has to be at least one
// part beyond that (e.g. ns1.example.com). The public suffix list includes all current
// ccTlds, so this check requires 4+ parts if it's a ccTld that doesn't delegate second
// level domains, such as .co.uk. But the list does not include new tlds, so in that case
// we just ensure 3+ parts. In the particular case where our own tld has a '.' in it, we know
// that there need to be 4 parts as well.
if (hostName.isUnderPublicSuffix()) {
if (hostName.parent().isUnderPublicSuffix()) {
return hostName;
}
} else {
// We need to know how many parts the hostname has beyond the public suffix, but we don't
// know what the public suffix is. If the host is in bailiwick and we are hosting a
// multipart "tld" like .co.uk the publix suffix might be 2 parts. Otherwise it's an
// unrecognized tld that's not on the public suffix list, so assume the tld alone is the
// public suffix.
Optional<InternetDomainName> tldParsed = findTldForName(hostName);
int suffixSize = tldParsed.isPresent() ? tldParsed.get().parts().size() : 1;
if (hostName.parts().size() >= suffixSize + 2) {
return hostName;
}
}
throw new HostNameTooShallowException();
} catch (IllegalArgumentException e) {
throw new InvalidHostNameException();
}
}
/** Return the {@link DomainResource} this host is subordinate to, or null for external hosts. */
static Ref<DomainResource> lookupSuperordinateDomain(
InternetDomainName hostName, DateTime now) throws EppException {
Optional<InternetDomainName> tldParsed = findTldForName(hostName);
if (!tldParsed.isPresent()) {
// This is an host on a TLD we don't run, therefore obviously external, so we are done.
return null;
}
// This is a subordinate host
@SuppressWarnings("deprecation")
String domainName = Joiner.on('.').join(Iterables.skip(
hostName.parts(), hostName.parts().size() - (tldParsed.get().parts().size() + 1)));
DomainResource superordinateDomain = loadByUniqueId(DomainResource.class, domainName, now);
if (superordinateDomain == null || !isActive(superordinateDomain, now)) {
throw new SuperordinateDomainDoesNotExistException(domainName);
}
return Ref.create(superordinateDomain);
}
/** Superordinate domain for this hostname does not exist. */
static class SuperordinateDomainDoesNotExistException extends ObjectDoesNotExistException {
public SuperordinateDomainDoesNotExistException(String domainName) {
super(DomainResource.class, domainName);
}
}
/** Ensure that the superordinate domain is sponsored by the provided clientId. */
static void verifyDomainIsSameRegistrar(
Ref<DomainResource> superordinateDomain,
String clientId) throws EppException {
if (superordinateDomain != null
&& !clientId.equals(superordinateDomain.get().getCurrentSponsorClientId())) {
throw new HostDomainNotOwnedException();
}
}
/** Domain for host is sponsored by another registrar. */
static class HostDomainNotOwnedException extends AuthorizationErrorException {
public HostDomainNotOwnedException() {
super("Domain for host is sponsored by another registrar");
}
}
/** Host names are limited to 253 characters. */
static class HostNameTooLongException extends ParameterValueRangeErrorException {
public HostNameTooLongException() {
super("Host names are limited to 253 characters");
}
}
/** Host names must be at least two levels below the public suffix. */
static class HostNameTooShallowException extends ParameterValuePolicyErrorException {
public HostNameTooShallowException() {
super("Host names must be at least two levels below the public suffix");
}
}
/** Invalid host name. */
static class InvalidHostNameException extends ParameterValueSyntaxErrorException {
public InvalidHostNameException() {
super("Invalid host name");
}
}
}

View file

@ -0,0 +1,26 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.host;
import com.google.domain.registry.flows.ResourceInfoFlow;
import com.google.domain.registry.model.host.HostCommand;
import com.google.domain.registry.model.host.HostResource;
/**
* An EPP flow that reads a host.
*
* @error {@link com.google.domain.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException}
*/
public class HostInfoFlow extends ResourceInfoFlow<HostResource, HostCommand.Info> {}

View file

@ -0,0 +1,235 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.host;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.domain.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain;
import static com.google.domain.registry.flows.host.HostFlowUtils.validateHostName;
import static com.google.domain.registry.flows.host.HostFlowUtils.verifyDomainIsSameRegistrar;
import static com.google.domain.registry.model.index.ForeignKeyIndex.loadAndGetReference;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.CollectionUtils.isNullOrEmpty;
import com.google.common.collect.ImmutableMap;
import com.google.domain.registry.dns.DnsQueue;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.ObjectAlreadyExistsException;
import com.google.domain.registry.flows.EppException.ParameterValueRangeErrorException;
import com.google.domain.registry.flows.EppException.RequiredParameterMissingException;
import com.google.domain.registry.flows.EppException.StatusProhibitsOperationException;
import com.google.domain.registry.flows.ResourceUpdateFlow;
import com.google.domain.registry.flows.async.AsyncFlowUtils;
import com.google.domain.registry.flows.async.DnsRefreshForHostRenameAction;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.host.HostCommand.Update;
import com.google.domain.registry.model.host.HostResource;
import com.google.domain.registry.model.host.HostResource.Builder;
import com.google.domain.registry.model.index.ForeignKeyIndex;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Ref;
import org.joda.time.Duration;
import java.util.Objects;
/**
* An EPP flow that updates a host resource.
*
* @error {@link com.google.domain.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link com.google.domain.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException}
* @error {@link com.google.domain.registry.flows.ResourceUpdateFlow.ResourceHasClientUpdateProhibitedException}
* @error {@link com.google.domain.registry.flows.ResourceUpdateFlow.StatusNotClientSettableException}
* @error {@link com.google.domain.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException}
* @error {@link HostFlowUtils.HostNameTooShallowException}
* @error {@link HostFlowUtils.InvalidHostNameException}
* @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException}
* @error {@link CannotAddIpToExternalHostException}
* @error {@link CannotRemoveSubordinateHostLastIpException}
* @error {@link HostAlreadyExistsException}
* @error {@link RenameHostToExternalRemoveIpException}
* @error {@link RenameHostToSubordinateRequiresIpException}
*/
public class HostUpdateFlow extends ResourceUpdateFlow<HostResource, Builder, Update> {
private Ref<DomainResource> superordinateDomain;
private String oldHostName;
private String newHostName;
private boolean isHostRename;
@Override
protected void initResourceCreateOrMutateFlow() throws EppException {
String suppliedNewHostName = command.getInnerChange().getFullyQualifiedHostName();
isHostRename = suppliedNewHostName != null;
oldHostName = targetId;
newHostName = firstNonNull(suppliedNewHostName, oldHostName);
superordinateDomain =
lookupSuperordinateDomain(validateHostName(newHostName), now);
}
@Override
protected void verifyUpdateIsAllowed() throws EppException {
verifyDomainIsSameRegistrar(superordinateDomain, getClientId());
if (isHostRename
&& loadAndGetReference(HostResource.class, newHostName, now) != null) {
throw new HostAlreadyExistsException(newHostName);
}
}
@Override
protected void verifyNewUpdatedStateIsAllowed() throws EppException {
boolean wasExternal = existingResource.getSuperordinateDomain() == null;
boolean wasSubordinate = !wasExternal;
boolean willBeExternal = superordinateDomain == null;
boolean willBeSubordinate = !willBeExternal;
boolean newResourceHasIps = !isNullOrEmpty(newResource.getInetAddresses());
boolean commandAddsIps = !isNullOrEmpty(command.getInnerAdd().getInetAddresses());
// These checks are order-dependent. For example a subordinate-to-external rename that adds new
// ips should hit the first exception, whereas one that only fails to remove the existing ips
// should hit the second.
if (willBeExternal && commandAddsIps) {
throw new CannotAddIpToExternalHostException();
}
if (wasSubordinate && willBeExternal && newResourceHasIps) {
throw new RenameHostToExternalRemoveIpException();
}
if (wasExternal && willBeSubordinate && !commandAddsIps) {
throw new RenameHostToSubordinateRequiresIpException();
}
if (willBeSubordinate && !newResourceHasIps) {
throw new CannotRemoveSubordinateHostLastIpException();
}
}
@Override
protected Builder setUpdateProperties(Builder builder) {
// The superordinateDomain can be null if the new name is external.
// Note that the value of superordinateDomain is projected to the current time inside of
// the lookupSuperordinateDomain(...) call above, so that it will never be stale.
builder.setSuperordinateDomain(superordinateDomain);
builder.setLastSuperordinateChange(superordinateDomain == null ? null : now);
// Rely on the host's cloneProjectedAtTime() method to handle setting of transfer data.
return builder.build().cloneProjectedAtTime(now).asBuilder();
}
/** Keep the {@link ForeignKeyIndex} for this host up to date. */
@Override
protected void modifyRelatedResources() {
if (isHostRename) {
// Update the foreign key for the old host name.
ofy().save().entity(ForeignKeyIndex.create(existingResource, now));
// Save the foreign key for the new host name.
ofy().save().entity(ForeignKeyIndex.create(newResource, newResource.getDeletionTime()));
updateSuperordinateDomains();
}
}
@Override
public void enqueueTasks() throws EppException {
DnsQueue dnsQueue = DnsQueue.create();
// Only update DNS for subordinate hosts. External hosts have no glue to write, so they
// are only written as NS records from the referencing domain.
if (existingResource.getSuperordinateDomain() != null) {
dnsQueue.addHostRefreshTask(oldHostName);
}
// In case of a rename, there are many updates we need to queue up.
if (isHostRename) {
// If the renamed host is also subordinate, then we must enqueue an update to write the new
// glue.
if (newResource.getSuperordinateDomain() != null) {
dnsQueue.addHostRefreshTask(newHostName);
}
// We must also enqueue updates for all domains that use this host as their nameserver so
// that their NS records can be updated to point at the new name.
AsyncFlowUtils.enqueueMapreduceAction(
DnsRefreshForHostRenameAction.class,
ImmutableMap.of(
DnsRefreshForHostRenameAction.PARAM_HOST_KEY,
Key.create(existingResource).getString()),
Duration.ZERO);
}
}
@Override
protected final HistoryEntry.Type getHistoryEntryType() {
return HistoryEntry.Type.HOST_UPDATE;
}
private void updateSuperordinateDomains() {
Ref<DomainResource> oldSuperordinateDomain = existingResource.getSuperordinateDomain();
if (oldSuperordinateDomain != null || superordinateDomain != null) {
if (Objects.equals(oldSuperordinateDomain, superordinateDomain)) {
ofy().save().entity(oldSuperordinateDomain.get().asBuilder()
.removeSubordinateHost(oldHostName)
.addSubordinateHost(newHostName)
.build());
} else {
if (oldSuperordinateDomain != null) {
ofy().save().entity(
oldSuperordinateDomain.get()
.asBuilder()
.removeSubordinateHost(oldHostName)
.build());
}
if (superordinateDomain != null) {
ofy().save().entity(
superordinateDomain.get()
.asBuilder()
.addSubordinateHost(newHostName)
.build());
}
}
}
}
/** Host with specified name already exists. */
static class HostAlreadyExistsException extends ObjectAlreadyExistsException {
public HostAlreadyExistsException(String hostName) {
super(String.format("Object with given ID (%s) already exists", hostName));
}
}
/** Cannot add ip addresses to an external host. */
static class CannotAddIpToExternalHostException extends ParameterValueRangeErrorException {
public CannotAddIpToExternalHostException() {
super("Cannot add ip addresses to external hosts");
}
}
/** Cannot remove all ip addresses from a subordinate host. */
static class CannotRemoveSubordinateHostLastIpException
extends StatusProhibitsOperationException {
public CannotRemoveSubordinateHostLastIpException() {
super("Cannot remove all ip addresses from a subordinate host");
}
}
/** Host rename from external to subordinate must also add an ip addresses. */
static class RenameHostToSubordinateRequiresIpException
extends RequiredParameterMissingException {
public RenameHostToSubordinateRequiresIpException() {
super("Host rename from external to subordinate must also add an ip address");
}
}
/** Host rename from subordinate to external must also remove all ip addresses. */
static class RenameHostToExternalRemoveIpException extends ParameterValueRangeErrorException {
public RenameHostToExternalRemoveIpException() {
super("Host rename from subordinate to external must also remove all ip addresses");
}
}
}

View file

@ -0,0 +1,148 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.poll;
import static com.google.common.base.Preconditions.checkState;
import static com.google.domain.registry.model.eppoutput.Result.Code.Success;
import static com.google.domain.registry.model.eppoutput.Result.Code.SuccessWithNoMessages;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.DateTimeUtils.isBeforeOrAt;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.AuthorizationErrorException;
import com.google.domain.registry.flows.EppException.ObjectDoesNotExistException;
import com.google.domain.registry.flows.EppException.ParameterValueSyntaxErrorException;
import com.google.domain.registry.flows.EppException.RequiredParameterMissingException;
import com.google.domain.registry.flows.TransactionalFlow;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.poll.MessageQueueInfo;
import com.google.domain.registry.model.poll.PollMessage;
import com.google.domain.registry.model.poll.PollMessageExternalKeyConverter.PollMessageExternalKeyParseException;
import com.googlecode.objectify.Key;
import org.joda.time.DateTime;
/**
* An EPP flow for acknowledging poll messages.
*
* @error {@link PollAckFlow.InvalidMessageIdException}
* @error {@link PollAckFlow.MessageDoesNotExistException}
* @error {@link PollAckFlow.MissingMessageIdException}
* @error {@link PollAckFlow.NotAuthorizedToAckMessageException}
*/
public class PollAckFlow extends PollFlow implements TransactionalFlow {
@Override
public final EppOutput run() throws EppException {
if (command.getMessageId() == null) {
throw new MissingMessageIdException();
}
Key<PollMessage> pollMessageKey;
// Try parsing the messageId, and throw an exception if it's invalid.
try {
pollMessageKey = PollMessage.EXTERNAL_KEY_CONVERTER.reverse().convert(command.getMessageId());
} catch (PollMessageExternalKeyParseException e) {
throw new InvalidMessageIdException(command.getMessageId());
}
// Load the message to be acked. If a message is queued to be delivered in the future, we treat
// it as if it doesn't exist yet.
PollMessage pollMessage = ofy().load().key(pollMessageKey).now();
if (pollMessage == null || !isBeforeOrAt(pollMessage.getEventTime(), now)) {
throw new MessageDoesNotExistException(command.getMessageId());
}
// Make sure this client is authorized to ack this message. It could be that the message is
// supposed to go to a different registrar.
if (!getClientId().equals(pollMessage.getClientId())) {
throw new NotAuthorizedToAckMessageException();
}
// This keeps track of whether we should include the current acked message in the updated
// message count that's returned to the user. The only case where we do so is if an autorenew
// poll message is acked, but its next event is already ready to be delivered.
boolean includeAckedMessageInCount = false;
if (pollMessage instanceof PollMessage.OneTime) {
// One-time poll messages are deleted once acked.
ofy().delete().entity(pollMessage);
} else {
checkState(pollMessage instanceof PollMessage.Autorenew, "Unknown poll message type");
PollMessage.Autorenew autorenewPollMessage = (PollMessage.Autorenew) pollMessage;
// Move the eventTime of this autorenew poll message forward by a year.
DateTime nextEventTime = autorenewPollMessage.getEventTime().plusYears(1);
// If the next event falls within the bounds of the end time, then just update the eventTime
// and re-save it for future autorenew poll messages to be delivered. Otherwise, this
// autorenew poll message has no more events to deliver and should be deleted.
if (nextEventTime.isBefore(autorenewPollMessage.getAutorenewEndTime())) {
ofy().save().entity(autorenewPollMessage.asBuilder().setEventTime(nextEventTime).build());
includeAckedMessageInCount = isBeforeOrAt(nextEventTime, now);
} else {
ofy().delete().entity(autorenewPollMessage);
}
}
// We need to return the new queue length. If this was the last message in the queue being
// acked, then we return a special status code indicating that. Note that the query will
// include the message being acked.
int messageCount = getMessageQueueLength();
if (!includeAckedMessageInCount) {
messageCount--;
}
if (messageCount <= 0) {
return createOutput(SuccessWithNoMessages);
}
return createOutput(
Success,
MessageQueueInfo.create(
null, // eventTime
null, // msg
messageCount,
command.getMessageId()),
null, // responseData
null); // extensions
}
/** Registrar is not authorized to ack this message. */
static class NotAuthorizedToAckMessageException extends AuthorizationErrorException {
public NotAuthorizedToAckMessageException() {
super("Registrar is not authorized to ack this message");
}
}
/** Message with this id does not exist. */
public static class MessageDoesNotExistException extends ObjectDoesNotExistException {
public MessageDoesNotExistException(String messageIdString) {
super(PollMessage.class, messageIdString);
}
}
/** Message id is invalid. */
static class InvalidMessageIdException extends ParameterValueSyntaxErrorException {
public InvalidMessageIdException(String messageIdStr) {
super(String.format("Message id \"%s\" is invalid", messageIdStr));
}
}
/** Message id is required. */
static class MissingMessageIdException extends RequiredParameterMissingException {
public MissingMessageIdException() {
super("Message id is required");
}
}
}

View file

@ -0,0 +1,90 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.poll;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.common.collect.ImmutableList;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.LoggedInFlow;
import com.google.domain.registry.model.eppinput.EppInput.Poll;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.eppoutput.Response;
import com.google.domain.registry.model.eppoutput.Response.ResponseData;
import com.google.domain.registry.model.eppoutput.Response.ResponseExtension;
import com.google.domain.registry.model.eppoutput.Result;
import com.google.domain.registry.model.poll.MessageQueueInfo;
import com.google.domain.registry.model.poll.PollMessage;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Work;
import com.googlecode.objectify.cmd.Query;
import java.util.List;
/** Base class of EPP Poll command flows. Mostly provides datastore helper methods. */
public abstract class PollFlow extends LoggedInFlow {
protected Poll command;
@Override
@SuppressWarnings("unchecked")
protected final void initLoggedInFlow() throws EppException {
command = (Poll) eppInput.getCommandWrapper().getCommand();
}
/**
* Returns a query for all poll messages for the logged in registrar in the current TLD which are
* not in the future.
*/
private Query<PollMessage> getQuery() {
return ofy().doTransactionless(new Work<Query<PollMessage>>() {
@Override
public Query<PollMessage> run() {
return ofy().load()
.type(PollMessage.class)
.filter("clientId", getClientId())
.filter("eventTime <=", now.toDate());
}});
}
/** Return the length of the message queue for the logged in registrar. */
protected int getMessageQueueLength() {
return getQuery().keys().list().size();
}
/**
* Retrieves the Keys of all active PollMessage entities for the current client ordered by
* eventTime.
*/
protected List<Key<PollMessage>> getMessageQueueKeysInOrder() {
return getQuery().order("eventTime").keys().list();
}
protected EppOutput createOutput(
Result.Code code,
MessageQueueInfo messageQueueInfo,
ImmutableList<ResponseData> responseData,
ImmutableList<ResponseExtension> responseExtensions) {
return EppOutput.create(new Response.Builder()
.setTrid(trid)
.setResult(Result.create(code))
.setMessageQueueInfo(messageQueueInfo)
.setResData(responseData)
.setExtensions(responseExtensions)
.setExecutionTime(now)
.build());
}
}

View file

@ -0,0 +1,71 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.poll;
import static com.google.domain.registry.model.eppoutput.Result.Code.SuccessWithAckMessage;
import static com.google.domain.registry.model.eppoutput.Result.Code.SuccessWithNoMessages;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.CollectionUtils.forceEmptyToNull;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.ParameterValueSyntaxErrorException;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.poll.MessageQueueInfo;
import com.google.domain.registry.model.poll.PollMessage;
import com.googlecode.objectify.Key;
import java.util.List;
/**
* An EPP flow for requesting poll messages.
*
* @error {@link PollRequestFlow.UnexpectedMessageIdException}
*/
public class PollRequestFlow extends PollFlow {
@Override
public final EppOutput run() throws EppException {
if (command.getMessageId() != null) {
throw new UnexpectedMessageIdException();
}
List<Key<PollMessage>> pollMessageKeys = getMessageQueueKeysInOrder();
// Retrieve the oldest message from the queue that still exists -- since the query is eventually
// consistent, it may return keys to some entities that no longer exist.
for (Key<PollMessage> key : pollMessageKeys) {
PollMessage pollMessage = ofy().load().key(key).now();
if (pollMessage != null) {
return createOutput(
SuccessWithAckMessage,
MessageQueueInfo.create(
pollMessage.getEventTime(),
pollMessage.getMsg(),
pollMessageKeys.size(),
PollMessage.EXTERNAL_KEY_CONVERTER.convert(key)),
forceEmptyToNull(pollMessage.getResponseData()),
forceEmptyToNull(pollMessage.getResponseExtensions()));
}
}
return createOutput(SuccessWithNoMessages);
}
/** Unexpected message id. */
static class UnexpectedMessageIdException extends ParameterValueSyntaxErrorException {
public UnexpectedMessageIdException() {
super("Unexpected message id");
}
}
}

View file

@ -0,0 +1,27 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.session;
import com.google.domain.registry.flows.Flow;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.eppoutput.Greeting;
/** A flow for an Epp "hello". */
public class HelloFlow extends Flow {
@Override
public EppOutput run() {
return EppOutput.create(new Greeting());
}
}

View file

@ -0,0 +1,200 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.session;
import static com.google.common.collect.Sets.difference;
import static com.google.domain.registry.util.CollectionUtils.nullToEmpty;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppException.AuthenticationErrorClosingConnectionException;
import com.google.domain.registry.flows.EppException.AuthenticationErrorException;
import com.google.domain.registry.flows.EppException.AuthorizationErrorException;
import com.google.domain.registry.flows.EppException.CommandUseErrorException;
import com.google.domain.registry.flows.EppException.ParameterValuePolicyErrorException;
import com.google.domain.registry.flows.EppException.UnimplementedExtensionException;
import com.google.domain.registry.flows.EppException.UnimplementedObjectServiceException;
import com.google.domain.registry.flows.EppException.UnimplementedOptionException;
import com.google.domain.registry.flows.Flow;
import com.google.domain.registry.flows.TransportCredentials;
import com.google.domain.registry.model.eppcommon.ProtocolDefinition;
import com.google.domain.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
import com.google.domain.registry.model.eppinput.EppInput.Login;
import com.google.domain.registry.model.eppinput.EppInput.Options;
import com.google.domain.registry.model.eppinput.EppInput.Services;
import com.google.domain.registry.model.eppoutput.EppOutput;
import com.google.domain.registry.model.eppoutput.Result.Code;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.util.FormattingLogger;
import java.util.Objects;
import java.util.Set;
/**
* An EPP flow for login.
*
* @error {@link com.google.domain.registry.flows.EppConsoleServlet.GaeUserCredentials.BadGaeUserIdException}
* @error {@link com.google.domain.registry.flows.EppConsoleServlet.GaeUserCredentials.UserNotLoggedInException}
* @error {@link com.google.domain.registry.flows.EppException.UnimplementedExtensionException}
* @error {@link com.google.domain.registry.flows.EppException.UnimplementedObjectServiceException}
* @error {@link com.google.domain.registry.flows.EppException.UnimplementedProtocolVersionException}
* @error {@link com.google.domain.registry.flows.TlsCredentials.BadRegistrarCertificateException}
* @error {@link com.google.domain.registry.flows.TlsCredentials.BadRegistrarIpAddressException}
* @error {@link com.google.domain.registry.flows.TlsCredentials.MissingRegistrarCertificateException}
* @error {@link com.google.domain.registry.flows.TlsCredentials.NoSniException}
* @error {@link LoginFlow.AlreadyLoggedInException}
* @error {@link LoginFlow.BadRegistrarClientIdException}
* @error {@link LoginFlow.BadRegistrarPasswordException}
* @error {@link LoginFlow.TooManyFailedLoginsException}
* @error {@link LoginFlow.PasswordChangesNotSupportedException}
* @error {@link LoginFlow.RegistrarAccountNotActiveException}
* @error {@link LoginFlow.UnsupportedLanguageException}
*/
public class LoginFlow extends Flow {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
/** This is the IANA ID used for the internal account of the registry. */
private static final long INTERNAL_IANA_REGISTRAR_ID = 9999L;
/** Maximum number of failed login attempts allowed per connection. */
private static final int MAX_FAILED_LOGIN_ATTEMPTS_PER_CONNECTION = 3;
/** Run the flow and log errors. */
@Override
public final EppOutput run() throws EppException {
try {
return runWithoutLogging();
} catch (EppException e) {
logger.warning("Login failed: " + e.getMessage());
throw e;
}
}
/** Run the flow without bothering to log errors. The {@link #run} method will do that for us. */
public final EppOutput runWithoutLogging() throws EppException {
Login login = (Login) eppInput.getCommandWrapper().getCommand();
if (getClientId() != null) {
throw new AlreadyLoggedInException();
}
Options options = login.getOptions();
if (!ProtocolDefinition.LANGUAGE.equals(options.getLanguage())) {
throw new UnsupportedLanguageException();
}
Services services = login.getServices();
Set<String> unsupportedObjectServices = difference(
nullToEmpty(services.getObjectServices()),
ProtocolDefinition.SUPPORTED_OBJECT_SERVICES);
if (!unsupportedObjectServices.isEmpty()) {
throw new UnimplementedObjectServiceException();
}
ImmutableSet.Builder<String> serviceExtensionUrisBuilder = new ImmutableSet.Builder<>();
for (String uri : nullToEmpty(services.getServiceExtensions())) {
ServiceExtension serviceExtension = ProtocolDefinition.getServiceExtensionFromUri(uri);
if (serviceExtension == null) {
throw new UnimplementedExtensionException();
}
serviceExtensionUrisBuilder.add(uri);
}
Registrar registrar = Registrar.loadByClientId(login.getClientId());
if (registrar == null) {
throw new BadRegistrarClientIdException(login.getClientId());
}
TransportCredentials credentials = sessionMetadata.getTransportCredentials();
// AuthenticationErrorExceptions will propagate up through here.
if (credentials != null) { // Allow no-credential logins, for load-testing and RDE.
try {
credentials.validate(registrar);
} catch (AuthenticationErrorException e) {
sessionMetadata.incrementFailedLoginAttempts();
throw e;
}
}
final boolean requiresLoginCheck = credentials == null || !credentials.performsLoginCheck();
if (requiresLoginCheck && !registrar.testPassword(login.getPassword())) {
sessionMetadata.incrementFailedLoginAttempts();
if (sessionMetadata.getFailedLoginAttempts() > MAX_FAILED_LOGIN_ATTEMPTS_PER_CONNECTION) {
throw new TooManyFailedLoginsException();
} else {
throw new BadRegistrarPasswordException();
}
}
if (registrar.getState().equals(Registrar.State.PENDING)) {
throw new RegistrarAccountNotActiveException();
}
if (login.getNewPassword() != null) { // We don't support in-band password changes.
throw new PasswordChangesNotSupportedException();
}
// We are in!
sessionMetadata.resetFailedLoginAttempts();
sessionMetadata.setClientId(login.getClientId());
sessionMetadata.setSuperuser(
Objects.equals(INTERNAL_IANA_REGISTRAR_ID, registrar.getIanaIdentifier()));
sessionMetadata.setServiceExtensionUris(serviceExtensionUrisBuilder.build());
return createOutput(Code.Success);
}
/** Registrar with this client ID could not be found. */
static class BadRegistrarClientIdException extends AuthenticationErrorException {
public BadRegistrarClientIdException(String clientId) {
super("Registrar with this client ID could not be found: " + clientId);
}
}
/** Registrar password is incorrect. */
static class BadRegistrarPasswordException extends AuthenticationErrorException {
public BadRegistrarPasswordException() {
super("Registrar password is incorrect");
}
}
/** Registrar login failed too many times. */
static class TooManyFailedLoginsException extends AuthenticationErrorClosingConnectionException {
public TooManyFailedLoginsException() {
super("Registrar login failed too many times");
}
}
/** Registrar account is not active. */
static class RegistrarAccountNotActiveException extends AuthorizationErrorException {
public RegistrarAccountNotActiveException() {
super("Registrar account is not active");
}
}
/** Registrar is already logged in. */
static class AlreadyLoggedInException extends CommandUseErrorException {
public AlreadyLoggedInException() {
super("Registrar is already logged in");
}
}
/** Specified language is not supported. */
static class UnsupportedLanguageException extends ParameterValuePolicyErrorException {
public UnsupportedLanguageException() {
super("Specified language is not supported");
}
}
/** In-band password changes are not supported. */
static class PasswordChangesNotSupportedException extends UnimplementedOptionException {
public PasswordChangesNotSupportedException() {
super("In-band password changes are not supported");
}
}
}

View file

@ -0,0 +1,34 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// 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.
package com.google.domain.registry.flows.session;
import static com.google.domain.registry.model.eppoutput.Result.Code.SuccessAndClose;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.LoggedInFlow;
import com.google.domain.registry.model.eppoutput.EppOutput;
/**
* An EPP flow for logout.
*
* @error {@link com.google.domain.registry.flows.LoggedInFlow.NotLoggedInException}
*/
public class LogoutFlow extends LoggedInFlow {
@Override
public final EppOutput run() throws EppException {
sessionMetadata.invalidate();
return createOutput(SuccessAndClose);
}
}