From 89c942b0d9f9345ca4f99c782504d1da7dc03cc8 Mon Sep 17 00:00:00 2001 From: guyben Date: Fri, 21 Sep 2018 09:00:45 -0700 Subject: [PATCH 01/60] Have a resource component receive the "registrar resource" instead of creating it We have several "consoles" ("views" in the registrar console). They all affect the same resource - the registrar itself. Currently, each view creates its own "RESTful resource", even though it's the same resource for all of them, meaning we have copied code and copied "URL endpoint" across multiple files. I assume this was made so that IF one day we have a "view" to another resource, we can easily add it. But we currently don't have any such view, nor plans to add any. So according to the YAGNI paradigm, it's better to move the resource creation outside of the console. Also, IF we do add a view to a different resource - it'll still be more readable to have a map from the "view" to the "resource URL endpoint" alongside the existing map from the "view" to the "console"... ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=213992967 --- java/google/registry/ui/js/registrar/console.js | 8 ++++++-- .../registry/ui/js/registrar/contact_settings.js | 12 ++++-------- java/google/registry/ui/js/registrar/contact_us.js | 13 ++++--------- java/google/registry/ui/js/registrar/resources.js | 13 ++++--------- .../registry/ui/js/registrar/security_settings.js | 13 ++++--------- .../registry/ui/js/registrar/whois_settings.js | 13 ++++--------- 6 files changed, 26 insertions(+), 46 deletions(-) diff --git a/java/google/registry/ui/js/registrar/console.js b/java/google/registry/ui/js/registrar/console.js index 99d8a4cfa..0796e0c5e 100644 --- a/java/google/registry/ui/js/registrar/console.js +++ b/java/google/registry/ui/js/registrar/console.js @@ -14,11 +14,13 @@ goog.provide('registry.registrar.Console'); +goog.require('goog.Uri'); goog.require('goog.dispose'); goog.require('goog.dom'); goog.require('goog.dom.classlist'); goog.require('goog.net.XhrIo'); goog.require('registry.Console'); +goog.require('registry.Resource'); goog.require('registry.registrar.Contact'); goog.require('registry.registrar.ContactSettings'); goog.require('registry.registrar.ContactUs'); @@ -76,7 +78,7 @@ registry.registrar.Console = function(params) { /** * @type {!Object.} + * !registry.Resource)>} */ this.pageMap = {}; this.pageMap['security-settings'] = registry.registrar.SecuritySettings; @@ -136,7 +138,9 @@ registry.registrar.Console.prototype.handleHashChange = function() { componentCtor = this.pageMap['']; } var oldComponent = this.component_; - this.component_ = new componentCtor(this, this.params.xsrfToken); + const resource = new registry.Resource( + new goog.Uri('/registrar-settings'), this.params.xsrfToken); + this.component_ = new componentCtor(this, resource); this.registerDisposable(this.component_); this.component_.basePath = type; this.component_.bindToDom(id); diff --git a/java/google/registry/ui/js/registrar/contact_settings.js b/java/google/registry/ui/js/registrar/contact_settings.js index 480155558..d690bc79a 100644 --- a/java/google/registry/ui/js/registrar/contact_settings.js +++ b/java/google/registry/ui/js/registrar/contact_settings.js @@ -14,7 +14,6 @@ goog.provide('registry.registrar.ContactSettings'); -goog.require('goog.Uri'); goog.require('goog.array'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); @@ -38,18 +37,15 @@ goog.forwardDeclare('registry.registrar.Console'); * updating only that field of the Registrar object and not * implementing the create action from edit_item. * @param {!registry.registrar.Console} console - * @param {string} xsrfToken Security token to pass back to the server. + * @param {!registry.Resource} resource the RESTful resource for the registrar. * @constructor * @extends {registry.ResourceComponent} * @final */ -registry.registrar.ContactSettings = function(console, xsrfToken) { +registry.registrar.ContactSettings = function(console, resource) { registry.registrar.ContactSettings.base( - this, 'constructor', - console, - new registry.Resource(new goog.Uri('/registrar-settings'), xsrfToken), - registry.soy.registrar.contacts.contact, - null); + this, 'constructor', console, resource, + registry.soy.registrar.contacts.contact, null); }; goog.inherits(registry.registrar.ContactSettings, registry.ResourceComponent); diff --git a/java/google/registry/ui/js/registrar/contact_us.js b/java/google/registry/ui/js/registrar/contact_us.js index ae7870d0f..0d19c09ad 100644 --- a/java/google/registry/ui/js/registrar/contact_us.js +++ b/java/google/registry/ui/js/registrar/contact_us.js @@ -14,7 +14,6 @@ goog.provide('registry.registrar.ContactUs'); -goog.require('goog.Uri'); goog.require('goog.dom'); goog.require('registry.Resource'); goog.require('registry.ResourceComponent'); @@ -27,19 +26,15 @@ goog.forwardDeclare('registry.registrar.Console'); /** * Contact Us page. * @param {!registry.registrar.Console} console - * @param {string} xsrfToken Security token to pass back to the server. + * @param {!registry.Resource} resource the RESTful resource for the registrar. * @constructor * @extends {registry.ResourceComponent} * @final */ -registry.registrar.ContactUs = function(console, xsrfToken) { +registry.registrar.ContactUs = function(console, resource) { registry.registrar.ContactUs.base( - this, - 'constructor', - console, - new registry.Resource(new goog.Uri('/registrar-settings'), xsrfToken), - registry.soy.registrar.console.contactUs, - null); + this, 'constructor', console, resource, + registry.soy.registrar.console.contactUs, null); }; goog.inherits(registry.registrar.ContactUs, registry.ResourceComponent); diff --git a/java/google/registry/ui/js/registrar/resources.js b/java/google/registry/ui/js/registrar/resources.js index 003636b90..5f188a38d 100644 --- a/java/google/registry/ui/js/registrar/resources.js +++ b/java/google/registry/ui/js/registrar/resources.js @@ -14,7 +14,6 @@ goog.provide('registry.registrar.Resources'); -goog.require('goog.Uri'); goog.require('goog.dom'); goog.require('registry.Resource'); goog.require('registry.ResourceComponent'); @@ -27,19 +26,15 @@ goog.forwardDeclare('registry.registrar.Console'); /** * Resources and billing page. * @param {!registry.registrar.Console} console - * @param {string} xsrfToken Security token to pass back to the server. + * @param {!registry.Resource} resource the RESTful resource for the registrar. * @constructor * @extends {registry.ResourceComponent} * @final */ -registry.registrar.Resources = function(console, xsrfToken) { +registry.registrar.Resources = function(console, resource) { registry.registrar.Resources.base( - this, - 'constructor', - console, - new registry.Resource(new goog.Uri('/registrar-settings'), xsrfToken), - registry.soy.registrar.console.resources, - null); + this, 'constructor', console, resource, + registry.soy.registrar.console.resources, null); }; goog.inherits(registry.registrar.Resources, registry.ResourceComponent); diff --git a/java/google/registry/ui/js/registrar/security_settings.js b/java/google/registry/ui/js/registrar/security_settings.js index 849c8fb9a..1a3f2ed34 100644 --- a/java/google/registry/ui/js/registrar/security_settings.js +++ b/java/google/registry/ui/js/registrar/security_settings.js @@ -14,7 +14,6 @@ goog.provide('registry.registrar.SecuritySettings'); -goog.require('goog.Uri'); goog.require('goog.array'); goog.require('goog.dom'); goog.require('goog.dom.classlist'); @@ -32,19 +31,15 @@ goog.forwardDeclare('registry.registrar.Console'); /** * Security Settings page. * @param {!registry.registrar.Console} console - * @param {string} xsrfToken Security token to pass back to the server. + * @param {!registry.Resource} resource the RESTful resource for the registrar. * @constructor * @extends {registry.ResourceComponent} * @final */ -registry.registrar.SecuritySettings = function(console, xsrfToken) { +registry.registrar.SecuritySettings = function(console, resource) { registry.registrar.SecuritySettings.base( - this, - 'constructor', - console, - new registry.Resource(new goog.Uri('/registrar-settings'), xsrfToken), - registry.soy.registrar.security.settings, - null); + this, 'constructor', console, resource, + registry.soy.registrar.security.settings, null); }; goog.inherits(registry.registrar.SecuritySettings, registry.ResourceComponent); diff --git a/java/google/registry/ui/js/registrar/whois_settings.js b/java/google/registry/ui/js/registrar/whois_settings.js index 7234c5117..68297dbb7 100644 --- a/java/google/registry/ui/js/registrar/whois_settings.js +++ b/java/google/registry/ui/js/registrar/whois_settings.js @@ -14,7 +14,6 @@ goog.provide('registry.registrar.WhoisSettings'); -goog.require('goog.Uri'); goog.require('goog.dom'); goog.require('registry.Resource'); goog.require('registry.ResourceComponent'); @@ -27,19 +26,15 @@ goog.forwardDeclare('registry.registrar.Console'); /** * WHOIS Settings page. * @param {!registry.registrar.Console} console - * @param {string} xsrfToken Cross-site request forgery protection token. + * @param {!registry.Resource} resource the RESTful resource for the registrar. * @constructor * @extends {registry.ResourceComponent} * @final */ -registry.registrar.WhoisSettings = function(console, xsrfToken) { +registry.registrar.WhoisSettings = function(console, resource) { registry.registrar.WhoisSettings.base( - this, - 'constructor', - console, - new registry.Resource(new goog.Uri('/registrar-settings'), xsrfToken), - registry.soy.registrar.whois.settings, - null); + this, 'constructor', console, resource, + registry.soy.registrar.whois.settings, null); }; goog.inherits(registry.registrar.WhoisSettings, registry.ResourceComponent); From c89cb6a3f3ca8c5c82bad709586e534162679020 Mon Sep 17 00:00:00 2001 From: guyben Date: Fri, 21 Sep 2018 14:54:16 -0700 Subject: [PATCH 02/60] Automated g4 rollback of changelist 212879670. *** Reason for rollback *** Automated tools sometimes don't have default credentials, and can't set them up. We should redo this CL once we figure out the credential thing. *** Original change description *** Add metrics for the command used in the registry CLI tool Puts the metric in /tools/commands_called It counts the use of the tool, with the following labels: - environment - tool (nomulus/gtech) - command called (class name) - success true/false - from the shell true/false *** ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=214048616 --- java/google/registry/tools/BUILD | 3 - .../registry/tools/MetricToolModule.java | 53 ------------ java/google/registry/tools/RegistryCli.java | 81 +------------------ .../registry/tools/RegistryToolComponent.java | 6 -- java/google/registry/tools/logging.properties | 3 - .../registry/tools/ShellCommandTest.java | 3 +- 6 files changed, 4 insertions(+), 145 deletions(-) delete mode 100644 java/google/registry/tools/MetricToolModule.java diff --git a/java/google/registry/tools/BUILD b/java/google/registry/tools/BUILD index 2c2327ef4..90e9fb3dc 100644 --- a/java/google/registry/tools/BUILD +++ b/java/google/registry/tools/BUILD @@ -69,7 +69,6 @@ java_library( "@com_google_api_client", "@com_google_apis_google_api_services_bigquery", "@com_google_apis_google_api_services_dns", - "@com_google_apis_google_api_services_monitoring", "@com_google_appengine_api_1_0_sdk", "@com_google_appengine_remote_api", "@com_google_appengine_remote_api//:link", @@ -81,8 +80,6 @@ java_library( "@com_google_guava", "@com_google_http_client", "@com_google_http_client_jackson2", - "@com_google_monitoring_client_metrics", - "@com_google_monitoring_client_stackdriver", "@com_google_oauth_client", "@com_google_oauth_client_java6", "@com_google_oauth_client_jetty", diff --git a/java/google/registry/tools/MetricToolModule.java b/java/google/registry/tools/MetricToolModule.java deleted file mode 100644 index 6ed06998b..000000000 --- a/java/google/registry/tools/MetricToolModule.java +++ /dev/null @@ -1,53 +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.tools; - -import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; -import com.google.api.services.monitoring.v3.Monitoring; -import com.google.api.services.monitoring.v3.model.MonitoredResource; -import com.google.monitoring.metrics.MetricWriter; -import com.google.monitoring.metrics.stackdriver.StackdriverWriter; -import dagger.Module; -import dagger.Provides; -import google.registry.config.CredentialModule.DefaultCredential; -import google.registry.config.RegistryConfig.Config; - -/** Dagger module for metrics on the client tool. */ -@Module -public final class MetricToolModule { - - @Provides - static Monitoring provideMonitoring( - @DefaultCredential GoogleCredential credential, @Config("projectId") String projectId) { - return new Monitoring.Builder( - credential.getTransport(), credential.getJsonFactory(), credential) - .setApplicationName(projectId) - .build(); - } - - @Provides - static MetricWriter provideMetricWriter( - Monitoring monitoringClient, - @Config("projectId") String projectId, - @Config("stackdriverMaxQps") int maxQps, - @Config("stackdriverMaxPointsPerRequest") int maxPointsPerRequest) { - return new StackdriverWriter( - monitoringClient, - projectId, - new MonitoredResource().setType("global"), - maxQps, - maxPointsPerRequest); - } -} diff --git a/java/google/registry/tools/RegistryCli.java b/java/google/registry/tools/RegistryCli.java index 2daa6ab02..61915f492 100644 --- a/java/google/registry/tools/RegistryCli.java +++ b/java/google/registry/tools/RegistryCli.java @@ -26,19 +26,10 @@ import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameters; import com.beust.jcommander.ParametersDelegate; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import com.google.monitoring.metrics.IncrementableMetric; -import com.google.monitoring.metrics.LabelDescriptor; -import com.google.monitoring.metrics.Metric; -import com.google.monitoring.metrics.MetricPoint; -import com.google.monitoring.metrics.MetricRegistryImpl; -import com.google.monitoring.metrics.MetricWriter; import google.registry.model.ofy.ObjectifyService; import google.registry.tools.params.ParameterFactory; -import java.io.IOException; import java.security.Security; import java.util.Map; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -62,9 +53,6 @@ final class RegistryCli implements AutoCloseable, CommandRunner { description = "Returns all command names.") private boolean showAllCommands; - @VisibleForTesting - boolean uploadMetrics = true; - // Do not make this final - compile-time constant inlining may interfere with JCommander. @ParametersDelegate private AppEngineConnectionFlags appEngineConnectionFlags = @@ -85,24 +73,6 @@ final class RegistryCli implements AutoCloseable, CommandRunner { // "shell". private boolean isFirstUse = true; - private static final ImmutableSet LABEL_DESCRIPTORS_FOR_COMMANDS = - ImmutableSet.of( - LabelDescriptor.create("program", "The program used - e.g. nomulus or gtech_tool"), - LabelDescriptor.create("environment", "The environment used - e.g. sandbox"), - LabelDescriptor.create("command", "The command used"), - LabelDescriptor.create("success", "Whether the command succeeded"), - LabelDescriptor.create("shell", "Whether the command was called from the nomulus shell")); - - private static final IncrementableMetric commandsCalledCount = - MetricRegistryImpl.getDefault() - .newIncrementableMetric( - "/tools/commands_called", - "Count of tool commands called", - "count", - LABEL_DESCRIPTORS_FOR_COMMANDS); - - private MetricWriter metricWriter = null; - Map> commands; String programName; @@ -124,13 +94,9 @@ final class RegistryCli implements AutoCloseable, CommandRunner { // http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeArguments.html#FAQ104 @Override public void run(String[] args) throws Exception { - boolean inShell = !isFirstUse; - isFirstUse = false; // Create the JCommander instance. - // If we're in the shell, we don't want to update the RegistryCli's parameters (so we give a - // dummy object to update) - JCommander jcommander = new JCommander(inShell ? new Object() : this); + JCommander jcommander = new JCommander(this); jcommander.addConverterFactory(new ParameterFactory()); jcommander.setProgramName(programName); @@ -149,8 +115,8 @@ final class RegistryCli implements AutoCloseable, CommandRunner { // Create the "help" and "shell" commands (these are special in that they don't have a default // constructor). jcommander.addCommand("help", new HelpCommand(jcommander)); - if (!inShell) { - // If we aren't inside a shell, then we want to add the shell command. + if (isFirstUse) { + isFirstUse = false; ShellCommand shellCommand = new ShellCommand(this); // We have to build the completions based on the jcommander *before* we add the "shell" // command - to avoid completion for the "shell" command itself. @@ -192,31 +158,17 @@ final class RegistryCli implements AutoCloseable, CommandRunner { jcommander.getCommands().get(jcommander.getParsedCommand()).getObjects()); loggingParams.configureLogging(); // Must be called after parameters are parsed. - boolean success = false; try { runCommand(command); - success = true; } catch (AuthModule.LoginRequiredException ex) { System.err.println("==================================================================="); System.err.println("You must login using 'nomulus login' prior to running this command."); System.err.println("==================================================================="); - } finally { - commandsCalledCount.increment( - programName, - environment.toString(), - command.getClass().getSimpleName(), - String.valueOf(success), - String.valueOf(inShell)); - exportMetrics(); } } @Override public void close() { - exportMetrics(); - if (metricWriter != null) { - metricWriter = null; - } if (installer != null) { installer.uninstall(); installer = null; @@ -233,14 +185,6 @@ final class RegistryCli implements AutoCloseable, CommandRunner { private void runCommand(Command command) throws Exception { injectReflectively(RegistryToolComponent.class, component, command); - if (metricWriter == null && uploadMetrics) { - try { - metricWriter = component.metricWriter(); - } catch (Exception e) { - System.err.format("Failed to get metricWriter. Got error:\n%s\n\n", e); - uploadMetrics = false; - } - } if (command instanceof CommandWithConnection) { ((CommandWithConnection) command).setConnection(getConnection()); @@ -272,25 +216,6 @@ final class RegistryCli implements AutoCloseable, CommandRunner { command.run(); } - private void exportMetrics() { - if (metricWriter == null) { - return; - } - try { - for (Metric metric : MetricRegistryImpl.getDefault().getRegisteredMetrics()) { - for (MetricPoint point : metric.getTimestampedValues()) { - metricWriter.write(point); - } - } - metricWriter.flush(); - } catch (IOException e) { - System.err.format("Failed to export metrics. Got error:\n%s\n\n", e); - System.err.println("Maybe you need to login? Try calling:"); - System.err.println(" gcloud auth application-default login"); - } - } - - @VisibleForTesting void setEnvironment(RegistryToolEnvironment environment) { this.environment = environment; } diff --git a/java/google/registry/tools/RegistryToolComponent.java b/java/google/registry/tools/RegistryToolComponent.java index f0ea20337..e069ff12d 100644 --- a/java/google/registry/tools/RegistryToolComponent.java +++ b/java/google/registry/tools/RegistryToolComponent.java @@ -14,7 +14,6 @@ package google.registry.tools; -import com.google.monitoring.metrics.MetricWriter; import dagger.Component; import google.registry.bigquery.BigqueryModule; import google.registry.config.CredentialModule; @@ -29,7 +28,6 @@ import google.registry.request.Modules.AppIdentityCredentialModule; import google.registry.request.Modules.DatastoreServiceModule; import google.registry.request.Modules.GoogleCredentialModule; import google.registry.request.Modules.Jackson2Module; -import google.registry.request.Modules.NetHttpTransportModule; import google.registry.request.Modules.URLFetchServiceModule; import google.registry.request.Modules.UrlFetchTransportModule; import google.registry.request.Modules.UseAppIdentityCredentialForGoogleApisModule; @@ -67,7 +65,6 @@ import javax.inject.Singleton; Jackson2Module.class, KeyModule.class, KmsModule.class, - NetHttpTransportModule.class, RdeModule.class, RegistryToolModule.class, SystemClockModule.class, @@ -79,7 +76,6 @@ import javax.inject.Singleton; UserServiceModule.class, VoidDnsWriterModule.class, WhoisModule.class, - MetricToolModule.class, }) interface RegistryToolComponent { void inject(CheckDomainClaimsCommand command); @@ -118,6 +114,4 @@ interface RegistryToolComponent { void inject(WhoisQueryCommand command); AppEngineConnection appEngineConnection(); - - MetricWriter metricWriter(); } diff --git a/java/google/registry/tools/logging.properties b/java/google/registry/tools/logging.properties index d66a2fcff..f724c987e 100644 --- a/java/google/registry/tools/logging.properties +++ b/java/google/registry/tools/logging.properties @@ -2,7 +2,4 @@ handlers = java.util.logging.ConsoleHandler .level = INFO com.google.wrappers.base.GoogleInit.level = WARNING com.google.monitoring.metrics.MetricRegistryImpl.level = WARNING -com.google.monitoring.metrics.MetricReporter.level = WARNING -com.google.monitoring.metrics.MetricExporter.level = WARNING -com.google.monitoring.metrics.stackdriver.StackdriverWriter.level = WARNING diff --git a/javatests/google/registry/tools/ShellCommandTest.java b/javatests/google/registry/tools/ShellCommandTest.java index bc6a1447d..af9a9ebca 100644 --- a/javatests/google/registry/tools/ShellCommandTest.java +++ b/javatests/google/registry/tools/ShellCommandTest.java @@ -152,7 +152,6 @@ public class ShellCommandTest { public void testMultipleCommandInvocations() throws Exception { try (RegistryCli cli = new RegistryCli("unittest", ImmutableMap.of("test_command", TestCommand.class))) { - cli.uploadMetrics = false; RegistryToolEnvironment.UNITTEST.setup(); cli.setEnvironment(RegistryToolEnvironment.UNITTEST); cli.run(new String[] {"test_command", "-x", "xval", "arg1", "arg2"}); @@ -170,7 +169,7 @@ public class ShellCommandTest { public void testNonExistentCommand() { try (RegistryCli cli = new RegistryCli("unittest", ImmutableMap.of("test_command", TestCommand.class))) { - cli.uploadMetrics = false; + cli.setEnvironment(RegistryToolEnvironment.UNITTEST); assertThrows(MissingCommandException.class, () -> cli.run(new String[] {"bad_command"})); } From 9faf7181c3ec14ca4507f4e4b1d52e9dfaf73af7 Mon Sep 17 00:00:00 2001 From: mcilwain Date: Mon, 24 Sep 2018 18:19:40 -0700 Subject: [PATCH 03/60] Index the allocation token's redemption history entry field We need this to efficiently query which tokens have and have not been redeemed. I'm also marking it as nullable for consistency with the domain name field (it's already null for unredeemed tokens). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=214366534 --- java/google/registry/model/domain/token/AllocationToken.java | 2 +- .../google/registry/model/domain/token/AllocationTokenTest.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/java/google/registry/model/domain/token/AllocationToken.java b/java/google/registry/model/domain/token/AllocationToken.java index aa783b918..cc82cb6fb 100644 --- a/java/google/registry/model/domain/token/AllocationToken.java +++ b/java/google/registry/model/domain/token/AllocationToken.java @@ -41,7 +41,7 @@ public class AllocationToken extends BackupGroupRoot implements Buildable { @Id String token; /** The key of the history entry for which the token was used. Null if not yet used. */ - Key redemptionHistoryEntry; + @Nullable @Index Key redemptionHistoryEntry; /** The fully-qualified domain name that this token is limited to, if any. */ @Nullable @Index String domainName; diff --git a/javatests/google/registry/model/domain/token/AllocationTokenTest.java b/javatests/google/registry/model/domain/token/AllocationTokenTest.java index 4395dcc68..ea4abcb79 100644 --- a/javatests/google/registry/model/domain/token/AllocationTokenTest.java +++ b/javatests/google/registry/model/domain/token/AllocationTokenTest.java @@ -48,10 +48,12 @@ public class AllocationTokenTest extends EntityTestCase { persistResource( new AllocationToken.Builder() .setToken("abc123") + .setRedemptionHistoryEntry(Key.create(HistoryEntry.class, 1L)) .setDomainName("blahdomain.fake") .setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z")) .build()), "token", + "redemptionHistoryEntry", "domainName"); } From 49e14387e7aa46a501e86a12a7786c67739ce1b7 Mon Sep 17 00:00:00 2001 From: mcilwain Date: Tue, 25 Sep 2018 10:16:02 -0700 Subject: [PATCH 04/60] Add nomulus command for deleting AllocationTokens ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=214459480 --- .../tools/DeleteAllocationTokensCommand.java | 107 +++++++++++++ .../GenerateAllocationTokensCommand.java | 4 +- java/google/registry/tools/RegistryTool.java | 1 + .../DeleteAllocationTokensCommandTest.java | 140 ++++++++++++++++++ 4 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 java/google/registry/tools/DeleteAllocationTokensCommand.java create mode 100644 javatests/google/registry/tools/DeleteAllocationTokensCommandTest.java diff --git a/java/google/registry/tools/DeleteAllocationTokensCommand.java b/java/google/registry/tools/DeleteAllocationTokensCommand.java new file mode 100644 index 000000000..aaf02f7f1 --- /dev/null +++ b/java/google/registry/tools/DeleteAllocationTokensCommand.java @@ -0,0 +1,107 @@ +// 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.tools; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.Iterables.partition; +import static com.google.common.collect.Streams.stream; +import static google.registry.model.ofy.ObjectifyService.ofy; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; +import com.googlecode.objectify.Key; +import com.googlecode.objectify.cmd.Query; +import google.registry.model.domain.token.AllocationToken; +import java.util.List; + +/** + * Command to delete unused {@link AllocationToken}s. + * + *

Allocation tokens that have been redeemed cannot be deleted. To delete a single allocation + * token, specify the entire token as the prefix. + */ +@Parameters( + separators = " =", + commandDescription = "Deletes the unused AllocationTokens with a given prefix.") +final class DeleteAllocationTokensCommand extends ConfirmingCommand + implements CommandWithRemoteApi { + + @Parameter( + names = {"-p", "--prefix"}, + description = "Allocation token prefix; if blank, deletes all unused tokens", + required = true) + private String prefix; + + @Parameter( + names = {"--with_domains"}, + description = "Allow deletion of allocation tokens with specified domains; defaults to false") + boolean withDomains; + + @Parameter( + names = {"--dry_run"}, + description = "Do not actually delete the tokens; defaults to false") + boolean dryRun; + + private static final int BATCH_SIZE = 20; + private static final Joiner JOINER = Joiner.on(", "); + + private ImmutableSet> tokensToDelete; + + @Override + public void init() { + Query query = + ofy().load().type(AllocationToken.class).filter("redemptionHistoryEntry", null); + tokensToDelete = + query.keys().list().stream() + .filter(key -> key.getName().startsWith(prefix)) + .collect(toImmutableSet()); + } + + @Override + protected String prompt() { + return String.format( + "Found %d unused tokens starting with '%s' to delete.", tokensToDelete.size(), prefix); + } + + @Override + protected String execute() { + long numDeleted = + stream(partition(tokensToDelete, BATCH_SIZE)) + .mapToLong(batch -> ofy().transact(() -> deleteBatch(batch))) + .sum(); + return String.format("Deleted %d tokens in total.", numDeleted); + } + + /** Deletes a (filtered) batch of AllocationTokens and returns how many were deleted. */ + private long deleteBatch(List> batch) { + // Load the tokens in the same transaction as they are deleted to verify they weren't redeemed + // since the query ran. This also filters out per-domain tokens if they're not to be deleted. + ImmutableSet tokensToDelete = + ofy().load().keys(batch).values().stream() + .filter(t -> withDomains || !t.getDomainName().isPresent()) + .filter(t -> !t.isRedeemed()) + .collect(toImmutableSet()); + if (!dryRun) { + ofy().delete().entities(tokensToDelete); + } + System.out.printf( + "%s tokens: %s\n", + dryRun ? "Would delete" : "Deleted", + JOINER.join(batch.stream().map(Key::getName).collect(toImmutableSet()))); + return tokensToDelete.size(); + } +} diff --git a/java/google/registry/tools/GenerateAllocationTokensCommand.java b/java/google/registry/tools/GenerateAllocationTokensCommand.java index 2daf39be6..f0cc11927 100644 --- a/java/google/registry/tools/GenerateAllocationTokensCommand.java +++ b/java/google/registry/tools/GenerateAllocationTokensCommand.java @@ -43,13 +43,13 @@ import javax.inject.Inject; import javax.inject.Named; /** Command to generate and persist {@link AllocationToken}s. */ -@NonFinalForTesting @Parameters( separators = " =", commandDescription = "Generates and persists the given number of AllocationTokens, printing each token to stdout." ) -public class GenerateAllocationTokensCommand implements CommandWithRemoteApi { +@NonFinalForTesting +class GenerateAllocationTokensCommand implements CommandWithRemoteApi { @Parameter( names = {"-p", "--prefix"}, diff --git a/java/google/registry/tools/RegistryTool.java b/java/google/registry/tools/RegistryTool.java index a16b59433..ac47090d9 100644 --- a/java/google/registry/tools/RegistryTool.java +++ b/java/google/registry/tools/RegistryTool.java @@ -48,6 +48,7 @@ public final class RegistryTool { .put("create_sandbox_tld", CreateSandboxTldCommand.class) .put("create_tld", CreateTldCommand.class) .put("curl", CurlCommand.class) + .put("delete_allocation_tokens", DeleteAllocationTokensCommand.class) .put("delete_domain", DeleteDomainCommand.class) .put("delete_host", DeleteHostCommand.class) .put("delete_premium_list", DeletePremiumListCommand.class) diff --git a/javatests/google/registry/tools/DeleteAllocationTokensCommandTest.java b/javatests/google/registry/tools/DeleteAllocationTokensCommandTest.java new file mode 100644 index 000000000..0ce485f23 --- /dev/null +++ b/javatests/google/registry/tools/DeleteAllocationTokensCommandTest.java @@ -0,0 +1,140 @@ +// 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.tools; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.testing.DatastoreHelper.persistResource; +import static google.registry.testing.JUnitBackports.assertThrows; + +import com.beust.jcommander.ParameterException; +import com.googlecode.objectify.Key; +import google.registry.model.domain.token.AllocationToken; +import google.registry.model.reporting.HistoryEntry; +import java.util.Collection; +import javax.annotation.Nullable; +import org.junit.Before; +import org.junit.Test; + +/** Unit tests for {@link DeleteAllocationTokensCommand}. */ +public class DeleteAllocationTokensCommandTest + extends CommandTestCase { + + private AllocationToken preRed1; + private AllocationToken preRed2; + private AllocationToken preNot1; + private AllocationToken preNot2; + private AllocationToken othrRed; + private AllocationToken othrNot; + + @Before + public void init() { + preRed1 = persistToken("prefix12345AA", null, true); + preRed2 = persistToken("prefixgh8907a", null, true); + preNot1 = persistToken("prefix2978204", null, false); + preNot2 = persistToken("prefix8ZZZhs8", null, false); + othrRed = persistToken("h97987sasdfhh", null, true); + othrNot = persistToken("asdgfho7HASDS", null, false); + } + + @Test + public void test_deleteAllUnredeemedTokens_whenEmptyPrefixSpecified() throws Exception { + runCommandForced("--prefix", ""); + assertThat(reloadTokens(preNot1, preNot2, othrNot)).isEmpty(); + assertThat(reloadTokens(preRed1, preRed2, othrRed)).containsExactly(preRed1, preRed2, othrRed); + } + + @Test + public void test_deleteOnlyUnredeemedTokensWithPrefix() throws Exception { + runCommandForced("--prefix", "prefix"); + assertThat(reloadTokens(preNot1, preNot2)).isEmpty(); + assertThat(reloadTokens(preRed1, preRed2, othrRed, othrNot)) + .containsExactly(preRed1, preRed2, othrRed, othrNot); + } + + @Test + public void test_deleteSingleAllocationToken() throws Exception { + runCommandForced("--prefix", "asdgfho7HASDS"); + assertThat(reloadTokens(othrNot)).isEmpty(); + assertThat(reloadTokens(preRed1, preRed2, preNot1, preNot2, othrRed)) + .containsExactly(preRed1, preRed2, preNot1, preNot2, othrRed); + } + + @Test + public void test_deleteTokensWithNonExistentPrefix_doesNothing() throws Exception { + runCommandForced("--prefix", "nonexistent"); + assertThat(reloadTokens(preRed1, preRed2, preNot1, preNot2, othrRed, othrNot)) + .containsExactly(preRed1, preRed2, preNot1, preNot2, othrRed, othrNot); + } + + @Test + public void test_dryRun_deletesNothing() throws Exception { + runCommandForced("--prefix", "", "--dry_run"); + assertThat(reloadTokens(preRed1, preRed2, preNot1, preNot2, othrRed, othrNot)) + .containsExactly(preRed1, preRed2, preNot1, preNot2, othrRed, othrNot); + } + + @Test + public void test_defaultOptions_doesntDeletePerDomainTokens() throws Exception { + AllocationToken preDom1 = persistToken("prefixasdfg897as", "foo.bar", false); + AllocationToken preDom2 = persistToken("prefix98HAZXadbn", "foo.bar", true); + runCommandForced("--prefix", "prefix"); + assertThat(reloadTokens(preNot1, preNot2)).isEmpty(); + assertThat(reloadTokens(preRed1, preRed2, preDom1, preDom2, othrRed, othrNot)) + .containsExactly(preRed1, preRed2, preDom1, preDom2, othrRed, othrNot); + } + + @Test + public void test_withDomains_doesDeletePerDomainTokens() throws Exception { + AllocationToken preDom1 = persistToken("prefixasdfg897as", "foo.bar", false); + AllocationToken preDom2 = persistToken("prefix98HAZXadbn", "foo.bar", true); + runCommandForced("--prefix", "prefix", "--with_domains"); + assertThat(reloadTokens(preNot1, preNot2, preDom1)).isEmpty(); + assertThat(reloadTokens(preRed1, preRed2, preDom2, othrRed, othrNot)) + .containsExactly(preRed1, preRed2, preDom2, othrRed, othrNot); + } + + @Test + public void test_batching() throws Exception { + for (int i = 0; i < 50; i++) { + persistToken(String.format("batch%2d", i), null, i % 2 == 0); + } + assertThat(ofy().load().type(AllocationToken.class).count()).isEqualTo(56); + runCommandForced("--prefix", "batch"); + assertThat(ofy().load().type(AllocationToken.class).count()).isEqualTo(56 - 25); + } + + @Test + public void test_prefixIsRequired() { + ParameterException thrown = assertThrows(ParameterException.class, () -> runCommandForced()); + assertThat(thrown) + .hasMessageThat() + .isEqualTo("The following option is required: -p, --prefix "); + } + + private static AllocationToken persistToken( + String token, @Nullable String domainName, boolean redeemed) { + AllocationToken.Builder builder = + new AllocationToken.Builder().setToken(token).setDomainName(domainName); + if (redeemed) { + builder.setRedemptionHistoryEntry(Key.create(HistoryEntry.class, 1051L)); + } + return persistResource(builder.build()); + } + + private static Collection reloadTokens(AllocationToken ... tokens) { + return ofy().load().entities(tokens).values(); + } +} From 6bddd5a8cb86ecc24291e055333c0035c9152e9d Mon Sep 17 00:00:00 2001 From: guyben Date: Tue, 25 Sep 2018 10:16:12 -0700 Subject: [PATCH 05/60] Send the "resource" ID in each resource action This is an intermediate CL, part of the Registrar Console cleanup. TL;DR: - the current state: resource.js points to a resource TYPE on the server (only registrars can be resources right now), but the specific resource is selected based on the user (we select the "first resource of this type that the user has access to) - new state: resource.js points to a SPECIFIC resource (TYPE + ID). In this CL the server still chooses the resource like before (first one that user has access to) but we make sure the returned resource is the same one we requested. In a subsequent CL we will use the requested ID to load the resource, and then make sure the user has access to that resource. --------------------------- When loading the RegistrarConsole HTML page, the server determines which clientId belongs to the user ("guesses" it by looking for the first registrar that has this user as contact). It sends the relevant clientId back with the page load. However, this information isn't currently used in the JS requests to read / update the registrar. Instead, currently the client ID is guessed again for each JS access to the server. It is also saved again in the client's "session" cookie. As a result, it is theoretically possible to have the JS access a different clientID than the original page load (not likely, since it requires a single user registered for multiple registrars AND that the contacts change for the original registrar). So our goal is to only have a single clientID "value" instead of the 3 we currently have for JS requests (the one from the initial page load, the one saved in the session cookie, the one guessed on the JS request) As a first step, we send over the "initial page load" clientId on every JS request, and make sure the "session + guessed" value is equal to that one. Later we will remove the "session+guessed" values from the RegistrarSettings, using the "initial page load" clientID instead. In addition to the "nicer code" implications, having the clientID from the initial page load always used means it'll be easy to have a clientID selection option for users who have access to multiple clientIDs (such as admins) SECURITY NOTE:the choice of clientID has no security implication since we make sure the user has access to the clientID no matter how we actually choose the clientID on every single server request. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=214459506 --- .../registry/ui/js/registrar/console.js | 3 +- java/google/registry/ui/js/resource.js | 6 ++- .../registrar/RegistrarSettingsAction.java | 15 +++++++ .../ui/js/registrar/contact_settings_test.js | 44 ++++++++++++++----- .../ui/js/registrar/security_settings_test.js | 8 ++-- .../ui/js/registrar/whois_settings_test.js | 10 ++--- javatests/google/registry/ui/js/testing.js | 9 ++-- .../server/registrar/ContactSettingsTest.java | 10 +++-- .../RegistrarSettingsActionTest.java | 26 ++++++++++- .../registrar/SecuritySettingsTest.java | 8 ++-- .../server/registrar/WhoisSettingsTest.java | 12 +++-- .../registrar/testdata/update_registrar.json | 1 + .../update_registrar_duplicate_contacts.json | 1 + 13 files changed, 113 insertions(+), 40 deletions(-) diff --git a/java/google/registry/ui/js/registrar/console.js b/java/google/registry/ui/js/registrar/console.js index 0796e0c5e..2fc49b3f9 100644 --- a/java/google/registry/ui/js/registrar/console.js +++ b/java/google/registry/ui/js/registrar/console.js @@ -139,7 +139,8 @@ registry.registrar.Console.prototype.handleHashChange = function() { } var oldComponent = this.component_; const resource = new registry.Resource( - new goog.Uri('/registrar-settings'), this.params.xsrfToken); + new goog.Uri('/registrar-settings'), this.params.clientId, + this.params.xsrfToken); this.component_ = new componentCtor(this, resource); this.registerDisposable(this.component_); this.component_.basePath = type; diff --git a/java/google/registry/ui/js/resource.js b/java/google/registry/ui/js/resource.js index c58bfe5ea..5209672e2 100644 --- a/java/google/registry/ui/js/resource.js +++ b/java/google/registry/ui/js/resource.js @@ -25,13 +25,16 @@ goog.forwardDeclare('goog.Uri'); * Provide a CRUD view of a server resource. * * @param {!goog.Uri} baseUri Target RESTful resource. + * @param {string} id the ID of the target resource * @param {string} xsrfToken Security token to pass back to the server. * @extends {registry.Session} * @constructor */ -registry.Resource = function(baseUri, xsrfToken) { +registry.Resource = function(baseUri, id, xsrfToken) { registry.Resource.base(this, 'constructor', baseUri, xsrfToken, registry.Session.ContentType.JSON); + /** @const @private {string} the ID of the target resource. */ + this.id_ = id; }; goog.inherits(registry.Resource, registry.Session); @@ -73,5 +76,6 @@ registry.Resource.prototype.send_ = var req = {}; req['op'] = opCode; req['args'] = argsObj; + req['id'] = this.id_; this.sendXhrIo(goog.json.serialize(req), callback); }; diff --git a/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java b/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java index 83d98cb00..a3fd9cb5e 100644 --- a/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java +++ b/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java @@ -74,6 +74,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA static final String OP_PARAM = "op"; static final String ARGS_PARAM = "args"; + static final String ID_PARAM = "id"; @Inject HttpServletRequest request; @Inject JsonActionRunner jsonActionRunner; @@ -100,6 +101,20 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA } Registrar initialRegistrar = sessionUtils.getRegistrarForAuthResult(request, authResult); + // Check that the clientId requested is the same as the one we get in the + // getRegistrarForAuthResult. + // TODO(b/113925293): remove this check, and instead use the requested clientId to select the + // registrar (in a secure way making sure authResult has access to that registrar!) + String clientId = (String) input.get(ID_PARAM); + if (Strings.isNullOrEmpty(clientId)) { + throw new BadRequestException(String.format("Missing key for resource ID: %s", ID_PARAM)); + } + if (!clientId.equals(initialRegistrar.getClientId())) { + throw new BadRequestException( + String.format( + "User's clientId changed from %s to %s. Please reload page", + clientId, initialRegistrar.getClientId())); + } // Process the operation. Though originally derived from a CRUD // handler, registrar-settings really only supports read and update. String op = Optional.ofNullable((String) input.get(OP_PARAM)).orElse("read"); diff --git a/javatests/google/registry/ui/js/registrar/contact_settings_test.js b/javatests/google/registry/ui/js/registrar/contact_settings_test.js index 66c78b3f4..330423e62 100644 --- a/javatests/google/registry/ui/js/registrar/contact_settings_test.js +++ b/javatests/google/registry/ui/js/registrar/contact_settings_test.js @@ -82,7 +82,7 @@ function testCollectionView() { registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'read', args: {}}, + {op: 'read', id: 'testClientId', args: {}}, { status: 'SUCCESS', message: 'OK', @@ -106,7 +106,7 @@ function testItemView() { registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'read', args: {}}, + {op: 'read', id: 'testClientId', args: {}}, { status: 'SUCCESS', message: 'OK', @@ -149,6 +149,7 @@ function testItemEdit() { '/registrar-settings', { op: 'update', + id: 'testClientId', args: { contacts: [testContact], readonly: false @@ -181,6 +182,7 @@ function testChangeContactTypes() { '/registrar-settings', { op: 'update', + id: 'testClientId', args: { contacts: [testContact], readonly: false @@ -204,7 +206,7 @@ function testOneOfManyUpdate() { registry.registrar.ConsoleTestUtil.visit(test, { path: 'contact-settings/test@example.com', xsrfToken: test.testXsrfToken, - testClientId: test.testClientId + clientId: test.testClientId }); var testContacts = [ createTestContact('new1@asdf.com'), @@ -214,7 +216,7 @@ function testOneOfManyUpdate() { registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'read', args: {}}, + {op: 'read', id: 'testClientId', args: {}}, { status: 'SUCCESS', message: 'OK', @@ -235,7 +237,14 @@ function testOneOfManyUpdate() { registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'update', args: {contacts: testContacts, readonly: false}}, + { + op: 'update', + id: 'testClientId', + args: { + contacts: testContacts, + readonly: false, + }, + }, { status: 'SUCCESS', message: 'OK', @@ -251,13 +260,15 @@ function testDomainWhoisAbuseContactOverride() { registry.registrar.ConsoleTestUtil.visit(test, { path: 'contact-settings/test@example.com', xsrfToken: test.testXsrfToken, - testClientId: test.testClientId + clientId: test.testClientId }); var oldDomainWhoisAbuseContact = createTestContact('old@asdf.com'); oldDomainWhoisAbuseContact.visibleInDomainWhoisAsAbuse = true; var testContacts = [oldDomainWhoisAbuseContact, testContact]; registry.testing.assertReqMockRsp( - test.testXsrfToken, '/registrar-settings', {op: 'read', args: {}}, + test.testXsrfToken, + '/registrar-settings', + {op: 'read', id: 'testClientId', args: {}}, {status: 'SUCCESS', message: 'OK', results: [{contacts: testContacts}]}); // Edit testContact. registry.testing.click($('reg-app-btn-edit')); @@ -271,8 +282,13 @@ function testDomainWhoisAbuseContactOverride() { testContact.visibleInDomainWhoisAsAbuse = true; oldDomainWhoisAbuseContact.visibleInDomainWhoisAsAbuse = false; registry.testing.assertReqMockRsp( - test.testXsrfToken, '/registrar-settings', - {op: 'update', args: {contacts: testContacts, readonly: false}}, + test.testXsrfToken, + '/registrar-settings', + { + op: 'update', + id: 'testClientId', + args: {contacts: testContacts, readonly: false}, + }, {status: 'SUCCESS', message: 'OK', results: [{contacts: testContacts}]}); } @@ -281,7 +297,7 @@ function testDelete() { registry.registrar.ConsoleTestUtil.visit(test, { path: 'contact-settings/test@example.com', xsrfToken: test.testXsrfToken, - testClientId: test.testClientId + clientId: test.testClientId }); var testContacts = [ createTestContact('new1@asdf.com'), @@ -291,7 +307,7 @@ function testDelete() { registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'read', args: {}}, + {op: 'read', id: 'testClientId', args: {}}, { status: 'SUCCESS', message: 'OK', @@ -309,7 +325,11 @@ function testDelete() { registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'update', args: {contacts: testContacts, readonly: false}}, + { + op: 'update', + id: 'testClientId', + args: {contacts: testContacts, readonly: false}, + }, { status: 'SUCCESS', message: 'OK', diff --git a/javatests/google/registry/ui/js/registrar/security_settings_test.js b/javatests/google/registry/ui/js/registrar/security_settings_test.js index 0babd4512..972ddca0c 100644 --- a/javatests/google/registry/ui/js/registrar/security_settings_test.js +++ b/javatests/google/registry/ui/js/registrar/security_settings_test.js @@ -80,12 +80,12 @@ function testView() { registry.registrar.ConsoleTestUtil.visit(test, { path: 'security-settings', xsrfToken: test.testXsrfToken, - testClientId: test.testClientId + clientId: test.testClientId }); registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'read', args: {}}, + {op: 'read', id: 'testClientId', args: {}}, { status: 'SUCCESS', message: 'OK', @@ -116,7 +116,7 @@ function testEdit() { registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'update', args: { + {op: 'update', id: 'testClientId', args: { clientCertificate: exampleCert, clientCertificateHash: null, failoverClientCertificate: 'bourgeois blues', @@ -137,7 +137,7 @@ function testEdit() { registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'read', args: {}}, + {op: 'read', id: 'testClientId', args: {}}, {status: 'SUCCESS', message: 'OK', results: [expectedRegistrar]}); diff --git a/javatests/google/registry/ui/js/registrar/whois_settings_test.js b/javatests/google/registry/ui/js/registrar/whois_settings_test.js index cc708d0be..c3c5db219 100644 --- a/javatests/google/registry/ui/js/registrar/whois_settings_test.js +++ b/javatests/google/registry/ui/js/registrar/whois_settings_test.js @@ -98,13 +98,13 @@ function testView() { registry.registrar.ConsoleTestUtil.visit(test, { path: 'whois-settings', xsrfToken: test.testXsrfToken, - testClientId: test.testClientId + clientId: test.testClientId }); var testRegistrar = createTestRegistrar(); registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'read', args: {}}, + {op: 'read', id: 'testClientId', args: {}}, { status: 'SUCCESS', message: 'OK', @@ -129,7 +129,7 @@ function testEdit() { registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'update', args: parsed}, + {op: 'update', id: 'testClientId', args: parsed}, { status: 'SUCCESS', message: 'OK', @@ -149,7 +149,7 @@ function testEditFieldError_insertsError() { registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'update', args: parsed}, + {op: 'update', id: 'testClientId', args: parsed}, { status: 'ERROR', field: 'phoneNumber', @@ -173,7 +173,7 @@ function testEditNonFieldError_showsButterBar() { registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', - {op: 'update', args: parsed}, + {op: 'update', id: 'testClientId', args: parsed}, { status: 'ERROR', message: errMsg diff --git a/javatests/google/registry/ui/js/testing.js b/javatests/google/registry/ui/js/testing.js index 84984c3f4..8d89987dd 100644 --- a/javatests/google/registry/ui/js/testing.js +++ b/javatests/google/registry/ui/js/testing.js @@ -106,11 +106,10 @@ registry.testing.assertObjectEqualsPretty = function(a, b) { try { assertObjectEquals(a, b); } catch (e) { - throw Error(e.message + '\n' + - 'expected: ' + - registry.testing.pretty_.format(a) + '\n' + - 'got: ' + - registry.testing.pretty_.format(b)); + e.message = e.message + '\n' + + 'expected: ' + registry.testing.pretty_.format(a) + '\n' + + 'got: ' + registry.testing.pretty_.format(b); + throw e; } }; diff --git a/javatests/google/registry/ui/server/registrar/ContactSettingsTest.java b/javatests/google/registry/ui/server/registrar/ContactSettingsTest.java index d27561321..7ae6a0043 100644 --- a/javatests/google/registry/ui/server/registrar/ContactSettingsTest.java +++ b/javatests/google/registry/ui/server/registrar/ContactSettingsTest.java @@ -45,6 +45,7 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase { public void testPost_readContacts_success() { Map response = action.handleJsonRequest(ImmutableMap.of( "op", "read", + "id", CLIENT_ID, "args", ImmutableMap.of())); @SuppressWarnings("unchecked") List> results = (List>) response.get("results"); @@ -56,6 +57,7 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase { public void testPost_loadSaveRegistrar_success() { Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", loadRegistrar(CLIENT_ID).toJsonMap())); assertThat(response).containsEntry("status", "SUCCESS"); } @@ -75,7 +77,7 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase { Map regMap = registrar.toJsonMap(); regMap.put("contacts", ImmutableList.of(adminContact1)); Map response = - action.handleJsonRequest(ImmutableMap.of("op", "update", "args", regMap)); + action.handleJsonRequest(ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", regMap)); assertThat(response).containsEntry("status", "SUCCESS"); RegistrarContact newContact = new RegistrarContact.Builder() @@ -98,6 +100,7 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase { .build().toJsonMap())); Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", reqJson)); assertThat(response).containsEntry("status", "ERROR"); assertThat(response).containsEntry("message", "Must have at least one " @@ -123,6 +126,7 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase { reqJson.put("contacts", ImmutableList.of(rc.toJsonMap())); Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", reqJson)); assertThat(response).containsEntry("status", "ERROR"); assertThat(response).containsEntry("message", "Please provide a phone number for at least one " @@ -148,7 +152,7 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase { Map reqJson = registrar.toJsonMap(); reqJson.put("contacts", ImmutableList.of(rc.toJsonMap())); Map response = - action.handleJsonRequest(ImmutableMap.of("op", "update", "args", reqJson)); + action.handleJsonRequest(ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", reqJson)); assertThat(response).containsEntry("status", "ERROR"); assertThat(response) .containsEntry( @@ -174,7 +178,7 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase { Map reqJson = registrar.toJsonMap(); reqJson.put("contacts", ImmutableList.of(rc.toJsonMap())); Map response = - action.handleJsonRequest(ImmutableMap.of("op", "update", "args", reqJson)); + action.handleJsonRequest(ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", reqJson)); assertThat(response).containsEntry("status", "ERROR"); assertThat(response) .containsEntry( diff --git a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java index 71dc6d7c3..d84a386e9 100644 --- a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java +++ b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java @@ -32,6 +32,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import google.registry.export.sheet.SyncRegistrarsSheetAction; import google.registry.model.registrar.Registrar; +import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.ForbiddenException; import google.registry.request.auth.AuthResult; import google.registry.testing.CertificateSamples; @@ -94,18 +95,33 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase * This is the default read test for the registrar settings actions. */ @Test - public void testRead_authorized_returnsRegistrarJson() { - Map response = action.handleJsonRequest(ImmutableMap.of()); + public void testSuccess_readRegistrarInfo_authorized() { + Map response = action.handleJsonRequest(ImmutableMap.of("id", CLIENT_ID)); assertThat(response).containsExactly( "status", "SUCCESS", "message", "Success", "results", asList(loadRegistrar(CLIENT_ID).toJsonMap())); } + /** + * We got a different CLIENT_ID from the JS than the one we find ourself. + * + *

This might happen if the user's "guessed" registrar changes after the initial page load. For + * example, if the user was added as contact to a different registrar, or removed as contact from + * the current registrar (but is still a contact of a different one, so the "guessing" works). + */ + @Test + public void testFailure_readRegistrarInfo_differentClientId() { + assertThrows( + BadRequestException.class, + () -> action.handleJsonRequest(ImmutableMap.of("id", "different"))); + } + @Test public void testUpdate_emptyJsonObject_errorLastUpdateTimeFieldRequired() { Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", ImmutableMap.of())); assertThat(response).containsExactly( "status", "ERROR", @@ -119,6 +135,7 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase public void testUpdate_noEmail_errorEmailFieldRequired() { Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", ImmutableMap.of("lastUpdateTime", getLastUpdateTime()))); assertThat(response).containsExactly( "status", "ERROR", @@ -134,6 +151,7 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", ImmutableMap.of("lastUpdateTime", getLastUpdateTime()))); assertThat(response).containsExactly( "status", "SUCCESS", @@ -145,6 +163,7 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase public void testUpdate_badEmail_errorEmailField() { Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", ImmutableMap.of( "lastUpdateTime", getLastUpdateTime(), "emailAddress", "lolcat"))); @@ -160,6 +179,7 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase public void testPost_nonParsableTime_getsAngry() { Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", ImmutableMap.of("lastUpdateTime", "cookies"))); assertThat(response).containsExactly( "status", "ERROR", @@ -173,6 +193,7 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase public void testPost_nonAsciiCharacters_getsAngry() { Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", ImmutableMap.of( "lastUpdateTime", getLastUpdateTime(), "emailAddress", "ヘ(◕。◕ヘ)@example.com"))); @@ -195,6 +216,7 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase action.handleJsonRequest( ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", setter.apply(registrar.asBuilder(), newValue).build().toJsonMap())); registrar = loadRegistrar(CLIENT_ID); diff --git a/javatests/google/registry/ui/server/registrar/SecuritySettingsTest.java b/javatests/google/registry/ui/server/registrar/SecuritySettingsTest.java index b5d383c10..d48b6bc14 100644 --- a/javatests/google/registry/ui/server/registrar/SecuritySettingsTest.java +++ b/javatests/google/registry/ui/server/registrar/SecuritySettingsTest.java @@ -51,6 +51,7 @@ public class SecuritySettingsTest extends RegistrarSettingsActionTestCase { .build(); Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", modified.toJsonMap())); // Empty whoisServer field should be set to default by server. modified = @@ -69,6 +70,7 @@ public class SecuritySettingsTest extends RegistrarSettingsActionTestCase { reqJson.put("clientCertificate", "BLAH"); Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", + "id", CLIENT_ID, "args", reqJson)); assertThat(response).containsEntry("status", "ERROR"); assertThat(response).containsEntry("message", "Invalid X.509 PEM certificate"); @@ -80,7 +82,7 @@ public class SecuritySettingsTest extends RegistrarSettingsActionTestCase { jsonMap.put("clientCertificate", SAMPLE_CERT); jsonMap.put("failoverClientCertificate", null); Map response = action.handleJsonRequest(ImmutableMap.of( - "op", "update", "args", jsonMap)); + "op", "update", "id", CLIENT_ID, "args", jsonMap)); assertThat(response).containsEntry("status", "SUCCESS"); Registrar registrar = loadRegistrar(CLIENT_ID); assertThat(registrar.getClientCertificate()).isEqualTo(SAMPLE_CERT); @@ -94,7 +96,7 @@ public class SecuritySettingsTest extends RegistrarSettingsActionTestCase { Map jsonMap = loadRegistrar(CLIENT_ID).toJsonMap(); jsonMap.put("failoverClientCertificate", SAMPLE_CERT2); Map response = action.handleJsonRequest(ImmutableMap.of( - "op", "update", "args", jsonMap)); + "op", "update", "id", CLIENT_ID, "args", jsonMap)); assertThat(response).containsEntry("status", "SUCCESS"); Registrar registrar = loadRegistrar(CLIENT_ID); assertThat(registrar.getFailoverClientCertificate()).isEqualTo(SAMPLE_CERT2); @@ -116,7 +118,7 @@ public class SecuritySettingsTest extends RegistrarSettingsActionTestCase { jsonMap.put("clientCertificate", null); jsonMap.put("failoverClientCertificate", ""); Map response = action.handleJsonRequest(ImmutableMap.of( - "op", "update", "args", jsonMap)); + "op", "update", "id", CLIENT_ID, "args", jsonMap)); assertThat(response).containsEntry("status", "SUCCESS"); Registrar registrar = loadRegistrar(CLIENT_ID); assertThat(registrar.getClientCertificate()).isEqualTo(SAMPLE_CERT); diff --git a/javatests/google/registry/ui/server/registrar/WhoisSettingsTest.java b/javatests/google/registry/ui/server/registrar/WhoisSettingsTest.java index 48cb4d9c3..3f4672100 100644 --- a/javatests/google/registry/ui/server/registrar/WhoisSettingsTest.java +++ b/javatests/google/registry/ui/server/registrar/WhoisSettingsTest.java @@ -56,7 +56,8 @@ public class WhoisSettingsTest extends RegistrarSettingsActionTestCase { .build()) .build(); Map response = - action.handleJsonRequest(ImmutableMap.of("op", "update", "args", modified.toJsonMap())); + action.handleJsonRequest( + ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", modified.toJsonMap())); assertThat(response.get("status")).isEqualTo("SUCCESS"); assertThat(response.get("results")).isEqualTo(asList(modified.toJsonMap())); assertThat(loadRegistrar(CLIENT_ID)).isEqualTo(modified); @@ -80,7 +81,8 @@ public class WhoisSettingsTest extends RegistrarSettingsActionTestCase { .build()) .build(); Map response = - action.handleJsonRequest(ImmutableMap.of("op", "update", "args", modified.toJsonMap())); + action.handleJsonRequest( + ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", modified.toJsonMap())); assertThat(response.get("status")).isEqualTo("ERROR"); assertThat(response.get("field")).isEqualTo("localizedAddress.state"); assertThat(response.get("message")).isEqualTo("Unknown US state code."); @@ -105,7 +107,8 @@ public class WhoisSettingsTest extends RegistrarSettingsActionTestCase { .build()) .build(); Map response = - action.handleJsonRequest(ImmutableMap.of("op", "update", "args", modified.toJsonMap())); + action.handleJsonRequest( + ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", modified.toJsonMap())); assertThat(response.get("status")).isEqualTo("ERROR"); assertThat(response.get("field")).isEqualTo("localizedAddress.street[1]"); assertThat((String) response.get("message")) @@ -118,7 +121,8 @@ public class WhoisSettingsTest extends RegistrarSettingsActionTestCase { Registrar modified = loadRegistrar(CLIENT_ID).asBuilder().setWhoisServer("tears@dry.tragical.lol").build(); Map response = - action.handleJsonRequest(ImmutableMap.of("op", "update", "args", modified.toJsonMap())); + action.handleJsonRequest( + ImmutableMap.of("op", "update", "id", CLIENT_ID, "args", modified.toJsonMap())); assertThat(response.get("status")).isEqualTo("ERROR"); assertThat(response.get("field")).isEqualTo("whoisServer"); assertThat(response.get("message")).isEqualTo("Not a valid hostname."); diff --git a/javatests/google/registry/ui/server/registrar/testdata/update_registrar.json b/javatests/google/registry/ui/server/registrar/testdata/update_registrar.json index f701bfe0d..672e1bd33 100644 --- a/javatests/google/registry/ui/server/registrar/testdata/update_registrar.json +++ b/javatests/google/registry/ui/server/registrar/testdata/update_registrar.json @@ -1,5 +1,6 @@ { "op": "update", + "id": "TheRegistrar", "args": { "clientIdentifier": "theregistrar", "driveFolderId": null, diff --git a/javatests/google/registry/ui/server/registrar/testdata/update_registrar_duplicate_contacts.json b/javatests/google/registry/ui/server/registrar/testdata/update_registrar_duplicate_contacts.json index 228eaf39a..b5b979c1f 100644 --- a/javatests/google/registry/ui/server/registrar/testdata/update_registrar_duplicate_contacts.json +++ b/javatests/google/registry/ui/server/registrar/testdata/update_registrar_duplicate_contacts.json @@ -1,5 +1,6 @@ { "op": "update", + "id": "TheRegistrar", "args": { "clientIdentifier": "theregistrar", "driveFolderId": null, From 84a0ace2eaa124b020f222ad0696eaea2e5e0374 Mon Sep 17 00:00:00 2001 From: guyben Date: Wed, 26 Sep 2018 10:53:14 -0700 Subject: [PATCH 06/60] Clean up registrar console login flow Replaced the plethora of inter winding access functions and inputs in SessionUtils with just 2 functions, that both accept the same type for the user (AuthResult): guessRegistrarForUser: given an AuthResult, finds a registrar that they have access to. If none is found - a ForbiddenException is thrown. getRegistrarForUser[Cached]: (maybe should be called getRegistrarOnBehalfOfUser?) given an AuthResult and a clientId, loads and returns the registrar ONLY IF the user has access to it. Otherwise throws a ForbiddenException. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=214630657 --- java/google/registry/rdap/RdapActionBase.java | 37 +- .../registry/request/auth/AuthResult.java | 6 + .../ui/server/registrar/ConsoleUiAction.java | 35 +- .../registrar/RegistrarSettingsAction.java | 37 +- .../ui/server/registrar/SessionUtils.java | 221 +++++------ .../registry/rdap/RdapDomainActionTest.java | 26 +- .../rdap/RdapDomainSearchActionTest.java | 27 +- .../registry/rdap/RdapEntityActionTest.java | 26 +- .../rdap/RdapEntitySearchActionTest.java | 26 +- .../rdap/RdapNameserverActionTest.java | 39 +- .../rdap/RdapNameserverSearchActionTest.java | 28 +- .../server/registrar/ConsoleUiActionTest.java | 16 +- .../RegistrarSettingsActionTest.java | 58 ++- .../RegistrarSettingsActionTestCase.java | 15 +- .../registrar/SecuritySettingsTest.java | 3 - .../ui/server/registrar/SessionUtilsTest.java | 354 +++++++----------- 16 files changed, 431 insertions(+), 523 deletions(-) diff --git a/java/google/registry/rdap/RdapActionBase.java b/java/google/registry/rdap/RdapActionBase.java index bfbc8e984..c08a71f78 100644 --- a/java/google/registry/rdap/RdapActionBase.java +++ b/java/google/registry/rdap/RdapActionBase.java @@ -16,7 +16,6 @@ package google.registry.rdap; import static com.google.common.base.Charsets.UTF_8; import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.DateTimeUtils.END_OF_TIME; @@ -57,7 +56,6 @@ import java.util.List; import java.util.Optional; import javax.annotation.Nullable; import javax.inject.Inject; -import javax.servlet.http.HttpServletRequest; import org.joda.time.DateTime; import org.json.simple.JSONValue; @@ -87,7 +85,6 @@ public abstract class RdapActionBase implements Runnable { INCLUDE } - @Inject HttpServletRequest request; @Inject Response response; @Inject @RequestMethod Action.Method requestMethod; @Inject @RequestPath String requestPath; @@ -176,6 +173,23 @@ public abstract class RdapActionBase implements Runnable { } } + /** + * Returns a clientId the given user has console access on, or Optional.empty if there is none. + */ + private Optional getAuthorizedClientId(AuthResult authResult) { + try { + String clientId = sessionUtils.guessClientIdForUser(authResult); + // We load the Registrar to make sure the user has access to it. We don't actually need it, + // we're just checking if an exception is thrown. + sessionUtils.getRegistrarForUserCached(clientId, authResult); + return Optional.of(clientId); + } catch (Exception e) { + logger.atWarning().withCause(e).log( + "Couldn't find registrar for User %s.", authResult.userIdForLogging()); + return Optional.empty(); + } + } + void setPayload(ImmutableMap rdapJson) { if (requestMethod == Action.Method.HEAD) { return; @@ -200,15 +214,11 @@ public abstract class RdapActionBase implements Runnable { if (userAuthInfo.isUserAdmin()) { return RdapAuthorization.ADMINISTRATOR_AUTHORIZATION; } - if (!sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)) { + Optional clientId = getAuthorizedClientId(authResult); + if (!clientId.isPresent()) { return RdapAuthorization.PUBLIC_AUTHORIZATION; } - String clientId = sessionUtils.getRegistrarClientId(request); - Optional registrar = Registrar.loadByClientIdCached(clientId); - if (!registrar.isPresent()) { - return RdapAuthorization.PUBLIC_AUTHORIZATION; - } - return RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, clientId); + return RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, clientId.get()); } /** Returns the registrar on which results should be filtered, or absent(). */ @@ -238,14 +248,9 @@ public abstract class RdapActionBase implements Runnable { if (userAuthInfo.isUserAdmin()) { return true; } - if (!sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)) { + if (!getAuthorizedClientId(authResult).isPresent()) { return false; } - String clientId = sessionUtils.getRegistrarClientId(request); - checkState( - Registrar.loadByClientIdCached(clientId).isPresent(), - "Registrar with clientId %s doesn't exist", - clientId); return true; } diff --git a/java/google/registry/request/auth/AuthResult.java b/java/google/registry/request/auth/AuthResult.java index d64b23c34..0903f26a0 100644 --- a/java/google/registry/request/auth/AuthResult.java +++ b/java/google/registry/request/auth/AuthResult.java @@ -36,6 +36,12 @@ public abstract class AuthResult { return authLevel() != AuthLevel.NONE; } + public String userIdForLogging() { + return userAuthInfo() + .map(userAuthInfo -> userAuthInfo.user().getUserId()) + .orElse(""); + } + public static AuthResult create(AuthLevel authLevel) { return new AutoValue_AuthResult(authLevel, Optional.empty()); } diff --git a/java/google/registry/ui/server/registrar/ConsoleUiAction.java b/java/google/registry/ui/server/registrar/ConsoleUiAction.java index e21a33131..11dbf510c 100644 --- a/java/google/registry/ui/server/registrar/ConsoleUiAction.java +++ b/java/google/registry/ui/server/registrar/ConsoleUiAction.java @@ -16,14 +16,15 @@ 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 google.registry.util.PreconditionsUtils.checkArgumentPresent; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY; import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; +import com.google.appengine.api.users.User; import com.google.appengine.api.users.UserService; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Supplier; +import com.google.common.flogger.FluentLogger; import com.google.common.io.Resources; import com.google.common.net.MediaType; import com.google.template.soy.data.SoyMapData; @@ -32,10 +33,10 @@ import com.google.template.soy.tofu.SoyTofu; import google.registry.config.RegistryConfig.Config; import google.registry.model.registrar.Registrar; import google.registry.request.Action; +import google.registry.request.HttpException.ForbiddenException; import google.registry.request.Response; import google.registry.request.auth.Auth; import google.registry.request.auth.AuthResult; -import google.registry.request.auth.UserAuthInfo; import google.registry.security.XsrfTokenManager; import google.registry.ui.server.SoyTemplateUtils; import google.registry.ui.soy.registrar.ConsoleSoyInfo; @@ -49,6 +50,8 @@ import javax.servlet.http.HttpServletRequest; ) public final class ConsoleUiAction implements Runnable { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + public static final String PATH = "/registrar"; private static final Supplier TOFU_SUPPLIER = @@ -97,7 +100,7 @@ public final class ConsoleUiAction implements Runnable { response.setHeader(LOCATION, location); return; } - UserAuthInfo userAuthInfo = authResult.userAuthInfo().get(); + User user = authResult.userAuthInfo().get().user(); response.setContentType(MediaType.HTML_UTF_8); response.setHeader(X_FRAME_OPTIONS, "SAMEORIGIN"); // Disallow iframing. response.setHeader("X-Ui-Compatible", "IE=edge"); // Ask IE not to be silly. @@ -119,9 +122,24 @@ public final class ConsoleUiAction implements Runnable { .render()); return; } - data.put("username", userAuthInfo.user().getNickname()); + data.put("username", user.getNickname()); data.put("logoutUrl", userService.createLogoutURL(PATH)); - if (!sessionUtils.checkRegistrarConsoleLogin(req, userAuthInfo)) { + data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmail())); + try { + String clientId = sessionUtils.guessClientIdForUser(authResult); + data.put("clientId", clientId); + // We want to load the registrar even if we won't use it later (even if we remove the + // requireFeeExtension) - to make sure the user indeed has access to the guessed registrar. + // + // Note that not doing so (and just passing the "clientId" as given) isn't a security issue + // since we double check the access to the registrar on any read / update request. We have to + // - since the access might get revoked between the initial page load and the request! (also + // because the requests come from the browser, and can easily be faked) + Registrar registrar = sessionUtils.getRegistrarForUser(clientId, authResult); + data.put("requireFeeExtension", registrar.getPremiumPriceAckRequired()); + } catch (ForbiddenException e) { + logger.atWarning().withCause(e).log( + "User %s doesn't have access to registrar console.", authResult.userIdForLogging()); response.setStatus(SC_FORBIDDEN); response.setPayload( TOFU_SUPPLIER.get() @@ -131,13 +149,6 @@ public final class ConsoleUiAction implements Runnable { .render()); return; } - String clientId = sessionUtils.getRegistrarClientId(req); - Registrar registrar = - checkArgumentPresent( - Registrar.loadByClientIdCached(clientId), "Registrar %s does not exist", clientId); - data.put("xsrfToken", xsrfTokenManager.generateToken(userAuthInfo.user().getEmail())); - data.put("clientId", clientId); - data.put("requireFeeExtension", registrar.getPremiumPriceAckRequired()); String payload = TOFU_SUPPLIER.get() .newRenderer(ConsoleSoyInfo.MAIN) diff --git a/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java b/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java index a3fd9cb5e..74f890d4a 100644 --- a/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java +++ b/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java @@ -54,7 +54,6 @@ import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import javax.inject.Inject; -import javax.servlet.http.HttpServletRequest; import org.joda.time.DateTime; /** @@ -76,7 +75,6 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA static final String ARGS_PARAM = "args"; static final String ID_PARAM = "id"; - @Inject HttpServletRequest request; @Inject JsonActionRunner jsonActionRunner; @Inject AppEngineServiceUtils appEngineServiceUtils; @Inject AuthResult authResult; @@ -100,60 +98,51 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA throw new BadRequestException("Malformed JSON"); } - Registrar initialRegistrar = sessionUtils.getRegistrarForAuthResult(request, authResult); - // Check that the clientId requested is the same as the one we get in the - // getRegistrarForAuthResult. - // TODO(b/113925293): remove this check, and instead use the requested clientId to select the - // registrar (in a secure way making sure authResult has access to that registrar!) String clientId = (String) input.get(ID_PARAM); if (Strings.isNullOrEmpty(clientId)) { throw new BadRequestException(String.format("Missing key for resource ID: %s", ID_PARAM)); } - if (!clientId.equals(initialRegistrar.getClientId())) { - throw new BadRequestException( - String.format( - "User's clientId changed from %s to %s. Please reload page", - clientId, initialRegistrar.getClientId())); - } + // Process the operation. Though originally derived from a CRUD // handler, registrar-settings really only supports read and update. String op = Optional.ofNullable((String) input.get(OP_PARAM)).orElse("read"); @SuppressWarnings("unchecked") Map args = (Map) Optional.ofNullable(input.get(ARGS_PARAM)).orElse(ImmutableMap.of()); - logger.atInfo().log( - "Received request '%s' on registrar '%s' with args %s", - op, initialRegistrar.getClientId(), args); + logger.atInfo().log("Received request '%s' on registrar '%s' with args %s", op, clientId, args); try { switch (op) { case "update": - return update(args, initialRegistrar.getClientId()); + return update(args, clientId); case "read": - return JsonResponseHelper.create(SUCCESS, "Success", initialRegistrar.toJsonMap()); + return read(clientId); default: return JsonResponseHelper.create(ERROR, "Unknown or unsupported operation: " + op); } } catch (FormFieldException e) { logger.atWarning().withCause(e).log( - "Failed to perform operation '%s' on registrar '%s' for args %s", - op, initialRegistrar.getClientId(), args); + "Failed to perform operation '%s' on registrar '%s' for args %s", op, clientId, args); return JsonResponseHelper.createFormFieldError(e.getMessage(), e.getFieldName()); } catch (FormException e) { logger.atWarning().withCause(e).log( - "Failed to perform operation '%s' on registrar '%s' for args %s", - op, initialRegistrar.getClientId(), args); + "Failed to perform operation '%s' on registrar '%s' for args %s", op, clientId, args); return JsonResponseHelper.create(ERROR, e.getMessage()); } } + Map read(String clientId) { + return JsonResponseHelper.create( + SUCCESS, "Success", sessionUtils.getRegistrarForUser(clientId, authResult).toJsonMap()); + } + Map update(final Map args, String clientId) { return ofy() .transact( () -> { - // We load the registrar here rather than use the initialRegistrar above - to make + // We load the registrar here rather than outside of the transaction - to make // sure we have the latest version. This one is loaded inside the transaction, so it's // guaranteed to not change before we update it. - Registrar registrar = Registrar.loadByClientId(clientId).get(); + Registrar registrar = sessionUtils.getRegistrarForUser(clientId, authResult); // Verify that the registrar hasn't been changed. // To do that - we find the latest update time (or null if the registrar has been // deleted) and compare to the update time from the args. The update time in the args diff --git a/java/google/registry/ui/server/registrar/SessionUtils.java b/java/google/registry/ui/server/registrar/SessionUtils.java index 38eeecc5d..91a375e03 100644 --- a/java/google/registry/ui/server/registrar/SessionUtils.java +++ b/java/google/registry/ui/server/registrar/SessionUtils.java @@ -14,15 +14,11 @@ package google.registry.ui.server.registrar; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Verify.verify; import static google.registry.model.ofy.ObjectifyService.ofy; -import static google.registry.util.PreconditionsUtils.checkArgumentPresent; import com.google.appengine.api.users.User; import com.google.common.base.Strings; import com.google.common.flogger.FluentLogger; -import com.googlecode.objectify.Key; import google.registry.config.RegistryConfig.Config; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarContact; @@ -30,20 +26,16 @@ import google.registry.request.HttpException.ForbiddenException; import google.registry.request.auth.AuthResult; import google.registry.request.auth.UserAuthInfo; import java.util.Optional; -import javax.annotation.CheckReturnValue; +import java.util.function.Function; import javax.annotation.concurrent.Immutable; import javax.inject.Inject; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; -/** HTTP session management helper class. */ +/** Authenticated Registrar access helper class. */ @Immutable public class SessionUtils { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private static final String CLIENT_ID_ATTRIBUTE = "clientId"; - @Inject @Config("registryAdminClientId") String registryAdminClientId; @@ -52,145 +44,118 @@ public class SessionUtils { public SessionUtils() {} /** - * Checks that the authentication result indicates a user that has access to the registrar - * console, then gets the associated registrar. + * Loads Registrar on behalf of an authorised user. * - *

Throws a {@link ForbiddenException} if the user is not logged in, or not authorized to use - * the registrar console. + *

Throws a {@link ForbiddenException} if the user is not logged in, or not authorized to + * access the requested registrar. + * + * @param clientId ID of the registrar we request + * @param authResult AuthResult of the user on behalf of which we want to access the data */ - @CheckReturnValue - Registrar getRegistrarForAuthResult(HttpServletRequest request, AuthResult authResult) { - if (!authResult.userAuthInfo().isPresent()) { - throw new ForbiddenException("Not logged in"); - } - if (!checkRegistrarConsoleLogin(request, authResult.userAuthInfo().get())) { - throw new ForbiddenException("Not authorized to access Registrar Console"); - } - String clientId = getRegistrarClientId(request); - return checkArgumentPresent( - Registrar.loadByClientId(clientId), - "Registrar %s not found", - clientId); + public Registrar getRegistrarForUser(String clientId, AuthResult authResult) { + return getAndAuthorize(Registrar::loadByClientId, clientId, authResult); } /** - * Checks that the specified user has access to the Registrar Console. + * Loads a Registrar from the cache on behalf of an authorised user. * - *

This routine will first check the HTTP session (creating one if it doesn't exist) for the - * {@code clientId} attribute: + *

Throws a {@link ForbiddenException} if the user is not logged in, or not authorized to + * access the requested registrar. * - *

    - *
  • If it does not exist, then we will attempt to guess the {@link Registrar} with which the - * user is associated. The {@code clientId} of the first matching {@code Registrar} will - * then be stored to the HTTP session. - *
  • If it does exist, then we'll fetch the Registrar from Datastore to make sure access - * wasn't revoked. - *
- * - *

Note: You must ensure the user has logged in before calling this method. - * - * @return {@code false} if user does not have access, in which case the caller should write an - * error response and abort the request. + * @param clientId ID of the registrar we request + * @param authResult AuthResult of the user on behalf of which we want to access the data */ - @CheckReturnValue - public boolean checkRegistrarConsoleLogin(HttpServletRequest req, UserAuthInfo userAuthInfo) { - checkState(userAuthInfo != null, "No logged in user found"); + public Registrar getRegistrarForUserCached(String clientId, AuthResult authResult) { + return getAndAuthorize(Registrar::loadByClientIdCached, clientId, authResult); + } + + Registrar getAndAuthorize( + Function> registrarLoader, + String clientId, + AuthResult authResult) { + UserAuthInfo userAuthInfo = + authResult.userAuthInfo().orElseThrow(() -> new ForbiddenException("Not logged in")); + boolean isAdmin = userAuthInfo.isUserAdmin(); User user = userAuthInfo.user(); - HttpSession session = req.getSession(); - String clientId = (String) session.getAttribute(CLIENT_ID_ATTRIBUTE); + String gaeUserId = user.getUserId(); - // Use the clientId if it exists - if (clientId != null) { - if (!hasAccessToRegistrar(clientId, user.getUserId(), userAuthInfo.isUserAdmin())) { - logger.atInfo().log("Registrar Console access revoked: %s", clientId); - session.invalidate(); - return false; - } - logger.atInfo().log( - "Associating user %s with given registrar %s.", user.getUserId(), clientId); - return true; + Registrar registrar = + registrarLoader + .apply(clientId) + .orElseThrow( + () -> new ForbiddenException(String.format("Registrar %s not found", clientId))); + + if (isInAllowedContacts(registrar, gaeUserId)) { + logger.atInfo().log("User %s has access to registrar %s.", gaeUserId, clientId); + return registrar; } - // The clientId was null, so let's try and find a registrar this user is associated with - Optional registrar = findRegistrarForUser(user.getUserId()); - if (registrar.isPresent()) { - verify(isInAllowedContacts(registrar.get(), user.getUserId())); - logger.atInfo().log( - "Associating user %s with found registrar %s.", - user.getUserId(), registrar.get().getClientId()); - session.setAttribute(CLIENT_ID_ATTRIBUTE, registrar.get().getClientId()); - return true; + if (isAdmin && clientId.equals(registryAdminClientId)) { + // Admins have access to the registryAdminClientId even if they aren't explicitly in the + // allowed contacts + logger.atInfo().log("Allowing admin %s access to registrar %s.", gaeUserId, clientId); + return registrar; } - // We couldn't guess the registrar, but maybe the user is an admin and we can use the + throw new ForbiddenException( + String.format("User %s doesn't have access to registrar %s", gaeUserId, clientId)); + } + + /** + * Tries to guess the {@link Registrar} with which the user is associated. + * + *

Returns the {@code clientId} of a {@link Registrar} the user has access to (is on the + * contact list). If the user has access to multiple {@link Registrar}s, an arbitrary one is + * selected. If the user is an admin without access to any {@link Registrar}s, {@link + * #registryAdminClientId} is returned if it is defined. + * + *

If no {@code clientId} is found, throws a {@link ForbiddenException}. + * + *

If you want to load the {@link Registrar} object from this (or any other) {@code clientId}, + * in order to perform actions on behalf of a user, you must use {@link #getRegistrarForUser} + * which makes sure the user has permissions. + * + *

Note that this is an OPTIONAL step in the authentication - only used if we don't have any + * other clue as to the requested {@code clientId}. It is perfectly OK to get a {@code clientId} + * from any other source, as long as the registrar is then loaded using {@link + * #getRegistrarForUser}. + */ + public String guessClientIdForUser(AuthResult authResult) { + + UserAuthInfo userAuthInfo = + authResult.userAuthInfo().orElseThrow(() -> new ForbiddenException("No logged in")); + boolean isAdmin = userAuthInfo.isUserAdmin(); + User user = userAuthInfo.user(); + String gaeUserId = user.getUserId(); + + RegistrarContact contact = + ofy().load().type(RegistrarContact.class).filter("gaeUserId", gaeUserId).first().now(); + if (contact != null) { + String registrarClientId = contact.getParent().getName(); + logger.atInfo().log( + "Associating user %s with found registrar %s.", gaeUserId, registrarClientId); + return registrarClientId; + } + + // We couldn't find the registrar, but maybe the user is an admin and we can use the // registryAdminClientId - if (userAuthInfo.isUserAdmin()) { - if (Strings.isNullOrEmpty(registryAdminClientId)) { + if (isAdmin) { + if (!Strings.isNullOrEmpty(registryAdminClientId)) { logger.atInfo().log( - "Cannot associate admin user %s with configured client Id." - + " ClientId is null or empty.", - user.getUserId()); - return false; - } - if (!Registrar.loadByClientIdCached(registryAdminClientId).isPresent()) { - logger.atInfo().log( - "Cannot associate admin user %s with configured client Id %s." - + " Registrar does not exist.", - user.getUserId(), registryAdminClientId); - return false; + "User %s is an admin with no associated registrar." + + " Automatically associating the user with configured client Id %s.", + gaeUserId, registryAdminClientId); + return registryAdminClientId; } logger.atInfo().log( - "User %s is an admin with no associated registrar." - + " Automatically associating the user with configured client Id %s.", - user.getUserId(), registryAdminClientId); - session.setAttribute(CLIENT_ID_ATTRIBUTE, registryAdminClientId); - return true; + "Cannot associate admin user %s with configured client Id." + + " ClientId is null or empty.", + gaeUserId); } // We couldn't find any relevant clientId - logger.atInfo().log("User not associated with any Registrar: %s", user.getUserId()); - return false; - } - - /** - * Returns {@link Registrar} clientId associated with HTTP session. - * - * @throws IllegalStateException if you forgot to call {@link #checkRegistrarConsoleLogin}. - */ - @CheckReturnValue - public String getRegistrarClientId(HttpServletRequest req) { - String clientId = (String) req.getSession().getAttribute(CLIENT_ID_ATTRIBUTE); - checkState(clientId != null, "You forgot to call checkRegistrarConsoleLogin()"); - return clientId; - } - - /** Returns first {@link Registrar} that {@code gaeUserId} is authorized to administer. */ - private static Optional findRegistrarForUser(String gaeUserId) { - RegistrarContact contact = - ofy().load().type(RegistrarContact.class).filter("gaeUserId", gaeUserId).first().now(); - if (contact == null) { - return Optional.empty(); - } - String registrarClientId = contact.getParent().getName(); - Optional result = Registrar.loadByClientIdCached(registrarClientId); - if (!result.isPresent()) { - logger.atSevere().log( - "A contact record exists for non-existent registrar: %s.", Key.create(contact)); - } - return result; - } - - /** @see #isInAllowedContacts(Registrar, String) */ - boolean hasAccessToRegistrar(String clientId, String gaeUserId, boolean isAdmin) { - Optional registrar = Registrar.loadByClientIdCached(clientId); - if (!registrar.isPresent()) { - logger.atWarning().log("Registrar '%s' disappeared from Datastore!", clientId); - return false; - } - if (isAdmin && clientId.equals(registryAdminClientId)) { - return true; - } - return isInAllowedContacts(registrar.get(), gaeUserId); + throw new ForbiddenException( + String.format("User %s isn't associated with any registrar", gaeUserId)); } /** diff --git a/javatests/google/registry/rdap/RdapDomainActionTest.java b/javatests/google/registry/rdap/RdapDomainActionTest.java index af19848e9..00f8f784c 100644 --- a/javatests/google/registry/rdap/RdapDomainActionTest.java +++ b/javatests/google/registry/rdap/RdapDomainActionTest.java @@ -58,7 +58,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; -import javax.servlet.http.HttpServletRequest; import org.joda.time.DateTime; import org.json.simple.JSONObject; import org.json.simple.JSONValue; @@ -81,17 +80,23 @@ public class RdapDomainActionTest { @Rule public final InjectRule inject = new InjectRule(); - private final HttpServletRequest request = mock(HttpServletRequest.class); private final FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); private final SessionUtils sessionUtils = mock(SessionUtils.class); - private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); - private final UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); - private final UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true); private final RdapMetrics rdapMetrics = mock(RdapMetrics.class); private RdapDomainAction action; + private static final AuthResult AUTH_RESULT = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@user.com", "gmail.com", "12345"), false)); + + private static final AuthResult AUTH_RESULT_ADMIN = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@google.com", "gmail.com", "12345"), true)); + @Before public void setUp() { inject.setStaticField(Ofy.class, "clock", clock); @@ -262,7 +267,6 @@ public class RdapDomainActionTest { action = new RdapDomainAction(); action.clock = clock; - action.request = request; action.requestMethod = Action.Method.GET; action.fullServletPath = "https://example.com/rdap"; action.response = response; @@ -272,19 +276,17 @@ public class RdapDomainActionTest { action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); action.rdapWhoisServer = null; action.sessionUtils = sessionUtils; - action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); + action.authResult = AUTH_RESULT; action.rdapMetrics = rdapMetrics; } private void login(String clientId) { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn(clientId); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn(clientId); } private void loginAsAdmin() { - when(sessionUtils.checkRegistrarConsoleLogin(request, adminUserAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("irrelevant"); - action.authResult = AuthResult.create(AuthLevel.USER, adminUserAuthInfo); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT_ADMIN)).thenReturn("irrelevant"); + action.authResult = AUTH_RESULT_ADMIN; } private Object generateActualJson(String domainName) { diff --git a/javatests/google/registry/rdap/RdapDomainSearchActionTest.java b/javatests/google/registry/rdap/RdapDomainSearchActionTest.java index b2bb88b84..2684e72ce 100644 --- a/javatests/google/registry/rdap/RdapDomainSearchActionTest.java +++ b/javatests/google/registry/rdap/RdapDomainSearchActionTest.java @@ -71,7 +71,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; -import javax.servlet.http.HttpServletRequest; import org.joda.time.DateTime; import org.json.simple.JSONArray; import org.json.simple.JSONObject; @@ -91,14 +90,21 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { @Rule public final InjectRule inject = new InjectRule(); - private final HttpServletRequest request = mock(HttpServletRequest.class); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z")); private final SessionUtils sessionUtils = mock(SessionUtils.class); - private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); - private final UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); - private final UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true); private final RdapDomainSearchAction action = new RdapDomainSearchAction(); + private static final AuthResult AUTH_RESULT = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@user.com", "gmail.com", "12345"), false)); + + private static final AuthResult AUTH_RESULT_ADMIN = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@google.com", "gmail.com", "12345"), true)); + + private FakeResponse response = new FakeResponse(); private Registrar registrar; @@ -388,7 +394,6 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { clock.nowUtc())); action.clock = clock; - action.request = request; action.requestMethod = Action.Method.GET; action.fullServletPath = "https://example.com/rdap"; action.requestUrl = "https://example.com/rdap/domains"; @@ -401,22 +406,20 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); action.rdapWhoisServer = null; action.sessionUtils = sessionUtils; - action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); + action.authResult = AUTH_RESULT; action.rdapMetrics = rdapMetrics; action.cursorTokenParam = Optional.empty(); action.rdapResultSetMaxSize = 4; } private void login(String clientId) { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn(clientId); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn(clientId); metricRole = REGISTRAR; } private void loginAsAdmin() { - when(sessionUtils.checkRegistrarConsoleLogin(request, adminUserAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("irrelevant"); - action.authResult = AuthResult.create(AuthLevel.USER, adminUserAuthInfo); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn("irrelevant"); + action.authResult = AUTH_RESULT_ADMIN; metricRole = ADMINISTRATOR; } diff --git a/javatests/google/registry/rdap/RdapEntityActionTest.java b/javatests/google/registry/rdap/RdapEntityActionTest.java index ae5ef267d..f79d216b7 100644 --- a/javatests/google/registry/rdap/RdapEntityActionTest.java +++ b/javatests/google/registry/rdap/RdapEntityActionTest.java @@ -53,7 +53,6 @@ import google.registry.ui.server.registrar.SessionUtils; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; -import javax.servlet.http.HttpServletRequest; import org.joda.time.DateTime; import org.json.simple.JSONValue; import org.junit.Before; @@ -74,13 +73,9 @@ public class RdapEntityActionTest { @Rule public final InjectRule inject = new InjectRule(); - private final HttpServletRequest request = mock(HttpServletRequest.class); private final FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); private final SessionUtils sessionUtils = mock(SessionUtils.class); - private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); - private final UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); - private final UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true); private final RdapMetrics rdapMetrics = mock(RdapMetrics.class); private RdapEntityAction action; @@ -92,6 +87,16 @@ public class RdapEntityActionTest { private ContactResource disconnectedContact; private ContactResource deletedContact; + private static final AuthResult AUTH_RESULT = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@user.com", "gmail.com", "12345"), false)); + + private static final AuthResult AUTH_RESULT_ADMIN = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@google.com", "gmail.com", "12345"), true)); + @Before public void setUp() { inject.setStaticField(Ofy.class, "clock", clock); @@ -163,7 +168,6 @@ public class RdapEntityActionTest { clock.nowUtc().minusMonths(6)); action = new RdapEntityAction(); action.clock = clock; - action.request = request; action.requestMethod = Action.Method.GET; action.fullServletPath = "https://example.com/rdap"; action.response = response; @@ -173,19 +177,17 @@ public class RdapEntityActionTest { action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); action.rdapWhoisServer = null; action.sessionUtils = sessionUtils; - action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); + action.authResult = AUTH_RESULT; action.rdapMetrics = rdapMetrics; } private void login(String registrar) { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn(registrar); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn(registrar); } private void loginAsAdmin() { - action.authResult = AuthResult.create(AuthLevel.USER, adminUserAuthInfo); - when(sessionUtils.checkRegistrarConsoleLogin(request, adminUserAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("irrelevant"); + action.authResult = AUTH_RESULT_ADMIN; + when(sessionUtils.guessClientIdForUser(AUTH_RESULT_ADMIN)).thenReturn("irrelevant"); } private Object generateActualJson(String name) { diff --git a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java index b5c4a576e..42932e373 100644 --- a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java +++ b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java @@ -59,7 +59,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; -import javax.servlet.http.HttpServletRequest; import org.joda.time.DateTime; import org.json.simple.JSONArray; import org.json.simple.JSONObject; @@ -82,12 +81,8 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase { HANDLE } - private final HttpServletRequest request = mock(HttpServletRequest.class); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z")); private final SessionUtils sessionUtils = mock(SessionUtils.class); - private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); - private final UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); - private final UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true); private final RdapEntitySearchAction action = new RdapEntitySearchAction(); private FakeResponse response = new FakeResponse(); @@ -132,6 +127,16 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase { return JSONValue.parse(response.getPayload()); } + private static final AuthResult AUTH_RESULT = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@user.com", "gmail.com", "12345"), false)); + + private static final AuthResult AUTH_RESULT_ADMIN = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@google.com", "gmail.com", "12345"), true)); + @Before public void setUp() { inject.setStaticField(Ofy.class, "clock", clock); @@ -182,7 +187,6 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase { clock.nowUtc().minusMonths(6)); action.clock = clock; - action.request = request; action.requestMethod = Action.Method.GET; action.fullServletPath = "https://example.com/rdap"; action.requestUrl = "https://example.com/rdap/entities"; @@ -199,21 +203,19 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase { action.includeDeletedParam = Optional.empty(); action.formatOutputParam = Optional.empty(); action.sessionUtils = sessionUtils; - action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); + action.authResult = AUTH_RESULT; action.rdapMetrics = rdapMetrics; action.cursorTokenParam = Optional.empty(); } private void login(String registrar) { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn(registrar); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn(registrar); metricRole = REGISTRAR; } private void loginAsAdmin() { - action.authResult = AuthResult.create(AuthLevel.USER, adminUserAuthInfo); - when(sessionUtils.checkRegistrarConsoleLogin(request, adminUserAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("noregistrar"); + action.authResult = AUTH_RESULT_ADMIN; + when(sessionUtils.guessClientIdForUser(AUTH_RESULT_ADMIN)).thenReturn("irrelevant"); metricRole = ADMINISTRATOR; } diff --git a/javatests/google/registry/rdap/RdapNameserverActionTest.java b/javatests/google/registry/rdap/RdapNameserverActionTest.java index 7a3196a0b..de8fc7b98 100644 --- a/javatests/google/registry/rdap/RdapNameserverActionTest.java +++ b/javatests/google/registry/rdap/RdapNameserverActionTest.java @@ -35,6 +35,7 @@ import google.registry.rdap.RdapMetrics.SearchType; import google.registry.rdap.RdapMetrics.WildcardType; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; import google.registry.request.Action; +import google.registry.request.HttpException.ForbiddenException; import google.registry.request.auth.AuthLevel; import google.registry.request.auth.AuthResult; import google.registry.request.auth.UserAuthInfo; @@ -46,7 +47,6 @@ import google.registry.ui.server.registrar.SessionUtils; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; -import javax.servlet.http.HttpServletRequest; import org.joda.time.DateTime; import org.json.simple.JSONValue; import org.junit.Before; @@ -67,15 +67,21 @@ public class RdapNameserverActionTest { @Rule public final InjectRule inject = new InjectRule(); - private final HttpServletRequest request = mock(HttpServletRequest.class); private final FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); private final SessionUtils sessionUtils = mock(SessionUtils.class); - private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); - private final UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); - private final UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true); private final RdapMetrics rdapMetrics = mock(RdapMetrics.class); + private static final AuthResult AUTH_RESULT = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@user.com", "gmail.com", "12345"), false)); + + private static final AuthResult AUTH_RESULT_ADMIN = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@google.com", "gmail.com", "12345"), true)); + @Before public void setUp() { inject.setStaticField(Ofy.class, "clock", clock); @@ -106,8 +112,7 @@ public class RdapNameserverActionTest { private RdapNameserverAction newRdapNameserverAction( String input, Optional desiredRegistrar, Optional includeDeleted) { - return newRdapNameserverAction( - input, desiredRegistrar, includeDeleted, AuthResult.create(AuthLevel.USER, userAuthInfo)); + return newRdapNameserverAction(input, desiredRegistrar, includeDeleted, AUTH_RESULT); } private RdapNameserverAction newRdapNameserverAction( @@ -117,7 +122,6 @@ public class RdapNameserverActionTest { AuthResult authResult) { RdapNameserverAction action = new RdapNameserverAction(); action.clock = clock; - action.request = request; action.requestMethod = Action.Method.GET; action.fullServletPath = "https://example.tld/rdap"; action.response = response; @@ -358,23 +362,21 @@ public class RdapNameserverActionTest { @Test public void testDeletedNameserver_notFound_notLoggedIn() { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(false); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenThrow(new ForbiddenException("blah")); generateActualJson("nsdeleted.cat.lol", Optional.empty(), Optional.of(true)); assertThat(response.getStatus()).isEqualTo(404); } @Test public void testDeletedNameserver_notFound_loggedInAsDifferentRegistrar() { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("otherregistrar"); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn("otherregistrar"); generateActualJson("nsdeleted.cat.lol", Optional.empty(), Optional.of(true)); assertThat(response.getStatus()).isEqualTo(404); } @Test public void testDeletedNameserver_found_loggedInAsCorrectRegistrar() { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("TheRegistrar"); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn("TheRegistrar"); assertThat( generateActualJson("nsdeleted.cat.lol", Optional.empty(), Optional.of(true))) .isEqualTo( @@ -391,13 +393,12 @@ public class RdapNameserverActionTest { @Test public void testDeletedNameserver_found_loggedInAsAdmin() { - when(sessionUtils.checkRegistrarConsoleLogin(request, adminUserAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("irrelevant"); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT_ADMIN)).thenReturn("irrelevant"); newRdapNameserverAction( "nsdeleted.cat.lol", Optional.empty(), Optional.of(true), - AuthResult.create(AuthLevel.USER, adminUserAuthInfo)) + AUTH_RESULT_ADMIN) .run(); assertThat(JSONValue.parse(response.getPayload())) .isEqualTo( @@ -414,8 +415,7 @@ public class RdapNameserverActionTest { @Test public void testDeletedNameserver_found_sameRegistrarRequested() { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("TheRegistrar"); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn("TheRegistrar"); assertThat( generateActualJson("nsdeleted.cat.lol", Optional.of("TheRegistrar"), Optional.of(true))) .isEqualTo( @@ -432,8 +432,7 @@ public class RdapNameserverActionTest { @Test public void testDeletedNameserver_notFound_differentRegistrarRequested() { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("TheRegistrar"); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn("TheRegistrar"); generateActualJson("ns1.cat.lol", Optional.of("otherregistrar"), Optional.of(false)); assertThat(response.getStatus()).isEqualTo(404); } diff --git a/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java b/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java index 2b477e6bd..9499fc8f4 100644 --- a/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java +++ b/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java @@ -58,7 +58,6 @@ import google.registry.ui.server.registrar.SessionUtils; import java.net.URLDecoder; import java.util.Optional; import javax.annotation.Nullable; -import javax.servlet.http.HttpServletRequest; import org.joda.time.DateTime; import org.json.simple.JSONArray; import org.json.simple.JSONObject; @@ -77,15 +76,22 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { @Rule public final InjectRule inject = new InjectRule(); - private final HttpServletRequest request = mock(HttpServletRequest.class); private FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z")); private final SessionUtils sessionUtils = mock(SessionUtils.class); - private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); - private final UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); - private final UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true); private final RdapNameserverSearchAction action = new RdapNameserverSearchAction(); + private static final AuthResult AUTH_RESULT = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@user.com", "gmail.com", "12345"), false)); + + private static final AuthResult AUTH_RESULT_ADMIN = + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(new User("rdap.user@google.com", "gmail.com", "12345"), true)); + + private DomainResource domainCatLol; private HostResource hostNs1CatLol; private HostResource hostNs2CatLol; @@ -183,7 +189,6 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { action.requestUrl = "https://example.tld/rdap/nameservers"; action.requestPath = RdapNameserverSearchAction.PATH; action.parameterMap = ImmutableListMultimap.of(); - action.request = request; action.requestMethod = Action.Method.GET; action.response = response; action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); @@ -194,22 +199,19 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { action.registrarParam = Optional.empty(); action.includeDeletedParam = Optional.empty(); action.formatOutputParam = Optional.empty(); - action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); + action.authResult = AUTH_RESULT; action.sessionUtils = sessionUtils; action.rdapMetrics = rdapMetrics; action.cursorTokenParam = Optional.empty(); } - private void login(String clientId) { - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn(clientId); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn(clientId); metricRole = REGISTRAR; } private void loginAsAdmin() { - when(sessionUtils.checkRegistrarConsoleLogin(request, adminUserAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("irrelevant"); - action.authResult = AuthResult.create(AuthLevel.USER, adminUserAuthInfo); + when(sessionUtils.guessClientIdForUser(AUTH_RESULT_ADMIN)).thenReturn("irrelevant"); + action.authResult = AUTH_RESULT_ADMIN; metricRole = ADMINISTRATOR; } diff --git a/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java b/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java index 9c7e39986..c259af5df 100644 --- a/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java +++ b/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java @@ -16,6 +16,7 @@ package google.registry.ui.server.registrar; import static com.google.common.net.HttpHeaders.LOCATION; import static com.google.common.truth.Truth.assertThat; +import static google.registry.testing.DatastoreHelper.loadRegistrar; import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; @@ -24,6 +25,7 @@ import static org.mockito.Mockito.when; import com.google.appengine.api.users.User; import com.google.appengine.api.users.UserServiceFactory; import com.google.common.net.MediaType; +import google.registry.request.HttpException.ForbiddenException; import google.registry.request.auth.AuthLevel; import google.registry.request.auth.AuthResult; import google.registry.request.auth.UserAuthInfo; @@ -70,10 +72,11 @@ public class ConsoleUiActionTest { action.sessionUtils = sessionUtils; action.userService = UserServiceFactory.getUserService(); action.xsrfTokenManager = new XsrfTokenManager(new FakeClock(), action.userService); - UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); - action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); - when(sessionUtils.checkRegistrarConsoleLogin(request, userAuthInfo)).thenReturn(true); - when(sessionUtils.getRegistrarClientId(request)).thenReturn("TheRegistrar"); + AuthResult authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false)); + action.authResult = authResult; + when(sessionUtils.guessClientIdForUser(authResult)).thenReturn("TheRegistrar"); + when(sessionUtils.getRegistrarForUser("TheRegistrar", authResult)) + .thenReturn(loadRegistrar("TheRegistrar")); } @Test @@ -110,9 +113,8 @@ public class ConsoleUiActionTest { @Test public void testUserDoesntHaveAccessToAnyRegistrar_showsWhoAreYouPage() { - when(sessionUtils.checkRegistrarConsoleLogin( - any(HttpServletRequest.class), any(UserAuthInfo.class))) - .thenReturn(false); + when(sessionUtils.guessClientIdForUser(any(AuthResult.class))) + .thenThrow(new ForbiddenException("forbidden")); action.run(); assertThat(response.getPayload()).contains("

You need permission

"); } diff --git a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java index d84a386e9..68efd8c58 100644 --- a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java +++ b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java @@ -22,19 +22,15 @@ import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued; import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued; import static google.registry.testing.TestDataHelper.loadFile; import static java.util.Arrays.asList; -import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import google.registry.export.sheet.SyncRegistrarsSheetAction; import google.registry.model.registrar.Registrar; -import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.ForbiddenException; -import google.registry.request.auth.AuthResult; import google.registry.testing.CertificateSamples; import google.registry.testing.TaskQueueHelper.TaskMatcher; import google.registry.util.CidrAddressBlock; @@ -42,7 +38,6 @@ import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; import javax.mail.internet.InternetAddress; -import javax.servlet.http.HttpServletRequest; import org.json.simple.JSONValue; import org.json.simple.parser.ParseException; import org.junit.Test; @@ -82,39 +77,27 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase assertNoTasksEnqueued("sheet"); } + /** + * Make sure that if someone spoofs a different registrar (they don't have access to), we fail. + * Also relevant if the person's privilege were revoked after the page load. + */ @Test - public void testRead_notAuthorized_failure() { - when(sessionUtils.getRegistrarForAuthResult( - any(HttpServletRequest.class), any(AuthResult.class))) - .thenThrow(new ForbiddenException("Not authorized to access Registrar Console")); - assertThrows(ForbiddenException.class, () -> action.handleJsonRequest(ImmutableMap.of())); + public void testFailure_readRegistrarInfo_notAuthorized() { + action.authResult = USER_UNAUTHORIZED; + assertThrows( + ForbiddenException.class, () -> action.handleJsonRequest(ImmutableMap.of("id", CLIENT_ID))); assertNoTasksEnqueued("sheet"); } - /** - * This is the default read test for the registrar settings actions. - */ + /** This is the default read test for the registrar settings actions. */ @Test public void testSuccess_readRegistrarInfo_authorized() { Map response = action.handleJsonRequest(ImmutableMap.of("id", CLIENT_ID)); - assertThat(response).containsExactly( - "status", "SUCCESS", - "message", "Success", - "results", asList(loadRegistrar(CLIENT_ID).toJsonMap())); - } - - /** - * We got a different CLIENT_ID from the JS than the one we find ourself. - * - *

This might happen if the user's "guessed" registrar changes after the initial page load. For - * example, if the user was added as contact to a different registrar, or removed as contact from - * the current registrar (but is still a contact of a different one, so the "guessing" works). - */ - @Test - public void testFailure_readRegistrarInfo_differentClientId() { - assertThrows( - BadRequestException.class, - () -> action.handleJsonRequest(ImmutableMap.of("id", "different"))); + assertThat(response) + .containsExactly( + "status", "SUCCESS", + "message", "Success", + "results", asList(loadRegistrar(CLIENT_ID).toJsonMap())); } @Test @@ -159,6 +142,19 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase "results", asList(loadRegistrar(CLIENT_ID).toJsonMap())); } + @Test + public void testFailute_updateRegistrarInfo_notAuthorized() { + action.authResult = USER_UNAUTHORIZED; + assertThrows( + ForbiddenException.class, + () -> + action.handleJsonRequest( + ImmutableMap.of( + "op", "update", + "id", CLIENT_ID, + "args", ImmutableMap.of("lastUpdateTime", getLastUpdateTime())))); + } + @Test public void testUpdate_badEmail_errorEmailField() { Map response = action.handleJsonRequest(ImmutableMap.of( diff --git a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java index 78118b608..7c4e4c8ca 100644 --- a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java +++ b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java @@ -24,6 +24,7 @@ import com.google.appengine.api.users.User; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import google.registry.model.ofy.Ofy; +import google.registry.request.HttpException.ForbiddenException; import google.registry.request.JsonActionRunner; import google.registry.request.JsonResponse; import google.registry.request.ResponseImpl; @@ -57,6 +58,13 @@ public class RegistrarSettingsActionTestCase { static final String CLIENT_ID = "TheRegistrar"; + static final AuthResult USER_AUTHORIZED = + AuthResult.create(AuthLevel.USER, UserAuthInfo.create(new User("user", "gmail.com"), false)); + + static final AuthResult USER_UNAUTHORIZED = + AuthResult.create( + AuthLevel.USER, UserAuthInfo.create(new User("unauthorized", "gmail.com"), false)); + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().withTaskQueue().build(); @@ -79,11 +87,10 @@ public class RegistrarSettingsActionTestCase { @Before public void setUp() throws Exception { - action.request = req; action.sessionUtils = sessionUtils; action.appEngineServiceUtils = appEngineServiceUtils; when(appEngineServiceUtils.getCurrentVersionHostname("backend")).thenReturn("backend.hostname"); - action.authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false)); + action.authResult = USER_AUTHORIZED; action.jsonActionRunner = new JsonActionRunner( ImmutableMap.of(), new JsonResponse(new ResponseImpl(rsp))); action.registrarChangesNotificationEmailAddresses = ImmutableList.of( @@ -102,7 +109,9 @@ public class RegistrarSettingsActionTestCase { // the result is out of date after mutations. // (for example, if someone wants to change the registrar to prepare for a test, the function // would still return the old value) - when(sessionUtils.getRegistrarForAuthResult(req, action.authResult)) + when(sessionUtils.getRegistrarForUser(CLIENT_ID, USER_AUTHORIZED)) .thenAnswer(x -> loadRegistrar(CLIENT_ID)); + when(sessionUtils.getRegistrarForUser(CLIENT_ID, USER_UNAUTHORIZED)) + .thenThrow(new ForbiddenException("forbidden")); } } diff --git a/javatests/google/registry/ui/server/registrar/SecuritySettingsTest.java b/javatests/google/registry/ui/server/registrar/SecuritySettingsTest.java index d48b6bc14..1720bf788 100644 --- a/javatests/google/registry/ui/server/registrar/SecuritySettingsTest.java +++ b/javatests/google/registry/ui/server/registrar/SecuritySettingsTest.java @@ -24,7 +24,6 @@ import static google.registry.testing.DatastoreHelper.loadRegistrar; import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.util.DateTimeUtils.START_OF_TIME; import static java.util.Arrays.asList; -import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; import google.registry.model.registrar.Registrar; @@ -112,8 +111,6 @@ public class SecuritySettingsTest extends RegistrarSettingsActionTestCase { .setClientCertificate(SAMPLE_CERT, START_OF_TIME) .setFailoverClientCertificate(SAMPLE_CERT2, START_OF_TIME) .build()); - when(sessionUtils.getRegistrarForAuthResult(req, action.authResult)) - .thenReturn(initialRegistrar); Map jsonMap = initialRegistrar.toJsonMap(); jsonMap.put("clientCertificate", null); jsonMap.put("failoverClientCertificate", ""); diff --git a/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java b/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java index 905de12a4..3a5137b1b 100644 --- a/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java +++ b/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java @@ -16,29 +16,25 @@ package google.registry.ui.server.registrar; import static com.google.common.truth.Truth.assertThat; import static google.registry.testing.AppEngineRule.THE_REGISTRAR_GAE_USER_ID; -import static google.registry.testing.DatastoreHelper.deleteResource; import static google.registry.testing.DatastoreHelper.loadRegistrar; import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.JUnitBackports.assertThrows; import static google.registry.testing.LogsSubject.assertAboutLogs; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; -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.common.flogger.LoggerConfig; import com.google.common.testing.NullPointerTester; import com.google.common.testing.TestLogHandler; -import google.registry.model.registrar.RegistrarContact; +import google.registry.request.HttpException.ForbiddenException; +import google.registry.request.auth.AuthLevel; +import google.registry.request.auth.AuthResult; import google.registry.request.auth.UserAuthInfo; import google.registry.testing.AppEngineRule; import google.registry.testing.InjectRule; import java.util.logging.Level; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -55,25 +51,27 @@ public class SessionUtilsTest { private final HttpServletRequest req = mock(HttpServletRequest.class); private final HttpServletResponse rsp = mock(HttpServletResponse.class); - private final HttpSession session = mock(HttpSession.class); private final TestLogHandler testLogHandler = new TestLogHandler(); private SessionUtils sessionUtils; - private static final UserAuthInfo AUTHORIZED_USER = createAuthInfo(true, false); - private static final UserAuthInfo UNAUTHORIZED_USER = createAuthInfo(false, false); - private static final UserAuthInfo AUTHORIZED_ADMIN = createAuthInfo(true, true); - private static final UserAuthInfo UNAUTHORIZED_ADMIN = createAuthInfo(false, true); + private static final AuthResult AUTHORIZED_USER = createAuthResult(true, false); + private static final AuthResult UNAUTHORIZED_USER = createAuthResult(false, false); + private static final AuthResult AUTHORIZED_ADMIN = createAuthResult(true, true); + private static final AuthResult UNAUTHORIZED_ADMIN = createAuthResult(false, true); + private static final AuthResult NO_USER = AuthResult.create(AuthLevel.NONE); private static final String DEFAULT_CLIENT_ID = "TheRegistrar"; private static final String ADMIN_CLIENT_ID = "NewRegistrar"; - private static UserAuthInfo createAuthInfo(boolean isAuthorized, boolean isAdmin) { - return UserAuthInfo.create( - new User( - "user1@google.com", - "google.com", - isAuthorized ? THE_REGISTRAR_GAE_USER_ID : "badGaeUserId"), - isAdmin); + private static AuthResult createAuthResult(boolean isAuthorized, boolean isAdmin) { + return AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create( + new User( + "user1@google.com", + "google.com", + isAuthorized ? THE_REGISTRAR_GAE_USER_ID : "badGaeUserId"), + isAdmin)); } @Before @@ -82,7 +80,6 @@ public class SessionUtilsTest { sessionUtils = new SessionUtils(); sessionUtils.registryAdminClientId = ADMIN_CLIENT_ID; persistResource(loadRegistrar(ADMIN_CLIENT_ID)); - when(req.getSession()).thenReturn(session); } @After @@ -90,240 +87,161 @@ public class SessionUtilsTest { LoggerConfig.getConfig(SessionUtils.class).removeHandler(testLogHandler); } - /** User needs to be logged in before calling checkRegistrarConsoleLogin */ - @Test - public void testCheckRegistrarConsoleLogin_notLoggedIn_throwsIllegalStateException() { - assertThrows( - IllegalStateException.class, - () -> { - @SuppressWarnings("unused") - boolean unused = sessionUtils.checkRegistrarConsoleLogin(req, null); - }); + private String formatMessage(String message, AuthResult authResult, String clientId) { + return message + .replace("{user}", authResult.userIdForLogging()) + .replace("{clientId}", String.valueOf(clientId)); } - /** - * If clientId exists in the session and the user does not have access to that registrar, then no - * access should be granted. - */ + /** Fail loading registrar if user doesn't have access to it. */ @Test - public void testCheckRegistrarConsoleLogin_hasSession_noAccess_isNotAdmin() { - when(session.getAttribute("clientId")).thenReturn(DEFAULT_CLIENT_ID); - assertThat(sessionUtils.checkRegistrarConsoleLogin(req, UNAUTHORIZED_USER)).isFalse(); - verify(session).invalidate(); - assertAboutLogs() - .that(testLogHandler) - .hasLogAtLevelWithMessage(Level.INFO, "Registrar Console access revoked"); + public void testGetRegistrarForUser_noAccess_isNotAdmin() { + expectGetRegistrarFailure( + DEFAULT_CLIENT_ID, + UNAUTHORIZED_USER, + "User {user} doesn't have access to registrar {clientId}"); } - /** - * If clientId exists in the session and the user does not have access to that registrar, then - * access should be revoked. The admin flag should be ignored. - */ + /** Fail loading registrar if there's no user associated with the request. */ @Test - public void testCheckRegistrarConsoleLogin_hasSession_noAccess_isAdmin() { - when(session.getAttribute("clientId")).thenReturn(DEFAULT_CLIENT_ID); - assertThat(sessionUtils.checkRegistrarConsoleLogin(req, UNAUTHORIZED_ADMIN)).isFalse(); - verify(session).invalidate(); - assertAboutLogs() - .that(testLogHandler) - .hasLogAtLevelWithMessage(Level.INFO, "Registrar Console access revoked"); + public void testGetRegistrarForUser_noUser() { + expectGetRegistrarFailure(DEFAULT_CLIENT_ID, NO_USER, "Not logged in"); } - /** - * If clientId exists in the session and the user does have access to that registrar, then access - * should be allowed. - */ + /** Succeed loading registrar if user has access to it. */ @Test - public void testCheckRegistrarConsoleLogin_hasSession_hasAccess_isNotAdmin() { - when(session.getAttribute("clientId")).thenReturn(DEFAULT_CLIENT_ID); - assertThat(sessionUtils.checkRegistrarConsoleLogin(req, AUTHORIZED_USER)).isTrue(); - verify(session).getAttribute("clientId"); - verifyNoMoreInteractions(session); + public void testGetRegistrarForUser_hasAccess_isNotAdmin() { + expectGetRegistrarSuccess(AUTHORIZED_USER, "User {user} has access to registrar {clientId}"); + } + + /** Succeed loading registrar if admin. */ + @Test + public void testGetRegistrarForUser_hasAccess_isAdmin() { + expectGetRegistrarSuccess(AUTHORIZED_ADMIN, "User {user} has access to registrar {clientId}"); + } + + /** Fail loading registrar if admin isn't on the approved contacts list. */ + @Test + public void testGetRegistrarForUser_noAccess_isAdmin() { + expectGetRegistrarFailure( + DEFAULT_CLIENT_ID, + UNAUTHORIZED_ADMIN, + "User {user} doesn't have access to registrar {clientId}"); + } + + /** Succeed loading registrarAdmin even if unauthorized admin. */ + @Test + public void testGetRegistrarForUser_registrarAdminClientId() { + sessionUtils.registryAdminClientId = DEFAULT_CLIENT_ID; + expectGetRegistrarSuccess( + UNAUTHORIZED_ADMIN, "Allowing admin {user} access to registrar {clientId}."); + } + + /** Fail loading registrar even if admin, if registrar doesn't exist. */ + @Test + public void testGetRegistrarForUser_doesntExist_isAdmin() { + expectGetRegistrarFailure("BadClientId", UNAUTHORIZED_ADMIN, "Registrar {clientId} not found"); + } + + private void expectGetRegistrarSuccess(AuthResult authResult, String message) { + assertThat(sessionUtils.getRegistrarForUser(DEFAULT_CLIENT_ID, authResult)).isNotNull(); assertAboutLogs() .that(testLogHandler) .hasLogAtLevelWithMessage( - Level.INFO, - String.format( - "Associating user %s with given registrar %s.", - AUTHORIZED_USER.user().getUserId(), DEFAULT_CLIENT_ID)); + Level.INFO, formatMessage(message, authResult, DEFAULT_CLIENT_ID)); + } + + private void expectGetRegistrarFailure(String clientId, AuthResult authResult, String message) { + ForbiddenException exception = + assertThrows( + ForbiddenException.class, () -> sessionUtils.getRegistrarForUser(clientId, authResult)); + + assertThat(exception).hasMessageThat().contains(formatMessage(message, authResult, clientId)); + assertAboutLogs().that(testLogHandler).hasNoLogsAtLevel(Level.INFO); + } + + /** If a user has access to a registrar, we should guess that registrar. */ + @Test + public void testGuessClientIdForUser_hasAccess_isNotAdmin() { + expectGuessRegistrarSuccess( + AUTHORIZED_USER, + DEFAULT_CLIENT_ID, + "Associating user {user} with found registrar {clientId}."); + } + + /** If a user doesn't have access to any registrars, guess returns nothing. */ + @Test + public void testGuessClientIdForUser_noAccess_isNotAdmin() { + expectGuessRegistrarFailure( + UNAUTHORIZED_USER, "User {user} isn't associated with any registrar"); } /** - * If clientId exists in the session and the user does have access to that registrar, then access - * should be allowed. The admin flag should be ignored. + * If an admin has access to a registrar, we should guess that registrar (rather than the + * ADMIN_CLIENT_ID). */ @Test - public void testCheckRegistrarConsoleLogin_hasSession_hasAccess_isAdmin() { - when(session.getAttribute("clientId")).thenReturn(DEFAULT_CLIENT_ID); - assertThat(sessionUtils.checkRegistrarConsoleLogin(req, AUTHORIZED_ADMIN)).isTrue(); - verify(session).getAttribute("clientId"); - verifyNoMoreInteractions(session); - assertAboutLogs() - .that(testLogHandler) - .hasLogAtLevelWithMessage( - Level.INFO, - String.format( - "Associating user %s with given registrar %s.", - AUTHORIZED_ADMIN.user().getUserId(), DEFAULT_CLIENT_ID)); + public void testGuessClientIdForUser_hasAccess_isAdmin() { + expectGuessRegistrarSuccess( + AUTHORIZED_ADMIN, + DEFAULT_CLIENT_ID, + "Associating user {user} with found registrar {clientId}."); + } + + /** If an admin doesn't have access to a registrar, we should guess the ADMIN_CLIENT_ID. */ + @Test + public void testGuessClientIdForUser_noAccess_isAdmin() { + expectGuessRegistrarSuccess( + UNAUTHORIZED_ADMIN, + ADMIN_CLIENT_ID, + "User {user} is an admin with no associated registrar." + + " Automatically associating the user with configured client Id {clientId}."); } /** - * If clientId does not exist in the session and the user has access to a registrar, then access - * should be granted to that registrar. + * If an admin is not associated with a registrar and there is no configured adminClientId, we + * can't guess the clientId. */ @Test - public void testCheckRegistrarConsoleLogin_noSession_hasAccess_isNotAdmin() { - assertThat(sessionUtils.checkRegistrarConsoleLogin(req, AUTHORIZED_USER)).isTrue(); - verify(session).setAttribute(eq("clientId"), eq(DEFAULT_CLIENT_ID)); - assertAboutLogs() - .that(testLogHandler) - .hasLogAtLevelWithMessage( - Level.INFO, - String.format( - "Associating user %s with found registrar %s.", - AUTHORIZED_USER.user().getUserId(), DEFAULT_CLIENT_ID)); - } - - /** - * If clientId does not exist in the session and the user has access to a registrar, then access - * should be granted to that registrar. The admin flag should be ignored. - */ - @Test - public void testCheckRegistrarConsoleLogin_noSession_hasAccess_isAdmin() { - assertThat(sessionUtils.checkRegistrarConsoleLogin(req, AUTHORIZED_ADMIN)).isTrue(); - verify(session).setAttribute(eq("clientId"), eq(DEFAULT_CLIENT_ID)); - assertAboutLogs() - .that(testLogHandler) - .hasLogAtLevelWithMessage( - Level.INFO, - String.format( - "Associating user %s with found registrar %s.", - AUTHORIZED_ADMIN.user().getUserId(), DEFAULT_CLIENT_ID)); - } - - /** - * If clientId does not exist in the session, the user is not associated with a registrar and the - * user is an admin, then access could be granted to the configured adminClientId. But if the - * configured adminClientId is empty or null, no access is granted. - */ - @Test - public void testCheckRegistrarConsoleLogin_noSession_noAccess_isAdmin_adminClientIdEmpty() { + public void testGuessClientIdForUser_noAccess_isAdmin_adminClientIdEmpty() { sessionUtils.registryAdminClientId = ""; - assertThat(sessionUtils.checkRegistrarConsoleLogin(req, UNAUTHORIZED_ADMIN)).isFalse(); + expectGuessRegistrarFailure( + UNAUTHORIZED_ADMIN, "User {user} isn't associated with any registrar"); assertAboutLogs() .that(testLogHandler) .hasLogAtLevelWithMessage( Level.INFO, - String.format( - "Cannot associate admin user %s with configured client Id." - + " ClientId is null or empty.", - UNAUTHORIZED_ADMIN.user().getUserId())); + "Cannot associate admin user badGaeUserId with configured client Id." + + " ClientId is null or empty."); } /** - * If clientId does not exist in the session, the user is not associated with a registrar and the - * user is an admin, then access could be granted to the configured adminClientId. But if the - * configured adminClientId does not reference a registry, then no access is granted. + * If an admin is not associated with a registrar and the configured adminClientId points to a + * non-existent registrar, we still guess it (we will later failing loading the registrar). */ @Test - public void testCheckRegistrarConsoleLogin_noSession_noAccess_isAdmin_adminClientIdInvalid() { - sessionUtils.registryAdminClientId = "NonexistentRegistry"; - assertThat(sessionUtils.checkRegistrarConsoleLogin(req, UNAUTHORIZED_ADMIN)).isFalse(); + public void testGuessClientIdForUser_noAccess_isAdmin_adminClientIdInvalid() { + sessionUtils.registryAdminClientId = "NonexistentRegistrar"; + expectGuessRegistrarSuccess( + UNAUTHORIZED_ADMIN, + "NonexistentRegistrar", + "User {user} is an admin with no associated registrar." + + " Automatically associating the user with configured client Id {clientId}."); + } + + private void expectGuessRegistrarSuccess(AuthResult authResult, String clientId, String message) { + assertThat(sessionUtils.guessClientIdForUser(authResult)).isEqualTo(clientId); assertAboutLogs() .that(testLogHandler) - .hasLogAtLevelWithMessage( - Level.INFO, - String.format( - "Cannot associate admin user %s with configured client Id %s." - + " Registrar does not exist.", - UNAUTHORIZED_ADMIN.user().getUserId(), "NonexistentRegistry")); + .hasLogAtLevelWithMessage(Level.INFO, formatMessage(message, authResult, clientId)); } - /** - * If clientId does not exist in the session, the user does not have access to a registrar and the - * user is an admin, then grant the user access to the validated configured adminClientId. - */ - @Test - public void testCheckRegistrarConsoleLogin_noSession_noAccess_isAdmin() { - assertThat(sessionUtils.checkRegistrarConsoleLogin(req, UNAUTHORIZED_ADMIN)).isTrue(); - verify(session).setAttribute(eq("clientId"), eq(ADMIN_CLIENT_ID)); - assertAboutLogs() - .that(testLogHandler) - .hasLogAtLevelWithMessage( - Level.INFO, - String.format( - "User %s is an admin with no associated registrar." - + " Automatically associating the user with configured client Id %s.", - UNAUTHORIZED_ADMIN.user().getUserId(), ADMIN_CLIENT_ID)); - } - - /** - * If session clientId points to the adminClientId, and the user is an admin that doesn't have - * access to this registrar - it means this is the second (or later) visit of this admin and they - * were granted access to the default registrar because they aren't associated with any other - * registrar. - * - * We continue to grant the admin access. - */ - @Test - public void testCheckRegistrarConsoleLogin_noSession_noAccess_isAdmin_secondRequest() { - when(session.getAttribute("clientId")).thenReturn(ADMIN_CLIENT_ID); - assertThat(sessionUtils.checkRegistrarConsoleLogin(req, UNAUTHORIZED_ADMIN)).isTrue(); - assertAboutLogs() - .that(testLogHandler) - .hasLogAtLevelWithMessage( - Level.INFO, - String.format( - "Associating user %s with given registrar %s.", - UNAUTHORIZED_ADMIN.user().getUserId(), ADMIN_CLIENT_ID)); - } - - /** - * If clientId does not exist in the session and the user is not associated with a registrar, then - * access should not be granted. - */ - @Test - public void testCheckRegistrarConsoleLogin_noSession_noAccess_isNotAdmin() { - assertThat(sessionUtils.checkRegistrarConsoleLogin(req, UNAUTHORIZED_USER)).isFalse(); - assertAboutLogs() - .that(testLogHandler) - .hasLogAtLevelWithMessage( - Level.INFO, - String.format( - "User not associated with any Registrar: %s", - UNAUTHORIZED_USER.user().getUserId())); - } - - @Test - public void testHasAccessToRegistrar_orphanedContact_returnsFalse() { - deleteResource(loadRegistrar(DEFAULT_CLIENT_ID)); - assertThat( - sessionUtils.hasAccessToRegistrar(DEFAULT_CLIENT_ID, THE_REGISTRAR_GAE_USER_ID, false)) - .isFalse(); - } - - @Test - public void testHasAccessToRegistrar_accessRevoked_returnsFalse() { - RegistrarContact.updateContacts(loadRegistrar(DEFAULT_CLIENT_ID), new java.util.HashSet<>()); - assertThat( - sessionUtils.hasAccessToRegistrar(DEFAULT_CLIENT_ID, THE_REGISTRAR_GAE_USER_ID, false)) - .isFalse(); - } - - @Test - public void testHasAccessToRegistrar_orphanedAdmin_notAdminRegistrar_returnsFalse() { - RegistrarContact.updateContacts(loadRegistrar(DEFAULT_CLIENT_ID), new java.util.HashSet<>()); - assertThat( - sessionUtils.hasAccessToRegistrar(DEFAULT_CLIENT_ID, THE_REGISTRAR_GAE_USER_ID, true)) - .isFalse(); - } - - @Test - public void testHasAccessToRegistrar_orphanedAdmin_onAdminRegistrar_returnsTrue() { - RegistrarContact.updateContacts(loadRegistrar(ADMIN_CLIENT_ID), new java.util.HashSet<>()); - assertThat( - sessionUtils.hasAccessToRegistrar(ADMIN_CLIENT_ID, THE_REGISTRAR_GAE_USER_ID, true)) - .isTrue(); + private void expectGuessRegistrarFailure(AuthResult authResult, String message) { + ForbiddenException exception = + assertThrows(ForbiddenException.class, () -> sessionUtils.guessClientIdForUser(authResult)); + assertThat(exception) + .hasMessageThat() + .contains(formatMessage(message, UNAUTHORIZED_USER, null)); } @Test From 3d71012acdcccb6b382ced46e6b7cedf240039cb Mon Sep 17 00:00:00 2001 From: mcilwain Date: Wed, 26 Sep 2018 11:50:18 -0700 Subject: [PATCH 07/60] Move RDAP Terms of Service into YAML config file ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=214642487 --- .../registry/config/RegistryConfig.java | 37 +++++------------- .../config/RegistryConfigSettings.java | 1 + .../registry/config/files/default-config.yaml | 39 +++++++++++++++++++ 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/java/google/registry/config/RegistryConfig.java b/java/google/registry/config/RegistryConfig.java index 802ae4fd2..f597ec730 100644 --- a/java/google/registry/config/RegistryConfig.java +++ b/java/google/registry/config/RegistryConfig.java @@ -1186,6 +1186,12 @@ public final class RegistryConfig { return config.registryTool.clientSecretFilename; } + @Provides + @Config("rdapTos") + public static ImmutableList provideRdapTos(RegistryConfigSettings config) { + return ImmutableList.copyOf(Splitter.on('\n').split(config.registryPolicy.rdapTos)); + } + /** * Returns the help text to be used by RDAP. * @@ -1195,7 +1201,8 @@ public final class RegistryConfig { @Singleton @Provides @Config("rdapHelpMap") - public static ImmutableMap provideRdapHelpMap() { + public static ImmutableMap provideRdapHelpMap( + @Config("rdapTos") ImmutableList rdapTos) { return new ImmutableMap.Builder() .put("/", RdapNoticeDescriptor.builder() .setTitle("RDAP Help") @@ -1216,33 +1223,7 @@ public final class RegistryConfig { .build()) .put("/tos", RdapNoticeDescriptor.builder() .setTitle("RDAP Terms of Service") - .setDescription(ImmutableList.of( - "By querying our Domain Database as part of the RDAP pilot program (RDAP Domain" - + "Database), you are agreeing to comply with these terms, so please read" - + " them carefully.", - "Any information provided is 'as is' without any guarantee of accuracy.", - "Please do not misuse the RDAP Domain Database. It is intended solely for" - + " query-based access on an experimental basis and should not be used for or" - + " relied upon for any other purpose.", - "Don't use the RDAP Domain Database to allow, enable, or otherwise support the" - + " transmission of mass unsolicited, commercial advertising or" - + " solicitations.", - "Don't access our RDAP Domain Database through the use of high volume, automated" - + " electronic processes that send queries or data to the systems of any" - + " ICANN-accredited registrar.", - "You may only use the information contained in the RDAP Domain Database for" - + " lawful purposes.", - "Do not compile, repackage, disseminate, or otherwise use the information" - + " contained in the RDAP Domain Database in its entirety, or in any" - + " substantial portion, without our prior written permission.", - "We may retain certain details about queries to our RDAP Domain Database for the" - + " purposes of detecting and preventing misuse.", - "We reserve the right to restrict or deny your access to the RDAP Domain Database" - + " if we suspect that you have failed to comply with these terms.", - "We reserve the right to modify or discontinue our participation in the RDAP" - + " pilot program and suspend or terminate access to the RDAP Domain Database" - + " at any time and for any reason in our sole discretion.", - "We reserve the right to modify this agreement at any time.")) + .setDescription(rdapTos) .setLinkValueSuffix("help/tos") .build()) .build(); diff --git a/java/google/registry/config/RegistryConfigSettings.java b/java/google/registry/config/RegistryConfigSettings.java index 0aeabba1d..eb6ce6f66 100644 --- a/java/google/registry/config/RegistryConfigSettings.java +++ b/java/google/registry/config/RegistryConfigSettings.java @@ -90,6 +90,7 @@ public class RegistryConfigSettings { public String premiumTermsExportDisclaimer; public String reservedTermsExportDisclaimer; public String whoisDisclaimer; + public String rdapTos; } /** Configuration for Cloud Datastore. */ diff --git a/java/google/registry/config/files/default-config.yaml b/java/google/registry/config/files/default-config.yaml index de850501e..8c51b0cfc 100644 --- a/java/google/registry/config/files/default-config.yaml +++ b/java/google/registry/config/files/default-config.yaml @@ -103,6 +103,45 @@ registryPolicy: unlawful behavior. We reserve the right to restrict or deny your access to the WHOIS database, and may modify these terms at any time. + # RDAP Terms of Service text displayed at the /rdap/help/tos endpoint. + rdapTos: > + By querying our Domain Database as part of the RDAP pilot program (RDAP + Domain Database), you are agreeing to comply with these terms, so please + read them carefully. + + Any information provided is 'as is' without any guarantee of accuracy. + + Please do not misuse the RDAP Domain Database. It is intended solely for + query-based access on an experimental basis and should not be used for or + relied upon for any other purpose. + + Don't use the RDAP Domain Database to allow, enable, or otherwise support + the transmission of mass unsolicited, commercial advertising or + solicitations. + + Don't access our RDAP Domain Database through the use of high volume, + automated electronic processes that send queries or data to the systems + of any ICANN-accredited registrar. + + You may only use the information contained in the RDAP Domain Database for + lawful purposes. + + Do not compile, repackage, disseminate, or otherwise use the information + contained in the RDAP Domain Database in its entirety, or in any + substantial portion, without our prior written permission. + + We may retain certain details about queries to our RDAP Domain Database + for the purposes of detecting and preventing misuse. + + We reserve the right to restrict or deny your access to the RDAP Domain + Database if we suspect that you have failed to comply with these terms. + + We reserve the right to modify or discontinue our participation in the + RDAP pilot program and suspend or terminate access to the RDAP Domain + Database at any time and for any reason in our sole discretion. + + We reserve the right to modify this agreement at any time. + datastore: # Number of commit log buckets in Datastore. Lowering this after initial # install risks losing up to a days' worth of differential backups. From a7ec72f335f6a382c0c04be49566e98c1b9e831f Mon Sep 17 00:00:00 2001 From: mcilwain Date: Thu, 27 Sep 2018 07:47:35 -0700 Subject: [PATCH 08/60] Use correct disclaimer for premium terms export Looks like a copy-paste error from the reserved list export disclaimer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=214773560 --- java/google/registry/config/RegistryConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/google/registry/config/RegistryConfig.java b/java/google/registry/config/RegistryConfig.java index f597ec730..1ad40e6ef 100644 --- a/java/google/registry/config/RegistryConfig.java +++ b/java/google/registry/config/RegistryConfig.java @@ -1061,7 +1061,7 @@ public final class RegistryConfig { @Provides @Config("premiumTermsExportDisclaimer") public static String providePremiumTermsExportDisclaimer(RegistryConfigSettings config) { - return formatComments(config.registryPolicy.reservedTermsExportDisclaimer); + return formatComments(config.registryPolicy.premiumTermsExportDisclaimer); } /** From aa204be45ef122f185a23d18792915459f34531a Mon Sep 17 00:00:00 2001 From: mcilwain Date: Thu, 27 Sep 2018 07:52:44 -0700 Subject: [PATCH 09/60] Delete unused default registrar referral URL We now require the URL to be filled out by all registrars and so there is no default needed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=214774054 --- java/google/registry/config/RegistryConfigSettings.java | 1 - 1 file changed, 1 deletion(-) diff --git a/java/google/registry/config/RegistryConfigSettings.java b/java/google/registry/config/RegistryConfigSettings.java index eb6ce6f66..6f4d73465 100644 --- a/java/google/registry/config/RegistryConfigSettings.java +++ b/java/google/registry/config/RegistryConfigSettings.java @@ -81,7 +81,6 @@ public class RegistryConfigSettings { public String greetingServerId; public List registrarChangesNotificationEmailAddresses; public String defaultRegistrarWhoisServer; - public String defaultRegistrarReferralUrl; public String tmchCaMode; public String tmchCrlUrl; public String tmchMarksDbUrl; From 2bf06eac77decc8add9d796f238a49852037f549 Mon Sep 17 00:00:00 2001 From: mmuller Date: Thu, 27 Sep 2018 07:58:25 -0700 Subject: [PATCH 10/60] Add dependency on diffutils com_google_truth has a dependency on diffutils that we're not exposing. This becomes problematic in cases where certain tests fail and the equality check can't show the difference. If this happens, instead of the original failure all we see is a failure to load diffutils. Note that com_google_truth appears to have some other dependencies that we are also not exposing, but that so far these have not been problematic. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=214774587 --- java/google/registry/repositories.bzl | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/java/google/registry/repositories.bzl b/java/google/registry/repositories.bzl index 202e7eb49..700e5258a 100644 --- a/java/google/registry/repositories.bzl +++ b/java/google/registry/repositories.bzl @@ -72,6 +72,7 @@ def domain_registry_repositories( omit_com_google_googlejavaformat_google_java_format = False, omit_com_google_guava = False, omit_com_google_guava_testlib = False, + omit_com_google_gwt_user = False, omit_com_google_http_client = False, omit_com_google_http_client_appengine = False, omit_com_google_http_client_jackson2 = False, @@ -88,6 +89,7 @@ def domain_registry_repositories( omit_com_google_template_soy = False, omit_com_google_truth = False, omit_com_google_truth_extensions_truth_java8_extension = False, + omit_com_googlecode_java_diff_utils_diffutils = False, omit_com_googlecode_charts4j = False, omit_com_googlecode_json_simple = False, omit_com_ibm_icu_icu4j = False, @@ -117,6 +119,7 @@ def domain_registry_repositories( omit_javax_inject = False, omit_javax_mail = False, omit_javax_servlet_api = False, + omit_javax_validation_api = False, omit_javax_xml_bind_jaxb_api = False, omit_javax_xml_soap_api = False, omit_javax_xml_ws_jaxws_api = False, @@ -262,6 +265,8 @@ def domain_registry_repositories( com_google_guava() if not omit_com_google_guava_testlib: com_google_guava_testlib() + if not omit_com_google_gwt_user: + com_google_gwt_user() if not omit_com_google_http_client: com_google_http_client() if not omit_com_google_http_client_appengine: @@ -294,6 +299,8 @@ def domain_registry_repositories( com_google_truth() if not omit_com_google_truth_extensions_truth_java8_extension: com_google_truth_extensions_truth_java8_extension() + if not omit_com_googlecode_java_diff_utils_diffutils: + com_googlecode_java_diff_utils_diffutils() if not omit_com_googlecode_charts4j: com_googlecode_charts4j() if not omit_com_googlecode_json_simple: @@ -352,6 +359,8 @@ def domain_registry_repositories( javax_mail() if not omit_javax_servlet_api: javax_servlet_api() + if not omit_javax_validation_api: + javax_validation_api() if not omit_javax_xml_bind_jaxb_api: javax_xml_bind_jaxb_api() if not omit_javax_xml_soap_api: @@ -1342,6 +1351,23 @@ def com_google_guava_testlib(): ], ) +def com_google_gwt_user(): + java_import_external( + name = "com_google_gwt_user", + neverlink = 1, + licenses = ["notice"], # GWT Terms + jar_sha256 = "9f420f0d0c2f177d71cb1794b3be1418f9755f6e4181101af3951b8302b9556d", + jar_urls = [ + "http://maven.ibiblio.org/maven2/com/google/gwt/gwt-user/2.8.2/gwt-user-2.8.2.jar", + "http://repo1.maven.org/maven2/com/google/gwt/gwt-user/2.8.2/gwt-user-2.8.2.jar", + ], + deps = [ + "@javax_validation_api", + "@javax_servlet_api", + "@org_w3c_css_sac", + ], + ) + def com_google_http_client(): java_import_external( name = "com_google_http_client", @@ -1547,6 +1573,7 @@ def com_google_truth(): "@junit", "@com_google_auto_value", "@com_google_errorprone_error_prone_annotations", + "@com_googlecode_java_diff_utils_diffutils", ], ) @@ -1577,6 +1604,17 @@ def com_googlecode_charts4j(): licenses = ["notice"], # The MIT License ) +def com_googlecode_java_diff_utils_diffutils(): + java_import_external( + name = "com_googlecode_java_diff_utils_diffutils", + licenses = ["notice"], # The Apache Software License, Version 2.0 + jar_sha256 = "61ba4dc49adca95243beaa0569adc2a23aedb5292ae78aa01186fa782ebdc5c2", + jar_urls = [ + "http://maven.ibiblio.org/maven2/com/googlecode/java-diff-utils/diffutils/1.3.0/diffutils-1.3.0.jar", + "http://repo1.maven.org/maven2/com/googlecode/java-diff-utils/diffutils/1.3.0/diffutils-1.3.0.jar", + ], + ) + def com_googlecode_json_simple(): java_import_external( name = "com_googlecode_json_simple", @@ -1907,6 +1945,17 @@ def javax_servlet_api(): licenses = ["notice"], # Apache ) +def javax_validation_api(): + java_import_external( + name = "javax_validation_api", + licenses = ["notice"], # Apache License, Version 2.0 + jar_sha256 = "e459f313ebc6db2483f8ceaad39af07086361b474fa92e40f442e8de5d9895dc", + jar_urls = [ + "http://maven.ibiblio.org/maven2/javax/validation/validation-api/1.0.0.GA/validation-api-1.0.0.GA.jar", + "http://repo1.maven.org/maven2/javax/validation/validation-api/1.0.0.GA/validation-api-1.0.0.GA.jar", + ], + ) + def javax_xml_bind_jaxb_api(): java_import_external( name = "javax_xml_bind_jaxb_api", From 70273fa791bcc2543c5115a950427b064d171c26 Mon Sep 17 00:00:00 2001 From: guyben Date: Mon, 1 Oct 2018 07:57:29 -0700 Subject: [PATCH 11/60] Fix error reply from RegistrarSettingsAction RegistrarSettingsAction is a JSON in / JSON out endpoint, meaning the reply is consumed as JSON. The current state is that if an error occurs, there are two possible replies: - a JSON error reply is sent out, or - a 402 HTML reply is sent out with the exception.getMessage() The difference is only - do we actively catch the exception to translate it to JSON or not. This fix catches ALL exceptions and translates them to JSON format. Note that there's no security change by giving the getMessage in the JSON reply since we were returning that anyway (in the HTML). In addition - changed the "gaeUserId" to "user.getEmail" as the identifier, since it's clearer to the users who see that error - and I do want to transition to a more "email identifier" way of checking access (since that's what users put in the registrar contact info) This too isn't leaking new information because - the initial HTML page load already gives the user's email, and - the logs already log the user's email for every request ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=215213807 --- .../registry/request/auth/AuthResult.java | 2 +- .../registrar/RegistrarSettingsAction.java | 2 +- .../ui/server/registrar/SessionUtils.java | 32 +++++++++++-------- .../RegistrarSettingsActionTest.java | 26 ++++++++------- .../RegistrarSettingsActionTestCase.java | 2 +- .../ui/server/registrar/SessionUtilsTest.java | 14 +++++--- 6 files changed, 45 insertions(+), 33 deletions(-) diff --git a/java/google/registry/request/auth/AuthResult.java b/java/google/registry/request/auth/AuthResult.java index 0903f26a0..24291ee74 100644 --- a/java/google/registry/request/auth/AuthResult.java +++ b/java/google/registry/request/auth/AuthResult.java @@ -38,7 +38,7 @@ public abstract class AuthResult { public String userIdForLogging() { return userAuthInfo() - .map(userAuthInfo -> userAuthInfo.user().getUserId()) + .map(userAuthInfo -> userAuthInfo.user().getEmail()) .orElse(""); } diff --git a/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java b/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java index 74f890d4a..39c90aa31 100644 --- a/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java +++ b/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java @@ -123,7 +123,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA logger.atWarning().withCause(e).log( "Failed to perform operation '%s' on registrar '%s' for args %s", op, clientId, args); return JsonResponseHelper.createFormFieldError(e.getMessage(), e.getFieldName()); - } catch (FormException e) { + } catch (Throwable e) { logger.atWarning().withCause(e).log( "Failed to perform operation '%s' on registrar '%s' for args %s", op, clientId, args); return JsonResponseHelper.create(ERROR, e.getMessage()); diff --git a/java/google/registry/ui/server/registrar/SessionUtils.java b/java/google/registry/ui/server/registrar/SessionUtils.java index 91a375e03..4b8c13907 100644 --- a/java/google/registry/ui/server/registrar/SessionUtils.java +++ b/java/google/registry/ui/server/registrar/SessionUtils.java @@ -77,7 +77,7 @@ public class SessionUtils { authResult.userAuthInfo().orElseThrow(() -> new ForbiddenException("Not logged in")); boolean isAdmin = userAuthInfo.isUserAdmin(); User user = userAuthInfo.user(); - String gaeUserId = user.getUserId(); + String userIdForLogging = authResult.userIdForLogging(); Registrar registrar = registrarLoader @@ -85,20 +85,20 @@ public class SessionUtils { .orElseThrow( () -> new ForbiddenException(String.format("Registrar %s not found", clientId))); - if (isInAllowedContacts(registrar, gaeUserId)) { - logger.atInfo().log("User %s has access to registrar %s.", gaeUserId, clientId); + if (isInAllowedContacts(registrar, user)) { + logger.atInfo().log("User %s has access to registrar %s.", userIdForLogging, clientId); return registrar; } if (isAdmin && clientId.equals(registryAdminClientId)) { // Admins have access to the registryAdminClientId even if they aren't explicitly in the // allowed contacts - logger.atInfo().log("Allowing admin %s access to registrar %s.", gaeUserId, clientId); + logger.atInfo().log("Allowing admin %s access to registrar %s.", userIdForLogging, clientId); return registrar; } throw new ForbiddenException( - String.format("User %s doesn't have access to registrar %s", gaeUserId, clientId)); + String.format("User %s doesn't have access to registrar %s", userIdForLogging, clientId)); } /** @@ -126,14 +126,19 @@ public class SessionUtils { authResult.userAuthInfo().orElseThrow(() -> new ForbiddenException("No logged in")); boolean isAdmin = userAuthInfo.isUserAdmin(); User user = userAuthInfo.user(); - String gaeUserId = user.getUserId(); + String userIdForLogging = authResult.userIdForLogging(); RegistrarContact contact = - ofy().load().type(RegistrarContact.class).filter("gaeUserId", gaeUserId).first().now(); + ofy() + .load() + .type(RegistrarContact.class) + .filter("gaeUserId", user.getUserId()) + .first() + .now(); if (contact != null) { String registrarClientId = contact.getParent().getName(); logger.atInfo().log( - "Associating user %s with found registrar %s.", gaeUserId, registrarClientId); + "Associating user %s with found registrar %s.", userIdForLogging, registrarClientId); return registrarClientId; } @@ -144,28 +149,29 @@ public class SessionUtils { logger.atInfo().log( "User %s is an admin with no associated registrar." + " Automatically associating the user with configured client Id %s.", - gaeUserId, registryAdminClientId); + userIdForLogging, registryAdminClientId); return registryAdminClientId; } logger.atInfo().log( "Cannot associate admin user %s with configured client Id." + " ClientId is null or empty.", - gaeUserId); + userIdForLogging); } // We couldn't find any relevant clientId throw new ForbiddenException( - String.format("User %s isn't associated with any registrar", gaeUserId)); + String.format("User %s isn't associated with any registrar", userIdForLogging)); } /** - * Returns {@code true} if {@code gaeUserId} is listed in contacts with access to the registrar. + * Returns {@code true} if {@code user} is listed in contacts with access to the registrar. * *

Each registrar contact can either have getGaeUserId equals null or the user's gaeUserId. * Null means the contact doesn't have access to the registrar console. None-null means the * contact has access. */ - private static boolean isInAllowedContacts(Registrar registrar, final String gaeUserId) { + private static boolean isInAllowedContacts(Registrar registrar, User user) { + String gaeUserId = user.getUserId(); return registrar .getContacts() .stream() diff --git a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java index 68efd8c58..4ffd12324 100644 --- a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java +++ b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java @@ -17,7 +17,6 @@ package google.registry.ui.server.registrar; import static com.google.common.truth.Truth.assertThat; import static google.registry.testing.DatastoreHelper.loadRegistrar; import static google.registry.testing.DatastoreHelper.persistResource; -import static google.registry.testing.JUnitBackports.assertThrows; import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued; import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued; import static google.registry.testing.TestDataHelper.loadFile; @@ -30,7 +29,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import google.registry.export.sheet.SyncRegistrarsSheetAction; import google.registry.model.registrar.Registrar; -import google.registry.request.HttpException.ForbiddenException; import google.registry.testing.CertificateSamples; import google.registry.testing.TaskQueueHelper.TaskMatcher; import google.registry.util.CidrAddressBlock; @@ -84,8 +82,11 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase @Test public void testFailure_readRegistrarInfo_notAuthorized() { action.authResult = USER_UNAUTHORIZED; - assertThrows( - ForbiddenException.class, () -> action.handleJsonRequest(ImmutableMap.of("id", CLIENT_ID))); + Map response = action.handleJsonRequest(ImmutableMap.of("id", CLIENT_ID)); + assertThat(response).containsExactly( + "status", "ERROR", + "results", ImmutableList.of(), + "message", "forbidden test error"); assertNoTasksEnqueued("sheet"); } @@ -145,14 +146,15 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase @Test public void testFailute_updateRegistrarInfo_notAuthorized() { action.authResult = USER_UNAUTHORIZED; - assertThrows( - ForbiddenException.class, - () -> - action.handleJsonRequest( - ImmutableMap.of( - "op", "update", - "id", CLIENT_ID, - "args", ImmutableMap.of("lastUpdateTime", getLastUpdateTime())))); + Map response = action.handleJsonRequest(ImmutableMap.of( + "op", "update", + "id", CLIENT_ID, + "args", ImmutableMap.of("lastUpdateTime", getLastUpdateTime()))); + assertThat(response).containsExactly( + "status", "ERROR", + "results", ImmutableList.of(), + "message", "forbidden test error"); + assertNoTasksEnqueued("sheet"); } @Test diff --git a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java index 7c4e4c8ca..627c884e8 100644 --- a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java +++ b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java @@ -112,6 +112,6 @@ public class RegistrarSettingsActionTestCase { when(sessionUtils.getRegistrarForUser(CLIENT_ID, USER_AUTHORIZED)) .thenAnswer(x -> loadRegistrar(CLIENT_ID)); when(sessionUtils.getRegistrarForUser(CLIENT_ID, USER_UNAUTHORIZED)) - .thenThrow(new ForbiddenException("forbidden")); + .thenThrow(new ForbiddenException("forbidden test error")); } } diff --git a/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java b/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java index 3a5137b1b..22af3a5eb 100644 --- a/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java +++ b/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java @@ -68,8 +68,9 @@ public class SessionUtilsTest { AuthLevel.USER, UserAuthInfo.create( new User( - "user1@google.com", - "google.com", + String.format( + "%s_%s@gmail.com", isAuthorized ? "good" : "evil", isAdmin ? "admin" : "user"), + "gmail.com", isAuthorized ? THE_REGISTRAR_GAE_USER_ID : "badGaeUserId"), isAdmin)); } @@ -211,8 +212,11 @@ public class SessionUtilsTest { .that(testLogHandler) .hasLogAtLevelWithMessage( Level.INFO, - "Cannot associate admin user badGaeUserId with configured client Id." - + " ClientId is null or empty."); + formatMessage( + "Cannot associate admin user {user} with configured client Id." + + " ClientId is null or empty.", + UNAUTHORIZED_ADMIN, + null)); } /** @@ -241,7 +245,7 @@ public class SessionUtilsTest { assertThrows(ForbiddenException.class, () -> sessionUtils.guessClientIdForUser(authResult)); assertThat(exception) .hasMessageThat() - .contains(formatMessage(message, UNAUTHORIZED_USER, null)); + .contains(formatMessage(message, authResult, null)); } @Test From 5038fa917c3a16d190594f65922d33c0a7a5fe4f Mon Sep 17 00:00:00 2001 From: weiminyu Date: Mon, 1 Oct 2018 12:21:34 -0700 Subject: [PATCH 12/60] Remove outdated credential modules All credentials provided by these modules have been replaced by those in the config/CredentialsModule, with a new set of Qualifiers. With Dagger 2, a successful build means that the removal is safe. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=215258792 --- .../module/backend/BackendComponent.java | 6 -- .../module/frontend/FrontendComponent.java | 6 -- .../module/pubapi/PubApiComponent.java | 6 -- .../registry/module/tools/ToolsComponent.java | 6 -- java/google/registry/request/Modules.java | 95 ------------------- .../registry/tools/RegistryToolComponent.java | 7 -- 6 files changed, 126 deletions(-) diff --git a/java/google/registry/module/backend/BackendComponent.java b/java/google/registry/module/backend/BackendComponent.java index 53073e658..9ef32033a 100644 --- a/java/google/registry/module/backend/BackendComponent.java +++ b/java/google/registry/module/backend/BackendComponent.java @@ -32,14 +32,11 @@ import google.registry.keyring.kms.KmsModule; import google.registry.module.backend.BackendRequestComponent.BackendRequestComponentModule; import google.registry.monitoring.whitebox.StackdriverModule; import google.registry.rde.JSchModule; -import google.registry.request.Modules.AppIdentityCredentialModule; import google.registry.request.Modules.DatastoreServiceModule; -import google.registry.request.Modules.GoogleCredentialModule; import google.registry.request.Modules.Jackson2Module; import google.registry.request.Modules.NetHttpTransportModule; import google.registry.request.Modules.URLFetchServiceModule; import google.registry.request.Modules.UrlFetchTransportModule; -import google.registry.request.Modules.UseAppIdentityCredentialForGoogleApisModule; import google.registry.request.Modules.UserServiceModule; import google.registry.request.auth.AuthModule; import google.registry.util.AppEngineServiceUtilsImpl.AppEngineServiceUtilsModule; @@ -52,7 +49,6 @@ import javax.inject.Singleton; @Component( modules = { AppEngineServiceUtilsModule.class, - AppIdentityCredentialModule.class, AuthModule.class, BackendRequestComponentModule.class, BigqueryModule.class, @@ -63,7 +59,6 @@ import javax.inject.Singleton; google.registry.keyring.api.DummyKeyringModule.class, DriveModule.class, GcsServiceModule.class, - GoogleCredentialModule.class, GroupsModule.class, GroupssettingsModule.class, JSchModule.class, @@ -77,7 +72,6 @@ import javax.inject.Singleton; SystemSleeperModule.class, URLFetchServiceModule.class, UrlFetchTransportModule.class, - UseAppIdentityCredentialForGoogleApisModule.class, UserServiceModule.class, VoidDnsWriterModule.class, }) diff --git a/java/google/registry/module/frontend/FrontendComponent.java b/java/google/registry/module/frontend/FrontendComponent.java index 263f4b6b7..ee2e04792 100644 --- a/java/google/registry/module/frontend/FrontendComponent.java +++ b/java/google/registry/module/frontend/FrontendComponent.java @@ -25,12 +25,9 @@ import google.registry.keyring.api.KeyModule; import google.registry.keyring.kms.KmsModule; import google.registry.module.frontend.FrontendRequestComponent.FrontendRequestComponentModule; import google.registry.monitoring.whitebox.StackdriverModule; -import google.registry.request.Modules.AppIdentityCredentialModule; -import google.registry.request.Modules.GoogleCredentialModule; import google.registry.request.Modules.Jackson2Module; import google.registry.request.Modules.NetHttpTransportModule; import google.registry.request.Modules.UrlFetchTransportModule; -import google.registry.request.Modules.UseAppIdentityCredentialForGoogleApisModule; import google.registry.request.Modules.UserServiceModule; import google.registry.request.auth.AuthModule; import google.registry.ui.ConsoleDebug.ConsoleConfigModule; @@ -44,7 +41,6 @@ import javax.inject.Singleton; @Component( modules = { AppEngineServiceUtilsModule.class, - AppIdentityCredentialModule.class, AuthModule.class, ConfigModule.class, ConsoleConfigModule.class, @@ -52,7 +48,6 @@ import javax.inject.Singleton; CustomLogicFactoryModule.class, google.registry.keyring.api.DummyKeyringModule.class, FrontendRequestComponentModule.class, - GoogleCredentialModule.class, Jackson2Module.class, KeyModule.class, KmsModule.class, @@ -62,7 +57,6 @@ import javax.inject.Singleton; SystemClockModule.class, SystemSleeperModule.class, UrlFetchTransportModule.class, - UseAppIdentityCredentialForGoogleApisModule.class, UserServiceModule.class, }) interface FrontendComponent { diff --git a/java/google/registry/module/pubapi/PubApiComponent.java b/java/google/registry/module/pubapi/PubApiComponent.java index da606f81b..383ba559c 100644 --- a/java/google/registry/module/pubapi/PubApiComponent.java +++ b/java/google/registry/module/pubapi/PubApiComponent.java @@ -25,12 +25,9 @@ import google.registry.keyring.api.KeyModule; import google.registry.keyring.kms.KmsModule; import google.registry.module.pubapi.PubApiRequestComponent.PubApiRequestComponentModule; import google.registry.monitoring.whitebox.StackdriverModule; -import google.registry.request.Modules.AppIdentityCredentialModule; -import google.registry.request.Modules.GoogleCredentialModule; import google.registry.request.Modules.Jackson2Module; import google.registry.request.Modules.NetHttpTransportModule; import google.registry.request.Modules.UrlFetchTransportModule; -import google.registry.request.Modules.UseAppIdentityCredentialForGoogleApisModule; import google.registry.request.Modules.UserServiceModule; import google.registry.request.auth.AuthModule; import google.registry.util.AppEngineServiceUtilsImpl.AppEngineServiceUtilsModule; @@ -43,14 +40,12 @@ import javax.inject.Singleton; @Component( modules = { AppEngineServiceUtilsModule.class, - AppIdentityCredentialModule.class, AuthModule.class, ConfigModule.class, CredentialModule.class, CustomLogicFactoryModule.class, google.registry.keyring.api.DummyKeyringModule.class, PubApiRequestComponentModule.class, - GoogleCredentialModule.class, Jackson2Module.class, KeyModule.class, KmsModule.class, @@ -60,7 +55,6 @@ import javax.inject.Singleton; SystemClockModule.class, SystemSleeperModule.class, UrlFetchTransportModule.class, - UseAppIdentityCredentialForGoogleApisModule.class, UserServiceModule.class, }) interface PubApiComponent { diff --git a/java/google/registry/module/tools/ToolsComponent.java b/java/google/registry/module/tools/ToolsComponent.java index 26495260a..bcf3b296b 100644 --- a/java/google/registry/module/tools/ToolsComponent.java +++ b/java/google/registry/module/tools/ToolsComponent.java @@ -27,13 +27,10 @@ import google.registry.groups.GroupssettingsModule; import google.registry.keyring.api.KeyModule; import google.registry.keyring.kms.KmsModule; import google.registry.module.tools.ToolsRequestComponent.ToolsRequestComponentModule; -import google.registry.request.Modules.AppIdentityCredentialModule; import google.registry.request.Modules.DatastoreServiceModule; -import google.registry.request.Modules.GoogleCredentialModule; import google.registry.request.Modules.Jackson2Module; import google.registry.request.Modules.NetHttpTransportModule; import google.registry.request.Modules.UrlFetchTransportModule; -import google.registry.request.Modules.UseAppIdentityCredentialForGoogleApisModule; import google.registry.request.Modules.UserServiceModule; import google.registry.request.auth.AuthModule; import google.registry.util.AppEngineServiceUtilsImpl.AppEngineServiceUtilsModule; @@ -46,7 +43,6 @@ import javax.inject.Singleton; @Component( modules = { AppEngineServiceUtilsModule.class, - AppIdentityCredentialModule.class, AuthModule.class, ConfigModule.class, CredentialModule.class, @@ -56,7 +52,6 @@ import javax.inject.Singleton; google.registry.keyring.api.DummyKeyringModule.class, DriveModule.class, GcsServiceModule.class, - GoogleCredentialModule.class, GroupsModule.class, GroupssettingsModule.class, Jackson2Module.class, @@ -68,7 +63,6 @@ import javax.inject.Singleton; SystemSleeperModule.class, ToolsRequestComponentModule.class, UrlFetchTransportModule.class, - UseAppIdentityCredentialForGoogleApisModule.class, UserServiceModule.class, }) interface ToolsComponent { diff --git a/java/google/registry/request/Modules.java b/java/google/registry/request/Modules.java index 5b9cdf0ff..af043552a 100644 --- a/java/google/registry/request/Modules.java +++ b/java/google/registry/request/Modules.java @@ -15,13 +15,9 @@ package google.registry.request; import static com.google.appengine.api.datastore.DatastoreServiceFactory.getDatastoreService; -import static java.nio.charset.StandardCharsets.UTF_8; import com.google.api.client.extensions.appengine.http.UrlFetchTransport; -import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; -import com.google.api.client.googleapis.extensions.appengine.auth.oauth2.AppIdentityCredential; import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; -import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.JsonFactory; @@ -31,15 +27,8 @@ import com.google.appengine.api.urlfetch.URLFetchService; import com.google.appengine.api.urlfetch.URLFetchServiceFactory; import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; -import dagger.Binds; import dagger.Module; import dagger.Provides; -import google.registry.keyring.api.KeyModule.Key; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Set; -import java.util.function.Function; -import javax.inject.Provider; import javax.inject.Singleton; /** Dagger modules for App Engine services and other vendor classes. */ @@ -120,88 +109,4 @@ public final class Modules { } } } - - /** - * Dagger module providing {@link AppIdentityCredential}. - * - *

This can be used to authenticate to Google APIs using the identity of your GAE app. - * - * @see UseAppIdentityCredentialForGoogleApisModule - */ - @Module - public static final class AppIdentityCredentialModule { - @Provides - static Function, AppIdentityCredential> provideAppIdentityCredential() { - return AppIdentityCredential::new; - } - } - - /** - * Dagger module causing Google APIs requests to be authorized with your GAE app identity. - * - *

You must also use the {@link AppIdentityCredentialModule}. - */ - @Module - public abstract static class UseAppIdentityCredentialForGoogleApisModule { - @Binds - abstract Function, ? extends HttpRequestInitializer> provideHttpRequestInitializer( - Function, AppIdentityCredential> credential); - } - - /** - * Module indicating Google API requests should be authorized with JSON {@link GoogleCredential}. - * - *

This is useful when configuring a component that runs the registry outside of the App Engine - * environment, for example, in a command line environment. - * - *

You must also use the {@link GoogleCredentialModule}. - */ - @Module - public abstract static class UseGoogleCredentialForGoogleApisModule { - @Binds - abstract Function, ? extends HttpRequestInitializer> provideHttpRequestInitializer( - Function, GoogleCredential> credential); - } - - /** - * Dagger module providing {@link GoogleCredential} from a JSON key file contents. - * - *

This satisfies the {@link HttpRequestInitializer} interface for authenticating Google APIs - * requests, just like {@link AppIdentityCredential}. - * - *

But we consider GAE authentication more desirable and easier to manage operations-wise. So - * this authentication method should only be used for the following situations: - * - *

    - *
  1. Locally-running programs (which aren't executing on the App Engine platform) - *
  2. Spreadsheet service (which can't use {@link AppIdentityCredential} due to an old library) - *
- * - * @see google.registry.keyring.api.Keyring#getJsonCredential() - */ - @Module - public static final class GoogleCredentialModule { - - @Provides - @Singleton - static GoogleCredential provideGoogleCredential( - NetHttpTransport netHttpTransport, - JsonFactory jsonFactory, - @Key("jsonCredential") String jsonCredential) { - try { - return GoogleCredential.fromStream( - new ByteArrayInputStream(jsonCredential.getBytes(UTF_8)), - netHttpTransport, - jsonFactory); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Provides - static Function, GoogleCredential> provideScopedGoogleCredential( - final Provider googleCredentialProvider) { - return scopes -> googleCredentialProvider.get().createScoped(scopes); - } - } } diff --git a/java/google/registry/tools/RegistryToolComponent.java b/java/google/registry/tools/RegistryToolComponent.java index e069ff12d..6dbae7c08 100644 --- a/java/google/registry/tools/RegistryToolComponent.java +++ b/java/google/registry/tools/RegistryToolComponent.java @@ -24,13 +24,10 @@ import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; import google.registry.keyring.api.KeyModule; import google.registry.keyring.kms.KmsModule; import google.registry.rde.RdeModule; -import google.registry.request.Modules.AppIdentityCredentialModule; import google.registry.request.Modules.DatastoreServiceModule; -import google.registry.request.Modules.GoogleCredentialModule; import google.registry.request.Modules.Jackson2Module; import google.registry.request.Modules.URLFetchServiceModule; import google.registry.request.Modules.UrlFetchTransportModule; -import google.registry.request.Modules.UseAppIdentityCredentialForGoogleApisModule; import google.registry.request.Modules.UserServiceModule; import google.registry.util.AppEngineServiceUtilsImpl.AppEngineServiceUtilsModule; import google.registry.util.SystemClock.SystemClockModule; @@ -50,7 +47,6 @@ import javax.inject.Singleton; AppEngineConnectionFlags.FlagsModule.class, AppEngineServiceUtilsModule.class, // TODO(b/36866706): Find a way to replace this with a command-line friendly version - AppIdentityCredentialModule.class, AuthModule.class, BigqueryModule.class, ConfigModule.class, @@ -61,7 +57,6 @@ import javax.inject.Singleton; DefaultRequestFactoryModule.class, DefaultRequestFactoryModule.RequestFactoryModule.class, DnsUpdateWriterModule.class, - GoogleCredentialModule.class, Jackson2Module.class, KeyModule.class, KmsModule.class, @@ -71,8 +66,6 @@ import javax.inject.Singleton; SystemSleeperModule.class, URLFetchServiceModule.class, UrlFetchTransportModule.class, - // TODO(b/36866706): Find a way to replace this with a command-line friendly version - UseAppIdentityCredentialForGoogleApisModule.class, UserServiceModule.class, VoidDnsWriterModule.class, WhoisModule.class, From 1d621bd14df7f3a3975f18fd2286d4179a204400 Mon Sep 17 00:00:00 2001 From: guyben Date: Tue, 2 Oct 2018 16:32:35 -0700 Subject: [PATCH 13/60] Allow admins read-only access to all registrars We want to be able to view / test / debug how the registrar console looks for our clients. However, we don't want to accidentally change the data for registrars, especially in a "non-accountable" way (where we later don't know who did that change) So we do 2 things here: - Add a "mode" (read-only and read-write) to the getRegistrarForUser function. We set it according to what we want to do with the registrar. Currently, read-write is only requested for the "update" RegistrarSetting action. Admins will have read-only access to all registrars, but read-write access only to the "admin registrar" (or whatever registrar they are contacts for). - Support an undocumented "clientId=XXX" query param that replaces the "guessClientIdForUser" function in the original page load. We can then set it when we want to view a different account. We also change the navigation links on the HTML page to preserve the query. ------------------------- This might be used also for a better user experience for our clients, especially those with multiple "clientId"s (some registrar entities have multiple "registrar" objects) Currently, they have to have a separate user for each clientId, and only have one user allowed which has both read and write permissions. Using this change, we can give them the possibility to add users on their own, some with read-only access (to view billing information without being able to change anything), and use a single user for all their clientIds. ------------------------- ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=215480610 --- .../frontend/FrontendRequestComponent.java | 2 + java/google/registry/rdap/RdapActionBase.java | 3 +- .../registry/ui/js/registrar/console.js | 2 +- .../ui/server/registrar/ConsoleUiAction.java | 10 +- .../registrar/RegistrarConsoleModule.java | 37 ++++++ .../registrar/RegistrarSettingsAction.java | 9 +- .../ui/server/registrar/SessionUtils.java | 33 +++-- .../registry/ui/soy/registrar/Console.soy | 32 +++-- .../registry/ui/js/registrar/console_test.js | 2 +- .../server/registrar/ConsoleUiActionTest.java | 26 +++- .../RegistrarSettingsActionTest.java | 30 ++++- .../RegistrarSettingsActionTestCase.java | 30 +++-- .../ui/server/registrar/SessionUtilsTest.java | 114 +++++++++++++----- 13 files changed, 267 insertions(+), 63 deletions(-) create mode 100644 java/google/registry/ui/server/registrar/RegistrarConsoleModule.java diff --git a/java/google/registry/module/frontend/FrontendRequestComponent.java b/java/google/registry/module/frontend/FrontendRequestComponent.java index 316da2b51..19c22964b 100644 --- a/java/google/registry/module/frontend/FrontendRequestComponent.java +++ b/java/google/registry/module/frontend/FrontendRequestComponent.java @@ -26,12 +26,14 @@ import google.registry.request.RequestComponentBuilder; import google.registry.request.RequestModule; import google.registry.request.RequestScope; import google.registry.ui.server.registrar.ConsoleUiAction; +import google.registry.ui.server.registrar.RegistrarConsoleModule; import google.registry.ui.server.registrar.RegistrarSettingsAction; /** Dagger component with per-request lifetime for "default" App Engine module. */ @RequestScope @Subcomponent( modules = { + RegistrarConsoleModule.class, DnsModule.class, EppTlsModule.class, RequestModule.class, diff --git a/java/google/registry/rdap/RdapActionBase.java b/java/google/registry/rdap/RdapActionBase.java index c08a71f78..6fe80ef49 100644 --- a/java/google/registry/rdap/RdapActionBase.java +++ b/java/google/registry/rdap/RdapActionBase.java @@ -18,6 +18,7 @@ import static com.google.common.base.Charsets.UTF_8; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY; import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DomainNameUtils.canonicalizeDomainName; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; @@ -181,7 +182,7 @@ public abstract class RdapActionBase implements Runnable { String clientId = sessionUtils.guessClientIdForUser(authResult); // We load the Registrar to make sure the user has access to it. We don't actually need it, // we're just checking if an exception is thrown. - sessionUtils.getRegistrarForUserCached(clientId, authResult); + sessionUtils.getRegistrarForUserCached(clientId, READ_ONLY, authResult); return Optional.of(clientId); } catch (Exception e) { logger.atWarning().withCause(e).log( diff --git a/java/google/registry/ui/js/registrar/console.js b/java/google/registry/ui/js/registrar/console.js index 2fc49b3f9..9c8d639da 100644 --- a/java/google/registry/ui/js/registrar/console.js +++ b/java/google/registry/ui/js/registrar/console.js @@ -160,7 +160,7 @@ registry.registrar.Console.prototype.changeNavStyle = function() { slashNdx = slashNdx == -1 ? hashToken.length : slashNdx; var regNavlist = goog.dom.getRequiredElement('reg-navlist'); var path = hashToken.substring(0, slashNdx); - var active = regNavlist.querySelector('a[href="/registrar#' + path + '"]'); + var active = regNavlist.querySelector('a[href="#' + path + '"]'); if (goog.isNull(active)) { registry.util.log('Unknown path or path form in changeNavStyle.'); return; diff --git a/java/google/registry/ui/server/registrar/ConsoleUiAction.java b/java/google/registry/ui/server/registrar/ConsoleUiAction.java index 11dbf510c..c3d682a3f 100644 --- a/java/google/registry/ui/server/registrar/ConsoleUiAction.java +++ b/java/google/registry/ui/server/registrar/ConsoleUiAction.java @@ -16,6 +16,8 @@ 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 google.registry.ui.server.registrar.RegistrarConsoleModule.PARAM_CLIENT_ID; +import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY; import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; @@ -34,12 +36,14 @@ import google.registry.config.RegistryConfig.Config; import google.registry.model.registrar.Registrar; import google.registry.request.Action; import google.registry.request.HttpException.ForbiddenException; +import google.registry.request.Parameter; import google.registry.request.Response; import google.registry.request.auth.Auth; import google.registry.request.auth.AuthResult; import google.registry.security.XsrfTokenManager; import google.registry.ui.server.SoyTemplateUtils; import google.registry.ui.soy.registrar.ConsoleSoyInfo; +import java.util.Optional; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; @@ -79,6 +83,7 @@ public final class ConsoleUiAction implements Runnable { @Inject @Config("supportPhoneNumber") String supportPhoneNumber; @Inject @Config("technicalDocsUrl") String technicalDocsUrl; @Inject @Config("registrarConsoleEnabled") boolean enabled; + @Inject @Parameter(PARAM_CLIENT_ID) Optional paramClientId; @Inject ConsoleUiAction() {} @Override @@ -126,7 +131,8 @@ public final class ConsoleUiAction implements Runnable { data.put("logoutUrl", userService.createLogoutURL(PATH)); data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmail())); try { - String clientId = sessionUtils.guessClientIdForUser(authResult); + String clientId = + paramClientId.orElseGet(() -> sessionUtils.guessClientIdForUser(authResult)); data.put("clientId", clientId); // We want to load the registrar even if we won't use it later (even if we remove the // requireFeeExtension) - to make sure the user indeed has access to the guessed registrar. @@ -135,7 +141,7 @@ public final class ConsoleUiAction implements Runnable { // since we double check the access to the registrar on any read / update request. We have to // - since the access might get revoked between the initial page load and the request! (also // because the requests come from the browser, and can easily be faked) - Registrar registrar = sessionUtils.getRegistrarForUser(clientId, authResult); + Registrar registrar = sessionUtils.getRegistrarForUser(clientId, READ_ONLY, authResult); data.put("requireFeeExtension", registrar.getPremiumPriceAckRequired()); } catch (ForbiddenException e) { logger.atWarning().withCause(e).log( diff --git a/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java b/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java new file mode 100644 index 000000000..9026c5ce5 --- /dev/null +++ b/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java @@ -0,0 +1,37 @@ +// 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.ui.server.registrar; + + +import static google.registry.request.RequestParameters.extractOptionalParameter; + +import dagger.Module; +import dagger.Provides; +import google.registry.request.Parameter; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; + +/** Dagger module for the Registrar Console parameters. */ +@Module +public final class RegistrarConsoleModule { + + static final String PARAM_CLIENT_ID = "clientId"; + + @Provides + @Parameter(PARAM_CLIENT_ID) + static Optional provideClientId(HttpServletRequest req) { + return extractOptionalParameter(req, PARAM_CLIENT_ID); + } +} diff --git a/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java b/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java index 39c90aa31..cbd054243 100644 --- a/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java +++ b/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java @@ -20,6 +20,8 @@ import static google.registry.export.sheet.SyncRegistrarsSheetAction.enqueueRegi import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.security.JsonResponseHelper.Status.ERROR; import static google.registry.security.JsonResponseHelper.Status.SUCCESS; +import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY; +import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_WRITE; import com.google.common.base.Strings; import com.google.common.collect.HashMultimap; @@ -132,7 +134,9 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA Map read(String clientId) { return JsonResponseHelper.create( - SUCCESS, "Success", sessionUtils.getRegistrarForUser(clientId, authResult).toJsonMap()); + SUCCESS, + "Success", + sessionUtils.getRegistrarForUser(clientId, READ_ONLY, authResult).toJsonMap()); } Map update(final Map args, String clientId) { @@ -142,7 +146,8 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA // We load the registrar here rather than outside of the transaction - to make // sure we have the latest version. This one is loaded inside the transaction, so it's // guaranteed to not change before we update it. - Registrar registrar = sessionUtils.getRegistrarForUser(clientId, authResult); + Registrar registrar = + sessionUtils.getRegistrarForUser(clientId, READ_WRITE, authResult); // Verify that the registrar hasn't been changed. // To do that - we find the latest update time (or null if the registrar has been // deleted) and compare to the update time from the args. The update time in the args diff --git a/java/google/registry/ui/server/registrar/SessionUtils.java b/java/google/registry/ui/server/registrar/SessionUtils.java index 4b8c13907..57a63048f 100644 --- a/java/google/registry/ui/server/registrar/SessionUtils.java +++ b/java/google/registry/ui/server/registrar/SessionUtils.java @@ -30,7 +30,7 @@ import java.util.function.Function; import javax.annotation.concurrent.Immutable; import javax.inject.Inject; -/** Authenticated Registrar access helper class. */ +/** HTTP session management helper class. */ @Immutable public class SessionUtils { @@ -43,6 +43,9 @@ public class SessionUtils { @Inject public SessionUtils() {} + /** Type of access we're requesting. */ + public enum AccessType {READ_ONLY, READ_WRITE} + /** * Loads Registrar on behalf of an authorised user. * @@ -50,10 +53,13 @@ public class SessionUtils { * access the requested registrar. * * @param clientId ID of the registrar we request + * @param accessType what kind of access do we want for this registrar - just read it or write as + * well? (different users might have different access levels) * @param authResult AuthResult of the user on behalf of which we want to access the data */ - public Registrar getRegistrarForUser(String clientId, AuthResult authResult) { - return getAndAuthorize(Registrar::loadByClientId, clientId, authResult); + public Registrar getRegistrarForUser( + String clientId, AccessType accessType, AuthResult authResult) { + return getAndAuthorize(Registrar::loadByClientId, clientId, accessType, authResult); } /** @@ -63,15 +69,19 @@ public class SessionUtils { * access the requested registrar. * * @param clientId ID of the registrar we request + * @param accessType what kind of access do we want for this registrar - just read it or write as + * well? (different users might have different access levels) * @param authResult AuthResult of the user on behalf of which we want to access the data */ - public Registrar getRegistrarForUserCached(String clientId, AuthResult authResult) { - return getAndAuthorize(Registrar::loadByClientIdCached, clientId, authResult); + public Registrar getRegistrarForUserCached( + String clientId, AccessType accessType, AuthResult authResult) { + return getAndAuthorize(Registrar::loadByClientIdCached, clientId, accessType, authResult); } - Registrar getAndAuthorize( + private Registrar getAndAuthorize( Function> registrarLoader, String clientId, + AccessType accessType, AuthResult authResult) { UserAuthInfo userAuthInfo = authResult.userAuthInfo().orElseThrow(() -> new ForbiddenException("Not logged in")); @@ -97,8 +107,17 @@ public class SessionUtils { return registrar; } + if (isAdmin && accessType == AccessType.READ_ONLY) { + // Admins have read-only access to all registrars + logger.atInfo().log( + "Allowing admin %s read-only access to registrar %s.", userIdForLogging, clientId); + return registrar; + } + throw new ForbiddenException( - String.format("User %s doesn't have access to registrar %s", userIdForLogging, clientId)); + String.format( + "User %s doesn't have %s access to registrar %s", + userIdForLogging, accessType, clientId)); } /** diff --git a/java/google/registry/ui/soy/registrar/Console.soy b/java/google/registry/ui/soy/registrar/Console.soy index 860aba56a..7beb32b87 100644 --- a/java/google/registry/ui/soy/registrar/Console.soy +++ b/java/google/registry/ui/soy/registrar/Console.soy @@ -78,21 +78,21 @@ {/template} @@ -130,19 +130,29 @@ {@param logoutUrl: string} /** Generated URL for logging out of Google. */ {@param logoFilename: string} {@param productName: string} + {@param? clientId: string} {call registry.soy.console.header} {param app: 'registrar' /} - {param subtitle: 'Please Login' /} + {param subtitle: 'Not Authorized' /} {/call}
{$productName}

You need permission

-

- The account you are logged in as is not associated with {$productName}. - Please contact your customer service representative or - switch to an account associated with {$productName}. + {if isNonnull($clientId)} // A clientId was given - but we don't have access to it +

+ The account you are logged in as is not associated with the registrar + {sp}{$clientId}. Please contact your customer service representative or + switch to an account associated with {$clientId}. Alternatively, click + {sp}here to find a registrar associated with your + account. + {else} +

+ The account you are logged in as is not associated with {$productName}. + Please contact your customer service representative or + switch to an account associated with {$productName}. + {/if}

You are signed in as {$username}.

diff --git a/javatests/google/registry/ui/js/registrar/console_test.js b/javatests/google/registry/ui/js/registrar/console_test.js index fd3a346e4..1ba16afa8 100644 --- a/javatests/google/registry/ui/js/registrar/console_test.js +++ b/javatests/google/registry/ui/js/registrar/console_test.js @@ -61,7 +61,7 @@ function setUp() { }); registry.registrar.ConsoleTestUtil.setup(test); var regNavlist = $('reg-navlist'); - var active = regNavlist.querySelector('a[href="/registrar#contact-us"]'); + var active = regNavlist.querySelector('a[href="#contact-us"]'); assertTrue(active != null); } diff --git a/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java b/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java index c259af5df..9e329e7ad 100644 --- a/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java +++ b/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java @@ -17,6 +17,7 @@ package google.registry.ui.server.registrar; import static com.google.common.net.HttpHeaders.LOCATION; import static com.google.common.truth.Truth.assertThat; import static google.registry.testing.DatastoreHelper.loadRegistrar; +import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY; import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; @@ -34,6 +35,7 @@ import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.UserInfo; +import java.util.Optional; import javax.servlet.http.HttpServletRequest; import org.junit.Before; import org.junit.Rule; @@ -72,10 +74,11 @@ public class ConsoleUiActionTest { action.sessionUtils = sessionUtils; action.userService = UserServiceFactory.getUserService(); action.xsrfTokenManager = new XsrfTokenManager(new FakeClock(), action.userService); + action.paramClientId = Optional.empty(); AuthResult authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false)); action.authResult = authResult; when(sessionUtils.guessClientIdForUser(authResult)).thenReturn("TheRegistrar"); - when(sessionUtils.getRegistrarForUser("TheRegistrar", authResult)) + when(sessionUtils.getRegistrarForUser("TheRegistrar", READ_ONLY, authResult)) .thenReturn(loadRegistrar("TheRegistrar")); } @@ -117,6 +120,7 @@ public class ConsoleUiActionTest { .thenThrow(new ForbiddenException("forbidden")); action.run(); assertThat(response.getPayload()).contains("

You need permission

"); + assertThat(response.getPayload()).contains("not associated with Nomulus."); } @Test @@ -136,4 +140,24 @@ public class ConsoleUiActionTest { assertThat(response.getStatus()).isEqualTo(SC_MOVED_TEMPORARILY); assertThat(response.getHeaders().get(LOCATION)).isEqualTo("/"); } + + @Test + public void testSettingClientId_notAllowed_showsNeedPermissionPage() { + action.paramClientId = Optional.of("OtherClientId"); + when(sessionUtils.getRegistrarForUser("OtherClientId", READ_ONLY, action.authResult)) + .thenThrow(new ForbiddenException("forbidden")); + action.run(); + assertThat(response.getPayload()).contains("

You need permission

"); + assertThat(response.getPayload()).contains("not associated with the registrar OtherClientId."); + } + + @Test + public void testSettingClientId_allowed_showsRegistrarConsole() { + action.paramClientId = Optional.of("OtherClientId"); + when(sessionUtils.getRegistrarForUser("OtherClientId", READ_ONLY, action.authResult)) + .thenReturn(loadRegistrar("TheRegistrar")); + action.run(); + assertThat(response.getPayload()).contains("Registrar Console"); + assertThat(response.getPayload()).contains("reg-content-and-footer"); + } } diff --git a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java index 4ffd12324..596a551c1 100644 --- a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java +++ b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java @@ -92,7 +92,19 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase /** This is the default read test for the registrar settings actions. */ @Test - public void testSuccess_readRegistrarInfo_authorized() { + public void testSuccess_readRegistrarInfo_authorizedReadWrite() { + Map response = action.handleJsonRequest(ImmutableMap.of("id", CLIENT_ID)); + assertThat(response) + .containsExactly( + "status", "SUCCESS", + "message", "Success", + "results", asList(loadRegistrar(CLIENT_ID).toJsonMap())); + } + + /** This is the default read test for the registrar settings actions. */ + @Test + public void testSuccess_readRegistrarInfo_authorizedReadOnly() { + action.authResult = USER_READ_ONLY; Map response = action.handleJsonRequest(ImmutableMap.of("id", CLIENT_ID)); assertThat(response) .containsExactly( @@ -144,7 +156,7 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase } @Test - public void testFailute_updateRegistrarInfo_notAuthorized() { + public void testFailure_updateRegistrarInfo_notAuthorized() { action.authResult = USER_UNAUTHORIZED; Map response = action.handleJsonRequest(ImmutableMap.of( "op", "update", @@ -157,6 +169,20 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase assertNoTasksEnqueued("sheet"); } + @Test + public void testFailure_updateRegistrarInfo_readOnlyAccess() { + action.authResult = USER_READ_ONLY; + Map response = action.handleJsonRequest(ImmutableMap.of( + "op", "update", + "id", CLIENT_ID, + "args", ImmutableMap.of("lastUpdateTime", getLastUpdateTime()))); + assertThat(response).containsExactly( + "status", "ERROR", + "results", ImmutableList.of(), + "message", "forbidden test error"); + assertNoTasksEnqueued("sheet"); + } + @Test public void testUpdate_badEmail_errorEmailField() { Map response = action.handleJsonRequest(ImmutableMap.of( diff --git a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java index 627c884e8..8f5c0b7b9 100644 --- a/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java +++ b/javatests/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java @@ -18,6 +18,8 @@ import static google.registry.config.RegistryConfig.getGSuiteOutgoingEmailAddres import static google.registry.config.RegistryConfig.getGSuiteOutgoingEmailDisplayName; import static google.registry.security.JsonHttpTestUtils.createJsonPayload; import static google.registry.testing.DatastoreHelper.loadRegistrar; +import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY; +import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_WRITE; import static org.mockito.Mockito.when; import com.google.appengine.api.users.User; @@ -58,12 +60,14 @@ public class RegistrarSettingsActionTestCase { static final String CLIENT_ID = "TheRegistrar"; - static final AuthResult USER_AUTHORIZED = - AuthResult.create(AuthLevel.USER, UserAuthInfo.create(new User("user", "gmail.com"), false)); + static final AuthResult USER_READ_WRITE = + AuthResult.create(AuthLevel.USER, UserAuthInfo.create(new User("rw", "gmail.com"), false)); + + static final AuthResult USER_READ_ONLY = + AuthResult.create(AuthLevel.USER, UserAuthInfo.create(new User("ro", "gmail.com"), false)); static final AuthResult USER_UNAUTHORIZED = - AuthResult.create( - AuthLevel.USER, UserAuthInfo.create(new User("unauthorized", "gmail.com"), false)); + AuthResult.create(AuthLevel.USER, UserAuthInfo.create(new User("evil", "gmail.com"), false)); @Rule public final AppEngineRule appEngine = @@ -90,7 +94,9 @@ public class RegistrarSettingsActionTestCase { action.sessionUtils = sessionUtils; action.appEngineServiceUtils = appEngineServiceUtils; when(appEngineServiceUtils.getCurrentVersionHostname("backend")).thenReturn("backend.hostname"); - action.authResult = USER_AUTHORIZED; + // We set the default user to one with read-write access, as that's the most common test case. + // When we want to specifically check read-only or unauthorized, we can switch the user here. + action.authResult = USER_READ_WRITE; action.jsonActionRunner = new JsonActionRunner( ImmutableMap.of(), new JsonResponse(new ResponseImpl(rsp))); action.registrarChangesNotificationEmailAddresses = ImmutableList.of( @@ -109,9 +115,19 @@ public class RegistrarSettingsActionTestCase { // the result is out of date after mutations. // (for example, if someone wants to change the registrar to prepare for a test, the function // would still return the old value) - when(sessionUtils.getRegistrarForUser(CLIENT_ID, USER_AUTHORIZED)) + when(sessionUtils.getRegistrarForUser(CLIENT_ID, READ_ONLY, USER_READ_WRITE)) .thenAnswer(x -> loadRegistrar(CLIENT_ID)); - when(sessionUtils.getRegistrarForUser(CLIENT_ID, USER_UNAUTHORIZED)) + when(sessionUtils.getRegistrarForUser(CLIENT_ID, READ_WRITE, USER_READ_WRITE)) + .thenAnswer(x -> loadRegistrar(CLIENT_ID)); + + when(sessionUtils.getRegistrarForUser(CLIENT_ID, READ_ONLY, USER_READ_ONLY)) + .thenAnswer(x -> loadRegistrar(CLIENT_ID)); + when(sessionUtils.getRegistrarForUser(CLIENT_ID, READ_WRITE, USER_READ_ONLY)) + .thenThrow(new ForbiddenException("forbidden test error")); + + when(sessionUtils.getRegistrarForUser(CLIENT_ID, READ_ONLY, USER_UNAUTHORIZED)) + .thenThrow(new ForbiddenException("forbidden test error")); + when(sessionUtils.getRegistrarForUser(CLIENT_ID, READ_WRITE, USER_UNAUTHORIZED)) .thenThrow(new ForbiddenException("forbidden test error")); } } diff --git a/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java b/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java index 22af3a5eb..86dce17ad 100644 --- a/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java +++ b/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java @@ -20,6 +20,8 @@ import static google.registry.testing.DatastoreHelper.loadRegistrar; import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.JUnitBackports.assertThrows; import static google.registry.testing.LogsSubject.assertAboutLogs; +import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY; +import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_WRITE; import static org.mockito.Mockito.mock; import com.google.appengine.api.users.User; @@ -32,6 +34,7 @@ import google.registry.request.auth.AuthResult; import google.registry.request.auth.UserAuthInfo; import google.registry.testing.AppEngineRule; import google.registry.testing.InjectRule; +import google.registry.ui.server.registrar.SessionUtils.AccessType; import java.util.logging.Level; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -96,66 +99,121 @@ public class SessionUtilsTest { /** Fail loading registrar if user doesn't have access to it. */ @Test - public void testGetRegistrarForUser_noAccess_isNotAdmin() { + public void testGetRegistrarForUser_readOnly_noAccess_isNotAdmin() { expectGetRegistrarFailure( DEFAULT_CLIENT_ID, + READ_ONLY, UNAUTHORIZED_USER, - "User {user} doesn't have access to registrar {clientId}"); + "User {user} doesn't have READ_ONLY access to registrar {clientId}"); + } + + /** Fail loading registrar if user doesn't have access to it. */ + @Test + public void testGetRegistrarForUser_readWrite_noAccess_isNotAdmin() { + expectGetRegistrarFailure( + DEFAULT_CLIENT_ID, + READ_WRITE, + UNAUTHORIZED_USER, + "User {user} doesn't have READ_WRITE access to registrar {clientId}"); } /** Fail loading registrar if there's no user associated with the request. */ @Test - public void testGetRegistrarForUser_noUser() { - expectGetRegistrarFailure(DEFAULT_CLIENT_ID, NO_USER, "Not logged in"); + public void testGetRegistrarForUser_readOnly_noUser() { + expectGetRegistrarFailure(DEFAULT_CLIENT_ID, READ_ONLY, NO_USER, "Not logged in"); } - /** Succeed loading registrar if user has access to it. */ + /** Fail loading registrar if there's no user associated with the request. */ @Test - public void testGetRegistrarForUser_hasAccess_isNotAdmin() { - expectGetRegistrarSuccess(AUTHORIZED_USER, "User {user} has access to registrar {clientId}"); + public void testGetRegistrarForUser_readWrite_noUser() { + expectGetRegistrarFailure(DEFAULT_CLIENT_ID, READ_WRITE, NO_USER, "Not logged in"); + + assertAboutLogs().that(testLogHandler).hasNoLogsAtLevel(Level.INFO); } - /** Succeed loading registrar if admin. */ + /** Succeed loading registrar in read-only mode if user has access to it. */ @Test - public void testGetRegistrarForUser_hasAccess_isAdmin() { - expectGetRegistrarSuccess(AUTHORIZED_ADMIN, "User {user} has access to registrar {clientId}"); + public void testGetRegistrarForUser_readOnly_hasAccess_isNotAdmin() { + expectGetRegistrarSuccess( + AUTHORIZED_USER, READ_ONLY, "User {user} has access to registrar {clientId}"); } - /** Fail loading registrar if admin isn't on the approved contacts list. */ + /** Succeed loading registrar in read-write mode if user has access to it. */ @Test - public void testGetRegistrarForUser_noAccess_isAdmin() { + public void testGetRegistrarForUser_readWrite_hasAccess_isNotAdmin() { + expectGetRegistrarSuccess( + AUTHORIZED_USER, READ_WRITE, "User {user} has access to registrar {clientId}"); + } + + /** Succeed loading registrar in read-only mode if admin with access. */ + @Test + public void testGetRegistrarForUser_readOnly_hasAccess_isAdmin() { + expectGetRegistrarSuccess( + AUTHORIZED_ADMIN, READ_ONLY, "User {user} has access to registrar {clientId}"); + } + + /** Succeed loading registrar in read-write mode if admin with access. */ + @Test + public void testGetRegistrarForUser_readWrite_hasAccess_isAdmin() { + expectGetRegistrarSuccess( + AUTHORIZED_ADMIN, READ_WRITE, "User {user} has access to registrar {clientId}"); + } + + /** Succeed loading registrar for read-only when admin isn't on the approved contacts list. */ + @Test + public void testGetRegistrarForUser_readOnly_noAccess_isAdmin() { + expectGetRegistrarSuccess( + UNAUTHORIZED_ADMIN, + READ_ONLY, + "Allowing admin {user} read-only access to registrar {clientId}."); + } + + /** Fail loading registrar for read-write when admin isn't on the approved contacts list. */ + @Test + public void testGetRegistrarForUser_readWrite_noAccess_isAdmin() { expectGetRegistrarFailure( DEFAULT_CLIENT_ID, + READ_WRITE, UNAUTHORIZED_ADMIN, - "User {user} doesn't have access to registrar {clientId}"); - } - - /** Succeed loading registrarAdmin even if unauthorized admin. */ - @Test - public void testGetRegistrarForUser_registrarAdminClientId() { - sessionUtils.registryAdminClientId = DEFAULT_CLIENT_ID; - expectGetRegistrarSuccess( - UNAUTHORIZED_ADMIN, "Allowing admin {user} access to registrar {clientId}."); + "User {user} doesn't have READ_WRITE access to registrar {clientId}"); } /** Fail loading registrar even if admin, if registrar doesn't exist. */ @Test - public void testGetRegistrarForUser_doesntExist_isAdmin() { - expectGetRegistrarFailure("BadClientId", UNAUTHORIZED_ADMIN, "Registrar {clientId} not found"); + public void testGetRegistrarForUser_readOnly_doesntExist_isAdmin() { + expectGetRegistrarFailure( + "BadClientId", + READ_ONLY, + AUTHORIZED_ADMIN, + "Registrar {clientId} not found"); } - private void expectGetRegistrarSuccess(AuthResult authResult, String message) { - assertThat(sessionUtils.getRegistrarForUser(DEFAULT_CLIENT_ID, authResult)).isNotNull(); + /** Fail loading registrar even if admin, if registrar doesn't exist. */ + @Test + public void testGetRegistrarForUser_readWrite_doesntExist_isAdmin() { + expectGetRegistrarFailure( + "BadClientId", + READ_WRITE, + AUTHORIZED_ADMIN, + "Registrar {clientId} not found"); + } + + private void expectGetRegistrarSuccess( + AuthResult authResult, AccessType accessType, String message) { + assertThat(sessionUtils.getRegistrarForUser(DEFAULT_CLIENT_ID, accessType, authResult)) + .isNotNull(); assertAboutLogs() .that(testLogHandler) .hasLogAtLevelWithMessage( Level.INFO, formatMessage(message, authResult, DEFAULT_CLIENT_ID)); } - private void expectGetRegistrarFailure(String clientId, AuthResult authResult, String message) { + private void expectGetRegistrarFailure( + String clientId, AccessType accessType, AuthResult authResult, String message) { ForbiddenException exception = assertThrows( - ForbiddenException.class, () -> sessionUtils.getRegistrarForUser(clientId, authResult)); + ForbiddenException.class, + () -> sessionUtils.getRegistrarForUser(clientId, accessType, authResult)); assertThat(exception).hasMessageThat().contains(formatMessage(message, authResult, clientId)); assertAboutLogs().that(testLogHandler).hasNoLogsAtLevel(Level.INFO); @@ -221,7 +279,7 @@ public class SessionUtilsTest { /** * If an admin is not associated with a registrar and the configured adminClientId points to a - * non-existent registrar, we still guess it (we will later failing loading the registrar). + * non-existent registrar, we still guess it (we will later fail loading the registrar). */ @Test public void testGuessClientIdForUser_noAccess_isAdmin_adminClientIdInvalid() { From ce5bbe4bfa76afc9398d8b3339764fcd65c08643 Mon Sep 17 00:00:00 2001 From: mcilwain Date: Thu, 20 Sep 2018 08:25:22 -0700 Subject: [PATCH 14/60] Add MOE equivalences for 2018-09-20 sync ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=213817394 --- javatests/google/registry/testing/sftp/BUILD | 2 ++ 1 file changed, 2 insertions(+) diff --git a/javatests/google/registry/testing/sftp/BUILD b/javatests/google/registry/testing/sftp/BUILD index 906daecf7..edc415924 100644 --- a/javatests/google/registry/testing/sftp/BUILD +++ b/javatests/google/registry/testing/sftp/BUILD @@ -17,6 +17,8 @@ java_library( "@junit", "@org_apache_ftpserver_core", "@org_apache_sshd_core", + "@org_apache_sshd_scp", + "@org_apache_sshd_sftp", "@org_bouncycastle_bcpkix_jdk15on", ], ) From 1586813398ee1951275bb098f4e1b4b56f819837 Mon Sep 17 00:00:00 2001 From: mcilwain Date: Wed, 3 Oct 2018 11:49:25 -0700 Subject: [PATCH 15/60] Bypass EAP fees for anchor tenants Note that the check flow does not yet handle any kind of allocation token handling at all. Step 2 will be to add allocation token handling there, so a RESERVED_FOR_ANCHOR_TENANT or RESERVED_FOR_SPECIFIC_USE domain will show as available instead of reserved if the right token is specified using the extension. Then once that's done, we can use that information to adjust the price accordingly as well. Right now the behavior with a domain check is that reserved domains always show as reserved, even if they're anchor tenants. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=215599350 --- .../flows/domain/DomainAllocateFlow.java | 3 +- .../domain/DomainApplicationCreateFlow.java | 7 +- .../flows/domain/DomainCreateFlow.java | 3 +- .../flows/domain/DomainFlowUtils.java | 5 +- .../flows/domain/DomainPricingLogic.java | 6 +- .../flows/domain/DomainCreateFlowTest.java | 167 ++++-------------- 6 files changed, 55 insertions(+), 136 deletions(-) diff --git a/java/google/registry/flows/domain/DomainAllocateFlow.java b/java/google/registry/flows/domain/DomainAllocateFlow.java index c0e5a9b6e..76fb423c0 100644 --- a/java/google/registry/flows/domain/DomainAllocateFlow.java +++ b/java/google/registry/flows/domain/DomainAllocateFlow.java @@ -391,7 +391,8 @@ public class DomainAllocateFlow implements TransactionalFlow { private ImmutableList createResponseExtensions( DateTime now, Registry registry, int years) throws EppException { - FeesAndCredits feesAndCredits = pricingLogic.getCreatePrice(registry, targetId, now, years); + FeesAndCredits feesAndCredits = + pricingLogic.getCreatePrice(registry, targetId, now, years, false); Optional feeCreate = eppInput.getSingleExtension(FeeCreateCommandExtension.class); return feeCreate.isPresent() diff --git a/java/google/registry/flows/domain/DomainApplicationCreateFlow.java b/java/google/registry/flows/domain/DomainApplicationCreateFlow.java index 233ae2f2a..7cb9a8bf0 100644 --- a/java/google/registry/flows/domain/DomainApplicationCreateFlow.java +++ b/java/google/registry/flows/domain/DomainApplicationCreateFlow.java @@ -211,8 +211,11 @@ public final class DomainApplicationCreateFlow implements TransactionalFlow { checkAllowedAccessToTld(clientId, tld); } Registry registry = Registry.get(tld); + boolean isAnchorTenant = + isAnchorTenant(domainName, Optional.empty(), authInfo.getPw().getValue(), Optional.empty()); FeesAndCredits feesAndCredits = - pricingLogic.getCreatePrice(registry, targetId, now, command.getPeriod().getValue()); + pricingLogic.getCreatePrice( + registry, targetId, now, command.getPeriod().getValue(), isAnchorTenant); verifyUnitIsYears(command.getPeriod()); int years = command.getPeriod().getValue(); validateRegistrationPeriod(years); @@ -220,8 +223,6 @@ public final class DomainApplicationCreateFlow implements TransactionalFlow { LaunchCreateExtension launchCreate = eppInput.getSingleExtension(LaunchCreateExtension.class).get(); validateLaunchCreateExtension(launchCreate, registry, domainName, now); - boolean isAnchorTenant = - isAnchorTenant(domainName, Optional.empty(), authInfo.getPw().getValue(), Optional.empty()); // Superusers can create reserved domains, force creations on domains that require a claims // notice without specifying a claims key, and override blocks on registering premium domains. if (!isSuperuser) { diff --git a/java/google/registry/flows/domain/DomainCreateFlow.java b/java/google/registry/flows/domain/DomainCreateFlow.java index 71e16f879..894d2421f 100644 --- a/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/java/google/registry/flows/domain/DomainCreateFlow.java @@ -308,7 +308,8 @@ public class DomainCreateFlow implements TransactionalFlow { .build()); Optional feeCreate = eppInput.getSingleExtension(FeeCreateCommandExtension.class); - FeesAndCredits feesAndCredits = pricingLogic.getCreatePrice(registry, targetId, now, years); + FeesAndCredits feesAndCredits = + pricingLogic.getCreatePrice(registry, targetId, now, years, isAnchorTenant); validateFeeChallenge(targetId, registry.getTldStr(), clientId, now, feeCreate, feesAndCredits); Optional secDnsCreate = validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class)); diff --git a/java/google/registry/flows/domain/DomainFlowUtils.java b/java/google/registry/flows/domain/DomainFlowUtils.java index c03f1d81b..85843db84 100644 --- a/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/java/google/registry/flows/domain/DomainFlowUtils.java @@ -621,7 +621,10 @@ public class DomainFlowUtils { builder.setReasonIfSupported("reserved"); } else { builder.setAvailIfSupported(true); - fees = pricingLogic.getCreatePrice(registry, domainNameString, now, years).getFees(); + // TODO(b/117145844): Once allocation token support for domain check flow is implemented, + // we should be able to calculate the correct price here. + fees = + pricingLogic.getCreatePrice(registry, domainNameString, now, years, false).getFees(); } break; case RENEW: diff --git a/java/google/registry/flows/domain/DomainPricingLogic.java b/java/google/registry/flows/domain/DomainPricingLogic.java index 4b4620ad0..22e900756 100644 --- a/java/google/registry/flows/domain/DomainPricingLogic.java +++ b/java/google/registry/flows/domain/DomainPricingLogic.java @@ -54,7 +54,8 @@ public final class DomainPricingLogic { /** Returns a new create price for the pricer. */ public FeesAndCredits getCreatePrice( - Registry registry, String domainName, DateTime date, int years) throws EppException { + Registry registry, String domainName, DateTime date, int years, boolean isAnchorTenant) + throws EppException { CurrencyUnit currency = registry.getCurrency(); // Get the vanilla create cost. @@ -65,7 +66,8 @@ public final class DomainPricingLogic { Fee eapFee = registry.getEapFeeFor(date); FeesAndCredits.Builder feesBuilder = new FeesAndCredits.Builder().setCurrency(currency).addFeeOrCredit(createFeeOrCredit); - if (!eapFee.hasZeroCost()) { + // Don't charge anchor tenants EAP fees. + if (!isAnchorTenant && !eapFee.hasZeroCost()) { feesBuilder.addFeeOrCredit(eapFee); } diff --git a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java index 33f417273..1698920c9 100644 --- a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java @@ -270,7 +270,8 @@ public class DomainCreateFlowTest extends ResourceFlowTestCase().add(createBillingEvent).add(renewBillingEvent); // If EAP is applied, a billing event for EAP should be present. - if (!eapFee.isZero()) { + // EAP fees are bypassed for anchor tenant domains. + if (!isAnchorTenant && !eapFee.isZero()) { BillingEvent.OneTime eapBillingEvent = new BillingEvent.OneTime.Builder() .setReason(Reason.FEE_EARLY_ACCESS) @@ -1012,6 +1013,22 @@ public class DomainCreateFlowTest extends ResourceFlowTestCase Date: Wed, 3 Oct 2018 12:12:21 -0700 Subject: [PATCH 16/60] Update bazel version to 0.17.2 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=215603517 --- docs/install.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/install.md b/docs/install.md index 5d2ac22d3..36b46c09c 100644 --- a/docs/install.md +++ b/docs/install.md @@ -7,8 +7,8 @@ This document covers the steps necessary to download, build, and deploy Nomulus. You will need the following programs installed on your local machine: * A recent version of the [Java 8 JDK][java-jdk8]. -* [Bazel build system](http://bazel.io/) (version [0.17.1][bazel-version] - works as of 2018-09-14). +* [Bazel build system](http://bazel.io/) (version [0.17.2][bazel-version] + works as of 2018-10-03). * [Google App Engine SDK for Java][app-engine-sdk], and configure aliases to to the `gcloud` and `appcfg.sh` utilities (you'll use them a lot). * [Git](https://git-scm.com/) version control system. @@ -181,4 +181,4 @@ See the [first steps tutorial](./first-steps-tutorial.md) for more information. [app-engine-sdk]: https://cloud.google.com/appengine/docs/java/download [java-jdk8]: http://www.oracle.com/technetwork/java/javase/downloads -[bazel-version]: https://github.com/bazelbuild/bazel/releases/download/0.17.1/bazel-0.17.1-installer-linux-x86_64.sh +[bazel-version]: https://github.com/bazelbuild/bazel/releases/download/0.17.2/bazel-0.17.2-installer-linux-x86_64.sh From e3a35f0aa0ceb822c76c63758c76c595f04f2e79 Mon Sep 17 00:00:00 2001 From: jianglai Date: Wed, 3 Oct 2018 12:57:33 -0700 Subject: [PATCH 17/60] Do not include transactions with zero unit price in invoice ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=215611195 --- java/google/registry/beam/invoicing/InvoicingPipeline.java | 2 ++ .../google/registry/beam/invoicing/InvoicingPipelineTest.java | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/java/google/registry/beam/invoicing/InvoicingPipeline.java b/java/google/registry/beam/invoicing/InvoicingPipeline.java index 69c8c1f0f..6e3bfe632 100644 --- a/java/google/registry/beam/invoicing/InvoicingPipeline.java +++ b/java/google/registry/beam/invoicing/InvoicingPipeline.java @@ -34,6 +34,7 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.ValueProvider; import org.apache.beam.sdk.options.ValueProvider.NestedValueProvider; import org.apache.beam.sdk.transforms.Count; +import org.apache.beam.sdk.transforms.Filter; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.values.KV; @@ -141,6 +142,7 @@ public class InvoicingPipeline implements Serializable { "Map to invoicing key", MapElements.into(TypeDescriptor.of(InvoiceGroupingKey.class)) .via(BillingEvent::getInvoiceGroupingKey)) + .apply(Filter.by((InvoiceGroupingKey key) -> key.unitPrice() != 0)) .setCoder(new InvoiceGroupingKeyCoder()) .apply("Count occurrences", Count.perElement()) .apply( diff --git a/javatests/google/registry/beam/invoicing/InvoicingPipelineTest.java b/javatests/google/registry/beam/invoicing/InvoicingPipelineTest.java index 3e6ead99f..538dae227 100644 --- a/javatests/google/registry/beam/invoicing/InvoicingPipelineTest.java +++ b/javatests/google/registry/beam/invoicing/InvoicingPipelineTest.java @@ -177,9 +177,7 @@ public class InvoicingPipelineTest { "2017-10-01,2022-09-30,234,70.75,JPY,10125,1,PURCHASE,theRegistrar - hello,1," + "CREATE | TLD: hello | TERM: 5-year,70.75,JPY,", "2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,bestdomains - test,1," - + "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688", - "2017-10-01,2018-09-30,789,0.00,USD,10125,1,PURCHASE,anotherRegistrar - test,1," - + "CREATE | TLD: test | TERM: 1-year,0.00,USD,"); + + "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688"); } @Test From ac05ccb95e823193556bc176e8c804e4a52cd4de Mon Sep 17 00:00:00 2001 From: Ben McIlwain Date: Thu, 4 Oct 2018 11:10:30 -0700 Subject: [PATCH 18/60] [CL 2 of 3] Update apache mina library from v1_6_0 to v2_1_0 Adapts all code for backwards incompatible API changes in one go and sets global default version to v2_1_0 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=215769488 --- javatests/google/registry/testing/sftp/TestSftpServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javatests/google/registry/testing/sftp/TestSftpServer.java b/javatests/google/registry/testing/sftp/TestSftpServer.java index 042d0967e..e7240f91b 100644 --- a/javatests/google/registry/testing/sftp/TestSftpServer.java +++ b/javatests/google/registry/testing/sftp/TestSftpServer.java @@ -31,11 +31,11 @@ import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.random.SingletonRandomFactory; -import org.apache.sshd.server.Command; import org.apache.sshd.server.ServerBuilder; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.password.PasswordAuthenticator; import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; +import org.apache.sshd.server.command.Command; import org.apache.sshd.server.scp.ScpCommandFactory; import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; From 68bd502ba767e8b0cea1cf072b4d1fa397f06773 Mon Sep 17 00:00:00 2001 From: jianglai Date: Thu, 4 Oct 2018 12:31:41 -0700 Subject: [PATCH 19/60] Update apache sshd version ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=215783691 --- java/google/registry/repositories.bzl | 50 ++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/java/google/registry/repositories.bzl b/java/google/registry/repositories.bzl index 700e5258a..d05fb830a 100644 --- a/java/google/registry/repositories.bzl +++ b/java/google/registry/repositories.bzl @@ -139,6 +139,8 @@ def domain_registry_repositories( omit_org_apache_httpcomponents_httpcore = False, omit_org_apache_mina_core = False, omit_org_apache_sshd_core = False, + omit_org_apache_sshd_scp = False, + omit_org_apache_sshd_sftp = False, omit_org_apache_tomcat_servlet_api = False, omit_org_apache_tomcat_annotations_api = False, omit_org_bouncycastle_bcpg_jdk15on = False, @@ -397,6 +399,10 @@ def domain_registry_repositories( org_apache_mina_core() if not omit_org_apache_sshd_core: org_apache_sshd_core() + if not omit_org_apache_sshd_scp: + org_apache_sshd_scp() + if not omit_org_apache_sshd_sftp: + org_apache_sshd_sftp() if not omit_org_apache_tomcat_servlet_api: org_apache_tomcat_servlet_api() if not omit_org_apache_tomcat_annotations_api: @@ -2244,15 +2250,51 @@ def org_apache_mina_core(): def org_apache_sshd_core(): java_import_external( name = "org_apache_sshd_core", - jar_sha256 = "5630fa11f7e2f7f5b6b7e6b9be06e476715dfb48db37998b4b7c3eea098d86ff", + # Apache 2.0 License + # http://www.apache.org/licenses/LICENSE-2.0 + # Apache License, Version 2.0 + # http://www.apache.org/licenses/LICENSE-2.0.txt + licenses = ["notice"], + jar_sha256 = "00c944fac00dec2e7ace4052e0a52c772ca3fa2653918bbcfadf7100df022e25", jar_urls = [ - "http://maven.ibiblio.org/maven2/org/apache/sshd/sshd-core/1.2.0/sshd-core-1.2.0.jar", - "http://repo1.maven.org/maven2/org/apache/sshd/sshd-core/1.2.0/sshd-core-1.2.0.jar", + "http://repo1.maven.org/maven2/org/apache/sshd/sshd-core/2.0.0/sshd-core-2.0.0.jar", + "http://maven.ibiblio.org/maven2/org/apache/sshd/sshd-core/2.0.0/sshd-core-2.0.0.jar", ], - licenses = ["notice"], # Apache 2.0 License deps = ["@org_slf4j_api"], ) +def org_apache_sshd_scp(): + java_import_external( + name = "org_apache_sshd_scp", + # Apache 2.0 License + # http://www.apache.org/licenses/LICENSE-2.0 + # Apache License, Version 2.0 + # http://www.apache.org/licenses/LICENSE-2.0.txt + licenses = ["notice"], + jar_sha256 = "ae32fcc16ab0a0ae04655b8832676b41199814184dc50028b3c6aa61053635ca", + jar_urls = [ + "http://repo1.maven.org/maven2/org/apache/sshd/sshd-scp/2.0.0/sshd-scp-2.0.0.jar", + "http://maven.ibiblio.org/maven2/org/apache/sshd/sshd-scp/2.0.0/sshd-scp-2.0.0.jar", + ], + deps = ["@org_apache_sshd_core"], + ) + +def org_apache_sshd_sftp(): + java_import_external( + name = "org_apache_sshd_sftp", + # Apache 2.0 License + # http://www.apache.org/licenses/LICENSE-2.0 + # Apache License, Version 2.0 + # http://www.apache.org/licenses/LICENSE-2.0.txt + licenses = ["notice"], + jar_sha256 = "0504af9a4afcaf61be9f0b56d3cfc76a9187a654e297bc57b7fa81aa76bb8cb0", + jar_urls = [ + "http://repo1.maven.org/maven2/org/apache/sshd/sshd-sftp/2.0.0/sshd-sftp-2.0.0.jar", + "http://maven.ibiblio.org/maven2/org/apache/sshd/sshd-sftp/2.0.0/sshd-sftp-2.0.0.jar", + ], + deps = ["@org_apache_sshd_core"], + ) + def org_apache_tomcat_servlet_api(): java_import_external( name = "org_apache_tomcat_servlet_api", From 7b9d562043fc5c580f7067aa6b6c3f22068c5a6d Mon Sep 17 00:00:00 2001 From: jianglai Date: Thu, 4 Oct 2018 14:40:48 -0700 Subject: [PATCH 20/60] Explicitly set terraform version in preparation for the incoming 1.13.0 update ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=215806094 --- java/google/registry/proxy/terraform/modules/common.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/java/google/registry/proxy/terraform/modules/common.tf b/java/google/registry/proxy/terraform/modules/common.tf index 10e1a70af..836f1fcaf 100644 --- a/java/google/registry/proxy/terraform/modules/common.tf +++ b/java/google/registry/proxy/terraform/modules/common.tf @@ -1,3 +1,4 @@ provider "google" { + version = "<= 1.12.0" project = "${var.proxy_project_name}" } From 218c4517ebf05fae6fe74bf9b011fbc14754f435 Mon Sep 17 00:00:00 2001 From: mcilwain Date: Fri, 5 Oct 2018 07:51:02 -0700 Subject: [PATCH 21/60] Stop exporting EPP flow metrics to BigQuery These are simply too costly in their current form now that we are handling double-digit QPS, so at a minimum we'd want to refactor these for batched exports using a background thread (like how Stackdriver metrics work). However, upon further review, that work isn't worth doing if this BigQuery table isn't actually being used for anything, and it seems that we aren't using it anymore given that ICANN transaction reporting no longer requires it. So the simplest thing to do is simply to get rid of this entirely, and just use a combination of Stackdriver metrics and App Engine logs. The eppMetrics BigQuery table is ~1.2 billion rows and takes up 223 GB, so that's not an insignificant GCP billings saving if we can delete it. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=215905466 --- .../env/common/default/WEB-INF/queue.xml | 11 -- java/google/registry/flows/EppController.java | 6 - java/google/registry/flows/FlowRunner.java | 2 - .../backend/BackendRequestComponent.java | 2 - .../monitoring/whitebox/BigQueryMetric.java | 36 ------ .../whitebox/BigQueryMetricsEnqueuer.java | 65 ---------- .../monitoring/whitebox/EppMetric.java | 96 +-------------- .../whitebox/MetricsExportAction.java | 104 ---------------- .../monitoring/whitebox/WhiteboxModule.java | 51 +------- .../registry/flows/EppCommitLogsTest.java | 2 +- .../registry/flows/EppControllerTest.java | 58 +-------- .../flows/EppLifecycleContactTest.java | 6 - .../flows/EppLifecycleDomainTest.java | 8 -- .../registry/flows/EppLifecycleHostTest.java | 6 - .../google/registry/flows/EppTestCase.java | 2 +- .../registry/flows/EppTestComponent.java | 10 +- .../google/registry/flows/FlowRunnerTest.java | 16 +-- .../google/registry/flows/FlowTestCase.java | 2 +- .../flows/domain/DomainCreateFlowTest.java | 1 - .../backend/testdata/backend_routing.txt | 1 - .../whitebox/BigQueryMetricsEnqueuerTest.java | 111 ------------------ .../monitoring/whitebox/EppMetricTest.java | 72 +----------- .../whitebox/MetricsExportActionTest.java | 108 ----------------- .../registry/testing/EppMetricSubject.java | 4 - 24 files changed, 17 insertions(+), 763 deletions(-) delete mode 100644 java/google/registry/monitoring/whitebox/BigQueryMetric.java delete mode 100644 java/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuer.java delete mode 100644 java/google/registry/monitoring/whitebox/MetricsExportAction.java delete mode 100644 javatests/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuerTest.java delete mode 100644 javatests/google/registry/monitoring/whitebox/MetricsExportActionTest.java diff --git a/java/google/registry/env/common/default/WEB-INF/queue.xml b/java/google/registry/env/common/default/WEB-INF/queue.xml index 3e14770b7..7b2cf9afb 100644 --- a/java/google/registry/env/common/default/WEB-INF/queue.xml +++ b/java/google/registry/env/common/default/WEB-INF/queue.xml @@ -192,17 +192,6 @@ - - - bigquery-streaming-metrics - 500/s - 500 - - 1 - 1m - - - retryable-cron-tasks diff --git a/java/google/registry/flows/EppController.java b/java/google/registry/flows/EppController.java index a98517a9f..7a43e3ff8 100644 --- a/java/google/registry/flows/EppController.java +++ b/java/google/registry/flows/EppController.java @@ -22,7 +22,6 @@ import static google.registry.flows.FlowReporter.extractTlds; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.flogger.FluentLogger; @@ -33,7 +32,6 @@ import google.registry.model.eppoutput.EppOutput; import google.registry.model.eppoutput.EppResponse; import google.registry.model.eppoutput.Result; import google.registry.model.eppoutput.Result.Code; -import google.registry.monitoring.whitebox.BigQueryMetricsEnqueuer; import google.registry.monitoring.whitebox.EppMetric; import java.util.Optional; import javax.inject.Inject; @@ -52,7 +50,6 @@ public final class EppController { @Inject FlowComponent.Builder flowComponentBuilder; @Inject EppMetric.Builder eppMetricBuilder; @Inject EppMetrics eppMetrics; - @Inject BigQueryMetricsEnqueuer bigQueryMetricsEnqueuer; @Inject ServerTridProvider serverTridProvider; @Inject EppController() {} @@ -65,7 +62,6 @@ public final class EppController { boolean isSuperuser, byte[] inputXmlBytes) { eppMetricBuilder.setClientId(Optional.ofNullable(sessionMetadata.getClientId())); - eppMetricBuilder.setPrivilegeLevel(isSuperuser ? "SUPERUSER" : "NORMAL"); try { EppInput eppInput; try { @@ -99,7 +95,6 @@ public final class EppController { e.getResult(), Trid.create(null, serverTridProvider.createServerTrid())); } if (!eppInput.getTargetIds().isEmpty()) { - eppMetricBuilder.setEppTarget(Joiner.on(',').join(eppInput.getTargetIds())); if (eppInput.isDomainResourceType()) { eppMetricBuilder.setTlds(extractTlds(eppInput.getTargetIds())); } @@ -122,7 +117,6 @@ public final class EppController { } finally { if (!isDryRun) { EppMetric metric = eppMetricBuilder.build(); - bigQueryMetricsEnqueuer.export(metric); eppMetrics.incrementEppRequests(metric); eppMetrics.recordProcessingTime(metric); } diff --git a/java/google/registry/flows/FlowRunner.java b/java/google/registry/flows/FlowRunner.java index b830e4e23..8df7f0083 100644 --- a/java/google/registry/flows/FlowRunner.java +++ b/java/google/registry/flows/FlowRunner.java @@ -72,7 +72,6 @@ public class FlowRunner { } eppMetricBuilder.setCommandNameFromFlow(flowClass.getSimpleName()); if (!isTransactional) { - eppMetricBuilder.incrementAttempts(); EppOutput eppOutput = EppOutput.create(flowProvider.get().run()); if (flowClass.equals(LoginFlow.class)) { // In LoginFlow, clientId isn't known until after the flow executes, so save it then. @@ -84,7 +83,6 @@ public class FlowRunner { return ofy() .transact( () -> { - eppMetricBuilder.incrementAttempts(); try { EppOutput output = EppOutput.create(flowProvider.get().run()); if (isDryRun) { diff --git a/java/google/registry/module/backend/BackendRequestComponent.java b/java/google/registry/module/backend/BackendRequestComponent.java index 0b5df3532..09c7fa8ab 100644 --- a/java/google/registry/module/backend/BackendRequestComponent.java +++ b/java/google/registry/module/backend/BackendRequestComponent.java @@ -53,7 +53,6 @@ import google.registry.export.sheet.SheetModule; import google.registry.export.sheet.SyncRegistrarsSheetAction; import google.registry.flows.async.AsyncFlowsModule; import google.registry.mapreduce.MapreduceModule; -import google.registry.monitoring.whitebox.MetricsExportAction; import google.registry.monitoring.whitebox.WhiteboxModule; import google.registry.rde.BrdaCopyAction; import google.registry.rde.RdeModule; @@ -136,7 +135,6 @@ interface BackendRequestComponent { IcannReportingStagingAction icannReportingStagingAction(); IcannReportingUploadAction icannReportingUploadAction(); LoadSnapshotAction loadSnapshotAction(); - MetricsExportAction metricsExportAction(); NordnUploadAction nordnUploadAction(); NordnVerifyAction nordnVerifyAction(); PublishDnsUpdatesAction publishDnsUpdatesAction(); diff --git a/java/google/registry/monitoring/whitebox/BigQueryMetric.java b/java/google/registry/monitoring/whitebox/BigQueryMetric.java deleted file mode 100644 index 70c9373ee..000000000 --- a/java/google/registry/monitoring/whitebox/BigQueryMetric.java +++ /dev/null @@ -1,36 +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.monitoring.whitebox; - -import com.google.api.services.bigquery.model.TableFieldSchema; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - -/** - * A metric which can be encoded into a BigQuery row. - * - * @see BigQueryMetricsEnqueuer - */ -public interface BigQueryMetric { - - /** Get the BigQuery table name for this metric. */ - String getTableId(); - - /** Get the schema description for the BigQuery table. */ - ImmutableList getSchemaFields(); - - /** Get a map of the row values for this metric instance. */ - ImmutableMap getBigQueryRowEncoding(); -} diff --git a/java/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuer.java b/java/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuer.java deleted file mode 100644 index 54905098c..000000000 --- a/java/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuer.java +++ /dev/null @@ -1,65 +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.monitoring.whitebox; - -import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl; - -import com.google.appengine.api.taskqueue.Queue; -import com.google.appengine.api.taskqueue.TaskOptions; -import com.google.appengine.api.taskqueue.TransientFailureException; -import com.google.common.base.Supplier; -import com.google.common.flogger.FluentLogger; -import google.registry.util.AppEngineServiceUtils; -import java.util.Map.Entry; -import javax.inject.Inject; -import javax.inject.Named; - -/** - * A collector of metric information. Enqueues collected metrics to a task queue to be written to - * BigQuery asynchronously. - * - * @see MetricsExportAction - */ -public class BigQueryMetricsEnqueuer { - - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - - public static final String QUEUE_BIGQUERY_STREAMING_METRICS = "bigquery-streaming-metrics"; - - @Inject AppEngineServiceUtils appEngineServiceUtils; - @Inject @Named("insertIdGenerator") Supplier idGenerator; - @Inject @Named(QUEUE_BIGQUERY_STREAMING_METRICS) Queue queue; - - @Inject BigQueryMetricsEnqueuer() {} - - public void export(BigQueryMetric metric) { - try { - String hostname = appEngineServiceUtils.getCurrentVersionHostname("backend"); - TaskOptions opts = - withUrl(MetricsExportAction.PATH) - .header("Host", hostname) - .param("insertId", idGenerator.get()); - for (Entry entry : metric.getBigQueryRowEncoding().entrySet()) { - opts.param(entry.getKey(), entry.getValue()); - } - opts.param("tableId", metric.getTableId()); - queue.add(opts); - } catch (TransientFailureException e) { - // Log and swallow. We may drop some metrics here but this should be rare. - logger.atInfo().withCause(e).log( - "Transient error occurred while recording metric; metric dropped."); - } - } -} diff --git a/java/google/registry/monitoring/whitebox/EppMetric.java b/java/google/registry/monitoring/whitebox/EppMetric.java index 26e298a42..d328e1d56 100644 --- a/java/google/registry/monitoring/whitebox/EppMetric.java +++ b/java/google/registry/monitoring/whitebox/EppMetric.java @@ -15,44 +15,19 @@ package google.registry.monitoring.whitebox; import static com.google.common.base.Preconditions.checkArgument; -import static google.registry.bigquery.BigqueryUtils.toBigqueryTimestamp; -import com.google.api.services.bigquery.model.TableFieldSchema; import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import google.registry.bigquery.BigqueryUtils.FieldType; import google.registry.model.eppoutput.Result.Code; import google.registry.model.registry.Registries; import google.registry.util.Clock; import java.util.Optional; import org.joda.time.DateTime; -/** - * A value class for recording attributes of an EPP metric. - * - * @see BigQueryMetricsEnqueuer - */ +/** A value class for recording attributes of an EPP metric. */ @AutoValue -public abstract class EppMetric implements BigQueryMetric { - - static final String TABLE_ID = "eppMetrics"; - static final ImmutableList SCHEMA_FIELDS = - ImmutableList.of( - new TableFieldSchema().setName("requestId").setType(FieldType.STRING.name()), - new TableFieldSchema().setName("startTime").setType(FieldType.TIMESTAMP.name()), - new TableFieldSchema().setName("endTime").setType(FieldType.TIMESTAMP.name()), - new TableFieldSchema().setName("commandName").setType(FieldType.STRING.name()), - new TableFieldSchema().setName("clientId").setType(FieldType.STRING.name()), - new TableFieldSchema().setName("tld").setType(FieldType.STRING.name()), - new TableFieldSchema().setName("privilegeLevel").setType(FieldType.STRING.name()), - new TableFieldSchema().setName("eppTarget").setType(FieldType.STRING.name()), - new TableFieldSchema().setName("eppStatus").setType(FieldType.INTEGER.name()), - new TableFieldSchema().setName("attempts").setType(FieldType.INTEGER.name())); - - public abstract String getRequestId(); +public abstract class EppMetric { public abstract DateTime getStartTimestamp(); @@ -64,55 +39,8 @@ public abstract class EppMetric implements BigQueryMetric { public abstract Optional getTld(); - public abstract Optional getPrivilegeLevel(); - - public abstract Optional getEppTarget(); - public abstract Optional getStatus(); - public abstract Integer getAttempts(); - - @Override - public String getTableId() { - return TABLE_ID; - } - - @Override - public ImmutableList getSchemaFields() { - return SCHEMA_FIELDS; - } - - @Override - public ImmutableMap getBigQueryRowEncoding() { - // Create map builder, start with required values - ImmutableMap.Builder map = - new ImmutableMap.Builder() - .put("requestId", getRequestId()) - .put("startTime", toBigqueryTimestamp(getStartTimestamp())) - .put("endTime", toBigqueryTimestamp(getEndTimestamp())) - .put("attempts", getAttempts().toString()); - // Populate optional values, if present - addOptional("commandName", getCommandName(), map); - addOptional("clientId", getClientId(), map); - addOptional("tld", getTld(), map); - addOptional("privilegeLevel", getPrivilegeLevel(), map); - addOptional("eppTarget", getEppTarget(), map); - if (getStatus().isPresent()) { - map.put("eppStatus", Integer.toString(getStatus().get().code)); - } - - return map.build(); - } - - /** - * Helper method to populate an {@link com.google.common.collect.ImmutableMap.Builder} with an - * {@link Optional} value if the value is {@link Optional#isPresent()}. - */ - private static void addOptional( - String key, Optional value, ImmutableMap.Builder map) { - value.ifPresent(t -> map.put(key, t.toString())); - } - /** Create an {@link EppMetric.Builder}. */ public static Builder builder() { return new AutoValue_EppMetric.Builder(); @@ -124,9 +52,8 @@ public abstract class EppMetric implements BigQueryMetric { * *

The start timestamp is recorded now, and the end timestamp at {@code build()}. */ - public static Builder builderForRequest(String requestId, Clock clock) { + public static Builder builderForRequest(Clock clock) { return builder() - .setRequestId(requestId) .setStartTimestamp(clock.nowUtc()) .setClock(clock); } @@ -135,14 +62,9 @@ public abstract class EppMetric implements BigQueryMetric { @AutoValue.Builder public abstract static class Builder { - /** Builder-only counter of the number of attempts, to support {@link #incrementAttempts()}. */ - private int attempts = 0; - /** Builder-only clock to support automatic recording of endTimestamp on {@link #build()}. */ private Clock clock = null; - abstract Builder setRequestId(String requestId); - abstract Builder setStartTimestamp(DateTime startTimestamp); abstract Builder setEndTimestamp(DateTime endTimestamp); @@ -191,19 +113,8 @@ public abstract class EppMetric implements BigQueryMetric { return this; } - public abstract Builder setPrivilegeLevel(String privilegeLevel); - - public abstract Builder setEppTarget(String eppTarget); - public abstract Builder setStatus(Code code); - abstract Builder setAttempts(Integer attempts); - - public Builder incrementAttempts() { - attempts++; - return this; - } - Builder setClock(Clock clock) { this.clock = clock; return this; @@ -216,7 +127,6 @@ public abstract class EppMetric implements BigQueryMetric { * current timestamp of the clock; otherwise end timestamp must have been previously set. */ public EppMetric build() { - setAttempts(attempts); if (clock != null) { setEndTimestamp(clock.nowUtc()); } diff --git a/java/google/registry/monitoring/whitebox/MetricsExportAction.java b/java/google/registry/monitoring/whitebox/MetricsExportAction.java deleted file mode 100644 index f5a802ff1..000000000 --- a/java/google/registry/monitoring/whitebox/MetricsExportAction.java +++ /dev/null @@ -1,104 +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.monitoring.whitebox; - -import static com.google.common.base.Predicates.in; -import static com.google.common.base.Predicates.not; -import static com.google.common.collect.Multimaps.filterKeys; -import static google.registry.request.Action.Method.POST; -import static java.util.stream.Collectors.joining; - -import com.google.api.services.bigquery.Bigquery; -import com.google.api.services.bigquery.model.TableDataInsertAllRequest; -import com.google.api.services.bigquery.model.TableDataInsertAllResponse; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.flogger.FluentLogger; -import google.registry.bigquery.CheckedBigquery; -import google.registry.config.RegistryConfig.Config; -import google.registry.request.Action; -import google.registry.request.Parameter; -import google.registry.request.ParameterMap; -import google.registry.request.auth.Auth; -import java.io.IOException; -import java.util.Map; -import javax.inject.Inject; - -/** Action for exporting metrics to BigQuery. */ -@Action( - path = MetricsExportAction.PATH, - method = POST, - auth = Auth.AUTH_INTERNAL_ONLY -) -public class MetricsExportAction implements Runnable { - - public static final String PATH = "/_dr/task/metrics"; - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private static final String DATASET_ID = "metrics"; - private static final ImmutableSet SPECIAL_PARAMS = ImmutableSet.of("tableId", "insertId"); - - @Inject @Parameter("tableId") String tableId; - @Inject @Parameter("insertId") String insertId; - @Inject @Config("projectId") String projectId; - - @Inject CheckedBigquery checkedBigquery; - @Inject @ParameterMap ImmutableListMultimap parameters; - @Inject MetricsExportAction() {} - - /** Exports metrics to BigQuery. */ - @Override - public void run() { - try { - Bigquery bigquery = - checkedBigquery.ensureDataSetAndTableExist(projectId, DATASET_ID, tableId); - // Filter out the special parameters that the Action is called with. Everything that's left - // is returned in a Map that is suitable to pass to Bigquery as row data. - Map jsonRows = - ImmutableMap.copyOf( - filterKeys(parameters, not(in(SPECIAL_PARAMS))).entries()); - TableDataInsertAllResponse response = bigquery.tabledata() - .insertAll( - projectId, - DATASET_ID, - tableId, - new TableDataInsertAllRequest() - .setRows( - ImmutableList.of(new TableDataInsertAllRequest.Rows() - .setInsertId(insertId) - .setJson(jsonRows)))) - .execute(); - - if (response.getInsertErrors() != null && !response.getInsertErrors().isEmpty()) { - throw new RuntimeException( - response - .getInsertErrors() - .stream() - .map( - error -> { - try { - return error.toPrettyString(); - } catch (IOException e) { - return error.toString(); - } - }) - .collect(joining("\n"))); - } - } catch (Throwable e) { - logger.atWarning().withCause(e).log("Unknown error while exporting metrics to BigQuery."); - } - } -} diff --git a/java/google/registry/monitoring/whitebox/WhiteboxModule.java b/java/google/registry/monitoring/whitebox/WhiteboxModule.java index 2203964b4..1c67aa483 100644 --- a/java/google/registry/monitoring/whitebox/WhiteboxModule.java +++ b/java/google/registry/monitoring/whitebox/WhiteboxModule.java @@ -14,24 +14,9 @@ package google.registry.monitoring.whitebox; -import static com.google.appengine.api.taskqueue.QueueFactory.getQueue; -import static google.registry.monitoring.whitebox.BigQueryMetricsEnqueuer.QUEUE_BIGQUERY_STREAMING_METRICS; -import static google.registry.request.RequestParameters.extractRequiredParameter; - -import com.google.api.services.bigquery.model.TableFieldSchema; -import com.google.appengine.api.taskqueue.Queue; -import com.google.common.base.Supplier; -import com.google.common.collect.ImmutableList; import dagger.Module; import dagger.Provides; -import dagger.multibindings.IntoMap; -import dagger.multibindings.StringKey; -import google.registry.request.Parameter; -import google.registry.request.RequestLogId; import google.registry.util.Clock; -import java.util.UUID; -import javax.inject.Named; -import javax.servlet.http.HttpServletRequest; /** * Dagger module for injecting common settings for Whitebox tasks. @@ -39,46 +24,14 @@ import javax.servlet.http.HttpServletRequest; @Module public class WhiteboxModule { - @Provides - @IntoMap - @StringKey(EppMetric.TABLE_ID) - static ImmutableList provideEppMetricsSchema() { - return EppMetric.SCHEMA_FIELDS; - } - - @Provides - @Parameter("tableId") - static String provideTableId(HttpServletRequest req) { - return extractRequiredParameter(req, "tableId"); - } - - @Provides - @Parameter("insertId") - static String provideInsertId(HttpServletRequest req) { - return extractRequiredParameter(req, "insertId"); - } - - @Provides - @Named("insertIdGenerator") - static Supplier provideInsertIdGenerator() { - return () -> UUID.randomUUID().toString(); - } - /** Provides an EppMetric builder with the request ID and startTimestamp already initialized. */ @Provides - static EppMetric.Builder provideEppMetricBuilder( - @RequestLogId String requestLogId, Clock clock) { - return EppMetric.builderForRequest(requestLogId, clock); + static EppMetric.Builder provideEppMetricBuilder(Clock clock) { + return EppMetric.builderForRequest(clock); } @Provides static CheckApiMetric.Builder provideCheckApiMetricBuilder(Clock clock) { return CheckApiMetric.builder(clock); } - - @Provides - @Named(QUEUE_BIGQUERY_STREAMING_METRICS) - static Queue provideBigQueryStreamingMetricsQueue() { - return getQueue(QUEUE_BIGQUERY_STREAMING_METRICS); - } } diff --git a/javatests/google/registry/flows/EppCommitLogsTest.java b/javatests/google/registry/flows/EppCommitLogsTest.java index aa07fa2ff..0ed3dddd0 100644 --- a/javatests/google/registry/flows/EppCommitLogsTest.java +++ b/javatests/google/registry/flows/EppCommitLogsTest.java @@ -70,7 +70,7 @@ public class EppCommitLogsTest extends ShardableTestCase { sessionMetadata.setClientId("TheRegistrar"); DaggerEppTestComponent.builder() .fakesAndMocksModule( - FakesAndMocksModule.create(clock, EppMetric.builderForRequest("request-id-1", clock))) + FakesAndMocksModule.create(clock, EppMetric.builderForRequest(clock))) .build() .startRequest() .flowComponentBuilder() diff --git a/javatests/google/registry/flows/EppControllerTest.java b/javatests/google/registry/flows/EppControllerTest.java index 7c062e5df..1e882aca6 100644 --- a/javatests/google/registry/flows/EppControllerTest.java +++ b/javatests/google/registry/flows/EppControllerTest.java @@ -16,7 +16,6 @@ package google.registry.flows; import static com.google.common.io.BaseEncoding.base64; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static google.registry.flows.EppXmlTransformer.marshal; import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.LogsSubject.assertAboutLogs; @@ -40,7 +39,6 @@ import google.registry.model.eppoutput.EppOutput; import google.registry.model.eppoutput.EppResponse; import google.registry.model.eppoutput.Result; import google.registry.model.eppoutput.Result.Code; -import google.registry.monitoring.whitebox.BigQueryMetricsEnqueuer; import google.registry.monitoring.whitebox.EppMetric; import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; @@ -60,7 +58,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.mockito.Mock; @@ -74,7 +71,6 @@ public class EppControllerTest extends ShardableTestCase { @Mock SessionMetadata sessionMetadata; @Mock TransportCredentials transportCredentials; @Mock EppMetrics eppMetrics; - @Mock BigQueryMetricsEnqueuer bigQueryMetricsEnqueuer; @Mock FlowComponent.Builder flowComponentBuilder; @Mock FlowComponent flowComponent; @Mock FlowRunner flowRunner; @@ -111,9 +107,8 @@ public class EppControllerTest extends ShardableTestCase { when(result.getCode()).thenReturn(Code.SUCCESS_WITH_NO_MESSAGES); eppController = new EppController(); - eppController.eppMetricBuilder = EppMetric.builderForRequest("request-id-1", clock); + eppController.eppMetricBuilder = EppMetric.builderForRequest(clock); when(flowRunner.run(eppController.eppMetricBuilder)).thenReturn(eppOutput); - eppController.bigQueryMetricsEnqueuer = bigQueryMetricsEnqueuer; eppController.flowComponentBuilder = flowComponentBuilder; eppController.eppMetrics = eppMetrics; eppController.serverTridProvider = new FakeServerTridProvider(); @@ -132,49 +127,6 @@ public class EppControllerTest extends ShardableTestCase { ValidationMode.STRICT); } - @Test - public void testHandleEppCommand_unmarshallableData_exportsMetric() { - eppController.handleEppCommand( - sessionMetadata, - transportCredentials, - EppRequestSource.UNIT_TEST, - false, - false, - new byte[0]); - - ArgumentCaptor metricCaptor = ArgumentCaptor.forClass(EppMetric.class); - verify(bigQueryMetricsEnqueuer).export(metricCaptor.capture()); - EppMetric metric = metricCaptor.getValue(); - assertThat(metric.getRequestId()).isEqualTo("request-id-1"); - assertThat(metric.getStartTimestamp()).isEqualTo(START_TIME); - assertThat(metric.getEndTimestamp()).isEqualTo(clock.nowUtc()); - assertThat(metric.getClientId()).hasValue("some-client"); - assertThat(metric.getPrivilegeLevel()).hasValue("NORMAL"); - assertThat(metric.getStatus()).hasValue(Code.SYNTAX_ERROR); - } - - @Test - public void testHandleEppCommand_regularEppCommand_exportsBigQueryMetric() { - eppController.handleEppCommand( - sessionMetadata, - transportCredentials, - EppRequestSource.UNIT_TEST, - false, - true, - domainCreateXml.getBytes(UTF_8)); - - ArgumentCaptor metricCaptor = ArgumentCaptor.forClass(EppMetric.class); - verify(bigQueryMetricsEnqueuer).export(metricCaptor.capture()); - EppMetric metric = metricCaptor.getValue(); - assertThat(metric.getRequestId()).isEqualTo("request-id-1"); - assertThat(metric.getStartTimestamp()).isEqualTo(START_TIME); - assertThat(metric.getEndTimestamp()).isEqualTo(clock.nowUtc()); - assertThat(metric.getClientId()).hasValue("some-client"); - assertThat(metric.getPrivilegeLevel()).hasValue("SUPERUSER"); - assertThat(metric.getStatus()).hasValue(Code.SUCCESS_WITH_NO_MESSAGES); - assertThat(metric.getEppTarget()).hasValue("example.tld"); - } - @Test public void testHandleEppCommand_regularEppCommand_exportsEppMetrics() { createTld("tld"); @@ -182,12 +134,10 @@ public class EppControllerTest extends ShardableTestCase { // FlowRunner, not EppController, and since FlowRunner is mocked out for these tests they won't // actually get values. EppMetric.Builder metricBuilder = - EppMetric.builderForRequest("request-id-1", clock) + EppMetric.builderForRequest(clock) .setClientId("some-client") - .setEppTarget("example.tld") .setStatus(Code.SUCCESS_WITH_NO_MESSAGES) - .setTld("tld") - .setPrivilegeLevel("SUPERUSER"); + .setTld("tld"); eppController.handleEppCommand( sessionMetadata, transportCredentials, @@ -210,8 +160,6 @@ public class EppControllerTest extends ShardableTestCase { true, true, domainCreateXml.getBytes(UTF_8)); - - verifyZeroInteractions(bigQueryMetricsEnqueuer); verifyZeroInteractions(eppMetrics); } diff --git a/javatests/google/registry/flows/EppLifecycleContactTest.java b/javatests/google/registry/flows/EppLifecycleContactTest.java index aa6f91c73..bdb33f4d3 100644 --- a/javatests/google/registry/flows/EppLifecycleContactTest.java +++ b/javatests/google/registry/flows/EppLifecycleContactTest.java @@ -52,8 +52,6 @@ public class EppLifecycleContactTest extends EppTestCase { .and() .hasCommandName("ContactCreate") .and() - .hasEppTarget("sh8013") - .and() .hasStatus(SUCCESS); assertThatCommand("contact_info.xml") .atTime("2000-06-01T00:01:00Z") @@ -63,8 +61,6 @@ public class EppLifecycleContactTest extends EppTestCase { .and() .hasCommandName("ContactInfo") .and() - .hasEppTarget("sh8013") - .and() .hasStatus(SUCCESS); assertThatCommand("contact_delete_sh8013.xml") .hasResponse("contact_delete_response_sh8013.xml"); @@ -73,8 +69,6 @@ public class EppLifecycleContactTest extends EppTestCase { .and() .hasCommandName("ContactDelete") .and() - .hasEppTarget("sh8013") - .and() .hasStatus(SUCCESS_WITH_ACTION_PENDING); assertThatLogoutSucceeds(); } diff --git a/javatests/google/registry/flows/EppLifecycleDomainTest.java b/javatests/google/registry/flows/EppLifecycleDomainTest.java index b4274518b..edd705e77 100644 --- a/javatests/google/registry/flows/EppLifecycleDomainTest.java +++ b/javatests/google/registry/flows/EppLifecycleDomainTest.java @@ -440,8 +440,6 @@ public class EppLifecycleDomainTest extends EppTestCase { .and() .hasCommandName("HostUpdate") .and() - .hasEppTarget("ns3.fakesite.example") - .and() .hasStatus(SUCCESS); // Delete the fakesite.example domain (which should succeed since it no longer has subords). assertThatCommand("domain_delete.xml", ImmutableMap.of("DOMAIN", "fakesite.example")) @@ -454,8 +452,6 @@ public class EppLifecycleDomainTest extends EppTestCase { .and() .hasCommandName("DomainDelete") .and() - .hasEppTarget("fakesite.example") - .and() .hasStatus(SUCCESS_WITH_ACTION_PENDING); // Check info on the renamed host and verify that it's still around and wasn't deleted. assertThatCommand("host_info_ns9000_example.xml") @@ -466,8 +462,6 @@ public class EppLifecycleDomainTest extends EppTestCase { .and() .hasCommandName("HostInfo") .and() - .hasEppTarget("ns9000.example.external") - .and() .hasStatus(SUCCESS); assertThatLogoutSucceeds(); assertThat(getRecordedEppMetric()) @@ -575,8 +569,6 @@ public class EppLifecycleDomainTest extends EppTestCase { .and() .hasCommandName("DomainCheck") .and() - .hasEppTarget("rich.example") - .and() .hasTld("example") .and() .hasStatus(SUCCESS); diff --git a/javatests/google/registry/flows/EppLifecycleHostTest.java b/javatests/google/registry/flows/EppLifecycleHostTest.java index fb918c292..2328424c3 100644 --- a/javatests/google/registry/flows/EppLifecycleHostTest.java +++ b/javatests/google/registry/flows/EppLifecycleHostTest.java @@ -67,8 +67,6 @@ public class EppLifecycleHostTest extends EppTestCase { .and() .hasCommandName("HostCreate") .and() - .hasEppTarget("ns1.example.tld") - .and() .hasStatus(SUCCESS); assertThatCommand("host_info.xml", ImmutableMap.of("HOSTNAME", "ns1.example.tld")) .atTime("2000-06-02T00:02:00Z") @@ -81,8 +79,6 @@ public class EppLifecycleHostTest extends EppTestCase { .and() .hasCommandName("HostInfo") .and() - .hasEppTarget("ns1.example.tld") - .and() .hasStatus(SUCCESS); assertThatCommand("host_delete.xml", ImmutableMap.of("HOSTNAME", "ns1.example.tld")) .atTime("2000-06-02T00:03:00Z") @@ -92,8 +88,6 @@ public class EppLifecycleHostTest extends EppTestCase { .and() .hasCommandName("HostDelete") .and() - .hasEppTarget("ns1.example.tld") - .and() .hasStatus(SUCCESS_WITH_ACTION_PENDING); assertThatLogoutSucceeds(); } diff --git a/javatests/google/registry/flows/EppTestCase.java b/javatests/google/registry/flows/EppTestCase.java index d76e602cf..9385849f4 100644 --- a/javatests/google/registry/flows/EppTestCase.java +++ b/javatests/google/registry/flows/EppTestCase.java @@ -167,7 +167,7 @@ public class EppTestCase extends ShardableTestCase { EppRequestHandler handler = new EppRequestHandler(); FakeResponse response = new FakeResponse(); handler.response = response; - eppMetricBuilder = EppMetric.builderForRequest("request-id-1", clock); + eppMetricBuilder = EppMetric.builderForRequest(clock); handler.eppController = DaggerEppTestComponent.builder() .fakesAndMocksModule(FakesAndMocksModule.create(clock, eppMetricBuilder)) .build() diff --git a/javatests/google/registry/flows/EppTestComponent.java b/javatests/google/registry/flows/EppTestComponent.java index d2e86259b..cfe140422 100644 --- a/javatests/google/registry/flows/EppTestComponent.java +++ b/javatests/google/registry/flows/EppTestComponent.java @@ -32,7 +32,6 @@ import google.registry.flows.async.AsyncFlowEnqueuer; import google.registry.flows.custom.CustomLogicFactory; import google.registry.flows.custom.TestCustomLogicFactory; import google.registry.flows.domain.DomainFlowTmchUtils; -import google.registry.monitoring.whitebox.BigQueryMetricsEnqueuer; import google.registry.monitoring.whitebox.EppMetric; import google.registry.request.RequestScope; import google.registry.request.lock.LockHandler; @@ -64,7 +63,6 @@ interface EppTestComponent { class FakesAndMocksModule { private AsyncFlowEnqueuer asyncFlowEnqueuer; - private BigQueryMetricsEnqueuer metricsEnqueuer; private DnsQueue dnsQueue; private DomainFlowTmchUtils domainFlowTmchUtils; private EppMetric.Builder metricBuilder; @@ -75,7 +73,7 @@ interface EppTestComponent { public static FakesAndMocksModule create() { FakeClock clock = new FakeClock(); - return create(clock, EppMetric.builderForRequest("request-id-1", clock)); + return create(clock, EppMetric.builderForRequest(clock)); } public static FakesAndMocksModule create(FakeClock clock, EppMetric.Builder metricBuilder) { @@ -106,7 +104,6 @@ interface EppTestComponent { instance.dnsQueue = DnsQueue.create(); instance.metricBuilder = eppMetricBuilder; instance.appEngineServiceUtils = appEngineServiceUtils; - instance.metricsEnqueuer = mock(BigQueryMetricsEnqueuer.class); instance.lockHandler = new FakeLockHandler(true); return instance; } @@ -116,11 +113,6 @@ interface EppTestComponent { return asyncFlowEnqueuer; } - @Provides - BigQueryMetricsEnqueuer provideBigQueryMetricsEnqueuer() { - return metricsEnqueuer; - } - @Provides Clock provideClock() { return clock; diff --git a/javatests/google/registry/flows/FlowRunnerTest.java b/javatests/google/registry/flows/FlowRunnerTest.java index 82a3362fa..c828425ba 100644 --- a/javatests/google/registry/flows/FlowRunnerTest.java +++ b/javatests/google/registry/flows/FlowRunnerTest.java @@ -54,8 +54,7 @@ public class FlowRunnerTest extends ShardableTestCase { public final AppEngineRule appEngineRule = new AppEngineRule.Builder().build(); private final FlowRunner flowRunner = new FlowRunner(); - private final EppMetric.Builder eppMetricBuilder = - EppMetric.builderForRequest("request-id-1", new FakeClock()); + private final EppMetric.Builder eppMetricBuilder = EppMetric.builderForRequest(new FakeClock()); private final TestLogHandler handler = new TestLogHandler(); @@ -84,19 +83,6 @@ public class FlowRunnerTest extends ShardableTestCase { flowRunner.flowReporter = Mockito.mock(FlowReporter.class); } - @Test - public void testRun_nonTransactionalCommand_incrementsMetricAttempts() throws Exception { - flowRunner.run(eppMetricBuilder); - assertThat(eppMetricBuilder.build().getAttempts()).isEqualTo(1); - } - - @Test - public void testRun_transactionalCommand_incrementsMetricAttempts() throws Exception { - flowRunner.isTransactional = true; - flowRunner.run(eppMetricBuilder); - assertThat(eppMetricBuilder.build().getAttempts()).isEqualTo(1); - } - @Test public void testRun_nonTransactionalCommand_setsCommandNameOnMetric() throws Exception { flowRunner.isTransactional = true; diff --git a/javatests/google/registry/flows/FlowTestCase.java b/javatests/google/registry/flows/FlowTestCase.java index 7db1baca2..145730833 100644 --- a/javatests/google/registry/flows/FlowTestCase.java +++ b/javatests/google/registry/flows/FlowTestCase.java @@ -278,7 +278,7 @@ public abstract class FlowTestCase extends ShardableTestCase { private EppOutput runFlowInternal(CommitMode commitMode, UserPrivileges userPrivileges) throws Exception { - eppMetricBuilder = EppMetric.builderForRequest("request-id-1", clock); + eppMetricBuilder = EppMetric.builderForRequest(clock); // Assert that the xml triggers the flow we expect. assertThat(FlowPicker.getFlowClass(eppLoader.getEpp())) .isEqualTo(new TypeInstantiator(getClass()){}.getExactType()); diff --git a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java index 1698920c9..da4f3a562 100644 --- a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java @@ -2541,6 +2541,5 @@ public class DomainCreateFlowTest extends ResourceFlowTestCase getSchemaFields() { - return null; - } - - @Override - public ImmutableMap getBigQueryRowEncoding() { - return ImmutableMap.of( - "startTime", toBigqueryTimestamp(getStartTimestamp()), - "endTime", toBigqueryTimestamp(getEndTimestamp())); - } - - abstract DateTime getStartTimestamp(); - - abstract DateTime getEndTimestamp(); - } -} diff --git a/javatests/google/registry/monitoring/whitebox/EppMetricTest.java b/javatests/google/registry/monitoring/whitebox/EppMetricTest.java index 6c21ee1ac..7fd47d227 100644 --- a/javatests/google/registry/monitoring/whitebox/EppMetricTest.java +++ b/javatests/google/registry/monitoring/whitebox/EppMetricTest.java @@ -14,18 +14,13 @@ package google.registry.monitoring.whitebox; -import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.createTlds; -import com.google.api.services.bigquery.model.TableFieldSchema; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import google.registry.model.eppoutput.Result.Code; import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; -import org.joda.time.DateTime; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,7 +35,7 @@ public class EppMetricTest { @Test public void test_invalidTld_isRecordedAsInvalid() { EppMetric metric = - EppMetric.builderForRequest("request-id-1", new FakeClock()) + EppMetric.builderForRequest(new FakeClock()) .setTlds(ImmutableSet.of("notarealtld")) .build(); assertThat(metric.getTld()).hasValue("_invalid"); @@ -50,9 +45,7 @@ public class EppMetricTest { public void test_validTld_isRecorded() { createTld("example"); EppMetric metric = - EppMetric.builderForRequest("request-id-1", new FakeClock()) - .setTlds(ImmutableSet.of("example")) - .build(); + EppMetric.builderForRequest(new FakeClock()).setTlds(ImmutableSet.of("example")).build(); assertThat(metric.getTld()).hasValue("example"); } @@ -60,7 +53,7 @@ public class EppMetricTest { public void test_multipleTlds_areRecordedAsVarious() { createTlds("foo", "bar"); EppMetric metric = - EppMetric.builderForRequest("request-id-1", new FakeClock()) + EppMetric.builderForRequest(new FakeClock()) .setTlds(ImmutableSet.of("foo", "bar", "baz")) .build(); assertThat(metric.getTld()).hasValue("_various"); @@ -69,64 +62,7 @@ public class EppMetricTest { @Test public void test_zeroTlds_areRecordedAsAbsent() { EppMetric metric = - EppMetric.builderForRequest("request-id-1", new FakeClock()) - .setTlds(ImmutableSet.of()) - .build(); + EppMetric.builderForRequest(new FakeClock()).setTlds(ImmutableSet.of()).build(); assertThat(metric.getTld()).isEmpty(); } - - @Test - public void testGetBigQueryRowEncoding_encodesCorrectly() { - EppMetric metric = - EppMetric.builder() - .setRequestId("request-id-1") - .setStartTimestamp(new DateTime(1337)) - .setEndTimestamp(new DateTime(1338)) - .setCommandName("command") - .setClientId("client") - .setTld("example") - .setPrivilegeLevel("level") - .setEppTarget("target") - .setStatus(Code.COMMAND_USE_ERROR) - .incrementAttempts() - .build(); - - assertThat(metric.getBigQueryRowEncoding()) - .containsExactlyEntriesIn( - new ImmutableMap.Builder() - .put("requestId", "request-id-1") - .put("startTime", "1.337000") - .put("endTime", "1.338000") - .put("commandName", "command") - .put("clientId", "client") - .put("tld", "example") - .put("privilegeLevel", "level") - .put("eppTarget", "target") - .put("eppStatus", "2002") - .put("attempts", "1") - .build()); - } - - @Test - public void testGetBigQueryRowEncoding_hasAllSchemaFields() { - EppMetric metric = - EppMetric.builder() - .setRequestId("request-id-1") - .setStartTimestamp(new DateTime(1337)) - .setEndTimestamp(new DateTime(1338)) - .setCommandName("command") - .setClientId("client") - .setTld("example") - .setPrivilegeLevel("level") - .setEppTarget("target") - .setStatus(Code.COMMAND_USE_ERROR) - .incrementAttempts() - .build(); - ImmutableSet.Builder schemaFieldNames = new ImmutableSet.Builder<>(); - for (TableFieldSchema schemaField : metric.getSchemaFields()) { - schemaFieldNames.add(schemaField.getName()); - } - - assertThat(metric.getBigQueryRowEncoding().keySet()).isEqualTo(schemaFieldNames.build()); - } } diff --git a/javatests/google/registry/monitoring/whitebox/MetricsExportActionTest.java b/javatests/google/registry/monitoring/whitebox/MetricsExportActionTest.java deleted file mode 100644 index 5e54adb64..000000000 --- a/javatests/google/registry/monitoring/whitebox/MetricsExportActionTest.java +++ /dev/null @@ -1,108 +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.monitoring.whitebox; - -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.api.services.bigquery.Bigquery; -import com.google.api.services.bigquery.Bigquery.Tabledata; -import com.google.api.services.bigquery.Bigquery.Tabledata.InsertAll; -import com.google.api.services.bigquery.model.TableDataInsertAllRequest; -import com.google.api.services.bigquery.model.TableDataInsertAllResponse; -import com.google.api.services.bigquery.model.TableDataInsertAllResponse.InsertErrors; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; -import google.registry.bigquery.CheckedBigquery; -import google.registry.testing.AppEngineRule; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.Matchers; - -/** Unit tests for {@link MetricsExportAction}. */ -@RunWith(JUnit4.class) -public class MetricsExportActionTest { - - @Rule - public final AppEngineRule appEngine = - AppEngineRule.builder().withDatastore().withTaskQueue().build(); - - private final CheckedBigquery checkedBigquery = mock(CheckedBigquery.class); - private final Bigquery bigquery = mock(Bigquery.class); - private final Tabledata tabledata = mock(Tabledata.class); - private final InsertAll insertAll = mock(InsertAll.class); - - private TableDataInsertAllResponse response = new TableDataInsertAllResponse(); - private long currentTimeMillis = 1000000000000L; - - private ImmutableListMultimap parameters = - new ImmutableListMultimap.Builder() - .put("startTime", String.valueOf(MILLISECONDS.toSeconds(currentTimeMillis - 100))) - .put("endTime", String.valueOf(MILLISECONDS.toSeconds(currentTimeMillis))) - .put("jobname", "test job") - .put("status", "success") - .put("tld", "test") - .build(); - - MetricsExportAction action; - - @Before - public void setup() throws Exception { - when(checkedBigquery.ensureDataSetAndTableExist(anyString(), anyString(), anyString())) - .thenReturn(bigquery); - - when(bigquery.tabledata()).thenReturn(tabledata); - when(tabledata.insertAll( - anyString(), - anyString(), - anyString(), - Matchers.any(TableDataInsertAllRequest.class))).thenReturn(insertAll); - action = new MetricsExportAction(); - action.checkedBigquery = checkedBigquery; - action.insertId = "insert id"; - action.parameters = parameters; - action.projectId = "project id"; - action.tableId = "eppMetrics"; - } - - @Test - public void testSuccess_nullErrors() throws Exception { - when(insertAll.execute()).thenReturn(response); - response.setInsertErrors(null); - action.run(); - verify(insertAll).execute(); - } - - @Test - public void testSuccess_emptyErrors() throws Exception { - when(insertAll.execute()).thenReturn(response); - response.setInsertErrors(ImmutableList.of()); - action.run(); - verify(insertAll).execute(); - } - - @Test - public void testFailure_errors() throws Exception { - when(insertAll.execute()).thenReturn(response); - response.setInsertErrors(ImmutableList.of(new InsertErrors())); - action.run(); - } -} diff --git a/javatests/google/registry/testing/EppMetricSubject.java b/javatests/google/registry/testing/EppMetricSubject.java index b80ba0e2e..c8187d2f8 100644 --- a/javatests/google/registry/testing/EppMetricSubject.java +++ b/javatests/google/registry/testing/EppMetricSubject.java @@ -44,10 +44,6 @@ public class EppMetricSubject extends Subject { return hasValue(commandName, actual().getCommandName(), "has commandName"); } - public And hasEppTarget(String eppTarget) { - return hasValue(eppTarget, actual().getEppTarget(), "has eppTarget"); - } - public And hasStatus(Code status) { return hasValue(status, actual().getStatus(), "has status"); } From 9e02502fd4ad0fc405adde8e905703399c2573da Mon Sep 17 00:00:00 2001 From: weiminyu Date: Wed, 10 Oct 2018 08:51:27 -0700 Subject: [PATCH 22/60] Fix bugs exposed by testing with Gradle The following issues are addressed: - XML sanitizer should preserve input encoding. Gradle loses any that is not UTF-8. Bazel loses any that is not ASCII. - Verify that XML sanitizer works with non-UTF8 encoding - GpgSystemCommandRule breaks when $TMPDIR env variable is not set - TestDataHelper throws exception when loading resources if resources are plain files on default file system as opposed to being in a jar file. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=216537258 --- .../registry/flows/EppXmlSanitizer.java | 45 ++++++++++++++----- .../registry/flows/EppXmlSanitizerTest.java | 32 +++++++++---- .../testing/GpgSystemCommandRule.java | 13 ++++-- .../registry/testing/TestDataHelper.java | 18 +++++++- 4 files changed, 84 insertions(+), 24 deletions(-) diff --git a/java/google/registry/flows/EppXmlSanitizer.java b/java/google/registry/flows/EppXmlSanitizer.java index 2113dc5dc..ee20afe71 100644 --- a/java/google/registry/flows/EppXmlSanitizer.java +++ b/java/google/registry/flows/EppXmlSanitizer.java @@ -14,14 +14,18 @@ package google.registry.flows; +import static com.google.common.base.Preconditions.checkState; + import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableSet; import com.google.common.flogger.FluentLogger; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Locale; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.xml.namespace.QName; @@ -32,6 +36,7 @@ import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.Characters; +import javax.xml.stream.events.StartDocument; import javax.xml.stream.events.XMLEvent; /** @@ -71,31 +76,47 @@ public class EppXmlSanitizer { private static final XMLEventFactory XML_EVENT_FACTORY = XMLEventFactory.newFactory(); /** - * Returns sanitized and pretty-printed EPP XML message. For malformed XML messages, - * base64-encoded raw bytes will be returned. + * Returns sanitized EPP XML message. For malformed XML messages, base64-encoded raw bytes will be + * returned. * - *

The xml version header {@code } is added to the result if the input - * does not already contain it. Also, an empty element will be formatted as {@code } - * instead of {@code }. + *

The output always begins with version and encoding declarations no matter if the input + * includes them. If encoding is not declared by input, UTF-8 will be used according to XML + * standard. + * + *

Also, an empty element will be formatted as {@code } instead of {@code }. */ public static String sanitizeEppXml(byte[] inputXmlBytes) { try { // Keep exactly one newline at end of sanitized string. - return CharMatcher.whitespace() - .trimTrailingFrom(new String(sanitize(inputXmlBytes), StandardCharsets.UTF_8)) - + "\n"; - } catch (XMLStreamException e) { + return CharMatcher.whitespace().trimTrailingFrom(sanitizeAndEncode(inputXmlBytes)) + "\n"; + } catch (XMLStreamException | UnsupportedEncodingException e) { logger.atWarning().withCause(e).log("Failed to sanitize EPP XML message."); return Base64.getMimeEncoder().encodeToString(inputXmlBytes); } } - private static byte[] sanitize(byte[] inputXmlBytes) throws XMLStreamException { + private static String sanitizeAndEncode(byte[] inputXmlBytes) + throws XMLStreamException, UnsupportedEncodingException { XMLEventReader xmlEventReader = XML_INPUT_FACTORY.createXMLEventReader(new ByteArrayInputStream(inputXmlBytes)); + if (!xmlEventReader.hasNext()) { + return ""; + } + + XMLEvent firstEvent = xmlEventReader.nextEvent(); + checkState(firstEvent.isStartDocument(), "Missing StartDocument"); + // Get input encoding for use in XMLEventWriter creation, so that sanitized XML preserves the + // encoding declaration. According to XML spec, UTF-8 is to be used unless input declares + // otherwise. Epp officially allows UTF-8 and UTF-16. + String inputEncoding = + Optional.ofNullable(((StartDocument) firstEvent).getCharacterEncodingScheme()) + .orElse(StandardCharsets.UTF_8.name()); + ByteArrayOutputStream outputXmlBytes = new ByteArrayOutputStream(); - XMLEventWriter xmlEventWriter = XML_OUTPUT_FACTORY.createXMLEventWriter(outputXmlBytes); + XMLEventWriter xmlEventWriter = + XML_OUTPUT_FACTORY.createXMLEventWriter(outputXmlBytes, inputEncoding); + xmlEventWriter.add(firstEvent); while (xmlEventReader.hasNext()) { XMLEvent xmlEvent = xmlEventReader.nextEvent(); @@ -118,7 +139,7 @@ public class EppXmlSanitizer { } } xmlEventWriter.flush(); - return outputXmlBytes.toByteArray(); + return outputXmlBytes.toString(inputEncoding); } private static String maskSensitiveData(String original) { diff --git a/javatests/google/registry/flows/EppXmlSanitizerTest.java b/javatests/google/registry/flows/EppXmlSanitizerTest.java index 3cc098608..1bdad99f6 100644 --- a/javatests/google/registry/flows/EppXmlSanitizerTest.java +++ b/javatests/google/registry/flows/EppXmlSanitizerTest.java @@ -17,6 +17,7 @@ package google.registry.flows; import static com.google.common.truth.Truth.assertThat; import static google.registry.flows.EppXmlSanitizer.sanitizeEppXml; import static google.registry.testing.TestDataHelper.loadBytes; +import static java.nio.charset.StandardCharsets.UTF_16LE; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableMap; @@ -30,12 +31,12 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class EppXmlSanitizerTest { - private static final String XML_HEADER = ""; + private static final String UTF8_HEADER = ""; @Test public void testSanitize_noSensitiveData_noop() throws Exception { byte[] inputXmlBytes = loadBytes(getClass(), "host_create.xml").read(); - String expectedXml = XML_HEADER + new String(inputXmlBytes, UTF_8); + String expectedXml = UTF8_HEADER + new String(inputXmlBytes, UTF_8); String sanitizedXml = sanitizeEppXml(inputXmlBytes); assertThat(sanitizedXml).isEqualTo(expectedXml); @@ -50,7 +51,7 @@ public class EppXmlSanitizerTest { ImmutableMap.of("PW", "oldpass", "NEWPW", "newPw")) .getEppXml(); String expectedXml = - XML_HEADER + UTF8_HEADER + new EppLoader( this, "login_update_password.xml", @@ -68,7 +69,7 @@ public class EppXmlSanitizerTest { this, "login_wrong_case.xml", ImmutableMap.of("PW", "oldpass", "NEWPW", "newPw")) .getEppXml(); String expectedXml = - XML_HEADER + UTF8_HEADER + new EppLoader( this, "login_wrong_case.xml", @@ -83,7 +84,7 @@ public class EppXmlSanitizerTest { public void testSanitize_contactAuthInfo_sanitized() throws Exception { byte[] inputXmlBytes = loadBytes(getClass(), "contact_info.xml").read(); String expectedXml = - XML_HEADER + UTF8_HEADER + new EppLoader(this, "contact_info_sanitized.xml", ImmutableMap.of()).getEppXml(); String sanitizedXml = sanitizeEppXml(inputXmlBytes); @@ -94,7 +95,7 @@ public class EppXmlSanitizerTest { public void testSanitize_contactCreateResponseAuthInfo_sanitized() throws Exception { byte[] inputXmlBytes = loadBytes(getClass(), "contact_info_from_create_response.xml").read(); String expectedXml = - XML_HEADER + UTF8_HEADER + new EppLoader( this, "contact_info_from_create_response_sanitized.xml", ImmutableMap.of()) .getEppXml(); @@ -106,7 +107,7 @@ public class EppXmlSanitizerTest { @Test public void testSanitize_emptyElement_transformedToLongForm() { byte[] inputXmlBytes = "".getBytes(UTF_8); - assertThat(sanitizeEppXml(inputXmlBytes)).isEqualTo(XML_HEADER + "\n"); + assertThat(sanitizeEppXml(inputXmlBytes)).isEqualTo(UTF8_HEADER + "\n"); } @Test @@ -119,7 +120,22 @@ public class EppXmlSanitizerTest { @Test public void testSanitize_unicode_hasCorrectCharCount() { byte[] inputXmlBytes = "\u007F\u4E43x".getBytes(UTF_8); - String expectedXml = XML_HEADER + "C**\n"; + String expectedXml = UTF8_HEADER + "C**\n"; assertThat(sanitizeEppXml(inputXmlBytes)).isEqualTo(expectedXml); } + + @Test + public void testSanitize_emptyString_encodedToBase64() { + byte[] inputXmlBytes = "".getBytes(UTF_8); + assertThat(sanitizeEppXml(inputXmlBytes)).isEqualTo(""); + } + + @Test + public void testSanitize_utf16_encodingPreserved() { + // Test data should specify an endian-specific UTF-16 scheme for easy assertion. If 'UTF-16' is + // used, the XMLEventReader in sanitizer may resolve it to an endian-specific one. + String inputXml = "

\u03bc

\n"; + String sanitizedXml = sanitizeEppXml(inputXml.getBytes(UTF_16LE)); + assertThat(sanitizedXml).isEqualTo(inputXml); + } } diff --git a/javatests/google/registry/testing/GpgSystemCommandRule.java b/javatests/google/registry/testing/GpgSystemCommandRule.java index b073a46b5..522295f32 100644 --- a/javatests/google/registry/testing/GpgSystemCommandRule.java +++ b/javatests/google/registry/testing/GpgSystemCommandRule.java @@ -17,6 +17,7 @@ package google.registry.testing; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.truth.Truth.assertWithMessage; import static java.nio.charset.StandardCharsets.UTF_8; @@ -84,9 +85,14 @@ public final class GpgSystemCommandRule extends ExternalResource { @Override protected void before() throws IOException, InterruptedException { checkState(Objects.equals(cwd, DEV_NULL)); - File tmpRoot = null; - tmpRoot = new File(System.getenv("TMPDIR")); - cwd = File.createTempFile(TEMP_FILE_PREFIX, "", tmpRoot); + String tmpRootDirString = System.getenv("TMPDIR"); + // Create the working directory for the forked process on Temp file system. Create under the + // path specified by 'TMPDIR' envrionment variable if defined, otherwise create under the + // runtime's default (typically /tmp). + cwd = + isNullOrEmpty(tmpRootDirString) + ? File.createTempFile(TEMP_FILE_PREFIX, "") + : File.createTempFile(TEMP_FILE_PREFIX, "", new File(tmpRootDirString)); cwd.delete(); cwd.mkdir(); conf = new File(cwd, ".gnupg"); @@ -118,6 +124,7 @@ public final class GpgSystemCommandRule extends ExternalResource { @Override protected void after() { + // TODO(weiminyu): we should delete the cwd tree. cwd = DEV_NULL; conf = DEV_NULL; } diff --git a/javatests/google/registry/testing/TestDataHelper.java b/javatests/google/registry/testing/TestDataHelper.java index a7cf34415..49dcd83c7 100644 --- a/javatests/google/registry/testing/TestDataHelper.java +++ b/javatests/google/registry/testing/TestDataHelper.java @@ -23,7 +23,9 @@ import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteSource; import com.google.common.io.MoreFiles; import com.google.common.io.Resources; +import java.io.IOException; import java.net.URI; +import java.nio.file.FileSystemAlreadyExistsException; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; @@ -93,7 +95,21 @@ public final class TestDataHelper { /** Returns a recursive iterable of all files in the given directory. */ public static Iterable listFiles(Class context, String directory) throws Exception { URI dir = Resources.getResource(context, directory).toURI(); - FileSystems.newFileSystem(dir, ImmutableMap.of("create", "true")); + ensureFileSystemPresentForUri(dir); return MoreFiles.fileTraverser().breadthFirst(Paths.get(dir)); } + + private static void ensureFileSystemPresentForUri(URI uri) throws IOException { + if (uri.getScheme().equals(FileSystems.getDefault().provider().getScheme())) { + // URI maps to default file system (file://...), which must be present. Besides, calling + // FileSystem.newFileSystem on this URI may trigger FileSystemAlreadyExistsException. + return; + } + try { + // URI maps to a special file system, e.g., jar:... + FileSystems.newFileSystem(uri, ImmutableMap.of("create", "true")); + } catch (FileSystemAlreadyExistsException e) { + // ignore. + } + } } From 3bb525349f18a9cce05168229e5cb837e4beab3a Mon Sep 17 00:00:00 2001 From: guyben Date: Thu, 11 Oct 2018 07:51:36 -0700 Subject: [PATCH 23/60] Wrap render of console.main soy in a utility function ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=216695490 --- .../google/registry/ui/js/registrar/BUILD | 2 +- .../registry/ui/js/registrar/console_test.js | 33 ++++-------- .../ui/js/registrar/console_test_util.js | 40 ++++++++++++-- .../ui/js/registrar/contact_settings_test.js | 38 +++++-------- .../registry/ui/js/registrar/contact_test.js | 23 ++------ .../registry/ui/js/registrar/domain_test.js | 54 +++++++------------ .../registry/ui/js/registrar/host_test.js | 54 +++++++------------ .../ui/js/registrar/security_settings_test.js | 28 +++------- .../ui/js/registrar/whois_settings_test.js | 40 +++++--------- 9 files changed, 125 insertions(+), 187 deletions(-) diff --git a/javatests/google/registry/ui/js/registrar/BUILD b/javatests/google/registry/ui/js/registrar/BUILD index 2107c6404..cb282707e 100644 --- a/javatests/google/registry/ui/js/registrar/BUILD +++ b/javatests/google/registry/ui/js/registrar/BUILD @@ -13,6 +13,7 @@ closure_js_library( deps = [ "//java/google/registry/ui/js", "//java/google/registry/ui/js/registrar", + "//java/google/registry/ui/soy/registrar", "@io_bazel_rules_closure//closure/library", "@io_bazel_rules_closure//closure/library:testing", ], @@ -28,7 +29,6 @@ closure_js_test( ":console_test_util", "//java/google/registry/ui/js", "//java/google/registry/ui/js/registrar", - "//java/google/registry/ui/soy/registrar", "//javatests/google/registry/ui/js:testing", "@io_bazel_rules_closure//closure/library", "@io_bazel_rules_closure//closure/library:testing", diff --git a/javatests/google/registry/ui/js/registrar/console_test.js b/javatests/google/registry/ui/js/registrar/console_test.js index 1ba16afa8..afb2347bf 100644 --- a/javatests/google/registry/ui/js/registrar/console_test.js +++ b/javatests/google/registry/ui/js/registrar/console_test.js @@ -17,7 +17,6 @@ goog.setTestOnly(); goog.require('goog.dom'); goog.require('goog.dom.classlist'); goog.require('goog.json'); -goog.require('goog.soy'); goog.require('goog.testing.MockControl'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.asserts'); @@ -25,15 +24,14 @@ goog.require('goog.testing.jsunit'); goog.require('goog.testing.mockmatchers'); goog.require('goog.testing.net.XhrIo'); goog.require('registry.registrar.ConsoleTestUtil'); -goog.require('registry.soy.registrar.console'); goog.require('registry.testing'); goog.require('registry.util'); -var $ = goog.dom.getRequiredElement; -var stubs = new goog.testing.PropertyReplacer(); +const $ = goog.dom.getRequiredElement; +const stubs = new goog.testing.PropertyReplacer(); -var test = { +const test = { testXsrfToken: 'testToken', testClientId: 'daddy', mockControl: new goog.testing.MockControl() @@ -44,24 +42,13 @@ function setUp() { registry.testing.addToDocument('
'); registry.testing.addToDocument('
'); stubs.setPath('goog.net.XhrIo', goog.testing.net.XhrIo); - var testElt = goog.dom.getElement('test'); - goog.soy.renderElement(testElt, registry.soy.registrar.console.main, { + registry.registrar.ConsoleTestUtil.renderConsoleMain($('test'), { xsrfToken: test.testXsrfToken, - username: 'blah', - logoutUrl: 'omg', - isAdmin: true, clientId: test.testClientId, - logoFilename: 'logo.png', - productName: 'Nomulus', - integrationEmail: 'integration@example.com', - supportEmail: 'support@example.com', - announcementsEmail: 'announcement@example.com', - supportPhoneNumber: '+1 (888) 555 0123', - technicalDocsUrl: 'http://example.com/techdocs', }); registry.registrar.ConsoleTestUtil.setup(test); - var regNavlist = $('reg-navlist'); - var active = regNavlist.querySelector('a[href="#contact-us"]'); + const regNavlist = $('reg-navlist'); + const active = regNavlist.querySelector('a[href="#contact-us"]'); assertTrue(active != null); } @@ -78,7 +65,7 @@ function testButter() { productName: 'Foo Registry' }); registry.util.butter('butter msg'); - var butter = goog.dom.getElementByClass(goog.getCssName('kd-butterbar')); + const butter = goog.dom.getElementByClass(goog.getCssName('kd-butterbar')); assertNotNull(butter.innerHTML.match(/.*butter msg.*/)); assertTrue(goog.dom.classlist.contains(butter, goog.getCssName('shown'))); } @@ -131,7 +118,7 @@ function testNavToResources() { premiumPriceAckRequired: false, readonly: true, }); - var xhr = goog.testing.net.XhrIo.getSendInstances().pop(); + const xhr = goog.testing.net.XhrIo.getSendInstances().pop(); assertTrue(xhr.isActive()); assertEquals('/registrar-settings', xhr.getLastUri()); assertEquals(test.testXsrfToken, @@ -157,12 +144,12 @@ function testNavToContactUs() { announcementsEmail: 'announcement@example.com', supportPhoneNumber: '+1 (888) 555 0123' }); - var xhr = goog.testing.net.XhrIo.getSendInstances().pop(); + const xhr = goog.testing.net.XhrIo.getSendInstances().pop(); assertTrue(xhr.isActive()); assertEquals('/registrar-settings', xhr.getLastUri()); assertEquals(test.testXsrfToken, xhr.getLastRequestHeaders()['X-CSRF-Token']); - var passcode = '5-5-5-5-5'; + const passcode = '5-5-5-5-5'; xhr.simulateResponse(200, goog.json.serialize({ status: 'SUCCESS', message: 'OK', diff --git a/javatests/google/registry/ui/js/registrar/console_test_util.js b/javatests/google/registry/ui/js/registrar/console_test_util.js index dc9f31685..2eac0449c 100644 --- a/javatests/google/registry/ui/js/registrar/console_test_util.js +++ b/javatests/google/registry/ui/js/registrar/console_test_util.js @@ -18,9 +18,11 @@ goog.setTestOnly('registry.registrar.ConsoleTestUtil'); goog.require('goog.History'); goog.require('goog.asserts'); goog.require('goog.dom.xml'); +goog.require('goog.soy'); goog.require('goog.testing.mockmatchers'); goog.require('registry.registrar.Console'); goog.require('registry.registrar.EppSession'); +goog.require('registry.soy.registrar.console'); goog.require('registry.xml'); @@ -28,7 +30,7 @@ goog.require('registry.xml'); * Utility method that attaches mocks to a `TestCase`. This was * originally in the ctor for ConsoleTest and should simply be * inherited but jstd_test breaks inheritance in test cases. - * @param {Object} test the test case to configure. + * @param {!Object} test the test case to configure. */ registry.registrar.ConsoleTestUtil.setup = function(test) { test.historyMock = test.mockControl.createLooseMock(goog.History, true); @@ -42,13 +44,41 @@ registry.registrar.ConsoleTestUtil.setup = function(test) { .$returns(test.sessionMock); }; +/** + * Utility method that renders the registry.soy.registrar.console.main element. + * + * This element has a lot of parameters. We use defaults everywhere, but you can + * override them with 'opt_args'. + * + * @param {!Element} element the element whose content we are rendering into. + * @param {?Object=} opt_args override for the default values of the soy params. + */ +registry.registrar.ConsoleTestUtil.renderConsoleMain = function( + element, opt_args) { + const args = opt_args || {}; + goog.soy.renderElement(element, registry.soy.registrar.console.main, { + xsrfToken: args.xsrfToken || 'ignore', + username: args.username || 'jart', + logoutUrl: args.logoutUrl || 'https://logout.url.com', + isAdmin: goog.isDefAndNotNull(args.isAdmin) ? args.isAdmin : true, + clientId: args.clientId || 'ignore', + logoFilename: args.logoFilename || 'logo.png', + productName: args.productName || 'Nomulus', + integrationEmail: args.integrationEmail || 'integration@example.com', + supportEmail: args.supportEmail || 'support@example.com', + announcementsEmail: args.announcementsEmail || 'announcement@example.com', + supportPhoneNumber: args.supportPhoneNumber || '+1 (888) 555 0123', + technicalDocsUrl: args.technicalDocsUrl || 'http://example.com/techdocs', + }); +}; + /** * Simulates visiting a page on the console. Sets path, then mocks * session and calls `handleHashChange_`. - * @param {Object} test the test case to configure. - * @param {Object=} opt_args may include path, isEppLoggedIn. - * @param {Function=} opt_moar extra setup after called just before + * @param {!Object} test the test case to configure. + * @param {?Object=} opt_args may include path, isEppLoggedIn. + * @param {?Function=} opt_moar extra setup after called just before * `$replayAll`. See memegen/3437690. */ registry.registrar.ConsoleTestUtil.visit = function( @@ -72,7 +102,7 @@ registry.registrar.ConsoleTestUtil.visit = function( goog.testing.mockmatchers.isFunction) .$does(function(args, cb) { // XXX: Args should be checked. - var xml = goog.dom.xml.loadXml(opt_args.rspXml); + const xml = goog.dom.xml.loadXml(opt_args.rspXml); goog.asserts.assert(xml != null); cb(registry.xml.convertToJson(xml)); }).$anyTimes(); diff --git a/javatests/google/registry/ui/js/registrar/contact_settings_test.js b/javatests/google/registry/ui/js/registrar/contact_settings_test.js index 330423e62..bf74d59f0 100644 --- a/javatests/google/registry/ui/js/registrar/contact_settings_test.js +++ b/javatests/google/registry/ui/js/registrar/contact_settings_test.js @@ -17,23 +17,21 @@ goog.setTestOnly(); goog.require('goog.array'); goog.require('goog.dispose'); goog.require('goog.dom'); -goog.require('goog.soy'); goog.require('goog.testing.MockControl'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.asserts'); goog.require('goog.testing.jsunit'); goog.require('goog.testing.net.XhrIo'); goog.require('registry.registrar.ConsoleTestUtil'); -goog.require('registry.soy.registrar.console'); goog.require('registry.testing'); goog.require('registry.util'); -var $ = goog.dom.getRequiredElement; -var stubs = new goog.testing.PropertyReplacer(); -var testContact = null; +const $ = goog.dom.getRequiredElement; +const stubs = new goog.testing.PropertyReplacer(); +let testContact = null; -var test = { +const test = { testXsrfToken: '༼༎෴ ༎༽', testClientId: 'testClientId', mockControl: new goog.testing.MockControl() @@ -44,19 +42,9 @@ function setUp() { registry.testing.addToDocument('
'); registry.testing.addToDocument('
'); testContact = createTestContact(); - goog.soy.renderElement($('test'), registry.soy.registrar.console.main, { + registry.registrar.ConsoleTestUtil.renderConsoleMain($('test'), { xsrfToken: test.testXsrfToken, - username: 'blah', - logoutUrl: 'omg', - isAdmin: true, clientId: test.testClientId, - logoFilename: 'logo.png', - productName: 'Nomulus', - integrationEmail: 'integration@google.com', - supportEmail: 'support@google.com', - announcementsEmail: 'announcements@google.com', - supportPhoneNumber: '123 456 7890', - technicalDocsUrl: 'http://example.com/techdocs' }); stubs.setPath('goog.net.XhrIo', goog.testing.net.XhrIo); registry.registrar.ConsoleTestUtil.setup(test); @@ -208,7 +196,7 @@ function testOneOfManyUpdate() { xsrfToken: test.testXsrfToken, clientId: test.testClientId }); - var testContacts = [ + const testContacts = [ createTestContact('new1@asdf.com'), testContact, createTestContact('new2@asdf.com') @@ -262,9 +250,9 @@ function testDomainWhoisAbuseContactOverride() { xsrfToken: test.testXsrfToken, clientId: test.testClientId }); - var oldDomainWhoisAbuseContact = createTestContact('old@asdf.com'); + const oldDomainWhoisAbuseContact = createTestContact('old@asdf.com'); oldDomainWhoisAbuseContact.visibleInDomainWhoisAsAbuse = true; - var testContacts = [oldDomainWhoisAbuseContact, testContact]; + const testContacts = [oldDomainWhoisAbuseContact, testContact]; registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', @@ -299,7 +287,7 @@ function testDelete() { xsrfToken: test.testXsrfToken, clientId: test.testClientId }); - var testContacts = [ + const testContacts = [ createTestContact('new1@asdf.com'), testContact, createTestContact('new2@asdf.com') @@ -343,10 +331,10 @@ function testDelete() { /** * @param {string=} opt_email - * @return {Object} + * @return {!Object} */ function createTestContact(opt_email) { - var nameMail = opt_email || 'test@example.com'; + const nameMail = opt_email || 'test@example.com'; return { name: nameMail, emailAddress: nameMail, @@ -363,14 +351,14 @@ function createTestContact(opt_email) { /** * Convert parsed formContact to simulated wire form. * @param {!Element} contact - * @return {Object} + * @return {!Object} */ function simulateJsonForContact(contact) { contact.visibleInWhoisAsAdmin = contact.visibleInWhoisAsAdmin == 'true'; contact.visibleInWhoisAsTech = contact.visibleInWhoisAsTech == 'true'; contact.visibleInDomainWhoisAsAbuse = contact.visibleInDomainWhoisAsAbuse == 'true'; contact.types = ''; - for (var tNdx in contact.type) { + for (const tNdx in contact.type) { if (contact.type[tNdx]) { if (contact.types.length > 0) { contact.types += ','; diff --git a/javatests/google/registry/ui/js/registrar/contact_test.js b/javatests/google/registry/ui/js/registrar/contact_test.js index 62781b12c..a639458d0 100644 --- a/javatests/google/registry/ui/js/registrar/contact_test.js +++ b/javatests/google/registry/ui/js/registrar/contact_test.js @@ -16,18 +16,16 @@ goog.setTestOnly(); goog.require('goog.dispose'); goog.require('goog.dom'); -goog.require('goog.soy'); goog.require('goog.testing.MockControl'); goog.require('goog.testing.asserts'); goog.require('goog.testing.jsunit'); goog.require('registry.registrar.ConsoleTestUtil'); -goog.require('registry.soy.registrar.console'); goog.require('registry.testing'); -var $ = goog.dom.getRequiredElement; +const $ = goog.dom.getRequiredElement; -var test = { +const test = { mockControl: new goog.testing.MockControl() }; @@ -35,20 +33,7 @@ var test = { function setUp() { registry.testing.addToDocument('
'); registry.testing.addToDocument('
'); - goog.soy.renderElement($('test'), registry.soy.registrar.console.main, { - xsrfToken: 'test', - username: 'blah', - logoutUrl: 'omg', - isAdmin: true, - clientId: 'daddy', - logoFilename: 'logo.png', - productName: 'Nomulus', - integrationEmail: 'integration@example.com', - supportEmail: 'support@example.com', - announcementsEmail: 'announcement@example.com', - supportPhoneNumber: '+1 (888) 555 0123', - technicalDocsUrl: 'http://example.com/techdocs' - }); + registry.registrar.ConsoleTestUtil.renderConsoleMain($('test'), {}); registry.registrar.ConsoleTestUtil.setup(test); } @@ -126,7 +111,7 @@ function testEdit() { /** Contact hash path should nav to contact page. */ function testAddPostalInfo() { testEdit(); - var addPiBtn = $('domain-contact-postalInfo-add-button'); + const addPiBtn = $('domain-contact-postalInfo-add-button'); assertNull(addPiBtn.getAttribute('disabled')); registry.testing.click(addPiBtn); assertTrue(addPiBtn.hasAttribute('disabled')); diff --git a/javatests/google/registry/ui/js/registrar/domain_test.js b/javatests/google/registry/ui/js/registrar/domain_test.js index 44e10d6a8..92630b8a8 100644 --- a/javatests/google/registry/ui/js/registrar/domain_test.js +++ b/javatests/google/registry/ui/js/registrar/domain_test.js @@ -17,7 +17,6 @@ goog.setTestOnly(); goog.require('goog.History'); goog.require('goog.dispose'); goog.require('goog.dom'); -goog.require('goog.soy'); goog.require('goog.testing.MockControl'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.asserts'); @@ -25,36 +24,23 @@ goog.require('goog.testing.jsunit'); goog.require('goog.testing.mockmatchers'); goog.require('goog.testing.net.XhrIo'); goog.require('registry.registrar.Console'); -goog.require('registry.soy.registrar.console'); +goog.require('registry.registrar.ConsoleTestUtil'); goog.require('registry.testing'); -var $ = goog.dom.getRequiredElement; -var _ = goog.testing.mockmatchers.ignoreArgument; -var stubs = new goog.testing.PropertyReplacer(); -var mocks = new goog.testing.MockControl(); +const $ = goog.dom.getRequiredElement; +const _ = goog.testing.mockmatchers.ignoreArgument; +const stubs = new goog.testing.PropertyReplacer(); +const mocks = new goog.testing.MockControl(); -var historyMock; -var registrarConsole; +let historyMock; +let registrarConsole; function setUp() { registry.testing.addToDocument('
'); registry.testing.addToDocument('
'); - goog.soy.renderElement($('test'), registry.soy.registrar.console.main, { - xsrfToken: 'ignore', - username: 'jart', - logoutUrl: 'https://justinetunney.com', - isAdmin: true, - clientId: 'ignore', - logoFilename: 'logo.png', - productName: 'Nomulus', - integrationEmail: 'integration@example.com', - supportEmail: 'support@example.com', - announcementsEmail: 'announcement@example.com', - supportPhoneNumber: '+1 (888) 555 0123', - technicalDocsUrl: 'http://example.com/techdocs' - }); + registry.registrar.ConsoleTestUtil.renderConsoleMain($('test'), {}); stubs.setPath('goog.net.XhrIo', goog.testing.net.XhrIo); historyMock = mocks.createStrictMock(goog.History); @@ -81,7 +67,7 @@ function tearDown() { /** Handles EPP login. */ function handleLogin() { - var request = registry.testing.loadXml( + const request = registry.testing.loadXml( '' + '' + ' ' + @@ -101,7 +87,7 @@ function handleLogin() { ' asdf-1235' + ' ' + ''); - var response = registry.testing.loadXml( + const response = registry.testing.loadXml( '' + '' + ' ' + @@ -114,7 +100,7 @@ function handleLogin() { ' ' + ' ' + ''); - var xhr = goog.testing.net.XhrIo.getSendInstances().pop(); + const xhr = goog.testing.net.XhrIo.getSendInstances().pop(); assertTrue(xhr.isActive()); assertEquals('/registrar-xhr', xhr.getLastUri()); assertEquals('☢', xhr.getLastRequestHeaders()['X-CSRF-Token']); @@ -132,7 +118,7 @@ function testView() { registrarConsole.handleHashChange(); handleLogin(); - var request = registry.testing.loadXml( + const request = registry.testing.loadXml( '' + '' + ' ' + @@ -144,7 +130,7 @@ function testView() { ' abc-1234' + ' ' + ''); - var response = registry.testing.loadXml( + const response = registry.testing.loadXml( '' + '' + ' ' + @@ -180,7 +166,7 @@ function testView() { ' ' + ' ' + ''); - var xhr = goog.testing.net.XhrIo.getSendInstances().pop(); + const xhr = goog.testing.net.XhrIo.getSendInstances().pop(); assertTrue('XHR is inactive.', xhr.isActive()); assertEquals('/registrar-xhr', xhr.getLastUri()); assertEquals('☢', xhr.getLastRequestHeaders()['X-CSRF-Token']); @@ -218,7 +204,7 @@ function testEdit() { registry.testing.click($('reg-app-btn-save')); - var request = registry.testing.loadXml( + let request = registry.testing.loadXml( '' + '' + ' ' + @@ -236,7 +222,7 @@ function testEdit() { ' abc-1234' + ' ' + ''); - var response = registry.testing.loadXml( + let response = registry.testing.loadXml( '' + '' + ' ' + @@ -249,7 +235,7 @@ function testEdit() { ' ' + ' ' + ''); - var xhr = goog.testing.net.XhrIo.getSendInstances().pop(); + let xhr = goog.testing.net.XhrIo.getSendInstances().pop(); assertTrue('XHR is inactive.', xhr.isActive()); assertEquals('/registrar-xhr', xhr.getLastUri()); assertEquals('☢', xhr.getLastRequestHeaders()['X-CSRF-Token']); @@ -371,7 +357,7 @@ function testCreate() { registry.testing.click($('reg-app-btn-save')); - var request = registry.testing.loadXml( + let request = registry.testing.loadXml( '' + '' + ' ' + @@ -391,7 +377,7 @@ function testCreate() { ' abc-1234' + ' ' + ''); - var response = registry.testing.loadXml( + let response = registry.testing.loadXml( '' + '' + ' ' + @@ -411,7 +397,7 @@ function testCreate() { ' ' + ' ' + ''); - var xhr = goog.testing.net.XhrIo.getSendInstances().pop(); + let xhr = goog.testing.net.XhrIo.getSendInstances().pop(); assertTrue('XHR is inactive.', xhr.isActive()); assertEquals('/registrar-xhr', xhr.getLastUri()); assertEquals('☢', xhr.getLastRequestHeaders()['X-CSRF-Token']); diff --git a/javatests/google/registry/ui/js/registrar/host_test.js b/javatests/google/registry/ui/js/registrar/host_test.js index 0ed64350e..71fea32c0 100644 --- a/javatests/google/registry/ui/js/registrar/host_test.js +++ b/javatests/google/registry/ui/js/registrar/host_test.js @@ -17,7 +17,6 @@ goog.setTestOnly(); goog.require('goog.History'); goog.require('goog.dispose'); goog.require('goog.dom'); -goog.require('goog.soy'); goog.require('goog.testing.MockControl'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.asserts'); @@ -25,36 +24,23 @@ goog.require('goog.testing.jsunit'); goog.require('goog.testing.mockmatchers'); goog.require('goog.testing.net.XhrIo'); goog.require('registry.registrar.Console'); -goog.require('registry.soy.registrar.console'); +goog.require('registry.registrar.ConsoleTestUtil'); goog.require('registry.testing'); -var $ = goog.dom.getRequiredElement; -var _ = goog.testing.mockmatchers.ignoreArgument; -var stubs = new goog.testing.PropertyReplacer(); -var mocks = new goog.testing.MockControl(); +const $ = goog.dom.getRequiredElement; +const _ = goog.testing.mockmatchers.ignoreArgument; +const stubs = new goog.testing.PropertyReplacer(); +const mocks = new goog.testing.MockControl(); -var historyMock; -var registrarConsole; +let historyMock; +let registrarConsole; function setUp() { registry.testing.addToDocument('
'); registry.testing.addToDocument('
'); - goog.soy.renderElement($('test'), registry.soy.registrar.console.main, { - xsrfToken: 'ignore', - username: 'jart', - logoutUrl: 'https://example.com', - isAdmin: true, - clientId: 'ignore', - logoFilename: 'logo.png', - productName: 'Nomulus', - integrationEmail: 'integration@example.com', - supportEmail: 'support@example.com', - announcementsEmail: 'announcement@example.com', - supportPhoneNumber: '+1 (888) 555 0123', - technicalDocsUrl: 'http://example.com/techdocs' - }); + registry.registrar.ConsoleTestUtil.renderConsoleMain($('test'), {}); stubs.setPath('goog.net.XhrIo', goog.testing.net.XhrIo); historyMock = mocks.createStrictMock(goog.History); @@ -81,7 +67,7 @@ function tearDown() { /** Handles EPP login. */ function handleLogin() { - var request = registry.testing.loadXml( + const request = registry.testing.loadXml( '' + '' + ' ' + @@ -101,7 +87,7 @@ function handleLogin() { ' asdf-1235' + ' ' + ''); - var response = registry.testing.loadXml( + const response = registry.testing.loadXml( '' + '' + ' ' + @@ -114,7 +100,7 @@ function handleLogin() { ' ' + ' ' + ''); - var xhr = goog.testing.net.XhrIo.getSendInstances().pop(); + const xhr = goog.testing.net.XhrIo.getSendInstances().pop(); assertTrue(xhr.isActive()); assertEquals('/registrar-xhr', xhr.getLastUri()); assertEquals('☢', xhr.getLastRequestHeaders()['X-CSRF-Token']); @@ -132,7 +118,7 @@ function testView() { registrarConsole.handleHashChange(); handleLogin(); - var request = registry.testing.loadXml( + const request = registry.testing.loadXml( '' + '' + ' ' + @@ -144,7 +130,7 @@ function testView() { ' abc-1234' + ' ' + ''); - var response = registry.testing.loadXml( + const response = registry.testing.loadXml( '' + '' + @@ -170,7 +156,7 @@ function testView() { ' ' + ' ' + ''); - var xhr = goog.testing.net.XhrIo.getSendInstances().pop(); + const xhr = goog.testing.net.XhrIo.getSendInstances().pop(); assertTrue('XHR is inactive.', xhr.isActive()); assertEquals('/registrar-xhr', xhr.getLastUri()); assertEquals('application/epp+xml', @@ -207,7 +193,7 @@ function testEditFirstAddr_ignoreSecond_addThird() { registry.testing.click($('reg-app-btn-save')); - var request = registry.testing.loadXml( + let request = registry.testing.loadXml( '' + '' + ' ' + @@ -226,7 +212,7 @@ function testEditFirstAddr_ignoreSecond_addThird() { ' abc-1234' + ' ' + ''); - var response = registry.testing.loadXml( + let response = registry.testing.loadXml( '' + '' + ' ' + @@ -239,7 +225,7 @@ function testEditFirstAddr_ignoreSecond_addThird() { ' ' + ' ' + ''); - var xhr = goog.testing.net.XhrIo.getSendInstances().pop(); + let xhr = goog.testing.net.XhrIo.getSendInstances().pop(); assertTrue('XHR is inactive.', xhr.isActive()); assertEquals('/registrar-xhr', xhr.getLastUri()); assertEquals('☢', xhr.getLastRequestHeaders()['X-CSRF-Token']); @@ -327,7 +313,7 @@ function testCreate() { registry.testing.click($('reg-app-btn-save')); - var request = registry.testing.loadXml( + let request = registry.testing.loadXml( '' + '' + ' ' + @@ -342,7 +328,7 @@ function testCreate() { ' abc-1234' + ' ' + ''); - var response = registry.testing.loadXml( + let response = registry.testing.loadXml( '' + '' + ' ' + @@ -361,7 +347,7 @@ function testCreate() { ' ' + ' ' + ''); - var xhr = goog.testing.net.XhrIo.getSendInstances().pop(); + let xhr = goog.testing.net.XhrIo.getSendInstances().pop(); assertTrue('XHR is inactive.', xhr.isActive()); assertEquals('/registrar-xhr', xhr.getLastUri()); assertEquals('☢', xhr.getLastRequestHeaders()['X-CSRF-Token']); diff --git a/javatests/google/registry/ui/js/registrar/security_settings_test.js b/javatests/google/registry/ui/js/registrar/security_settings_test.js index 972ddca0c..4e7aaafc1 100644 --- a/javatests/google/registry/ui/js/registrar/security_settings_test.js +++ b/javatests/google/registry/ui/js/registrar/security_settings_test.js @@ -16,22 +16,20 @@ goog.setTestOnly(); goog.require('goog.dispose'); goog.require('goog.dom'); -goog.require('goog.soy'); goog.require('goog.testing.MockControl'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.asserts'); goog.require('goog.testing.jsunit'); goog.require('goog.testing.net.XhrIo'); goog.require('registry.registrar.ConsoleTestUtil'); -goog.require('registry.soy.registrar.console'); goog.require('registry.testing'); goog.require('registry.util'); -var $ = goog.dom.getRequiredElement; -var stubs = new goog.testing.PropertyReplacer(); +const $ = goog.dom.getRequiredElement; +const stubs = new goog.testing.PropertyReplacer(); -var expectedRegistrar = { +const expectedRegistrar = { ipAddressWhitelist: [], phonePasscode: '12345', clientCertificate: null, @@ -39,7 +37,7 @@ var expectedRegistrar = { failoverClientCertificate: null }; -var test = { +const test = { testXsrfToken: '༼༎෴ ༎༽', testClientId: 'testClientId', mockControl: new goog.testing.MockControl() @@ -49,19 +47,9 @@ var test = { function setUp() { registry.testing.addToDocument('
'); registry.testing.addToDocument('
'); - goog.soy.renderElement($('test'), registry.soy.registrar.console.main, { - username: 'jart', - logoutUrl: 'https://example.com', - isAdmin: true, + registry.registrar.ConsoleTestUtil.renderConsoleMain($('test'), { xsrfToken: test.testXsrfToken, clientId: test.testClientId, - logoFilename: 'logo.png', - productName: 'Nomulus', - integrationEmail: 'integration@example.com', - supportEmail: 'support@example.com', - announcementsEmail: 'announcement@example.com', - supportPhoneNumber: '+1 (888) 555 0123', - technicalDocsUrl: 'http://example.com/techdocs' }); stubs.setPath('goog.net.XhrIo', goog.testing.net.XhrIo); registry.registrar.ConsoleTestUtil.setup(test); @@ -101,14 +89,14 @@ function testEdit() { registry.testing.click($('reg-app-btn-edit')); - var form = document.forms.namedItem('item'); + const form = document.forms.namedItem('item'); form.elements['newIp'].value = '1.1.1.1'; registry.testing.click($('btn-add-ip')); form.elements['newIp'].value = '2.2.2.2'; registry.testing.click($('btn-add-ip')); - var exampleCert = $('exampleCert').value; - var exampleCertHash = '6NKKNBnd2fKFooBINmn3V7L3JOTHh02+2lAqYHdlTgk'; + const exampleCert = $('exampleCert').value; + const exampleCertHash = '6NKKNBnd2fKFooBINmn3V7L3JOTHh02+2lAqYHdlTgk'; form.elements['clientCertificate'].value = exampleCert; form.elements['failoverClientCertificate'].value = 'bourgeois blues'; registry.testing.click($('reg-app-btn-save')); diff --git a/javatests/google/registry/ui/js/registrar/whois_settings_test.js b/javatests/google/registry/ui/js/registrar/whois_settings_test.js index c3c5db219..1891ccf13 100644 --- a/javatests/google/registry/ui/js/registrar/whois_settings_test.js +++ b/javatests/google/registry/ui/js/registrar/whois_settings_test.js @@ -17,23 +17,21 @@ goog.setTestOnly(); goog.require('goog.dispose'); goog.require('goog.dom'); goog.require('goog.dom.classlist'); -goog.require('goog.soy'); goog.require('goog.testing.MockControl'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.asserts'); goog.require('goog.testing.jsunit'); goog.require('goog.testing.net.XhrIo'); goog.require('registry.registrar.ConsoleTestUtil'); -goog.require('registry.soy.registrar.console'); goog.require('registry.testing'); goog.require('registry.util'); -var $ = goog.dom.getRequiredElement; -var $$ = goog.dom.getRequiredElementByClass; -var stubs = new goog.testing.PropertyReplacer(); +const $ = goog.dom.getRequiredElement; +const $$ = goog.dom.getRequiredElementByClass; +const stubs = new goog.testing.PropertyReplacer(); -var test = { +const test = { testXsrfToken: '༼༎෴ ༎༽', testClientId: 'testClientId', mockControl: new goog.testing.MockControl() @@ -43,19 +41,9 @@ var test = { function setUp() { registry.testing.addToDocument('
'); registry.testing.addToDocument('
'); - goog.soy.renderElement($('test'), registry.soy.registrar.console.main, { + registry.registrar.ConsoleTestUtil.renderConsoleMain($('test'), { xsrfToken: test.testXsrfToken, - username: 'blah', - logoutUrl: 'omg', - isAdmin: true, clientId: test.testClientId, - logoFilename: 'logo.png', - productName: 'Nomulus', - integrationEmail: 'integration@example.com', - supportEmail: 'support@example.com', - announcementsEmail: 'announcement@example.com', - supportPhoneNumber: '+1 (888) 555 0123', - technicalDocsUrl: 'http://example.com/techdocs' }); stubs.setPath('goog.net.XhrIo', goog.testing.net.XhrIo); registry.registrar.ConsoleTestUtil.setup(test); @@ -72,7 +60,7 @@ function tearDown() { /** * Creates test registrar. - * @return {Object} + * @return {!Object} */ function createTestRegistrar() { return { @@ -100,7 +88,7 @@ function testView() { xsrfToken: test.testXsrfToken, clientId: test.testClientId }); - var testRegistrar = createTestRegistrar(); + const testRegistrar = createTestRegistrar(); registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', @@ -110,7 +98,7 @@ function testView() { message: 'OK', results: [testRegistrar] }); - var parsed = registry.util.parseForm('item'); + const parsed = registry.util.parseForm('item'); parsed.ianaIdentifier = parseInt(parsed.ianaIdentifier); registry.testing.assertObjectEqualsPretty(testRegistrar, parsed); } @@ -123,7 +111,7 @@ function testEdit() { $('localizedAddress.street[0]').value = 'look at me i am'; $('localizedAddress.street[1]').value = 'the mistress of the night'; $('localizedAddress.street[2]').value = ''; - var parsed = registry.util.parseForm('item'); + const parsed = registry.util.parseForm('item'); parsed.readonly = false; registry.testing.click($('reg-app-btn-save')); registry.testing.assertReqMockRsp( @@ -142,10 +130,10 @@ function testEditFieldError_insertsError() { testView(); registry.testing.click($('reg-app-btn-edit')); $('phoneNumber').value = 'foo'; - var parsed = registry.util.parseForm('item'); + const parsed = registry.util.parseForm('item'); parsed.readonly = false; registry.testing.click($('reg-app-btn-save')); - var errMsg = 'Carpe brunchus. --Pablo'; + const errMsg = 'Carpe brunchus. --Pablo'; registry.testing.assertReqMockRsp( test.testXsrfToken, '/registrar-settings', @@ -155,7 +143,7 @@ function testEditFieldError_insertsError() { field: 'phoneNumber', message: errMsg }); - var msgBox = goog.dom.getNextElementSibling($('phoneNumber')); + const msgBox = goog.dom.getNextElementSibling($('phoneNumber')); assertTrue(goog.dom.classlist.contains(msgBox, 'kd-errormessage')); assertTrue(goog.dom.classlist.contains($('phoneNumber'), 'kd-formerror')); assertEquals(errMsg, goog.dom.getTextContent(msgBox)); @@ -165,10 +153,10 @@ function testEditFieldError_insertsError() { function testEditNonFieldError_showsButterBar() { testView(); registry.testing.click($('reg-app-btn-edit')); - var parsed = registry.util.parseForm('item'); + const parsed = registry.util.parseForm('item'); parsed.readonly = false; registry.testing.click($('reg-app-btn-save')); - var errMsg = 'One must still have chaos in oneself to be able to give ' + + const errMsg = 'One must still have chaos in oneself to be able to give ' + 'birth to a dancing star. --Nietzsche'; registry.testing.assertReqMockRsp( test.testXsrfToken, From bec7a91cfc90a97ea606841b630b340edad6d7fd Mon Sep 17 00:00:00 2001 From: mcilwain Date: Fri, 12 Oct 2018 11:55:23 -0700 Subject: [PATCH 24/60] Allow choice of Keyring to be configured in YAML This uses a Dagger-provided map of Keyring implementations, with two currently available, "KMS" and "Dummy". The active keyring is configured in the YAML file, so we no longer require MOE directives to choose which one to use for internal/external builds. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=216898058 --- .../registry/config/RegistryConfig.java | 10 +++++++-- .../config/RegistryConfigSettings.java | 20 +++++++++++------- .../registry/config/files/default-config.yaml | 19 ++++++++++------- .../nomulus-config-production-sample.yaml | 6 ++++-- java/google/registry/keyring/BUILD | 21 +++++++++++++++++++ .../keyring/{kms => }/KeyringModule.java | 18 ++++++++++++---- .../keyring/api/DummyKeyringModule.java | 18 ++++++++++++++-- .../registry/keyring/kms/KmsModule.java | 12 ++++++++++- java/google/registry/module/backend/BUILD | 1 + .../module/backend/BackendComponent.java | 5 ++++- java/google/registry/module/frontend/BUILD | 1 + .../module/frontend/FrontendComponent.java | 5 ++++- java/google/registry/module/pubapi/BUILD | 1 + .../module/pubapi/PubApiComponent.java | 5 ++++- java/google/registry/module/tools/BUILD | 1 + .../registry/module/tools/ToolsComponent.java | 5 ++++- java/google/registry/tools/BUILD | 1 + .../registry/tools/RegistryToolComponent.java | 5 ++++- 18 files changed, 124 insertions(+), 30 deletions(-) create mode 100644 java/google/registry/keyring/BUILD rename java/google/registry/keyring/{kms => }/KeyringModule.java (58%) diff --git a/java/google/registry/config/RegistryConfig.java b/java/google/registry/config/RegistryConfig.java index 1ad40e6ef..0fa174519 100644 --- a/java/google/registry/config/RegistryConfig.java +++ b/java/google/registry/config/RegistryConfig.java @@ -1021,6 +1021,12 @@ public final class RegistryConfig { return config.registryPolicy.greetingServerId; } + @Provides + @Config("activeKeyring") + public static String provideKeyring(RegistryConfigSettings config) { + return config.keyring.activeKeyring; + } + /** * The name to use for the Cloud KMS KeyRing containing encryption keys for Nomulus secrets. * @@ -1030,13 +1036,13 @@ public final class RegistryConfig { @Provides @Config("cloudKmsKeyRing") public static String provideCloudKmsKeyRing(RegistryConfigSettings config) { - return config.kms.keyringName; + return config.keyring.kms.keyringName; } @Provides @Config("cloudKmsProjectId") public static String provideCloudKmsProjectId(RegistryConfigSettings config) { - return config.kms.projectId; + return config.keyring.kms.projectId; } @Provides diff --git a/java/google/registry/config/RegistryConfigSettings.java b/java/google/registry/config/RegistryConfigSettings.java index 6f4d73465..15118679a 100644 --- a/java/google/registry/config/RegistryConfigSettings.java +++ b/java/google/registry/config/RegistryConfigSettings.java @@ -34,7 +34,7 @@ public class RegistryConfigSettings { public Monitoring monitoring; public Misc misc; public Beam beam; - public Kms kms; + public Keyring keyring; public RegistryTool registryTool; /** Configuration options that apply to the entire App Engine project. */ @@ -99,12 +99,6 @@ public class RegistryConfigSettings { public int baseOfyRetryMillis; } - /** Configuration for Cloud KMS. */ - public static class Kms { - public String keyringName; - public String projectId; - } - /** Configuration for Apache Beam (Cloud Dataflow). */ public static class Beam { public String defaultJobZone; @@ -170,6 +164,18 @@ public class RegistryConfigSettings { public int asyncDeleteDelaySeconds; } + /** Configuration for keyrings (used to store secrets outside of source). */ + public static class Keyring { + public String activeKeyring; + public Kms kms; + } + + /** Configuration for Cloud KMS. */ + public static class Kms { + public String keyringName; + public String projectId; + } + /** Configuration options for the registry tool. */ public static class RegistryTool { public String clientSecretFilename; diff --git a/java/google/registry/config/files/default-config.yaml b/java/google/registry/config/files/default-config.yaml index 8c51b0cfc..912353b48 100644 --- a/java/google/registry/config/files/default-config.yaml +++ b/java/google/registry/config/files/default-config.yaml @@ -322,14 +322,19 @@ beam: # The default zone to run Apache Beam (Cloud Dataflow) jobs in. defaultJobZone: us-east1-c -kms: - # GCP project containing the KMS keyring. Should only be used for KMS in - # order to keep a simple locked down IAM configuration. - projectId: registry-kms-project-id +keyring: + # The name of the active keyring, either "KMS" or "Dummy". + activeKeyring: Dummy - # The name to use for the Cloud KMS KeyRing which will store encryption keys - # for Nomulus secrets. - keyringName: nomulus + # Configuration options specific to Google Cloud KMS. + kms: + # GCP project containing the KMS keyring. Should only be used for KMS in + # order to keep a simple locked down IAM configuration. + projectId: registry-kms-project-id + + # The name to use for the Cloud KMS KeyRing which will store encryption keys + # for Nomulus secrets. + keyringName: nomulus # Configuration options relevant to the "nomulus" registry tool. registryTool: diff --git a/java/google/registry/config/files/nomulus-config-production-sample.yaml b/java/google/registry/config/files/nomulus-config-production-sample.yaml index 4a891e1d9..01a051f7c 100644 --- a/java/google/registry/config/files/nomulus-config-production-sample.yaml +++ b/java/google/registry/config/files/nomulus-config-production-sample.yaml @@ -61,5 +61,7 @@ cloudDns: rootUrl: null servicePath: null -kms: - projectId: placeholder +keyring: + activeKeyring: KMS + kms: + projectId: placeholder diff --git a/java/google/registry/keyring/BUILD b/java/google/registry/keyring/BUILD new file mode 100644 index 000000000..ec999ce7c --- /dev/null +++ b/java/google/registry/keyring/BUILD @@ -0,0 +1,21 @@ +package( + default_visibility = ["//visibility:public"], +) + +licenses(["notice"]) # Apache 2.0 + +java_library( + name = "keyring", + srcs = glob(["*.java"]), + deps = [ + "//java/google/registry/config", + "//java/google/registry/keyring/api", + "@com_google_code_findbugs_jsr305", + "@com_google_dagger", + "@com_google_flogger", + "@com_google_flogger_system_backend", + "@com_google_guava", + "@javax_inject", + "@org_bouncycastle_bcpg_jdk15on", + ], +) diff --git a/java/google/registry/keyring/kms/KeyringModule.java b/java/google/registry/keyring/KeyringModule.java similarity index 58% rename from java/google/registry/keyring/kms/KeyringModule.java rename to java/google/registry/keyring/KeyringModule.java index 7a9b53f80..28c525900 100644 --- a/java/google/registry/keyring/kms/KeyringModule.java +++ b/java/google/registry/keyring/KeyringModule.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// 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. @@ -12,11 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package google.registry.keyring.kms; +package google.registry.keyring; + +import static com.google.common.base.Preconditions.checkState; import dagger.Module; import dagger.Provides; +import google.registry.config.RegistryConfig.Config; import google.registry.keyring.api.Keyring; +import java.util.Map; import javax.inject.Singleton; /** Dagger module for {@link Keyring} */ @@ -25,7 +29,13 @@ public final class KeyringModule { @Provides @Singleton - public static Keyring provideKeyring(KmsKeyring kmsKeyring) { - return kmsKeyring; + public static Keyring provideKeyring( + Map keyrings, @Config("activeKeyring") String activeKeyring) { + checkState( + keyrings.containsKey(activeKeyring), + "Invalid Keyring %s is configured; valid choices are %s", + activeKeyring, + keyrings.keySet()); + return keyrings.get(activeKeyring); } } diff --git a/java/google/registry/keyring/api/DummyKeyringModule.java b/java/google/registry/keyring/api/DummyKeyringModule.java index 9e35c14fe..9e2f2aec3 100644 --- a/java/google/registry/keyring/api/DummyKeyringModule.java +++ b/java/google/registry/keyring/api/DummyKeyringModule.java @@ -21,11 +21,15 @@ import static google.registry.keyring.api.PgpHelper.lookupKeyPair; import com.google.common.base.VerifyException; import com.google.common.io.ByteSource; import com.google.common.io.Resources; +import dagger.Binds; import dagger.Module; import dagger.Provides; +import dagger.multibindings.IntoMap; +import dagger.multibindings.StringKey; import java.io.IOException; import java.io.InputStream; import javax.annotation.concurrent.Immutable; +import javax.inject.Named; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; @@ -68,7 +72,9 @@ import org.bouncycastle.openpgp.bc.BcPGPSecretKeyRingCollection; */ @Module @Immutable -public final class DummyKeyringModule { +public abstract class DummyKeyringModule { + + public static final String NAME = "Dummy"; /** The contents of a dummy PGP public key stored in a file. */ private static final ByteSource PGP_PUBLIC_KEYRING = @@ -81,9 +87,15 @@ public final class DummyKeyringModule { /** The email address of the aforementioned PGP key. */ private static final String EMAIL_ADDRESS = "test-registry@example.com"; + @Binds + @IntoMap + @StringKey(NAME) + abstract Keyring provideKeyring(@Named("DummyKeyring") InMemoryKeyring keyring); + /** Always returns a {@link InMemoryKeyring} instance. */ @Provides - static Keyring provideKeyring() { + @Named("DummyKeyring") + static InMemoryKeyring provideDummyKeyring() { PGPKeyPair dummyKey; try (InputStream publicInput = PGP_PUBLIC_KEYRING.openStream(); InputStream privateInput = PGP_PRIVATE_KEYRING.openStream()) { @@ -112,4 +124,6 @@ public final class DummyKeyringModule { "not a real login", "not a real credential"); } + + private DummyKeyringModule() {} } diff --git a/java/google/registry/keyring/kms/KmsModule.java b/java/google/registry/keyring/kms/KmsModule.java index 1b63fff76..1c96ca50e 100644 --- a/java/google/registry/keyring/kms/KmsModule.java +++ b/java/google/registry/keyring/kms/KmsModule.java @@ -19,13 +19,23 @@ import com.google.api.services.cloudkms.v1.CloudKMS; import dagger.Binds; import dagger.Module; import dagger.Provides; +import dagger.multibindings.IntoMap; +import dagger.multibindings.StringKey; import google.registry.config.CredentialModule.DefaultCredential; import google.registry.config.RegistryConfig.Config; +import google.registry.keyring.api.Keyring; -/** Dagger module for Cloud KMS connection objects. */ +/** Dagger module for Cloud KMS. */ @Module public abstract class KmsModule { + public static final String NAME = "KMS"; + + @Binds + @IntoMap + @StringKey(NAME) + abstract Keyring provideKeyring(KmsKeyring keyring); + @Provides static CloudKMS provideKms( @DefaultCredential GoogleCredential credential, diff --git a/java/google/registry/module/backend/BUILD b/java/google/registry/module/backend/BUILD index d4527f2ea..75aa46b18 100644 --- a/java/google/registry/module/backend/BUILD +++ b/java/google/registry/module/backend/BUILD @@ -22,6 +22,7 @@ java_library( "//java/google/registry/flows", "//java/google/registry/gcs", "//java/google/registry/groups", + "//java/google/registry/keyring", "//java/google/registry/keyring/api", "//java/google/registry/keyring/kms", "//java/google/registry/mapreduce", diff --git a/java/google/registry/module/backend/BackendComponent.java b/java/google/registry/module/backend/BackendComponent.java index 9ef32033a..9844eee40 100644 --- a/java/google/registry/module/backend/BackendComponent.java +++ b/java/google/registry/module/backend/BackendComponent.java @@ -27,6 +27,8 @@ import google.registry.gcs.GcsServiceModule; import google.registry.groups.DirectoryModule; import google.registry.groups.GroupsModule; import google.registry.groups.GroupssettingsModule; +import google.registry.keyring.KeyringModule; +import google.registry.keyring.api.DummyKeyringModule; import google.registry.keyring.api.KeyModule; import google.registry.keyring.kms.KmsModule; import google.registry.module.backend.BackendRequestComponent.BackendRequestComponentModule; @@ -56,7 +58,7 @@ import javax.inject.Singleton; CredentialModule.class, DatastoreServiceModule.class, DirectoryModule.class, - google.registry.keyring.api.DummyKeyringModule.class, + DummyKeyringModule.class, DriveModule.class, GcsServiceModule.class, GroupsModule.class, @@ -64,6 +66,7 @@ import javax.inject.Singleton; JSchModule.class, Jackson2Module.class, KeyModule.class, + KeyringModule.class, KmsModule.class, NetHttpTransportModule.class, SheetsServiceModule.class, diff --git a/java/google/registry/module/frontend/BUILD b/java/google/registry/module/frontend/BUILD index 1f99a1b17..589a82287 100644 --- a/java/google/registry/module/frontend/BUILD +++ b/java/google/registry/module/frontend/BUILD @@ -11,6 +11,7 @@ java_library( "//java/google/registry/config", "//java/google/registry/dns", "//java/google/registry/flows", + "//java/google/registry/keyring", "//java/google/registry/keyring/api", "//java/google/registry/keyring/kms", "//java/google/registry/monitoring/whitebox", diff --git a/java/google/registry/module/frontend/FrontendComponent.java b/java/google/registry/module/frontend/FrontendComponent.java index ee2e04792..1c96edc5b 100644 --- a/java/google/registry/module/frontend/FrontendComponent.java +++ b/java/google/registry/module/frontend/FrontendComponent.java @@ -21,6 +21,8 @@ import google.registry.config.CredentialModule; import google.registry.config.RegistryConfig.ConfigModule; import google.registry.flows.ServerTridProviderModule; import google.registry.flows.custom.CustomLogicFactoryModule; +import google.registry.keyring.KeyringModule; +import google.registry.keyring.api.DummyKeyringModule; import google.registry.keyring.api.KeyModule; import google.registry.keyring.kms.KmsModule; import google.registry.module.frontend.FrontendRequestComponent.FrontendRequestComponentModule; @@ -46,10 +48,11 @@ import javax.inject.Singleton; ConsoleConfigModule.class, CredentialModule.class, CustomLogicFactoryModule.class, - google.registry.keyring.api.DummyKeyringModule.class, + DummyKeyringModule.class, FrontendRequestComponentModule.class, Jackson2Module.class, KeyModule.class, + KeyringModule.class, KmsModule.class, NetHttpTransportModule.class, ServerTridProviderModule.class, diff --git a/java/google/registry/module/pubapi/BUILD b/java/google/registry/module/pubapi/BUILD index 9af5ff86d..e3d388311 100644 --- a/java/google/registry/module/pubapi/BUILD +++ b/java/google/registry/module/pubapi/BUILD @@ -11,6 +11,7 @@ java_library( "//java/google/registry/config", "//java/google/registry/dns", "//java/google/registry/flows", + "//java/google/registry/keyring", "//java/google/registry/keyring/api", "//java/google/registry/keyring/kms", "//java/google/registry/monitoring/whitebox", diff --git a/java/google/registry/module/pubapi/PubApiComponent.java b/java/google/registry/module/pubapi/PubApiComponent.java index 383ba559c..ef5ffbc65 100644 --- a/java/google/registry/module/pubapi/PubApiComponent.java +++ b/java/google/registry/module/pubapi/PubApiComponent.java @@ -21,6 +21,8 @@ import google.registry.config.CredentialModule; import google.registry.config.RegistryConfig.ConfigModule; import google.registry.flows.ServerTridProviderModule; import google.registry.flows.custom.CustomLogicFactoryModule; +import google.registry.keyring.KeyringModule; +import google.registry.keyring.api.DummyKeyringModule; import google.registry.keyring.api.KeyModule; import google.registry.keyring.kms.KmsModule; import google.registry.module.pubapi.PubApiRequestComponent.PubApiRequestComponentModule; @@ -44,10 +46,11 @@ import javax.inject.Singleton; ConfigModule.class, CredentialModule.class, CustomLogicFactoryModule.class, - google.registry.keyring.api.DummyKeyringModule.class, + DummyKeyringModule.class, PubApiRequestComponentModule.class, Jackson2Module.class, KeyModule.class, + KeyringModule.class, KmsModule.class, NetHttpTransportModule.class, ServerTridProviderModule.class, diff --git a/java/google/registry/module/tools/BUILD b/java/google/registry/module/tools/BUILD index a76e62b89..a46e09e90 100644 --- a/java/google/registry/module/tools/BUILD +++ b/java/google/registry/module/tools/BUILD @@ -15,6 +15,7 @@ java_library( "//java/google/registry/flows", "//java/google/registry/gcs", "//java/google/registry/groups", + "//java/google/registry/keyring", "//java/google/registry/keyring/api", "//java/google/registry/keyring/kms", "//java/google/registry/loadtest", diff --git a/java/google/registry/module/tools/ToolsComponent.java b/java/google/registry/module/tools/ToolsComponent.java index bcf3b296b..605ddc95a 100644 --- a/java/google/registry/module/tools/ToolsComponent.java +++ b/java/google/registry/module/tools/ToolsComponent.java @@ -24,6 +24,8 @@ import google.registry.gcs.GcsServiceModule; import google.registry.groups.DirectoryModule; import google.registry.groups.GroupsModule; import google.registry.groups.GroupssettingsModule; +import google.registry.keyring.KeyringModule; +import google.registry.keyring.api.DummyKeyringModule; import google.registry.keyring.api.KeyModule; import google.registry.keyring.kms.KmsModule; import google.registry.module.tools.ToolsRequestComponent.ToolsRequestComponentModule; @@ -49,13 +51,14 @@ import javax.inject.Singleton; CustomLogicFactoryModule.class, DatastoreServiceModule.class, DirectoryModule.class, - google.registry.keyring.api.DummyKeyringModule.class, + DummyKeyringModule.class, DriveModule.class, GcsServiceModule.class, GroupsModule.class, GroupssettingsModule.class, Jackson2Module.class, KeyModule.class, + KeyringModule.class, KmsModule.class, NetHttpTransportModule.class, ServerTridProviderModule.class, diff --git a/java/google/registry/tools/BUILD b/java/google/registry/tools/BUILD index 90e9fb3dc..d4671b9b9 100644 --- a/java/google/registry/tools/BUILD +++ b/java/google/registry/tools/BUILD @@ -46,6 +46,7 @@ java_library( "//java/google/registry/export", "//java/google/registry/flows", "//java/google/registry/gcs", + "//java/google/registry/keyring", "//java/google/registry/keyring/api", "//java/google/registry/keyring/kms", "//java/google/registry/loadtest", diff --git a/java/google/registry/tools/RegistryToolComponent.java b/java/google/registry/tools/RegistryToolComponent.java index 6dbae7c08..42d65f523 100644 --- a/java/google/registry/tools/RegistryToolComponent.java +++ b/java/google/registry/tools/RegistryToolComponent.java @@ -21,6 +21,8 @@ import google.registry.config.RegistryConfig.ConfigModule; import google.registry.dns.writer.VoidDnsWriterModule; import google.registry.dns.writer.clouddns.CloudDnsWriterModule; import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; +import google.registry.keyring.KeyringModule; +import google.registry.keyring.api.DummyKeyringModule; import google.registry.keyring.api.KeyModule; import google.registry.keyring.kms.KmsModule; import google.registry.rde.RdeModule; @@ -52,13 +54,14 @@ import javax.inject.Singleton; ConfigModule.class, CredentialModule.class, DatastoreServiceModule.class, - google.registry.keyring.api.DummyKeyringModule.class, + DummyKeyringModule.class, CloudDnsWriterModule.class, DefaultRequestFactoryModule.class, DefaultRequestFactoryModule.RequestFactoryModule.class, DnsUpdateWriterModule.class, Jackson2Module.class, KeyModule.class, + KeyringModule.class, KmsModule.class, RdeModule.class, RegistryToolModule.class, From 8d93cd8edf371748ac320033b8aec7a78e181362 Mon Sep 17 00:00:00 2001 From: guyben Date: Fri, 12 Oct 2018 19:54:53 -0700 Subject: [PATCH 25/60] Refactor SessionUtil, and Add dropdown menu to switch clientId SessionUtil is renames AuthenticatedRegistrarAccessor, as it's used to access a registrar for an authenticated user. It will now be injected with the AuthResult instead of receiving it in every function call, since there's only one "legal" AuthResult to use. The AccessType names are changed from READ_ONLY/READ_WRITE to READ/UPDATE, as it was confusing that a user could have both READ_ONLY AND READ_WRITE access to the same registrar. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=216958306 --- java/google/registry/rdap/RdapActionBase.java | 16 +- .../AuthenticatedRegistrarAccessor.java | 234 ++++++++++++++++++ .../ui/server/registrar/ConsoleUiAction.java | 19 +- .../registrar/RegistrarSettingsAction.java | 20 +- .../ui/server/registrar/SessionUtils.java | 199 --------------- .../registry/ui/soy/registrar/Console.soy | 34 +++ .../registry/rdap/RdapActionBaseTest.java | 7 +- .../registry/rdap/RdapDomainActionTest.java | 11 +- .../rdap/RdapDomainSearchActionTest.java | 11 +- .../registry/rdap/RdapEntityActionTest.java | 11 +- .../rdap/RdapEntitySearchActionTest.java | 11 +- .../registry/rdap/RdapHelpActionTest.java | 7 +- .../rdap/RdapNameserverActionTest.java | 19 +- .../rdap/RdapNameserverSearchActionTest.java | 11 +- .../ui/js/registrar/console_test_util.js | 2 + ...> AuthenticatedRegistrarAccessorTest.java} | 203 +++++++++------ .../server/registrar/ConsoleUiActionTest.java | 39 ++- .../RegistrarSettingsActionTest.java | 8 +- .../RegistrarSettingsActionTestCase.java | 65 ++--- 19 files changed, 539 insertions(+), 388 deletions(-) create mode 100644 java/google/registry/ui/server/registrar/AuthenticatedRegistrarAccessor.java delete mode 100644 java/google/registry/ui/server/registrar/SessionUtils.java rename javatests/google/registry/ui/server/registrar/{SessionUtilsTest.java => AuthenticatedRegistrarAccessorTest.java} (58%) diff --git a/java/google/registry/rdap/RdapActionBase.java b/java/google/registry/rdap/RdapActionBase.java index 6fe80ef49..7dfefa84a 100644 --- a/java/google/registry/rdap/RdapActionBase.java +++ b/java/google/registry/rdap/RdapActionBase.java @@ -18,7 +18,7 @@ import static com.google.common.base.Charsets.UTF_8; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; import static google.registry.model.ofy.ObjectifyService.ofy; -import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY; +import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.READ; import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DomainNameUtils.canonicalizeDomainName; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; @@ -48,7 +48,7 @@ import google.registry.request.RequestPath; import google.registry.request.Response; import google.registry.request.auth.AuthResult; import google.registry.request.auth.UserAuthInfo; -import google.registry.ui.server.registrar.SessionUtils; +import google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -91,7 +91,7 @@ public abstract class RdapActionBase implements Runnable { @Inject @RequestPath String requestPath; @Inject @FullServletPath String fullServletPath; @Inject AuthResult authResult; - @Inject SessionUtils sessionUtils; + @Inject AuthenticatedRegistrarAccessor registrarAccessor; @Inject RdapJsonFormatter rdapJsonFormatter; @Inject @Parameter("registrar") Optional registrarParam; @Inject @Parameter("includeDeleted") Optional includeDeletedParam; @@ -177,12 +177,12 @@ public abstract class RdapActionBase implements Runnable { /** * Returns a clientId the given user has console access on, or Optional.empty if there is none. */ - private Optional getAuthorizedClientId(AuthResult authResult) { + private Optional getAuthorizedClientId() { try { - String clientId = sessionUtils.guessClientIdForUser(authResult); + String clientId = registrarAccessor.guessClientId(); // We load the Registrar to make sure the user has access to it. We don't actually need it, // we're just checking if an exception is thrown. - sessionUtils.getRegistrarForUserCached(clientId, READ_ONLY, authResult); + registrarAccessor.getRegistrarForUserCached(clientId, READ); return Optional.of(clientId); } catch (Exception e) { logger.atWarning().withCause(e).log( @@ -215,7 +215,7 @@ public abstract class RdapActionBase implements Runnable { if (userAuthInfo.isUserAdmin()) { return RdapAuthorization.ADMINISTRATOR_AUTHORIZATION; } - Optional clientId = getAuthorizedClientId(authResult); + Optional clientId = getAuthorizedClientId(); if (!clientId.isPresent()) { return RdapAuthorization.PUBLIC_AUTHORIZATION; } @@ -249,7 +249,7 @@ public abstract class RdapActionBase implements Runnable { if (userAuthInfo.isUserAdmin()) { return true; } - if (!getAuthorizedClientId(authResult).isPresent()) { + if (!getAuthorizedClientId().isPresent()) { return false; } return true; diff --git a/java/google/registry/ui/server/registrar/AuthenticatedRegistrarAccessor.java b/java/google/registry/ui/server/registrar/AuthenticatedRegistrarAccessor.java new file mode 100644 index 000000000..44334676d --- /dev/null +++ b/java/google/registry/ui/server/registrar/AuthenticatedRegistrarAccessor.java @@ -0,0 +1,234 @@ +// 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.ui.server.registrar; + +import static google.registry.model.ofy.ObjectifyService.ofy; + +import com.google.appengine.api.users.User; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.flogger.FluentLogger; +import google.registry.config.RegistryConfig.Config; +import google.registry.model.registrar.Registrar; +import google.registry.model.registrar.RegistrarContact; +import google.registry.request.HttpException.ForbiddenException; +import google.registry.request.auth.AuthResult; +import google.registry.request.auth.UserAuthInfo; +import java.util.Optional; +import java.util.function.Function; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +/** + * Allows access only to {@link Registrar}s the current user has access to. + * + *

A user has read+write access to a Registrar if there exists a {@link RegistrarContact} with + * that user's gaeId and the registrar as a parent. + * + *

An admin has in addition read+write access to {@link #registryAdminClientId}. + * + *

An admin also has read access to ALL registrars. + */ +@Immutable +public class AuthenticatedRegistrarAccessor { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + /** Type of access we're requesting. */ + public enum AccessType { + READ, + UPDATE + } + + AuthResult authResult; + String registryAdminClientId; + + /** + * For any AccessType requested - all clientIDs this user is allowed that AccessType on. + * + *

The order is significant, with "more specific to this user" coming first. + */ + private final ImmutableSetMultimap accessMap; + + @Inject + public AuthenticatedRegistrarAccessor( + AuthResult authResult, @Config("registryAdminClientId") String registryAdminClientId) { + this.authResult = authResult; + this.registryAdminClientId = registryAdminClientId; + this.accessMap = createAccessMap(authResult, registryAdminClientId); + + logger.atInfo().log( + "User %s has the following accesses: %s", authResult.userIdForLogging(), accessMap); + } + + /** + * Loads a Registrar IFF the user is authorized. + * + *

Throws a {@link ForbiddenException} if the user is not logged in, or not authorized to + * access the requested registrar. + * + * @param clientId ID of the registrar we request + * @param accessType what kind of access do we want for this registrar - just read it or write as + * well? (different users might have different access levels) + */ + public Registrar getRegistrar(String clientId, AccessType accessType) { + return getAndAuthorize(Registrar::loadByClientId, clientId, accessType); + } + + /** + * Loads a Registrar from the cache IFF the user is authorized. + * + *

Throws a {@link ForbiddenException} if the user is not logged in, or not authorized to + * access the requested registrar. + * + * @param clientId ID of the registrar we request + * @param accessType what kind of access do we want for this registrar - just read it or write as + * well? (different users might have different access levels) + */ + public Registrar getRegistrarForUserCached(String clientId, AccessType accessType) { + return getAndAuthorize(Registrar::loadByClientIdCached, clientId, accessType); + } + + /** + * For all {@link AccessType}s, Returns all ClientIds this user is allowed this access. + * + *

Throws a {@link ForbiddenException} if the user is not logged in. + * + *

The result is ordered starting from "most specific to this user". + * + *

If you want to load the {@link Registrar} object from these (or any other) {@code clientId}, + * in order to perform actions on behalf of a user, you must use {@link #getRegistrar} which makes + * sure the user has permissions. + * + *

Note that this is an OPTIONAL step in the authentication - only used if we don't have any + * other clue as to the requested {@code clientId}. It is perfectly OK to get a {@code clientId} + * from any other source, as long as the registrar is then loaded using {@link #getRegistrar}. + */ + public ImmutableSetMultimap getAllClientIdWithAccess() { + verifyLoggedIn(); + return accessMap; + } + + /** + * "Guesses" which client ID the user wants from all those they have access to. + * + *

If no such ClientIds exist, throws a ForbiddenException. + * + *

This should be the ClientId "most likely wanted by the user". + * + *

If you want to load the {@link Registrar} object from this (or any other) {@code clientId}, + * in order to perform actions on behalf of a user, you must use {@link #getRegistrar} which makes + * sure the user has permissions. + * + *

Note that this is an OPTIONAL step in the authentication - only used if we don't have any + * other clue as to the requested {@code clientId}. It is perfectly OK to get a {@code clientId} + * from any other source, as long as the registrar is then loaded using {@link #getRegistrar}. + */ + public String guessClientId() { + verifyLoggedIn(); + return getAllClientIdWithAccess().values().stream() + .findFirst() + .orElseThrow( + () -> + new ForbiddenException( + String.format( + "User %s isn't associated with any registrar", + authResult.userIdForLogging()))); + } + + private Registrar getAndAuthorize( + Function> registrarLoader, + String clientId, + AccessType accessType) { + verifyLoggedIn(); + + if (!accessMap.containsEntry(accessType, clientId)) { + throw new ForbiddenException( + String.format( + "User %s doesn't have %s access to registrar %s", + authResult.userIdForLogging(), accessType, clientId)); + } + + Registrar registrar = + registrarLoader + .apply(clientId) + .orElseThrow( + () -> new ForbiddenException(String.format("Registrar %s not found", clientId))); + + if (!clientId.equals(registrar.getClientId())) { + logger.atSevere().log( + "registrarLoader.apply(clientId) returned a Registrar with a different clientId. " + + "Requested: %s, returned: %s.", + clientId, registrar.getClientId()); + throw new ForbiddenException("Internal error - please check logs"); + } + + logger.atInfo().log( + "User %s has %s access to registrar %s.", + authResult.userIdForLogging(), accessType, clientId); + return registrar; + } + + private static ImmutableSetMultimap createAccessMap( + AuthResult authResult, String registryAdminClientId) { + + if (!authResult.userAuthInfo().isPresent()) { + return ImmutableSetMultimap.of(); + } + + UserAuthInfo userAuthInfo = authResult.userAuthInfo().get(); + + boolean isAdmin = userAuthInfo.isUserAdmin(); + User user = userAuthInfo.user(); + + ImmutableSetMultimap.Builder builder = new ImmutableSetMultimap.Builder<>(); + + ofy() + .load() + .type(RegistrarContact.class) + .filter("gaeUserId", user.getUserId()) + .forEach( + contact -> + builder + .put(AccessType.UPDATE, contact.getParent().getName()) + .put(AccessType.READ, contact.getParent().getName())); + if (isAdmin && !Strings.isNullOrEmpty(registryAdminClientId)) { + logger.atInfo().log( + "Giving admin %s read+write access to admin registrar %s", + authResult.userIdForLogging(), registryAdminClientId); + builder + .put(AccessType.UPDATE, registryAdminClientId) + .put(AccessType.READ, registryAdminClientId); + } + + if (isAdmin) { + // Admins have READ access to all registrars + logger.atInfo().log( + "Giving admin %s read-only access to all registrars", authResult.userIdForLogging()); + ofy() + .load() + .type(Registrar.class) + .forEach(registrar -> builder.put(AccessType.READ, registrar.getClientId())); + } + + return builder.build(); + } + + private void verifyLoggedIn() { + if (!authResult.userAuthInfo().isPresent()) { + throw new ForbiddenException("Not logged in"); + } + } +} diff --git a/java/google/registry/ui/server/registrar/ConsoleUiAction.java b/java/google/registry/ui/server/registrar/ConsoleUiAction.java index c3d682a3f..6e5d9ff55 100644 --- a/java/google/registry/ui/server/registrar/ConsoleUiAction.java +++ b/java/google/registry/ui/server/registrar/ConsoleUiAction.java @@ -16,8 +16,9 @@ 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 google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.READ; +import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.UPDATE; import static google.registry.ui.server.registrar.RegistrarConsoleModule.PARAM_CLIENT_ID; -import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY; import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; @@ -26,6 +27,7 @@ import com.google.appengine.api.users.User; import com.google.appengine.api.users.UserService; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Supplier; +import com.google.common.collect.Sets; import com.google.common.flogger.FluentLogger; import com.google.common.io.Resources; import com.google.common.net.MediaType; @@ -71,7 +73,7 @@ public final class ConsoleUiAction implements Runnable { @Inject HttpServletRequest req; @Inject Response response; - @Inject SessionUtils sessionUtils; + @Inject AuthenticatedRegistrarAccessor registrarAccessor; @Inject UserService userService; @Inject XsrfTokenManager xsrfTokenManager; @Inject AuthResult authResult; @@ -131,9 +133,16 @@ public final class ConsoleUiAction implements Runnable { data.put("logoutUrl", userService.createLogoutURL(PATH)); data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmail())); try { - String clientId = - paramClientId.orElseGet(() -> sessionUtils.guessClientIdForUser(authResult)); + String clientId = paramClientId.orElse(registrarAccessor.guessClientId()); data.put("clientId", clientId); + + data.put("readWriteClientIds", registrarAccessor.getAllClientIdWithAccess().get(UPDATE)); + data.put( + "readOnlyClientIds", + Sets.difference( + registrarAccessor.getAllClientIdWithAccess().get(READ), + registrarAccessor.getAllClientIdWithAccess().get(UPDATE))); + // We want to load the registrar even if we won't use it later (even if we remove the // requireFeeExtension) - to make sure the user indeed has access to the guessed registrar. // @@ -141,7 +150,7 @@ public final class ConsoleUiAction implements Runnable { // since we double check the access to the registrar on any read / update request. We have to // - since the access might get revoked between the initial page load and the request! (also // because the requests come from the browser, and can easily be faked) - Registrar registrar = sessionUtils.getRegistrarForUser(clientId, READ_ONLY, authResult); + Registrar registrar = registrarAccessor.getRegistrar(clientId, READ); data.put("requireFeeExtension", registrar.getPremiumPriceAckRequired()); } catch (ForbiddenException e) { logger.atWarning().withCause(e).log( diff --git a/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java b/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java index cbd054243..d6ce6a466 100644 --- a/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java +++ b/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java @@ -20,8 +20,8 @@ import static google.registry.export.sheet.SyncRegistrarsSheetAction.enqueueRegi import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.security.JsonResponseHelper.Status.ERROR; import static google.registry.security.JsonResponseHelper.Status.SUCCESS; -import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY; -import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_WRITE; +import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.READ; +import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.UPDATE; import com.google.common.base.Strings; import com.google.common.collect.HashMultimap; @@ -34,13 +34,11 @@ import com.google.common.flogger.FluentLogger; import google.registry.config.RegistryConfig.Config; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarContact; -import google.registry.model.registrar.RegistrarContact.Builder; import google.registry.model.registrar.RegistrarContact.Type; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; import google.registry.request.JsonActionRunner; import google.registry.request.auth.Auth; -import google.registry.request.auth.AuthResult; import google.registry.security.JsonResponseHelper; import google.registry.ui.forms.FormException; import google.registry.ui.forms.FormFieldException; @@ -79,9 +77,9 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA @Inject JsonActionRunner jsonActionRunner; @Inject AppEngineServiceUtils appEngineServiceUtils; - @Inject AuthResult authResult; @Inject SendEmailUtils sendEmailUtils; - @Inject SessionUtils sessionUtils; + @Inject AuthenticatedRegistrarAccessor registrarAccessor; + @Inject @Config("registrarChangesNotificationEmailAddresses") ImmutableList registrarChangesNotificationEmailAddresses; @Inject RegistrarSettingsAction() {} @@ -134,9 +132,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA Map read(String clientId) { return JsonResponseHelper.create( - SUCCESS, - "Success", - sessionUtils.getRegistrarForUser(clientId, READ_ONLY, authResult).toJsonMap()); + SUCCESS, "Success", registrarAccessor.getRegistrar(clientId, READ).toJsonMap()); } Map update(final Map args, String clientId) { @@ -146,8 +142,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA // We load the registrar here rather than outside of the transaction - to make // sure we have the latest version. This one is loaded inside the transaction, so it's // guaranteed to not change before we update it. - Registrar registrar = - sessionUtils.getRegistrarForUser(clientId, READ_WRITE, authResult); + Registrar registrar = registrarAccessor.getRegistrar(clientId, UPDATE); // Verify that the registrar hasn't been changed. // To do that - we find the latest update time (or null if the registrar has been // deleted) and compare to the update time from the args. The update time in the args @@ -263,7 +258,8 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA Registrar registrar, Map args) { ImmutableSet.Builder contacts = new ImmutableSet.Builder<>(); - Optional> builders = RegistrarFormFields.CONTACTS_FIELD.extractUntyped(args); + Optional> builders = + RegistrarFormFields.CONTACTS_FIELD.extractUntyped(args); if (builders.isPresent()) { builders.get().forEach(c -> contacts.add(c.setParent(registrar).build())); } diff --git a/java/google/registry/ui/server/registrar/SessionUtils.java b/java/google/registry/ui/server/registrar/SessionUtils.java deleted file mode 100644 index 57a63048f..000000000 --- a/java/google/registry/ui/server/registrar/SessionUtils.java +++ /dev/null @@ -1,199 +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.ui.server.registrar; - -import static google.registry.model.ofy.ObjectifyService.ofy; - -import com.google.appengine.api.users.User; -import com.google.common.base.Strings; -import com.google.common.flogger.FluentLogger; -import google.registry.config.RegistryConfig.Config; -import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.RegistrarContact; -import google.registry.request.HttpException.ForbiddenException; -import google.registry.request.auth.AuthResult; -import google.registry.request.auth.UserAuthInfo; -import java.util.Optional; -import java.util.function.Function; -import javax.annotation.concurrent.Immutable; -import javax.inject.Inject; - -/** HTTP session management helper class. */ -@Immutable -public class SessionUtils { - - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - - @Inject - @Config("registryAdminClientId") - String registryAdminClientId; - - @Inject - public SessionUtils() {} - - /** Type of access we're requesting. */ - public enum AccessType {READ_ONLY, READ_WRITE} - - /** - * Loads Registrar on behalf of an authorised user. - * - *

Throws a {@link ForbiddenException} if the user is not logged in, or not authorized to - * access the requested registrar. - * - * @param clientId ID of the registrar we request - * @param accessType what kind of access do we want for this registrar - just read it or write as - * well? (different users might have different access levels) - * @param authResult AuthResult of the user on behalf of which we want to access the data - */ - public Registrar getRegistrarForUser( - String clientId, AccessType accessType, AuthResult authResult) { - return getAndAuthorize(Registrar::loadByClientId, clientId, accessType, authResult); - } - - /** - * Loads a Registrar from the cache on behalf of an authorised user. - * - *

Throws a {@link ForbiddenException} if the user is not logged in, or not authorized to - * access the requested registrar. - * - * @param clientId ID of the registrar we request - * @param accessType what kind of access do we want for this registrar - just read it or write as - * well? (different users might have different access levels) - * @param authResult AuthResult of the user on behalf of which we want to access the data - */ - public Registrar getRegistrarForUserCached( - String clientId, AccessType accessType, AuthResult authResult) { - return getAndAuthorize(Registrar::loadByClientIdCached, clientId, accessType, authResult); - } - - private Registrar getAndAuthorize( - Function> registrarLoader, - String clientId, - AccessType accessType, - AuthResult authResult) { - UserAuthInfo userAuthInfo = - authResult.userAuthInfo().orElseThrow(() -> new ForbiddenException("Not logged in")); - boolean isAdmin = userAuthInfo.isUserAdmin(); - User user = userAuthInfo.user(); - String userIdForLogging = authResult.userIdForLogging(); - - Registrar registrar = - registrarLoader - .apply(clientId) - .orElseThrow( - () -> new ForbiddenException(String.format("Registrar %s not found", clientId))); - - if (isInAllowedContacts(registrar, user)) { - logger.atInfo().log("User %s has access to registrar %s.", userIdForLogging, clientId); - return registrar; - } - - if (isAdmin && clientId.equals(registryAdminClientId)) { - // Admins have access to the registryAdminClientId even if they aren't explicitly in the - // allowed contacts - logger.atInfo().log("Allowing admin %s access to registrar %s.", userIdForLogging, clientId); - return registrar; - } - - if (isAdmin && accessType == AccessType.READ_ONLY) { - // Admins have read-only access to all registrars - logger.atInfo().log( - "Allowing admin %s read-only access to registrar %s.", userIdForLogging, clientId); - return registrar; - } - - throw new ForbiddenException( - String.format( - "User %s doesn't have %s access to registrar %s", - userIdForLogging, accessType, clientId)); - } - - /** - * Tries to guess the {@link Registrar} with which the user is associated. - * - *

Returns the {@code clientId} of a {@link Registrar} the user has access to (is on the - * contact list). If the user has access to multiple {@link Registrar}s, an arbitrary one is - * selected. If the user is an admin without access to any {@link Registrar}s, {@link - * #registryAdminClientId} is returned if it is defined. - * - *

If no {@code clientId} is found, throws a {@link ForbiddenException}. - * - *

If you want to load the {@link Registrar} object from this (or any other) {@code clientId}, - * in order to perform actions on behalf of a user, you must use {@link #getRegistrarForUser} - * which makes sure the user has permissions. - * - *

Note that this is an OPTIONAL step in the authentication - only used if we don't have any - * other clue as to the requested {@code clientId}. It is perfectly OK to get a {@code clientId} - * from any other source, as long as the registrar is then loaded using {@link - * #getRegistrarForUser}. - */ - public String guessClientIdForUser(AuthResult authResult) { - - UserAuthInfo userAuthInfo = - authResult.userAuthInfo().orElseThrow(() -> new ForbiddenException("No logged in")); - boolean isAdmin = userAuthInfo.isUserAdmin(); - User user = userAuthInfo.user(); - String userIdForLogging = authResult.userIdForLogging(); - - RegistrarContact contact = - ofy() - .load() - .type(RegistrarContact.class) - .filter("gaeUserId", user.getUserId()) - .first() - .now(); - if (contact != null) { - String registrarClientId = contact.getParent().getName(); - logger.atInfo().log( - "Associating user %s with found registrar %s.", userIdForLogging, registrarClientId); - return registrarClientId; - } - - // We couldn't find the registrar, but maybe the user is an admin and we can use the - // registryAdminClientId - if (isAdmin) { - if (!Strings.isNullOrEmpty(registryAdminClientId)) { - logger.atInfo().log( - "User %s is an admin with no associated registrar." - + " Automatically associating the user with configured client Id %s.", - userIdForLogging, registryAdminClientId); - return registryAdminClientId; - } - logger.atInfo().log( - "Cannot associate admin user %s with configured client Id." - + " ClientId is null or empty.", - userIdForLogging); - } - - // We couldn't find any relevant clientId - throw new ForbiddenException( - String.format("User %s isn't associated with any registrar", userIdForLogging)); - } - - /** - * Returns {@code true} if {@code user} is listed in contacts with access to the registrar. - * - *

Each registrar contact can either have getGaeUserId equals null or the user's gaeUserId. - * Null means the contact doesn't have access to the registrar console. None-null means the - * contact has access. - */ - private static boolean isInAllowedContacts(Registrar registrar, User user) { - String gaeUserId = user.getUserId(); - return registrar - .getContacts() - .stream() - .anyMatch(contact -> gaeUserId.equals(contact.getGaeUserId())); - } -} diff --git a/java/google/registry/ui/soy/registrar/Console.soy b/java/google/registry/ui/soy/registrar/Console.soy index 7beb32b87..516a0212a 100644 --- a/java/google/registry/ui/soy/registrar/Console.soy +++ b/java/google/registry/ui/soy/registrar/Console.soy @@ -23,6 +23,8 @@ {template .main} {@param xsrfToken: string} /** Security token. */ {@param clientId: string} /** Registrar client identifier. */ + {@param readWriteClientIds: list} + {@param readOnlyClientIds: list} {@param username: string} /** Arbitrary username to display. */ {@param logoutUrl: string} /** Generated URL for logging out of Google. */ {@param productName: string} /** Name to display for this software product. */ @@ -75,7 +77,33 @@ /** Sidebar nav. Ids on each elt for testing only. */ {template .navbar_ visibility="private"} + {@param clientId: string} /** Registrar client identifier. */ + {@param readWriteClientIds: list} + {@param readOnlyClientIds: list} +

{/template} diff --git a/javatests/google/registry/rdap/RdapActionBaseTest.java b/javatests/google/registry/rdap/RdapActionBaseTest.java index e2143861d..5a84b6c96 100644 --- a/javatests/google/registry/rdap/RdapActionBaseTest.java +++ b/javatests/google/registry/rdap/RdapActionBaseTest.java @@ -40,7 +40,7 @@ import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.InjectRule; -import google.registry.ui.server.registrar.SessionUtils; +import google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor; import java.util.Optional; import org.joda.time.DateTime; import org.json.simple.JSONValue; @@ -64,7 +64,8 @@ public class RdapActionBaseTest { private final FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); - private final SessionUtils sessionUtils = mock(SessionUtils.class); + private final AuthenticatedRegistrarAccessor registrarAccessor = + mock(AuthenticatedRegistrarAccessor.class); private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); private final UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); private final RdapMetrics rdapMetrics = mock(RdapMetrics.class); @@ -119,7 +120,7 @@ public class RdapActionBaseTest { createTld("thing"); inject.setStaticField(Ofy.class, "clock", clock); action = new RdapTestAction(); - action.sessionUtils = sessionUtils; + action.registrarAccessor = registrarAccessor; action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); action.includeDeletedParam = Optional.empty(); action.registrarParam = Optional.empty(); diff --git a/javatests/google/registry/rdap/RdapDomainActionTest.java b/javatests/google/registry/rdap/RdapDomainActionTest.java index 00f8f784c..fd0d4cb65 100644 --- a/javatests/google/registry/rdap/RdapDomainActionTest.java +++ b/javatests/google/registry/rdap/RdapDomainActionTest.java @@ -53,7 +53,7 @@ import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.InjectRule; -import google.registry.ui.server.registrar.SessionUtils; +import google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor; import java.util.List; import java.util.Map; import java.util.Optional; @@ -82,7 +82,8 @@ public class RdapDomainActionTest { private final FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); - private final SessionUtils sessionUtils = mock(SessionUtils.class); + private final AuthenticatedRegistrarAccessor registrarAccessor = + mock(AuthenticatedRegistrarAccessor.class); private final RdapMetrics rdapMetrics = mock(RdapMetrics.class); private RdapDomainAction action; @@ -275,17 +276,17 @@ public class RdapDomainActionTest { action.formatOutputParam = Optional.empty(); action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); action.rdapWhoisServer = null; - action.sessionUtils = sessionUtils; + action.registrarAccessor = registrarAccessor; action.authResult = AUTH_RESULT; action.rdapMetrics = rdapMetrics; } private void login(String clientId) { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn(clientId); + when(registrarAccessor.guessClientId()).thenReturn(clientId); } private void loginAsAdmin() { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT_ADMIN)).thenReturn("irrelevant"); + when(registrarAccessor.guessClientId()).thenReturn("irrelevant"); action.authResult = AUTH_RESULT_ADMIN; } diff --git a/javatests/google/registry/rdap/RdapDomainSearchActionTest.java b/javatests/google/registry/rdap/RdapDomainSearchActionTest.java index 2684e72ce..20af008a8 100644 --- a/javatests/google/registry/rdap/RdapDomainSearchActionTest.java +++ b/javatests/google/registry/rdap/RdapDomainSearchActionTest.java @@ -62,7 +62,7 @@ import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.InjectRule; -import google.registry.ui.server.registrar.SessionUtils; +import google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor; import google.registry.util.Idn; import java.net.IDN; import java.net.URLDecoder; @@ -91,7 +91,8 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { @Rule public final InjectRule inject = new InjectRule(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z")); - private final SessionUtils sessionUtils = mock(SessionUtils.class); + private final AuthenticatedRegistrarAccessor registrarAccessor = + mock(AuthenticatedRegistrarAccessor.class); private final RdapDomainSearchAction action = new RdapDomainSearchAction(); private static final AuthResult AUTH_RESULT = @@ -405,7 +406,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { action.formatOutputParam = Optional.empty(); action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); action.rdapWhoisServer = null; - action.sessionUtils = sessionUtils; + action.registrarAccessor = registrarAccessor; action.authResult = AUTH_RESULT; action.rdapMetrics = rdapMetrics; action.cursorTokenParam = Optional.empty(); @@ -413,12 +414,12 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { } private void login(String clientId) { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn(clientId); + when(registrarAccessor.guessClientId()).thenReturn(clientId); metricRole = REGISTRAR; } private void loginAsAdmin() { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn("irrelevant"); + when(registrarAccessor.guessClientId()).thenReturn("irrelevant"); action.authResult = AUTH_RESULT_ADMIN; metricRole = ADMINISTRATOR; } diff --git a/javatests/google/registry/rdap/RdapEntityActionTest.java b/javatests/google/registry/rdap/RdapEntityActionTest.java index f79d216b7..808b479e0 100644 --- a/javatests/google/registry/rdap/RdapEntityActionTest.java +++ b/javatests/google/registry/rdap/RdapEntityActionTest.java @@ -49,7 +49,7 @@ import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.InjectRule; -import google.registry.ui.server.registrar.SessionUtils; +import google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; @@ -75,7 +75,8 @@ public class RdapEntityActionTest { private final FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); - private final SessionUtils sessionUtils = mock(SessionUtils.class); + private final AuthenticatedRegistrarAccessor registrarAccessor = + mock(AuthenticatedRegistrarAccessor.class); private final RdapMetrics rdapMetrics = mock(RdapMetrics.class); private RdapEntityAction action; @@ -176,18 +177,18 @@ public class RdapEntityActionTest { action.formatOutputParam = Optional.empty(); action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); action.rdapWhoisServer = null; - action.sessionUtils = sessionUtils; + action.registrarAccessor = registrarAccessor; action.authResult = AUTH_RESULT; action.rdapMetrics = rdapMetrics; } private void login(String registrar) { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn(registrar); + when(registrarAccessor.guessClientId()).thenReturn(registrar); } private void loginAsAdmin() { action.authResult = AUTH_RESULT_ADMIN; - when(sessionUtils.guessClientIdForUser(AUTH_RESULT_ADMIN)).thenReturn("irrelevant"); + when(registrarAccessor.guessClientId()).thenReturn("irrelevant"); } private Object generateActualJson(String name) { diff --git a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java index 42932e373..7d9ad6626 100644 --- a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java +++ b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java @@ -53,7 +53,7 @@ import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.InjectRule; -import google.registry.ui.server.registrar.SessionUtils; +import google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor; import java.net.URLDecoder; import java.util.List; import java.util.Map; @@ -82,7 +82,8 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase { } private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z")); - private final SessionUtils sessionUtils = mock(SessionUtils.class); + private final AuthenticatedRegistrarAccessor registrarAccessor = + mock(AuthenticatedRegistrarAccessor.class); private final RdapEntitySearchAction action = new RdapEntitySearchAction(); private FakeResponse response = new FakeResponse(); @@ -202,20 +203,20 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase { action.registrarParam = Optional.empty(); action.includeDeletedParam = Optional.empty(); action.formatOutputParam = Optional.empty(); - action.sessionUtils = sessionUtils; + action.registrarAccessor = registrarAccessor; action.authResult = AUTH_RESULT; action.rdapMetrics = rdapMetrics; action.cursorTokenParam = Optional.empty(); } private void login(String registrar) { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn(registrar); + when(registrarAccessor.guessClientId()).thenReturn(registrar); metricRole = REGISTRAR; } private void loginAsAdmin() { action.authResult = AUTH_RESULT_ADMIN; - when(sessionUtils.guessClientIdForUser(AUTH_RESULT_ADMIN)).thenReturn("irrelevant"); + when(registrarAccessor.guessClientId()).thenReturn("irrelevant"); metricRole = ADMINISTRATOR; } diff --git a/javatests/google/registry/rdap/RdapHelpActionTest.java b/javatests/google/registry/rdap/RdapHelpActionTest.java index e808daf04..33262a0cc 100644 --- a/javatests/google/registry/rdap/RdapHelpActionTest.java +++ b/javatests/google/registry/rdap/RdapHelpActionTest.java @@ -33,7 +33,7 @@ import google.registry.request.auth.UserAuthInfo; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.InjectRule; -import google.registry.ui.server.registrar.SessionUtils; +import google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor; import java.util.Optional; import org.joda.time.DateTime; import org.json.simple.JSONValue; @@ -52,7 +52,8 @@ public class RdapHelpActionTest { private final FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); - private final SessionUtils sessionUtils = mock(SessionUtils.class); + private final AuthenticatedRegistrarAccessor registrarAccessor = + mock(AuthenticatedRegistrarAccessor.class); private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); private final UserAuthInfo userAuthInfo = UserAuthInfo.create(user, false); private final RdapMetrics rdapMetrics = mock(RdapMetrics.class); @@ -67,7 +68,7 @@ public class RdapHelpActionTest { action.clock = clock; action.fullServletPath = "https://example.tld/rdap"; action.requestMethod = Action.Method.GET; - action.sessionUtils = sessionUtils; + action.registrarAccessor = registrarAccessor; action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); action.includeDeletedParam = Optional.empty(); action.registrarParam = Optional.empty(); diff --git a/javatests/google/registry/rdap/RdapNameserverActionTest.java b/javatests/google/registry/rdap/RdapNameserverActionTest.java index de8fc7b98..340ba3bf7 100644 --- a/javatests/google/registry/rdap/RdapNameserverActionTest.java +++ b/javatests/google/registry/rdap/RdapNameserverActionTest.java @@ -43,7 +43,7 @@ import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.InjectRule; -import google.registry.ui.server.registrar.SessionUtils; +import google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; @@ -69,7 +69,8 @@ public class RdapNameserverActionTest { private final FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); - private final SessionUtils sessionUtils = mock(SessionUtils.class); + private final AuthenticatedRegistrarAccessor registrarAccessor = + mock(AuthenticatedRegistrarAccessor.class); private final RdapMetrics rdapMetrics = mock(RdapMetrics.class); private static final AuthResult AUTH_RESULT = @@ -132,7 +133,7 @@ public class RdapNameserverActionTest { action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); action.rdapWhoisServer = null; action.authResult = authResult; - action.sessionUtils = sessionUtils; + action.registrarAccessor = registrarAccessor; action.rdapMetrics = rdapMetrics; return action; } @@ -362,21 +363,21 @@ public class RdapNameserverActionTest { @Test public void testDeletedNameserver_notFound_notLoggedIn() { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenThrow(new ForbiddenException("blah")); + when(registrarAccessor.guessClientId()).thenThrow(new ForbiddenException("blah")); generateActualJson("nsdeleted.cat.lol", Optional.empty(), Optional.of(true)); assertThat(response.getStatus()).isEqualTo(404); } @Test public void testDeletedNameserver_notFound_loggedInAsDifferentRegistrar() { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn("otherregistrar"); + when(registrarAccessor.guessClientId()).thenReturn("otherregistrar"); generateActualJson("nsdeleted.cat.lol", Optional.empty(), Optional.of(true)); assertThat(response.getStatus()).isEqualTo(404); } @Test public void testDeletedNameserver_found_loggedInAsCorrectRegistrar() { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn("TheRegistrar"); + when(registrarAccessor.guessClientId()).thenReturn("TheRegistrar"); assertThat( generateActualJson("nsdeleted.cat.lol", Optional.empty(), Optional.of(true))) .isEqualTo( @@ -393,7 +394,7 @@ public class RdapNameserverActionTest { @Test public void testDeletedNameserver_found_loggedInAsAdmin() { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT_ADMIN)).thenReturn("irrelevant"); + when(registrarAccessor.guessClientId()).thenReturn("irrelevant"); newRdapNameserverAction( "nsdeleted.cat.lol", Optional.empty(), @@ -415,7 +416,7 @@ public class RdapNameserverActionTest { @Test public void testDeletedNameserver_found_sameRegistrarRequested() { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn("TheRegistrar"); + when(registrarAccessor.guessClientId()).thenReturn("TheRegistrar"); assertThat( generateActualJson("nsdeleted.cat.lol", Optional.of("TheRegistrar"), Optional.of(true))) .isEqualTo( @@ -432,7 +433,7 @@ public class RdapNameserverActionTest { @Test public void testDeletedNameserver_notFound_differentRegistrarRequested() { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn("TheRegistrar"); + when(registrarAccessor.guessClientId()).thenReturn("TheRegistrar"); generateActualJson("ns1.cat.lol", Optional.of("otherregistrar"), Optional.of(false)); assertThat(response.getStatus()).isEqualTo(404); } diff --git a/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java b/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java index 9499fc8f4..804864d28 100644 --- a/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java +++ b/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java @@ -54,7 +54,7 @@ import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.InjectRule; -import google.registry.ui.server.registrar.SessionUtils; +import google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor; import java.net.URLDecoder; import java.util.Optional; import javax.annotation.Nullable; @@ -78,7 +78,8 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { private FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z")); - private final SessionUtils sessionUtils = mock(SessionUtils.class); + private final AuthenticatedRegistrarAccessor registrarAccessor = + mock(AuthenticatedRegistrarAccessor.class); private final RdapNameserverSearchAction action = new RdapNameserverSearchAction(); private static final AuthResult AUTH_RESULT = @@ -200,17 +201,17 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { action.includeDeletedParam = Optional.empty(); action.formatOutputParam = Optional.empty(); action.authResult = AUTH_RESULT; - action.sessionUtils = sessionUtils; + action.registrarAccessor = registrarAccessor; action.rdapMetrics = rdapMetrics; action.cursorTokenParam = Optional.empty(); } private void login(String clientId) { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT)).thenReturn(clientId); + when(registrarAccessor.guessClientId()).thenReturn(clientId); metricRole = REGISTRAR; } private void loginAsAdmin() { - when(sessionUtils.guessClientIdForUser(AUTH_RESULT_ADMIN)).thenReturn("irrelevant"); + when(registrarAccessor.guessClientId()).thenReturn("irrelevant"); action.authResult = AUTH_RESULT_ADMIN; metricRole = ADMINISTRATOR; } diff --git a/javatests/google/registry/ui/js/registrar/console_test_util.js b/javatests/google/registry/ui/js/registrar/console_test_util.js index 2eac0449c..158f95277 100644 --- a/javatests/google/registry/ui/js/registrar/console_test_util.js +++ b/javatests/google/registry/ui/js/registrar/console_test_util.js @@ -62,6 +62,8 @@ registry.registrar.ConsoleTestUtil.renderConsoleMain = function( logoutUrl: args.logoutUrl || 'https://logout.url.com', isAdmin: goog.isDefAndNotNull(args.isAdmin) ? args.isAdmin : true, clientId: args.clientId || 'ignore', + readWriteClientIds: args.readWriteClientIds || ['readWrite'], + readOnlyClientIds: args.readOnlyClientIds || ['readOnly'], logoFilename: args.logoFilename || 'logo.png', productName: args.productName || 'Nomulus', integrationEmail: args.integrationEmail || 'integration@example.com', diff --git a/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java b/javatests/google/registry/ui/server/registrar/AuthenticatedRegistrarAccessorTest.java similarity index 58% rename from javatests/google/registry/ui/server/registrar/SessionUtilsTest.java rename to javatests/google/registry/ui/server/registrar/AuthenticatedRegistrarAccessorTest.java index 86dce17ad..a97b6523c 100644 --- a/javatests/google/registry/ui/server/registrar/SessionUtilsTest.java +++ b/javatests/google/registry/ui/server/registrar/AuthenticatedRegistrarAccessorTest.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// 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. @@ -20,8 +20,8 @@ import static google.registry.testing.DatastoreHelper.loadRegistrar; import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.JUnitBackports.assertThrows; import static google.registry.testing.LogsSubject.assertAboutLogs; -import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY; -import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_WRITE; +import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.READ; +import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.UPDATE; import static org.mockito.Mockito.mock; import com.google.appengine.api.users.User; @@ -34,7 +34,7 @@ import google.registry.request.auth.AuthResult; import google.registry.request.auth.UserAuthInfo; import google.registry.testing.AppEngineRule; import google.registry.testing.InjectRule; -import google.registry.ui.server.registrar.SessionUtils.AccessType; +import google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType; import java.util.logging.Level; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -45,9 +45,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** Unit tests for {@link SessionUtils}. */ +/** Unit tests for {@link AuthenticatedRegistrarAccessor}. */ @RunWith(JUnit4.class) -public class SessionUtilsTest { +public class AuthenticatedRegistrarAccessorTest { @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); @Rule public final InjectRule inject = new InjectRule(); @@ -56,8 +56,6 @@ public class SessionUtilsTest { private final HttpServletResponse rsp = mock(HttpServletResponse.class); private final TestLogHandler testLogHandler = new TestLogHandler(); - private SessionUtils sessionUtils; - private static final AuthResult AUTHORIZED_USER = createAuthResult(true, false); private static final AuthResult UNAUTHORIZED_USER = createAuthResult(false, false); private static final AuthResult AUTHORIZED_ADMIN = createAuthResult(true, true); @@ -80,15 +78,13 @@ public class SessionUtilsTest { @Before public void before() { - LoggerConfig.getConfig(SessionUtils.class).addHandler(testLogHandler); - sessionUtils = new SessionUtils(); - sessionUtils.registryAdminClientId = ADMIN_CLIENT_ID; + LoggerConfig.getConfig(AuthenticatedRegistrarAccessor.class).addHandler(testLogHandler); persistResource(loadRegistrar(ADMIN_CLIENT_ID)); } @After public void after() { - LoggerConfig.getConfig(SessionUtils.class).removeHandler(testLogHandler); + LoggerConfig.getConfig(AuthenticatedRegistrarAccessor.class).removeHandler(testLogHandler); } private String formatMessage(String message, AuthResult authResult, String clientId) { @@ -97,14 +93,78 @@ public class SessionUtilsTest { .replace("{clientId}", String.valueOf(clientId)); } + /** Users only have access to the registrars they are a contact for. */ + @Test + public void getAllClientIdWithAccess_authorizedUser() { + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(AUTHORIZED_USER, ADMIN_CLIENT_ID); + + assertThat(registrarAccessor.getAllClientIdWithAccess()) + .containsExactly( + UPDATE, DEFAULT_CLIENT_ID, + READ, DEFAULT_CLIENT_ID) + .inOrder(); + } + + /** Logged out users don't have access to anything. */ + @Test + public void getAllClientIdWithAccess_loggedOutUser() { + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(NO_USER, ADMIN_CLIENT_ID); + + ForbiddenException exception = + assertThrows(ForbiddenException.class, () -> registrarAccessor.getAllClientIdWithAccess()); + assertThat(exception).hasMessageThat().contains("Not logged in"); + } + + /** Unauthorized users don't have access to anything. */ + @Test + public void getAllClientIdWithAccess_unauthorizedUser() { + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(UNAUTHORIZED_USER, ADMIN_CLIENT_ID); + + assertThat(registrarAccessor.getAllClientIdWithAccess()).isEmpty(); + } + + /** Admins have read/write access to the authorized registrars, AND the admin registrar. */ + @Test + public void getAllClientIdWithAccess_authorizedAdmin() { + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(AUTHORIZED_ADMIN, ADMIN_CLIENT_ID); + + assertThat(registrarAccessor.getAllClientIdWithAccess()) + .containsExactly( + UPDATE, DEFAULT_CLIENT_ID, + READ, DEFAULT_CLIENT_ID, + UPDATE, ADMIN_CLIENT_ID, + READ, ADMIN_CLIENT_ID) + .inOrder(); + } + + /** + * Unauthorized admins only have full access to the admin registrar, and read-only to the rest. + */ + @Test + public void getAllClientIdWithAccess_unauthorizedAdmin() { + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(UNAUTHORIZED_ADMIN, ADMIN_CLIENT_ID); + + assertThat(registrarAccessor.getAllClientIdWithAccess()) + .containsExactly( + UPDATE, ADMIN_CLIENT_ID, + READ, ADMIN_CLIENT_ID, + READ, DEFAULT_CLIENT_ID) + .inOrder(); + } + /** Fail loading registrar if user doesn't have access to it. */ @Test public void testGetRegistrarForUser_readOnly_noAccess_isNotAdmin() { expectGetRegistrarFailure( DEFAULT_CLIENT_ID, - READ_ONLY, + READ, UNAUTHORIZED_USER, - "User {user} doesn't have READ_ONLY access to registrar {clientId}"); + "User {user} doesn't have READ access to registrar {clientId}"); } /** Fail loading registrar if user doesn't have access to it. */ @@ -112,60 +172,56 @@ public class SessionUtilsTest { public void testGetRegistrarForUser_readWrite_noAccess_isNotAdmin() { expectGetRegistrarFailure( DEFAULT_CLIENT_ID, - READ_WRITE, + UPDATE, UNAUTHORIZED_USER, - "User {user} doesn't have READ_WRITE access to registrar {clientId}"); + "User {user} doesn't have UPDATE access to registrar {clientId}"); } /** Fail loading registrar if there's no user associated with the request. */ @Test public void testGetRegistrarForUser_readOnly_noUser() { - expectGetRegistrarFailure(DEFAULT_CLIENT_ID, READ_ONLY, NO_USER, "Not logged in"); + expectGetRegistrarFailure(DEFAULT_CLIENT_ID, READ, NO_USER, "Not logged in"); } /** Fail loading registrar if there's no user associated with the request. */ @Test public void testGetRegistrarForUser_readWrite_noUser() { - expectGetRegistrarFailure(DEFAULT_CLIENT_ID, READ_WRITE, NO_USER, "Not logged in"); - - assertAboutLogs().that(testLogHandler).hasNoLogsAtLevel(Level.INFO); + expectGetRegistrarFailure(DEFAULT_CLIENT_ID, UPDATE, NO_USER, "Not logged in"); } /** Succeed loading registrar in read-only mode if user has access to it. */ @Test public void testGetRegistrarForUser_readOnly_hasAccess_isNotAdmin() { expectGetRegistrarSuccess( - AUTHORIZED_USER, READ_ONLY, "User {user} has access to registrar {clientId}"); + AUTHORIZED_USER, READ, "User {user} has READ access to registrar {clientId}"); } /** Succeed loading registrar in read-write mode if user has access to it. */ @Test public void testGetRegistrarForUser_readWrite_hasAccess_isNotAdmin() { expectGetRegistrarSuccess( - AUTHORIZED_USER, READ_WRITE, "User {user} has access to registrar {clientId}"); + AUTHORIZED_USER, UPDATE, "User {user} has UPDATE access to registrar {clientId}"); } /** Succeed loading registrar in read-only mode if admin with access. */ @Test public void testGetRegistrarForUser_readOnly_hasAccess_isAdmin() { expectGetRegistrarSuccess( - AUTHORIZED_ADMIN, READ_ONLY, "User {user} has access to registrar {clientId}"); + AUTHORIZED_ADMIN, READ, "User {user} has READ access to registrar {clientId}"); } /** Succeed loading registrar in read-write mode if admin with access. */ @Test public void testGetRegistrarForUser_readWrite_hasAccess_isAdmin() { expectGetRegistrarSuccess( - AUTHORIZED_ADMIN, READ_WRITE, "User {user} has access to registrar {clientId}"); + AUTHORIZED_ADMIN, UPDATE, "User {user} has UPDATE access to registrar {clientId}"); } /** Succeed loading registrar for read-only when admin isn't on the approved contacts list. */ @Test public void testGetRegistrarForUser_readOnly_noAccess_isAdmin() { expectGetRegistrarSuccess( - UNAUTHORIZED_ADMIN, - READ_ONLY, - "Allowing admin {user} read-only access to registrar {clientId}."); + UNAUTHORIZED_ADMIN, READ, "User {user} has READ access to registrar {clientId}."); } /** Fail loading registrar for read-write when admin isn't on the approved contacts list. */ @@ -173,9 +229,9 @@ public class SessionUtilsTest { public void testGetRegistrarForUser_readWrite_noAccess_isAdmin() { expectGetRegistrarFailure( DEFAULT_CLIENT_ID, - READ_WRITE, + UPDATE, UNAUTHORIZED_ADMIN, - "User {user} doesn't have READ_WRITE access to registrar {clientId}"); + "User {user} doesn't have UPDATE access to registrar {clientId}"); } /** Fail loading registrar even if admin, if registrar doesn't exist. */ @@ -183,9 +239,9 @@ public class SessionUtilsTest { public void testGetRegistrarForUser_readOnly_doesntExist_isAdmin() { expectGetRegistrarFailure( "BadClientId", - READ_ONLY, + READ, AUTHORIZED_ADMIN, - "Registrar {clientId} not found"); + "User {user} doesn't have READ access to registrar {clientId}"); } /** Fail loading registrar even if admin, if registrar doesn't exist. */ @@ -193,15 +249,17 @@ public class SessionUtilsTest { public void testGetRegistrarForUser_readWrite_doesntExist_isAdmin() { expectGetRegistrarFailure( "BadClientId", - READ_WRITE, + UPDATE, AUTHORIZED_ADMIN, - "Registrar {clientId} not found"); + "User {user} doesn't have UPDATE access to registrar {clientId}"); } private void expectGetRegistrarSuccess( AuthResult authResult, AccessType accessType, String message) { - assertThat(sessionUtils.getRegistrarForUser(DEFAULT_CLIENT_ID, accessType, authResult)) - .isNotNull(); + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(authResult, ADMIN_CLIENT_ID); + + assertThat(registrarAccessor.getRegistrar(DEFAULT_CLIENT_ID, accessType)).isNotNull(); assertAboutLogs() .that(testLogHandler) .hasLogAtLevelWithMessage( @@ -210,22 +268,23 @@ public class SessionUtilsTest { private void expectGetRegistrarFailure( String clientId, AccessType accessType, AuthResult authResult, String message) { + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(authResult, ADMIN_CLIENT_ID); + ForbiddenException exception = assertThrows( - ForbiddenException.class, - () -> sessionUtils.getRegistrarForUser(clientId, accessType, authResult)); + ForbiddenException.class, () -> registrarAccessor.getRegistrar(clientId, accessType)); assertThat(exception).hasMessageThat().contains(formatMessage(message, authResult, clientId)); - assertAboutLogs().that(testLogHandler).hasNoLogsAtLevel(Level.INFO); } /** If a user has access to a registrar, we should guess that registrar. */ @Test public void testGuessClientIdForUser_hasAccess_isNotAdmin() { - expectGuessRegistrarSuccess( - AUTHORIZED_USER, - DEFAULT_CLIENT_ID, - "Associating user {user} with found registrar {clientId}."); + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(AUTHORIZED_USER, ADMIN_CLIENT_ID); + + assertThat(registrarAccessor.guessClientId()).isEqualTo(DEFAULT_CLIENT_ID); } /** If a user doesn't have access to any registrars, guess returns nothing. */ @@ -241,40 +300,32 @@ public class SessionUtilsTest { */ @Test public void testGuessClientIdForUser_hasAccess_isAdmin() { - expectGuessRegistrarSuccess( - AUTHORIZED_ADMIN, - DEFAULT_CLIENT_ID, - "Associating user {user} with found registrar {clientId}."); + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(AUTHORIZED_ADMIN, ADMIN_CLIENT_ID); + + assertThat(registrarAccessor.guessClientId()).isEqualTo(DEFAULT_CLIENT_ID); } /** If an admin doesn't have access to a registrar, we should guess the ADMIN_CLIENT_ID. */ @Test public void testGuessClientIdForUser_noAccess_isAdmin() { - expectGuessRegistrarSuccess( - UNAUTHORIZED_ADMIN, - ADMIN_CLIENT_ID, - "User {user} is an admin with no associated registrar." - + " Automatically associating the user with configured client Id {clientId}."); + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(UNAUTHORIZED_ADMIN, ADMIN_CLIENT_ID); + + assertThat(registrarAccessor.guessClientId()).isEqualTo(ADMIN_CLIENT_ID); } /** - * If an admin is not associated with a registrar and there is no configured adminClientId, we - * can't guess the clientId. + * If an admin is not associated with a registrar and there is no configured adminClientId, but + * since it's an admin - we have read-only access to everything - return one of the existing + * registrars. */ @Test public void testGuessClientIdForUser_noAccess_isAdmin_adminClientIdEmpty() { - sessionUtils.registryAdminClientId = ""; - expectGuessRegistrarFailure( - UNAUTHORIZED_ADMIN, "User {user} isn't associated with any registrar"); - assertAboutLogs() - .that(testLogHandler) - .hasLogAtLevelWithMessage( - Level.INFO, - formatMessage( - "Cannot associate admin user {user} with configured client Id." - + " ClientId is null or empty.", - UNAUTHORIZED_ADMIN, - null)); + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(UNAUTHORIZED_ADMIN, ""); + + assertThat(registrarAccessor.guessClientId()).isAnyOf(ADMIN_CLIENT_ID, DEFAULT_CLIENT_ID); } /** @@ -283,24 +334,18 @@ public class SessionUtilsTest { */ @Test public void testGuessClientIdForUser_noAccess_isAdmin_adminClientIdInvalid() { - sessionUtils.registryAdminClientId = "NonexistentRegistrar"; - expectGuessRegistrarSuccess( - UNAUTHORIZED_ADMIN, - "NonexistentRegistrar", - "User {user} is an admin with no associated registrar." - + " Automatically associating the user with configured client Id {clientId}."); - } + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(UNAUTHORIZED_ADMIN, "NonexistentRegistrar"); - private void expectGuessRegistrarSuccess(AuthResult authResult, String clientId, String message) { - assertThat(sessionUtils.guessClientIdForUser(authResult)).isEqualTo(clientId); - assertAboutLogs() - .that(testLogHandler) - .hasLogAtLevelWithMessage(Level.INFO, formatMessage(message, authResult, clientId)); + assertThat(registrarAccessor.guessClientId()).isEqualTo("NonexistentRegistrar"); } private void expectGuessRegistrarFailure(AuthResult authResult, String message) { + AuthenticatedRegistrarAccessor registrarAccessor = + new AuthenticatedRegistrarAccessor(authResult, ADMIN_CLIENT_ID); + ForbiddenException exception = - assertThrows(ForbiddenException.class, () -> sessionUtils.guessClientIdForUser(authResult)); + assertThrows(ForbiddenException.class, () -> registrarAccessor.guessClientId()); assertThat(exception) .hasMessageThat() .contains(formatMessage(message, authResult, null)); @@ -311,6 +356,6 @@ public class SessionUtilsTest { new NullPointerTester() .setDefault(HttpServletRequest.class, req) .setDefault(HttpServletResponse.class, rsp) - .testAllPublicStaticMethods(SessionUtils.class); + .testAllPublicStaticMethods(AuthenticatedRegistrarAccessor.class); } } diff --git a/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java b/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java index 9e329e7ad..3e5f80112 100644 --- a/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java +++ b/javatests/google/registry/ui/server/registrar/ConsoleUiActionTest.java @@ -17,14 +17,15 @@ package google.registry.ui.server.registrar; import static com.google.common.net.HttpHeaders.LOCATION; import static com.google.common.truth.Truth.assertThat; import static google.registry.testing.DatastoreHelper.loadRegistrar; -import static google.registry.ui.server.registrar.SessionUtils.AccessType.READ_ONLY; +import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.READ; +import static google.registry.ui.server.registrar.AuthenticatedRegistrarAccessor.AccessType.UPDATE; import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY; -import static org.mockito.Matchers.any; 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.UserServiceFactory; +import com.google.common.collect.ImmutableSetMultimap; import com.google.common.net.MediaType; import google.registry.request.HttpException.ForbiddenException; import google.registry.request.auth.AuthLevel; @@ -53,7 +54,8 @@ public class ConsoleUiActionTest { .withUserService(UserInfo.create("marla.singer@example.com", "12345")) .build(); - private final SessionUtils sessionUtils = mock(SessionUtils.class); + private final AuthenticatedRegistrarAccessor registrarAccessor = + mock(AuthenticatedRegistrarAccessor.class); private final HttpServletRequest request = mock(HttpServletRequest.class); private final FakeResponse response = new FakeResponse(); private final ConsoleUiAction action = new ConsoleUiAction(); @@ -71,15 +73,25 @@ public class ConsoleUiActionTest { action.technicalDocsUrl = "http://example.com/technical-docs"; action.req = request; action.response = response; - action.sessionUtils = sessionUtils; + action.registrarAccessor = registrarAccessor; action.userService = UserServiceFactory.getUserService(); action.xsrfTokenManager = new XsrfTokenManager(new FakeClock(), action.userService); action.paramClientId = Optional.empty(); AuthResult authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false)); action.authResult = authResult; - when(sessionUtils.guessClientIdForUser(authResult)).thenReturn("TheRegistrar"); - when(sessionUtils.getRegistrarForUser("TheRegistrar", READ_ONLY, authResult)) + when(registrarAccessor.getRegistrar("TheRegistrar", READ)) .thenReturn(loadRegistrar("TheRegistrar")); + when(registrarAccessor.getAllClientIdWithAccess()) + .thenReturn( + ImmutableSetMultimap.of( + UPDATE, "TheRegistrar", + READ, "TheRegistrar", + UPDATE, "ReadWriteRegistrar", + READ, "ReadWriteRegistrar", + READ, "ReadOnlyRegistrar")); + when(registrarAccessor.guessClientId()).thenCallRealMethod(); + // Used for error message in guessClientId + registrarAccessor.authResult = authResult; } @Test @@ -116,8 +128,7 @@ public class ConsoleUiActionTest { @Test public void testUserDoesntHaveAccessToAnyRegistrar_showsWhoAreYouPage() { - when(sessionUtils.guessClientIdForUser(any(AuthResult.class))) - .thenThrow(new ForbiddenException("forbidden")); + when(registrarAccessor.getAllClientIdWithAccess()).thenReturn(ImmutableSetMultimap.of()); action.run(); assertThat(response.getPayload()).contains("

You need permission

"); assertThat(response.getPayload()).contains("not associated with Nomulus."); @@ -144,7 +155,7 @@ public class ConsoleUiActionTest { @Test public void testSettingClientId_notAllowed_showsNeedPermissionPage() { action.paramClientId = Optional.of("OtherClientId"); - when(sessionUtils.getRegistrarForUser("OtherClientId", READ_ONLY, action.authResult)) + when(registrarAccessor.getRegistrar("OtherClientId", READ)) .thenThrow(new ForbiddenException("forbidden")); action.run(); assertThat(response.getPayload()).contains("

You need permission

"); @@ -154,10 +165,18 @@ public class ConsoleUiActionTest { @Test public void testSettingClientId_allowed_showsRegistrarConsole() { action.paramClientId = Optional.of("OtherClientId"); - when(sessionUtils.getRegistrarForUser("OtherClientId", READ_ONLY, action.authResult)) + when(registrarAccessor.getRegistrar("OtherClientId", READ)) .thenReturn(loadRegistrar("TheRegistrar")); action.run(); assertThat(response.getPayload()).contains("Registrar Console"); assertThat(response.getPayload()).contains("reg-content-and-footer"); } + + @Test + public void testUserHasAccessAsTheRegistrar_showsClientIdChooser() { + action.run(); + assertThat(response.getPayload()).contains("