1
0
mirror of https://github.com/google/nomulus synced 2026-06-09 16:33:02 +00:00

Compare commits

...

7 Commits

Author SHA1 Message Date
Weimin Yu b3e67e58b5 Change billing for multi-year domain creation (#2446)
* Change billing for multi-year domain creation

From the second year on, charge the renewal price.

See b/322833077
2024-05-29 13:19:54 -04:00
gbrodman 589041b3ed Fully reset domain-list page on registrar change (#2456)
When the registrar changes we should reset the page and the total
results to 0 (since we haven't loaded them yet)

https://b.corp.google.com/issues/343193698
2024-05-29 12:54:01 -04:00
Lai Jiang 455364ff29 Remove GAE Users service API usage (#2414)
This is the last remaining GAE API that we depend on. By removing it, we are able to remove all common GAE dependencies as well.

To merge this PR, we need to create console User objects that have the same email address as the RegistrarPoc objects' login_email_address and copy over the existing registry lock hashes and salts.

We are also able to simply the code base by removing some redundant logic like AuthMethod (API is now the only supported one) and UserAuthInfo (console user is now the only supported one)

There are several behavioral changes that are worth noting:

The XsrfTokenManager now uses the console user's email address to mint and verify the token. Previously, only email addresses returned by the GAE Users service are used, whereas a blank email address will be used if the user is logged in as a console user. I believe this was an oversight that is now corrected.
The legacy console will return 401 when no user is logged in, instead of redirecting to the Users service login flow.
The logout URL in the legacy console is changed to use the IAP logout flow. It will clear the cookie and redirect the users to IAP login page (tested on QA).
The screenshot changes are mostly due to the console users lacking a display name and therefore showing the email address instead. Some changes are due to using the console user's email address as the registry lock email address, which is being fixed in Add DB column for separate rlock email address #2413 and its follow-up RPs.
2024-05-29 12:37:44 -04:00
Lai Jiang d90bc1a3e4 Update db README (#2449) 2024-05-29 11:54:31 -04:00
Lai Jiang 0e3875c1ff Removing leading newline from GKE log messages (#2454)
GKE now displays log messages correctly. There is no need for an extra
leading newline, which now results in a useless blank line for each log
entry in Log Explorer.
2024-05-29 11:54:19 -04:00
Lai Jiang 3b565b96b7 Add the ability to add/remove console users from a Google Group (#2450)
# Conflicts:
#	config/presubmits.py
2024-05-28 17:00:37 +00:00
Pavlo Tkach ec6c77927f Add console backend for editing registrar (#2452) 2024-05-28 00:53:32 +00:00
232 changed files with 1736 additions and 1976 deletions
+6
View File
@@ -196,6 +196,12 @@ PRESUBMITS = {
{"/node_modules/"},
):
"Use status code from jakarta.servlet.http.HttpServletResponse.",
PresubmitCheck(
r".*mock\(Response\.class\).*",
"java",
{"/node_modules/"},
):
"Do not mock Response, use FakeResponse.",
}
# Note that this regex only works for one kind of Flyway file. If we want to
@@ -59,6 +59,8 @@ export class DomainListComponent {
) {
effect(() => {
if (this.registrarService.registrarId()) {
this.pageNumber = 0;
this.totalResults = 0;
this.reloadData();
}
});
-6
View File
@@ -153,7 +153,6 @@ dependencies {
implementation deps['com.google.apis:google-api-services-monitoring']
implementation deps['com.google.apis:google-api-services-sheets']
implementation deps['com.google.apis:google-api-services-storage']
testImplementation deps['com.google.appengine:appengine-api-stubs']
implementation deps['com.google.auth:google-auth-library-credentials']
implementation deps['com.google.auth:google-auth-library-oauth2-http']
implementation deps['com.google.cloud.bigdataoss:util']
@@ -250,10 +249,6 @@ dependencies {
implementation deps['us.fatehi:schemacrawler-tools']
implementation deps['xerces:xmlParserAPIs']
implementation deps['org.ogce:xpp3']
// This dependency must come after javax.mail:mail as it would otherwise
// shadow classes in package javax.mail with its own implementation.
implementation deps['com.google.appengine:appengine-api-1.0-sdk']
// Known issue: nebula-lint misses inherited dependency.
implementation project(':common')
testImplementation project(path: ':common', configuration: 'testing')
@@ -271,7 +266,6 @@ dependencies {
testAnnotationProcessor project(':processor')
testImplementation deps['com.google.cloud:google-cloud-nio']
testImplementation deps['com.google.appengine:appengine-testing']
testImplementation deps['com.google.guava:guava-testlib']
testImplementation deps['com.google.monitoring-client:contrib']
testImplementation deps['com.google.protobuf:protobuf-java-util']
-5
View File
@@ -105,11 +105,6 @@ com.google.apis:google-api-services-sheets:v4-rev20240423-2.0.0=compileClasspath
com.google.apis:google-api-services-sqladmin:v1beta4-rev20240324-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-storage:v1-rev20240311-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.apis:google-api-services-storage:v1-rev20240319-2.0.0=testCompileClasspath,testRuntimeClasspath
com.google.appengine:appengine-api-1.0-sdk:2.0.27=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.appengine:appengine-api-stubs:2.0.27=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.appengine:appengine-remote-api:2.0.27=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.appengine:appengine-testing:2.0.27=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.appengine:appengine-tools-sdk:2.0.27=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auth:google-auth-library-credentials:1.23.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auth:google-auth-library-oauth2-http:1.23.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auto.service:auto-service-annotations:1.0.1=errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
@@ -46,7 +46,7 @@ import javax.net.ssl.HttpsURLConnection;
path = "/_dr/task/executeCannedScript",
method = {POST, GET},
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class CannedScriptExecutionAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -38,10 +38,7 @@ import org.joda.time.Days;
* An action that checks all {@link BulkPricingPackage} objects for compliance with their max create
* limit.
*/
@Action(
service = Service.BACKEND,
path = CheckBulkComplianceAction.PATH,
auth = Auth.AUTH_API_ADMIN)
@Action(service = Service.BACKEND, path = CheckBulkComplianceAction.PATH, auth = Auth.AUTH_ADMIN)
public class CheckBulkComplianceAction implements Runnable {
public static final String PATH = "/_dr/task/checkBulkCompliance";
@@ -69,7 +69,7 @@ import org.joda.time.Duration;
@Action(
service = Action.Service.BACKEND,
path = DeleteExpiredDomainsAction.PATH,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class DeleteExpiredDomainsAction implements Runnable {
public static final String PATH = "/_dr/task/deleteExpiredDomains";
@@ -56,7 +56,7 @@ import javax.inject.Inject;
service = Action.Service.BACKEND,
path = "/_dr/task/deleteLoadTestData",
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class DeleteLoadTestDataAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -61,7 +61,7 @@ import org.joda.time.Duration;
service = Action.Service.BACKEND,
path = "/_dr/task/deleteProberData",
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class DeleteProberDataAction implements Runnable {
// TODO(b/323026070): Add email alert on failure of this action
@@ -52,7 +52,7 @@ import org.joda.time.DateTime;
@Action(
service = Action.Service.BACKEND,
path = "/_dr/task/expandBillingRecurrences",
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class ExpandBillingRecurrencesAction implements Runnable {
public static final String PARAM_START_TIME = "startTime";
@@ -53,7 +53,7 @@ import org.joda.time.Duration;
path = RelockDomainAction.PATH,
method = POST,
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class RelockDomainAction implements Runnable {
public static final String PATH = "/_dr/task/relockDomain";
@@ -55,7 +55,7 @@ import javax.inject.Inject;
@Action(
service = Action.Service.BACKEND,
path = ResaveAllEppResourcesPipelineAction.PATH,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class ResaveAllEppResourcesPipelineAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -40,7 +40,7 @@ import org.joda.time.DateTime;
@Action(
service = Action.Service.BACKEND,
path = ResaveEntityAction.PATH,
auth = Auth.AUTH_API_ADMIN,
auth = Auth.AUTH_ADMIN,
method = Method.POST)
public class ResaveEntityAction implements Runnable {
@@ -51,7 +51,7 @@ import org.joda.time.format.DateTimeFormatter;
@Action(
service = Action.Service.BACKEND,
path = SendExpiringCertificateNotificationEmailAction.PATH,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class SendExpiringCertificateNotificationEmailAction implements Runnable {
public static final String PATH = "/_dr/task/sendExpiringCertificateNotificationEmail";
@@ -51,7 +51,7 @@ import org.joda.time.DateTime;
@Action(
service = Service.BACKEND,
path = WipeOutContactHistoryPiiAction.PATH,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class WipeOutContactHistoryPiiAction implements Runnable {
public static final String PATH = "/_dr/task/wipeOutContactHistoryPii";
@@ -53,7 +53,7 @@ import javax.inject.Inject;
service = Action.Service.BSA,
path = BsaDownloadAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class BsaDownloadAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -44,7 +44,7 @@ import org.joda.time.Duration;
service = Action.Service.BSA,
path = BsaRefreshAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class BsaRefreshAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -70,7 +70,7 @@ import org.joda.time.Duration;
service = Action.Service.BSA,
path = BsaValidateAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class BsaValidateAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -75,7 +75,7 @@ import org.joda.time.DateTime;
service = Service.BSA,
path = "/_dr/task/uploadBsaUnavailableNames",
method = {GET, POST},
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class UploadBsaUnavailableDomainsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -491,6 +491,18 @@ public final class RegistryConfig {
return Optional.ofNullable(Strings.emptyToNull(config.gSuite.supportGroupEmailAddress));
}
/**
* Returns the email address of the group containing emails of console users.
*
* <p>This group should be granted the {@code roles/iap.httpsResourceAccessor} role.
*/
@Provides
@Config("gSuiteConsoleUserGroupEmailAddress")
public static Optional<String> provideGSuiteConsoleUserGroupEmailAddress(
RegistryConfigSettings config) {
return Optional.ofNullable(Strings.emptyToNull(config.gSuite.consoleUserGroupEmailAddress));
}
/**
* Returns the email address(es) that notifications of registrar and/or registrar contact
* updates should be sent to, or the empty list if updates should not be sent.
@@ -83,6 +83,7 @@ public class RegistryConfigSettings {
public String outgoingEmailDisplayName;
public String adminAccountEmailAddress;
public String supportGroupEmailAddress;
public String consoleUserGroupEmailAddress;
}
/** Configuration options for registry policy. */
@@ -47,6 +47,11 @@ gSuite:
# given "ADMIN" role on the registrar console.
supportGroupEmailAddress: support@example.com
# Group containing the emails of console users. This group should be granted
# roles/iap.httpsResourceAccessor out-of-band. If this field is empty, each
# console user will be granted to the role individually when they are created.
consoleUserGroupEmailAddress:
registryPolicy:
# Repository identifier (ROID) suffix for contacts and hosts.
contactAndHostRoidSuffix: ROID
@@ -81,7 +81,7 @@ import javax.inject.Inject;
service = Action.Service.BACKEND,
path = "/_dr/cron/fanout",
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class TldFanoutAction implements Runnable {
/** A set of control params to TldFanoutAction that aren't passed down to the executing action. */
@@ -77,7 +77,7 @@ import org.joda.time.Duration;
path = PublishDnsUpdatesAction.PATH,
method = POST,
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
public static final String PATH = "/_dr/task/publishDnsUpdates";
@@ -64,7 +64,7 @@ import org.joda.time.Duration;
path = "/_dr/task/readDnsRefreshRequests",
automaticallyPrintOk = true,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class ReadDnsRefreshRequestsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -38,7 +38,7 @@ import javax.inject.Inject;
service = Action.Service.BACKEND,
path = "/_dr/dnsRefresh",
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class RefreshDnsAction implements Runnable {
private final Clock clock;
@@ -33,11 +33,7 @@ import google.registry.request.auth.Auth;
import javax.inject.Inject;
import org.joda.time.DateTime;
@Action(
service = Service.BACKEND,
path = PATH,
method = Action.Method.POST,
auth = Auth.AUTH_API_ADMIN)
@Action(service = Service.BACKEND, path = PATH, method = Action.Method.POST, auth = Auth.AUTH_ADMIN)
public class RefreshDnsOnHostRenameAction implements Runnable {
public static final String QUEUE_HOST_RENAME = "async-host-rename";
@@ -12,6 +12,11 @@
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>tools-servlet</servlet-name>
<url-pattern>/_dr/admin/updateUserGroup</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>tools-servlet</servlet-name>
<url-pattern>/_dr/admin/verifyOte</url-pattern>
@@ -49,7 +49,7 @@ import javax.inject.Inject;
service = Action.Service.BACKEND,
path = "/_dr/task/exportDomainLists",
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class ExportDomainListsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -48,7 +48,7 @@ import javax.inject.Inject;
service = Action.Service.BACKEND,
path = "/_dr/task/exportPremiumTerms",
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class ExportPremiumTermsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -37,7 +37,7 @@ import javax.inject.Inject;
service = Action.Service.BACKEND,
path = "/_dr/task/exportReservedTerms",
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class ExportReservedTermsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -56,7 +56,7 @@ import javax.inject.Inject;
service = Action.Service.BACKEND,
path = "/_dr/task/syncGroupMembers",
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class SyncGroupMembersAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -57,7 +57,7 @@ import org.joda.time.Duration;
service = Action.Service.BACKEND,
path = SyncRegistrarsSheetAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class SyncRegistrarsSheetAction implements Runnable {
private enum Result {
@@ -29,7 +29,7 @@ import javax.inject.Inject;
service = Action.Service.DEFAULT,
path = "/_dr/epp",
method = Method.POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class EppTlsAction implements Runnable {
@Inject @Payload byte[] inputXmlBytes;
@@ -33,7 +33,7 @@ import javax.inject.Inject;
service = Action.Service.TOOLS,
path = EppToolAction.PATH,
method = Method.POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class EppToolAction implements Runnable {
public static final String PATH = "/_dr/epptool";
@@ -159,7 +159,11 @@ public final class DomainPricingLogic {
case NONPREMIUM -> {
renewCost =
getDomainCostWithDiscount(
false, years, allocationToken, tld.getStandardRenewCost(dateTime));
false,
years,
allocationToken,
tld.getStandardRenewCost(dateTime),
Optional.empty());
isRenewCostPremiumPrice = false;
}
default ->
@@ -252,7 +256,11 @@ public final class DomainPricingLogic {
DomainPrices domainPrices, int years, Optional<AllocationToken> allocationToken)
throws EppException {
return getDomainCostWithDiscount(
domainPrices.isPremium(), years, allocationToken, domainPrices.getCreateCost());
domainPrices.isPremium(),
years,
allocationToken,
domainPrices.getCreateCost(),
Optional.of(domainPrices.getRenewCost()));
}
/** Returns the domain renew cost with allocation-token-related discounts applied. */
@@ -272,24 +280,45 @@ public final class DomainPricingLogic {
}
}
return getDomainCostWithDiscount(
domainPrices.isPremium(), years, allocationToken, domainPrices.getRenewCost());
domainPrices.isPremium(),
years,
allocationToken,
domainPrices.getRenewCost(),
Optional.empty());
}
/**
* Returns the domain creation or renewal cost for the given number of {@code years}.
*
* <p>For domain creation, {@code firstYearCost} is the creation cost while {@code
* subsequentYearCost} is the single-year renewal cost (which is guaranteed to be present).
*
* <p>For domain renewal, {@code firstYearCost} is the single-year renewal cost and {@code
* subsequentYearCost} should be empty.
*/
private Money getDomainCostWithDiscount(
boolean isPremium, int years, Optional<AllocationToken> allocationToken, Money oneYearCost)
boolean isPremium,
int years,
Optional<AllocationToken> allocationToken,
Money firstYearCost,
Optional<Money> subsequentYearCost)
throws AllocationTokenInvalidForPremiumNameException {
checkArgument(years > 0, "Registration years to get cost for must be positive.");
validateTokenForPossiblePremiumName(allocationToken, isPremium);
Money totalDomainFlowCost = oneYearCost.multipliedBy(years);
Money totalDomainFlowCost =
firstYearCost.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(years - 1));
// Apply the allocation token discount, if applicable.
if (allocationToken.isPresent()
&& allocationToken.get().getTokenBehavior().equals(TokenBehavior.DEFAULT)) {
int discountedYears = Math.min(years, allocationToken.get().getDiscountYears());
Money discount =
oneYearCost.multipliedBy(
discountedYears * allocationToken.get().getDiscountFraction(),
RoundingMode.HALF_EVEN);
totalDomainFlowCost = totalDomainFlowCost.minus(discount);
if (discountedYears > 0) {
var discount =
firstYearCost
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(discountedYears - 1))
.multipliedBy(allocationToken.get().getDiscountFraction(), RoundingMode.HALF_EVEN);
totalDomainFlowCost = totalDomainFlowCost.minus(discount);
}
}
return totalDomainFlowCost;
}
@@ -58,7 +58,7 @@ import org.joda.time.DateTime;
path = LoadTestAction.PATH,
method = Action.Method.POST,
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class LoadTestAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -350,9 +350,8 @@ public class LoadTestAction implements Runnable {
.toBuilder()
.getAppEngineHttpRequest()
.toBuilder()
// instead of adding the X_CSRF_TOKEN to params, this remains as part of
// headers because of the existing setup for authentication in {@link
// google.registry.request.auth.LegacyAuthenticationMechanism}
// TODO: investigate if the following is necessary now that
// LegacyAuthenticationMechanism is gone.
.putHeaders(X_CSRF_TOKEN, xsrfToken)
.build())
.setScheduleTime(
@@ -1,20 +0,0 @@
// Copyright 2021 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.model.annotations;
/**
* Annotation to indicate a class that should be deleted after the database migration is complete.
*/
public @interface DeleteAfterMigration {}
@@ -47,7 +47,6 @@ import google.registry.rde.JSchModule;
import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.RequestHandler;
import google.registry.request.auth.AuthModule;
import google.registry.request.auth.RequestAuthenticator;
@@ -88,7 +87,6 @@ import javax.inject.Singleton;
SheetsServiceModule.class,
StackdriverModule.class,
UrlConnectionServiceModule.class,
UserServiceModule.class,
UtilsModule.class,
VoidDnsWriterModule.class,
})
@@ -107,6 +107,7 @@ import google.registry.tools.server.ListReservedListsAction;
import google.registry.tools.server.ListTldsAction;
import google.registry.tools.server.RefreshDnsForAllDomainsAction;
import google.registry.tools.server.ToolsServerModule;
import google.registry.tools.server.UpdateUserGroupAction;
import google.registry.tools.server.VerifyOteAction;
import google.registry.ui.server.console.ConsoleDomainGetAction;
import google.registry.ui.server.console.ConsoleDomainListAction;
@@ -323,6 +324,8 @@ interface RequestComponent {
UpdateRegistrarRdapBaseUrlsAction updateRegistrarRdapBaseUrlsAction();
UpdateUserGroupAction updateUserGroupAction();
UploadBsaUnavailableDomainsAction uploadBsaUnavailableDomains();
VerifyOteAction verifyOteAction();
@@ -43,7 +43,6 @@ import google.registry.rde.JSchModule;
import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule;
import google.registry.util.UtilsModule;
import javax.inject.Singleton;
@@ -78,7 +77,6 @@ import javax.inject.Singleton;
SheetsServiceModule.class,
StackdriverModule.class,
UrlConnectionServiceModule.class,
UserServiceModule.class,
VoidDnsWriterModule.class,
UtilsModule.class
})
@@ -28,7 +28,6 @@ import google.registry.persistence.PersistenceModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule;
import google.registry.util.UtilsModule;
import javax.inject.Singleton;
@@ -48,7 +47,6 @@ import javax.inject.Singleton;
SecretManagerModule.class,
StackdriverModule.class,
UrlConnectionServiceModule.class,
UserServiceModule.class,
UtilsModule.class
})
interface BsaComponent {
@@ -35,7 +35,6 @@ import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule;
import google.registry.ui.ConsoleDebug.ConsoleConfigModule;
import google.registry.util.UtilsModule;
@@ -66,7 +65,6 @@ import javax.inject.Singleton;
SecretManagerModule.class,
ServerTridProviderModule.class,
StackdriverModule.class,
UserServiceModule.class,
UtilsModule.class
})
interface FrontendComponent {
@@ -34,7 +34,6 @@ import google.registry.persistence.PersistenceModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule;
import google.registry.util.UtilsModule;
import javax.inject.Singleton;
@@ -61,7 +60,6 @@ import javax.inject.Singleton;
SecretManagerModule.class,
ServerTridProviderModule.class,
StackdriverModule.class,
UserServiceModule.class,
UtilsModule.class
})
interface PubApiComponent {
@@ -35,7 +35,6 @@ import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule;
import google.registry.util.UtilsModule;
import javax.inject.Singleton;
@@ -63,7 +62,6 @@ import javax.inject.Singleton;
ServerTridProviderModule.class,
StackdriverModule.class,
ToolsRequestComponentModule.class,
UserServiceModule.class,
UtilsModule.class
})
interface ToolsComponent {
@@ -36,6 +36,7 @@ import google.registry.tools.server.ListReservedListsAction;
import google.registry.tools.server.ListTldsAction;
import google.registry.tools.server.RefreshDnsForAllDomainsAction;
import google.registry.tools.server.ToolsServerModule;
import google.registry.tools.server.UpdateUserGroupAction;
import google.registry.tools.server.VerifyOteAction;
/** Dagger component with per-request lifetime for "tools" App Engine module. */
@@ -50,9 +51,10 @@ import google.registry.tools.server.VerifyOteAction;
WhiteboxModule.class,
})
public interface ToolsRequestComponent {
FlowComponent.Builder flowComponentBuilder();
CreateGroupsAction createGroupsAction();
EppToolAction eppToolAction();
FlowComponent.Builder flowComponentBuilder();
GenerateZoneFilesAction generateZoneFilesAction();
ListDomainsAction listDomainsAction();
ListHostsAction listHostsAction();
@@ -62,6 +64,9 @@ public interface ToolsRequestComponent {
ListTldsAction listTldsAction();
LoadTestAction loadTestAction();
RefreshDnsForAllDomainsAction refreshDnsForAllDomainsAction();
UpdateUserGroupAction updateUserGroupAction();
VerifyOteAction verifyOteAction();
@Subcomponent.Builder
@@ -18,11 +18,11 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import dagger.Module;
import dagger.Provides;
import google.registry.model.console.User;
import google.registry.request.Parameter;
import google.registry.request.RequestParameters;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.UserAuthInfo;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
@@ -101,11 +101,11 @@ public final class RdapModule {
@Provides
static RdapAuthorization provideRdapAuthorization(
AuthResult authResult, AuthenticatedRegistrarAccessor registrarAccessor) {
if (authResult.userAuthInfo().isEmpty()) {
if (authResult.user().isEmpty()) {
return RdapAuthorization.PUBLIC_AUTHORIZATION;
}
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
if (userAuthInfo.isUserAdmin()) {
User user = authResult.user().get();
if (user.getUserRoles().isAdmin()) {
return RdapAuthorization.ADMINISTRATOR_AUTHORIZATION;
}
ImmutableSet<String> clientIds = registrarAccessor.getAllRegistrarIdsWithRoles().keySet();
@@ -55,7 +55,7 @@ import org.apache.commons.csv.CSVRecord;
service = Action.Service.BACKEND,
path = "/_dr/task/updateRegistrarRdapBaseUrls",
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class UpdateRegistrarRdapBaseUrlsAction implements Runnable {
private static final String RDAP_IDS_URL =
@@ -66,7 +66,7 @@ import org.joda.time.DateTime;
path = BrdaCopyAction.PATH,
method = POST,
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class BrdaCopyAction implements Runnable {
public static final String PATH = "/_dr/task/brdaCopy";
@@ -56,7 +56,7 @@ import org.joda.time.Duration;
service = Action.Service.BACKEND,
path = RdeReportAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class RdeReportAction implements Runnable, EscrowTask {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -207,7 +207,7 @@ import org.joda.time.Duration;
service = Action.Service.BACKEND,
path = RdeStagingAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class RdeStagingAction implements Runnable {
public static final String PATH = "/_dr/task/rdeStaging";
@@ -87,7 +87,7 @@ import org.joda.time.Duration;
service = Action.Service.BACKEND,
path = RdeUploadAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class RdeUploadAction implements Runnable, EscrowTask {
public static final String PATH = "/_dr/task/rdeUpload";
@@ -47,7 +47,7 @@ import javax.inject.Inject;
service = Action.Service.BACKEND,
path = CopyDetailReportsAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class CopyDetailReportsAction implements Runnable {
public static final String PATH = "/_dr/task/copyDetailReports";
@@ -54,7 +54,7 @@ import org.joda.time.YearMonth;
service = Action.Service.BACKEND,
path = GenerateInvoicesAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class GenerateInvoicesAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -52,7 +52,7 @@ import org.joda.time.YearMonth;
service = Action.Service.BACKEND,
path = PublishInvoicesAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class PublishInvoicesAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -67,7 +67,7 @@ import org.joda.time.format.DateTimeFormat;
service = Action.Service.BACKEND,
path = IcannReportingStagingAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class IcannReportingStagingAction implements Runnable {
static final String PATH = "/_dr/task/icannReportingStaging";
@@ -70,7 +70,7 @@ import org.joda.time.Duration;
service = Action.Service.BACKEND,
path = IcannReportingUploadAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class IcannReportingUploadAction implements Runnable {
static final String PATH = "/_dr/task/icannReportingUpload";
@@ -53,7 +53,7 @@ import org.joda.time.LocalDate;
service = Action.Service.BACKEND,
path = GenerateSpec11ReportAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class GenerateSpec11ReportAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -59,7 +59,7 @@ import org.json.JSONException;
service = Action.Service.BACKEND,
path = PublishSpec11ReportAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class PublishSpec11ReportAction implements Runnable {
static final String PATH = "/_dr/task/publishSpec11";
@@ -18,8 +18,6 @@ import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import dagger.Module;
import dagger.Provides;
import java.net.HttpURLConnection;
@@ -47,17 +45,6 @@ public final class Modules {
}
}
/** Dagger module for {@link UserService}. */
@Module
public static final class UserServiceModule {
private static final UserService userService = UserServiceFactory.getUserService();
@Provides
static UserService provideUserService() {
return userService;
}
}
/** Dagger module that causes the Google GSON parser to be used for Google APIs requests. */
@Module
public static final class GsonModule {
@@ -67,10 +54,7 @@ public final class Modules {
}
}
/**
* Dagger module that provides standard {@link NetHttpTransport}. Used in non App Engine
* environment.
*/
/** Dagger module that provides standard {@link NetHttpTransport}. */
@Module
public static final class NetHttpTransportModule {
@@ -27,7 +27,7 @@ import java.util.Map;
* Utility class to help in dumping routing maps.
*
* <p>Each of the App Engine services (frontend, backend, and tools) has a Dagger component used for
* routing requests (e.g. FrontendRequestComponent). This class produces a text file representation
* routing requests (e.g., FrontendRequestComponent). This class produces a text file representation
* of the routing configuration, showing what paths map to what action classes, as well as the
* properties of the action classes' annotations (which cover things like allowable HTTP methods,
* authentication settings, etc.). The text file can be useful for documentation, and is also used
@@ -37,13 +37,12 @@ import java.util.Map;
* the content to be displayed. The columns are:
*
* <ol>
* <li>the URL path which maps to this action (with a "(*)" after it if the prefix flag is set)
* <li>the simple name of the action class
* <li>the allowable HTTP methods
* <li>whether to automatically print "ok" in the response
* <li>the allowable authentication methods
* <li>the minimum authentication level
* <li>the user policy
* <li>the URL path which maps to this action (with a "(*)" after it if the prefix flag is set)
* <li>the simple name of the action class
* <li>the allowable HTTP methods
* <li>whether to automatically print "ok" in the response
* <li>the minimum authentication level
* <li>the user policy
* </ol>
*
* <p>See the Auth class for more information about authentication settings.
@@ -53,11 +52,9 @@ public class RouterDisplayHelper {
private static final String PATH = "path";
private static final String CLASS = "class";
private static final String METHODS = "methods";
private static final String AUTH_METHODS = "authMethods";
private static final String MINIMUM_LEVEL = "minLevel";
private static final String FORMAT =
"%%-%ds %%-%ds %%-%ds %%-2s %%-%ds %%-%ds %%s";
private static final String FORMAT = "%%-%ds %%-%ds %%-%ds %%-2s %%-%ds %%s";
/** Returns a string representation of the routing map in the specified component. */
public static String extractHumanReadableRoutesFromComponent(Class<?> componentClass) {
@@ -82,7 +79,6 @@ public class RouterDisplayHelper {
columnWidths.get(PATH),
columnWidths.get(CLASS),
columnWidths.get(METHODS),
columnWidths.get(AUTH_METHODS),
columnWidths.get(MINIMUM_LEVEL));
}
@@ -93,7 +89,6 @@ public class RouterDisplayHelper {
"CLASS",
"METHODS",
"OK",
"AUTH_METHODS",
"MIN",
"USER_POLICY");
}
@@ -105,7 +100,6 @@ public class RouterDisplayHelper {
route.actionClass().getSimpleName(),
Joiner.on(",").join(route.action().method()),
route.action().automaticallyPrintOk() ? "y" : "n",
Joiner.on(",").join(route.action().auth().authSettings().methods()),
route.action().auth().authSettings().minimumLevel(),
route.action().auth().authSettings().userPolicy());
}
@@ -116,7 +110,6 @@ public class RouterDisplayHelper {
int pathWidth = 4;
int classWidth = 5;
int methodsWidth = 7;
int authMethodsWidth = 12;
int minLevelWidth = 3;
for (Route route : routes) {
int len =
@@ -134,10 +127,6 @@ public class RouterDisplayHelper {
if (len > methodsWidth) {
methodsWidth = len;
}
len = Joiner.on(",").join(route.action().auth().authSettings().methods()).length();
if (len > authMethodsWidth) {
authMethodsWidth = len;
}
len = route.action().auth().authSettings().minimumLevel().toString().length();
if (len > minLevelWidth) {
minLevelWidth = len;
@@ -149,7 +138,6 @@ public class RouterDisplayHelper {
.put(PATH, pathWidth)
.put(CLASS, classWidth)
.put(METHODS, methodsWidth)
.put(AUTH_METHODS, authMethodsWidth)
.put(MINIMUM_LEVEL, minLevelWidth)
.build());
return headerToString(formatString)
@@ -14,12 +14,8 @@
package google.registry.request.auth;
import com.google.common.collect.ImmutableList;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.AuthSettings.AuthMethod;
import google.registry.request.auth.AuthSettings.UserPolicy;
import google.registry.ui.server.registrar.HtmlAction;
import google.registry.ui.server.registrar.JsonGetAction;
/** Enum used to configure authentication settings for Actions. */
public enum Auth {
@@ -27,35 +23,17 @@ public enum Auth {
/**
* Allows anyone to access.
*
* <p>If a user is logged in, will authenticate (and return) them. Otherwise, access is still
* granted, but NOT_AUTHENTICATED is returned.
*
* <p>User-facing legacy console endpoints (those that extend {@link HtmlAction}) use it. They
* need to allow requests from signed-out users so that they can redirect users to the login page.
* After a user is logged in, they check if the user actually has access to the specific console
* using {@link AuthenticatedRegistrarAccessor}.
*
* @see HtmlAction
* <p>This is used for public HTML endpoints like RDAP, the check API, and web WHOIS.
*/
AUTH_PUBLIC_LEGACY(
ImmutableList.of(AuthMethod.API, AuthMethod.LEGACY), AuthLevel.NONE, UserPolicy.PUBLIC),
AUTH_PUBLIC(AuthLevel.NONE, UserPolicy.PUBLIC),
/**
* Allows anyone to access, as long as they are logged in.
*
* <p>This is used by legacy registrar console programmatic endpoints (those that extend {@link
* JsonGetAction}), which are accessed via XHR requests sent from a logged-in user when performing
* actions on the console.
* <p>Note that the action might use {@link AuthenticatedRegistrarAccessor} to impose a more
* fine-grained access control pattern than merely whether the user is logged in/out.
*/
AUTH_PUBLIC_LOGGED_IN(
ImmutableList.of(AuthMethod.API, AuthMethod.LEGACY), AuthLevel.USER, UserPolicy.PUBLIC),
/**
* Allows anyone to access.
*
* <p>This is used for public HTML endpoints like RDAP, the check API, and web WHOIS.
*/
AUTH_PUBLIC(ImmutableList.of(AuthMethod.API), AuthLevel.NONE, UserPolicy.PUBLIC),
AUTH_PUBLIC_LOGGED_IN(AuthLevel.USER, UserPolicy.PUBLIC),
/**
* Allows only the app itself (via service accounts) or admins to access.
@@ -64,12 +42,12 @@ public enum Auth {
* associated service account needs to be allowlisted in the {@code
* auth.allowedServiceAccountEmails} field in the config YAML file.
*/
AUTH_API_ADMIN(ImmutableList.of(AuthMethod.API), AuthLevel.APP, UserPolicy.ADMIN);
AUTH_ADMIN(AuthLevel.APP, UserPolicy.ADMIN);
private final AuthSettings authSettings;
Auth(ImmutableList<AuthMethod> methods, AuthLevel minimumLevel, UserPolicy userPolicy) {
authSettings = AuthSettings.create(methods, minimumLevel, userPolicy);
Auth(AuthLevel minimumLevel, UserPolicy userPolicy) {
authSettings = new AuthSettings(minimumLevel, userPolicy);
}
public AuthSettings authSettings() {
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.request.auth.AuthSettings.AuthLevel.APP;
import static google.registry.request.auth.AuthSettings.AuthLevel.USER;
import google.registry.model.console.User;
import google.registry.request.auth.AuthSettings.AuthLevel;
import java.util.Optional;
import javax.annotation.Nullable;
@@ -26,24 +27,23 @@ import javax.annotation.Nullable;
* Results of authentication for a given HTTP request, as emitted by an {@link
* AuthenticationMechanism}.
*
* @param userAuthInfo Information about the authenticated user, if there is one.
* @param appServiceAccount Service account email of the authenticated app, if there is one. This
* will be logged upon successful login.
* @param authLevel the level of authentication obtained
* @param user information about the authenticated user, if there is one
* @param serviceAccountEmail service account email of the authenticated app, if there is one
*/
public record AuthResult(
AuthLevel authLevel, Optional<UserAuthInfo> userAuthInfo, Optional<String> appServiceAccount) {
AuthLevel authLevel, Optional<User> user, Optional<String> serviceAccountEmail) {
public boolean isAuthenticated() {
return authLevel() != AuthLevel.NONE;
}
public String userIdForLogging() {
return userAuthInfo()
.map(
userAuthInfo ->
return user.map(
user ->
String.format(
"%s %s",
userAuthInfo.isUserAdmin() ? "admin" : "user", userAuthInfo.getEmailAddress()))
user.getUserRoles().isAdmin() ? "admin" : "user", user.getEmailAddress()))
.orElse("<logged-out user>");
}
@@ -51,22 +51,21 @@ public record AuthResult(
return create(APP, null, email);
}
public static AuthResult createUser(UserAuthInfo userAuthInfo) {
return create(USER, userAuthInfo, null);
public static AuthResult createUser(User user) {
return create(USER, user, null);
}
private static AuthResult create(
AuthLevel authLevel, @Nullable UserAuthInfo userAuthInfo, @Nullable String email) {
AuthLevel authLevel, @Nullable User user, @Nullable String serviceAccountEmail) {
checkArgument(
userAuthInfo == null || email == null,
"User auth info and service account email cannot be specificed at the same time");
user == null || serviceAccountEmail == null,
"User and service account email cannot be specified at the same time");
checkArgument(authLevel != USER || user != null, "User must be specified for auth level USER");
checkArgument(
authLevel != USER || userAuthInfo != null,
"User auth info must be specified for auth level USER");
checkArgument(
authLevel != APP || email != null,
authLevel != APP || serviceAccountEmail != null,
"Service account email must be specified for auth level APP");
return new AuthResult(authLevel, Optional.ofNullable(userAuthInfo), Optional.ofNullable(email));
return new AuthResult(
authLevel, Optional.ofNullable(user), Optional.ofNullable(serviceAccountEmail));
}
/**
@@ -14,7 +14,6 @@
package google.registry.request.auth;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
import google.registry.model.console.UserRoles;
@@ -25,26 +24,7 @@ import google.registry.model.console.UserRoles;
* values.
*/
@Immutable
public record AuthSettings(
ImmutableList<AuthMethod> methods, AuthLevel minimumLevel, UserPolicy userPolicy) {
static AuthSettings create(
ImmutableList<AuthMethod> methods, AuthLevel minimumLevel, UserPolicy userPolicy) {
return new AuthSettings(methods, minimumLevel, userPolicy);
}
/** Available methods for authentication. */
public enum AuthMethod {
/**
* Authentication methods suitable for API-style access, such as {@link
* OidcTokenAuthenticationMechanism}.
*/
API,
/** Legacy authentication using cookie-based App Engine Users API. Must come last if present. */
LEGACY
}
public record AuthSettings(AuthLevel minimumLevel, UserPolicy userPolicy) {
/**
* Authentication level.
@@ -90,16 +70,7 @@ public record AuthSettings(
/** No user policy is enforced; anyone can access this action. */
PUBLIC,
/**
* If there is a user, it must be an admin, as determined by {@link UserAuthInfo#isUserAdmin()}.
*
* <p>Note that, if the user returned is an App Engine {@link
* com.google.appengine.api.users.User} , anybody with access to the app in the GCP Console,
* including editors and viewers, is an admin.
*
* <p>On the other hand, if the user is a {@link google.registry.model.console.User}, the admin
* role is explicitly defined in that object via the {@link UserRoles#isAdmin()} method.
*/
/** If there is a user, it must be an admin, as determined by {@link UserRoles#isAdmin()}. */
ADMIN
}
}
@@ -18,18 +18,16 @@ import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.appengine.api.users.User;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.flogger.FluentLogger;
import dagger.Lazy;
import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GroupsConnection;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.model.registrar.RegistrarPoc;
import java.util.Optional;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@@ -37,24 +35,23 @@ import javax.inject.Inject;
/**
* Allows access only to {@link Registrar}s the current user has access to.
*
* <p>A user has OWNER role on a Registrar if there exists a {@link RegistrarPoc} with that user's
* gaeId and the registrar as a parent.
* <p>A user has OWNER role on a Registrar if there exists a mapping to the registrar in its {@link
* google.registry.model.console.UserRoles} map, regardless of the role.
*
* <p>An "admin" has in addition OWNER role on {@code #registryAdminRegistrarId} and to all
* <p>An "admin" has, in addition, OWNER role on {@code #registryAdminRegistrarId} and to all
* non-{@code REAL} registrars (see {@link Registrar#getType}).
*
* <p>An "admin" also has ADMIN role on ALL registrars.
*
* <p>A user is an "admin" if they are a GAE-admin, or if their email is in the "Support" G Suite
* group.
* <p>A user is an "admin" if it has global admin permission, or if their email is in the "Support"
* G Suite group.
*
* <p>NOTE: to check whether the user is in the "Support" G Suite group, we need a connection to G
* Suite. This in turn requires we have valid JsonCredentials, which not all environments have set
* up. This connection will be created lazily (only if needed).
* Suite. This, in turn, requires us to have valid JsonCredentials, which not all environments have
* set up. This connection will be created lazily (only if needed).
*
* <p>Specifically, we don't instantiate the connection if: (a) gSuiteSupportGroupEmailAddress isn't
* defined, or (b) the user is logged out, or (c) the user is a GAE-admin, or (d) bypassAdminCheck
* is true.
* defined, or (b) the user is logged out, or (c) the user is an admin.
*/
@Immutable
public class AuthenticatedRegistrarAccessor {
@@ -70,8 +67,8 @@ public class AuthenticatedRegistrarAccessor {
private final String userIdForLogging;
/**
* Whether this user is an Admin, meaning either a GAE-admin or a member of the Support G Suite
* group.
* Whether this user is an admin, meaning either they have global admin permission or a member of
* the Support G Suite group.
*/
private final boolean isAdmin;
@@ -84,26 +81,6 @@ public class AuthenticatedRegistrarAccessor {
*/
private final ImmutableSetMultimap<String, Role> roleMap;
/**
* Bypass the "isAdmin" check making all users NOT admins.
*
* <p>Currently our test server doesn't let you change the user after the test server was created.
* This means we'd need multiple test files to test the same actions as both a "regular" user and
* an admin.
*
* <p>To overcome this - we add a flag that lets you dynamically choose whether a user is an admin
* or not by creating a fake "GAE-admin" user and then bypassing the admin check if they want to
* fake a "regular" user.
*
* <p>The reason we don't do it the other way around (have a flag that makes anyone an admin) is
* that such a flag would be a security risk, especially since VisibleForTesting is unenforced
* (and you could set it with reflection anyway).
*
* <p>Instead of having a test flag that elevates permissions (which has security concerns) we add
* this flag that reduces permissions.
*/
@VisibleForTesting public static boolean bypassAdminCheck = false;
@Inject
public AuthenticatedRegistrarAccessor(
AuthResult authResult,
@@ -140,9 +117,7 @@ public class AuthenticatedRegistrarAccessor {
return new AuthenticatedRegistrarAccessor("TestUserId", isAdmin, roleMap);
}
/**
* Returns whether this user is allowed to create new Registrars and TLDs.
*/
/** Returns whether this user is allowed to create new Registrars and TLDs. */
public boolean isAdmin() {
return isAdmin;
}
@@ -282,53 +257,39 @@ public class AuthenticatedRegistrarAccessor {
AuthResult authResult,
Optional<String> gSuiteSupportGroupEmailAddress,
Lazy<GroupsConnection> lazyGroupsConnection) {
if (authResult.userAuthInfo().isEmpty()) {
if (authResult.user().isEmpty()) {
return false;
}
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
// both GAE project admin and members of the gSuiteSupportGroupEmailAddress are considered
// admins for the RegistrarConsole.
return !bypassAdminCheck
&& (userAuthInfo.isUserAdmin()
|| checkIsSupport(
lazyGroupsConnection,
userAuthInfo.getEmailAddress(),
gSuiteSupportGroupEmailAddress));
User user = authResult.user().get();
// both user object with admin permission and members of the gSuiteSupportGroupEmailAddress are
// considered admins for the RegistrarConsole.
return user.getUserRoles().isAdmin()
|| checkIsSupport(
lazyGroupsConnection, user.getEmailAddress(), gSuiteSupportGroupEmailAddress);
}
/** Returns a map of registrar IDs to roles for all registrars that the user has access to. */
private static ImmutableSetMultimap<String, Role> createRoleMap(
AuthResult authResult, boolean isAdmin, String registryAdminRegistrarId) {
if (authResult.userAuthInfo().isEmpty()) {
if (authResult.user().isEmpty()) {
return ImmutableSetMultimap.of();
}
ImmutableSetMultimap.Builder<String, Role> builder = new ImmutableSetMultimap.Builder<>();
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
if (userAuthInfo.appEngineUser().isPresent()) {
User user = userAuthInfo.appEngineUser().get();
logger.atInfo().log("Checking registrar contacts for user ID %s.", user.getEmail());
// Find all registrars that have a registrar contact with this user's ID.
tm().transact(
() ->
tm().query(
"SELECT r FROM Registrar r INNER JOIN RegistrarPoc rp ON r.registrarId ="
+ " rp.registrarId WHERE lower(rp.loginEmailAddress) = :email AND"
+ " r.state != :state",
Registrar.class)
.setParameter("email", Ascii.toLowerCase(user.getEmail()))
.setParameter("state", State.DISABLED)
.getResultStream()
.forEach(registrar -> builder.put(registrar.getRegistrarId(), Role.OWNER)));
} else {
userAuthInfo
.consoleUser()
.get()
.getUserRoles()
.getRegistrarRoles()
.forEach((k, v) -> builder.put(k, Role.OWNER));
}
authResult
.user()
.get()
.getUserRoles()
.getRegistrarRoles()
.forEach(
(k, v) ->
Registrar.loadByRegistrarId(k)
.ifPresent(
registrar -> {
if (registrar.getState() != State.DISABLED) {
builder.put(k, Role.OWNER);
}
}));
// Admins have ADMIN access to all registrars, and also OWNER access to the registry registrar
// and all non-REAL or non-live registrars.
@@ -1,84 +0,0 @@
// Copyright 2017 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.request.auth;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.base.Strings.nullToEmpty;
import static google.registry.request.auth.AuthResult.NOT_AUTHENTICATED;
import static google.registry.security.XsrfTokenManager.P_CSRF_TOKEN;
import static google.registry.security.XsrfTokenManager.X_CSRF_TOKEN;
import com.google.appengine.api.users.UserService;
import com.google.common.collect.ImmutableSet;
import google.registry.security.XsrfTokenManager;
import jakarta.servlet.http.HttpServletRequest;
import javax.inject.Inject;
/**
* Authentication mechanism for legacy cookie-based App Engine authentication.
*
* <p>Just use the values returned by UserService.
*/
public class LegacyAuthenticationMechanism implements AuthenticationMechanism {
private final UserService userService;
private final XsrfTokenManager xsrfTokenManager;
/** HTTP methods which are considered safe, and do not require XSRF protection. */
private static final ImmutableSet<String> SAFE_METHODS = ImmutableSet.of("GET", "HEAD");
@Inject
public LegacyAuthenticationMechanism(UserService userService, XsrfTokenManager xsrfTokenManager) {
this.userService = userService;
this.xsrfTokenManager = xsrfTokenManager;
}
@Override
public AuthResult authenticate(HttpServletRequest request) {
if (!userService.isUserLoggedIn()) {
return NOT_AUTHENTICATED;
}
if (!SAFE_METHODS.contains(request.getMethod()) && !validateXsrf(request)) {
return NOT_AUTHENTICATED;
}
return AuthResult.createUser(
UserAuthInfo.create(userService.getCurrentUser(), userService.isUserAdmin()));
}
private boolean validateXsrf(HttpServletRequest request) {
String headerToken = emptyToNull(request.getHeader(X_CSRF_TOKEN));
if (headerToken != null) {
return xsrfTokenManager.validateToken(headerToken);
}
// If we got here - the header didn't have the token.
// It might be in the POST data - however even checking whether the POST data has this entry
// could break the Action!
//
// Reason: if we do request.getParameter, any Action that injects @Payload or @JsonPayload
// would break since it uses request.getReader - and it's an error to call both getReader and
// getParameter!
//
// However, in this case it's acceptable since if we got here - the POST request didn't even
// have the XSRF header meaning if it doesn't have POST data - it's not from a valid source at
// all (a valid but outdated source would have a bad header value, but getting here means we had
// no value at all)
//
// TODO(b/120201577): Once we know from the @Action whether we can use getParameter or not -
// only check getParameter if that's how this @Action uses getParameters.
return xsrfTokenManager.validateToken(nullToEmpty(request.getParameter(P_CSRF_TOKEN)));
}
}
@@ -14,6 +14,8 @@
package google.registry.request.auth;
import static com.google.common.base.Preconditions.checkState;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.auth.oauth2.TokenVerifier;
import com.google.common.annotations.VisibleForTesting;
@@ -117,7 +119,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
}
Optional<User> maybeUser = UserDao.loadUser(email);
if (maybeUser.isPresent()) {
return AuthResult.createUser(UserAuthInfo.create(maybeUser.get()));
return AuthResult.createUser(maybeUser.get());
}
logger.atInfo().log("No end user found for email address %s", email);
if (serviceAccountEmails.stream().anyMatch(e -> e.equals(email))) {
@@ -131,11 +133,17 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
@VisibleForTesting
public static void setAuthResultForTesting(@Nullable AuthResult authResult) {
checkState(
RegistryEnvironment.get() == RegistryEnvironment.UNITTEST,
"Explicitly setting auth result is only supported in tests");
authResultForTesting = authResult;
}
@VisibleForTesting
public static void unsetAuthResultForTesting() {
checkState(
RegistryEnvironment.get() == RegistryEnvironment.UNITTEST,
"Explicitly unsetting auth result is only supported in tests");
authResultForTesting = null;
}
@@ -21,9 +21,7 @@ import static google.registry.request.auth.AuthSettings.AuthLevel.USER;
import static google.registry.request.auth.AuthSettings.UserPolicy.ADMIN;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import com.google.common.flogger.FluentLogger;
import google.registry.request.auth.AuthSettings.AuthMethod;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import javax.inject.Inject;
@@ -32,16 +30,12 @@ import javax.inject.Inject;
public class RequestAuthenticator {
private final ImmutableList<AuthenticationMechanism> apiAuthenticationMechanisms;
private final LegacyAuthenticationMechanism legacyAuthenticationMechanism;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject
public RequestAuthenticator(
ImmutableList<AuthenticationMechanism> apiAuthenticationMechanisms,
LegacyAuthenticationMechanism legacyAuthenticationMechanism) {
public RequestAuthenticator(ImmutableList<AuthenticationMechanism> apiAuthenticationMechanisms) {
this.apiAuthenticationMechanisms = apiAuthenticationMechanisms;
this.legacyAuthenticationMechanism = legacyAuthenticationMechanism;
}
/**
@@ -66,8 +60,8 @@ public class RequestAuthenticator {
return Optional.empty();
}
if (auth.userPolicy() == ADMIN
&& authResult.userAuthInfo().isPresent()
&& !authResult.userAuthInfo().get().isUserAdmin()) {
&& authResult.user().isPresent()
&& !authResult.user().get().getUserRoles().isAdmin()) {
logger.atWarning().log(
"Not authorized; user policy is ADMIN, but the user was not an admin.");
return Optional.empty();
@@ -84,28 +78,13 @@ public class RequestAuthenticator {
*/
AuthResult authenticate(AuthSettings auth, HttpServletRequest req) {
checkAuthConfig(auth);
for (AuthMethod authMethod : auth.methods()) {
AuthResult authResult;
switch (authMethod) {
// API-based user authentication mechanisms, such as OIDC.
case API -> {
for (AuthenticationMechanism authMechanism : apiAuthenticationMechanisms) {
authResult = authMechanism.authenticate(req);
if (authResult.isAuthenticated()) {
logger.atInfo().log(
"Authenticated via %s: %s", authMechanism.getClass().getSimpleName(), authResult);
return authResult;
}
}
}
// Legacy authentication via UserService
case LEGACY -> {
authResult = legacyAuthenticationMechanism.authenticate(req);
if (authResult.isAuthenticated()) {
logger.atInfo().log("Authenticated via legacy auth: %s", authResult);
return authResult;
}
}
AuthResult authResult;
for (AuthenticationMechanism authMechanism : apiAuthenticationMechanisms) {
authResult = authMechanism.authenticate(req);
if (authResult.isAuthenticated()) {
logger.atInfo().log(
"Authenticated via %s: %s", authMechanism.getClass().getSimpleName(), authResult);
return authResult;
}
}
logger.atInfo().log("No authentication found.");
@@ -114,10 +93,6 @@ public class RequestAuthenticator {
/** Validates an AuthSettings object, checking for invalid setting combinations. */
static void checkAuthConfig(AuthSettings auth) {
checkArgument(!auth.methods().isEmpty(), "Must specify at least one auth method");
checkArgument(
Ordering.explicit(AuthMethod.API, AuthMethod.LEGACY).isStrictlyOrdered(auth.methods()),
"Auth methods must be unique and strictly in order - API, LEGACY");
checkArgument(
(auth.minimumLevel() != NONE) || (auth.userPolicy() != ADMIN),
"Actions with minimal auth level at NONE should not specify ADMIN user policy");
@@ -1,53 +0,0 @@
// Copyright 2017 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.request.auth;
import com.google.appengine.api.users.User;
import java.util.Optional;
/**
* Extra information provided by the authentication mechanism about the user.
*
* @param appEngineUser User object from the AppEngine Users API.
* @param isUserAdmin Whether the user is an admin.
* <p>Note that, in App Engine parlance, an admin is any user who is a project owner, editor, OR
* viewer (as well as the specific role App Engine Admin). So even users with read-only access
* to the App Engine product qualify as an "admin".
*/
public record UserAuthInfo(
Optional<google.registry.model.console.User> consoleUser,
Optional<User> appEngineUser,
boolean isUserAdmin) {
public String getEmailAddress() {
return appEngineUser()
.map(User::getEmail)
.orElseGet(() -> consoleUser().get().getEmailAddress());
}
public String getUsername() {
return appEngineUser()
.map(User::getNickname)
.orElseGet(() -> consoleUser().get().getEmailAddress());
}
public static UserAuthInfo create(User user, boolean isUserAdmin) {
return new UserAuthInfo(Optional.empty(), Optional.of(user), isUserAdmin);
}
public static UserAuthInfo create(google.registry.model.console.User user) {
return new UserAuthInfo(Optional.of(user), Optional.empty(), user.getUserRoles().isAdmin());
}
}
@@ -19,7 +19,6 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.time.DateTimeZone.UTC;
import com.google.appengine.api.users.UserService;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.flogger.FluentLogger;
@@ -49,12 +48,10 @@ public final class XsrfTokenManager {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Clock clock;
private final UserService userService;
@Inject
public XsrfTokenManager(Clock clock, UserService userService) {
public XsrfTokenManager(Clock clock) {
this.clock = clock;
this.userService = userService;
}
/** Generates an XSRF token for a given user based on email address. */
@@ -81,7 +78,7 @@ public final class XsrfTokenManager {
}
/** Validates an XSRF token against the current logged-in user. */
public boolean validateToken(String token) {
public boolean validateToken(String email, String token) {
checkArgumentNotNull(token);
List<String> tokenParts = Splitter.on(':').splitToList(token);
if (tokenParts.size() != 3) {
@@ -104,12 +101,8 @@ public final class XsrfTokenManager {
logger.atInfo().log("Expired timestamp in XSRF token: %s", token);
return false;
}
String currentUserEmail =
userService.isUserLoggedIn() ? userService.getCurrentUser().getEmail() : "";
// Reconstruct the token to verify validity.
String reconstructedToken =
encodeToken(ServerSecret.get().asBytes(), currentUserEmail, timestampMillis);
String reconstructedToken = encodeToken(ServerSecret.get().asBytes(), email, timestampMillis);
if (!token.equals(reconstructedToken)) {
logger.atWarning().log(
"Reconstructed XSRF mismatch (got != expected): %s != %s", token, reconstructedToken);
@@ -70,7 +70,7 @@ import org.joda.time.Duration;
path = NordnUploadAction.PATH,
method = Action.Method.POST,
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class NordnUploadAction implements Runnable {
static final String PATH = "/_dr/task/nordnUpload";
@@ -54,7 +54,7 @@ import javax.inject.Inject;
path = NordnVerifyAction.PATH,
method = Action.Method.POST,
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class NordnVerifyAction implements Runnable {
static final String PATH = "/_dr/task/nordnVerify";
@@ -32,7 +32,7 @@ import javax.inject.Inject;
path = "/_dr/task/tmchCrl",
method = POST,
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class TmchCrlAction implements Runnable {
@Inject Marksdb marksdb;
@@ -35,7 +35,7 @@ import org.bouncycastle.openpgp.PGPException;
path = "/_dr/task/tmchDnl",
method = POST,
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class TmchDnlAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -34,7 +34,7 @@ import org.bouncycastle.openpgp.PGPException;
path = "/_dr/task/tmchSmdrl",
method = POST,
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class TmchSmdrlAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -17,19 +17,32 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.tools.server.UpdateUserGroupAction;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
/** Command to create a new User. */
@Parameters(separators = " =", commandDescription = "Update a user account")
public class CreateUserCommand extends CreateOrUpdateUserCommand {
public class CreateUserCommand extends CreateOrUpdateUserCommand implements CommandWithConnection {
static final String IAP_SECURED_WEB_APP_USER_ROLE = "roles/iap.httpsResourceAccessor";
static final FluentLogger logger = FluentLogger.forEnclosingClass();
private ServiceConnection connection;
@Inject IamClient iamClient;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@Nullable
@Override
User getExistingUser(String email) {
@@ -40,7 +53,29 @@ public class CreateUserCommand extends CreateOrUpdateUserCommand {
@Override
protected String execute() throws Exception {
String ret = super.execute();
iamClient.addBinding(email, IAP_SECURED_WEB_APP_USER_ROLE);
String groupEmailAddress = maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress != null) {
logger.atInfo().log("Adding %s to group %s", email, groupEmailAddress);
connection.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
email,
"groupEmailAddress",
groupEmailAddress,
"groupUpdateMode",
"ADD"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
} else {
logger.atInfo().log("Granting IAP role to user %s", email);
iamClient.addBinding(email, IAP_SECURED_WEB_APP_USER_ROLE);
}
return ret;
}
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
}
@@ -21,22 +21,39 @@ import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.tools.server.UpdateUserGroupAction;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
/** Deletes a {@link User}. */
@Parameters(separators = " =", commandDescription = "Delete a user account")
public class DeleteUserCommand extends ConfirmingCommand {
public class DeleteUserCommand extends ConfirmingCommand implements CommandWithConnection {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private ServiceConnection connection;
@Inject IamClient iamClient;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@Nullable
@Parameter(names = "--email", description = "Email address of the user", required = true)
String email;
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@Override
protected String prompt() {
checkArgumentNotNull(email, "Email must be provided");
@@ -52,7 +69,24 @@ public class DeleteUserCommand extends ConfirmingCommand {
checkArgumentPresent(optionalUser, "Email no longer corresponds to a valid user");
tm().delete(optionalUser.get());
});
iamClient.removeBinding(email, IAP_SECURED_WEB_APP_USER_ROLE);
String groupEmailAddress = maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress != null) {
logger.atInfo().log("Removing %s from group %s", email, groupEmailAddress);
connection.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
email,
"groupEmailAddress",
groupEmailAddress,
"groupUpdateMode",
"REMOVE"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
} else {
logger.atInfo().log("Removing IAP role from user %s", email);
iamClient.removeBinding(email, IAP_SECURED_WEB_APP_USER_ROLE);
}
return String.format("Deleted user with email %s", email);
}
}
@@ -39,7 +39,6 @@ import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.rde.RdeModule;
import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.tools.AuthModule.LocalCredentialModule;
import google.registry.util.UtilsModule;
import google.registry.whois.NonCachingWhoisModule;
@@ -75,7 +74,6 @@ import javax.inject.Singleton;
SecretManagerKeyringModule.class,
SecretManagerModule.class,
UrlConnectionServiceModule.class,
UserServiceModule.class,
UtilsModule.class,
VoidDnsWriterModule.class,
NonCachingWhoisModule.class
@@ -43,7 +43,7 @@ import javax.inject.Inject;
service = Action.Service.TOOLS,
path = CreateGroupsAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class CreateGroupsAction implements Runnable {
public static final String PATH = "/_dr/admin/createGroups";
@@ -65,7 +65,7 @@ import org.joda.time.Duration;
service = Action.Service.TOOLS,
path = GenerateZoneFilesAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class GenerateZoneFilesAction implements Runnable, JsonActionRunner.JsonAction {
private static final FluentLogger log = FluentLogger.forEnclosingClass();
@@ -39,7 +39,7 @@ import javax.inject.Inject;
service = Action.Service.TOOLS,
path = ListDomainsAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class ListDomainsAction extends ListObjectsAction<Domain> {
/** An App Engine limitation on how many subqueries can be used in a single query. */
@@ -34,7 +34,7 @@ import org.joda.time.DateTime;
service = Action.Service.TOOLS,
path = ListHostsAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class ListHostsAction extends ListObjectsAction<Host> {
public static final String PATH = "/_dr/admin/list/hosts";
@@ -35,7 +35,7 @@ import javax.inject.Inject;
service = Action.Service.TOOLS,
path = ListPremiumListsAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class ListPremiumListsAction extends ListObjectsAction<PremiumList> {
public static final String PATH = "/_dr/admin/list/premiumLists";
@@ -30,7 +30,7 @@ import javax.inject.Inject;
service = Action.Service.TOOLS,
path = ListRegistrarsAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class ListRegistrarsAction extends ListObjectsAction<Registrar> {
public static final String PATH = "/_dr/admin/list/registrars";
@@ -33,7 +33,7 @@ import javax.inject.Inject;
service = Action.Service.TOOLS,
path = ListReservedListsAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class ListReservedListsAction extends ListObjectsAction<ReservedList> {
public static final String PATH = "/_dr/admin/list/reservedLists";
@@ -34,7 +34,7 @@ import org.joda.time.DateTime;
service = Action.Service.TOOLS,
path = ListTldsAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public final class ListTldsAction extends ListObjectsAction<Tld> {
public static final String PATH = "/_dr/admin/list/tlds";
@@ -54,7 +54,7 @@ import org.joda.time.Duration;
@Action(
service = Action.Service.TOOLS,
path = "/_dr/task/refreshDnsForAllDomains",
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class RefreshDnsForAllDomainsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -23,12 +23,11 @@ import static google.registry.request.RequestParameters.extractRequiredParameter
import dagger.Module;
import dagger.Provides;
import google.registry.request.Parameter;
import google.registry.tools.server.UpdateUserGroupAction.Mode;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
/**
* Dagger module for the tools package.
*/
/** Dagger module for the tools package. */
@Module
public class ToolsServerModule {
@@ -75,4 +74,21 @@ public class ToolsServerModule {
static Optional<Integer> provideRefreshQps(HttpServletRequest req) {
return extractOptionalIntParameter(req, "refreshQps");
}
@Provides
static Mode provideGroupUpdateMode(HttpServletRequest req) {
return Mode.valueOf(extractRequiredParameter(req, "groupUpdateMode"));
}
@Provides
@Parameter("userEmailAddress")
static String provideUserEmailAddress(HttpServletRequest req) {
return extractRequiredParameter(req, "userEmailAddress");
}
@Provides
@Parameter("groupEmailAddress")
static String provideGroupEmailAddress(HttpServletRequest req) {
return extractRequiredParameter(req, "groupEmailAddress");
}
}
@@ -0,0 +1,83 @@
// 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.tools.server;
import static google.registry.request.Action.Method.POST;
import com.google.common.flogger.FluentLogger;
import google.registry.groups.GroupsConnection;
import google.registry.groups.GroupsConnection.Role;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import javax.inject.Inject;
/** Action that adds or deletes a console user to/from the group that has IAP permissions. */
@Action(
service = Action.Service.TOOLS,
path = UpdateUserGroupAction.PATH,
method = POST,
auth = Auth.AUTH_ADMIN)
public class UpdateUserGroupAction implements Runnable {
public static final String PATH = "/_dr/admin/updateUserGroup";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject GroupsConnection groupsConnection;
@Inject Response response;
@Inject
@Parameter("userEmailAddress")
String userEmailAddress;
@Inject
@Parameter("groupEmailAddress")
String groupEmailAddress;
@Inject Mode mode;
@Inject
UpdateUserGroupAction() {}
enum Mode {
ADD,
REMOVE
}
@Override
public void run() {
logger.atInfo().log(
"Updating group %s: %s user %s",
groupEmailAddress, mode == Mode.ADD ? "adding" : "removing", userEmailAddress);
try {
if (mode == Mode.ADD) {
// The group will be created if it does not exist.
groupsConnection.addMemberToGroup(groupEmailAddress, userEmailAddress, Role.MEMBER);
} else {
if (groupsConnection.isMemberOfGroup(userEmailAddress, groupEmailAddress)) {
groupsConnection.removeMemberFromGroup(groupEmailAddress, userEmailAddress);
} else {
logger.atInfo().log(
"Ignoring request to remove non-member %s from group %s",
userEmailAddress, groupEmailAddress);
}
}
} catch (Exception e) {
throw new RuntimeException("Cannot update group", e);
}
}
}
@@ -37,7 +37,7 @@ import javax.inject.Inject;
service = Action.Service.TOOLS,
path = VerifyOteAction.PATH,
method = Action.Method.POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class VerifyOteAction implements Runnable, JsonAction {
public static final String PATH = "/_dr/admin/verifyOte";
@@ -26,7 +26,6 @@ import google.registry.model.console.ConsolePermission;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.request.HttpException;
import google.registry.request.auth.AuthResult;
import google.registry.security.XsrfTokenManager;
import google.registry.ui.server.registrar.ConsoleApiParams;
import google.registry.ui.server.registrar.ConsoleUiAction;
@@ -50,13 +49,11 @@ public abstract class ConsoleApiAction implements Runnable {
@Override
public final void run() {
// Shouldn't be even possible because of Auth annotations on the various implementing classes
AuthResult authResult = consoleApiParams.authResult();
if (authResult.userAuthInfo().isEmpty()
|| authResult.userAuthInfo().get().consoleUser().isEmpty()) {
if (consoleApiParams.authResult().user().isEmpty()) {
consoleApiParams.response().setStatus(SC_UNAUTHORIZED);
return;
}
User user = consoleApiParams.authResult().userAuthInfo().get().consoleUser().get();
User user = consoleApiParams.authResult().user().get();
// This allows us to enable console to a selected cohort of users with release
// We can ignore it in tests
@@ -74,7 +71,7 @@ public abstract class ConsoleApiAction implements Runnable {
if (consoleApiParams.request().getMethod().equals(GET.toString())) {
getHandler(user);
} else {
if (verifyXSRF()) {
if (verifyXSRF(user)) {
postHandler(user);
}
}
@@ -112,13 +109,15 @@ public abstract class ConsoleApiAction implements Runnable {
consoleApiParams.response().setPayload(message);
}
private boolean verifyXSRF() {
private boolean verifyXSRF(User user) {
Optional<Cookie> maybeCookie =
Arrays.stream(consoleApiParams.request().getCookies())
.filter(c -> XsrfTokenManager.X_CSRF_TOKEN.equals(c.getName()))
.findFirst();
if (maybeCookie.isEmpty()
|| !consoleApiParams.xsrfTokenManager().validateToken(maybeCookie.get().getValue())) {
|| !consoleApiParams
.xsrfTokenManager()
.validateToken(user.getEmailAddress(), maybeCookie.get().getValue())) {
consoleApiParams.response().setStatus(SC_UNAUTHORIZED);
return false;
}
@@ -0,0 +1,145 @@
// 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.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import static org.apache.http.HttpStatus.SC_OK;
import com.google.common.base.Strings;
import google.registry.groups.GmailClient;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.ui.server.registrar.ConsoleApiParams;
import google.registry.util.DomainNameUtils;
import google.registry.util.EmailMessage;
import google.registry.util.RegistryEnvironment;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
@Action(
service = Action.Service.DEFAULT,
path = ConsoleEppPasswordAction.PATH,
method = {POST},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
static final String PATH = "/console-api/registrar";
private static final String EMAIL_SUBJ = "Registrar %s has been updated";
private static final String EMAIL_BODY =
"The following changes were made in registry %s environment to the registrar %s:";
private final Optional<Registrar> registrar;
private final GmailClient gmailClient;
@Inject
ConsoleUpdateRegistrarAction(
ConsoleApiParams consoleApiParams,
GmailClient gmailClient,
@Parameter("registrar") Optional<Registrar> registrar) {
super(consoleApiParams);
this.registrar = registrar;
this.gmailClient = gmailClient;
}
@Override
protected void postHandler(User user) {
var errorMsg = "Missing param(s): %s";
Registrar updatedRegistrar =
registrar.orElseThrow(() -> new BadRequestException(String.format(errorMsg, "registrar")));
checkArgument(
!Strings.isNullOrEmpty(updatedRegistrar.getRegistrarId()), errorMsg, "registrarId");
checkPermission(
user, updatedRegistrar.getRegistrarId(), ConsolePermission.EDIT_REGISTRAR_DETAILS);
tm().transact(
() -> {
Optional<Registrar> existingRegistrar =
Registrar.loadByRegistrarId(updatedRegistrar.getRegistrarId());
checkArgument(
!existingRegistrar.isEmpty(),
"Registrar with registrarId %s doesn't exists",
updatedRegistrar.getRegistrarId());
// Only allow modifying allowed TLDs if we're in a non-PRODUCTION environment, if the
// registrar is not REAL, or the registrar has a WHOIS abuse contact set.
if (!updatedRegistrar.getAllowedTlds().isEmpty()) {
boolean isRealRegistrar =
Registrar.Type.REAL.equals(existingRegistrar.get().getType());
if (RegistryEnvironment.PRODUCTION.equals(RegistryEnvironment.get())
&& isRealRegistrar) {
checkArgumentPresent(
existingRegistrar.get().getWhoisAbuseContact(),
"Cannot modify allowed TLDs if there is no WHOIS abuse contact set. Please"
+ " use the \"nomulus registrar_contact\" command on this registrar to"
+ " set a WHOIS abuse contact.");
}
}
tm().put(
existingRegistrar
.get()
.asBuilder()
.setAllowedTlds(
updatedRegistrar.getAllowedTlds().stream()
.map(DomainNameUtils::canonicalizeHostname)
.collect(Collectors.toSet()))
.setRegistryLockAllowed(updatedRegistrar.isRegistryLockAllowed())
.build());
sendEmail(existingRegistrar.get(), updatedRegistrar);
});
consoleApiParams.response().setStatus(SC_OK);
}
void sendEmail(Registrar oldRegistrar, Registrar updatedRegistrar) throws AddressException {
String emailBody =
String.format(EMAIL_BODY, RegistryEnvironment.get(), oldRegistrar.getRegistrarId());
StringBuilder diff = new StringBuilder();
if (oldRegistrar.isRegistryLockAllowed() != updatedRegistrar.isRegistryLockAllowed()) {
diff.append("/n");
diff.append(
String.format(
"Registry Lock Allowed: %s -> %s",
oldRegistrar.isRegistryLockAllowed(), updatedRegistrar.isRegistryLockAllowed()));
}
if (!oldRegistrar.getAllowedTlds().equals(updatedRegistrar.getAllowedTlds())) {
diff.append("/n");
diff.append(
String.format(
"Allowed TLDs: %s -> %s",
oldRegistrar.getAllowedTlds(), updatedRegistrar.getAllowedTlds()));
}
if (diff.length() > 0) {
this.gmailClient.sendEmail(
EmailMessage.create(
String.format(EMAIL_SUBJ, oldRegistrar.getRegistrarId()),
emailBody + diff,
new InternetAddress(oldRegistrar.getEmailAddress(), true)));
}
}
}
@@ -58,7 +58,7 @@ public class ConsoleUserDataAction extends ConsoleApiAction {
@Override
protected void getHandler(User user) {
// As this is a first GET request we use it as an opportunity to set a XSRF cookie
// As this is the first GET request, we use it as an opportunity to set a XSRF cookie
// for angular to read - https://angular.io/guide/http-security-xsrf-protection
Cookie xsrfCookie =
new Cookie(
@@ -54,7 +54,7 @@ import javax.inject.Named;
service = Action.Service.DEFAULT,
path = ConsoleOteSetupAction.PATH,
method = {Method.POST, Method.GET},
auth = Auth.AUTH_PUBLIC_LEGACY)
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public final class ConsoleOteSetupAction extends HtmlAction {
public static final String PATH = "/registrar-ote-setup";
@@ -64,7 +64,7 @@ import org.joda.money.CurrencyUnit;
service = Service.DEFAULT,
path = ConsoleRegistrarCreatorAction.PATH,
method = {Method.POST, Method.GET},
auth = Auth.AUTH_PUBLIC_LEGACY)
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public final class ConsoleRegistrarCreatorAction extends HtmlAction {
private static final int PASSWORD_LENGTH = 16;
@@ -43,7 +43,7 @@ import javax.inject.Inject;
@Action(
service = Action.Service.DEFAULT,
path = ConsoleUiAction.PATH,
auth = Auth.AUTH_PUBLIC_LEGACY)
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public final class ConsoleUiAction extends HtmlAction {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -14,19 +14,17 @@
package google.registry.ui.server.registrar;
import static com.google.common.net.HttpHeaders.LOCATION;
import static com.google.common.net.HttpHeaders.X_FRAME_OPTIONS;
import static jakarta.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.appengine.api.users.UserService;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.request.Action;
import google.registry.request.RequestMethod;
import google.registry.request.Response;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
import google.registry.security.XsrfTokenManager;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
@@ -45,7 +43,6 @@ public abstract class HtmlAction implements Runnable {
@Inject HttpServletRequest req;
@Inject Response response;
@Inject UserService userService;
@Inject XsrfTokenManager xsrfTokenManager;
@Inject AuthResult authResult;
@Inject @RequestMethod Action.Method method;
@@ -67,34 +64,21 @@ public abstract class HtmlAction implements Runnable {
response.setHeader(X_FRAME_OPTIONS, "SAMEORIGIN"); // Disallow iframing.
response.setHeader("X-Ui-Compatible", "IE=edge"); // Ask IE not to be silly.
if (authResult.userAuthInfo().isEmpty()) {
response.setStatus(SC_MOVED_TEMPORARILY);
String location;
try {
location = userService.createLoginURL(req.getRequestURI());
} catch (IllegalArgumentException e) {
// UserServiceImpl.createLoginURL() throws IllegalArgumentException if underlying API call
// returns an error code of NOT_ALLOWED. createLoginURL() assumes that the error is caused
// by an invalid URL. But in fact, the error can also occur if UserService doesn't have any
// user information, which happens when the request has been authenticated as internal. In
// this case, we want to avoid dying before we can send the redirect, so just redirect to
// the root path.
location = "/";
}
response.setHeader(LOCATION, location);
if (authResult.user().isEmpty()) {
response.setStatus(SC_UNAUTHORIZED);
return;
}
response.setContentType(MediaType.HTML_UTF_8);
UserAuthInfo authInfo = authResult.userAuthInfo().get();
User user = authResult.user().get();
// Using HashMap to allow null values
HashMap<String, Object> data = new HashMap<>();
data.put("logoFilename", logoFilename);
data.put("productName", productName);
data.put("username", authInfo.getUsername());
data.put("logoutUrl", userService.createLogoutURL(getPath()));
data.put("username", user.getEmailAddress());
data.put("logoutUrl", "/registrar?gcp-iap-mode=CLEAR_LOGIN_COOKIE");
data.put("analyticsConfig", analyticsConfig);
data.put("xsrfToken", xsrfTokenManager.generateToken(authInfo.getEmailAddress()));
data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmailAddress()));
logger.atInfo().log(
"User %s is accessing %s with method %s.",
@@ -22,18 +22,16 @@ import static google.registry.ui.server.registrar.RegistrarConsoleModule.PARAM_C
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import com.google.appengine.api.users.User;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.domain.RegistryLock;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.tld.RegistryLockDao;
import google.registry.request.Action;
import google.registry.request.Action.Method;
@@ -44,9 +42,7 @@ import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.request.auth.UserAuthInfo;
import google.registry.security.JsonResponseHelper;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
import org.joda.time.DateTime;
@@ -101,7 +97,7 @@ public final class RegistryLockGetAction implements JsonGetAction {
@Override
public void run() {
checkArgument(Method.GET.equals(method), "Only GET requests allowed");
checkArgument(authResult.userAuthInfo().isPresent(), "User auth info must be present");
checkArgument(authResult.user().isPresent(), "User must be present");
checkArgument(paramClientId.isPresent(), "clientId must be present");
response.setContentType(MediaType.JSON_UTF_8);
@@ -121,29 +117,7 @@ public final class RegistryLockGetAction implements JsonGetAction {
}
}
static Optional<RegistrarPoc> getContactMatchingLogin(User user, Registrar registrar) {
ImmutableList<RegistrarPoc> matchingContacts =
registrar.getContacts().stream()
.filter(contact -> contact.getLoginEmailAddress() != null)
.filter(
contact ->
Objects.equals(
Ascii.toLowerCase(contact.getLoginEmailAddress()),
Ascii.toLowerCase(user.getEmail())))
.collect(toImmutableList());
if (matchingContacts.size() > 1) {
ImmutableList<String> matchingEmails =
matchingContacts.stream().map(RegistrarPoc::getEmailAddress).collect(toImmutableList());
throw new IllegalArgumentException(
String.format(
"User with login email %s had multiple matching contacts with contact email addresses"
+ " %s",
user.getEmail(), matchingEmails));
}
return matchingContacts.stream().findFirst();
}
static Registrar getRegistrarAndVerifyLockAccess(
static void verifyLockAccess(
AuthenticatedRegistrarAccessor registrarAccessor, String clientId, boolean isAdmin)
throws RegistrarAccessDeniedException {
Registrar registrar = registrarAccessor.getRegistrar(clientId);
@@ -151,37 +125,22 @@ public final class RegistryLockGetAction implements JsonGetAction {
isAdmin || registrar.isRegistryLockAllowed(),
"Registry lock not allowed for registrar %s",
clientId);
return registrar;
}
private ImmutableMap<String, ?> getLockedDomainsMap(String registrarId)
throws RegistrarAccessDeniedException {
// Note: admins always have access to the locks page
checkArgument(authResult.userAuthInfo().isPresent(), "User auth info must be present");
checkArgument(authResult.user().isPresent(), "User must be present");
boolean isAdmin = registrarAccessor.isAdmin();
Registrar registrar = getRegistrarAndVerifyLockAccess(registrarAccessor, registrarId, isAdmin);
verifyLockAccess(registrarAccessor, registrarId, isAdmin);
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
User user = authResult.user().get();
// Split logic depending on whether we are using the old auth system or the new one
boolean isRegistryLockAllowed;
String relevantEmail;
if (userAuthInfo.appEngineUser().isPresent()) {
User user = userAuthInfo.appEngineUser().get();
Optional<RegistrarPoc> contactOptional = getContactMatchingLogin(user, registrar);
isRegistryLockAllowed =
isAdmin || contactOptional.map(RegistrarPoc::isRegistryLockAllowed).orElse(false);
relevantEmail =
isAdmin
? user.getEmail()
// if the contact isn't present, we shouldn't display the email anyway
: contactOptional.flatMap(RegistrarPoc::getRegistryLockEmailAddress).orElse("");
} else {
google.registry.model.console.User user = userAuthInfo.consoleUser().get();
isRegistryLockAllowed =
user.getUserRoles().hasPermission(registrarId, ConsolePermission.REGISTRY_LOCK);
relevantEmail = user.getEmailAddress();
}
isRegistryLockAllowed =
user.getUserRoles().hasPermission(registrarId, ConsolePermission.REGISTRY_LOCK);
String relevantEmail = user.getRegistryLockEmailAddress().orElse(user.getEmailAddress());
// Use the contact's registry lock email if it's present, else use the login email (for admins)
return ImmutableMap.of(
LOCK_ENABLED_FOR_CONTACT_PARAM,
@@ -19,11 +19,9 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.security.JsonResponseHelper.Status.ERROR;
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
import static google.registry.ui.server.registrar.RegistryLockGetAction.getContactMatchingLogin;
import static google.registry.ui.server.registrar.RegistryLockGetAction.getRegistrarAndVerifyLockAccess;
import static google.registry.ui.server.registrar.RegistryLockGetAction.verifyLockAccess;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.appengine.api.users.User;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
@@ -31,9 +29,8 @@ import com.google.common.flogger.FluentLogger;
import com.google.gson.Gson;
import google.registry.flows.domain.DomainFlowUtils;
import google.registry.groups.GmailClient;
import google.registry.model.console.User;
import google.registry.model.domain.RegistryLock;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.request.Action;
import google.registry.request.Action.Method;
import google.registry.request.HttpException.ForbiddenException;
@@ -42,7 +39,6 @@ import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.request.auth.UserAuthInfo;
import google.registry.security.JsonResponseHelper;
import google.registry.tools.DomainLockUtils;
import google.registry.util.EmailMessage;
@@ -119,13 +115,11 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc
checkArgument(!Strings.isNullOrEmpty(postInput.domainName), "Missing key for domainName");
DomainFlowUtils.validateDomainName(postInput.domainName);
checkNotNull(postInput.isLock, "Missing key for isLock");
UserAuthInfo userAuthInfo =
authResult
.userAuthInfo()
.orElseThrow(() -> new ForbiddenException("User is not logged in"));
User user =
authResult.user().orElseThrow(() -> new ForbiddenException("User is not logged in"));
// TODO: Move this line to the transaction below during nested transaction refactoring.
String userEmail = verifyPasswordAndGetEmail(userAuthInfo, postInput);
String userEmail = verifyPasswordAndGetEmail(user, postInput);
tm().transact(
() -> {
RegistryLock registryLock =
@@ -177,24 +171,13 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc
}
}
private String verifyPasswordAndGetEmail(
UserAuthInfo userAuthInfo, RegistryLockPostInput postInput)
private String verifyPasswordAndGetEmail(User user, RegistryLockPostInput postInput)
throws RegistrarAccessDeniedException {
if (registrarAccessor.isAdmin()) {
return userAuthInfo.getEmailAddress();
return user.getEmailAddress();
}
if (userAuthInfo.appEngineUser().isPresent()) {
return verifyPasswordAndGetEmailLegacyUser(userAuthInfo.appEngineUser().get(), postInput);
} else {
return verifyPasswordAndGetEmailConsoleUser(userAuthInfo.consoleUser().get(), postInput);
}
}
private String verifyPasswordAndGetEmailConsoleUser(
google.registry.model.console.User user, RegistryLockPostInput postInput)
throws RegistrarAccessDeniedException {
// Verify that the registrar has locking enabled
getRegistrarAndVerifyLockAccess(registrarAccessor, postInput.registrarId, false);
verifyLockAccess(registrarAccessor, postInput.registrarId, false);
checkArgument(
user.verifyRegistryLockPassword(postInput.password),
"Incorrect registry lock password for user");
@@ -202,33 +185,6 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc
.orElseThrow(() -> new IllegalArgumentException("User has no registry lock email address"));
}
private String verifyPasswordAndGetEmailLegacyUser(User user, RegistryLockPostInput postInput)
throws RegistrarAccessDeniedException {
// Verify that the user can access the registrar, that the user has
// registry lock enabled, and that the user provided a correct password
Registrar registrar =
getRegistrarAndVerifyLockAccess(registrarAccessor, postInput.registrarId, false);
RegistrarPoc registrarPoc =
getContactMatchingLogin(user, registrar)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Cannot match user %s to registrar contact", user.getUserId())));
checkArgument(
registrarPoc.verifyRegistryLockPassword(postInput.password),
"Incorrect registry lock password for contact");
return registrarPoc
.getRegistryLockEmailAddress()
.orElseThrow(
() ->
new IllegalStateException(
String.format(
"Contact %s had no registry lock email address",
registrarPoc.getEmailAddress())));
}
/** Value class that represents the expected input body from the UI request. */
private static class RegistryLockPostInput {
private String registrarId;
@@ -34,7 +34,7 @@ import javax.inject.Inject;
@Action(
service = Action.Service.DEFAULT,
path = RegistryLockVerifyAction.PATH,
auth = Auth.AUTH_PUBLIC_LEGACY)
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public final class RegistryLockVerifyAction extends HtmlAction {
public static final String PATH = "/registry-lock-verify";
@@ -62,7 +62,7 @@ public final class RegistryLockVerifyAction extends HtmlAction {
@Override
public void runAfterLogin(Map<String, Object> data) {
try {
boolean isAdmin = authResult.userAuthInfo().get().isUserAdmin();
boolean isAdmin = authResult.user().get().getUserRoles().isAdmin();
RegistryLock resultLock =
domainLockUtils.verifyVerificationCode(lockVerificationCode, isAdmin);
data.put("isLock", resultLock.getUnlockCompletionTime().isEmpty());

Some files were not shown because too many files have changed in this diff Show More