mirror of
https://github.com/google/nomulus
synced 2026-06-09 16:33:02 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3e67e58b5 | |||
| 589041b3ed | |||
| 455364ff29 | |||
| d90bc1a3e4 | |||
| 0e3875c1ff | |||
| 3b565b96b7 | |||
| ec6c77927f |
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+34
-73
@@ -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)));
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -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;
|
||||
}
|
||||
|
||||
+145
@@ -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";
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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
Reference in New Issue
Block a user