1
0
mirror of https://github.com/google/nomulus synced 2026-07-07 16:46:56 +00:00

Compare commits

..

37 Commits

Author SHA1 Message Date
gbrodman d9a857133a Remove class name option in GetRoutingMapCommand (#3101)
we only have the one component now, and in general it's a good idea to
remove class loading based on user input, even if that user is one of us
2026-07-01 18:55:26 +00:00
gbrodman c7a27061d8 Remove entries from token store atomically (#3112)
just in case some other thread comes in while we're iterating
2026-07-01 18:42:59 +00:00
Weimin Yu 7766db36a7 Support custom npm location in local environment (#3127)
Make sure Gradle can find the npm executable at custom locations,
e.g., when installed by nvm.
2026-07-01 18:39:29 +00:00
Ben McIlwain eda0f7ad7c Harden EppXmlSanitizer against XXE and entity expansion (#3117)
Configure a throwing XMLResolver on XMLInputFactory in XmlTransformer
to act as an additional defense-in-depth security layer against XML
External Entity (XXE) and entity expansion/resolution attacks.
This prevents resolution of external and internal entities in StAX
parsing pipelines, ensuring that malformed payloads containing entity
definitions are safely blocked and rejected.

Also add a unit test to EppXmlSanitizerTest verifying that EPP
messages containing DTD and external entities fail parsing and fall
back safely to their Base64 representation.

TAG=agy
CONV=610c2358-a99f-4605-94cd-ff0d4ee08176

BUG= http://b/529387728
2026-06-30 18:19:50 +00:00
gbrodman 67527f1560 Invalidate TLD cache on any type of update (#3123) 2026-06-30 18:15:45 +00:00
Ben McIlwain 4aeba6e3f7 Validate non-empty tlds in refresh DNS action (#3114)
Add validation to RefreshDnsForAllDomainsAction to ensure the tlds parameter is not empty when invoked. Previously, invoking the action without specifying tlds would result in a no-op that logged an empty list and exited without error. This change makes missing tlds an explicit error, throwing an IllegalArgumentException so users running the command via nomulus curl receive clear feedback that the parameter is required.
2026-06-30 16:34:15 +00:00
Ben McIlwain d6f1f5894b Restore RFC schemas, prober XML, and OT&E route (#3120)
In commit 17b851de42 (#2979), <element name="contact"> and related types were incorrectly removed from core/src/main/java/google/registry/xml/xsd/domain.xsd and rde-domain.xsd. Because those XSD schemas are published standards (RFC 5731 and RFC 5831), altering them caused JAXB unmarshaling to reject historical database records (HistoryEntry) containing <domain:contact> elements during OT&E status checks.

Restore <element name="contact" minOccurs="0" maxOccurs="unbounded"/> to domain.xsd and rde-domain.xsd so all XSD schemas conform strictly to published EPP RFCs and historical contact elements unmarshal cleanly without throwing cvc-complex-type.2.4.a schema validation errors.

Remove deprecated <domain:contact> elements from prober creation payload (prober/.../create.xml) to match current flow rules where contact associations are forbidden.

Remove trailing slash from ote-status route parameter in RegistrarDetailsComponent to prevent double-slash (/ote-status//<id>) URL generation when checking OT&E status.

BUG= http://b/529391412
2026-06-30 16:07:31 +00:00
gbrodman 47ad569cb0 Verify that marksdb verify URL starts with ry.marksdb.org (#3115) 2026-06-30 16:02:00 +00:00
gbrodman 06934daf94 Don't allow registrar-based ops on global users (#3100) 2026-06-29 19:13:20 +00:00
gbrodman 9a032e4bb9 Add extra perm check for EPP password changes (#3099) 2026-06-29 18:30:52 +00:00
gbrodman 0ab612ab23 Forbid mismatched inner element in XML flow inputs (#3104) 2026-06-26 19:04:12 +00:00
gbrodman 403c7ad275 Add test enforcing host references in redemption domains (#3109)
Domains in the redemption grace period can still be restored, so hosts
referencing them cannot be deleted. Fortunately this is already the
case. This just adds a test.
2026-06-26 19:04:09 +00:00
gbrodman 11d625b837 Enforce additional pw reset permissions (#3098)
This should not have been an issue in practice due to defense in depth,
but we should check these just in case.
2026-06-25 20:45:58 +00:00
gbrodman 6a47287da7 Forbid non-routable IPs for host glue records (#3105)
Reject loopback, link-local, site-local, wildcard, and multicast IP
addresses during host creation and update flows.

Glue records (A/AAAA records published in the parent zone for subordinate
name servers) must point to globally routable, public IP addresses to
ensure that recursive DNS resolvers on the public internet can reach the
authoritative name servers.

Using non-public or non-routable IP addresses in glue records is invalid
for the following reasons:
- Loopback (127.0.0.1, ::1) and Any-Local (0.0.0.0, ::) addresses point
  back to the client or are unspecified, causing resolvers to query
  themselves and fail.
- Private/Site-Local (e.g., 10.0.0.0/8, 192.168.0.0/16) and Link-Local
  (169.254.0.0/16) addresses are not routable on the public internet,
  rendering the delegated domain completely unreachable to external clients.
- Multicast addresses are designed for one-to-many delivery and cannot
  be used for standard unicast DNS queries to a specific name server.

Rename LoopbackIpNotValidForHostException to IpAddressNotRoutableException
to reflect the broader set of forbidden non-routable IP addresses.
2026-06-25 18:06:33 +00:00
gbrodman cdc0ffe831 Use filename regex in CopyDetailReportsAction (#3102)
No registrars have an underscore in their name, but as far as I'm aware
there's nothing explicitly preventing that.
2026-06-25 16:58:52 +00:00
Ben McIlwain 7c23413d83 Fix Console API and Angular XSS security flaws (#3076)
This commit addresses the following security vulnerabilities identified in the recent audit of the Console App and Backend APIs:

1. Angular XSS: Removed unsafe [innerHTML] bindings across all console-webapp templates (Contact, Registrars, Registrar Details, Users List) in favor of standard Angular interpolation.
2. Broken Access Control (IDOR): PasswordResetRequestAction and PasswordResetVerifyAction now explicitly verify that the target user's email belongs to the authorized registrarId.
3. Missing Permission Check: ConsoleEppPasswordAction now explicitly checks for CONFIGURE_EPP_CONNECTION permission before updating the EPP password.
4. Denial of Service (DoS): ConsoleBulkDomainAction now strictly limits the size of bulk domain lists (configurable, default 500) to prevent thread exhaustion.
5. Denial of Service (OOM): ConsoleHistoryDataAction now uses .setMaxResults() (configurable, default 500) on JPA native queries to prevent eager loading of the entire database into memory.

Makes the history query limit and bulk domain action limit configurable via RegistryConfig, allowing smaller limits to be used in tests to avoid heavy resource persistence.

Also removes an outdated Joda-Time migration reference from GEMINI.md.
2026-06-24 20:39:42 +00:00
gbrodman fe222bbdcf Fix a couple edge cases with token pricing (#3103)
It's unlikely we'd run into this because our tokens are generally just
valid for one year, but we should fix these regardless
2026-06-24 15:55:01 +00:00
Ben McIlwain f770f6a46d Improve db/README.md with refactoring guide (#3096)
This commit improves the database documentation in db/README.md by adding comprehensive guidelines for refactoring column types and managing two-PR schema deployments.

Key additions:
- Added a section on the "Expand and Contract" pattern for refactoring column types, explaining when it is safe to drop columns immediately vs. when a three-step release process is required.
- Added a section on writing safe NOT NULL migrations for timed transition properties, explaining the "Temporary Database Default" pattern to maintain backward compatibility with running servers during Two-PR deployments, and demonstrating the required explicit PostgreSQL `::hstore` casting syntax.
- Added a step-by-step "Recommended Git Workflow" section to help developers cleanly split their database and Java changes into chained PRs using Git.

TAG=agy
CONV=88271e71-e272-40e0-85f8-a075a423b7c2
2026-06-23 21:32:22 +00:00
Ben McIlwain e071f5579c Implement database schema for scheduled XAP launch (#3095)
This commit implements the database schema changes for the Expiry Access Period (XAP) launch configuration on TLDs. It represents the first step of a Two-PR deployment strategy, deploying the database schema changes in advance of the server logic.

Specifically, it replaces the `expiry_access_period_enabled` boolean column (originally introduced in PR #2804) with a new `expiry_access_period_transitions` hstore column.

Why we are making this change:
A basic boolean flag only allows an immediate, manual on/off toggle. To launch XAP on a TLD, registry operators would have to manually flip the flag at the exact launch time, which is operationally fragile and cannot be planned in advance. Refactoring this to an hstore-backed timed transition map (mapping Instant to ExpiryAccessPeriodMode) allows operators to schedule the XAP launch in advance via TLD YAML configurations. The registry will automatically transition the TLD to the ENABLED mode at the scheduled timestamp, aligning with how other scheduled TLD changes (like TLD states and EAP fee schedules) are managed.

Since the original boolean column was never mapped in Java (PR #2804 only added the database column), it is completely safe to drop it immediately in this migration.

To ensure backward compatibility with running servers (which are still executing the old Java code during the deployment transition), the new column is added as `NOT NULL` with a temporary `DEFAULT` constraint. This prevents constraint violations on inserts from old servers. A TODO has been left in the SQL migration to drop this default in a subsequent schema release once the Java changes have been deployed.

TAG=agy
CONV=88271e71-e272-40e0-85f8-a075a423b7c2
2026-06-23 19:46:17 +00:00
Ben McIlwain 6080cd2f7a Fix flaky RdapDomainSearchActionTest (#3097)
Refactor RdapDomainSearchActionTest to dynamically resolve all domain
and host Repository IDs (ROIDs) instead of asserting on hardcoded,
sequence-generated strings (like "2E-LOL" or "6-LOL").

When tests are executed in parallel (as is common in CI environments like
Kokoro), multiple test threads concurrently reset and allocate from the
shared database sequence 'project_wide_unique_id_seq'. This interleaves
ID allocations non-deterministically, causing any test asserting on
exact, hardcoded sequence values to flake.

To fix this, createManyDomainsAndHosts was updated to return the list of
persisted domains, allowing tests to dynamically resolve their ROIDs.
All other test cases were refactored to dynamically fetch the ROIDs of
pre-created domains and hosts (stored in fields or in hostNameToHostMap,
using punycode keys for IDN hosts) for their JSON assertions, rendering
the entire suite robust against sequence shifts.
2026-06-23 16:30:04 +00:00
gbrodman 90f583910e Add max retry duration for cloud tasks that don't have it (#3094) 2026-06-22 19:56:45 +00:00
gbrodman a85bf5c30a Use a (small) map to cache token verifiers (#3088)
we shouldn't have to rebuild it each time we get a request to a
different service or really ever at all -- we might get a tiny bit of
cache benefit here
2026-06-22 18:26:02 +00:00
Ben McIlwain dcfe939c38 Update docs for Java 25 and GKE migration (#3089)
Summarize all documentation updates across the repository to align with modern GKE, Cloud SQL Proxy v2, standard EPP fee v1.0, and Postgres database environments.

Key Updates:
- Prerequisites: Bump Java requirement to Java 25.
- Architecture & Scaling: Document GKE workloads, Cloud Tasks queues, and scheduled tasks. Replace App Engine references with GKE deployment restart commands (kubectl rollout restart).
- Configuration: Update Cloud SQL Proxy instructions to v2, fix keyring verification commands, and document IAP configuration.
- Escrow (RDE/BRDA): Fix manual generation and download procedures to match the Dataflow job ID folder structure, and correct deposit encryption/verification command parameters.
- Monitoring: Correct metric names and expand the documented metrics list with caching, locking, and reserved list metrics.
- Fixes: Standardize lists formatting across markdown files, fix broken webdriver links, and resolve various typos.
- Cleanup: Remove leftover cloud scheduler configurations for the deleted wipeOutContactHistoryPii task, and update ICANN reporting documentation to reflect open-sourced DNS query coordinator.

TAG=agy
CONV=88271e71-e272-40e0-85f8-a075a423b7c2
2026-06-22 18:20:00 +00:00
gbrodman ae61922318 Use existing pw reset code if creating from an existing instance (#3091)
this means that modifying requests changes them in place in the db
2026-06-22 17:33:17 +00:00
gbrodman 84e97aa2db Use time-constant comparison in password testing (#3090)
Normal comparison exits out once a difference is found so theoretically
a time variance can leak information. This isn't really a huge deal but
is probably still worth doing.
2026-06-17 21:00:13 +00:00
Ben McIlwain 4df4bf1489 Add FeatureFlag helper methods to DatabaseHelper (#3087)
Introduced overloaded helper methods `persistFeatureFlag` in `DatabaseHelper` to simplify the creation and persistence of `FeatureFlag` entities in test setups.

1. `persistFeatureFlag(FeatureName, FeatureStatus)`: Persists a feature flag with a single status starting from the Unix Epoch (`START_INSTANT`).
2. `persistFeatureFlag(FeatureName, FeatureStatus, Instant, FeatureStatus)`: Persists a feature flag with an initial status at `START_INSTANT` and a subsequent transition at a specified time.

Refactored 11 occurrences of manual 1-transition flag creation and 5 occurrences of 2-transition flag creation across the test suite to use these new helpers, significantly reducing boilerplate and improving test readability.

TAG=agy
CONV=583b8a23-9fe5-476d-ac35-aeba7b218eb0
2026-06-16 21:24:01 +00:00
gbrodman 9faabb0f1d Forbid deprecated algorithms in DNSSEC data (#3078)
This is similar to PR #3069 but for the algorithms themselves rather
than the digest data. This forbids algorithms, that, according to RFC
9904, should not be used.
2026-06-16 19:09:05 +00:00
gbrodman ea9d717378 Use custom Protostuff delegate for InetAddresses (#3079)
We need this to serialize/deserialize hosts, because we're not allowed
to reflectively access InetAddress.
2026-06-08 19:16:35 +00:00
Juan Celhay d1769b29ef fix relative path of manifests (#3084) 2026-06-08 15:37:17 +00:00
gbrodman 14376953e5 Skip EPP params for BRDA (#3083)
this is an extra field that shouldn't be included in this output
apparently
2026-06-05 20:52:54 +00:00
Juan Celhay c13c9b53e3 Add sandbox to the Cloud Deploy delivery pipeline (#3080)
* add sandbox target to the delivery pipeline

* add sandbox target
2026-06-05 19:32:28 +00:00
Ben McIlwain 1cd6026151 Fix npx build overriding Angular output paths (#3082)
This commit reverts changes from PR #3068 that swapped 'npm run build' for 'npx ng build' while attempting to dynamically set the '--output-path' via the CLI.

Passing '--output-path' on the command line overrides the entire 'outputPath' configuration object in angular.json. Because the new Angular 18 Application Builder (esbuild) nests outputs inside a 'browser/' directory by default, overriding the configuration bypassed the 'browser: ""' flattening property, causing all client assets to be nested deeper than expected.

This resulted in empty deployments because downstream tasks (like Jetty's copyConsole and the deployment tar scripts) expected the assets to be completely flat. By removing the '--output-path' override from the 'npx ng build' calls, the Angular CLI once again respects angular.json, flattens the output into 'staged/dist/', and the restored 'doLast' block successfully copies the artifacts where they belong.
2026-06-05 16:31:14 +00:00
Ben McIlwain 75524fd403 Restore default builds and fix Kokoro tests (#3081)
This commit reverts changes from 5599a0eb3d and most of 5286b1a0dc (PR #3068) that stripped essential dependencies (buildConsoleForAll, buildNomulusImage, buildToolImage, fragileTest) from the default './gradlew build' target, which broke downstream deployment pipelines. It restores the default build to correctly generate all necessary production artifacts and Docker images.

It introduces a new 'fastBuild' target designed explicitly for local developers and CI checks. This lightweight target disables the execution of heavy Docker image builds, Angular compilations, and fragile tests to provide rapid feedback. Sequential execution constraints for parallel Angular builds are maintained to prevent cache corruption.

It updates the ':core:generateSqlSchema' task to execute using the 'unittest' environment instead of 'alpha'. The 'alpha' configuration is a private, internal environment config that is not distributed in the open-source repository, which caused the task to fail for public contributors. By switching to 'unittest', the generator can successfully run using the public test configuration. With this fixed, it also includes the newly generated 'db-schema.sql.generated' file, which now correctly tracks the 'FORBID_INSECURE_ALGORITHMS_RFC_9904' feature flag that was recently added.

Finally, it implements a split-runner execution strategy for the 'sqlIntegrationTest' task to permanently resolve 'failed to discover tests' and 'NoSuchMethodError' exceptions on Kokoro. Because Kokoro tests cross-version compatibility against both legacy deployed artifacts (compiled with JUnit 4 @RunWith wrappers) and modern artifacts (compiled with JUnit 5 @Suite annotations), we cannot statically configure a single test runner. We now dynamically run both the legacy 'useJUnit()' and modern 'useJUnitPlatform()' runners sequentially with 'failOnNoDiscoveredTests' disabled, allowing the appropriate engine to discover and execute the suite without causing classpath collisions.
2026-06-04 15:38:03 +00:00
gbrodman a5b280838c Remove usages of json-simple (#3060)
We should use gson wherever possible. There's no point in having
unnecessary dependencies (we'll need to keep around jackson for YAML
parsing).
2026-06-01 20:16:32 +00:00
Ben McIlwain 5599a0eb3d Add buildAll task and fix fragile builds (#3077)
This commit adds the buildAll task to restore the existence of a target that builds everything, which was unintentionally removed when the default build was stripped down in PR #3068. It also introduces necessary sequential constraints to the console-webapp build tasks to prevent parallel execution from corrupting the Angular CLI cache. Finally, it addresses paths for the newer Angular esbuild output and hardens the style injection in ConsoleScreenshotTest to prevent fragile test failures.
2026-06-01 19:11:05 +00:00
gbrodman dde41078cd Forbid SHA-1 digests as part of RFC 9904 changes (#3069)
We can't change digest types that are already in the database but that's
fine (since we just store them as integers). But we forbid them as part
of domain creates/updates.
2026-06-01 17:59:19 +00:00
Ben McIlwain 0030645b1a Harden XML parsing, serialization, and randomness (#3075)
This commit introduces several security hardening improvements across the codebase:
1. XML Processing: Hardened `TransformerFactory` and `SchemaFactory` instantiations in `EppMessage.java` by explicitly enabling `XMLConstants.FEATURE_SECURE_PROCESSING` and disabling external schema access.
2. Randomness: Replaced instances of `java.util.Random` with `java.security.SecureRandom` in `SelfSignedCaCertificate.java` for stronger entropy. (Added documentation in `ProxyModule.java` explaining why `java.util.Random` is intentionally retained there for metrics sampling).
3. Deserialization: Hardened `SerializeUtils.java` by injecting an `ObjectInputFilter` into the `ObjectInputStream`, restricting deserialization strictly to expected `google.registry` classes and standard Java collections.
2026-06-01 14:25:42 +00:00
195 changed files with 2824 additions and 1255 deletions
+20
View File
@@ -285,6 +285,25 @@ def check_diff_anti_patterns():
if 'src/test/' in current_file and clock_now_pattern.search(code_line):
log_warning(f"[{current_file}] Prefer using a fixed, static constant Instant over capturing clock.now() in tests to prevent flakiness.")
def check_missing_tests():
print("\n--- Checking for Missing Tests ---")
diff_files = run_cmd("git diff HEAD^ --name-only").split('\n')
main_files_modified = False
test_files_modified = False
for f in diff_files:
if f.startswith('core/src/main/') or f.startswith('util/src/main/'):
if f.endswith('.java'):
main_files_modified = True
if f.startswith('core/src/test/') or f.startswith('util/src/test/'):
if f.endswith('Test.java'):
test_files_modified = True
if main_files_modified and not test_files_modified:
log_warning("Production code (.java files in src/main/) was modified, but no corresponding tests (.java files in src/test/) were added or updated. You MUST proactively write tests for any new logic or bug fixes.")
else:
log_success("Test coverage check passed (tests were included or no production Java code was changed).")
def main():
print("========================================")
print(" NOMULUS PR POLISHER CHECKLIST ")
@@ -295,6 +314,7 @@ def main():
check_workspace_clean()
check_package_lock()
check_license_headers()
check_missing_tests()
check_formatting()
check_diff_anti_patterns()
+2 -1
View File
@@ -46,6 +46,7 @@ This document outlines foundational mandates, architectural patterns, and projec
- **Transactional Time:** Ensure code that relies on `tm().getTransactionTime()` (or `tm().getTxTime()`) is executed within a transaction context.
### 5. Testing Best Practices
- **Mandatory Proactive Testing:** You MUST automatically write and update tests alongside your code changes WITHOUT waiting for the user to prompt you. If you add a new feature, fix a bug, or change core logic, you are explicitly required to identify the corresponding `*Test.java` file and implement comprehensive test coverage for your changes.
- **FakeClock and Sleeper:** Use `FakeClock` and `Sleeper` for any logic involving timeouts, delays, or expiration.
- **Empirical Reproduction:** Before fixing a bug, always create a test case that reproduces the failure.
- **Base Classes:** Leverage `CommandTestCase`, `EppToolCommandTestCase`, etc., to reduce boilerplate and ensure consistent setup (e.g., clock initialization).
@@ -93,7 +94,7 @@ Do not wait for the user to tell you to improve the skills; it is your responsib
# Gemini Engineering Guide: Nomulus Codebase
This document captures high-level architectural patterns, lessons learned from large-scale refactorings (like the Joda-Time to `java.time` migration), and specific instructions to avoid common pitfalls in this environment.
This document captures high-level architectural patterns, lessons learned from large-scale refactorings, and specific instructions to avoid common pitfalls in this environment.
## 🏛 Architecture Overview
+2 -2
View File
@@ -12,7 +12,7 @@ Nomulus is an open source, scalable, cloud-based service for operating
[top-level domains](https://en.wikipedia.org/wiki/Top-level_domain) (TLDs). It
is the authoritative source for the TLDs that it runs, meaning that it is
responsible for tracking domain name ownership and handling registrations,
renewals, availability checks, and WHOIS requests. End-user registrants (i.e.,
renewals, availability checks, and RDAP requests. End-user registrants (i.e.,
people or companies that want to register a domain name) use an intermediate
domain name registrar acting on their behalf to interact with the registry.
@@ -97,7 +97,7 @@ Nomulus has the following capabilities:
for details), and an implementation based on
[Google Cloud Secret Manager](https://cloud.google.com/security/products/secret-manager) is
available.
* **TPC Proxy**: Nomulus is built on top of the [Jetty](https://jetty.org/)
* **TCP Proxy**: Nomulus is built on top of the [Jetty](https://jetty.org/)
container that implements the [Jakarta Servlet](https://jakarta.ee/specifications/servlet/)
specification and only serves HTTP/S traffic. A proxy to translate raw TCP traffic (e.g., EPP)
to and from HTTP is provided.
+20
View File
@@ -609,3 +609,23 @@ gradle.taskGraph.whenReady { graph ->
}
}
}
task fastBuild {
group = 'build'
description = 'A lightweight build for local dev. Compiles Java, runs standard tests, and checks formatting, but skips Docker images, fragile tests, and the massive Angular console builds. (Do not use this target to verify console changes.)'
dependsOn build
// Remove the heavy default dependencies specifically for fastBuild
gradle.taskGraph.whenReady { graph ->
if (graph.hasTask(fastBuild)) {
project(':console-webapp').tasks.named('buildConsoleForAll').get().enabled = false
project(':jetty').tasks.named('buildNomulusImage').get().enabled = false
project(':core').tasks.named('buildToolImage').get().enabled = false
project(':core').tasks.named('fragileTest').get().enabled = false
project(':jetty').tasks.named('stage').get().enabled = false
if (project.tasks.findByName('stage') != null) {
project.tasks.named('stage').get().enabled = false
}
}
}
}
+4 -5
View File
@@ -9,15 +9,14 @@ expected to change.
## Deployment
The webapp is deployed with the nomulus default service war to GKE.
During nomulus default service war build task, gradle script triggers the
following:
The webapp is deployed as part of the default Nomulus GKE service image.
During the image build task, the Gradle script triggers the following:
1) Console webapp build script `buildConsoleWebapp`, which installs
dependencies, assembles a compiled ts -> js, minified, optimized static
artifact (html, css, js)
2) Artifact assembled in step 1 then gets copied to core project web artifact
location, so that it can be deployed with the rest of the core webapp
2) Artifact assembled in step 1 then gets copied to the jetty webapp resource
location, so that it can be staged inside the default GKE service container.
## Development server
+23 -14
View File
@@ -21,27 +21,23 @@ clean {
task npmInstallDeps(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'i', '--no-audit', '--no-fund', '--loglevel=error'
commandLine 'sh', '-c', 'npm i --no-audit --no-fund --loglevel=error'
}
task runConsoleWebappLocally(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'start:dev'
commandLine 'sh', '-c', 'npm run start:dev'
}
task runConsoleWebappUnitTests(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'test'
commandLine 'sh', '-c', 'npm run test'
}
task buildConsoleWebapp(type: Exec) {
workingDir "${consoleDir}/"
executable 'npx'
def configuration = project.getProperty('configuration')
args 'ng', 'build', '--base-href=/console/', "--configuration=${configuration}", "--output-path=staged/dist"
commandLine 'sh', '-c', "npx ng build --base-href=/console/ --configuration=${configuration} --output-path=staged/dist"
doFirst {
println "Building console for environment: ${configuration}"
}
@@ -52,11 +48,17 @@ task buildConsoleForAll() {}
def createConsoleTask = { env ->
project.tasks.register("buildConsoleFor${env.capitalize()}", Exec) {
workingDir "${consoleDir}/"
executable 'npx'
args 'ng', 'build', '--base-href=/console/', "--configuration=${env}", "--output-path=staged/console-${env}"
commandLine 'sh', '-c', "npx ng build --base-href=/console/ --configuration=${env}"
doFirst {
println "Building console for environment: ${env}"
}
doLast {
copy {
from "${consoleDir}/staged/dist/"
into "${consoleDir}/staged/console-${env}"
}
delete "${consoleDir}/staged/dist"
}
dependsOn(tasks.npmInstallDeps)
}
project.tasks.register("deleteConsoleFor${env.capitalize()}", Delete) {
@@ -74,16 +76,22 @@ def createConsoleTask = { env ->
createConsoleTask(env)
}
// Force an order so we don't run these tasks in parallel.
tasks.buildConsoleForCrash.mustRunAfter(tasks.buildConsoleForAlpha)
tasks.buildConsoleForQa.mustRunAfter(tasks.buildConsoleForCrash)
tasks.buildConsoleForSandbox.mustRunAfter(tasks.buildConsoleForQa)
tasks.buildConsoleForProduction.mustRunAfter(tasks.buildConsoleForSandbox)
// This task must run last, otherwise the previous tasks will have deleted the "dist" folder.
tasks.buildConsoleWebapp.mustRunAfter(tasks.buildConsoleForProduction)
task applyFormatting(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'prettify'
commandLine 'sh', '-c', 'npm run prettify'
}
task checkFormatting(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'prettify:check'
commandLine 'sh', '-c', 'npm run prettify:check'
}
tasks.buildConsoleWebapp.dependsOn(tasks.npmInstallDeps)
@@ -92,3 +100,4 @@ tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)
tasks.build.dependsOn(tasks.checkFormatting)
tasks.build.dependsOn(tasks.runConsoleWebappUnitTests)
tasks.build.dependsOn(tasks.buildConsoleForAll)
@@ -48,7 +48,11 @@ interface DomainData {
selector: 'app-response-dialog',
template: `
<h2 mat-dialog-title>{{ data.title }}</h2>
<mat-dialog-content [innerHTML]="data.content" />
<mat-dialog-content>
@for (line of data.content; track line) {
<div>{{ line }}</div>
}
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="onClose()">Close</button>
</mat-dialog-actions>
@@ -59,7 +63,7 @@ export class ResponseDialogComponent {
constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { title: string; content: string }
public data: { title: string; content: string[] }
) {}
onClose(): void {
@@ -312,11 +316,13 @@ export class DomainListComponent {
this.dialog.open(ResponseDialogComponent, {
data: {
title: 'Domain Deletion Results',
content: `Successfully deleted - ${successCount} domain(s)<br/>Failed to delete - ${failureCount} domain(s)<br/>${
content: [
`Successfully deleted - ${successCount} domain(s)`,
`Failed to delete - ${failureCount} domain(s)`,
failureCount
? 'Some domains could not be deleted due to ongoing processes or server errors. '
: ''
}Please check the table for more information.`,
? 'Some domains could not be deleted due to ongoing processes or server errors. Please check the table for more information.'
: 'Please check the table for more information.',
],
},
});
this.selection.clear();
@@ -97,10 +97,9 @@
@for (column of columns; track column.columnDef) {
<mat-list-item role="listitem">
<span class="console-app__list-key">{{ column.header }} </span>
<span
class="console-app__list-value"
[innerHTML]="column.cell(registrarInEdit).replace('<br/>', ' ')"
></span>
<span class="console-app__list-value">{{
column.cell(registrarInEdit)
}}</span>
</mat-list-item>
<mat-divider></mat-divider>
}
@@ -75,7 +75,7 @@ export class RegistrarDetailsComponent implements OnInit {
}
checkOteStatus() {
this.router.navigate(['ote-status/', this.registrarInEdit.registrarId], {
this.router.navigate(['ote-status', this.registrarInEdit.registrarId], {
queryParamsHandling: 'merge',
});
}
@@ -49,10 +49,9 @@
<mat-header-cell *matHeaderCellDef>
{{ column.header }}
</mat-header-cell>
<mat-cell
*matCellDef="let row"
[innerHTML]="column.cell(row)"
></mat-cell>
<mat-cell *matCellDef="let row" style="white-space: pre-wrap">{{
column.cell(row)
}}</mat-cell>
</ng-container>
}
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
@@ -56,7 +56,7 @@ export const columns = [
cell: (record: Registrar) =>
`${Object.entries(record.billingAccountMap || {}).reduce(
(acc, [key, val]) => {
return `${acc}${key}=${val}<br/>`;
return `${acc}${key}=${val}\n`;
},
''
)}`,
@@ -25,7 +25,18 @@
@for (column of columns; track column) {
<ng-container [matColumnDef]="column.columnDef">
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
<mat-cell *matCellDef="let row">
@if (column.columnDef === 'name') {
<div class="contact__name-column">
<div class="contact__name-column-title">{{ row.name }}</div>
<div class="contact__name-column-roles">
{{ row.userFriendlyTypes.join(" • ") }}
</div>
</div>
} @else {
{{ column.cell(row) }}
}
</mat-cell>
</ng-container>
}
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
@@ -34,14 +34,7 @@ export default class ContactComponent {
{
columnDef: 'name',
header: 'Name',
cell: (contact: ViewReadyContact) => `
<div class="contact__name-column">
<div class="contact__name-column-title">${contact.name}</div>
<div class="contact__name-column-roles">${contact.userFriendlyTypes.join(
' • '
)}</div>
</div>
`,
cell: (contact: ViewReadyContact) => `${contact.name}`,
},
{
columnDef: 'emailAddress',
@@ -10,7 +10,7 @@
<mat-header-cell *matHeaderCellDef>
{{ column.header }}
</mat-header-cell>
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
<mat-cell *matCellDef="let row">{{ column.cell(row) }}</mat-cell>
</ng-container>
}
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
+4 -5
View File
@@ -170,7 +170,6 @@ dependencies {
implementation deps['com.google.oauth-client:google-oauth-client-servlet']
implementation deps['com.google.re2j:re2j']
implementation deps['org.freemarker:freemarker']
implementation deps['com.googlecode.json-simple:json-simple']
implementation deps['com.github.mwiede:jsch']
implementation deps['com.zaxxer:HikariCP']
implementation deps['com.squareup.okhttp3:okhttp']
@@ -443,7 +442,7 @@ project.tasks.create('generateSqlSchema', JavaExec) {
mainClass = 'google.registry.tools.DevTool'
jvmArgs "--sun-misc-unsafe-memory-access=allow"
args = [
'-e', 'alpha',
'-e', 'unittest',
'generate_sql_schema', '--start_postgresql', '-o',
"${rootProject.projectRootDir}/db/src/main/resources/sql/schema/" +
"db-schema.sql.generated"
@@ -742,9 +741,9 @@ test {
// Don't run any tests from this task, all testing gets done in the
// FilteringTest tasks.
exclude "**"
}.dependsOn(standardTest, registryToolIntegrationTest, sqlIntegrationTest)
}.dependsOn(standardTest, registryToolIntegrationTest, sqlIntegrationTest, fragileTest)
// When we override tests, we also break the cleanTest command.
cleanTest.dependsOn(cleanStandardTest, cleanRegistryToolIntegrationTest, cleanSqlIntegrationTest)
cleanTest.dependsOn(cleanStandardTest, cleanRegistryToolIntegrationTest, cleanSqlIntegrationTest, cleanFragileTest)
project.build.dependsOn devtool
project.build.dependsOn devtool, buildToolImage
-1
View File
@@ -192,7 +192,6 @@ com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,nonprodAnnotationPr
com.google.protobuf:protobuf-java:4.35.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.re2j:re2j:1.8=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.truth:truth:1.4.5=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.googlecode.json-simple:json-simple:1.1.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.ibm.icu:icu4j:73.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.lmax:disruptor:3.4.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.puppycrawl.tools:checkstyle:10.24.0=checkstyle
@@ -212,17 +212,16 @@ public class RdeIO {
}
}
// Don't write the IDN elements for BRDA.
// Don't write the IDN elements or EPP params for BRDA.
if (mode == RdeMode.FULL) {
for (IdnTableEnum idn : IdnTableEnum.values()) {
output.write(marshaller.marshalIdn(idn.getTable()));
counter.increment(RdeResourceType.IDN);
}
output.write(marshaller.marshalRdeEppParams());
counter.increment(RdeResourceType.EPP_PARAMS);
}
output.write(marshaller.marshalRdeEppParams());
counter.increment(RdeResourceType.EPP_PARAMS);
// Output XML that says how many resources were emitted.
header = counter.makeHeader(tld, mode);
output.write(marshaller.marshalOrDie(new XjcRdeHeaderElement(header)));
@@ -372,7 +372,7 @@ public class RdePipeline implements Serializable {
* <p>The (repoId, pendingDeposit) pairs denote hosts that are referenced from a domain, that are
* to be included in the corresponding pending deposit.
*
* <p>The (repoId, revisionId) paris come from the most recent history entry query, which can be
* <p>The (repoId, revisionId) pairs come from the most recent history entry query, which can be
* used to load the embedded resources themselves.
*
* @return a pair of (repoId, ([pendingDeposit], [revisionId])) where neither the pendingDeposit
@@ -25,10 +25,20 @@ import com.google.common.flogger.FluentLogger;
import google.registry.model.EppResource;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import io.protostuff.Input;
import io.protostuff.LinkedBuffer;
import io.protostuff.Output;
import io.protostuff.Pipe;
import io.protostuff.ProtostuffIOUtil;
import io.protostuff.Schema;
import io.protostuff.WireFormat;
import io.protostuff.runtime.DefaultIdStrategy;
import io.protostuff.runtime.Delegate;
import io.protostuff.runtime.RuntimeSchema;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import redis.clients.jedis.AbstractPipeline;
@@ -52,11 +62,20 @@ public class SimplifiedJedisClient {
Domain.class, "d_",
Host.class, "h_");
/** We need to inform Protostuff of the custom {@link InetAddress} delegates. */
private static DefaultIdStrategy createIdStrategy() {
DefaultIdStrategy strategy = new DefaultIdStrategy();
strategy.registerDelegate(new GenericInetAddressDelegate<>(InetAddress.class));
strategy.registerDelegate(new GenericInetAddressDelegate<>(Inet4Address.class));
strategy.registerDelegate(new GenericInetAddressDelegate<>(Inet6Address.class));
return strategy;
}
private static final ImmutableMap<Class<? extends EppResource>, Schema<? extends EppResource>>
VALUE_SCHEMAS =
ImmutableMap.of(
Domain.class, RuntimeSchema.getSchema(Domain.class),
Host.class, RuntimeSchema.getSchema(Host.class));
Host.class, RuntimeSchema.getSchema(Host.class, createIdStrategy()));
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -151,4 +170,46 @@ public class SimplifiedJedisClient {
checkArgument(VALUE_SCHEMAS.containsKey(clazz), "Unknown class type %s", clazz);
return (Schema<V>) VALUE_SCHEMAS.get(clazz);
}
/**
* A custom Protostuff {@link Delegate} for {@link InetAddress} and its subclasses.
*
* <p>This is required in Java 17+ because Protostuff's default runtime schema serialization
* relies on reflection. Since {@link InetAddress} is part of the encapsulated {@code java.base}
* module, reflective access is restricted and throws {@link
* java.lang.reflect.InaccessibleObjectException}.
*
* <p>This delegate serializes the IP address as a raw byte array using {@link
* InetAddress#getAddress()} and reconstructs it using {@link InetAddress#getByAddress(byte[])}
*/
private record GenericInetAddressDelegate<T extends InetAddress>(Class<T> clazz)
implements Delegate<T> {
@Override
public WireFormat.FieldType getFieldType() {
return WireFormat.FieldType.BYTES;
}
@Override
public Class<T> typeClass() {
return clazz;
}
@SuppressWarnings("unchecked")
@Override
public T readFrom(Input input) throws IOException {
return (T) InetAddress.getByAddress(input.readByteArray());
}
@Override
public void writeTo(Output output, int number, T value, boolean repeated) throws IOException {
output.writeByteArray(number, value.getAddress(), repeated);
}
@Override
public void transfer(Pipe pipe, Input input, Output output, int number, boolean repeated)
throws IOException {
output.writeByteArray(number, input.readByteArray(), repeated);
}
}
}
@@ -173,6 +173,18 @@ public final class RegistryConfig {
return config.registrarConsole.supportEmailAddress;
}
@Provides
@Config("consoleHistoryQueryLimit")
public static int provideConsoleHistoryQueryLimit(RegistryConfigSettings config) {
return config.registrarConsole.historyQueryLimit;
}
@Provides
@Config("consoleBulkDomainActionLimit")
public static int provideConsoleBulkDomainActionLimit(RegistryConfigSettings config) {
return config.registrarConsole.bulkDomainActionLimit;
}
/**
* The DUM file name, used as a file name base for DUM csv file
*
@@ -181,6 +181,8 @@ public class RegistryConfigSettings {
public String supportPhoneNumber;
public String supportEmailAddress;
public String technicalDocsUrl;
public int historyQueryLimit;
public int bulkDomainActionLimit;
}
/** Configuration for monitoring. */
@@ -19,6 +19,7 @@
<queue>
<name>dns-refresh</name>
<max-dispatches-per-second>100</max-dispatches-per-second>
<max-retry-duration>82800s</max-retry-duration>
</queue>
<!-- Queue for publishing DNS updates in batches. -->
@@ -29,6 +30,7 @@
<min-backoff>30s</min-backoff>
<max-backoff>1800s</max-backoff>
<max-doublings>0</max-doublings>
<max-retry-duration>82800s</max-retry-duration>
</queue>
<!-- Queue for uploading RDE deposits to the escrow provider. -->
@@ -59,6 +61,7 @@
<queue>
<name>async-host-rename</name>
<max-dispatches-per-second>1</max-dispatches-per-second>
<max-retry-duration>82800s</max-retry-duration>
</queue>
<!-- Queue for tasks that wait for a Beam pipeline to complete (i.e. Spec11 and invoicing). -->
@@ -110,11 +113,12 @@
<max-attempts>3</max-attempts>
</queue>
<!-- &lt;!&ndash; Queue for async actions that should be run at some point in the future. &ndash;&gt;-->
<!-- Queue for async actions that should be run at some point in the future.-->
<queue>
<name>async-actions</name>
<max-dispatches-per-second>1</max-dispatches-per-second>
<max-concurrent-dispatches>5</max-concurrent-dispatches>
<max-retry-duration>82800s</max-retry-duration>
</queue>
</entries>
@@ -380,6 +380,12 @@ registrarConsole:
# URL linking to directory of technical support docs on the registry.
technicalDocsUrl: http://example.com/your_support_docs/
# Maximum number of history records returned in a single query.
historyQueryLimit: 500
# Maximum number of domains allowed in a single bulk action.
bulkDomainActionLimit: 500
monitoring:
# Max queries per second for the Google Cloud Monitoring V3 (aka Stackdriver)
# API. The limit can be adjusted by contacting Cloud Support.
@@ -266,16 +266,6 @@
<schedule>0 15 * * *</schedule>
</task>
<task>
<url><![CDATA[/_dr/task/wipeOutContactHistoryPii]]></url>
<name>wipeOutContactHistoryPii</name>
<description>
This job runs weekly to wipe out PII fields of ContactHistory entities
that have been in the database for a certain period of time.
</description>
<schedule>0 15 * * 1</schedule>
</task>
<task>
<url><![CDATA[/_dr/task/bsaDownload]]></url>
<name>bsaDownload</name>
@@ -155,16 +155,6 @@
<schedule>*/1 * * * *</schedule>
</task>
<task>
<url><![CDATA[/_dr/task/wipeOutContactHistoryPii]]></url>
<name>wipeOutContactHistoryPii</name>
<description>
This job runs weekly to wipe out PII fields of ContactHistory entities
that have been in the database for a certain period of time.
</description>
<schedule>0 15 * * 1</schedule>
</task>
<task>
<url><![CDATA[/_dr/task/bsaDownload]]></url>
<name>bsaDownload</name>
@@ -36,13 +36,13 @@ import static google.registry.persistence.PersistenceModule.TransactionIsolation
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import static org.json.simple.JSONValue.toJSONString;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InternetDomainName;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import dagger.Module;
import dagger.Provides;
import google.registry.flows.domain.DomainFlowUtils.BadCommandForRegistryPhaseException;
@@ -83,6 +83,7 @@ public class CheckApiAction implements Runnable {
@Inject Response response;
@Inject CheckApiMetric.Builder metricBuilder;
@Inject CheckApiMetrics checkApiMetrics;
@Inject Gson gson;
@Inject
CheckApiAction() {}
@@ -94,7 +95,7 @@ public class CheckApiAction implements Runnable {
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
response.setContentType(MediaType.JSON_UTF_8);
response.setPayload(toJSONString(doCheck()));
response.setPayload(gson.toJson(doCheck()));
} finally {
CheckApiMetric metric = metricBuilder.build();
checkApiMetrics.incrementCheckApiRequest(metric);
@@ -24,6 +24,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.gson.Gson;
import google.registry.flows.FlowModule.EppExceptionInProviderException;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppinput.EppInput;
@@ -34,7 +35,6 @@ import google.registry.model.eppoutput.Result.Code;
import google.registry.monitoring.whitebox.EppMetric;
import jakarta.inject.Inject;
import java.util.Optional;
import org.json.simple.JSONValue;
/**
* An implementation of the EPP command/response protocol.
@@ -50,6 +50,8 @@ public final class EppController {
@Inject EppMetric.Builder eppMetricBuilder;
@Inject EppMetrics eppMetrics;
@Inject ServerTridProvider serverTridProvider;
@Inject Gson gson;
@Inject EppController() {}
/** Reads EPP XML, executes the matching flow, and returns an {@link EppOutput}. */
@@ -72,7 +74,7 @@ public final class EppController {
e.getMessage(),
lazy(
() ->
JSONValue.toJSONString(
gson.toJson(
ImmutableMap.<String, Object>of(
"clientId",
nullToEmpty(sessionMetadata.getRegistrarId()),
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.gson.Gson;
import google.registry.flows.FlowModule.InputXml;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.annotations.ReportingSpec;
@@ -30,7 +31,6 @@ import google.registry.model.eppcommon.Trid;
import google.registry.model.eppinput.EppInput;
import jakarta.inject.Inject;
import java.util.Optional;
import org.json.simple.JSONValue;
/** Reporter used by {@link FlowRunner} to record flow execution data for reporting. */
public class FlowReporter {
@@ -49,6 +49,8 @@ public class FlowReporter {
@Inject @InputXml byte[] inputXmlBytes;
@Inject EppInput eppInput;
@Inject Class<? extends Flow> flowClass;
@Inject Gson gson;
@Inject FlowReporter() {}
/** Records information about the current flow execution in the request logs. */
@@ -61,7 +63,7 @@ public class FlowReporter {
logger.atInfo().log(
"%s: %s",
METADATA_LOG_SIGNATURE,
JSONValue.toJSONString(
gson.toJson(
new ImmutableMap.Builder<String, Object>()
.put("serverTrid", trid.getServerTransactionId())
.put("clientId", registrarId)
@@ -44,6 +44,8 @@ import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.Host;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
@@ -134,7 +136,9 @@ public final class ResourceFlowUtils {
}
String authPassword = authInfo.getPw().getValue();
String domainPassword = domain.getAuthInfo().getPw().getValue();
if (!domainPassword.equals(authPassword)) {
if (!MessageDigest.isEqual(
authPassword.getBytes(StandardCharsets.UTF_8),
domainPassword.getBytes(StandardCharsets.UTF_8))) {
throw new BadAuthInfoForResourceException();
}
}
@@ -23,6 +23,7 @@ import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.intersection;
import static com.google.common.collect.Sets.union;
import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked;
import static google.registry.model.common.FeatureFlag.FeatureName.FORBID_INSECURE_ALGORITHMS_RFC_9904;
import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS;
import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA;
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
@@ -73,6 +74,7 @@ import google.registry.model.EppResource;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.common.FeatureFlag;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Create;
import google.registry.model.domain.DomainCommand.CreateOrUpdate;
@@ -341,7 +343,7 @@ public class DomainFlowUtils {
}
ImmutableList<DomainDsData> invalidAlgorithms =
dsData.stream()
.filter(ds -> !validateAlgorithm(ds.getAlgorithm()))
.filter(ds -> algorithmIsInvalid(ds.getAlgorithm()))
.collect(toImmutableList());
if (!invalidAlgorithms.isEmpty()) {
throw new InvalidDsRecordException(
@@ -349,9 +351,16 @@ public class DomainFlowUtils {
"Domain contains DS record(s) with an invalid algorithm wire value: %s",
invalidAlgorithms));
}
boolean forbidInsecureTypes = FeatureFlag.isActiveNow(FORBID_INSECURE_ALGORITHMS_RFC_9904);
ImmutableList<DomainDsData> invalidDigestTypes =
dsData.stream()
.filter(ds -> DigestType.fromWireValue(ds.getDigestType()).isEmpty())
.filter(
ds -> {
Optional<DigestType> digestType = DigestType.fromWireValue(ds.getDigestType());
return digestType
.map(type -> forbidInsecureTypes && !type.isAllowedInRfc9904())
.orElse(true);
})
.collect(toImmutableList());
if (!invalidDigestTypes.isEmpty()) {
throw new InvalidDsRecordException(
@@ -376,14 +385,18 @@ public class DomainFlowUtils {
}
}
public static boolean validateAlgorithm(int alg) {
public static boolean algorithmIsInvalid(int alg) {
if (alg > 255 || alg < 0) {
return false;
return true;
}
if (DomainDsData.FORBIDDEN_ALGORITHMS.contains(alg)
&& FeatureFlag.isActiveNow(FORBID_INSECURE_ALGORITHMS_RFC_9904)) {
return true;
}
// Algorithms that are reserved or unassigned will just return a string representation of their
// integer wire value.
String algorithm = Algorithm.string(alg);
return !algorithm.equals(Integer.toString(alg));
return algorithm.equals(Integer.toString(alg));
}
/** We only allow specifying years in a period. */
@@ -283,7 +283,7 @@ public final class DomainPricingLogic {
return tld.getStandardRenewCost(dateTime).multipliedBy(years);
}
if (token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.SPECIFIED)) {
return token.getRenewalPrice().get();
return token.getRenewalPrice().get().multipliedBy(years);
}
}
return getDomainCostWithDiscount(
@@ -328,12 +328,13 @@ public final class DomainPricingLogic {
// Apply the allocation token discount, if applicable.
if (token.getDiscountPrice().isPresent()
&& tld.getCurrency().equals(token.getDiscountPrice().get().getCurrencyUnit())) {
int nonDiscountedYears = Math.max(0, years - token.getDiscountYears());
int discountedYears = Math.min(years, token.getDiscountYears());
int nonDiscountedYears = years - discountedYears;
totalDomainFlowCost =
token
.getDiscountPrice()
.get()
.multipliedBy(token.getDiscountYears())
.multipliedBy(discountedYears)
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(nonDiscountedYears));
} else if (token.getDiscountFraction() > 0) {
int discountedYears = Math.min(years, token.getDiscountYears());
@@ -116,15 +116,21 @@ public class HostFlowUtils {
if (inetAddresses == null) {
return;
}
if (inetAddresses.stream().anyMatch(InetAddress::isLoopbackAddress)) {
throw new LoopbackIpNotValidForHostException();
for (InetAddress inetAddress : inetAddresses) {
if (inetAddress.isLoopbackAddress()
|| inetAddress.isLinkLocalAddress()
|| inetAddress.isSiteLocalAddress()
|| inetAddress.isAnyLocalAddress()
|| inetAddress.isMulticastAddress()) {
throw new IpAddressNotRoutableException(inetAddress.getHostAddress());
}
}
}
/** Loopback IPs are not valid for hosts. */
static class LoopbackIpNotValidForHostException extends ParameterValuePolicyErrorException {
public LoopbackIpNotValidForHostException() {
super("Loopback IPs are not valid for hosts");
/** IP address is not a public, routable address. */
static class IpAddressNotRoutableException extends ParameterValuePolicyErrorException {
public IpAddressNotRoutableException(String ipAddress) {
super(String.format("IP address %s is not a public, globally routable address", ipAddress));
}
}
@@ -14,6 +14,7 @@
package google.registry.flows.picker;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableTable;
@@ -255,6 +256,17 @@ public class FlowPicker {
if (innerCommand == null && !(eppInput.getCommandWrapper() instanceof Hello)) {
throw new MissingCommandException();
}
if (innerCommand instanceof ResourceCommandWrapper resourceCommandWrapper) {
ResourceCommand resourceCommand = resourceCommandWrapper.getResourceCommand();
if (resourceCommand != null) {
String wrapperName = innerCommand.getClass().getSimpleName();
String commandName = resourceCommand.getClass().getSimpleName();
if (!wrapperName.equals(commandName)) {
throw new MismatchedCommandException(
Ascii.toLowerCase(wrapperName), Ascii.toLowerCase(commandName));
}
}
}
// Try the FlowProviders until we find a match. The order matters because it's possible to
// match multiple FlowProviders and so more specific matches are tried first.
for (FlowProvider flowProvider : FLOW_PROVIDERS) {
@@ -279,4 +291,14 @@ public class FlowPicker {
super("Command missing");
}
}
/** Command wrapper and inner resource command do not match. */
static class MismatchedCommandException extends SyntaxErrorException {
public MismatchedCommandException(String wrapperName, String commandName) {
super(
String.format(
"EPP command wrapper <%s> does not match resource command <%s>",
wrapperName, commandName));
}
}
}
@@ -84,7 +84,10 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS(FeatureStatus.INACTIVE),
/** If we're prohibiting the inclusion of the contact object URI on login. */
PROHIBIT_CONTACT_OBJECTS_ON_LOGIN(FeatureStatus.INACTIVE);
PROHIBIT_CONTACT_OBJECTS_ON_LOGIN(FeatureStatus.INACTIVE),
/** If we're prohibiting insecure algorithms as detailed by RFC 9904. */
FORBID_INSECURE_ALGORITHMS_RFC_9904(FeatureStatus.INACTIVE);
private final FeatureStatus defaultStatus;
@@ -118,7 +118,9 @@ public class PasswordResetRequest extends ImmutableObject implements Buildable {
checkArgumentNotNull(getInstance().requester, "Requester must be specified");
checkArgumentNotNull(getInstance().destinationEmail, "Destination email must be specified");
checkArgumentNotNull(getInstance().registrarId, "Registrar ID must be specified");
getInstance().verificationCode = UUID.randomUUID().toString();
if (getInstance().verificationCode == null) {
getInstance().verificationCode = UUID.randomUUID().toString();
}
return super.build();
}
@@ -158,12 +158,12 @@ public class DomainCommand {
throws InvalidReferencesException, ParameterValuePolicyErrorException {
Create clone = clone(this);
clone.nameservers = linkHosts(nullSafeImmutableCopy(clone.nameserverHostNames), now);
if (registrantContactId != null) {
throw new RegistrantProhibitedException();
}
if (!isNullOrEmpty(foreignKeyedDesignatedContacts)) {
throw new ContactsProhibitedException();
}
if (registrantContactId != null) {
throw new RegistrantProhibitedException();
}
return clone;
}
@@ -14,6 +14,7 @@
package google.registry.model.domain.secdns;
import com.google.common.collect.ImmutableSet;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import jakarta.persistence.Access;
@@ -26,6 +27,7 @@ import jakarta.xml.bind.annotation.XmlTransient;
import jakarta.xml.bind.annotation.XmlType;
import jakarta.xml.bind.annotation.adapters.HexBinaryAdapter;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.xbill.DNS.DNSSEC.Algorithm;
/** Base class for {@link DomainDsData} and {@link DomainDsDataHistory}. */
@MappedSuperclass
@@ -33,6 +35,16 @@ import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlType(propOrder = {"keyTag", "algorithm", "digestType", "digest"})
public abstract class DomainDsDataBase extends ImmutableObject implements UnsafeSerializable {
// A set of algorithms that we do not allow. See RFC 9904 for more details.
public static final ImmutableSet<Integer> FORBIDDEN_ALGORITHMS =
ImmutableSet.of(
Algorithm.RSAMD5,
Algorithm.RSASHA1,
Algorithm.RSA_NSEC3_SHA1,
Algorithm.DSA,
Algorithm.DSA_NSEC3_SHA1,
Algorithm.ECC_GOST);
@XmlTransient @Transient @Insignificant String domainRepoId;
/** The identifier for this particular key in the domain. */
@@ -71,6 +71,8 @@ import google.registry.model.domain.token.VKeyConverter_AllocationToken;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.ReservedList;
import google.registry.persistence.EntityCallbacksListener.RecursivePostPersist;
import google.registry.persistence.EntityCallbacksListener.RecursivePostRemove;
import google.registry.persistence.EntityCallbacksListener.RecursivePostUpdate;
import google.registry.persistence.VKey;
import google.registry.persistence.converter.AllocationTokenVkeyListUserType;
import google.registry.persistence.converter.BillingCostTransitionUserType;
@@ -219,9 +221,12 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
* Invalidates the cache entry.
*
* <p>This is called automatically when the tld is saved. One should also call it when a tld is
* deleted.
* deleted. This only affects the pod-local cache so most pods won't catch it, but it's still the
* right thing to do.
*/
@RecursivePostPersist
@RecursivePostRemove
@RecursivePostUpdate
public void invalidateInCache() {
CACHE.invalidate(tldStr);
}
@@ -164,7 +164,7 @@ import google.registry.ui.server.console.settings.SecurityAction;
ToolsServerModule.class,
WhiteboxModule.class
})
interface RequestComponent {
public interface RequestComponent {
FlowComponent.Builder flowComponentBuilder();
BrdaCopyAction brdaCopyAction();
@@ -30,7 +30,7 @@ public enum RdeResourceType {
REGISTRAR("urn:ietf:params:xml:ns:rdeRegistrar-1.0", EnumSet.of(FULL, THIN)),
IDN("urn:ietf:params:xml:ns:rdeIDN-1.0", EnumSet.of(FULL)),
HEADER("urn:ietf:params:xml:ns:rdeHeader-1.0", EnumSet.of(FULL, THIN)),
EPP_PARAMS("urn:ietf:params:xml:ns:rdeEppParams-1.0", EnumSet.of(FULL, THIN));
EPP_PARAMS("urn:ietf:params:xml:ns:rdeEppParams-1.0", EnumSet.of(FULL));
private final String uri;
private final ImmutableSet<RdeMode> modes;
@@ -21,10 +21,8 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.util.stream.Collectors.joining;
import com.google.cloud.storage.BlobId;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteStreams;
import com.google.common.net.MediaType;
@@ -41,6 +39,8 @@ import jakarta.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Copy all registrar detail reports in a given bucket's subdirectory from GCS to Drive. */
@Action(
@@ -54,6 +54,9 @@ public final class CopyDetailReportsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Pattern FILENAME_PATTERN =
Pattern.compile("^invoice_details_[0-9]{4}-[0-9]{2}_(.+)_.+\\.csv$");
private final String billingBucket;
private final String invoiceDirectoryPrefix;
private final DriveConnection driveConnection;
@@ -101,8 +104,13 @@ public final class CopyDetailReportsAction implements Runnable {
new ImmutableMultimap.Builder<>();
for (String detailReportName : detailReportObjectNames) {
// The standard report format is "invoice_details_yyyy-MM_registrarId_tld.csv
// TODO(larryruili): Determine a safer way of enforcing this.
String registrarId = Iterables.get(Splitter.on('_').split(detailReportName), 3);
Matcher matcher = FILENAME_PATTERN.matcher(detailReportName);
if (!matcher.matches()) {
logger.atWarning().log(
"Detail report filename '%s' does not match the expected pattern.", detailReportName);
continue;
}
String registrarId = matcher.group(1);
Optional<Registrar> registrar = Registrar.loadByRegistrarId(registrarId);
if (registrar.isEmpty()) {
logger.atWarning().log(
@@ -18,8 +18,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.net.HttpHeaders.CONTENT_DISPOSITION;
import static com.google.common.net.HttpHeaders.X_CONTENT_TYPE_OPTIONS;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static org.json.simple.JSONValue.toJSONString;
import com.google.gson.Gson;
import jakarta.inject.Inject;
import java.time.Instant;
import java.util.Map;
@@ -31,10 +31,12 @@ public class JsonResponse {
public static final String JSON_SAFETY_PREFIX = ")]}'\n";
protected final Response response;
protected final Gson gson;
@Inject
public JsonResponse(Response rsp) {
public JsonResponse(Response rsp, Gson gson) {
this.response = rsp;
this.gson = gson;
}
/**
@@ -55,7 +57,7 @@ public class JsonResponse {
// response, even if all else fails. It's basically another anti-sniffing mechanism in the sense
// that if you hit this url directly, it would try to download the file instead of showing it.
response.setHeader(CONTENT_DISPOSITION, "attachment");
response.setPayload(JSON_SAFETY_PREFIX + toJSONString(checkNotNull(responseMap)));
response.setPayload(JSON_SAFETY_PREFIX + gson.toJson(checkNotNull(responseMap)));
}
/**
@@ -14,6 +14,7 @@
package google.registry.request;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static google.registry.dns.PublishDnsUpdatesAction.CLOUD_TASKS_RETRY_HEADER;
import static google.registry.model.tld.Tlds.assertTldExists;
@@ -28,7 +29,6 @@ import static google.registry.request.RequestParameters.extractSetOfParameters;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
@@ -36,6 +36,8 @@ import com.google.common.io.CharStreams;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import com.google.protobuf.ByteString;
import dagger.Module;
import dagger.Provides;
@@ -50,8 +52,6 @@ import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
/** Dagger module for servlets. */
@Module
@@ -202,18 +202,16 @@ public final class RequestModule {
@Provides
@JsonPayload
@SuppressWarnings("unchecked")
static Map<String, Object> provideJsonPayload(
@Header("Content-Type") MediaType contentType, @Payload String payload) {
@Header("Content-Type") MediaType contentType, @Payload String payload, Gson gson) {
if (!JSON_UTF_8.is(contentType.withCharset(UTF_8))) {
throw new UnsupportedMediaTypeException(
String.format("Expected %s Content-Type", JSON_UTF_8.withoutParameters()));
}
try {
return (Map<String, Object>) JSONValue.parseWithException(payload);
} catch (ParseException e) {
throw new BadRequestException(
"Malformed JSON", new VerifyException("Malformed JSON:\n" + payload, e));
return checkNotNull(gson.fromJson(payload, new TypeToken<>() {}));
} catch (JsonSyntaxException | NullPointerException e) {
throw new BadRequestException("Malformed JSON:\n" + payload);
}
}
@@ -14,11 +14,9 @@
package google.registry.request;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.joining;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import java.util.Comparator;
@@ -64,18 +62,6 @@ public class RouterDisplayHelper {
return formatRoutes(Router.extractRoutesFromComponent(componentClass).values());
}
public static ImmutableList<String> extractHumanReadableRoutesWithWrongService(
Class<?> componentClass, Action.Service expectedService) {
return Router.extractRoutesFromComponent(componentClass).values().stream()
.filter(route -> route.action().service() != expectedService)
.map(
route ->
String.format(
"%s (%s%s)",
route.actionClass(), route.action().service(), route.action().path()))
.collect(toImmutableList());
}
private static String getFormatString(Map<String, Integer> columnWidths) {
return String.format(
FORMAT,
@@ -43,6 +43,7 @@ import jakarta.inject.Qualifier;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import javax.annotation.Nullable;
@@ -88,8 +89,8 @@ public class AuthModule {
TokenVerifier provideIapTokenVerifier(
@Config("projectIdNumber") long projectIdNumber,
@Named("backendServiceIdMap") Supplier<ImmutableMap<String, Long>> backendServiceIdMap) {
com.google.auth.oauth2.TokenVerifier.Builder tokenVerifierBuilder =
com.google.auth.oauth2.TokenVerifier.newBuilder().setIssuer(IAP_ISSUER_URL);
ConcurrentHashMap<String, com.google.auth.oauth2.TokenVerifier> tokenVerifiers =
new ConcurrentHashMap<>();
return (String service, String token) -> {
Long backendServiceId = backendServiceIdMap.get().get(service);
checkNotNull(
@@ -98,7 +99,15 @@ public class AuthModule {
service,
backendServiceIdMap);
String audience = String.format(IAP_AUDIENCE_FORMAT, projectIdNumber, backendServiceId);
return tokenVerifierBuilder.setAudience(audience).build().verify(token);
com.google.auth.oauth2.TokenVerifier verifier =
tokenVerifiers.computeIfAbsent(
audience,
aud ->
com.google.auth.oauth2.TokenVerifier.newBuilder()
.setIssuer(IAP_ISSUER_URL)
.setAudience(aud)
.build());
return verifier.verify(token);
};
}
@@ -18,10 +18,13 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.net.HttpHeaders.CONTENT_DISPOSITION;
import static com.google.common.net.HttpHeaders.X_CONTENT_TYPE_OPTIONS;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static org.json.simple.JSONValue.writeJSONString;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import google.registry.tools.GsonUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@@ -29,8 +32,6 @@ import java.io.Reader;
import java.io.Writer;
import java.util.Map;
import javax.annotation.Nullable;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
/**
* Helper class for servlets that read or write JSON.
@@ -41,6 +42,8 @@ public final class JsonHttp {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Gson GSON = GsonUtils.provideGson();
/** String prefixed to all JSON-like responses. */
public static final String JSON_SAFETY_PREFIX = ")]}'\n";
@@ -51,7 +54,6 @@ public final class JsonHttp {
* @throws IOException if we failed to read from {@code req}.
*/
@Nullable
@SuppressWarnings("unchecked")
public static Map<String, ?> read(HttpServletRequest req) throws IOException {
if (!"POST".equals(req.getMethod())
&& !"PUT".equals(req.getMethod())) {
@@ -64,8 +66,8 @@ public final class JsonHttp {
}
try (Reader jsonReader = req.getReader()) {
try {
return checkNotNull((Map<String, ?>) JSONValue.parseWithException(jsonReader));
} catch (ParseException | NullPointerException | ClassCastException e) {
return checkNotNull(GSON.fromJson(jsonReader, new TypeToken<>() {}));
} catch (JsonSyntaxException | NullPointerException | ClassCastException e) {
logger.atWarning().withCause(e).log("Malformed JSON.");
return null;
}
@@ -88,7 +90,7 @@ public final class JsonHttp {
rsp.setHeader(CONTENT_DISPOSITION, "attachment");
try (Writer writer = rsp.getWriter()) {
writer.write(JSON_SAFETY_PREFIX);
writeJSONString(jsonObject, writer);
GSON.toJson(jsonObject, writer);
}
}
}
@@ -25,6 +25,7 @@ import com.google.common.hash.Hashing;
import google.registry.model.server.ServerSecret;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
@@ -102,7 +103,7 @@ public final class XsrfTokenManager {
}
// Reconstruct the token to verify validity.
String reconstructedToken = encodeToken(ServerSecret.get().asBytes(), email, timestampMillis);
if (!token.equals(reconstructedToken)) {
if (!MessageDigest.isEqual(token.getBytes(UTF_8), reconstructedToken.getBytes(UTF_8))) {
logger.atWarning().log(
"Reconstructed XSRF mismatch (got != expected): %s != %s", token, reconstructedToken);
return false;
@@ -14,12 +14,14 @@
package google.registry.tmch;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.request.UrlConnectionUtils.getResponseBytes;
import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteSource;
import google.registry.request.Action;
@@ -62,6 +64,8 @@ public final class NordnVerifyAction implements Runnable {
static final String NORDN_URL_PARAM = "nordnUrl";
static final String NORDN_LOG_ID_PARAM = "nordnLogId";
private static final String MARKSDB_URL_BEGINNING = "ry.marksdb.org";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject LordnRequestInitializer lordnRequestInitializer;
@@ -104,6 +108,12 @@ public final class NordnVerifyAction implements Runnable {
*/
@VisibleForTesting
LordnLog verify() throws IOException, GeneralSecurityException {
String host = Ascii.toLowerCase(url.getHost());
checkArgument(
host.startsWith(MARKSDB_URL_BEGINNING),
"URL %s must start with %s",
url,
MARKSDB_URL_BEGINNING);
logger.atInfo().log("LORDN verify task %s: Sending request to URL %s", actionLogId, url);
HttpURLConnection connection = urlConnectionService.createConnection(url);
lordnRequestInitializer.initialize(connection, tld);
@@ -25,6 +25,7 @@ import google.registry.request.HttpException.BadRequestException;
import google.registry.request.Parameter;
import jakarta.servlet.http.HttpServletRequest;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import org.bouncycastle.openpgp.PGPPublicKey;
@@ -53,7 +54,7 @@ public final class TmchModule {
@Parameter(NordnVerifyAction.NORDN_URL_PARAM)
static URL provideNordnUrl(HttpServletRequest req) {
try {
return new URL(extractRequiredParameter(req, NordnVerifyAction.NORDN_URL_PARAM));
return URI.create(extractRequiredParameter(req, NordnVerifyAction.NORDN_URL_PARAM)).toURL();
} catch (MalformedURLException e) {
throw new BadRequestException("Bad URL: " + NordnVerifyAction.NORDN_URL_PARAM);
}
@@ -29,21 +29,26 @@ import java.util.Optional;
* https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml
*/
public enum DigestType {
SHA1(1, 20),
SHA256(2, 32),
// Algorithm number 1 is SHA-1 and will be is deliberately NOT SUPPORTED.
// RFC 9904 specifies that this algorithm MUST NOT be used for DNSSEC delegations.
// This prohibition is gated behind a feature flag.
SHA1(1, 20, false),
SHA256(2, 32, true),
// Algorithm number 3 is GOST R 34.11-94 and is deliberately NOT SUPPORTED.
// This algorithm was reviewed by ise-crypto and deemed academically broken (b/207029800).
// In addition, RFC 8624 specifies that this algorithm MUST NOT be used for DNSSEC delegations.
// TODO(sarhabot@): Add note in Cloud DNS code to notify the Registry of any new changes to
// supported digest types.
SHA384(4, 48);
SHA384(4, 48, true);
private final int wireValue;
private final int bytes;
private final boolean allowedInRfc9904;
DigestType(int wireValue, int bytes) {
DigestType(int wireValue, int bytes, boolean allowedInRfc9904) {
this.wireValue = wireValue;
this.bytes = bytes;
this.allowedInRfc9904 = allowedInRfc9904;
}
private static final ImmutableMap<Integer, DigestType> WIRE_VALUE_TO_DIGEST_TYPE =
@@ -63,4 +68,9 @@ public enum DigestType {
public int getBytes() {
return bytes;
}
/** Whether this digest type is supported as of RFC 9904. */
public boolean isAllowedInRfc9904() {
return allowedInRfc9904;
}
}
@@ -15,6 +15,7 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.beust.jcommander.IStringConverter;
@@ -46,7 +47,7 @@ record DsRecord(int keyTag, int alg, int digestType, String digest) {
String.format("DS record has an invalid digest length: %s", digest));
}
if (!DomainFlowUtils.validateAlgorithm(alg)) {
if (tm().reTransact(() -> DomainFlowUtils.algorithmIsInvalid(alg))) {
throw new IllegalArgumentException(
String.format("DS record uses an unrecognized algorithm: %d", alg));
}
@@ -25,6 +25,7 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.persistence.transaction.QueryComposer.Comparator;
@@ -38,7 +39,6 @@ import java.nio.file.Paths;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import org.json.simple.JSONValue;
/** Command to generate a report of all DNS data. */
@Parameters(separators = " =", commandDescription = "Generate report of all DNS data in a TLD.")
@@ -57,6 +57,7 @@ final class GenerateDnsReportCommand implements Command {
private Path output = Paths.get("/dev/stdout");
@Inject Clock clock;
@Inject Gson gson;
@Override
public void run() throws Exception {
@@ -144,7 +145,7 @@ final class GenerateDnsReportCommand implements Command {
} else {
result.append(",\n");
}
result.append(JSONValue.toJSONString(map));
result.append(gson.toJson(map));
}
}
}
@@ -14,27 +14,17 @@
package google.registry.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.module.RequestComponent;
import google.registry.request.RouterDisplayHelper;
/** Generates the routing map file used for unit testing. */
@Parameters(commandDescription = "Generate a routing map file")
final class GetRoutingMapCommand implements Command {
@Parameter(
names = {"-c", "--class"},
description =
"Request component class (e.g. google.registry.module.backend.BackendRequestComponent)"
+ " for which routing map should be generated",
required = true
)
private String serviceClassName;
@Override
public void run() throws Exception {
System.out.println(
RouterDisplayHelper.extractHumanReadableRoutesFromComponent(
Class.forName(serviceClassName)));
RouterDisplayHelper.extractHumanReadableRoutesFromComponent(RequestComponent.class));
}
}
@@ -16,6 +16,7 @@ package google.registry.tools;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.ToNumberPolicy;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
@@ -82,6 +83,7 @@ public class GsonUtils {
.registerTypeAdapter(Serializable.class, new SerializableJsonTypeAdapter())
.registerTypeAdapterFactory(new ClassProcessingTypeAdapterFactory())
.registerTypeAdapterFactory(new GsonPostProcessableTypeAdapterFactory())
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
.excludeFieldsWithoutExposeAnnotation()
.create();
}
@@ -23,10 +23,13 @@ import com.beust.jcommander.Parameter;
import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.json.simple.JSONValue;
/**
* Abstract base class for commands that list objects by calling a server task.
@@ -35,6 +38,8 @@ import org.json.simple.JSONValue;
*/
abstract class ListObjectsCommand implements CommandWithConnection {
private static final Gson GSON = GsonUtils.provideGson();
@Nullable
@Parameter(
names = {"-f", "--fields"},
@@ -87,14 +92,13 @@ abstract class ListObjectsCommand implements CommandWithConnection {
connection.sendPostRequest(
getCommandPath(), params.build(), MediaType.PLAIN_TEXT_UTF_8, new byte[0]);
// Parse the returned JSON and make sure it's a map.
Object obj = JSONValue.parse(response.substring(JSON_SAFETY_PREFIX.length()));
if (!(obj instanceof Map<?, ?>)) {
JsonElement element = JsonParser.parseString(response.substring(JSON_SAFETY_PREFIX.length()));
if (!element.isJsonObject()) {
throw new VerifyException("Server returned unexpected JSON: " + response);
}
@SuppressWarnings("unchecked")
Map<String, Object> responseMap = (Map<String, Object>) obj;
Map<String, Object> responseMap = GSON.fromJson(element, new TypeToken<>() {});
// Get the status.
obj = responseMap.get("status");
Object obj = responseMap.get("status");
if (obj == null) {
throw new VerifyException("Server returned no status");
}
@@ -34,6 +34,8 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.CharStreams;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import google.registry.config.RegistryConfig.Config;
import google.registry.request.Action.Service;
import jakarta.inject.Inject;
@@ -42,7 +44,6 @@ import java.io.InputStreamReader;
import java.net.URL;
import java.util.Map;
import javax.annotation.Nullable;
import org.json.simple.JSONValue;
/**
* An HTTP connection to a service.
@@ -55,23 +56,25 @@ public class ServiceConnection {
private final Service service;
private final boolean useCanary;
private final HttpRequestFactory requestFactory;
private final Gson gson;
@Inject
ServiceConnection(
@Config("useCanary") boolean useCanary,
HttpRequestFactory requestFactory) {
this(Service.BACKEND, requestFactory, useCanary);
@Config("useCanary") boolean useCanary, HttpRequestFactory requestFactory, Gson gson) {
this(Service.BACKEND, requestFactory, useCanary, gson);
}
private ServiceConnection(Service service, HttpRequestFactory requestFactory, boolean useCanary) {
private ServiceConnection(
Service service, HttpRequestFactory requestFactory, boolean useCanary, Gson gson) {
this.service = service;
this.requestFactory = requestFactory;
this.useCanary = useCanary;
this.gson = gson;
}
/** Returns a copy of this connection that talks to a different service endpoint. */
public ServiceConnection withService(Service service, boolean useCanary) {
return new ServiceConnection(service, requestFactory, useCanary);
return new ServiceConnection(service, requestFactory, useCanary, gson);
}
/** Returns the HTML from the connection error stream, if any, otherwise the empty string. */
@@ -99,7 +102,7 @@ public class ServiceConnection {
request.setFollowRedirects(false);
request.setThrowExceptionOnExecuteError(false);
request.setUnsuccessfulResponseHandler(
(request1, response, supportsRetry) -> {
(request1, response, _) -> {
String error = getErrorHtmlAsString(response);
throw new IOException(
String.format(
@@ -137,14 +140,10 @@ public class ServiceConnection {
return internalSend(endpoint, params, MediaType.PLAIN_TEXT_UTF_8, null);
}
@SuppressWarnings("unchecked")
public Map<String, Object> sendJson(String endpoint, Map<String, ?> object) throws IOException {
String response =
sendPostRequest(
endpoint,
ImmutableMap.of(),
JSON_UTF_8,
JSONValue.toJSONString(object).getBytes(UTF_8));
return (Map<String, Object>) JSONValue.parse(response.substring(JSON_SAFETY_PREFIX.length()));
endpoint, ImmutableMap.of(), JSON_UTF_8, gson.toJson(object).getBytes(UTF_8));
return gson.fromJson(response.substring(JSON_SAFETY_PREFIX.length()), new TypeToken<>() {});
}
}
@@ -105,6 +105,7 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
@Override
public void run() {
checkArgument(!tlds.isEmpty(), "Must specify TLDs to refresh");
assertTldsExist(tlds);
checkArgument(batchSize > 0, "Must specify a positive number for batch size");
logger.atInfo().log("Enqueueing DNS refresh tasks for TLDs %s.", tlds);
@@ -105,7 +105,8 @@ public abstract class ConsoleApiAction implements Runnable {
}
}
protected void checkPermission(User user, String registrarId, ConsolePermission permission) {
protected static void checkPermission(
User user, String registrarId, ConsolePermission permission) {
if (!user.getUserRoles().hasPermission(registrarId, permission)) {
throw new ConsolePermissionForbiddenException(
String.format(
@@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.gson.annotations.Expose;
import google.registry.flows.EppException.AuthenticationErrorException;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
@@ -84,6 +85,8 @@ public class ConsoleEppPasswordAction extends ConsoleApiAction {
eppRequestBody.newPassword().equals(eppRequestBody.newPasswordRepeat()),
"New password fields don't match");
checkPermission(user, eppRequestBody.registrarId(), ConsolePermission.CONFIGURE_EPP_CONNECTION);
Registrar registrar;
try {
registrar = registrarAccessor.getRegistrar(eppRequestBody.registrarId());
@@ -62,17 +62,20 @@ public class ConsoleHistoryDataAction extends ConsoleApiAction {
private final Optional<String> consoleUserEmail;
private final String supportEmail;
private final int historyQueryLimit;
@Inject
public ConsoleHistoryDataAction(
ConsoleApiParams consoleApiParams,
@Config("supportEmail") String supportEmail,
@Config("consoleHistoryQueryLimit") int historyQueryLimit,
@Parameter("registrarId") String registrarId,
@Parameter("consoleUserEmail") Optional<String> consoleUserEmail) {
super(consoleApiParams);
this.registrarId = registrarId;
this.consoleUserEmail = consoleUserEmail;
this.supportEmail = supportEmail;
this.historyQueryLimit = historyQueryLimit;
}
@Override
@@ -117,6 +120,7 @@ public class ConsoleHistoryDataAction extends ConsoleApiAction {
.createNativeQuery(SQL_REGISTRAR_HISTORY, ConsoleUpdateHistory.class)
.setParameter("registrarId", registrarId)
.setHint("org.hibernate.fetchSize", 1000)
.setMaxResults(historyQueryLimit)
.getResultList());
List<ConsoleUpdateHistory> formattedHistoryList =
@@ -37,6 +37,7 @@ import com.google.gson.annotations.Expose;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
@@ -149,10 +150,15 @@ public class ConsoleUsersAction extends ConsoleApiAction {
throw new BadRequestException("Total users amount per registrar is limited to 4");
}
User userToAppend = verifyUserExists(this.userData.get().emailAddress);
if (userToAppend.getUserRoles().isAdmin()
|| !userToAppend.getUserRoles().getGlobalRole().equals(GlobalRole.NONE)) {
throw new BadRequestException(
"Cannot append a global administrator or user with a global role to a registrar");
}
updateUserRegistrarRoles(
this.userData.get().emailAddress,
registrarId,
requestRoleToAllowedRoles(this.userData.get().role));
userToAppend, registrarId, requestRoleToAllowedRoles(this.userData.get().role));
sendConfirmationEmail(registrarId, this.userData.get().emailAddress, "Added existing user");
consoleApiParams.response().setStatus(SC_OK);
@@ -164,7 +170,14 @@ public class ConsoleUsersAction extends ConsoleApiAction {
}
String email = this.userData.get().emailAddress;
User updatedUser = updateUserRegistrarRoles(email, registrarId, null);
User userToDelete = verifyUserExists(email);
if (userToDelete.getUserRoles().isAdmin()
|| !userToDelete.getUserRoles().getGlobalRole().equals(GlobalRole.NONE)) {
throw new BadRequestException(
"Cannot delete a global administrator or user with a global role");
}
User updatedUser = updateUserRegistrarRoles(userToDelete, registrarId, null);
// User has no registrars assigned
if (updatedUser.getUserRoles().getRegistrarRoles().isEmpty()) {
@@ -251,7 +264,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
}
updateUserRegistrarRoles(
this.userData.get().emailAddress,
verifyUserExists(this.userData.get().emailAddress),
registrarId,
requestRoleToAllowedRoles(this.userData.get().role));
@@ -297,9 +310,8 @@ public class ConsoleUsersAction extends ConsoleApiAction {
return false;
}
private User updateUserRegistrarRoles(String email, String registrarId, RegistrarRole newRole) {
private User updateUserRegistrarRoles(User user, String registrarId, RegistrarRole newRole) {
Map<String, RegistrarRole> updatedRegistrarRoles;
User user = verifyUserExists(email);
if (newRole == null) {
updatedRegistrarRoles =
user.getUserRoles().getRegistrarRoles().entrySet().stream()
@@ -86,7 +86,7 @@ public class PasswordResetRequestAction extends ConsoleApiAction {
"Must provide registry lock email to reset");
requiredPermission = ConsolePermission.MANAGE_USERS;
destinationEmail = passwordResetRequestData.registryLockEmail;
checkUserExistsWithRegistryLockEmail(destinationEmail);
checkUserExistsWithRegistryLockEmail(destinationEmail, registrarId);
emailSubject = "Registry lock password reset request";
}
default -> throw new IllegalArgumentException("Unknown type " + type);
@@ -121,12 +121,23 @@ public class PasswordResetRequestAction extends ConsoleApiAction {
.sendEmail(EmailMessage.create(emailSubject, body, destinationAddress));
}
static User checkUserExistsWithRegistryLockEmail(String destinationEmail) {
return tm().createQueryComposer(User.class)
.where("registryLockEmailAddress", QueryComposer.Comparator.EQ, destinationEmail)
.first()
.orElseThrow(
() -> new IllegalArgumentException("Unknown user with lock email " + destinationEmail));
static User checkUserExistsWithRegistryLockEmail(String destinationEmail, String registrarId) {
User targetUser =
tm().createQueryComposer(User.class)
.where("registryLockEmailAddress", QueryComposer.Comparator.EQ, destinationEmail)
.first()
.orElseThrow(
() ->
new IllegalArgumentException(
"Unknown user with lock email " + destinationEmail));
// Prevent IDOR: Ensure the resolved user has the right permission
checkArgument(
targetUser.getUserRoles().hasPermission(registrarId, ConsolePermission.REGISTRY_LOCK),
"User %s does not have permission REGISTRY_LOCK on registrar %s",
targetUser.getEmailAddress(),
registrarId);
return targetUser;
}
private String getAdminPocEmail(String registrarId) {
@@ -98,7 +98,9 @@ public class PasswordResetVerifyAction extends ConsoleApiAction {
}
private void handleRegistryLockPasswordReset(PasswordResetRequest request) {
User affectedUser = checkUserExistsWithRegistryLockEmail(request.getDestinationEmail());
User affectedUser =
checkUserExistsWithRegistryLockEmail(
request.getDestinationEmail(), request.getRegistrarId());
tm().put(
affectedUser
.asBuilder()
@@ -119,7 +121,15 @@ public class PasswordResetVerifyAction extends ConsoleApiAction {
ConsolePermission requiredVerifyPermission =
switch (request.getType()) {
case EPP -> ConsolePermission.MANAGE_USERS;
case REGISTRY_LOCK -> ConsolePermission.REGISTRY_LOCK;
case REGISTRY_LOCK -> {
checkArgument(
user.getRegistryLockEmailAddress()
.map(address -> address.equals(request.getDestinationEmail()))
.orElse(false),
"User %s has the wrong registry lock email address",
user.getEmailAddress());
yield ConsolePermission.REGISTRY_LOCK;
}
};
checkPermission(user, request.getRegistrarId(), requiredVerifyPermission);
if (plusHours(request.getRequestTime(), 1).isBefore(tm().getTxTime())) {
@@ -14,6 +14,7 @@
package google.registry.ui.server.console.domains;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
@@ -22,6 +23,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import com.google.gson.annotations.Expose;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
@@ -64,11 +66,13 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction {
private final String registrarId;
private final String bulkDomainAction;
private final Optional<JsonElement> optionalJsonPayload;
private final int bulkDomainActionLimit;
@Inject
public ConsoleBulkDomainAction(
ConsoleApiParams consoleApiParams,
EppController eppController,
@Config("consoleBulkDomainActionLimit") int bulkDomainActionLimit,
@Parameter("registrarId") String registrarId,
@Parameter("bulkDomainAction") String bulkDomainAction,
@OptionalJsonPayload Optional<JsonElement> optionalJsonPayload) {
@@ -77,6 +81,7 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction {
this.registrarId = registrarId;
this.bulkDomainAction = bulkDomainAction;
this.optionalJsonPayload = optionalJsonPayload;
this.bulkDomainActionLimit = bulkDomainActionLimit;
}
@Override
@@ -85,6 +90,13 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction {
optionalJsonPayload.orElseThrow(
() -> new IllegalArgumentException("Bulk action payload must be present"));
BulkDomainList domainList = consoleApiParams.gson().fromJson(jsonPayload, BulkDomainList.class);
checkArgument(
domainList.domainList != null && !domainList.domainList.isEmpty(),
"Domain list cannot be empty");
checkArgument(
domainList.domainList.size() <= bulkDomainActionLimit,
"Cannot process more than %s domains in a single bulk action",
bulkDomainActionLimit);
ConsoleDomainActionType actionType =
ConsoleDomainActionType.parseActionType(bulkDomainAction, jsonPayload);
@@ -115,6 +115,10 @@ public class XmlTransformer {
// Prevent XXE attacks.
xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xmlInputFactory.setXMLResolver(
(publicID, systemID, baseURI, namespace) -> {
throw new XMLStreamException("Entity resolution disabled.");
});
return xmlInputFactory;
}
@@ -47,6 +47,8 @@ Child elements of the <create> command.
minOccurs="0"/>
<element name="registrant" type="eppcom:clIDType"
minOccurs="0"/>
<element name="contact" type="domain:contactType"
minOccurs="0" maxOccurs="unbounded"/>
<element name="authInfo" type="domain:authInfoType"/>
</sequence>
</complexType>
@@ -98,6 +100,22 @@ If attributes, addresses are optional and follow the
structure defined in the host mapping.
-->
<complexType name="contactType">
<simpleContent>
<extension base="eppcom:clIDType">
<attribute name="type" type="domain:contactAttrType"/>
</extension>
</simpleContent>
</complexType>
<simpleType name="contactAttrType">
<restriction base="token">
<enumeration value="admin"/>
<enumeration value="billing"/>
<enumeration value="tech"/>
</restriction>
</simpleType>
<complexType name="authInfoType">
<choice>
<element name="pw" type="eppcom:pwAuthInfoType"/>
@@ -198,6 +216,8 @@ Data elements that can be added or removed.
<sequence>
<element name="ns" type="domain:nsType"
minOccurs="0"/>
<element name="contact" type="domain:contactType"
minOccurs="0" maxOccurs="unbounded"/>
<element name="status" type="domain:statusType"
minOccurs="0" maxOccurs="11"/>
</sequence>
@@ -299,6 +319,8 @@ Child response elements.
minOccurs="0" maxOccurs="11"/>
<element name="registrant" type="eppcom:clIDType"
minOccurs="0"/>
<element name="contact" type="domain:contactType"
minOccurs="0" maxOccurs="unbounded"/>
<element name="ns" type="domain:nsType"
minOccurs="0"/>
<element name="host" type="eppcom:labelType"
@@ -60,6 +60,9 @@
maxOccurs="unbounded"/>
<element name="registrant"
type="eppcom:clIDType" minOccurs="0"/>
<element name="contact"
type="domain:contactType"
minOccurs="0" maxOccurs="unbounded"/>
<element name="ns"
type="domain:nsType" minOccurs="0"/>
<element name="clID"
@@ -19,6 +19,7 @@ import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableO
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistActiveSubordinateHost;
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
import com.google.common.collect.ImmutableList;
@@ -67,7 +68,8 @@ public class SimplifiedJedisClientTest {
@Test
void testClient_roundTrip_host() {
Host host = persistActiveHost("ns1.example.tld");
Domain domain = persistActiveDomain("example.tld");
Host host = persistActiveSubordinateHost("ns1.example.tld", domain);
SimplifiedJedisClient client = createJedisClient();
client.set(new SimplifiedJedisClient.JedisResource<>("repoId1", host));
assertThat(client.get(Host.class, "repoId1")).hasValue(host);
@@ -21,8 +21,8 @@ import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
import static google.registry.testing.DatabaseHelper.persistFeatureFlag;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_INSTANT;
import static google.registry.util.DateTimeUtils.plusDays;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -35,10 +35,8 @@ import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.StorageException;
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.net.MediaType;
import google.registry.gcs.GcsUtils;
import google.registry.model.common.FeatureFlag;
import google.registry.model.domain.Domain;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
@@ -80,7 +78,7 @@ class ExportDomainListsActionTest {
action.gcsUtils = gcsUtils;
action.clock = clock;
action.driveConnection = driveConnection;
persistFeatureFlag(INACTIVE);
persistFeatureFlag(INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS, INACTIVE);
}
private void verifyExportedToDrive(String folderId, String filename, String domains)
@@ -110,7 +108,7 @@ class ExportDomainListsActionTest {
@Test
void test_outputsOnlyActiveDomains_csv() throws Exception {
persistFeatureFlag(ACTIVE);
persistFeatureFlag(INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS, ACTIVE);
persistActiveDomain("onetwo.tld");
persistActiveDomain("rudnitzky.tld");
persistDeletedDomain("mortuary.tld", Instant.parse("2001-03-14T10:11:12Z"));
@@ -144,7 +142,7 @@ class ExportDomainListsActionTest {
@Test
void test_outputsOnlyDomainsOnRealTlds_csv() throws Exception {
persistFeatureFlag(ACTIVE);
persistFeatureFlag(INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS, ACTIVE);
persistActiveDomain("onetwo.tld");
persistActiveDomain("rudnitzky.tld");
persistActiveDomain("wontgo.testtld");
@@ -164,7 +162,7 @@ class ExportDomainListsActionTest {
@Test
void test_outputIncludesDeletionTimes_forPendingDeletes_notRdemption() throws Exception {
persistFeatureFlag(ACTIVE);
persistFeatureFlag(INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS, ACTIVE);
// Domains pending delete (meaning the 5 day period, not counting the 30 day redemption period)
// should include their pending deletion date
persistActiveDomain("active.tld");
@@ -227,7 +225,7 @@ class ExportDomainListsActionTest {
@Test
void test_outputsDomainsFromDifferentTldsToMultipleFiles_csv() throws Exception {
persistFeatureFlag(ACTIVE);
persistFeatureFlag(INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS, ACTIVE);
createTld("tldtwo");
persistResource(Tld.get("tldtwo").asBuilder().setDriveFolderId("hooray").build());
@@ -255,13 +253,4 @@ class ExportDomainListsActionTest {
// tldthree does not have a drive id, so no export to drive is performed.
verifyNoMoreInteractions(driveConnection);
}
private void persistFeatureFlag(FeatureFlag.FeatureStatus status) {
persistResource(
new FeatureFlag()
.asBuilder()
.setFeatureName(INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS)
.setStatusMap(ImmutableSortedMap.of(START_INSTANT, status))
.build());
}
}
@@ -29,6 +29,7 @@ import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_INSTANT;
import static org.mockito.Mockito.verify;
import com.google.gson.reflect.TypeToken;
import google.registry.bsa.persistence.BsaTestingUtils;
import google.registry.model.tld.Tld;
import google.registry.monitoring.whitebox.CheckApiMetric;
@@ -39,10 +40,10 @@ import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.tools.GsonUtils;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import org.json.simple.JSONValue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -83,9 +84,9 @@ class CheckApiActionTest {
.build());
}
@SuppressWarnings("unchecked")
private Map<String, Object> getCheckResponse(String domain) {
CheckApiAction action = new CheckApiAction();
action.gson = GsonUtils.provideGson();
action.domain = domain;
action.response = new FakeResponse();
action.metricBuilder = CheckApiMetric.builder(fakeClock);
@@ -94,7 +95,8 @@ class CheckApiActionTest {
endTime = fakeClock.now();
action.run();
return (Map<String, Object>) JSONValue.parse(((FakeResponse) action.response).getPayload());
return action.gson.fromJson(
((FakeResponse) action.response).getPayload(), new TypeToken<>() {});
}
@Test
@@ -31,6 +31,8 @@ import static org.mockito.Mockito.when;
import com.google.common.base.Splitter;
import com.google.common.testing.TestLogHandler;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import google.registry.flows.EppException.UnimplementedExtensionException;
import google.registry.flows.EppTestComponent.FakeServerTridProvider;
import google.registry.flows.FlowModule.EppExceptionInProviderException;
@@ -43,6 +45,7 @@ import google.registry.monitoring.whitebox.EppMetric;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.tools.GsonUtils;
import google.registry.util.Clock;
import google.registry.xml.ValidationMode;
import java.time.Instant;
@@ -50,7 +53,6 @@ import java.util.List;
import java.util.Map;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.json.simple.JSONValue;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -82,6 +84,7 @@ class EppControllerTest {
@Mock Result result;
private static final Instant START_TIME = Instant.parse("2016-09-01T00:00:00Z");
private static final Gson GSON = GsonUtils.provideGson();
private final Clock clock = new FakeClock(START_TIME);
private final TestLogHandler logHandler = new TestLogHandler();
@@ -110,6 +113,7 @@ class EppControllerTest {
when(result.getCode()).thenReturn(Code.SUCCESS_WITH_NO_MESSAGES);
eppController = new EppController();
eppController.gson = GSON;
eppController.eppMetricBuilder = EppMetric.builderForRequest(clock);
when(flowRunner.run(eppController.eppMetricBuilder)).thenReturn(eppOutput);
eppController.flowComponentBuilder = flowComponentBuilder;
@@ -247,8 +251,7 @@ class EppControllerTest {
assertThat(logRecord.getThrown()).isInstanceOf(IllegalStateException.class);
}
@SuppressWarnings("unchecked")
private static Map<String, Object> parseJsonMap(String json) throws Exception {
return (Map<String, Object>) JSONValue.parseWithException(json);
return GSON.fromJson(json, new TypeToken<>() {});
}
}
@@ -28,6 +28,7 @@ import google.registry.flows.custom.TestCustomLogicFactory;
import google.registry.flows.domain.DomainDeletionTimeCache;
import google.registry.flows.domain.DomainFlowTmchUtils;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.request.Modules.GsonModule;
import google.registry.request.RequestScope;
import google.registry.request.lock.LockHandler;
import google.registry.testing.CloudTasksHelper;
@@ -42,7 +43,8 @@ import jakarta.inject.Singleton;
/** Dagger component for running EPP tests. */
@Singleton
@Component(modules = {ConfigModule.class, EppTestComponent.FakesAndMocksModule.class})
@Component(
modules = {ConfigModule.class, GsonModule.class, EppTestComponent.FakesAndMocksModule.class})
public interface EppTestComponent {
RequestComponent startRequest();
@@ -116,4 +116,13 @@ class EppXmlSanitizerTest {
String sanitizedXml = sanitizeEppXml(inputXml.getBytes(UTF_16LE));
assertThat(sanitizedXml).isEqualTo(inputXml);
}
@Test
void testSanitize_withDtd_returnsBase64() {
String inputXml = "<!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><pw>&xxe;</pw>";
byte[] inputXmlBytes = inputXml.getBytes(UTF_8);
// Since DTDs are disabled, parsing should fail and fallback to base64 encoding of input.
String expectedBase64 = Base64.getMimeEncoder().encodeToString(inputXmlBytes);
assertThat(sanitizeEppXml(inputXmlBytes).trim()).isEqualTo(expectedBase64.trim());
}
}
@@ -22,22 +22,26 @@ import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.testing.TestLogHandler;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppOutput.ResponseOrGreeting;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.tools.GsonUtils;
import google.registry.util.JdkLoggerConfig;
import java.util.Map;
import java.util.Optional;
import org.json.simple.JSONValue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link FlowReporter}. */
class FlowReporterTest {
private static final Gson GSON = GsonUtils.provideGson();
static class TestCommandFlow implements Flow {
@Override
public ResponseOrGreeting run() {
@@ -60,6 +64,7 @@ class FlowReporterTest {
void beforeEach() {
JdkLoggerConfig.getConfig(FlowReporter.class).addHandler(handler);
flowReporter.trid = Trid.create("client-123", "server-456");
flowReporter.gson = GSON;
flowReporter.registrarId = "TheRegistrar";
flowReporter.inputXmlBytes = "<xml/>".getBytes(UTF_8);
flowReporter.flowClass = TestCommandFlow.class;
@@ -205,8 +210,7 @@ class FlowReporterTest {
assertThat(json).containsEntry("tlds", ImmutableList.of());
}
@SuppressWarnings("unchecked")
private static Map<String, Object> parseJsonMap(String json) throws Exception {
return (Map<String, Object>) JSONValue.parseWithException(json);
return GSON.fromJson(json, new TypeToken<>() {});
}
}
@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.testing.TestLogHandler;
import com.google.gson.Gson;
import google.registry.model.EppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.DomainBase;
@@ -34,13 +35,13 @@ import google.registry.model.tmch.ClaimsList;
import google.registry.model.tmch.ClaimsListDao;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.TestCacheExtension;
import google.registry.tools.GsonUtils;
import google.registry.util.JdkLoggerConfig;
import google.registry.util.TypeUtils.TypeInstantiator;
import java.time.Duration;
import java.time.Instant;
import java.util.logging.Level;
import javax.annotation.Nullable;
import org.json.simple.JSONValue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -53,6 +54,8 @@ import org.junit.jupiter.api.extension.RegisterExtension;
public abstract class ResourceFlowTestCase<F extends Flow, R extends EppResource>
extends FlowTestCase<F> {
private static final Gson GSON = GsonUtils.provideGson();
protected final TestLogHandler logHandler = new TestLogHandler();
@RegisterExtension
@@ -108,21 +111,23 @@ public abstract class ResourceFlowTestCase<F extends Flow, R extends EppResource
.that(logHandler)
.hasLogAtLevelWithMessage(Level.INFO, "FLOW-LOG-SIGNATURE-METADATA")
.which()
.contains("\"clientId\":" + JSONValue.toJSONString(registrarId));
.contains("\"clientId\":" + GSON.toJson(registrarId));
}
protected void assertTldsFieldLogged(String... tlds) {
assertAboutLogs().that(logHandler)
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(Level.INFO, "FLOW-LOG-SIGNATURE-METADATA")
.which()
.contains("\"tlds\":" + JSONValue.toJSONString(ImmutableList.copyOf(tlds)));
.contains("\"tlds\":" + GSON.toJson(ImmutableList.copyOf(tlds)));
}
protected void assertIcannReportingActivityFieldLogged(String fieldName) {
assertAboutLogs().that(logHandler)
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(Level.INFO, "FLOW-LOG-SIGNATURE-METADATA")
.which()
.contains("\"icannActivityReportField\":" + JSONValue.toJSONString(fieldName));
.contains("\"icannActivityReportField\":" + GSON.toJson(fieldName));
}
protected void assertLastHistoryContainsResource(EppResource resource) {
@@ -24,6 +24,7 @@ import static google.registry.model.billing.BillingBase.Flag.RESERVED;
import static google.registry.model.billing.BillingBase.Flag.SUNRISE;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.common.FeatureFlag.FeatureName.FORBID_INSECURE_ALGORITHMS_RFC_9904;
import static google.registry.model.domain.fee.Fee.FEE_EXTENSION_URIS;
import static google.registry.model.domain.token.AllocationToken.TokenType.BULK_PRICING;
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
@@ -52,6 +53,7 @@ import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.newHost;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistFeatureFlag;
import static google.registry.testing.DatabaseHelper.persistReservedList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DomainSubject.assertAboutDomains;
@@ -79,7 +81,6 @@ import google.registry.flows.EppException;
import google.registry.flows.EppException.UnimplementedExtensionException;
import google.registry.flows.EppRequestSource;
import google.registry.flows.ExtensionManager.UndeclaredServiceExtensionException;
import google.registry.flows.FlowUtils;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.FlowUtils.UnknownCurrencyEppException;
import google.registry.flows.ResourceFlowTestCase;
@@ -142,6 +143,7 @@ import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTok
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.NonexistentAllocationTokenException;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.flows.exceptions.OnlyToolCanPassMetadataException;
import google.registry.flows.exceptions.ResourceCreateContentionException;
import google.registry.model.billing.BillingBase;
@@ -150,6 +152,7 @@ import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
@@ -794,7 +797,11 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.that(domain)
.hasExactlyDsData(
DomainDsData.create(
12345, 3, 1, base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))
12345,
8,
2,
base16()
.decode("D4B7D520E7BB5F0F67674A0CCEB1E3E0614B93C4F9E99B8383F6A1E4469DA50A"))
.cloneWithDomainRepoId(domain.getRepoId()));
}
@@ -957,6 +964,30 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_secDnsSha1DigestType() throws Exception {
setEppInput("domain_create_dsdata_sha1.xml");
persistHosts();
persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.ACTIVE);
EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_secDnsSha1_flagInactive() throws Exception {
setEppInput("domain_create_dsdata_sha1.xml");
persistHosts();
persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.INACTIVE);
doSuccessfulTest("tld");
Domain domain = reloadResourceByForeignKey();
assertAboutDomains()
.that(domain)
.hasExactlyDsData(
DomainDsData.create(
12345, 8, 1, base16().decode("49FD46E6C4B45C55D4AC49FD46E6C4B45C55D4AC"))
.cloneWithDomainRepoId(domain.getRepoId()));
}
@Test
void testFailure_secDnsInvalidAlgorithm() throws Exception {
setEppInput("domain_create_dsdata_bad_algorithms.xml");
@@ -965,6 +996,34 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_secDnsForbiddenAlgorithm() throws Exception {
setEppInput("domain_create_dsdata_forbidden_algorithm.xml");
persistHosts();
persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.ACTIVE);
EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_secDnsForbiddenAlgorithm_flagInactive() throws Exception {
setEppInput("domain_create_dsdata_forbidden_algorithm.xml");
persistHosts();
persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.INACTIVE);
doSuccessfulTest("tld");
Domain domain = reloadResourceByForeignKey();
assertAboutDomains()
.that(domain)
.hasExactlyDsData(
DomainDsData.create(
12345,
1,
2,
base16()
.decode("D4B7D520E7BB5F0F67674A0CCEB1E3E0614B93C4F9E99B8383F6A1E4469DA50A"))
.cloneWithDomainRepoId(domain.getRepoId()));
}
@Test
void testFailure_wrongExtension() {
setEppInput("domain_create_wrong_extension.xml");
@@ -1878,8 +1937,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
void testFailure_minimumDataset_noRegistrantButSomeOtherContactTypes() throws Exception {
setEppInput("domain_create_other_contact_types.xml");
persistHosts();
EppException thrown =
assertThrows(FlowUtils.GenericXmlSyntaxErrorException.class, this::runFlow);
EppException thrown = assertThrows(ContactsProhibitedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -98,6 +98,8 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
"UNIT", "y");
private static final Pattern OK_PATTERN = Pattern.compile("\"ok\"");
private static final String SHA_256_DIGEST =
"D4B7D520E7BB5F0F67674A0CCEB1E3E0614B93C4F9E99B8383F6A1E4469DA50A";
private Host host1;
private Host host2;
@@ -314,8 +316,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
domain
.asBuilder()
.setDsData(
ImmutableSet.of(
DomainDsData.create(12345, 3, 1, base16().decode("49FD46E6C4B45C55D4AC"))))
ImmutableSet.of(DomainDsData.create(12345, 8, 2, base16().decode(SHA_256_DIGEST))))
.setNameservers(ImmutableSet.of(host1.createVKey(), host3.createVKey()))
.build());
doSuccessfulTest("domain_info_response_dsdata.xml", false);
@@ -519,8 +520,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
"TheRegistrar",
null))
.setDsData(
ImmutableSet.of(
DomainDsData.create(12345, 3, 1, base16().decode("49FD46E6C4B45C55D4AC"))))
ImmutableSet.of(DomainDsData.create(12345, 8, 2, base16().decode(SHA_256_DIGEST))))
.build());
doSuccessfulTest("domain_info_response_dsdata_addperiod.xml", false);
}
@@ -203,6 +203,29 @@ public class DomainPricingLogicTest {
.build());
}
@Test
void testGetDomainCreatePrice_discountPriceAllocationToken_oneYearCreate_moreDiscountYears()
throws EppException {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123_more_discount")
.setTokenType(SINGLE_USE)
.setDomainName("default.example")
.setDiscountPrice(Money.of(USD, 5))
.setDiscountYears(2)
.setRegistrationBehavior(RegistrationBehavior.DEFAULT)
.build());
assertThat(
domainPricingLogic.getCreatePrice(
tld, "default.example", clock.now(), 1, false, false, Optional.of(allocationToken)))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(new BigDecimal("5.00"), CREATE, false))
.build());
}
@Test
void testGetDomainRenewPrice_oneYear_standardDomain_noBilling_isStandardPrice()
throws EppException {
@@ -1093,4 +1116,23 @@ public class DomainPricingLogicTest {
.getRenewCost())
.isEqualTo(Money.of(USD, 5));
}
@Test
void testDomainRenewPrice_specifiedToken_multiYear() throws Exception {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123_multi")
.setTokenType(SINGLE_USE)
.setDomainName("premium.example")
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 5))
.build());
assertThat(
domainPricingLogic
.getRenewPrice(
tld, "premium.example", clock.now(), 5, null, Optional.of(allocationToken))
.getRenewCost())
.isEqualTo(Money.of(USD, 25));
}
}
@@ -19,6 +19,7 @@ import static com.google.common.collect.Sets.union;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ForeignKeyUtils.loadResource;
import static google.registry.model.common.FeatureFlag.FeatureName.FORBID_INSECURE_ALGORITHMS_RFC_9904;
import static google.registry.model.eppcommon.StatusValue.CLIENT_DELETE_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.CLIENT_HOLD;
import static google.registry.model.eppcommon.StatusValue.CLIENT_RENEW_PROHIBITED;
@@ -46,6 +47,7 @@ import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistActiveSubordinateHost;
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
import static google.registry.testing.DatabaseHelper.persistFeatureFlag;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DomainSubject.assertAboutDomains;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
@@ -65,7 +67,6 @@ import google.registry.config.RegistryConfig;
import google.registry.flows.EppException;
import google.registry.flows.EppException.UnimplementedExtensionException;
import google.registry.flows.EppRequestSource;
import google.registry.flows.FlowUtils;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.AddExistingValueException;
@@ -88,12 +89,14 @@ import google.registry.flows.domain.DomainFlowUtils.SecDnsAllUsageException;
import google.registry.flows.domain.DomainFlowUtils.TooManyDsRecordsException;
import google.registry.flows.domain.DomainFlowUtils.TooManyNameserversException;
import google.registry.flows.domain.DomainFlowUtils.UrgentAttributeNotSupportedException;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.flows.exceptions.OnlyToolCanPassMetadataException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
@@ -115,6 +118,9 @@ import org.junit.jupiter.api.Test;
/** Unit tests for {@link DomainUpdateFlow}. */
class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain> {
private static final String SHA_256_DIGEST =
"D4B7D520E7BB5F0F67674A0CCEB1E3E0614B93C4F9E99B8383F6A1E4469DA50A";
private static final DomainDsData SOME_DSDATA =
DomainDsData.create(
1,
@@ -124,9 +130,9 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
private static final ImmutableMap<String, String> OTHER_DSDATA_TEMPLATE_MAP =
ImmutableMap.of(
"KEY_TAG", "12346",
"ALG", "3",
"DIGEST_TYPE", "1",
"DIGEST", "A94A8FE5CCB19BA61C4C0873D391E987982FBBD3");
"ALG", "8",
"DIGEST_TYPE", "2",
"DIGEST", SHA_256_DIGEST);
@BeforeEach
void beforeEach() {
@@ -277,8 +283,10 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
// This EPP adds a new technical contact mak21 that wasn't already present.
setEppInput("domain_update_empty_registrant.xml");
persistReferencedEntities();
persistDomain();
// Fails because the update adds some new contacts, although the registrant has been removed.
assertThrows(FlowUtils.GenericXmlSyntaxErrorException.class, this::persistDomain);
EppException thrown = assertThrows(ContactsProhibitedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
private void modifyDomainToHave13Nameservers() throws Exception {
@@ -453,18 +461,9 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
doSecDnsSuccessfulTest(
"domain_update_dsdata_add.xml",
null,
ImmutableSet.of(
DomainDsData.create(
12346, 3, 1, base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))),
ImmutableSet.of(DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))),
ImmutableMap.of(
"KEY_TAG",
"12346",
"ALG",
"3",
"DIGEST_TYPE",
"1",
"DIGEST",
"A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"),
"KEY_TAG", "12346", "ALG", "8", "DIGEST_TYPE", "2", "DIGEST", SHA_256_DIGEST),
true);
}
@@ -474,18 +473,9 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
"domain_update_dsdata_add.xml",
ImmutableSet.of(SOME_DSDATA),
ImmutableSet.of(
SOME_DSDATA,
DomainDsData.create(
12346, 3, 1, base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))),
SOME_DSDATA, DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))),
ImmutableMap.of(
"KEY_TAG",
"12346",
"ALG",
"3",
"DIGEST_TYPE",
"1",
"DIGEST",
"A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"),
"KEY_TAG", "12346", "ALG", "8", "DIGEST_TYPE", "2", "DIGEST", SHA_256_DIGEST),
true);
}
@@ -660,11 +650,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
union(
commonDsData,
ImmutableSet.of(
DomainDsData.create(
12346,
3,
1,
base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))))),
DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))))),
true);
}
@@ -673,9 +659,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
doSecDnsSuccessfulTest(
"domain_update_dsdata_rem.xml",
ImmutableSet.of(
SOME_DSDATA,
DomainDsData.create(
12346, 3, 1, base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))),
SOME_DSDATA, DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))),
ImmutableSet.of(SOME_DSDATA),
true);
}
@@ -686,9 +670,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
doSecDnsSuccessfulTest(
"domain_update_dsdata_rem_all.xml",
ImmutableSet.of(
SOME_DSDATA,
DomainDsData.create(
12346, 3, 1, base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))),
SOME_DSDATA, DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))),
ImmutableSet.of(),
true);
}
@@ -698,13 +680,9 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
doSecDnsSuccessfulTest(
"domain_update_dsdata_add_rem.xml",
ImmutableSet.of(
SOME_DSDATA,
DomainDsData.create(
12345, 3, 1, base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))),
SOME_DSDATA, DomainDsData.create(12345, 8, 2, base16().decode(SHA_256_DIGEST))),
ImmutableSet.of(
SOME_DSDATA,
DomainDsData.create(
12346, 3, 1, base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))),
SOME_DSDATA, DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))),
true);
}
@@ -727,20 +705,12 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
union(
commonDsData,
ImmutableSet.of(
DomainDsData.create(
12345,
3,
1,
base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))))),
DomainDsData.create(12345, 8, 2, base16().decode(SHA_256_DIGEST))))),
ImmutableSet.copyOf(
union(
commonDsData,
ImmutableSet.of(
DomainDsData.create(
12346,
3,
1,
base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))))),
DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))))),
true);
}
@@ -915,6 +885,41 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_secDnsSha1DigestType() throws Exception {
setEppInput(
"domain_update_dsdata_add.xml",
ImmutableMap.of(
"KEY_TAG", "12346",
"ALG", "3",
"DIGEST_TYPE", "1",
"DIGEST", "A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"));
persistResource(DatabaseHelper.newDomain(getUniqueIdFromCommand()));
persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.ACTIVE);
EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_secDnsSha1_flagInactive() throws Exception {
setEppInput(
"domain_update_dsdata_add.xml",
ImmutableMap.of(
"KEY_TAG", "12346",
"ALG", "3",
"DIGEST_TYPE", "1",
"DIGEST", "A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"));
persistResource(DatabaseHelper.newDomain(getUniqueIdFromCommand()));
persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.INACTIVE);
runFlow();
Domain domain = reloadResourceByForeignKey();
assertAboutDomains()
.that(domain)
.hasExactlyDsData(
DomainDsData.create(
12346, 3, 1, base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3")));
}
@Test
void testFailure_secDnsMultipleInvalidDigestTypes() throws Exception {
setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
@@ -938,7 +943,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setDsData(ImmutableSet.of(DomainDsData.create(1, 2, 1, new byte[] {0, 1, 2})))
.setDsData(ImmutableSet.of(DomainDsData.create(1, 2, 2, new byte[] {0, 1, 2})))
.build());
EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
@@ -955,7 +960,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
.asBuilder()
.setDsData(
ImmutableSet.of(
DomainDsData.create(1, 2, 1, new byte[] {0, 1, 2, 3, 4}),
DomainDsData.create(1, 2, 2, new byte[] {0, 1, 2, 3, 4}),
DomainDsData.create(2, 2, 2, new byte[] {5, 6, 7})))
.build());
EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
@@ -979,6 +984,70 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_secDnsForbiddenAlgorithm() throws Exception {
setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setDsData(
ImmutableSet.of(DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST))))
.build());
persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.ACTIVE);
EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_secDnsForbiddenAlgorithm_flagInactive() throws Exception {
setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setDsData(
ImmutableSet.of(DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST))))
.build());
persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.INACTIVE);
runFlow();
Domain domain = reloadResourceByForeignKey();
assertAboutDomains()
.that(domain)
.hasExactlyDsData(
DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST)),
DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST)));
}
@Test
void testFailure_unrelatedUpdate_existingForbiddenAlgorithm() throws Exception {
persistReferencedEntities();
persistResource(
persistDomain()
.asBuilder()
.setDsData(
ImmutableSet.of(DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST))))
.build());
persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.ACTIVE);
assertAboutEppExceptions()
.that(assertThrows(InvalidDsRecordException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testSuccess_unrelatedUpdate_existingForbiddenAlgorithm_flagInactive() throws Exception {
persistReferencedEntities();
persistResource(
persistDomain()
.asBuilder()
.setDsData(
ImmutableSet.of(DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST))))
.build());
runFlow();
Domain updatedDomain = reloadResourceByForeignKey();
assertAboutDomains()
.that(updatedDomain)
.hasExactlyDsData(DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST)));
}
@Test
void testFailure_secDnsMultipleInvalidAlgorithms() throws Exception {
setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
@@ -48,7 +48,7 @@ import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
import google.registry.flows.host.HostFlowUtils.HostNameTooLongException;
import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException;
import google.registry.flows.host.HostFlowUtils.InvalidHostNameException;
import google.registry.flows.host.HostFlowUtils.LoopbackIpNotValidForHostException;
import google.registry.flows.host.HostFlowUtils.IpAddressNotRoutableException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException;
import google.registry.model.ForeignKeyUtils;
@@ -354,22 +354,62 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, Host> {
}
@Test
void testFailure_localhostInetAddress_ipv4() {
void testFailure_loopbackInetAddress_ipv4() {
createTld("tld");
persistActiveDomain("example.tld");
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v4\">127.0.0.1</host:addr>");
assertAboutEppExceptions()
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_localhostInetAddress_ipv6() {
void testFailure_loopbackInetAddress_ipv6() {
createTld("tld");
persistActiveDomain("example.tld");
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v6\">::1</host:addr>");
assertAboutEppExceptions()
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_linkLocalInetAddress_ipv4() {
createTld("tld");
persistActiveDomain("example.tld");
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v4\">169.254.1.1</host:addr>");
assertAboutEppExceptions()
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_linkLocalInetAddress_ipv6() {
createTld("tld");
persistActiveDomain("example.tld");
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v6\">fe80::1</host:addr>");
assertAboutEppExceptions()
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_privateInetAddress_ipv4() {
createTld("tld");
persistActiveDomain("example.tld");
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v4\">192.168.1.1</host:addr>");
assertAboutEppExceptions()
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_anyLocalInetAddress_ipv4() {
createTld("tld");
persistActiveDomain("example.tld");
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v4\">0.0.0.0</host:addr>");
assertAboutEppExceptions()
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@@ -27,6 +27,7 @@ import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.testing.HostSubject.assertAboutHosts;
import static google.registry.util.DateTimeUtils.minusDays;
import static google.registry.util.DateTimeUtils.plusDays;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableMap;
@@ -42,6 +43,8 @@ import google.registry.flows.host.HostFlowUtils.HostNameNotLowerCaseException;
import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException;
import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
import google.registry.model.domain.Domain;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.Host;
import google.registry.model.reporting.HistoryEntry.Type;
@@ -311,6 +314,30 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, Host> {
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_failfastWhenLinkedToDomainInRedemption() throws Exception {
createTld("tld");
Host host = persistActiveHost("ns1.example.tld");
Domain domain = persistResource(DatabaseHelper.newDomain("example.tld"));
persistResource(
domain
.asBuilder()
.setNameservers(ImmutableSet.of(host.createVKey()))
.setDeletionTime(plusDays(clock.now(), 35))
.addGracePeriod(
GracePeriod.create(
GracePeriodStatus.REDEMPTION,
domain.getRepoId(),
plusDays(clock.now(), 1),
"TheRegistrar",
null))
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.build());
EppException thrown = assertThrows(ResourceToDeleteIsReferencedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_nonLowerCaseHostname() {
setEppInput("host_delete.xml", ImmutableMap.of("HOSTNAME", "NS1.EXAMPLE.NET"));
@@ -65,7 +65,7 @@ import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException;
import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
import google.registry.flows.host.HostFlowUtils.HostNameTooLongException;
import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException;
import google.registry.flows.host.HostFlowUtils.LoopbackIpNotValidForHostException;
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.HostUpdateFlow.CannotAddIpToExternalHostException;
@@ -1391,24 +1391,68 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, Host> {
}
@Test
void testFailure_localhostInetAddress_ipv4() throws Exception {
void testFailure_loopbackInetAddress_ipv4() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
setEppHostUpdateInput(
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v4\">127.0.0.1</host:addr>", null);
assertAboutEppExceptions()
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_localhostInetAddress_ipv6() throws Exception {
void testFailure_loopbackInetAddress_ipv6() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
setEppHostUpdateInput(
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v6\">::1</host:addr>", null);
assertAboutEppExceptions()
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_linkLocalInetAddress_ipv4() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
setEppHostUpdateInput(
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v4\">169.254.1.1</host:addr>", null);
assertAboutEppExceptions()
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_linkLocalInetAddress_ipv6() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
setEppHostUpdateInput(
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v6\">fe80::1</host:addr>", null);
assertAboutEppExceptions()
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_privateInetAddress_ipv4() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
setEppHostUpdateInput(
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v4\">192.168.1.1</host:addr>", null);
assertAboutEppExceptions()
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@Test
void testFailure_anyLocalInetAddress_ipv4() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
setEppHostUpdateInput(
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v4\">0.0.0.0</host:addr>", null);
assertAboutEppExceptions()
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
.marshalsToXml();
}
@@ -0,0 +1,96 @@
// Copyright 2026 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.flows.picker;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.Flow;
import google.registry.flows.domain.DomainCheckFlow;
import google.registry.flows.domain.DomainCreateFlow;
import google.registry.model.domain.DomainCommand;
import google.registry.model.eppcommon.EppXmlTransformer;
import google.registry.model.eppinput.EppInput;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link FlowPicker}. */
class FlowPickerTest {
@Test
void testGetFlowClass_matchingWrapperAndCommand_returnsFlow() throws Exception {
EppInput checkEppInput = EppInput.create(EppInput.Check.create(new DomainCommand.Check()));
Class<? extends Flow> checkFlow = FlowPicker.getFlowClass(checkEppInput);
assertThat(checkFlow).isEqualTo(DomainCheckFlow.class);
EppInput createEppInput = EppInput.create(EppInput.Create.create(new DomainCommand.Create()));
Class<? extends Flow> createFlow = FlowPicker.getFlowClass(createEppInput);
assertThat(createFlow).isEqualTo(DomainCreateFlow.class);
}
@Test
void testGetFlowClass_mismatchedWrapperAndCommand_throwsSyntaxErrorException() {
EppInput mismatchedEppInput1 =
EppInput.create(EppInput.Check.create(new DomainCommand.Create()));
assertThat(
assertThrows(
FlowPicker.MismatchedCommandException.class,
() -> FlowPicker.getFlowClass(mismatchedEppInput1)))
.hasMessageThat()
.isEqualTo("EPP command wrapper <check> does not match resource command <create>");
EppInput mismatchedEppInput2 =
EppInput.create(EppInput.Create.create(new DomainCommand.Check()));
assertThat(
assertThrows(
FlowPicker.MismatchedCommandException.class,
() -> FlowPicker.getFlowClass(mismatchedEppInput2)))
.hasMessageThat()
.contains("EPP command wrapper <create> does not match resource command <check>");
}
@Test
void testGetFlowClass_mismatchedXml_throwsMismatchedCommandException() throws Exception {
String mismatchedXml =
"""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<check>
<domain:create xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.com</domain:name>
<domain:authInfo>
<domain:pw>fooBAR123</domain:pw>
</domain:authInfo>
</domain:create>
</check>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
""";
// Verify JAXB successfully unmarshals the mismatched XML without throwing any errors
EppInput eppInput = EppXmlTransformer.unmarshal(EppInput.class, mismatchedXml.getBytes(UTF_8));
assertThat(eppInput).isNotNull();
// Verify that FlowPicker intercepts the unmarshalled input and blocks it
assertThat(
assertThrows(
FlowPicker.MismatchedCommandException.class,
() -> FlowPicker.getFlowClass(eppInput)))
.hasMessageThat()
.contains("EPP command wrapper <check> does not match resource command <create>");
}
}
@@ -20,13 +20,12 @@ import static google.registry.model.common.FeatureFlag.FeatureName.PROHIBIT_CONT
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.deleteResource;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistFeatureFlag;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.util.DateTimeUtils.START_INSTANT;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.flows.EppException;
import google.registry.flows.EppException.UnimplementedExtensionException;
import google.registry.flows.EppException.UnimplementedObjectServiceException;
@@ -39,7 +38,6 @@ import google.registry.flows.session.LoginFlow.BadRegistrarIdException;
import google.registry.flows.session.LoginFlow.RegistrarAccountNotActiveException;
import google.registry.flows.session.LoginFlow.TooManyFailedLoginsException;
import google.registry.flows.session.LoginFlow.UnsupportedLanguageException;
import google.registry.model.common.FeatureFlag;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.registrar.Registrar;
@@ -61,11 +59,7 @@ public abstract class LoginFlowTestCase extends FlowTestCase<LoginFlow> {
sessionMetadata.setRegistrarId(null); // Don't implicitly log in (all other flows need to).
registrar = loadRegistrar("NewRegistrar");
registrarBuilder = registrar.asBuilder();
persistResource(
new FeatureFlag.Builder()
.setFeatureName(PROHIBIT_CONTACT_OBJECTS_ON_LOGIN)
.setStatusMap(ImmutableSortedMap.of(START_INSTANT, FeatureStatus.ACTIVE))
.build());
persistFeatureFlag(PROHIBIT_CONTACT_OBJECTS_ON_LOGIN, FeatureStatus.ACTIVE);
}
// Can't inline this since it may be overridden in subclasses.
@@ -17,8 +17,8 @@ package google.registry.model.domain;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.flows.FlowUtils;
import google.registry.flows.domain.DomainFlowUtils.RegistrantProhibitedException;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.model.ResourceCommandTestCase;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppinput.EppInput.ResourceCommandWrapper;
@@ -88,9 +88,10 @@ class DomainCommandTest extends ResourceCommandTestCase {
void testCreate_cloneAndLinkReferences_failsWithContacts() throws Exception {
persistActiveHost("ns1.example.net");
persistActiveHost("ns2.example.net");
DomainCommand.Create create =
(DomainCommand.Create) loadEppResourceCommand("domain_create_with_contacts.xml");
assertThrows(
FlowUtils.GenericXmlSyntaxErrorException.class,
() -> loadEppResourceCommand("domain_create_with_contacts.xml"));
ContactsProhibitedException.class, () -> create.cloneAndLinkReferences(fakeClock.now()));
}
@Test
@@ -138,9 +139,10 @@ class DomainCommandTest extends ResourceCommandTestCase {
void testUpdate_cloneAndLinkReferences_failsWithContacts() throws Exception {
persistActiveHost("ns1.example.com");
persistActiveHost("ns2.example.com");
DomainCommand.Update update =
(DomainCommand.Update) loadEppResourceCommand("domain_update_with_contacts.xml");
assertThrows(
FlowUtils.GenericXmlSyntaxErrorException.class,
() -> loadEppResourceCommand("domain_update_with_contacts.xml"));
ContactsProhibitedException.class, () -> update.cloneAndLinkReferences(fakeClock.now()));
}
@Test
@@ -283,15 +283,15 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
private JsonObject generateExpectedJsonForTwoDomainsNsReply() {
return jsonFileBuilder()
.addDomain("cat.example", "F-EXAMPLE")
.addDomain("cat.lol", "6-LOL")
.addDomain("cat.example", domainCatExample.getRepoId())
.addDomain("cat.lol", domainCatLol.getRepoId())
.load("rdap_domains_two.json");
}
private JsonObject generateExpectedJsonForTwoDomainsCatStarReplySql() {
return jsonFileBuilder()
.addDomain("cat.lol", "6-LOL")
.addDomain("cat2.lol", "B-LOL")
.addDomain("cat.lol", domainCatLol.getRepoId())
.addDomain("cat2.lol", domainCatLol2.getRepoId())
.load("rdap_domains_two.json");
}
@@ -306,7 +306,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
minusMonths(clock.now(), 6)));
}
private void createManyDomainsAndHosts(
private ImmutableList<Domain> createManyDomainsAndHosts(
int numActiveDomains, int numTotalDomainsPerActiveDomain, int numHosts) {
ImmutableSet.Builder<VKey<Host>> hostKeysBuilder = new ImmutableSet.Builder<>();
ImmutableSet.Builder<String> subordinateHostnamesBuilder = new ImmutableSet.Builder<>();
@@ -340,7 +340,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
}
domainsBuilder.add(builder.build());
}
persistResources(domainsBuilder.build());
return persistResources(domainsBuilder.build());
}
private void checkNumberOfDomainsInResult(JsonObject obj, int expected) {
@@ -353,10 +353,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
requestType,
queryString,
jsonFileBuilder()
.addDomain("cat.lol", "6-LOL")
.addDomain("cat.lol", domainCatLol.getRepoId())
.addRegistrar("Yes Virginia <script>")
.addNameserver("ns1.cat.lol", "2-ROID")
.addNameserver("ns2.cat.lol", "4-ROID")
.addNameserver("ns1.cat.lol", hostNs1CatLol.getRepoId())
.addNameserver("ns2.cat.lol", hostNs2CatLol.getRepoId())
.setNextQuery(queryString)
.load(filename));
}
@@ -367,10 +367,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
requestType,
queryString,
jsonFileBuilder()
.addDomain("cat2.lol", "B-LOL")
.addDomain("cat2.lol", domainCatLol2.getRepoId())
.addRegistrar("Yes Virginia <script>")
.addNameserver("ns1.cat.example", "7-ROID")
.addNameserver("ns2.dog.lol", "9-ROID")
.addNameserver("ns1.cat.example", hostNameToHostMap.get("ns1.cat.example").getRepoId())
.addNameserver("ns2.dog.lol", hostNameToHostMap.get("ns2.dog.lol").getRepoId())
.setNextQuery(queryString)
.load(filename));
}
@@ -679,10 +679,11 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NAME,
"cat.example",
jsonFileBuilder()
.addDomain("cat.example", "F-EXAMPLE")
.addDomain("cat.example", domainCatExample.getRepoId())
.addRegistrar("St. John Chrysostom")
.addNameserver("ns1.cat.lol", "2-ROID")
.addNameserver("ns2.external.tld", "D-ROID")
.addNameserver("ns1.cat.lol", hostNs1CatLol.getRepoId())
.addNameserver(
"ns2.external.tld", hostNameToHostMap.get("ns2.external.tld").getRepoId())
.load("rdap_domain.json"));
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(1L));
}
@@ -693,10 +694,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NAME,
"cat.みんな",
jsonFileBuilder()
.addDomain("cat.みんな", "15-Q9JYB4C")
.addDomain("cat.みんな", domainIdn.getRepoId())
.addRegistrar("みんな")
.addNameserver("ns1.cat.みんな", "11-ROID")
.addNameserver("ns2.cat.みんな", "13-ROID")
.addNameserver("ns1.cat.みんな", hostNameToHostMap.get("ns1.cat.xn--q9jyb4c").getRepoId())
.addNameserver("ns2.cat.みんな", hostNameToHostMap.get("ns2.cat.xn--q9jyb4c").getRepoId())
.load("rdap_domain_unicode_with_unicode_nameservers.json"));
// The unicode gets translated to ASCII before getting parsed into a search pattern.
metricPrefixLength = 15;
@@ -709,10 +710,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NAME,
"cat.xn--q9jyb4c",
jsonFileBuilder()
.addDomain("cat.みんな", "15-Q9JYB4C")
.addDomain("cat.みんな", domainIdn.getRepoId())
.addRegistrar("みんな")
.addNameserver("ns1.cat.みんな", "11-ROID")
.addNameserver("ns2.cat.みんな", "13-ROID")
.addNameserver("ns1.cat.みんな", hostNameToHostMap.get("ns1.cat.xn--q9jyb4c").getRepoId())
.addNameserver("ns2.cat.みんな", hostNameToHostMap.get("ns2.cat.xn--q9jyb4c").getRepoId())
.load("rdap_domain_unicode_with_unicode_nameservers.json"));
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(1L));
}
@@ -723,10 +724,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NAME,
"cat.1.test",
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1TEST")
.addDomain("cat.1.test", domainMultipart.getRepoId())
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "17-ROID")
.addNameserver("ns2.cat.2.test", "19-ROID")
.addNameserver("ns1.cat.1.test", hostNameToHostMap.get("ns1.cat.1.test").getRepoId())
.addNameserver("ns2.cat.2.test", hostNameToHostMap.get("ns2.cat.2.test").getRepoId())
.load("rdap_domain.json"));
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(1L));
}
@@ -737,10 +738,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NAME,
"ca*.1.test",
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1TEST")
.addDomain("cat.1.test", domainMultipart.getRepoId())
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "17-ROID")
.addNameserver("ns2.cat.2.test", "19-ROID")
.addNameserver("ns1.cat.1.test", hostNameToHostMap.get("ns1.cat.1.test").getRepoId())
.addNameserver("ns2.cat.2.test", hostNameToHostMap.get("ns2.cat.2.test").getRepoId())
.load("rdap_domain.json"));
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(1L));
}
@@ -813,10 +814,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.that(generateActualJson(RequestType.NAME, "cat.*"))
.isEqualTo(
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1TEST")
.addDomain("cat.example", "F-EXAMPLE")
.addDomain("cat.lol", "6-LOL")
.addDomain("cat.みんな", "15-Q9JYB4C")
.addDomain("cat.1.test", domainMultipart.getRepoId())
.addDomain("cat.example", domainCatExample.getRepoId())
.addDomain("cat.lol", domainCatLol.getRepoId())
.addDomain("cat.みんな", domainIdn.getRepoId())
.load("rdap_domains_four_with_one_unicode.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(4L));
@@ -851,10 +852,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.that(generateActualJson(RequestType.NAME, "cat*"))
.isEqualTo(
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1TEST")
.addDomain("cat.example", "F-EXAMPLE")
.addDomain("cat.lol", "6-LOL")
.addDomain("cat.みんな", "15-Q9JYB4C")
.addDomain("cat.1.test", domainMultipart.getRepoId())
.addDomain("cat.example", domainCatExample.getRepoId())
.addDomain("cat.lol", domainCatLol.getRepoId())
.addDomain("cat.みんな", domainIdn.getRepoId())
.setNextQuery("name=cat*&cursor=Y2F0LnhuLS1xOWp5YjRj")
.load("rdap_domains_four_with_one_unicode_truncated.json"));
assertThat(response.getStatus()).isEqualTo(200);
@@ -970,15 +971,15 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
// This is not exactly desired behavior, but expected: There are enough domains to fill a full
// result set, but there are so many deleted domains that we run out of patience before we work
// our way through all of them.
createManyDomainsAndHosts(4, 50, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(4, 50, 2);
rememberWildcardType("domain*.lol");
assertAboutJson()
.that(generateActualJson(RequestType.NAME, "domain*.lol"))
.isEqualTo(
jsonFileBuilder()
.addDomain("domain100.lol", "8E-LOL")
.addDomain("domain150.lol", "5C-LOL")
.addDomain("domain200.lol", "2A-LOL")
.addDomain("domain100.lol", domains.get(100).getRepoId())
.addDomain("domain150.lol", domains.get(50).getRepoId())
.addDomain("domain200.lol", domains.get(0).getRepoId())
.addDomain("domainunused.lol", "unused-LOL")
.load("rdap_incomplete_domain_result_set.json"));
assertThat(response.getStatus()).isEqualTo(200);
@@ -990,28 +991,28 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
@Test
void testDomainMatch_nontruncatedResultsSet() {
createManyDomainsAndHosts(4, 1, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(4, 1, 2);
runSuccessfulTestWithFourDomains(
RequestType.NAME,
"domain*.lol",
"2D-LOL",
"2C-LOL",
"2B-LOL",
"2A-LOL",
domains.get(3).getRepoId(),
domains.get(2).getRepoId(),
domains.get(1).getRepoId(),
domains.get(0).getRepoId(),
"rdap_nontruncated_domains.json");
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(4L));
}
@Test
void testDomainMatch_truncatedResultsSet() {
createManyDomainsAndHosts(5, 1, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(5, 1, 2);
runSuccessfulTestWithFourDomains(
RequestType.NAME,
"domain*.lol",
"2E-LOL",
"2D-LOL",
"2C-LOL",
"2B-LOL",
domains.get(4).getRepoId(),
domains.get(3).getRepoId(),
domains.get(2).getRepoId(),
domains.get(1).getRepoId(),
"name=domain*.lol&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json");
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED);
@@ -1019,16 +1020,16 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
@Test
void testDomainMatch_tldSearchOrderedProperly_sql() {
createManyDomainsAndHosts(4, 1, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(4, 1, 2);
rememberWildcardType("*.lol");
assertAboutJson()
.that(generateActualJson(RequestType.NAME, "*.lol"))
.isEqualTo(
jsonFileBuilder()
.addDomain("cat.lol", "6-LOL")
.addDomain("cat2.lol", "B-LOL")
.addDomain("domain1.lol", "2D-LOL")
.addDomain("domain2.lol", "2C-LOL")
.addDomain("cat.lol", domainCatLol.getRepoId())
.addDomain("cat2.lol", domainCatLol2.getRepoId())
.addDomain("domain1.lol", domains.get(3).getRepoId())
.addDomain("domain2.lol", domains.get(2).getRepoId())
.setNextQuery("name=*.lol&cursor=ZG9tYWluMi5sb2w%3D")
.load("rdap_domains_four_truncated.json"));
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED);
@@ -1038,14 +1039,14 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
void testDomainMatch_reallyTruncatedResultsSet() {
// Don't use 10 or more domains for this test, because domain10.lol will come before
// domain2.lol, and you'll get the wrong domains in the result set.
createManyDomainsAndHosts(9, 1, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(9, 1, 2);
runSuccessfulTestWithFourDomains(
RequestType.NAME,
"domain*.lol",
"32-LOL",
"31-LOL",
"30-LOL",
"2F-LOL",
domains.get(8).getRepoId(),
domains.get(7).getRepoId(),
domains.get(6).getRepoId(),
domains.get(5).getRepoId(),
"name=domain*.lol&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json");
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED);
@@ -1053,16 +1054,16 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
@Test
void testDomainMatch_truncatedResultsAfterMultipleChunks_sql() {
createManyDomainsAndHosts(5, 6, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(5, 6, 2);
rememberWildcardType("domain*.lol");
assertAboutJson()
.that(generateActualJson(RequestType.NAME, "domain*.lol"))
.isEqualTo(
jsonFileBuilder()
.addDomain("domain12.lol", "3C-LOL")
.addDomain("domain18.lol", "36-LOL")
.addDomain("domain24.lol", "30-LOL")
.addDomain("domain30.lol", "2A-LOL")
.addDomain("domain12.lol", domains.get(18).getRepoId())
.addDomain("domain18.lol", domains.get(12).getRepoId())
.addDomain("domain24.lol", domains.get(6).getRepoId())
.addDomain("domain30.lol", domains.get(0).getRepoId())
.setNextQuery("name=domain*.lol&cursor=ZG9tYWluMzAubG9s")
.load("rdap_domains_four_truncated.json"));
assertThat(response.getStatus()).isEqualTo(200);
@@ -1261,10 +1262,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NS_LDH_NAME,
"ns1.cat.xn--q9jyb4c",
jsonFileBuilder()
.addDomain("cat.みんな", "15-Q9JYB4C")
.addDomain("cat.みんな", domainIdn.getRepoId())
.addRegistrar("みんな")
.addNameserver("ns1.cat.みんな", "11-ROID")
.addNameserver("ns2.cat.みんな", "13-ROID")
.addNameserver("ns1.cat.みんな", hostNameToHostMap.get("ns1.cat.xn--q9jyb4c").getRepoId())
.addNameserver("ns2.cat.みんな", hostNameToHostMap.get("ns2.cat.xn--q9jyb4c").getRepoId())
.load("rdap_domain_unicode_with_unicode_nameservers.json"));
verifyMetrics(SearchType.BY_NAMESERVER_NAME, 1, 1);
}
@@ -1275,10 +1276,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NS_LDH_NAME,
"ns1.cat.1.test",
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1TEST")
.addDomain("cat.1.test", domainMultipart.getRepoId())
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "17-ROID")
.addNameserver("ns2.cat.2.test", "19-ROID")
.addNameserver("ns1.cat.1.test", hostNameToHostMap.get("ns1.cat.1.test").getRepoId())
.addNameserver("ns2.cat.2.test", hostNameToHostMap.get("ns2.cat.2.test").getRepoId())
.load("rdap_domain.json"));
verifyMetrics(SearchType.BY_NAMESERVER_NAME, 1, 1);
}
@@ -1289,10 +1290,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
RequestType.NS_LDH_NAME,
"ns*.cat.1.test",
jsonFileBuilder()
.addDomain("cat.1.test", "1B-1TEST")
.addDomain("cat.1.test", domainMultipart.getRepoId())
.addRegistrar("1.test")
.addNameserver("ns1.cat.1.test", "17-ROID")
.addNameserver("ns2.cat.2.test", "19-ROID")
.addNameserver("ns1.cat.1.test", hostNameToHostMap.get("ns1.cat.1.test").getRepoId())
.addNameserver("ns2.cat.2.test", hostNameToHostMap.get("ns2.cat.2.test").getRepoId())
.load("rdap_domain.json"));
verifyMetrics(SearchType.BY_NAMESERVER_NAME, 1, 1);
}
@@ -1430,28 +1431,28 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
@Test
void testNameserverMatch_nontruncatedResultsSet() {
createManyDomainsAndHosts(4, 1, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(4, 1, 2);
runSuccessfulTestWithFourDomains(
RequestType.NS_LDH_NAME,
"ns1.domain1.lol",
"2D-LOL",
"2C-LOL",
"2B-LOL",
"2A-LOL",
domains.get(3).getRepoId(),
domains.get(2).getRepoId(),
domains.get(1).getRepoId(),
domains.get(0).getRepoId(),
"rdap_nontruncated_domains.json");
verifyMetrics(SearchType.BY_NAMESERVER_NAME, Optional.of(4L), Optional.of(1L));
}
@Test
void testNameserverMatch_truncatedResultsSet() {
createManyDomainsAndHosts(5, 1, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(5, 1, 2);
runSuccessfulTestWithFourDomains(
RequestType.NS_LDH_NAME,
"ns1.domain1.lol",
"2E-LOL",
"2D-LOL",
"2C-LOL",
"2B-LOL",
domains.get(4).getRepoId(),
domains.get(3).getRepoId(),
domains.get(2).getRepoId(),
domains.get(1).getRepoId(),
"nsLdhName=ns1.domain1.lol&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json");
verifyMetrics(
@@ -1463,14 +1464,14 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
@Test
void testNameserverMatch_reallyTruncatedResultsSet() {
createManyDomainsAndHosts(9, 1, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(9, 1, 2);
runSuccessfulTestWithFourDomains(
RequestType.NS_LDH_NAME,
"ns1.domain1.lol",
"32-LOL",
"31-LOL",
"30-LOL",
"2F-LOL",
domains.get(8).getRepoId(),
domains.get(7).getRepoId(),
domains.get(6).getRepoId(),
domains.get(5).getRepoId(),
"nsLdhName=ns1.domain1.lol&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json");
verifyMetrics(
@@ -1484,16 +1485,16 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
void testNameserverMatch_duplicatesNotTruncated() {
// 36 nameservers for each of 4 domains; these should translate into two fetches, which should
// not trigger the truncation warning because all the domains will be duplicates.
createManyDomainsAndHosts(4, 1, 36);
ImmutableList<Domain> domains = createManyDomainsAndHosts(4, 1, 36);
rememberWildcardType("ns*.domain1.lol");
assertAboutJson()
.that(generateActualJson(RequestType.NS_LDH_NAME, "ns*.domain1.lol"))
.isEqualTo(
jsonFileBuilder()
.addDomain("domain1.lol", "71-LOL")
.addDomain("domain2.lol", "70-LOL")
.addDomain("domain3.lol", "6F-LOL")
.addDomain("domain4.lol", "6E-LOL")
.addDomain("domain1.lol", domains.get(3).getRepoId())
.addDomain("domain2.lol", domains.get(2).getRepoId())
.addDomain("domain3.lol", domains.get(1).getRepoId())
.addDomain("domain4.lol", domains.get(0).getRepoId())
.load("rdap_nontruncated_domains.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(SearchType.BY_NAMESERVER_NAME, Optional.of(4L), Optional.of(36L));
@@ -1501,14 +1502,14 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
@Test
void testNameserverMatch_incompleteResultsSet() {
createManyDomainsAndHosts(2, 1, 41);
ImmutableList<Domain> domains = createManyDomainsAndHosts(2, 1, 41);
rememberWildcardType("ns*.domain1.lol");
assertAboutJson()
.that(generateActualJson(RequestType.NS_LDH_NAME, "ns*.domain1.lol"))
.isEqualTo(
jsonFileBuilder()
.addDomain("domain1.lol", "79-LOL")
.addDomain("domain2.lol", "78-LOL")
.addDomain("domain1.lol", domains.get(1).getRepoId())
.addDomain("domain2.lol", domains.get(0).getRepoId())
.load("rdap_incomplete_domains.json"));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(
@@ -1641,10 +1642,10 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.isEqualTo(
wrapInSearchReply(
jsonFileBuilder()
.addDomain("cat.lol", "6-LOL")
.addDomain("cat.lol", domainCatLol.getRepoId())
.addRegistrar("Yes Virginia <script>")
.addNameserver("ns1.cat.lol", "2-ROID")
.addNameserver("ns2.cat.lol", "4-ROID")
.addNameserver("ns1.cat.lol", hostNs1CatLol.getRepoId())
.addNameserver("ns2.cat.lol", hostNs2CatLol.getRepoId())
.load("rdap_domain.json")));
assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(SearchType.BY_NAMESERVER_ADDRESS, 1, 1);
@@ -1667,28 +1668,28 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
@Test
void testAddressMatch_nontruncatedResultsSet() {
createManyDomainsAndHosts(4, 1, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(4, 1, 2);
runSuccessfulTestWithFourDomains(
RequestType.NS_IP,
"5.5.5.1",
"2D-LOL",
"2C-LOL",
"2B-LOL",
"2A-LOL",
domains.get(3).getRepoId(),
domains.get(2).getRepoId(),
domains.get(1).getRepoId(),
domains.get(0).getRepoId(),
"rdap_nontruncated_domains.json");
verifyMetrics(SearchType.BY_NAMESERVER_ADDRESS, 4, 1);
}
@Test
void testAddressMatch_truncatedResultsSet() {
createManyDomainsAndHosts(5, 1, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(5, 1, 2);
runSuccessfulTestWithFourDomains(
RequestType.NS_IP,
"5.5.5.1",
"2E-LOL",
"2D-LOL",
"2C-LOL",
"2B-LOL",
domains.get(4).getRepoId(),
domains.get(3).getRepoId(),
domains.get(2).getRepoId(),
domains.get(1).getRepoId(),
"nsIp=5.5.5.1&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json");
verifyMetrics(
@@ -1700,14 +1701,14 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
@Test
void testAddressMatch_reallyTruncatedResultsSet() {
createManyDomainsAndHosts(9, 1, 2);
ImmutableList<Domain> domains = createManyDomainsAndHosts(9, 1, 2);
runSuccessfulTestWithFourDomains(
RequestType.NS_IP,
"5.5.5.1",
"32-LOL",
"31-LOL",
"30-LOL",
"2F-LOL",
domains.get(8).getRepoId(),
domains.get(7).getRepoId(),
domains.get(6).getRepoId(),
domains.get(5).getRepoId(),
"nsIp=5.5.5.1&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json");
verifyMetrics(
@@ -37,7 +37,7 @@ import google.registry.testing.FakeUrlConnectionService;
import google.registry.util.UrlConnectionException;
import java.io.ByteArrayInputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -88,7 +88,8 @@ public final class UpdateRegistrarRdapBaseUrlsActionTest {
private void assertCorrectRequestSent() throws Exception {
assertThat(urlConnectionService.getConnectedUrls())
.containsExactly(
new URL("https://www.iana.org/assignments/registrar-ids/registrar-ids-1.csv"));
URI.create("https://www.iana.org/assignments/registrar-ids/registrar-ids-1.csv")
.toURL());
verify(connection).setRequestProperty("Accept-Encoding", "gzip");
}
@@ -248,4 +248,38 @@ class CopyDetailReportsActionTest {
action.run();
verifyNoInteractions(driveConnection);
}
@Test
void testSuccess_registrarIdWithUnderscores() throws IOException {
persistResource(
loadRegistrar("TheRegistrar")
.asBuilder()
.setRegistrarId("The_Registrar")
.setDriveFolderId("0B-99999")
.build());
gcsUtils.createFromBytes(
BlobId.of("test-bucket", "results/invoice_details_2017-10_The_Registrar_test.csv"),
"hello,world\n1,2".getBytes(UTF_8));
action.run();
verify(driveConnection)
.createOrUpdateFile(
"invoice_details_2017-10_The_Registrar_test.csv",
MediaType.CSV_UTF_8,
"0B-99999",
"hello,world\n1,2".getBytes(UTF_8));
assertThat(response.getStatus()).isEqualTo(SC_OK);
}
@Test
void testSuccess_invalidFilenamePattern_skipped() throws IOException {
gcsUtils.createFromBytes(
BlobId.of("test-bucket", "results/invoice_details_2017-10.csv"),
"hello,world\n1,2".getBytes(UTF_8));
action.run();
verifyNoInteractions(driveConnection);
assertThat(response.getStatus()).isEqualTo(SC_OK);
}
}
@@ -34,7 +34,7 @@ import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationT
import google.registry.testing.FakeUrlConnectionService;
import java.io.ByteArrayOutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URI;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -74,7 +74,7 @@ class IcannHttpReporterTest {
assertThat(reporter.send(FAKE_PAYLOAD, "test-transactions-201706.csv")).isTrue();
assertThat(urlConnectionService.getConnectedUrls())
.containsExactly(new URL("https://fake-transactions.url/test/2017-06"));
.containsExactly(URI.create("https://fake-transactions.url/test/2017-06").toURL());
String userPass = "test_ry:fakePass";
String expectedAuth =
String.format("Basic %s", BaseEncoding.base64().encode(StringUtils.getBytesUtf8(userPass)));
@@ -88,7 +88,7 @@ class IcannHttpReporterTest {
assertThat(reporter.send(FAKE_PAYLOAD, "xn--abc123-transactions-201706.csv")).isTrue();
assertThat(urlConnectionService.getConnectedUrls())
.containsExactly(new URL("https://fake-transactions.url/xn--abc123/2017-06"));
.containsExactly(URI.create("https://fake-transactions.url/xn--abc123/2017-06").toURL());
String userPass = "xn--abc123_ry:fakePass";
String expectedAuth =
String.format("Basic %s", BaseEncoding.base64().encode(StringUtils.getBytesUtf8(userPass)));
@@ -18,17 +18,21 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.request.JsonResponse.JSON_SAFETY_PREFIX;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import google.registry.testing.FakeResponse;
import google.registry.tools.GsonUtils;
import java.time.Instant;
import java.util.Map;
import org.json.simple.JSONValue;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link JsonResponse}. */
class JsonResponseTest {
private static final Gson GSON = GsonUtils.provideGson();
private FakeResponse fakeResponse = new FakeResponse();
private JsonResponse jsonResponse = new JsonResponse(fakeResponse);
private JsonResponse jsonResponse = new JsonResponse(fakeResponse, GSON);
@Test
void testSetStatus() {
@@ -44,9 +48,8 @@ class JsonResponseTest {
jsonResponse.setPayload(responseValues);
String payload = fakeResponse.getPayload();
assertThat(payload).startsWith(JSON_SAFETY_PREFIX);
@SuppressWarnings("unchecked")
Map<String, Object> responseMap = (Map<String, Object>)
JSONValue.parse(payload.substring(JSON_SAFETY_PREFIX.length()));
Map<String, Object> responseMap =
GSON.fromJson(payload.substring(JSON_SAFETY_PREFIX.length()), new TypeToken<>() {});
assertThat(responseMap).containsExactlyEntriesIn(responseValues);
}
@@ -19,21 +19,26 @@ import static google.registry.request.RequestModule.provideJsonPayload;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.HttpException.UnsupportedMediaTypeException;
import google.registry.tools.GsonUtils;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link RequestModule}. */
final class RequestModuleTest {
private static final Gson GSON = GsonUtils.provideGson();
@Test
void testProvideJsonPayload() {
assertThat(provideJsonPayload(MediaType.JSON_UTF_8, "{\"k\":\"v\"}")).containsExactly("k", "v");
assertThat(provideJsonPayload(MediaType.JSON_UTF_8, "{\"k\":\"v\"}", GSON))
.containsExactly("k", "v");
}
@Test
void testProvideJsonPayload_contentTypeWithoutCharsetAllowed() {
assertThat(provideJsonPayload(MediaType.JSON_UTF_8.withoutParameters(), "{\"k\":\"v\"}"))
assertThat(provideJsonPayload(MediaType.JSON_UTF_8.withoutParameters(), "{\"k\":\"v\"}", GSON))
.containsExactly("k", "v");
}
@@ -41,14 +46,16 @@ final class RequestModuleTest {
void testProvideJsonPayload_malformedInput_throws500() {
BadRequestException thrown =
assertThrows(
BadRequestException.class, () -> provideJsonPayload(MediaType.JSON_UTF_8, "{\"k\":"));
BadRequestException.class,
() -> provideJsonPayload(MediaType.JSON_UTF_8, "{\"k\":", GSON));
assertThat(thrown).hasMessageThat().contains("Malformed JSON");
}
@Test
void testProvideJsonPayload_emptyInput_throws500() {
BadRequestException thrown =
assertThrows(BadRequestException.class, () -> provideJsonPayload(MediaType.JSON_UTF_8, ""));
assertThrows(
BadRequestException.class, () -> provideJsonPayload(MediaType.JSON_UTF_8, "", GSON));
assertThat(thrown).hasMessageThat().contains("Malformed JSON");
}
@@ -56,13 +63,13 @@ final class RequestModuleTest {
void testProvideJsonPayload_nonJsonContentType_throws415() {
assertThrows(
UnsupportedMediaTypeException.class,
() -> provideJsonPayload(MediaType.PLAIN_TEXT_UTF_8, "{}"));
() -> provideJsonPayload(MediaType.PLAIN_TEXT_UTF_8, "{}", GSON));
}
@Test
void testProvideJsonPayload_contentTypeWithWeirdParam_throws415() {
assertThrows(
UnsupportedMediaTypeException.class,
() -> provideJsonPayload(MediaType.JSON_UTF_8.withParameter("omg", "handel"), "{}"));
() -> provideJsonPayload(MediaType.JSON_UTF_8.withParameter("omg", "handel"), "{}", GSON));
}
}
@@ -20,21 +20,25 @@ import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.security.JsonHttp.JSON_SAFETY_PREFIX;
import com.google.common.base.Supplier;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import google.registry.tools.GsonUtils;
import java.io.BufferedReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Map;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
/**
* Helper class for testing JSON RPC servlets.
*/
public final class JsonHttpTestUtils {
private static final Gson GSON = GsonUtils.provideGson();
/** Returns JSON payload for mocked result of {@code rsp.getReader()}. */
public static BufferedReader createJsonPayload(Map<String, ?> object) {
return createJsonPayload(JSONValue.toJSONString(object));
return createJsonPayload(GSON.toJson(object));
}
/** @see #createJsonPayload(Map) */
@@ -58,10 +62,8 @@ public final class JsonHttpTestUtils {
assertThat(jsonText).startsWith(JSON_SAFETY_PREFIX);
jsonText = jsonText.substring(JSON_SAFETY_PREFIX.length());
try {
@SuppressWarnings("unchecked")
Map<String, Object> json = (Map<String, Object>) JSONValue.parseWithException(jsonText);
return json;
} catch (ClassCastException | ParseException e) {
return GSON.fromJson(jsonText, new TypeToken<>() {});
} catch (ClassCastException | JsonSyntaxException e) {
assertWithMessage("Bad JSON: %s\n%s", e.getMessage(), jsonText).fail();
throw new AssertionError();
}
@@ -33,7 +33,7 @@ public final class RegistryTestServer {
public static final ImmutableMap<String, Path> RUNFILES =
new ImmutableMap.Builder<String, Path>()
.put("/console/*", PROJECT_ROOT.resolve("console-webapp/staged/dist"))
.put("/console/*", PROJECT_ROOT.resolve("console-webapp/staged/dist/browser"))
.build();
public static final ImmutableList<Route> ROUTES =
@@ -69,6 +69,9 @@ import google.registry.model.billing.BillingCancellation;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.common.DnsRefreshRequest;
import google.registry.model.common.FeatureFlag;
import google.registry.model.common.FeatureFlag.FeatureName;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
@@ -682,6 +685,32 @@ public final class DatabaseHelper {
return newRegistrars.build();
}
/** Persists and returns a {@link FeatureFlag} with a single status starting from the epoch. */
public static FeatureFlag persistFeatureFlag(FeatureName featureName, FeatureStatus status) {
return persistFeatureFlag(featureName, ImmutableSortedMap.of(START_INSTANT, status));
}
/** Persists and returns a {@link FeatureFlag} with an initial status and one transition. */
public static FeatureFlag persistFeatureFlag(
FeatureName featureName,
FeatureStatus initialStatus,
Instant transitionTime,
FeatureStatus status) {
return persistFeatureFlag(
featureName,
ImmutableSortedMap.<Instant, FeatureStatus>naturalOrder()
.put(START_INSTANT, initialStatus)
.put(transitionTime, status)
.build());
}
/** Persists and returns a {@link FeatureFlag} with a custom status map. */
public static FeatureFlag persistFeatureFlag(
FeatureName featureName, ImmutableSortedMap<Instant, FeatureStatus> statusMap) {
return persistResource(
new FeatureFlag.Builder().setFeatureName(featureName).setStatusMap(statusMap).build());
}
public static Iterable<BillingBase> getBillingEvents() {
return tm().transact(
() ->
@@ -15,6 +15,7 @@
package google.registry.testing;
import google.registry.request.JsonResponse;
import google.registry.tools.GsonUtils;
import java.util.Map;
/** Fake implementation of {@link JsonResponse} for testing. */
@@ -23,7 +24,7 @@ public final class FakeJsonResponse extends JsonResponse {
private Map<String, ?> responseMap;
public FakeJsonResponse() {
super(new FakeResponse());
super(new FakeResponse(), GsonUtils.provideGson());
}
@Override
@@ -55,7 +55,7 @@ public class GoldenFileTestHelper {
public static GoldenFileTestHelper assertThatRoutesFromComponent(Class<?> component) {
return assertThat(RouterDisplayHelper.extractHumanReadableRoutesFromComponent(component))
.createdByNomulusCommand("get_routing_map -c " + component.getName());
.createdByNomulusCommand("get_routing_map");
}
public GoldenFileTestHelper createdByNomulusCommand(String nomulusCommand) {
@@ -57,7 +57,7 @@ import google.registry.util.UrlConnectionException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URI;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
@@ -249,7 +249,7 @@ class NordnUploadActionTest {
.setRequestProperty(eq(CONTENT_TYPE), startsWith("multipart/form-data; boundary="));
verify(httpUrlConnection).setRequestMethod("POST");
assertThat(httpUrlConnection.getURL())
.isEqualTo(new URL("http://127.0.0.1/LORDN/tld/" + phase));
.isEqualTo(URI.create("http://127.0.0.1/LORDN/tld/" + phase).toURL());
assertThat(connectionOutputStream.toString(UTF_8)).contains(csv);
verifyColumnCleared(domain1);
verifyColumnCleared(domain2);
@@ -38,7 +38,7 @@ import google.registry.testing.FakeResponse;
import google.registry.testing.FakeUrlConnectionService;
import java.io.ByteArrayInputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URI;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -49,33 +49,35 @@ class NordnVerifyActionTest {
private static final String LOG_ACCEPTED =
"""
1,2012-08-16T02:15:00.0Z,2012-08-16T00:00:00.0Z,0000000000000478Nzs+3VMkR8ckuUynOLmyeqTmZQSbzDuf/R50n2n5QX4=,accepted,no-warnings,1
roid,result-code
SH8013-REP,2000""";
1,2012-08-16T02:15:00.0Z,2012-08-16T00:00:00.0Z,0000000000000478Nzs+3VMkR8ckuUynOLmyeqTmZQSbzDuf/R50n2n5QX4=,accepted,no-warnings,1
roid,result-code
SH8013-REP,2000\
""";
private static final String LOG_REJECTED =
"""
1,2012-08-16T02:15:00.0Z,2012-08-16T00:00:00.0Z,0000000000000478Nzs+3VMkR8ckuUynOLmyeqTmZQSbzDuf/R50n2n5QX4=,rejected,no-warnings,1
roid,result-code
SH8013-REP,2001""";
1,2012-08-16T02:15:00.0Z,2012-08-16T00:00:00.0Z,0000000000000478Nzs+3VMkR8ckuUynOLmyeqTmZQSbzDuf/R50n2n5QX4=,rejected,no-warnings,1
roid,result-code
SH8013-REP,2001\
""";
private static final String LOG_WARNINGS =
"""
1,2012-08-16T02:15:00.0Z,2012-08-16T00:00:00.0Z,0000000000000478Nzs+3VMkR8ckuUynOLmyeqTmZQSbzDuf/R50n2n5QX4=,accepted,warnings-present,3
roid,result-code
SH8013-REP,2001
lulz-roid,3609
sabokitty-roid,3610
""";
1,2012-08-16T02:15:00.0Z,2012-08-16T00:00:00.0Z,0000000000000478Nzs+3VMkR8ckuUynOLmyeqTmZQSbzDuf/R50n2n5QX4=,accepted,warnings-present,3
roid,result-code
SH8013-REP,2001
lulz-roid,3609
sabokitty-roid,3610
""";
private static final String LOG_ERRORS =
"""
1,2012-08-16T02:15:00.0Z,2012-08-16T00:00:00.0Z,0000000000000478Nzs+3VMkR8ckuUynOLmyeqTmZQSbzDuf/R50n2n5QX4=,accepted,warnings-present,3
roid,result-code
SH8013-REP,2000
lulz-roid,4601
bogpog,4611
""";
1,2012-08-16T02:15:00.0Z,2012-08-16T00:00:00.0Z,0000000000000478Nzs+3VMkR8ckuUynOLmyeqTmZQSbzDuf/R50n2n5QX4=,accepted,warnings-present,3
roid,result-code
SH8013-REP,2000
lulz-roid,4601
bogpog,4611
""";
@RegisterExtension
final JpaIntegrationTestExtension jpa =
@@ -101,14 +103,15 @@ class NordnVerifyActionTest {
.thenReturn(new ByteArrayInputStream(LOG_ACCEPTED.getBytes(UTF_8)));
action.lordnRequestInitializer = lordnRequestInitializer;
action.response = response;
action.url = new URL("http://127.0.0.1/blobio");
action.url = URI.create("http://ry.marksdb.org/blobio").toURL();
}
@Test
@SuppressWarnings("DirectInvocationOnMock")
void testSuccess_sendHttpRequest_urlIsCorrect() throws Exception {
action.run();
assertThat(httpUrlConnection.getURL()).isEqualTo(new URL("http://127.0.0.1/blobio"));
assertThat(httpUrlConnection.getURL())
.isEqualTo(URI.create("http://ry.marksdb.org/blobio").toURL());
}
@Test
@@ -162,4 +165,22 @@ class NordnVerifyActionTest {
ConflictException thrown = assertThrows(ConflictException.class, action::run);
assertThat(thrown).hasMessageThat().contains("Not ready");
}
@Test
void testFailure_badUrl() throws Exception {
action.url = URI.create("http://example.com/blobio").toURL();
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, action::run);
assertThat(thrown)
.hasMessageThat()
.isEqualTo("URL http://example.com/blobio must start with ry.marksdb.org");
}
@Test
@SuppressWarnings("DirectInvocationOnMock")
void testSuccess_uppercaseUrl() throws Exception {
action.url = URI.create("http://RY.MARKSDB.ORG/blobio").toURL();
action.run();
assertThat(httpUrlConnection.getURL())
.isEqualTo(URI.create("http://RY.MARKSDB.ORG/blobio").toURL());
}
}
@@ -24,7 +24,7 @@ import static org.mockito.Mockito.when;
import google.registry.config.RegistryConfig.ConfigModule.TmchCaMode;
import java.io.ByteArrayInputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URI;
import java.security.SignatureException;
import java.security.cert.CRLException;
import java.security.cert.CertificateNotYetValidException;
@@ -39,7 +39,7 @@ class TmchCrlActionTest extends TmchActionTestCase {
TmchCrlAction action = new TmchCrlAction();
action.marksdb = marksdb;
action.tmchCertificateAuthority = new TmchCertificateAuthority(tmchCaMode, clock);
action.tmchCrlUrl = new URL("https://sloth.lol/tmch.crl");
action.tmchCrlUrl = URI.create("https://sloth.lol/tmch.crl").toURL();
return action;
}
@@ -57,7 +57,7 @@ class TmchCrlActionTest extends TmchActionTestCase {
newTmchCrlAction(TmchCaMode.PILOT).run();
verify(httpUrlConnection).getInputStream();
assertThat(urlConnectionService.getConnectedUrls())
.containsExactly(new URL("https://sloth.lol/tmch.crl"));
.containsExactly(URI.create("https://sloth.lol/tmch.crl").toURL());
}
@Test
@@ -19,7 +19,7 @@ import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATAS
import static google.registry.model.common.FeatureFlag.FeatureName.TEST_FEATURE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.persistFeatureFlag;
import static google.registry.util.DateTimeUtils.START_INSTANT;
import static google.registry.util.DateTimeUtils.plusWeeks;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -27,7 +27,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.FeatureFlag;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.testing.FakeClock;
import java.time.Instant;
@@ -81,14 +80,7 @@ public class ConfigureFeatureFlagCommandTest extends CommandTestCase<ConfigureFe
@Test
void testUpdate() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<Instant, FeatureStatus>naturalOrder()
.put(START_INSTANT, INACTIVE)
.build())
.build());
persistFeatureFlag(TEST_FEATURE, INACTIVE);
Instant featureStart = plusWeeks(clock.now(), 6);
assertThat(FeatureFlag.get(TEST_FEATURE).getStatusMap())
@@ -110,14 +102,7 @@ public class ConfigureFeatureFlagCommandTest extends CommandTestCase<ConfigureFe
@Test
void testConfigure_multipleFlags() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<Instant, FeatureStatus>naturalOrder()
.put(START_INSTANT, INACTIVE)
.build())
.build());
persistFeatureFlag(TEST_FEATURE, INACTIVE);
Instant featureStart = plusWeeks(clock.now(), 6);
assertThat(FeatureFlag.get(TEST_FEATURE).getStatusMap())
@@ -179,14 +164,7 @@ public class ConfigureFeatureFlagCommandTest extends CommandTestCase<ConfigureFe
@Test
void testUpdate_invalidStatusMap() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<Instant, FeatureStatus>naturalOrder()
.put(START_INSTANT, INACTIVE)
.build())
.build());
persistFeatureFlag(TEST_FEATURE, INACTIVE);
Instant featureStart = plusWeeks(clock.now(), 6);
assertThat(FeatureFlag.get(TEST_FEATURE).getStatusMap())

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