1
0
mirror of https://github.com/google/nomulus synced 2026-04-21 16:50:44 +00:00

Remove GAE Users service API usage (#2414)

This is the last remaining GAE API that we depend on. By removing it, we are able to remove all common GAE dependencies as well.

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

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

There are several behavioral changes that are worth noting:

The XsrfTokenManager now uses the console user's email address to mint and verify the token. Previously, only email addresses returned by the GAE Users service are used, whereas a blank email address will be used if the user is logged in as a console user. I believe this was an oversight that is now corrected.
The legacy console will return 401 when no user is logged in, instead of redirecting to the Users service login flow.
The logout URL in the legacy console is changed to use the IAP logout flow. It will clear the cookie and redirect the users to IAP login page (tested on QA).
The screenshot changes are mostly due to the console users lacking a display name and therefore showing the email address instead. Some changes are due to using the console user's email address as the registry lock email address, which is being fixed in Add DB column for separate rlock email address #2413 and its follow-up RPs.
This commit is contained in:
Lai Jiang
2024-05-29 12:37:44 -04:00
committed by GitHub
parent d90bc1a3e4
commit 455364ff29
193 changed files with 893 additions and 1833 deletions

View File

@@ -153,7 +153,6 @@ dependencies {
implementation deps['com.google.apis:google-api-services-monitoring']
implementation deps['com.google.apis:google-api-services-sheets']
implementation deps['com.google.apis:google-api-services-storage']
testImplementation deps['com.google.appengine:appengine-api-stubs']
implementation deps['com.google.auth:google-auth-library-credentials']
implementation deps['com.google.auth:google-auth-library-oauth2-http']
implementation deps['com.google.cloud.bigdataoss:util']
@@ -250,10 +249,6 @@ dependencies {
implementation deps['us.fatehi:schemacrawler-tools']
implementation deps['xerces:xmlParserAPIs']
implementation deps['org.ogce:xpp3']
// This dependency must come after javax.mail:mail as it would otherwise
// shadow classes in package javax.mail with its own implementation.
implementation deps['com.google.appengine:appengine-api-1.0-sdk']
// Known issue: nebula-lint misses inherited dependency.
implementation project(':common')
testImplementation project(path: ':common', configuration: 'testing')
@@ -271,7 +266,6 @@ dependencies {
testAnnotationProcessor project(':processor')
testImplementation deps['com.google.cloud:google-cloud-nio']
testImplementation deps['com.google.appengine:appengine-testing']
testImplementation deps['com.google.guava:guava-testlib']
testImplementation deps['com.google.monitoring-client:contrib']
testImplementation deps['com.google.protobuf:protobuf-java-util']

View File

@@ -105,11 +105,6 @@ com.google.apis:google-api-services-sheets:v4-rev20240423-2.0.0=compileClasspath
com.google.apis:google-api-services-sqladmin:v1beta4-rev20240324-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-storage:v1-rev20240311-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.apis:google-api-services-storage:v1-rev20240319-2.0.0=testCompileClasspath,testRuntimeClasspath
com.google.appengine:appengine-api-1.0-sdk:2.0.27=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.appengine:appengine-api-stubs:2.0.27=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.appengine:appengine-remote-api:2.0.27=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.appengine:appengine-testing:2.0.27=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.appengine:appengine-tools-sdk:2.0.27=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auth:google-auth-library-credentials:1.23.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auth:google-auth-library-oauth2-http:1.23.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auto.service:auto-service-annotations:1.0.1=errorprone,nonprodAnnotationProcessor,testAnnotationProcessor

View File

@@ -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();

View File

@@ -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";

View File

@@ -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";

View File

@@ -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();

View File

@@ -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

View File

@@ -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";

View File

@@ -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";

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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";

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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. */

View File

@@ -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";

View File

@@ -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();

View File

@@ -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;

View File

@@ -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";

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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";

View File

@@ -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(

View File

@@ -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 {}

View File

@@ -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,
})

View File

@@ -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
})

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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 =

View File

@@ -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";

View File

@@ -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();

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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();

View File

@@ -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();

View File

@@ -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";

View File

@@ -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";

View File

@@ -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();

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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() {

View File

@@ -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));
}
/**

View File

@@ -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
}
}

View File

@@ -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.

View File

@@ -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)));
}
}

View File

@@ -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;
}

View File

@@ -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");

View File

@@ -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());
}
}

View File

@@ -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);

View File

@@ -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";

View File

@@ -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";

View File

@@ -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;

View File

@@ -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();

View File

@@ -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();

View File

@@ -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

View File

@@ -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";

View File

@@ -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();

View File

@@ -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. */

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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();

View File

@@ -30,7 +30,7 @@ import javax.inject.Inject;
service = Action.Service.TOOLS,
path = UpdateUserGroupAction.PATH,
method = POST,
auth = Auth.AUTH_API_ADMIN)
auth = Auth.AUTH_ADMIN)
public class UpdateUserGroupAction implements Runnable {
public static final String PATH = "/_dr/admin/updateUserGroup";

View File

@@ -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";

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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";

View File

@@ -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;

View File

@@ -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();

View File

@@ -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.",

View File

@@ -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,

View File

@@ -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;

View File

@@ -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());

View File

@@ -47,11 +47,7 @@ import org.joda.time.DateTime;
* @see WhoisHttpAction
* @see <a href="http://www.ietf.org/rfc/rfc3912.txt">RFC 3912: WHOIS Protocol Specification</a>
*/
@Action(
service = Action.Service.PUBAPI,
path = "/_dr/whois",
method = POST,
auth = Auth.AUTH_API_ADMIN)
@Action(service = Action.Service.PUBAPI, path = "/_dr/whois", method = POST, auth = Auth.AUTH_ADMIN)
public class WhoisAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();

View File

@@ -67,26 +67,14 @@ public class RequestComponentTest {
}
private record Route(
String path,
String clazz,
String methods,
String ok,
String authMethods,
String min,
String userPolicy) {
String path, String clazz, String methods, String ok, String min, String userPolicy) {
private static final Splitter splitter = Splitter.on(' ').omitEmptyStrings().trimResults();
static Route create(String line) {
ImmutableList<String> parts = ImmutableList.copyOf(splitter.split(line));
assertThat(parts.size()).isEqualTo(7);
assertThat(parts.size()).isEqualTo(6);
return new Route(
parts.get(0),
parts.get(1),
parts.get(2),
parts.get(3),
parts.get(4),
parts.get(5),
parts.get(6));
parts.get(0), parts.get(1), parts.get(2), parts.get(3), parts.get(4), parts.get(5));
}
}
}

