-
Notifications
You must be signed in to change notification settings - Fork 275
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a bulk-domain-action console endpoint (#2611)
For now it only includes two options (domain deletion and domain suspension). In the future, as necessary, we can add other actions but this seems like a relatively simple starting point (actions like bulk updates are much more conceptually complex).
- Loading branch information
Showing
5 changed files
with
478 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
228 changes: 228 additions & 0 deletions
228
core/src/main/java/google/registry/ui/server/console/ConsoleBulkDomainAction.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
// Copyright 2024 The Nomulus 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 google.registry.ui.server.console; | ||
|
||
import static com.google.common.collect.ImmutableMap.toImmutableMap; | ||
import static jakarta.servlet.http.HttpServletResponse.SC_OK; | ||
import static java.nio.charset.StandardCharsets.UTF_8; | ||
|
||
import com.google.common.collect.ImmutableMap; | ||
import com.google.common.escape.Escaper; | ||
import com.google.common.xml.XmlEscapers; | ||
import com.google.gson.JsonElement; | ||
import com.google.gson.annotations.Expose; | ||
import google.registry.flows.EppController; | ||
import google.registry.flows.EppRequestSource; | ||
import google.registry.flows.PasswordOnlyTransportCredentials; | ||
import google.registry.flows.StatelessRequestSessionMetadata; | ||
import google.registry.model.console.ConsolePermission; | ||
import google.registry.model.console.User; | ||
import google.registry.model.eppcommon.ProtocolDefinition; | ||
import google.registry.model.eppoutput.EppOutput; | ||
import google.registry.model.eppoutput.Result; | ||
import google.registry.request.Action; | ||
import google.registry.request.OptionalJsonPayload; | ||
import google.registry.request.Parameter; | ||
import google.registry.request.auth.Auth; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import javax.inject.Inject; | ||
|
||
/** | ||
* Console endpoint to perform the same action to a list of domains. | ||
* | ||
* <p>All requests must include the {@link BulkAction} to perform as well as a {@link | ||
* BulkDomainList} of domains on which to apply the action. The remaining contents of the request | ||
* body depend on the type of action -- some requests may require more data than others. | ||
*/ | ||
@Action( | ||
service = Action.GaeService.DEFAULT, | ||
gkeService = Action.GkeService.CONSOLE, | ||
path = ConsoleBulkDomainAction.PATH, | ||
auth = Auth.AUTH_PUBLIC_LOGGED_IN) | ||
public class ConsoleBulkDomainAction extends ConsoleApiAction { | ||
|
||
public static final String PATH = "/console-api/bulk-domain"; | ||
|
||
private static Escaper XML_ESCAPER = XmlEscapers.xmlContentEscaper(); | ||
|
||
public enum BulkAction { | ||
DELETE, | ||
SUSPEND | ||
} | ||
|
||
/** All requests must include at least a list of domain names on which to perform the action. */ | ||
public record BulkDomainList(@Expose List<String> domainList) {} | ||
|
||
public record BulkDomainDeleteRequest(@Expose String reason) {} | ||
|
||
public record BulkDomainSuspendRequest(@Expose String reason) {} | ||
|
||
private static final String DOMAIN_DELETE_XML = | ||
""" | ||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | ||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"> | ||
<command> | ||
<delete> | ||
<domain:delete | ||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0"> | ||
<domain:name>%DOMAIN_NAME%</domain:name> | ||
</domain:delete> | ||
</delete> | ||
<extension> | ||
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0"> | ||
<metadata:reason>%REASON%</metadata:reason> | ||
<metadata:requestedByRegistrar>true</metadata:requestedByRegistrar> | ||
</metadata:metadata> | ||
</extension> | ||
<clTRID>RegistryConsole</clTRID> | ||
</command> | ||
</epp>"""; | ||
|
||
private static final String DOMAIN_SUSPEND_XML = | ||
""" | ||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | ||
<epp | ||
xmlns="urn:ietf:params:xml:ns:epp-1.0"> | ||
<command> | ||
<update> | ||
<domain:update | ||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0"> | ||
<domain:name>%DOMAIN_NAME%</domain:name> | ||
<domain:add> | ||
<domain:status s="serverDeleteProhibited" lang="en"></domain:status> | ||
<domain:status s="serverHold" lang="en"></domain:status> | ||
<domain:status s="serverRenewProhibited" lang="en"></domain:status> | ||
<domain:status s="serverTransferProhibited" lang="en"></domain:status> | ||
<domain:status s="serverUpdateProhibited" lang="en"></domain:status> | ||
</domain:add> | ||
<domain:rem></domain:rem> | ||
</domain:update> | ||
</update> | ||
<extension> | ||
<metadata:metadata | ||
xmlns:metadata="urn:google:params:xml:ns:metadata-1.0"> | ||
<metadata:reason>Console suspension: %REASON%</metadata:reason> | ||
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar> | ||
</metadata:metadata> | ||
</extension> | ||
<clTRID>RegistryTool</clTRID> | ||
</command> | ||
</epp>"""; | ||
|
||
private final EppController eppController; | ||
private final String registrarId; | ||
private final String bulkDomainAction; | ||
private final Optional<JsonElement> optionalJsonPayload; | ||
|
||
@Inject | ||
public ConsoleBulkDomainAction( | ||
ConsoleApiParams consoleApiParams, | ||
EppController eppController, | ||
@Parameter("registrarId") String registrarId, | ||
@Parameter("bulkDomainAction") String bulkDomainAction, | ||
@OptionalJsonPayload Optional<JsonElement> optionalJsonPayload) { | ||
super(consoleApiParams); | ||
this.eppController = eppController; | ||
this.registrarId = registrarId; | ||
this.bulkDomainAction = bulkDomainAction; | ||
this.optionalJsonPayload = optionalJsonPayload; | ||
} | ||
|
||
@Override | ||
protected void postHandler(User user) { | ||
BulkAction bulkAction = BulkAction.valueOf(bulkDomainAction); | ||
JsonElement jsonPayload = | ||
optionalJsonPayload.orElseThrow( | ||
() -> new IllegalArgumentException("Bulk action payload must be present")); | ||
BulkDomainList domainList = consoleApiParams.gson().fromJson(jsonPayload, BulkDomainList.class); | ||
checkPermission(user, registrarId, ConsolePermission.EXECUTE_EPP_COMMANDS); | ||
ImmutableMap<String, ConsoleEppOutput> result = | ||
switch (bulkAction) { | ||
case DELETE -> handleBulkDelete(jsonPayload, domainList, user); | ||
case SUSPEND -> handleBulkSuspend(jsonPayload, domainList, user); | ||
}; | ||
// Front end should parse situations where only some commands worked | ||
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(result)); | ||
consoleApiParams.response().setStatus(SC_OK); | ||
} | ||
|
||
private ImmutableMap<String, ConsoleEppOutput> handleBulkDelete( | ||
JsonElement jsonPayload, BulkDomainList domainList, User user) { | ||
String reason = | ||
consoleApiParams.gson().fromJson(jsonPayload, BulkDomainDeleteRequest.class).reason; | ||
return runCommandOverDomains( | ||
domainList, | ||
DOMAIN_DELETE_XML, | ||
new ImmutableMap.Builder<String, String>().put("REASON", reason), | ||
user); | ||
} | ||
|
||
private ImmutableMap<String, ConsoleEppOutput> handleBulkSuspend( | ||
JsonElement jsonPayload, BulkDomainList domainList, User user) { | ||
String reason = | ||
consoleApiParams.gson().fromJson(jsonPayload, BulkDomainSuspendRequest.class).reason; | ||
return runCommandOverDomains( | ||
domainList, | ||
DOMAIN_SUSPEND_XML, | ||
new ImmutableMap.Builder<String, String>().put("REASON", reason), | ||
user); | ||
} | ||
|
||
/** Runs the provided XML template and substitutions over a provided list of domains. */ | ||
private ImmutableMap<String, ConsoleEppOutput> runCommandOverDomains( | ||
BulkDomainList domainList, | ||
String xmlTemplate, | ||
ImmutableMap.Builder<String, String> replacements, | ||
User user) { | ||
return domainList.domainList.stream() | ||
.collect( | ||
toImmutableMap( | ||
d -> d, | ||
d -> | ||
executeEpp( | ||
fillSubstitutions(xmlTemplate, replacements.put("DOMAIN_NAME", d)), user))); | ||
} | ||
|
||
private ConsoleEppOutput executeEpp(String xml, User user) { | ||
return ConsoleEppOutput.fromEppOutput( | ||
eppController.handleEppCommand( | ||
new StatelessRequestSessionMetadata( | ||
registrarId, ProtocolDefinition.getVisibleServiceExtensionUris()), | ||
new PasswordOnlyTransportCredentials(), | ||
EppRequestSource.CONSOLE, | ||
false, | ||
user.getUserRoles().isAdmin(), | ||
xml.getBytes(UTF_8))); | ||
} | ||
|
||
/** Fills the provided XML template with the replacement values, including escaping the values. */ | ||
private String fillSubstitutions( | ||
String xmlTemplate, ImmutableMap.Builder<String, String> replacements) { | ||
String xml = xmlTemplate; | ||
for (Map.Entry<String, String> entry : replacements.buildKeepingLast().entrySet()) { | ||
xml = xml.replaceAll("%" + entry.getKey() + "%", XML_ESCAPER.escape(entry.getValue())); | ||
} | ||
return xml; | ||
} | ||
|
||
public record ConsoleEppOutput(@Expose String message, @Expose int responseCode) { | ||
static ConsoleEppOutput fromEppOutput(EppOutput eppOutput) { | ||
Result result = eppOutput.getResponse().getResult(); | ||
return new ConsoleEppOutput(result.getMsg(), result.getCode().code); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.