Skip to content

Commit 94a8739

Browse files
committed
Add a bulk-domain-action console endpoint
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).
1 parent ae61cd4 commit 94a8739

File tree

5 files changed

+449
-2
lines changed

5 files changed

+449
-2
lines changed

core/src/main/java/google/registry/flows/ExtensionManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ private void checkForUndeclaredExtensions(
105105
}
106106

107107
private static final ImmutableSet<EppRequestSource> ALLOWED_METADATA_EPP_REQUEST_SOURCES =
108-
ImmutableSet.of(EppRequestSource.TOOL, EppRequestSource.BACKEND);
108+
ImmutableSet.of(EppRequestSource.BACKEND, EppRequestSource.CONSOLE, EppRequestSource.TOOL);
109109

110110
private void checkForRestrictedExtensions(
111111
ImmutableSet<Class<? extends CommandExtension>> suppliedExtensions)
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.ui.server.console;
16+
17+
import static com.google.common.collect.ImmutableMap.toImmutableMap;
18+
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
19+
import static java.nio.charset.StandardCharsets.UTF_8;
20+
21+
import com.google.common.collect.ImmutableMap;
22+
import com.google.gson.Gson;
23+
import com.google.gson.JsonElement;
24+
import com.google.gson.annotations.Expose;
25+
import google.registry.flows.EppController;
26+
import google.registry.flows.EppRequestSource;
27+
import google.registry.flows.PasswordOnlyTransportCredentials;
28+
import google.registry.flows.StatelessRequestSessionMetadata;
29+
import google.registry.model.console.ConsolePermission;
30+
import google.registry.model.console.User;
31+
import google.registry.model.eppcommon.ProtocolDefinition;
32+
import google.registry.model.eppoutput.EppOutput;
33+
import google.registry.model.eppoutput.Result;
34+
import google.registry.request.Action;
35+
import google.registry.request.OptionalJsonPayload;
36+
import google.registry.request.Parameter;
37+
import google.registry.request.auth.Auth;
38+
import java.util.List;
39+
import java.util.Optional;
40+
import javax.inject.Inject;
41+
42+
/**
43+
* Console endpoint to perform the same action to a list of domains.
44+
*
45+
* <p>All requests must include the {@link BulkAction} to perform as well as a {@link
46+
* BulkDomainList} of domains on which to apply the action. The remaining contents of the request
47+
* body depend on the type of action -- some requests may require more data than others.
48+
*/
49+
@Action(
50+
service = Action.GaeService.DEFAULT,
51+
gkeService = Action.GkeService.CONSOLE,
52+
path = ConsoleBulkDomainAction.PATH,
53+
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
54+
public class ConsoleBulkDomainAction extends ConsoleApiAction {
55+
56+
public static final String PATH = "/console-api/bulk-domain";
57+
58+
public enum BulkAction {
59+
DELETE,
60+
SUSPEND
61+
}
62+
63+
/** All requests must include at least a list of domain names on which to perform the action. */
64+
public record BulkDomainList(@Expose List<String> domainList) {}
65+
66+
public record BulkDomainDeleteRequest(@Expose String reason) {}
67+
68+
public record BulkDomainSuspendRequest(@Expose String reason) {}
69+
70+
private static final String DOMAIN_DELETE_XML =
71+
"""
72+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
73+
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
74+
<command>
75+
<delete>
76+
<domain:delete
77+
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
78+
<domain:name>%DOMAIN_NAME%</domain:name>
79+
</domain:delete>
80+
</delete>
81+
<extension>
82+
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
83+
<metadata:reason>%REASON%</metadata:reason>
84+
<metadata:requestedByRegistrar>true</metadata:requestedByRegistrar>
85+
</metadata:metadata>
86+
</extension>
87+
<clTRID>RegistryConsole</clTRID>
88+
</command>
89+
</epp>""";
90+
91+
private static final String DOMAIN_SUSPEND_XML =
92+
"""
93+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
94+
<epp
95+
xmlns="urn:ietf:params:xml:ns:epp-1.0">
96+
<command>
97+
<update>
98+
<domain:update
99+
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
100+
<domain:name>%DOMAIN_NAME%</domain:name>
101+
<domain:add>
102+
<domain:status s="serverDeleteProhibited" lang="en"></domain:status>
103+
<domain:status s="serverHold" lang="en"></domain:status>
104+
<domain:status s="serverRenewProhibited" lang="en"></domain:status>
105+
<domain:status s="serverTransferProhibited" lang="en"></domain:status>
106+
<domain:status s="serverUpdateProhibited" lang="en"></domain:status>
107+
</domain:add>
108+
<domain:rem></domain:rem>
109+
</domain:update>
110+
</update>
111+
<extension>
112+
<metadata:metadata
113+
xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
114+
<metadata:reason>Console suspension: %REASON%</metadata:reason>
115+
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
116+
</metadata:metadata>
117+
</extension>
118+
<clTRID>RegistryTool</clTRID>
119+
</command>
120+
</epp>""";
121+
122+
private final Gson gson;
123+
private final EppController eppController;
124+
private final String registrarId;
125+
private final String bulkDomainAction;
126+
private final Optional<JsonElement> optionalJsonPayload;
127+
128+
@Inject
129+
public ConsoleBulkDomainAction(
130+
ConsoleApiParams consoleApiParams,
131+
Gson gson,
132+
EppController eppController,
133+
@Parameter("registrarId") String registrarId,
134+
@Parameter("bulkDomainAction") String bulkDomainAction,
135+
@OptionalJsonPayload Optional<JsonElement> optionalJsonPayload) {
136+
super(consoleApiParams);
137+
this.gson = gson;
138+
this.eppController = eppController;
139+
this.registrarId = registrarId;
140+
this.bulkDomainAction = bulkDomainAction;
141+
this.optionalJsonPayload = optionalJsonPayload;
142+
}
143+
144+
@Override
145+
protected void postHandler(User user) {
146+
BulkAction bulkAction = BulkAction.valueOf(bulkDomainAction);
147+
JsonElement jsonPayload =
148+
optionalJsonPayload.orElseThrow(
149+
() -> new IllegalArgumentException("Bulk action payload must be present"));
150+
BulkDomainList domainList = gson.fromJson(jsonPayload, BulkDomainList.class);
151+
checkPermission(user, registrarId, ConsolePermission.EXECUTE_EPP_COMMANDS);
152+
ImmutableMap<String, ConsoleEppOutput> result =
153+
switch (bulkAction) {
154+
case DELETE -> handleBulkDelete(jsonPayload, domainList, user);
155+
case SUSPEND -> handleBulkSuspend(jsonPayload, domainList, user);
156+
};
157+
// Front end should parse situations where only some commands worked
158+
consoleApiParams.response().setPayload(gson.toJson(result));
159+
consoleApiParams.response().setStatus(SC_OK);
160+
}
161+
162+
private ImmutableMap<String, ConsoleEppOutput> handleBulkDelete(
163+
JsonElement jsonPayload, BulkDomainList domainList, User user) {
164+
String reason = gson.fromJson(jsonPayload, BulkDomainDeleteRequest.class).reason;
165+
return runCommandOverDomains(
166+
domainList, DOMAIN_DELETE_XML.replaceAll("%REASON%", reason), user);
167+
}
168+
169+
private ImmutableMap<String, ConsoleEppOutput> handleBulkSuspend(
170+
JsonElement jsonPayload, BulkDomainList domainList, User user) {
171+
String reason = gson.fromJson(jsonPayload, BulkDomainSuspendRequest.class).reason;
172+
return runCommandOverDomains(
173+
domainList, DOMAIN_SUSPEND_XML.replaceAll("%REASON%", reason), user);
174+
}
175+
176+
private ImmutableMap<String, ConsoleEppOutput> runCommandOverDomains(
177+
BulkDomainList domainList, String xml, User user) {
178+
return domainList.domainList.stream()
179+
.collect(toImmutableMap(d -> d, d -> executeEpp(xml.replaceAll("%DOMAIN_NAME%", d), user)));
180+
}
181+
182+
private ConsoleEppOutput executeEpp(String xml, User user) {
183+
return ConsoleEppOutput.fromEppOutput(
184+
eppController.handleEppCommand(
185+
new StatelessRequestSessionMetadata(
186+
registrarId, ProtocolDefinition.getVisibleServiceExtensionUris()),
187+
new PasswordOnlyTransportCredentials(),
188+
EppRequestSource.CONSOLE,
189+
!user.getUserRoles().isAdmin(),
190+
user.getUserRoles().isAdmin(),
191+
xml.getBytes(UTF_8)));
192+
}
193+
194+
public record ConsoleEppOutput(@Expose String message, @Expose int responseCode) {
195+
static ConsoleEppOutput fromEppOutput(EppOutput eppOutput) {
196+
Result result = eppOutput.getResponse().getResult();
197+
return new ConsoleEppOutput(result.getMsg(), result.getCode().code);
198+
}
199+
}
200+
}

core/src/main/java/google/registry/ui/server/console/ConsoleModule.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,12 @@ public static Optional<String> provideSearchTerm(HttpServletRequest req) {
238238
return extractOptionalParameter(req, "searchTerm");
239239
}
240240

241+
@Provides
242+
@Parameter("bulkDomainAction")
243+
public static String provideBulkDomainAction(HttpServletRequest req) {
244+
return extractRequiredParameter(req, "bulkDomainAction");
245+
}
246+
241247
@Provides
242248
@Parameter("eppPasswordChangeRequest")
243249
public static Optional<EppPasswordData> provideEppPasswordChangeRequest(

core/src/test/java/google/registry/flows/ExtensionManagerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ void testMetadataExtension_allowedForToolSource() throws Exception {
121121
void testMetadataExtension_forbiddenWhenNotToolSource() {
122122
ExtensionManager manager =
123123
new TestInstanceBuilder()
124-
.setEppRequestSource(EppRequestSource.CONSOLE)
124+
.setEppRequestSource(EppRequestSource.TLS)
125125
.setDeclaredUris()
126126
.setSuppliedExtensions(MetadataExtension.class)
127127
.build();

0 commit comments

Comments
 (0)