Compare commits

...
Author SHA1 Message Date
gbrodmanandGitHub bc44e095d4 Forbid subordinate host changes when superord domain is locked (#3171)
This is not explicitly called out that I could find in the RFCs, however
it is very clear from the RFCs that hosts are subordinate to their
domains and it stands to reason that updates to a subordinate host are
updates to the superordinate domain.

Section 1.1 of RFC 5732 specifies:

```
host name "ns1.example.com" has a subordinate relationship to domain
name "example.com". EPP actions (such as object transfers) that do not
preserve this relationship MUST be explicitly disallowed.
```

Allowing host updates (e.g. renames) opens up situations where this
relationship could be severed. We add analogous prohibitions on deletion
and creation of subordinate hosts as well.

b/534930957
2026-07-24 19:27:00 +00:00
gbrodmanandGitHub 355e8ba423 Don't claim that only one abuse contact is allowed (#3173)
nowhere in the RFCs does it say that only one contact is allowed, and
indeed there are many situations where it's actually an array of
contacts. We have a bunch of registrars that already have multiple abuse
contacts. I'm not sure why the comment originally claimed this, but it's
from years and years ago.

b/534931561
2026-07-24 17:01:09 +00:00
gbrodmanandGitHub dca5a010d5 Update registrar-update auth block for allowed TLDs + rlock (#3175)
Verified in support docs 2.19 and 2.33 that agents should have the
ability to edit allowed TLDs but should not have the ability to edit
whether registry lock is allowed. This reflects that.

b/534931470
2026-07-24 15:52:51 +00:00
gbrodmanandGitHub 147e7dabcb Also skip inline style optimization in sandbox (#3177) 2026-07-24 15:52:44 +00:00
gbrodmanandGitHub 0b8025d3db Disallow adding ADMIN contacts via console (#3160)
this is a potential security issue if we give users who only have
EDIT_REGISTRAR_DETAILS access to become admin PoCs, which can have a
different set of permissions.
2026-07-24 15:11:06 +00:00
Ben McIlwainandGitHub 9c4a558d7b Censor domain authcode by default in tools (#3170)
When inspecting domain entities with tools like GetDomainCommand, displaying
plaintext authentication codes is unnecessary and presents a security risk
for credential exposure in logs and bug ticket comments (b/537293980).

This commit modifies GetDomainCommand to redact sensitive authcodes by
default using an in-memory entity builder snapshot while preserving database
records. An optional --show_authcode flag with arity 1 is introduced to
explicitly display authcodes when required for transfer authorization.

BUG= http://b/537293980
2026-07-24 14:26:38 +00:00
Ben McIlwainandGitHub 7347dbf762 Add arity to registrar_request boolean flags (#3168)
Define arity 1 on requestedByRegistrar options in BulkDomainTransferCommand
and DeleteDomainCommand to accept explicit false arguments without parsing
failures. Up-to-date tests verify space-separated and equals argument syntax.

BUG= http://b/537308816
2026-07-24 14:26:34 +00:00
gbrodmanandGitHub 53b6044ff8 Fix CSP directive in production (#3176)
In prod, when loading the console, we were failing to get some
scripts/styles with the error "Executing
  inline event handler violates the following Content Security Policy directive 'script-src
  'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-
  ...') is required to enable inline execution. Note that hashes do not apply to event
  handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword
  is present. The action has been blocked."

We fix this by disabling inline-critical optimization for prod in the
angular file.

In addition, the web.xml header values are comma-separated -- we forgot
one comma before.
2026-07-23 21:00:18 +00:00
Juan CelhayandGitHub 12ab7f47e2 Fix oversized postal-address fields (#3165) 2026-07-23 14:29:54 +00:00
gbrodmanandGitHub d3a1d0709e Only grant IAP access to the console for new users (#3167)
Currently we grant users IAP_SECURED_WEB_APP_USER_ROLE access to the
entire project upon creation if there is no preexisting group set up for
them to use. These users only need to be able to access the console, so
we should restrict it to the console.

If there is a group (which we do have in our prod + sandbox
configuration) then we just add them to that group instead and that
group controls all the privileges. So this is a no-op for our setup.

We prioritize the group -- if both the group and the IAP service ID are
specified, we'll still just add them to the group

b/534931170
2026-07-22 16:53:41 +00:00
gbrodmanandGitHub f0fae9898b Fix GCS gradle hash verification in Cloud Build (#3174) 2026-07-22 13:09:09 +00:00
42 changed files with 743 additions and 154 deletions
+16
View File
@@ -61,6 +61,14 @@
"with": "src/environments/environment.prod.ts"
}
],
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all"
},
"sandbox": {
@@ -82,6 +90,14 @@
"with": "src/environments/environment.sandbox.ts"
}
],
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all"
},
"crash": {
@@ -165,6 +165,12 @@ public final class RegistryConfig {
return config.misc.isEmailSendingEnabled;
}
@Provides
@Config("consoleIapServiceId")
public static Optional<String> provideConsoleIapServiceId(RegistryConfigSettings config) {
return Optional.ofNullable(config.registrarConsole.consoleIapServiceId);
}
/**
* The e-mail address for general support. Used in the "contact-us" section of the registrar
* console.
@@ -187,6 +187,7 @@ public class RegistryConfigSettings {
/** Configuration for the web-based registrar console. */
public static class RegistrarConsole {
public String consoleIapServiceId;
public String dumFileName;
public String supportPhoneNumber;
public String supportEmailAddress;
@@ -390,6 +390,11 @@ rde:
sshIdentityEmailAddress: rde@example.com
registrarConsole:
# IAP service ID of the HTTP(s) load balancer that serves the console-api backend.
# Users created in the console will be bound only to this service to limit access.
# This is only applicable to non-group-based auth (if there is no consoleUserGroupEmailAddress)
consoleIapServiceId: null
# DUM download file name, excluding the extension
dumFileName: dum_file_name
@@ -20,6 +20,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist
import static google.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotServerUpdateProhibited;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
import static google.registry.model.EppResourceUtils.createRepoId;
import static google.registry.model.reporting.HistoryEntry.Type.HOST_CREATE;
@@ -73,6 +74,7 @@ import java.util.Optional;
* @error {@link HostFlowUtils.HostNameNotPunyCodedException}
* @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException}
* @error {@link HostFlowUtils.SuperordinateDomainInPendingDeleteException}
* @error {@link HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException}
* @error {@link SubordinateHostMustHaveIpException}
* @error {@link UnexpectedExternalHostIpException}
*/
@@ -107,6 +109,7 @@ public final class HostCreateFlow implements MutatingFlow {
Optional<Domain> superordinateDomain =
lookupSuperordinateDomain(validateHostName(targetId), now);
verifySuperordinateDomainNotInPendingDelete(superordinateDomain.orElse(null));
verifySuperordinateDomainNotServerUpdateProhibited(superordinateDomain.orElse(null));
verifySuperordinateDomainOwnership(registrarId, superordinateDomain.orElse(null));
boolean willBeSubordinate = superordinateDomain.isPresent();
boolean hasIpAddresses = !isNullOrEmpty(command.getInetAddresses());
@@ -22,6 +22,7 @@ import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotServerUpdateProhibited;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -33,7 +34,7 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.EppResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
@@ -62,6 +63,7 @@ import java.time.Instant;
* @error {@link HostFlowUtils.HostNameNotLowerCaseException}
* @error {@link HostFlowUtils.HostNameNotNormalizedException}
* @error {@link HostFlowUtils.HostNameNotPunyCodedException}
* @error {@link HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException}
*/
@ReportingSpec(ActivityReportField.HOST_DELETE)
public final class HostDeleteFlow implements MutatingFlow {
@@ -90,12 +92,15 @@ public final class HostDeleteFlow implements MutatingFlow {
if (!isSuperuser) {
verifyNoDisallowedStatuses(existingHost, DELETE_PROHIBITED_STATUSES);
// Hosts transfer with their superordinate domains, so for hosts with a superordinate domain,
// the client id, needs to be read off of it.
EppResource owningResource =
existingHost.isSubordinate()
? tm().loadByKey(existingHost.getSuperordinateDomain()).cloneProjectedAtTime(now)
: existingHost;
verifyResourceOwnership(registrarId, owningResource);
// the client id needs to be read off of it.
if (existingHost.isSubordinate()) {
Domain superordinateDomain =
tm().loadByKey(existingHost.getSuperordinateDomain()).cloneProjectedAtTime(now);
verifyResourceOwnership(registrarId, superordinateDomain);
verifySuperordinateDomainNotServerUpdateProhibited(superordinateDomain);
} else {
verifyResourceOwnership(registrarId, existingHost);
}
}
Host newHost = existingHost.asBuilder().setStatusValues(null).setDeletionTime(now).build();
if (existingHost.isSubordinate()) {
@@ -174,6 +174,31 @@ public class HostFlowUtils {
}
}
/**
* Ensure that the superordinate domain does not have {@code serverUpdateProhibited} status.
*
* <p>Subordinate hosts publish glue A/AAAA records in the TLD zone on behalf of their
* superordinate domain. If the superordinate domain is registry-locked (carries {@link
* StatusValue#SERVER_UPDATE_PROHIBITED}), non-superuser modifications to its subordinate hosts
* must be rejected as well; otherwise a compromised sponsoring registrar could hijack DNS for a
* locked domain by swapping the glue addresses of its in-bailiwick nameservers.
*/
static void verifySuperordinateDomainNotServerUpdateProhibited(Domain superordinateDomain)
throws EppException {
if ((superordinateDomain != null)
&& superordinateDomain.getStatusValues().contains(StatusValue.SERVER_UPDATE_PROHIBITED)) {
throw new SuperordinateDomainServerUpdateProhibitedException();
}
}
/** Superordinate domain for this hostname has serverUpdateProhibited status. */
static class SuperordinateDomainServerUpdateProhibitedException
extends StatusProhibitsOperationException {
public SuperordinateDomainServerUpdateProhibitedException() {
super("Superordinate domain for this hostname has serverUpdateProhibited status");
}
}
/** Host names are limited to 253 characters. */
static class HostNameTooLongException extends ParameterValueRangeErrorException {
public HostNameTooLongException() {
@@ -28,6 +28,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotServerUpdateProhibited;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
import static google.registry.model.reporting.HistoryEntry.Type.HOST_UPDATE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -98,6 +99,7 @@ import java.util.Optional;
* @error {@link HostFlowUtils.InvalidHostNameException}
* @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException}
* @error {@link HostFlowUtils.SuperordinateDomainInPendingDeleteException}
* @error {@link HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException}
* @error {@link CannotAddIpToExternalHostException}
* @error {@link CannotRemoveSubordinateHostLastIpException}
* @error {@link CannotRenameExternalHostException}
@@ -152,7 +154,12 @@ public final class HostUpdateFlow implements MutatingFlow {
verifySuperordinateDomainNotInPendingDelete(newSuperordinateDomain.orElse(null));
EppResource owningResource = firstNonNull(oldSuperordinateDomain, existingHost);
verifyUpdateAllowed(
command, existingHost, newSuperordinateDomain.orElse(null), owningResource, isHostRename);
command,
existingHost,
oldSuperordinateDomain,
newSuperordinateDomain.orElse(null),
owningResource,
isHostRename);
if (isHostRename && ForeignKeyUtils.loadKey(Host.class, newHostName, now).isPresent()) {
throw new HostAlreadyExistsException(newHostName);
}
@@ -211,30 +218,38 @@ public final class HostUpdateFlow implements MutatingFlow {
private void verifyUpdateAllowed(
Update command,
Host existingHost,
Domain oldSuperordinateDomain,
Domain newSuperordinateDomain,
EppResource owningResource,
boolean isHostRename)
throws EppException {
if (!isSuperuser) {
// Verify that the host belongs to this registrar, either directly or because it is currently
// subordinate to a domain owned by this registrar.
verifyResourceOwnership(registrarId, owningResource);
if (isHostRename && !existingHost.isSubordinate()) {
throw new CannotRenameExternalHostException();
}
// Verify that the new superordinate domain belongs to this registrar.
verifySuperordinateDomainOwnership(registrarId, newSuperordinateDomain);
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
ImmutableSet<StatusValue> statusesToRemove = command.getInnerRemove().getStatusValues();
// If the resource is marked with clientUpdateProhibited, and this update does not clear that
// status, then the update must be disallowed.
if (existingHost.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
&& !statusesToRemove.contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
throw new ResourceHasClientUpdateProhibitedException();
}
verifyAllStatusesAreClientSettable(union(statusesToAdd, statusesToRemove));
}
verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES);
if (isSuperuser) {
return;
}
// Verify that the host belongs to this registrar, either directly or because it is currently
// subordinate to a domain owned by this registrar.
verifyResourceOwnership(registrarId, owningResource);
if (isHostRename && !existingHost.isSubordinate()) {
throw new CannotRenameExternalHostException();
}
// Verify that the new superordinate domain belongs to this registrar.
verifySuperordinateDomainOwnership(registrarId, newSuperordinateDomain);
// Subordinate hosts inherit the registry-lock protection of their superordinate domain: if
// either the current or the target superordinate domain carries SERVER_UPDATE_PROHIBITED,
// non-superuser updates (including glue IP changes and renames) must be rejected so that a
// compromised sponsoring registrar cannot hijack DNS for a registry-locked domain.
verifySuperordinateDomainNotServerUpdateProhibited(oldSuperordinateDomain);
verifySuperordinateDomainNotServerUpdateProhibited(newSuperordinateDomain);
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
ImmutableSet<StatusValue> statusesToRemove = command.getInnerRemove().getStatusValues();
// If the resource is marked with clientUpdateProhibited, and this update does not clear that
// status, then the update must be disallowed.
if (existingHost.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
&& !statusesToRemove.contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
throw new ResourceHasClientUpdateProhibitedException();
}
verifyAllStatusesAreClientSettable(union(statusesToAdd, statusesToRemove));
}
private static void verifyHasIpsIffIsExternal(Update command, Host existingHost, Host newHost)
@@ -197,8 +197,7 @@ public final class OteAccountBuilder {
registrars.stream()
.collect(
toImmutableMap(
Registrar::getRegistrarId,
registrar -> RegistrarRole.ACCOUNT_MANAGER)))
Registrar::getRegistrarId, _ -> RegistrarRole.ACCOUNT_MANAGER)))
.build())
.build());
return this;
@@ -265,10 +264,18 @@ public final class OteAccountBuilder {
/** Grants the users permission to pass IAP. */
public void grantIapPermission(
Optional<String> groupEmailAddress, CloudTasksUtils cloudTasksUtils, IamClient iamClient) {
Optional<String> groupEmailAddress,
Optional<String> consoleIapServiceId,
CloudTasksUtils cloudTasksUtils,
IamClient iamClient) {
for (User user : users) {
User.grantIapPermission(
user.getEmailAddress(), groupEmailAddress, cloudTasksUtils, null, iamClient);
user.getEmailAddress(),
groupEmailAddress,
consoleIapServiceId,
cloudTasksUtils,
null,
iamClient);
}
}
@@ -86,6 +86,7 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
public static void grantIapPermission(
String emailAddress,
Optional<String> groupEmailAddress,
Optional<String> consoleIapServiceId,
@Nullable CloudTasksUtils cloudTasksUtils,
@Nullable ServiceConnection connection,
IamClient iamClient) {
@@ -93,14 +94,14 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
return;
}
checkArgument(
cloudTasksUtils != null || connection != null,
"At least one of cloudTasksUtils or connection must be set");
cloudTasksUtils == null ^ connection == null,
"Precisely one of cloudTasksUtils or connection can be set");
checkArgument(
cloudTasksUtils == null || connection == null,
"Only one of cloudTasksUtils or connection can be set");
groupEmailAddress.isPresent() || consoleIapServiceId.isPresent(),
"At least one of groupEmailAddress or consoleIapServiceId must be present");
if (groupEmailAddress.isEmpty()) {
logger.atInfo().log("Granting IAP role to user %s", emailAddress);
iamClient.addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
iamClient.addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE, consoleIapServiceId.get());
} else {
logger.atInfo().log("Adding %s to group %s", emailAddress, groupEmailAddress.get());
if (cloudTasksUtils != null) {
@@ -129,11 +130,8 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
return;
}
checkArgument(
cloudTasksUtils != null || connection != null,
"At least one of cloudTasksUtils or connection must be set");
checkArgument(
cloudTasksUtils == null || connection == null,
"Only one of cloudTasksUtils or connection can be set");
cloudTasksUtils == null ^ connection == null,
"Precisely one of cloudTasksUtils or connection can be set");
if (groupEmailAddress.isEmpty()) {
logger.atInfo().log("Removing IAP role from user %s", emailAddress);
iamClient.removeBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
@@ -49,6 +49,10 @@ import java.util.stream.Stream;
* also matches the "addrType" type from <a
* href="http://tools.ietf.org/html/draft-lozano-tmch-smd">Mark and Signed Mark Objects Mapping</a>.
*
* <p>The lengths of the fields are limited to match the constraints defined in XSD schemas like
* {@code rde-registrar.xsd}. Specifically, {@code zip} is limited to 16 characters and {@code
* city}, {@code state}, and each {@code street} line are limited to 255 characters.
*
* @see google.registry.model.mark.MarkAddress
* @see google.registry.model.registrar.RegistrarAddress
*/
@@ -169,11 +173,20 @@ public class Address extends ImmutableObject
street == null || (!street.isEmpty() && street.size() <= 3),
"Street address must have [1-3] lines: %s",
street);
//noinspection ConstantConditions
if (street != null) {
checkArgument(
street.stream().noneMatch(String::isEmpty),
"Street address cannot contain empty string: %s",
street);
checkArgument(
street.stream().allMatch(s -> s.length() <= 255),
"Street address lines cannot be longer than 255 characters");
}
checkArgument(
street == null || street.stream().noneMatch(String::isEmpty),
"Street address cannot contain empty string: %s",
street);
city == null || city.length() <= 255, "City cannot be longer than 255 characters");
checkArgument(
state == null || state.length() <= 255, "State cannot be longer than 255 characters");
checkArgument(zip == null || zip.length() <= 16, "Zip cannot be longer than 16 characters");
checkArgument(
countryCode == null || countryCode.length() == 2,
"Country code should be a 2 character string");
@@ -196,9 +209,9 @@ public class Address extends ImmutableObject
public Builder<T> setStreet(ImmutableList<String> street) {
getInstance().street = street;
getInstance().streetLine1 = street.get(0);
getInstance().streetLine2 = street.size() >= 2 ? street.get(1) : null;
getInstance().streetLine3 = street.size() == 3 ? street.get(2) : null;
getInstance().streetLine1 = (street != null && street.size() >= 1) ? street.get(0) : null;
getInstance().streetLine2 = (street != null && street.size() >= 2) ? street.get(1) : null;
getInstance().streetLine3 = (street != null && street.size() == 3) ? street.get(2) : null;
return this;
}
@@ -631,7 +631,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
}
/**
* Returns the {@link RegistrarPoc} that is the RDAP abuse contact for this registrar, or empty if
* Returns a {@link RegistrarPoc} that is the RDAP abuse contact for this registrar, or empty if
* one does not exist.
*/
public Optional<RegistrarPoc> getRdapAbuseContact() {
@@ -88,7 +88,8 @@ public class BulkDomainTransferCommand extends ConfirmingCommand implements Comm
@Parameter(
names = {"--registrar_request"},
description = "Whether the change was requested by a registrar.")
description = "Whether the change was requested by a registrar.",
arity = 1)
private boolean requestedByRegistrar = false;
@Parameter(
@@ -38,6 +38,10 @@ public class CreateUserCommand extends CreateOrUpdateUserCommand implements Comm
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@Inject
@Config("consoleIapServiceId")
Optional<String> consoleIapServiceId;
@Nullable
@Override
User getExistingUser(String email) {
@@ -49,7 +53,8 @@ public class CreateUserCommand extends CreateOrUpdateUserCommand implements Comm
@Override
protected String execute() throws Exception {
String ret = super.execute();
grantIapPermission(email, maybeGroupEmailAddress, null, connection, iamClient);
grantIapPermission(
email, maybeGroupEmailAddress, consoleIapServiceId, null, connection, iamClient);
return ret;
}
@@ -52,7 +52,8 @@ final class DeleteDomainCommand extends MutatingEppToolCommand {
@Parameter(
names = {"--registrar_request"},
description = "Whether the change was requested by a registrar.")
description = "Whether the change was requested by a registrar.",
arity = 1)
private boolean requestedByRegistrar = false;
@Override
@@ -20,6 +20,8 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.util.DomainNameUtils;
import java.util.List;
@@ -32,6 +34,12 @@ final class GetDomainCommand extends GetEppResourceCommand {
@Parameter(names = "--show_deleted", description = "Include deleted domains in the print out")
private boolean showDeleted = false;
@Parameter(
names = "--show_authcode",
description = "Include domain authentication code in output",
arity = 1)
private boolean showAuthcode = false;
@Parameter(
description = "Fully qualified domain name(s)",
required = true)
@@ -51,14 +59,26 @@ final class GetDomainCommand extends GetEppResourceCommand {
.stream()
.forEach(
d -> {
printResource("Domain", canonicalDomain, Optional.of(d));
printResource(
"Domain", canonicalDomain, Optional.of(censorAuthcode(d)));
}));
} else {
printResource(
"Domain",
canonicalDomain,
ForeignKeyUtils.loadResource(Domain.class, canonicalDomain, readTimestamp));
ForeignKeyUtils.loadResource(Domain.class, canonicalDomain, readTimestamp)
.map(this::censorAuthcode));
}
}
}
private Domain censorAuthcode(Domain domain) {
if (showAuthcode || domain.getAuthInfo() == null) {
return domain;
}
return domain
.asBuilder()
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("[REDACTED]")))
.build();
}
}
@@ -16,6 +16,7 @@ package google.registry.tools;
import com.google.api.services.cloudresourcemanager.CloudResourceManager;
import com.google.api.services.cloudresourcemanager.model.Binding;
import com.google.api.services.cloudresourcemanager.model.Expr;
import com.google.api.services.cloudresourcemanager.model.GetIamPolicyRequest;
import com.google.api.services.cloudresourcemanager.model.Policy;
import com.google.api.services.cloudresourcemanager.model.SetIamPolicyRequest;
@@ -31,7 +32,10 @@ import java.util.Optional;
@Singleton
public class IamClient {
private static final String MEMBER_FORMAT = "user:%s";
private static final String IAP_ACCESS_EXPRESSION_FORMAT =
"resource.name == 'projects/%s/iap_web/compute/services/%s'";
private final CloudResourceManager resourceManager;
private final String projectId;
@@ -60,7 +64,7 @@ public class IamClient {
*
* <p>No-op if the role is already bound to the account.
*/
public void addBinding(String account, String role) {
public void addBinding(String account, String role, String consoleIapServiceId) {
String member = String.format(MEMBER_FORMAT, account);
Policy policy = getPolicy();
Binding binding =
@@ -69,7 +73,21 @@ public class IamClient {
.findFirst()
.orElseGet(
() -> {
Binding newBinding = new Binding().setRole(role).setMembers(new ArrayList<>());
Binding newBinding =
new Binding()
.setRole(role)
.setMembers(new ArrayList<>())
.setCondition(
new Expr()
.setTitle("Registrar Console IAP access")
.setDescription(
"Restrict IAP access only to the Registrar Console HTTP(s)"
+ " load balancer")
.setExpression(
String.format(
IAP_ACCESS_EXPRESSION_FORMAT,
projectId,
consoleIapServiceId)));
policy.getBindings().add(newBinding);
return newBinding;
});
@@ -86,6 +86,10 @@ final class SetupOteCommand extends ConfirmingCommand {
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@Inject
@Config("consoleIapServiceId")
Optional<String> consoleIapServiceId;
OteAccountBuilder oteAccountBuilder;
String password;
@@ -138,7 +142,8 @@ you sure you didn't mean to run this against sandbox (e.g. "-e SANDBOX")?\
@Override
public String execute() {
ImmutableMap<String, String> clientIdToTld = oteAccountBuilder.buildAndPersist();
oteAccountBuilder.grantIapPermission(maybeGroupEmailAddress, cloudTasksUtils, iamClient);
oteAccountBuilder.grantIapPermission(
maybeGroupEmailAddress, consoleIapServiceId, cloudTasksUtils, iamClient);
StringBuilder output = new StringBuilder();
@@ -127,6 +127,14 @@ public abstract class ConsoleApiAction implements Runnable {
}
}
protected static void checkGlobalPermission(User user, ConsolePermission permission) {
if (!user.getUserRoles().hasGlobalPermission(permission)) {
throw new ConsolePermissionForbiddenException(
String.format(
"User %s does not have global permission %s", user.getEmailAddress(), permission));
}
}
protected void postHandler(User user) {
throw new UnsupportedOperationException("Console API POST handler not implemented");
}
@@ -62,6 +62,7 @@ public class ConsoleOteAction extends ConsoleApiAction {
private final StringGenerator passwordGenerator;
private final Optional<OteCreateData> oteCreateData;
private final Optional<String> maybeGroupEmailAddress;
private final Optional<String> consoleIapServiceId;
private final IamClient iamClient;
private final String registrarId;
@@ -71,12 +72,14 @@ public class ConsoleOteAction extends ConsoleApiAction {
IamClient iamClient,
@Parameter("registrarId") String registrarId, // Get request param
@Config("gSuiteConsoleUserGroupEmailAddress") Optional<String> maybeGroupEmailAddress,
@Config("consoleIapServiceId") Optional<String> consoleIapServiceId,
@Named("base58StringGenerator") StringGenerator passwordGenerator,
@Parameter("oteCreateData") Optional<OteCreateData> oteCreateData) {
super(consoleApiParams);
this.passwordGenerator = passwordGenerator;
this.oteCreateData = oteCreateData;
this.maybeGroupEmailAddress = maybeGroupEmailAddress;
this.consoleIapServiceId = consoleIapServiceId;
this.iamClient = iamClient;
this.registrarId = registrarId;
}
@@ -105,7 +108,8 @@ public class ConsoleOteAction extends ConsoleApiAction {
.setPassword(password);
ImmutableMap<String, String> registrarIdToTld = oteAccountBuilder.buildAndPersist();
oteAccountBuilder.grantIapPermission(maybeGroupEmailAddress, cloudTasksUtils, iamClient);
oteAccountBuilder.grantIapPermission(
maybeGroupEmailAddress, consoleIapServiceId, cloudTasksUtils, iamClient);
consoleApiParams.response().setStatus(SC_OK);
consoleApiParams
.response()
@@ -15,6 +15,7 @@
package google.registry.ui.server.console;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.DateTimeUtils.START_INSTANT;
@@ -38,7 +39,6 @@ import google.registry.util.RegistryEnvironment;
import jakarta.inject.Inject;
import java.time.Instant;
import java.util.Optional;
import java.util.stream.Collectors;
@Action(
service = Service.CONSOLE,
@@ -69,22 +69,23 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
tm().transact(
() -> {
Optional<Registrar> existingRegistrar =
Registrar.loadByRegistrarId(registrarParam.getRegistrarId());
checkArgument(
!existingRegistrar.isEmpty(),
"Registrar with registrarId %s doesn't exists",
registrarParam.getRegistrarId());
Registrar existingRegistrar =
Registrar.loadByRegistrarId(registrarParam.getRegistrarId())
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Registrar %s does not exist",
registrarParam.getRegistrarId())));
// Only allow modifying allowed TLDs if we're in a non-PRODUCTION environment, if the
// registrar is not REAL, or the registrar has a RDAP abuse contact set.
if (!registrarParam.getAllowedTlds().isEmpty()) {
boolean isRealRegistrar =
Registrar.Type.REAL.equals(existingRegistrar.get().getType());
boolean isRealRegistrar = Registrar.Type.REAL.equals(existingRegistrar.getType());
if (RegistryEnvironment.PRODUCTION.equals(RegistryEnvironment.get())
&& isRealRegistrar) {
checkArgumentPresent(
existingRegistrar.get().getRdapAbuseContact(),
existingRegistrar.getRdapAbuseContact(),
"Cannot modify allowed TLDs if there is no RDAP abuse contact set. Please"
+ " use the \"nomulus registrar_contact\" command on this registrar to"
+ " set a RDAP abuse contact.");
@@ -103,20 +104,27 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
var updatedRegistrarBuilder =
existingRegistrar
.get()
.asBuilder()
.setLastPocVerificationDate(newLastPocVerificationDate);
if (user.getUserRoles()
.hasGlobalPermission(ConsolePermission.EDIT_REGISTRAR_DETAILS)) {
updatedRegistrarBuilder =
updatedRegistrarBuilder
.setAllowedTlds(
registrarParam.getAllowedTlds().stream()
.map(DomainNameUtils::canonicalizeHostname)
.collect(Collectors.toSet()))
.setRegistryLockAllowed(registrarParam.isRegistryLockAllowed())
.setLastPocVerificationDate(newLastPocVerificationDate);
if (!registrarParam.getAllowedTlds().equals(existingRegistrar.getAllowedTlds())) {
// The global permission EDIT_REGISTRAR_DETAILS signifies a support agent who *does*
// have the ability to edit allowed TLDs. See support docs 2.19: "Enabling TLD
// Access in Production"
checkGlobalPermission(user, ConsolePermission.EDIT_REGISTRAR_DETAILS);
updatedRegistrarBuilder.setAllowedTlds(
registrarParam.getAllowedTlds().stream()
.map(DomainNameUtils::canonicalizeHostname)
.collect(toImmutableSet()));
}
if (registrarParam.isRegistryLockAllowed()
!= existingRegistrar.isRegistryLockAllowed()) {
// Enabling registry lock requires a support lead or FTE, which maps to
// MANAGE_REGISTRARS. See support docs 2.33: "Registry Lock Onboarding Process"
checkGlobalPermission(user, ConsolePermission.MANAGE_REGISTRARS);
updatedRegistrarBuilder.setRegistryLockAllowed(
registrarParam.isRegistryLockAllowed());
}
var updatedRegistrar = updatedRegistrarBuilder.build();
@@ -126,14 +134,11 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setDescription(updatedRegistrar.getRegistrarId()));
logConsoleChangesIfNecessary(updatedRegistrar, existingRegistrar.get());
logConsoleChangesIfNecessary(updatedRegistrar, existingRegistrar);
sendExternalUpdatesIfNecessary(
EmailInfo.create(
existingRegistrar.get(),
updatedRegistrar,
ImmutableSet.of(),
ImmutableSet.of()));
existingRegistrar, updatedRegistrar, ImmutableSet.of(), ImmutableSet.of()));
});
consoleApiParams.response().setStatus(SC_OK);
@@ -79,6 +79,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
private final StringGenerator passwordGenerator;
private final Optional<UserData> userData;
private final Optional<String> maybeGroupEmailAddress;
private final Optional<String> consoleIapServiceId;
private final IamClient iamClient;
private final String gSuiteDomainName;
@@ -89,6 +90,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
IamClient iamClient,
@Config("gSuiteDomainName") String gSuiteDomainName,
@Config("gSuiteConsoleUserGroupEmailAddress") Optional<String> maybeGroupEmailAddress,
@Config("consoleIapServiceId") Optional<String> consoleIapServiceId,
@Named("base58StringGenerator") StringGenerator passwordGenerator,
@Parameter("userData") Optional<UserData> userData,
@Parameter("registrarId") String registrarId) {
@@ -98,6 +100,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
this.passwordGenerator = passwordGenerator;
this.userData = userData;
this.maybeGroupEmailAddress = maybeGroupEmailAddress;
this.consoleIapServiceId = consoleIapServiceId;
this.iamClient = iamClient;
this.gSuiteDomainName = gSuiteDomainName;
}
@@ -256,7 +259,8 @@ public class ConsoleUsersAction extends ConsoleApiAction {
User.Builder builder = new User.Builder().setUserRoles(userRoles).setEmailAddress(newEmail);
tm().put(builder.build());
User.grantIapPermission(newEmail, maybeGroupEmailAddress, cloudTasksUtils, null, iamClient);
User.grantIapPermission(
newEmail, maybeGroupEmailAddress, consoleIapServiceId, cloudTasksUtils, null, iamClient);
sendConfirmationEmail(registrarId, newEmail, "Created user");
consoleApiParams.response().setStatus(SC_CREATED);
consoleApiParams
@@ -228,17 +228,23 @@ public class ContactAction extends ConsoleApiAction {
enforcePrimaryContactRestrictions(oldContactsByType, newContactsByType);
ensurePhoneNumberNotRemovedForContactTypes(oldContactsByType, newContactsByType, Type.TECH);
Optional<RegistrarPoc> domainRdapAbuseContact =
getDomainRdapVisibleAbuseContact(updatedContacts);
// If the new set has a domain RDAP abuse contact, it must have a phone number.
if (domainRdapAbuseContact.isPresent()
&& domainRdapAbuseContact.get().getPhoneNumber() == null) {
throw new ContactRequirementException(
"The abuse contact visible in domain RDAP query must have a phone number");
}
ImmutableSet<RegistrarPoc> abusePocs =
updatedContacts.stream()
.filter(RegistrarPoc::getVisibleInDomainRdapAsAbuse)
.collect(toImmutableSet());
// All abuse POCs must have a phone number attached
abusePocs.forEach(
poc -> {
if (poc.getPhoneNumber() == null) {
throw new ContactRequirementException(
"The abuse contact visible in domain RDAP query must have a phone number");
}
});
// If there was a domain RDAP abuse contact in the old set, the new set must have one.
if (getDomainRdapVisibleAbuseContact(existingContacts).isPresent()
&& domainRdapAbuseContact.isEmpty()) {
if (existingContacts.stream().anyMatch(RegistrarPoc::getVisibleInDomainRdapAsAbuse)
&& abusePocs.isEmpty()) {
throw new ContactRequirementException(
"An abuse contact visible in domain RDAP query must be designated");
}
@@ -255,26 +261,14 @@ public class ContactAction extends ConsoleApiAction {
newContactsByType.get(Type.ADMIN).stream()
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableSet());
if (!newAdminEmails.containsAll(oldAdminEmails)) {
throw new ContactRequirementException(
"Cannot remove or change the email address of primary contacts");
// ADMIN (primary) PoC emails are used for EPP password resets and potentially other
// security-sensitive tasks. A user with only EDIT_REGISTRAR_DETAILS should not be able to
// change this.
if (!newAdminEmails.equals(oldAdminEmails)) {
throw new ContactRequirementException("Cannot alter the set of primary contacts");
}
}
/**
* Retrieves the registrar contact whose phone number and email address is visible in domain RDAP
* query as abuse contact (if any).
*
* <p>Frontend processing ensures that only one contact can be set as abuse contact in domain RDAP
* record.
*
* <p>Therefore, it is possible to return inside the loop once one such contact is found.
*/
private static Optional<RegistrarPoc> getDomainRdapVisibleAbuseContact(
Set<RegistrarPoc> contacts) {
return contacts.stream().filter(RegistrarPoc::getVisibleInDomainRdapAsAbuse).findFirst();
}
/**
* Ensure that for each given registrar type, a phone number is present after update, if there was
* one before.
@@ -51,6 +51,7 @@ import google.registry.flows.host.HostFlowUtils.InvalidHostNameException;
import google.registry.flows.host.HostFlowUtils.IpAddressNotRoutableException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
@@ -413,6 +414,22 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, Host> {
.marshalsToXml();
}
@Test
void testFailure_superordinateDomainIsServerUpdateProhibited() {
createTld("tld");
persistResource(
DatabaseHelper.newDomain("example.tld")
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
.build());
setEppHostCreateInputWithIps("ns1.example.tld");
SuperordinateDomainServerUpdateProhibitedException thrown =
assertThrows(SuperordinateDomainServerUpdateProhibitedException.class, this::runFlow);
assertThat(thrown)
.hasMessageThat()
.contains("Superordinate domain for this hostname has serverUpdateProhibited status");
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
@@ -42,6 +42,7 @@ import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException;
import google.registry.flows.host.HostFlowUtils.HostNameNotLowerCaseException;
import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException;
import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException;
import google.registry.model.domain.Domain;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
@@ -360,6 +361,25 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, Host> {
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_superordinateDomainIsServerUpdateProhibited() {
createTld("tld");
Domain domain =
persistResource(
DatabaseHelper.newDomain("example.tld")
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
.build());
persistResource(
newHost("ns1.example.tld").asBuilder().setSuperordinateDomain(domain.createVKey()).build());
clock.advanceOneMilli();
SuperordinateDomainServerUpdateProhibitedException thrown =
assertThrows(SuperordinateDomainServerUpdateProhibitedException.class, this::runFlow);
assertThat(thrown)
.hasMessageThat()
.contains("Superordinate domain for this hostname has serverUpdateProhibited status");
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
persistActiveHost("ns1.example.tld");
@@ -68,6 +68,7 @@ import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException;
import google.registry.flows.host.HostFlowUtils.IpAddressNotRoutableException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException;
import google.registry.flows.host.HostUpdateFlow.CannotAddIpToExternalHostException;
import google.registry.flows.host.HostUpdateFlow.CannotRemoveSubordinateHostLastIpException;
import google.registry.flows.host.HostUpdateFlow.CannotRenameExternalHostException;
@@ -889,6 +890,30 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, Host> {
.contains("Superordinate domain for this hostname is in pending delete");
}
@Test
void testFailure_superordinateDomainIsServerUpdateProhibited() throws Exception {
setEppHostUpdateInput(
"ns1.example.tld",
"ns2.example.tld",
"<host:addr ip=\"v4\">192.0.2.22</host:addr>",
"<host:addr ip=\"v6\">1080:0:0:0:8:800:200C:417A</host:addr>");
createTld("tld");
Domain domain =
persistResource(
DatabaseHelper.newDomain("example.tld")
.asBuilder()
.setSubordinateHosts(ImmutableSet.of(oldHostName()))
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
.build());
persistActiveSubordinateHost(oldHostName(), domain);
clock.advanceOneMilli();
SuperordinateDomainServerUpdateProhibitedException thrown =
assertThrows(SuperordinateDomainServerUpdateProhibitedException.class, this::runFlow);
assertThat(thrown)
.hasMessageThat()
.contains("Superordinate domain for this hostname has serverUpdateProhibited status");
}
@Test
void testFailure_neverExisted() throws Exception {
ResourceDoesNotExistException thrown =
@@ -116,6 +116,7 @@ public final class OteAccountBuilderTest {
public static void verifyIapPermission(
@Nullable String emailAddress,
Optional<String> maybeGroupEmailAddress,
Optional<String> consoleIapServiceId,
CloudTasksHelper cloudTasksHelper,
IamClient iamClient) {
if (emailAddress == null) {
@@ -125,7 +126,8 @@ public final class OteAccountBuilderTest {
String groupEmailAddress = maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress == null) {
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verify(iamClient).addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE, consoleIapServiceId.get());
} else {
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
@@ -146,9 +148,32 @@ public final class OteAccountBuilderTest {
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
OteAccountBuilder.forRegistrarId("myclientid")
.addUser("email@example.com")
.grantIapPermission(Optional.of("console@example.com"), cloudTasksUtils, iamClient);
.grantIapPermission(
Optional.of("console@example.com"),
Optional.of("consoleIapServiceId"),
cloudTasksUtils,
iamClient);
verifyIapPermission(
"email@example.com", Optional.of("console@example.com"), cloudTasksHelper, iamClient);
"email@example.com",
Optional.of("console@example.com"),
Optional.of("consoleIapServiceId"),
cloudTasksHelper,
iamClient);
}
@Test
void testUpdateUserGroup_worksWithoutIapServiceId() {
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
OteAccountBuilder.forRegistrarId("myclientid")
.addUser("email@example.com")
.grantIapPermission(
Optional.of("console@example.com"), Optional.empty(), cloudTasksUtils, iamClient);
verifyIapPermission(
"email@example.com",
Optional.of("console@example.com"),
Optional.empty(),
cloudTasksHelper,
iamClient);
}
@Test
@@ -156,8 +181,14 @@ public final class OteAccountBuilderTest {
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
OteAccountBuilder.forRegistrarId("myclientid")
.addUser("email@example.com")
.grantIapPermission(Optional.empty(), cloudTasksUtils, iamClient);
verifyIapPermission("email@example.com", Optional.empty(), cloudTasksHelper, iamClient);
.grantIapPermission(
Optional.empty(), Optional.of("consoleIapServiceId"), cloudTasksUtils, iamClient);
verifyIapPermission(
"email@example.com",
Optional.empty(),
Optional.of("consoleIapServiceId"),
cloudTasksHelper,
iamClient);
}
@Test
@@ -66,13 +66,21 @@ public class UserTest extends EntityTestCase {
void testFailure_asyncAndSyncModeConflict() {
assertThrows(
IllegalArgumentException.class,
() -> User.grantIapPermission("email@example.com", Optional.empty(), null, null, null));
() ->
User.grantIapPermission(
"email@example.com",
Optional.empty(),
Optional.of("consoleIapServiceId"),
null,
null,
null));
assertThrows(
IllegalArgumentException.class,
() ->
User.grantIapPermission(
"email@example.com",
Optional.empty(),
Optional.of("consoleIapServiceId"),
mock(CloudTasksUtils.class),
mock(ServiceConnection.class),
null));
@@ -90,6 +98,24 @@ public class UserTest extends EntityTestCase {
null));
}
@Test
void testGrantIapPermission_neitherGroupNorIapServiceId_fails() {
IllegalArgumentException e =
assertThrows(
IllegalArgumentException.class,
() ->
User.grantIapPermission(
"email@example.com",
Optional.empty(),
Optional.empty(),
mock(CloudTasksUtils.class),
null,
mock(IamClient.class)));
assertThat(e)
.hasMessageThat()
.isEqualTo("At least one of groupEmailAddress or consoleIapServiceId must be present");
}
@Test
void testFailure_badInputs() {
User.Builder builder = new User.Builder();
@@ -156,13 +182,33 @@ public class UserTest extends EntityTestCase {
// Individual permission.
User.grantIapPermission(
"email@example.com", Optional.empty(), cloudTasksUtils, null, iamClient);
"email@example.com",
Optional.empty(),
Optional.of("consoleIapServiceId"),
cloudTasksUtils,
null,
iamClient);
cloudTasksHelper.assertNoTasksEnqueued();
verify(iamClient).addBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
// Group membership.
User.grantIapPermission(
"email@example.com", Optional.of("console@example.com"), cloudTasksUtils, null, iamClient);
"email@example.com",
Optional.of("console@example.com"),
Optional.empty(),
cloudTasksUtils,
null,
iamClient);
// Group membership takes precedence over individual membership
User.grantIapPermission(
"groupemail@example.com",
Optional.of("console@example.com"),
Optional.of("consoleIapServiceId"),
cloudTasksUtils,
null,
iamClient);
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
@@ -171,6 +217,13 @@ public class UserTest extends EntityTestCase {
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", "email@example.com")
.param("groupEmailAddress", "console@example.com")
.param("groupUpdateMode", "ADD"),
new TaskMatcher()
.service("BACKEND")
.method(HttpMethod.POST)
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", "groupemail@example.com")
.param("groupEmailAddress", "console@example.com")
.param("groupUpdateMode", "ADD"));
verifyNoMoreInteractions(iamClient);
}
@@ -181,13 +234,25 @@ public class UserTest extends EntityTestCase {
IamClient iamClient = mock(IamClient.class);
// Individual permission.
User.grantIapPermission("email@example.com", Optional.empty(), null, connection, iamClient);
User.grantIapPermission(
"email@example.com",
Optional.empty(),
Optional.of("consoleIapServiceId"),
null,
connection,
iamClient);
verifyNoInteractions(connection);
verify(iamClient).addBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
// Group membership.
User.grantIapPermission(
"email@example.com", Optional.of("console@example.com"), null, connection, iamClient);
"email@example.com",
Optional.of("console@example.com"),
Optional.empty(),
null,
connection,
iamClient);
verify(connection)
.sendPostRequest(
UpdateUserGroupAction.PATH,
@@ -109,6 +109,32 @@ class AddressTest {
assertThrows(IllegalArgumentException.class, () -> createAddress("line1", "", "line3"));
}
@Test
void testFailure_streetLineTooLong() {
assertThrows(IllegalArgumentException.class, () -> createAddress("a".repeat(256)));
}
@Test
void testFailure_cityTooLong() {
assertThrows(
IllegalArgumentException.class,
() -> createAddress("line1").asBuilder().setCity("a".repeat(256)).build());
}
@Test
void testFailure_stateTooLong() {
assertThrows(
IllegalArgumentException.class,
() -> createAddress("line1").asBuilder().setState("a".repeat(256)).build());
}
@Test
void testFailure_zipTooLong() {
assertThrows(
IllegalArgumentException.class,
() -> createAddress("line1").asBuilder().setZip("12345678901234567").build());
}
@Test
void testSuccess_pojoToAndFromXml() throws Exception {
JAXBContext jaxbContext = JAXBContext.newInstance(TestEntity.class);
@@ -165,4 +165,32 @@ public class BulkDomainTransferCommandTest extends CommandTestCase<BulkDomainTra
.isEqualTo(
"Must specify exactly one input method, either --domains or --domain_names_file");
}
@Test
void testSuccess_registrarRequestExplicit() throws Exception {
runCommandForced(
"--gaining_registrar_id",
"NewRegistrar",
"--losing_registrar_id",
"TheRegistrar",
"--reason",
"someReason",
"--domains",
"foo.tld,bar.tld",
"--registrar_request=false");
verify(connection)
.sendPostRequest(
"/_dr/task/bulkDomainTransfer",
ImmutableMap.of(
"gainingRegistrarId",
"NewRegistrar",
"losingRegistrarId",
"TheRegistrar",
"requestedByRegistrar",
false,
"reason",
"someReason"),
MediaType.PLAIN_TEXT_UTF_8,
"[\"foo.tld\",\"bar.tld\"]".getBytes(UTF_8));
}
}
@@ -45,6 +45,7 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
void beforeEach() {
command.iamClient = iamClient;
command.maybeGroupEmailAddress = Optional.empty();
command.consoleIapServiceId = Optional.of("consoleIapServiceId");
command.setConnection(connection);
}
@@ -56,7 +57,8 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
assertThat(onlyUser.getUserRoles().isAdmin()).isFalse();
assertThat(onlyUser.getUserRoles().getGlobalRole()).isEqualTo(GlobalRole.NONE);
assertThat(onlyUser.getUserRoles().getRegistrarRoles()).isEmpty();
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
}
@@ -86,6 +88,41 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
verifyNoMoreInteractions(connection);
}
@Test
void testSuccess_addToGroup_noIapServiceId() throws Exception {
command.maybeGroupEmailAddress = Optional.of("group@example.test");
command.consoleIapServiceId = Optional.empty();
runCommandForced("--email", "user@example.test");
User onlyUser = Iterables.getOnlyElement(DatabaseHelper.loadAllOf(User.class));
assertThat(onlyUser.getEmailAddress()).isEqualTo("user@example.test");
verify(connection)
.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
"user@example.test",
"groupEmailAddress",
"group@example.test",
"groupUpdateMode",
"ADD"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
verifyNoInteractions(iamClient);
verifyNoMoreInteractions(connection);
}
@Test
void testFailure_neitherGroupNorIapServiceId() {
command.maybeGroupEmailAddress = Optional.empty();
command.consoleIapServiceId = Optional.empty();
IllegalArgumentException e =
assertThrows(
IllegalArgumentException.class, () -> runCommandForced("--email", "user@example.test"));
assertThat(e)
.hasMessageThat()
.isEqualTo("At least one of groupEmailAddress or consoleIapServiceId must be present");
}
@Test
void testSuccess_registryLock() throws Exception {
runCommandForced(
@@ -107,7 +144,8 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
void testSuccess_admin() throws Exception {
runCommandForced("--email", "user@example.test", "--admin", "true");
assertThat(loadExistingUser("user@example.test").getUserRoles().isAdmin()).isTrue();
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
}
@@ -117,7 +155,8 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
runCommandForced("--email", "user@example.test", "--global_role", "FTE");
assertThat(loadExistingUser("user@example.test").getUserRoles().getGlobalRole())
.isEqualTo(GlobalRole.FTE);
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
}
@@ -136,7 +175,8 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
RegistrarRole.ACCOUNT_MANAGER,
"NewRegistrar",
RegistrarRole.PRIMARY_CONTACT));
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
}
@@ -144,7 +184,8 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
@Test
void testFailure_alreadyExists() throws Exception {
runCommandForced("--email", "user@example.test");
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
verifyNoMoreInteractions(iamClient);
assertThat(
assertThrows(
@@ -53,10 +53,20 @@ class DeleteDomainCommandTest extends EppToolCommandTestCase<DeleteDomainCommand
"--client=NewRegistrar",
"--domain_name=example.tld",
"--reason=Test",
"--registrar_request");
"--registrar_request=true");
eppVerifier.verifySent("domain_delete_by_registrar.xml");
}
@Test
void testSuccess_requestedByRegistrarExplicitFalse() throws Exception {
runCommandForced(
"--client=NewRegistrar",
"--domain_name=example.tld",
"--reason=Test",
"--registrar_request=false");
eppVerifier.verifySent("domain_delete.xml");
}
@Test
void testFailure_noReason() {
assertThrows(
@@ -24,6 +24,8 @@ import static google.registry.util.DateTimeUtils.plusMonths;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.testing.DatabaseHelper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -136,4 +138,28 @@ class GetDomainCommandTest extends CommandTestCase<GetDomainCommand> {
// Deleted
assertInStdout("Websafe key: kind:Domain@sql:rO0ABXQABTMtVExE");
}
@Test
void testSuccess_censorsAuthcodeByDefault() throws Exception {
persistResource(
DatabaseHelper.newDomain("example.tld")
.asBuilder()
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("secret123")))
.build());
runCommand("example.tld");
assertInStdout("value=[REDACTED]");
assertNotInStdout("secret123");
}
@Test
void testSuccess_showAuthcode() throws Exception {
persistResource(
DatabaseHelper.newDomain("example.tld")
.asBuilder()
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("secret123")))
.build());
runCommand("example.tld", "--show_authcode=true");
assertInStdout("value=secret123");
assertNotInStdout("[REDACTED]");
}
}
@@ -38,6 +38,7 @@ import org.mockito.ArgumentCaptor;
/** Unit tests for {@link IamClient}. */
public class IamClientTest {
private final CloudResourceManager resourceManager = mock(CloudResourceManager.class);
private final String consoleIapServiceId = "123456789";
private final String projectId = "my-project";
private final String account = "test@example.test";
private final String role = "roles/fakeRole";
@@ -89,11 +90,17 @@ public class IamClientTest {
void testSuccess_addBinding_noMatchedBindingExists() throws Exception {
setupRequests();
assertThat(bindings.size()).isEqualTo(1);
client.addBinding(account, role);
client.addBinding(account, role, consoleIapServiceId);
assertThat(bindings.size()).isEqualTo(2);
Binding binding = bindings.get(1);
assertThat(binding.getRole()).isEqualTo(role);
assertThat(binding.getMembers()).containsExactly("user:" + account);
assertThat(binding.getCondition()).isNotNull();
assertThat(binding.getCondition().getTitle()).isEqualTo("Registrar Console IAP access");
assertThat(binding.getCondition().getDescription())
.isEqualTo("Restrict IAP access only to the Registrar Console HTTP(s) load balancer");
assertThat(binding.getCondition().getExpression())
.isEqualTo("resource.name == 'projects/my-project/iap_web/compute/services/123456789'");
verifySetPolicyRequest();
}
@@ -107,7 +114,7 @@ public class IamClientTest {
when(matchedBinding.getMembers()).thenReturn(existingMembers);
bindings.add(matchedBinding);
assertThat(bindings.size()).isEqualTo(2);
client.addBinding(account, role);
client.addBinding(account, role, consoleIapServiceId);
assertThat(bindings.size()).isEqualTo(2);
assertThat(existingMembers)
.containsExactly("serviceAccount:service@example.test", "user:" + account);
@@ -125,7 +132,7 @@ public class IamClientTest {
when(matchedBinding.getMembers()).thenReturn(existingMembers);
bindings.add(matchedBinding);
assertThat(bindings.size()).isEqualTo(2);
client.addBinding(account, role);
client.addBinding(account, role, consoleIapServiceId);
assertThat(bindings.size()).isEqualTo(2);
assertThat(existingMembers)
.containsExactly("serviceAccount:service@example.test", "user:" + account);
@@ -73,6 +73,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
command.clock = fakeClock;
command.passwordGenerator = passwordGenerator;
command.maybeGroupEmailAddress = Optional.of("group@example.com");
command.consoleIapServiceId = Optional.of("consoleIapServiceId");
command.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
command.iamClient = iamClient;
persistPremiumList("default_sandbox_list", USD, "sandbox,USD 1000");
@@ -129,7 +130,9 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
String groupEmailAddress = command.maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress == null) {
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verify(iamClient).addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding(
emailAddress, IAP_SECURED_WEB_APP_USER_ROLE, command.consoleIapServiceId.get());
} else {
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
@@ -144,6 +144,7 @@ class ConsoleOteActionTest extends ConsoleActionBaseTestCase {
verifyIapPermission(
"contact@registry.example",
Optional.of("someRandomString@email.test"),
Optional.of("consoleIapServiceId"),
cloudTasksHelper,
iamClient);
}
@@ -216,6 +217,7 @@ class ConsoleOteActionTest extends ConsoleActionBaseTestCase {
iamClient,
registrarId,
maybeGroupEmailAddress,
Optional.of("consoleIapServiceId"),
passwordGenerator,
oteCreateData);
}
@@ -20,6 +20,7 @@ import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.loadSingleton;
import static google.registry.testing.DatabaseHelper.persistResource;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -37,6 +38,7 @@ import google.registry.request.Action;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.testing.ConsoleApiParamsUtils;
import google.registry.testing.FakeResponse;
import google.registry.testing.SystemPropertyExtension;
import google.registry.util.EmailMessage;
import google.registry.util.RegistryEnvironment;
@@ -55,8 +57,6 @@ class ConsoleUpdateRegistrarActionTest extends ConsoleActionBaseTestCase {
private Registrar registrar;
private User user;
private static String registrarPostData =
"{\"registrarId\":\"%s\",\"allowedTlds\":[%s],\"registryLockAllowed\":%s,"
+ " \"lastPocVerificationDate\":%s }";
@@ -76,12 +76,6 @@ class ConsoleUpdateRegistrarActionTest extends ConsoleActionBaseTestCase {
.setAllowedTlds(ImmutableSet.of())
.setRegistryLockAllowed(false)
.build());
user =
persistResource(
new User.Builder()
.setEmailAddress("user@registrarId.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build());
}
@Test
@@ -209,15 +203,123 @@ class ConsoleUpdateRegistrarActionTest extends ConsoleActionBaseTestCase {
.build());
}
private ConsoleApiParams createParams() {
AuthResult authResult = AuthResult.createUser(user);
return ConsoleApiParamsUtils.createFake(authResult);
@Test
void testFailure_registrarNotFound() throws IOException {
var action =
createAction(
String.format(
registrarPostData,
"nonexistent",
"app, dev",
false,
"\"2023-12-12T00:00:00.000Z\""));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
}
private ConsoleUpdateRegistrarAction createAction(String requestData) throws IOException {
@Test
void testSuccess_supportAgentCanUpdateAllowedTlds() throws IOException {
User supportAgentUser =
persistResource(
new User.Builder()
.setEmailAddress("agent@registrarId.com")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build());
var action =
createAction(
supportAgentUser,
String.format(
registrarPostData,
"TheRegistrar",
"app, dev",
false,
"\"2023-12-12T00:00:00.000Z\""));
action.run();
Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get();
assertThat(newRegistrar.getAllowedTlds()).containsExactly("app", "dev");
assertThat(response.getStatus()).isEqualTo(SC_OK);
}
@Test
void testFailure_supportAgentCannotUpdateRegistryLock() throws IOException {
User supportAgentUser =
persistResource(
new User.Builder()
.setEmailAddress("agent@registrarId.com")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build());
var action =
createAction(
supportAgentUser,
String.format(
registrarPostData,
"TheRegistrar",
"app, dev",
true,
"\"2023-12-12T00:00:00.000Z\""));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
Registrar unupdatedRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get();
assertThat(unupdatedRegistrar.isRegistryLockAllowed()).isFalse();
}
@Test
void testFailure_userWithoutEditRegistrarDetailsCannotUpdateAllowedTlds() throws IOException {
User normalUser =
persistResource(
new User.Builder()
.setEmailAddress("normal@registrarId.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.NONE).build())
.build());
var action =
createAction(
normalUser,
String.format(
registrarPostData,
"TheRegistrar",
"app, dev",
false,
"\"2023-12-12T00:00:00.000Z\""));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
Registrar unupdatedRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get();
assertThat(unupdatedRegistrar.getAllowedTlds()).isEmpty();
}
@Test
void testSuccess_supportLeadCanUpdateRegistryLock() throws IOException {
User supportLeadUser =
persistResource(
new User.Builder()
.setEmailAddress("lead@registrarId.com")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_LEAD).build())
.build());
var action =
createAction(
supportLeadUser,
String.format(
registrarPostData, "TheRegistrar", "", true, "\"2023-12-12T00:00:00.000Z\""));
action.run();
Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get();
assertThat(newRegistrar.isRegistryLockAllowed()).isTrue();
assertThat(response.getStatus()).isEqualTo(SC_OK);
}
private ConsoleUpdateRegistrarAction createAction(User actionUser, String requestData)
throws IOException {
AuthResult authResult = AuthResult.createUser(actionUser);
consoleApiParams = ConsoleApiParamsUtils.createFake(authResult);
response = (FakeResponse) consoleApiParams.response();
when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.POST.toString());
Optional<Registrar> maybeRegistrarUpdateData =
ConsoleModule.provideRegistrar(GSON, RequestModule.provideJsonBody(requestData, GSON));
return new ConsoleUpdateRegistrarAction(consoleApiParams, maybeRegistrarUpdateData);
}
private ConsoleUpdateRegistrarAction createAction(String requestData) throws IOException {
return createAction(fteUser, requestData);
}
}
@@ -663,6 +663,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
iamClient,
"email.com",
Optional.of("someRandomString"),
Optional.of("consoleIapServiceId"),
passwordGenerator,
userData,
"TheRegistrar");
@@ -343,10 +343,23 @@ visibleInRdapAsTech=true, visibleInDomainRdapAsAbuse=false}
testRegistrar.getRegistrarId(),
adminPoc.asBuilder().setEmailAddress("testemail@example.com").build());
action.run();
FakeResponse fakeResponse = response;
assertThat(fakeResponse.getStatus()).isEqualTo(400);
assertThat(fakeResponse.getPayload())
.isEqualTo("Cannot remove or change the email address of primary contacts");
assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("Cannot alter the set of primary contacts");
}
@Test
void testFailure_addsAdminEmail() throws Exception {
RegistrarPoc newAdminPoc =
adminPoc
.asBuilder()
.setName("New Admin Contact")
.setEmailAddress("new.admin@example.com")
.build();
ContactAction action =
createAction(Action.Method.POST, fteUser, testRegistrar.getRegistrarId(), newAdminPoc);
action.run();
assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("Cannot alter the set of primary contacts");
}
private ContactAction createAction(
@@ -19,6 +19,7 @@ import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableO
import static google.registry.testing.DatabaseHelper.loadSingleton;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
@@ -67,7 +68,8 @@ public class RdapRegistrarFieldsActionTest extends ConsoleActionBaseTestCase {
"url",
"\"http://my.fake.url\"",
"localizedAddress",
"{\"street\": [\"123 Example Boulevard\"], \"city\": \"Williamsburg\", \"state\":"
"{\"street\": [\"123 Example Boulevard\"], \"city\":"
+ " \"Williamsburg\", \"state\":"
+ " \"NY\", \"zip\": \"11201\", \"countryCode\": \"US\"}"));
@Test
@@ -132,6 +134,17 @@ public class RdapRegistrarFieldsActionTest extends ConsoleActionBaseTestCase {
assertThat(DatabaseHelper.loadByEntity(newRegistrar)).isEqualTo(newRegistrar);
}
@Test
void testFailure_zipTooLong() throws Exception {
uiRegistrarMap.put(
"localizedAddress",
"{\"street\": [\"123 Fake St\"], \"city\": \"Fakeville\", \"state\":"
+ " \"NL\", \"zip\": \"12345678901234567\", \"countryCode\": \"CA\"}");
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, this::createAction);
assertThat(exception).hasMessageThat().contains("Zip cannot be longer than 16 characters");
}
private RdapRegistrarFieldsAction createAction() throws IOException {
return createAction(fteUser);
}
@@ -9,8 +9,8 @@
<init-param>
<param-name>headerConfig</param-name>
<param-value>
set Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self';
set X-Content-Type-Options: nosniff
set Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self';,
set X-Content-Type-Options: nosniff,
set X-Frame-Options: DENY
</param-value>
</init-param>
+1 -1
View File
@@ -269,7 +269,7 @@ steps:
if gcloud storage objects describe gs://${gcs_loc}/${gradle_bin}
then
local_md5=$(md5sum ${gradle_bin} | awk '{print $1}')
remote_md5=$(gcloud storage hash -h gs://${gcs_loc}/${gradle_bin} | grep md5 | awk '{print $3}')
remote_md5=$(gcloud storage hash --hex gs://${gcs_loc}/${gradle_bin} | grep md5 | awk '{print $2}')
if [[ ${local_md5} != ${remote_md5} ]]
then
echo "ERROR: MD5 hash mismatch for ${gradle_bin}! Local (official): ${local_md5}, Remote (GCS cache): ${remote_md5}. Aborting build." >&2