View File

@@ -22,13 +22,13 @@ import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static org.mockito.Mockito.mock;
import com.google.appengine.api.users.User;
import com.google.gson.JsonObject;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.request.Actions;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.util.Idn;
@@ -48,11 +48,17 @@ abstract class RdapActionBaseTestCase<A extends RdapActionBase> {
protected static final AuthResult AUTH_RESULT =
AuthResult.createUser(
UserAuthInfo.create(new User("rdap.user@user.com", "gmail.com", "12345"), false));
new User.Builder()
.setEmailAddress("rdap.user@user.com")
.setUserRoles(new UserRoles.Builder().setIsAdmin(false).build())
.build());
protected static final AuthResult AUTH_RESULT_ADMIN =
AuthResult.createUser(
UserAuthInfo.create(new User("rdap.admin@google.com", "gmail.com", "12345"), true));
new User.Builder()
.setEmailAddress("rdap.admin@google.com")
.setUserRoles(new UserRoles.Builder().setIsAdmin(true).build())
.build());
protected FakeResponse response = new FakeResponse();
protected final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ"));

View File

@@ -17,7 +17,7 @@ package google.registry.request;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.auth.Auth.AUTH_API_ADMIN;
import static google.registry.request.auth.Auth.AUTH_ADMIN;
import static google.registry.request.auth.Auth.AUTH_PUBLIC;
import static google.registry.request.auth.AuthResult.NOT_AUTHENTICATED;
import static org.mockito.ArgumentMatchers.any;
@@ -28,13 +28,13 @@ import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.appengine.api.users.User;
import com.google.common.testing.NullPointerTester;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.request.HttpException.ServiceUnavailableException;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.RequestAuthenticator;
import google.registry.request.auth.UserAuthInfo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
@@ -122,7 +122,7 @@ public final class RequestHandlerTest {
@Action(
service = Action.Service.DEFAULT,
path = "/auth/adminUser",
auth = AUTH_API_ADMIN,
auth = AUTH_ADMIN,
method = GET)
public class AuthAdminUserAction extends AuthBase {
AuthAdminUserAction(AuthResult authResult) {
@@ -192,7 +192,11 @@ public final class RequestHandlerTest {
private final StringWriter httpOutput = new StringWriter();
private RequestHandler<Component> handler;
private AuthResult providedAuthResult = null;
private final User testUser = new User("test@example.com", "test@example.com");
private final User testUser =
new User.Builder()
.setEmailAddress("test@example.com")
.setUserRoles(new UserRoles.Builder().setIsAdmin(true).build())
.build();
@BeforeEach
void beforeEach() throws Exception {
@@ -418,7 +422,7 @@ public final class RequestHandlerTest {
assertThat(providedAuthResult).isNotNull();
assertThat(providedAuthResult.authLevel()).isEqualTo(AuthLevel.NONE);
assertThat(providedAuthResult.userAuthInfo()).isEmpty();
assertThat(providedAuthResult.user()).isEmpty();
assertMetric("/auth/none", GET, AuthLevel.NONE, true);
}
@@ -426,7 +430,7 @@ public final class RequestHandlerTest {
void testAuthNeeded_failure() throws Exception {
when(req.getMethod()).thenReturn("GET");
when(req.getRequestURI()).thenReturn("/auth/adminUser");
when(requestAuthenticator.authorize(AUTH_API_ADMIN.authSettings(), req))
when(requestAuthenticator.authorize(AUTH_ADMIN.authSettings(), req))
.thenReturn(Optional.empty());
handler.handleRequest(req, rsp);
@@ -439,15 +443,15 @@ public final class RequestHandlerTest {
void testAuthNeeded_success() throws Exception {
when(req.getMethod()).thenReturn("GET");
when(req.getRequestURI()).thenReturn("/auth/adminUser");
when(requestAuthenticator.authorize(AUTH_API_ADMIN.authSettings(), req))
.thenReturn(Optional.of(AuthResult.createUser(UserAuthInfo.create(testUser, true))));
when(requestAuthenticator.authorize(AUTH_ADMIN.authSettings(), req))
.thenReturn(Optional.of(AuthResult.createUser(testUser)));
handler.handleRequest(req, rsp);
assertThat(providedAuthResult).isNotNull();
assertThat(providedAuthResult.authLevel()).isEqualTo(AuthLevel.USER);
assertThat(providedAuthResult.userAuthInfo()).isPresent();
assertThat(providedAuthResult.userAuthInfo().get().appEngineUser()).hasValue(testUser);
assertThat(providedAuthResult.user()).isPresent();
assertThat(providedAuthResult.user()).hasValue(testUser);
assertMetric("/auth/adminUser", GET, AuthLevel.USER, true);
}
}

View File

@@ -15,7 +15,7 @@
package google.registry.request;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.request.auth.Auth.AUTH_API_ADMIN;
import static google.registry.request.auth.Auth.AUTH_ADMIN;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Optional;
@@ -41,7 +41,7 @@ public final class RouterTest {
////////////////////////////////////////////////////////////////////////////////////////////////
@Action(service = Action.Service.DEFAULT, path = "/sloth", auth = AUTH_API_ADMIN)
@Action(service = Action.Service.DEFAULT, path = "/sloth", auth = AUTH_ADMIN)
public static final class SlothTask implements Runnable {
@Override
public void run() {}
@@ -71,11 +71,7 @@ public final class RouterTest {
////////////////////////////////////////////////////////////////////////////////////////////////
@Action(
service = Action.Service.DEFAULT,
path = "/prefix",
isPrefix = true,
auth = AUTH_API_ADMIN)
@Action(service = Action.Service.DEFAULT, path = "/prefix", isPrefix = true, auth = AUTH_ADMIN)
public static final class PrefixTask implements Runnable {
@Override
public void run() {}
@@ -105,7 +101,7 @@ public final class RouterTest {
service = Action.Service.DEFAULT,
path = "/prefix/long",
isPrefix = true,
auth = AUTH_API_ADMIN)
auth = AUTH_ADMIN)
public static final class LongTask implements Runnable {
@Override
public void run() {}
@@ -157,13 +153,13 @@ public final class RouterTest {
////////////////////////////////////////////////////////////////////////////////////////////////
@Action(service = Action.Service.DEFAULT, path = "/samePathAsOtherTask", auth = AUTH_API_ADMIN)
@Action(service = Action.Service.DEFAULT, path = "/samePathAsOtherTask", auth = AUTH_ADMIN)
public static final class DuplicateTask1 implements Runnable {
@Override
public void run() {}
}
@Action(service = Action.Service.DEFAULT, path = "/samePathAsOtherTask", auth = AUTH_API_ADMIN)
@Action(service = Action.Service.DEFAULT, path = "/samePathAsOtherTask", auth = AUTH_ADMIN)
public static final class DuplicateTask2 implements Runnable {
@Override
public void run() {}

View File

@@ -15,6 +15,7 @@
package google.registry.request.auth;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.console.RegistrarRole.ACCOUNT_MANAGER;
import static google.registry.request.auth.AuthResult.NOT_AUTHENTICATED;
import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.ADMIN;
import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.OWNER;
@@ -27,7 +28,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import com.google.appengine.api.users.User;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.testing.NullPointerTester;
@@ -35,7 +35,7 @@ import com.google.common.testing.TestLogHandler;
import dagger.Lazy;
import google.registry.groups.GroupsConnection;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase.State;
@@ -74,28 +74,37 @@ class AuthenticatedRegistrarAccessorTest {
private final TestLogHandler testLogHandler = new TestLogHandler();
private static final AuthResult USER = createAuthResult(false);
private static final AuthResult GAE_ADMIN = createAuthResult(true);
private static final AuthResult ADMIN_USER = createAuthResult(true);
private static final AuthResult NO_USER = NOT_AUTHENTICATED;
private static final Optional<String> SUPPORT_GROUP = Optional.of("support@registry.example");
/** Registrar ID of a REAL registrar with a RegistrarContact for USER and GAE_ADMIN. */
private static final String REGISTRAR_ID_WITH_CONTACT = "TheRegistrar";
/** Registrar ID of a REAL registrar without a RegistrarContact. */
private static final String REAL_REGISTRAR_ID_WITHOUT_CONTACT = "NewRegistrar";
/** Registrar ID of a REAL registrar which the {@link USER} has access to. */
private static final String REGISTRAR_ID_WITH_ACCESS = "TheRegistrar";
/** Registrar ID of a REAL registrar which the {@link USER} has no access to. */
private static final String REAL_REGISTRAR_ID_WITHOUT_ACCESS = "NewRegistrar";
/** Registrar ID of an OTE registrar without a RegistrarContact. */
private static final String OTE_REGISTRAR_ID_WITHOUT_CONTACT = "OteRegistrar";
/** Registrar ID of the Admin registrar without a RegistrarContact. */
private static final String ADMIN_REGISTRAR_ID = "AdminRegistrar";
/**
* Creates an AuthResult for a fake user.
*
* <p>The user will be a RegistrarContact for "TheRegistrar", but not for "NewRegistrar".
*
* @param isAdmin if true, the user is an administrator for the app-engine project.
* <p>The user will have access to TheRegistrar", but not "NewRegistrar".
*/
private static AuthResult createAuthResult(boolean isAdmin) {
return AuthResult.createUser(
UserAuthInfo.create(new User("johndoe@theregistrar.com", "theregistrar.com"), isAdmin));
new User.Builder()
.setEmailAddress("johndoe@theregistrar.com")
.setUserRoles(
new UserRoles.Builder()
.setIsAdmin(isAdmin)
.setRegistrarRoles(ImmutableMap.of(REGISTRAR_ID_WITH_ACCESS, ACCOUNT_MANAGER))
.build())
.build());
}
@BeforeEach
@@ -103,14 +112,14 @@ class AuthenticatedRegistrarAccessorTest {
when(lazyGroupsConnection.get()).thenReturn(groupsConnection);
JdkLoggerConfig.getConfig(AuthenticatedRegistrarAccessor.class).addHandler(testLogHandler);
persistResource(
loadRegistrar(REAL_REGISTRAR_ID_WITHOUT_CONTACT)
loadRegistrar(REAL_REGISTRAR_ID_WITHOUT_ACCESS)
.asBuilder()
.setRegistrarId(OTE_REGISTRAR_ID_WITHOUT_CONTACT)
.setType(Registrar.Type.OTE)
.setIanaIdentifier(null)
.build());
persistResource(
loadRegistrar(REAL_REGISTRAR_ID_WITHOUT_CONTACT)
loadRegistrar(REAL_REGISTRAR_ID_WITHOUT_ACCESS)
.asBuilder()
.setRegistrarId(ADMIN_REGISTRAR_ID)
.setType(Registrar.Type.OTE)
@@ -132,7 +141,7 @@ class AuthenticatedRegistrarAccessorTest {
USER, ADMIN_REGISTRAR_ID, SUPPORT_GROUP, lazyGroupsConnection);
assertThat(registrarAccessor.getAllRegistrarIdsWithRoles())
.containsExactly(REGISTRAR_ID_WITH_CONTACT, OWNER);
.containsExactly(REGISTRAR_ID_WITH_ACCESS, OWNER);
verify(lazyGroupsConnection).get();
}
@@ -147,35 +156,6 @@ class AuthenticatedRegistrarAccessorTest {
verifyNoInteractions(lazyGroupsConnection);
}
/**
* GAE admins have admin access to everything.
*
* <p>They also have OWNER access if they are in the RegistrarContacts.
*
* <p>They also have OWNER access to the Admin Registrar.
*
* <p>They also have OWNER access to non-REAL Registrars.
*
* <p>(in other words - they don't have OWNER access only to REAL registrars owned by others)
*/
@Test
void getAllRegistrarIdWithAccess_gaeAdmin() {
AuthenticatedRegistrarAccessor registrarAccessor =
new AuthenticatedRegistrarAccessor(
GAE_ADMIN, ADMIN_REGISTRAR_ID, SUPPORT_GROUP, lazyGroupsConnection);
assertThat(registrarAccessor.getAllRegistrarIdsWithRoles())
.containsExactly(
REGISTRAR_ID_WITH_CONTACT, ADMIN,
REGISTRAR_ID_WITH_CONTACT, OWNER,
REAL_REGISTRAR_ID_WITHOUT_CONTACT, ADMIN,
OTE_REGISTRAR_ID_WITHOUT_CONTACT, ADMIN,
OTE_REGISTRAR_ID_WITHOUT_CONTACT, OWNER,
ADMIN_REGISTRAR_ID, ADMIN,
ADMIN_REGISTRAR_ID, OWNER);
verifyNoInteractions(lazyGroupsConnection);
}
/**
* Users in support group have admin access to everything.
*
@@ -197,9 +177,9 @@ class AuthenticatedRegistrarAccessorTest {
assertThat(registrarAccessor.getAllRegistrarIdsWithRoles())
.containsExactly(
REGISTRAR_ID_WITH_CONTACT, ADMIN,
REGISTRAR_ID_WITH_CONTACT, OWNER,
REAL_REGISTRAR_ID_WITHOUT_CONTACT, ADMIN,
REGISTRAR_ID_WITH_ACCESS, ADMIN,
REGISTRAR_ID_WITH_ACCESS, OWNER,
REAL_REGISTRAR_ID_WITHOUT_ACCESS, ADMIN,
OTE_REGISTRAR_ID_WITHOUT_CONTACT, ADMIN,
OTE_REGISTRAR_ID_WITHOUT_CONTACT, OWNER,
ADMIN_REGISTRAR_ID, ADMIN,
@@ -215,7 +195,7 @@ class AuthenticatedRegistrarAccessorTest {
USER, ADMIN_REGISTRAR_ID, Optional.empty(), lazyGroupsConnection);
assertThat(registrarAccessor.getAllRegistrarIdsWithRoles())
.containsExactly(REGISTRAR_ID_WITH_CONTACT, OWNER);
.containsExactly(REGISTRAR_ID_WITH_ACCESS, OWNER);
// Make sure we didn't instantiate the lazyGroupsConnection
verifyNoInteractions(lazyGroupsConnection);
}
@@ -230,7 +210,7 @@ class AuthenticatedRegistrarAccessorTest {
verify(groupsConnection).isMemberOfGroup("johndoe@theregistrar.com", SUPPORT_GROUP.get());
assertThat(registrarAccessor.getAllRegistrarIdsWithRoles())
.containsExactly(REGISTRAR_ID_WITH_CONTACT, OWNER);
.containsExactly(REGISTRAR_ID_WITH_ACCESS, OWNER);
verify(lazyGroupsConnection).get();
}
@@ -238,7 +218,7 @@ class AuthenticatedRegistrarAccessorTest {
@Test
void testGetRegistrarForUser_noAccess_isNotAdmin() {
expectGetRegistrarFailure(
REAL_REGISTRAR_ID_WITHOUT_CONTACT,
REAL_REGISTRAR_ID_WITHOUT_ACCESS,
USER,
"user johndoe@theregistrar.com doesn't have access to registrar NewRegistrar");
verify(lazyGroupsConnection).get();
@@ -247,13 +227,13 @@ class AuthenticatedRegistrarAccessorTest {
@Test
void testGetRegistrarForUser_registrarIsDisabled_isNotAdmin() {
persistResource(
Registrar.loadByRegistrarId("TheRegistrar")
Registrar.loadByRegistrarId(REGISTRAR_ID_WITH_ACCESS)
.get()
.asBuilder()
.setState(State.DISABLED)
.build());
expectGetRegistrarFailure(
REGISTRAR_ID_WITH_CONTACT,
REGISTRAR_ID_WITH_ACCESS,
USER,
"user johndoe@theregistrar.com doesn't have access to registrar TheRegistrar");
verify(lazyGroupsConnection).get();
@@ -273,7 +253,7 @@ class AuthenticatedRegistrarAccessorTest {
@Test
void testGetRegistrarForUser_noUser() {
expectGetRegistrarFailure(
REGISTRAR_ID_WITH_CONTACT,
REGISTRAR_ID_WITH_ACCESS,
NO_USER,
"<logged-out user> doesn't have access to registrar TheRegistrar");
verifyNoInteractions(lazyGroupsConnection);
@@ -283,29 +263,18 @@ class AuthenticatedRegistrarAccessorTest {
@Test
void testGetRegistrarForUser_inContacts_isNotAdmin() throws Exception {
expectGetRegistrarSuccess(
REGISTRAR_ID_WITH_CONTACT,
REGISTRAR_ID_WITH_ACCESS,
USER,
"user johndoe@theregistrar.com has [OWNER] access to registrar TheRegistrar");
verify(lazyGroupsConnection).get();
}
/** Succeed loading registrar if user has access to it. Email address is case-insensitive */
@Test
void testGetRegistrarForUser_inContacts_isNotAdmin_caseInsensitive() throws Exception {
expectGetRegistrarSuccess(
REGISTRAR_ID_WITH_CONTACT,
AuthResult.createUser(
UserAuthInfo.create(new User("JohnDoe@theregistrar.com", "theregistrar.com"), false)),
"user JohnDoe@theregistrar.com has [OWNER] access to registrar TheRegistrar");
verify(lazyGroupsConnection).get();
}
/** Succeed loading registrar if admin with access. */
@Test
void testGetRegistrarForUser_inContacts_isAdmin() throws Exception {
expectGetRegistrarSuccess(
REGISTRAR_ID_WITH_CONTACT,
GAE_ADMIN,
REGISTRAR_ID_WITH_ACCESS,
ADMIN_USER,
"admin johndoe@theregistrar.com has [OWNER, ADMIN] access to registrar TheRegistrar");
verifyNoInteractions(lazyGroupsConnection);
}
@@ -314,8 +283,8 @@ class AuthenticatedRegistrarAccessorTest {
@Test
void testGetRegistrarForUser_notInContacts_isAdmin() throws Exception {
expectGetRegistrarSuccess(
REAL_REGISTRAR_ID_WITHOUT_CONTACT,
GAE_ADMIN,
REAL_REGISTRAR_ID_WITHOUT_ACCESS,
ADMIN_USER,
"admin johndoe@theregistrar.com has [ADMIN] access to registrar NewRegistrar.");
verifyNoInteractions(lazyGroupsConnection);
}
@@ -329,8 +298,8 @@ class AuthenticatedRegistrarAccessorTest {
.setState(State.DISABLED)
.build());
expectGetRegistrarSuccess(
REAL_REGISTRAR_ID_WITHOUT_CONTACT,
GAE_ADMIN,
REAL_REGISTRAR_ID_WITHOUT_ACCESS,
ADMIN_USER,
"admin johndoe@theregistrar.com has [OWNER, ADMIN] access to registrar NewRegistrar.");
verifyNoInteractions(lazyGroupsConnection);
}
@@ -340,7 +309,7 @@ class AuthenticatedRegistrarAccessorTest {
void testGetRegistrarForUser_notInContacts_isAdmin_notReal() throws Exception {
expectGetRegistrarSuccess(
OTE_REGISTRAR_ID_WITHOUT_CONTACT,
GAE_ADMIN,
ADMIN_USER,
"admin johndoe@theregistrar.com has [OWNER, ADMIN] access to registrar OteRegistrar.");
verifyNoInteractions(lazyGroupsConnection);
}
@@ -349,7 +318,7 @@ class AuthenticatedRegistrarAccessorTest {
@Test
void testGetRegistrarForUser_doesntExist_isAdmin() {
expectGetRegistrarFailure(
"BadRegistrarId", GAE_ADMIN, "Registrar BadRegistrarId does not exist");
"BadRegistrarId", ADMIN_USER, "Registrar BadRegistrarId does not exist");
verifyNoInteractions(lazyGroupsConnection);
}
@@ -419,7 +388,7 @@ class AuthenticatedRegistrarAccessorTest {
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
.build();
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(consoleUser));
AuthResult authResult = AuthResult.createUser(consoleUser);
AuthenticatedRegistrarAccessor registrarAccessor =
new AuthenticatedRegistrarAccessor(
authResult, ADMIN_REGISTRAR_ID, SUPPORT_GROUP, lazyGroupsConnection);
@@ -427,8 +396,8 @@ class AuthenticatedRegistrarAccessorTest {
// Admin access to all, and owner access to the non-real registrar and the admin registrar
assertThat(registrarAccessor.getAllRegistrarIdsWithRoles())
.containsExactly(
REGISTRAR_ID_WITH_CONTACT, ADMIN,
REAL_REGISTRAR_ID_WITHOUT_CONTACT, ADMIN,
REGISTRAR_ID_WITH_ACCESS, ADMIN,
REAL_REGISTRAR_ID_WITHOUT_ACCESS, ADMIN,
OTE_REGISTRAR_ID_WITHOUT_CONTACT, ADMIN,
OTE_REGISTRAR_ID_WITHOUT_CONTACT, OWNER,
ADMIN_REGISTRAR_ID, ADMIN,
@@ -444,7 +413,7 @@ class AuthenticatedRegistrarAccessorTest {
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(consoleUser));
AuthResult authResult = AuthResult.createUser(consoleUser);
AuthenticatedRegistrarAccessor registrarAccessor =
new AuthenticatedRegistrarAccessor(
authResult, ADMIN_REGISTRAR_ID, SUPPORT_GROUP, lazyGroupsConnection);
@@ -463,19 +432,19 @@ class AuthenticatedRegistrarAccessorTest {
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of(
REGISTRAR_ID_WITH_CONTACT,
RegistrarRole.ACCOUNT_MANAGER,
REAL_REGISTRAR_ID_WITHOUT_CONTACT,
RegistrarRole.ACCOUNT_MANAGER))
REGISTRAR_ID_WITH_ACCESS,
ACCOUNT_MANAGER,
REAL_REGISTRAR_ID_WITHOUT_ACCESS,
ACCOUNT_MANAGER))
.build())
.build();
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(consoleUser));
AuthResult authResult = AuthResult.createUser(consoleUser);
AuthenticatedRegistrarAccessor registrarAccessor =
new AuthenticatedRegistrarAccessor(
authResult, ADMIN_REGISTRAR_ID, SUPPORT_GROUP, lazyGroupsConnection);
assertThat(registrarAccessor.getAllRegistrarIdsWithRoles())
.containsExactly(
REGISTRAR_ID_WITH_CONTACT, OWNER,
REAL_REGISTRAR_ID_WITHOUT_CONTACT, OWNER);
REGISTRAR_ID_WITH_ACCESS, OWNER,
REAL_REGISTRAR_ID_WITHOUT_ACCESS, OWNER);
}
}

View File

@@ -1,177 +0,0 @@
// Copyright 2018 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.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.security.XsrfTokenManager;
import google.registry.testing.FakeClock;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
/** Unit tests for {@link LegacyAuthenticationMechanism}. */
@ExtendWith(MockitoExtension.class)
final class LegacyAuthenticationMechanismTest {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
@Mock private UserService userService;
@Mock private HttpServletRequest req;
private final FakeClock clock = new FakeClock();
private XsrfTokenManager xsrfTokenManager;
private LegacyAuthenticationMechanism legacyAuthenticationMechanism;
private String goodToken;
@BeforeEach
void beforeEach() {
xsrfTokenManager = new XsrfTokenManager(clock, userService);
legacyAuthenticationMechanism =
new LegacyAuthenticationMechanism(userService, xsrfTokenManager);
when(userService.getCurrentUser()).thenReturn(new User("email@example.com", "example.com"));
when(userService.isUserAdmin()).thenReturn(false);
goodToken = xsrfTokenManager.generateToken("email@example.com");
}
@AfterEach
void afterEach() {
// Make sure we didn't use getParameter or getInputStream or any of the other "with side
// effects" getters unexpectedly. But allow "no side effect" getters.
//
// Unfortunately HttpServletRequest doesn't document well which getters "have side effects". It
// does explicitly state getReader and getInputStream, and that getParameter can also interfere
// with them, but it doesn't say anything about getParameterNames, getParameterValues,
// getParameterMap - even though I'm pretty sure they are similar to getParameter in that
// effect.
//
// Feel free to add other "no side effect" functions with atLeast(0) to exempt them from the
// verifyNoMoreInteractions
verify(req, atLeast(0)).getMethod();
verify(req, atLeast(0)).getHeader(any());
verifyNoMoreInteractions(req);
}
@Test
@MockitoSettings(strictness = Strictness.LENIENT)
void testAuthenticate_notLoggedIn() {
when(userService.isUserLoggedIn()).thenReturn(false);
assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
.isEqualTo(AuthLevel.NONE);
}
@Test
void testAuthenticate_loggedInSafeMethod_get() {
when(userService.isUserLoggedIn()).thenReturn(true);
when(req.getMethod()).thenReturn("GET");
assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
.isEqualTo(AuthLevel.USER);
}
@Test
void testAuthenticate_loggedInSafeMethod_head() {
when(userService.isUserLoggedIn()).thenReturn(true);
when(req.getMethod()).thenReturn("HEAD");
assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
.isEqualTo(AuthLevel.USER);
}
@Test
@MockitoSettings(strictness = Strictness.LENIENT)
void testAuthenticate_loggedInUnsafeMethod_post_noXsrfToken() {
when(userService.isUserLoggedIn()).thenReturn(true);
when(req.getMethod()).thenReturn("POST");
assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
.isEqualTo(AuthLevel.NONE);
// Make sure we looked for the token in all relevant places before giving up
verify(req).getHeader("X-CSRF-Token");
verify(req).getParameter("xsrfToken");
}
@Test
void testAuthenticate_loggedInUnsafeMethod_post_goodTokenInHeader() {
when(userService.isUserLoggedIn()).thenReturn(true);
when(req.getMethod()).thenReturn("POST");
when(req.getHeader("X-CSRF-Token")).thenReturn(goodToken);
assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
.isEqualTo(AuthLevel.USER);
// Make sure we didn't call getParameter (we already verify it in the @After, but we're doing it
// here explicitly as well for clarity, since this is important in this test)
verify(req, times(0)).getParameter(any());
}
@Test
@MockitoSettings(strictness = Strictness.LENIENT)
void testAuthenticate_loggedInUnsafeMethod_post_badTokenInHeader() {
when(userService.isUserLoggedIn()).thenReturn(true);
when(req.getMethod()).thenReturn("POST");
when(req.getHeader("X-CSRF-Token")).thenReturn("bad");
assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
.isEqualTo(AuthLevel.NONE);
// Make sure we didn't call getParameter (we already verify it in the @After, but we're doing it
// here explicitly as well for clarity, since this is important in this test)
verify(req, times(0)).getParameter(any());
}
@Test
void testAuthenticate_loggedInUnsafeMethod_post_goodTokenInParam() {
when(userService.isUserLoggedIn()).thenReturn(true);
when(req.getMethod()).thenReturn("POST");
when(req.getParameter("xsrfToken")).thenReturn(goodToken);
assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
.isEqualTo(AuthLevel.USER);
// we allow getParameter to be called in this case (we verify it so it's not caught in the
// @After's verifyNoMoreInteractions)
verify(req).getParameter("xsrfToken");
}
@Test
@MockitoSettings(strictness = Strictness.LENIENT)
void testAuthenticate_loggedInUnsafeMethod_post_badTokenInParam() {
when(userService.isUserLoggedIn()).thenReturn(true);
when(req.getMethod()).thenReturn("POST");
when(req.getParameter("xsrfToken")).thenReturn("bad");
assertThat(legacyAuthenticationMechanism.authenticate(req).authLevel())
.isEqualTo(AuthLevel.NONE);
// we allow getParameter to be called in this case (we verify it so it's not caught in the
// @After's verifyNoMoreInteractions)
verify(req).getParameter("xsrfToken");
}
}

View File

@@ -129,7 +129,7 @@ public class OidcTokenAuthenticationMechanismTest {
authResult = authenticationMechanism.authenticate(request);
assertThat(authResult.isAuthenticated()).isTrue();
assertThat(authResult.authLevel()).isEqualTo(AuthLevel.USER);
assertThat(authResult.userAuthInfo().get().consoleUser().get()).isEqualTo(user);
assertThat(authResult.user().get()).isEqualTo(user);
}
@Test
@@ -153,7 +153,7 @@ public class OidcTokenAuthenticationMechanismTest {
authResult = authenticationMechanism.authenticate(request);
assertThat(authResult.isAuthenticated()).isTrue();
assertThat(authResult.authLevel()).isEqualTo(AuthLevel.USER);
assertThat(authResult.userAuthInfo().get().consoleUser().get()).isEqualTo(serviceUser);
assertThat(authResult.user().get()).isEqualTo(serviceUser);
}
@Test

View File

@@ -19,8 +19,6 @@ import static google.registry.request.auth.AuthResult.NOT_AUTHENTICATED;
import static google.registry.request.auth.AuthSettings.AuthLevel.APP;
import static google.registry.request.auth.AuthSettings.AuthLevel.NONE;
import static google.registry.request.auth.AuthSettings.AuthLevel.USER;
import static google.registry.request.auth.AuthSettings.AuthMethod.API;
import static google.registry.request.auth.AuthSettings.AuthMethod.LEGACY;
import static google.registry.request.auth.AuthSettings.UserPolicy.ADMIN;
import static google.registry.request.auth.AuthSettings.UserPolicy.PUBLIC;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -34,7 +32,6 @@ import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.AuthSettings.AuthMethod;
import google.registry.request.auth.AuthSettings.UserPolicy;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
@@ -48,27 +45,18 @@ class RequestAuthenticatorTest {
private static final AuthResult USER_PUBLIC_AUTH =
AuthResult.createUser(
UserAuthInfo.create(
new User.Builder()
.setEmailAddress("user@registry.example")
.setUserRoles(
new UserRoles.Builder()
.setIsAdmin(false)
.setGlobalRole(GlobalRole.NONE)
.build())
.build()));
new User.Builder()
.setEmailAddress("user@registry.example")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.NONE).build())
.build());
private static final AuthResult USER_ADMIN_AUTH =
AuthResult.createUser(
UserAuthInfo.create(
new User.Builder()
.setEmailAddress("admin@registry.example")
.setUserRoles(
new UserRoles.Builder()
.setIsAdmin(true)
.setGlobalRole(GlobalRole.FTE)
.build())
.build()));
new User.Builder()
.setEmailAddress("admin@registry.example")
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
.build());
private final HttpServletRequest req = mock(HttpServletRequest.class);
@@ -76,28 +64,23 @@ class RequestAuthenticatorTest {
mock(AuthenticationMechanism.class);
private final AuthenticationMechanism apiAuthenticationMechanism2 =
mock(AuthenticationMechanism.class);
private final LegacyAuthenticationMechanism legacyAuthenticationMechanism =
mock(LegacyAuthenticationMechanism.class);
private Optional<AuthResult> authorize(AuthLevel authLevel, UserPolicy userPolicy) {
return new RequestAuthenticator(
ImmutableList.of(apiAuthenticationMechanism1, apiAuthenticationMechanism2),
legacyAuthenticationMechanism)
.authorize(AuthSettings.create(ImmutableList.of(API, LEGACY), authLevel, userPolicy), req);
ImmutableList.of(apiAuthenticationMechanism1, apiAuthenticationMechanism2))
.authorize(new AuthSettings(authLevel, userPolicy), req);
}
private AuthResult authenticate(AuthMethod... methods) {
private AuthResult authenticate() {
return new RequestAuthenticator(
ImmutableList.of(apiAuthenticationMechanism1, apiAuthenticationMechanism2),
legacyAuthenticationMechanism)
.authenticate(AuthSettings.create(ImmutableList.copyOf(methods), NONE, PUBLIC), req);
ImmutableList.of(apiAuthenticationMechanism1, apiAuthenticationMechanism2))
.authenticate(new AuthSettings(NONE, PUBLIC), req);
}
@BeforeEach
void beforeEach() {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(NOT_AUTHENTICATED);
when(apiAuthenticationMechanism2.authenticate(req)).thenReturn(NOT_AUTHENTICATED);
when(legacyAuthenticationMechanism.authenticate(req)).thenReturn(NOT_AUTHENTICATED);
}
@Test
@@ -160,117 +143,29 @@ class RequestAuthenticatorTest {
@Test
void testAuthenticate_apiFirst() {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(APP_AUTH);
assertThat(authenticate(API, LEGACY)).isEqualTo(APP_AUTH);
assertThat(authenticate()).isEqualTo(APP_AUTH);
verify(apiAuthenticationMechanism1).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testAuthenticate_apiSecond() {
when(apiAuthenticationMechanism2.authenticate(req)).thenReturn(APP_AUTH);
assertThat(authenticate(API, LEGACY)).isEqualTo(APP_AUTH);
assertThat(authenticate()).isEqualTo(APP_AUTH);
verify(apiAuthenticationMechanism1).authenticate(req);
verify(apiAuthenticationMechanism2).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testAuthenticate_legacy() {
when(legacyAuthenticationMechanism.authenticate(req)).thenReturn(APP_AUTH);
assertThat(authenticate(API, LEGACY)).isEqualTo(APP_AUTH);
verify(apiAuthenticationMechanism1).authenticate(req);
verify(apiAuthenticationMechanism2).authenticate(req);
verify(legacyAuthenticationMechanism).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testAuthenticate_returnFirstResult() {
// API auth 2 returns an authenticted auth result, so we don't bother trying the next auth
// (legacy auth).
when(apiAuthenticationMechanism2.authenticate(req)).thenReturn(APP_AUTH);
when(legacyAuthenticationMechanism.authenticate(req)).thenReturn(USER_PUBLIC_AUTH);
assertThat(authenticate(API, LEGACY)).isEqualTo(APP_AUTH);
verify(apiAuthenticationMechanism1).authenticate(req);
verify(apiAuthenticationMechanism2).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testAuthenticate_notAuthenticated() {
assertThat(authenticate(API, LEGACY)).isEqualTo(NOT_AUTHENTICATED);
verify(apiAuthenticationMechanism1).authenticate(req);
verify(apiAuthenticationMechanism2).authenticate(req);
verify(legacyAuthenticationMechanism).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testAuthenticate_apiOnly() {
when(legacyAuthenticationMechanism.authenticate(req)).thenReturn(USER_PUBLIC_AUTH);
assertThat(authenticate(API)).isEqualTo(NOT_AUTHENTICATED);
assertThat(authenticate()).isEqualTo(NOT_AUTHENTICATED);
verify(apiAuthenticationMechanism1).authenticate(req);
verify(apiAuthenticationMechanism2).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testAuthenticate_legacyOnly() {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(USER_PUBLIC_AUTH);
assertThat(authenticate(LEGACY)).isEqualTo(NOT_AUTHENTICATED);
verify(legacyAuthenticationMechanism).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testFailure_checkAuthConfig_noMethods() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
RequestAuthenticator.checkAuthConfig(
AuthSettings.create(ImmutableList.of(), NONE, PUBLIC)));
assertThat(thrown).hasMessageThat().contains("Must specify at least one auth method");
}
@Test
void testFailure_checkAuthConfig_wrongMethodOrder() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
RequestAuthenticator.checkAuthConfig(
AuthSettings.create(ImmutableList.of(LEGACY, API), NONE, PUBLIC)));
assertThat(thrown)
.hasMessageThat()
.contains("Auth methods must be unique and strictly in order - API, LEGACY");
}
@Test
void testFailure_CheckAuthConfig_duplicateMethods() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
RequestAuthenticator.checkAuthConfig(
AuthSettings.create(ImmutableList.of(API, API), NONE, PUBLIC)));
assertThat(thrown)
.hasMessageThat()
.contains("Auth methods must be unique and strictly in order - API, LEGACY");
}
@Test
@@ -278,9 +173,7 @@ class RequestAuthenticatorTest {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
RequestAuthenticator.checkAuthConfig(
AuthSettings.create(ImmutableList.of(API, LEGACY), NONE, ADMIN)));
() -> RequestAuthenticator.checkAuthConfig(new AuthSettings(NONE, ADMIN)));
assertThat(thrown)
.hasMessageThat()
.contains("Actions with minimal auth level at NONE should not specify ADMIN user policy");

View File

@@ -22,7 +22,6 @@ import static org.mockito.Mockito.verify;
import google.registry.model.server.Lock;
import google.registry.testing.FakeClock;
import google.registry.testing.UserServiceExtension;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;
@@ -30,7 +29,6 @@ import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link LockHandler}. */
final class LockHandlerImplTest {
@@ -39,10 +37,6 @@ final class LockHandlerImplTest {
private final FakeClock clock = new FakeClock(DateTime.parse("2001-08-29T12:20:00Z"));
// We do not actually need to set up user service, rather, we just need this extension to set up
// App Engine environment so the status checker can make an App Engine API call.
@RegisterExtension UserServiceExtension userService = new UserServiceExtension("");
private static class CountingCallable implements Callable<Void> {
int numCalled;

View File

@@ -16,11 +16,7 @@ package google.registry.security;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.common.base.Splitter;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
@@ -37,59 +33,56 @@ class XsrfTokenManagerTest {
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final User testUser = new User("test@example.com", "test@example.com");
private final String email = "test@example.com";
private final FakeClock clock = new FakeClock(START_OF_TIME);
private final UserService userService = mock(UserService.class);
private final XsrfTokenManager xsrfTokenManager = new XsrfTokenManager(clock, userService);
private final XsrfTokenManager xsrfTokenManager = new XsrfTokenManager(clock);
private String token;
@BeforeEach
void beforeEach() {
when(userService.isUserLoggedIn()).thenReturn(true);
when(userService.getCurrentUser()).thenReturn(testUser);
when(userService.isUserAdmin()).thenReturn(false);
token = xsrfTokenManager.generateToken(testUser.getEmail());
token = xsrfTokenManager.generateToken(email);
}
@Test
void testValidate_validToken() {
assertThat(xsrfTokenManager.validateToken(token)).isTrue();
assertThat(xsrfTokenManager.validateToken(email, token)).isTrue();
}
@Test
void testValidate_tokenWithMissingParts() {
assertThat(xsrfTokenManager.validateToken("1:123")).isFalse();
assertThat(xsrfTokenManager.validateToken(email, "1:123")).isFalse();
}
@Test
void testValidate_tokenWithBadVersion() {
assertThat(xsrfTokenManager.validateToken("2:123:base64")).isFalse();
assertThat(xsrfTokenManager.validateToken(email, "2:123:base64")).isFalse();
}
@Test
void testValidate_tokenWithBadNumberTimestamp() {
assertThat(xsrfTokenManager.validateToken("1:notanumber:base64")).isFalse();
assertThat(xsrfTokenManager.validateToken(email, "1:notanumber:base64")).isFalse();
}
@Test
void testValidate_tokenExpiresAfterOneDay() {
clock.advanceBy(Duration.standardDays(1));
assertThat(xsrfTokenManager.validateToken(token)).isTrue();
assertThat(xsrfTokenManager.validateToken(email, token)).isTrue();
clock.advanceOneMilli();
assertThat(xsrfTokenManager.validateToken(token)).isFalse();
assertThat(xsrfTokenManager.validateToken(email, token)).isFalse();
}
@Test
void testValidate_tokenTimestampTamperedWith() {
String encodedPart = Splitter.on(':').splitToList(token).get(2);
long fakeTimestamp = clock.nowUtc().plusMillis(1).getMillis();
assertThat(xsrfTokenManager.validateToken("1:" + fakeTimestamp + ':' + encodedPart)).isFalse();
assertThat(xsrfTokenManager.validateToken(email, "1:" + fakeTimestamp + ':' + encodedPart))
.isFalse();
}
@Test
void testValidate_tokenForDifferentUser() {
String otherToken = xsrfTokenManager.generateToken("eve@example.com");
assertThat(xsrfTokenManager.validateToken(otherToken)).isFalse();
assertThat(xsrfTokenManager.validateToken(email, otherToken)).isFalse();
}
}

View File

@@ -26,9 +26,6 @@ import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTransactionManagerExtension;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.OidcTokenAuthenticationMechanism;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.UserInfo;
import google.registry.testing.UserServiceExtension;
import google.registry.tools.params.HostAndPortParameter;
import google.registry.ui.ConsoleDebug;
import java.util.List;
@@ -139,9 +136,6 @@ public final class RegistryTestServerMain {
final RegistryTestServer server = new RegistryTestServer(address);
System.out.printf("%sLoading SQL fixtures and User service...%s\n", BLUE, RESET);
new UserServiceExtension(
loginIsAdmin ? UserInfo.createAdmin(loginEmail) : UserInfo.create(loginEmail))
.beforeEach(null);
UserRoles userRoles =
new UserRoles.Builder().setIsAdmin(loginIsAdmin).setGlobalRole(GlobalRole.FTE).build();
User user =
@@ -150,8 +144,7 @@ public final class RegistryTestServerMain {
.setUserRoles(userRoles)
.setRegistryLockPassword("registryLockPassword")
.build();
OidcTokenAuthenticationMechanism.setAuthResultForTesting(
AuthResult.createUser(UserAuthInfo.create(user)));
OidcTokenAuthenticationMechanism.setAuthResultForTesting(AuthResult.createUser(user));
new JpaTestExtensions.Builder().buildIntegrationTestExtension().beforeEach(null);
JpaTransactionManagerExtension.loadInitialData();
System.out.printf("%sLoading fixtures...%s\n", BLUE, RESET);

View File

@@ -17,35 +17,27 @@ package google.registry.testing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.appengine.api.users.UserService;
import google.registry.model.console.User;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
import google.registry.security.XsrfTokenManager;
import google.registry.ui.server.registrar.ConsoleApiParams;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.joda.time.DateTime;
public final class FakeConsoleApiParams {
public final class ConsoleApiParamsUtils {
public static ConsoleApiParams get(Optional<AuthResult> maybeAuthResult) {
AuthResult authResult =
maybeAuthResult.orElseGet(
() ->
AuthResult.createUser(
UserAuthInfo.create(
new com.google.appengine.api.users.User(
"JohnDoe@theregistrar.com", "theregistrar.com"),
false)));
public static ConsoleApiParams createFake(AuthResult authResult) {
HttpServletRequest request = mock(HttpServletRequest.class);
XsrfTokenManager xsrfTokenManager =
new XsrfTokenManager(
new FakeClock(DateTime.parse("2020-02-02T01:23:45Z")), mock(UserService.class));
new XsrfTokenManager(new FakeClock(DateTime.parse("2020-02-02T01:23:45Z")));
when(request.getCookies())
.thenReturn(
new Cookie[] {
new Cookie(XsrfTokenManager.X_CSRF_TOKEN, xsrfTokenManager.generateToken(""))
new Cookie(
XsrfTokenManager.X_CSRF_TOKEN,
xsrfTokenManager.generateToken(
authResult.user().map(User::getEmailAddress).orElse("")))
});
return ConsoleApiParams.create(request, new FakeResponse(), authResult, xsrfTokenManager);
}

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