mirror of
https://github.com/google/nomulus
synced 2026-07-12 11:02:36 +00:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 653da19e53 | |||
| 4df734da2a | |||
| b8a51cacc4 | |||
| 5067b3d339 | |||
| 93ec04018f | |||
| ae310ea141 | |||
| c785365590 | |||
| 6ad85a2d21 | |||
| 2e43295d63 | |||
| a7118dfbba | |||
| 0bf0b4fc66 | |||
| 2ce91e3477 | |||
| 4d6b5a82df | |||
| 1fe1043306 | |||
| b1e42cfd5e | |||
| 0fa82e30bb | |||
| 6608ee282d | |||
| 22c867f3f2 | |||
| 160abef731 | |||
| a6e4017971 | |||
| d9a857133a | |||
| c7a27061d8 | |||
| 7766db36a7 | |||
| eda0f7ad7c | |||
| 67527f1560 | |||
| 4aeba6e3f7 | |||
| d6f1f5894b | |||
| 47ad569cb0 | |||
| 06934daf94 | |||
| 9a032e4bb9 | |||
| 0ab612ab23 | |||
| 403c7ad275 | |||
| 11d625b837 | |||
| 6a47287da7 | |||
| cdc0ffe831 | |||
| 7c23413d83 | |||
| fe222bbdcf | |||
| f770f6a46d | |||
| e071f5579c | |||
| 6080cd2f7a | |||
| 90f583910e | |||
| a85bf5c30a | |||
| dcfe939c38 | |||
| ae61922318 |
@@ -44,6 +44,11 @@ This document outlines foundational mandates, architectural patterns, and projec
|
||||
- **Test Helpers & Timestamps:** If a static test helper method (like in `DatabaseHelper`) needs the database transaction time but might be called from outside a transaction, using `tm().reTransact(tm()::getTxTime)` is acceptable. However, NEVER wrap it redundantly like `tm().transact(() -> tm().reTransact(tm()::getTxTime))`. If you are just setting an arbitrary timestamp in a test where the exact DB transaction time isn't strictly required, prefer `Instant.now()` or `clock.now()` to avoid creating unnecessary database transactions.
|
||||
- **Production Code:** In production code, if a flow fails because it is calling `getTxTime()` outside of a transaction, you must wrap the *caller* in a transaction instead of adding an unnecessary `reTransact()` around `getTxTime()`.
|
||||
- **Transactional Time:** Ensure code that relies on `tm().getTransactionTime()` (or `tm().getTxTime()`) is executed within a transaction context.
|
||||
- **Database Schema Migrations & 2-PR Split Mandate:**
|
||||
- **Mandatory Consultation of `db/README.md`:** Before planning, drafting, or executing any database schema modifications (e.g., adding/altering columns, creating Flyway `.sql` migration scripts, or modifying JPA entity `@Column` mappings), you **MUST read and strictly adhere to `db/README.md`**.
|
||||
- **Strict 2-PR Deployment Split:** Never propose or submit combining Flyway SQL scripts (`db/src/main/resources/sql/flyway/V*.sql`) and Java ORM changes (`.java` entity files + `db-schema.sql.generated`) into a single PR for submission to `master`. Because live servers during a rolling deployment will fail if Java code attempts to access unmigrated database columns/constraints, all schema additions must be split into two sequential PRs per `db/README.md`:
|
||||
1. **PR #1 (Database Schema Only):** Contains *only* the new Flyway `.sql` script, the `flyway.txt` index update (`:db:generateFlywayIndex`), the `nomulus.golden.sql` dump (`:nom:generate_golden_file`), and ER diagrams (`er_diagram/`). Must contain **zero `.java` files or `db-schema.sql.generated` changes**.
|
||||
2. **PR #2 (Java ORM, EPP Flows & Generated Schema Map):** Submitted *only after* PR #1 is deployed to production. Contains all `.java` entity/flow modifications, tests, and the regenerated `db-schema.sql.generated` (`generateSqlSchema`).
|
||||
|
||||
### 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.
|
||||
@@ -51,6 +56,7 @@ This document outlines foundational mandates, architectural patterns, and projec
|
||||
- **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).
|
||||
- **Gradle Test Patterns:** When running tests to investigate fixes in the "core" directory, try to first use the "standardTest" Gradle task. It is faster than the "test" task, which includes the "fragileTest" task. Only run the full "test" task after "standardTest" succeeds.
|
||||
- **Mandatory SQL Integration Verification:** Whenever you modify any database schema, Flyway script (`.sql`), or JPA entity class, you MUST explicitly run both `./gradlew :db:test` and `./gradlew :core:sqlIntegrationTest` **in addition to** the standard test suites (`./gradlew standardTest` or `./gradlew test`) before finalizing the task or declaring completion. Do not rely solely on `standardTest` when database schemas or ORM mappings are touched; all three test suites (`standardTest`, `:db:test`, and `:core:sqlIntegrationTest`) are mandatory.
|
||||
|
||||
### 6. Project Dependencies
|
||||
- **Common Module:** When using `Clock` or other core utilities in a new or separate module (like `load-testing`), ensure `implementation project(':common')` is added to the module's `build.gradle`.
|
||||
@@ -94,7 +100,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
|
||||
|
||||
@@ -171,3 +177,4 @@ This protocol defines the standard for interacting with GitHub repositories and
|
||||
- **One Commit Per PR:** Ensure all changes are squashed into a single, clean commit. Use `git commit --amend --no-edit` for follow-up fixes.
|
||||
- **Clean Workspace:** Always run `git status` and verify the repository state before declaring a task complete.
|
||||
- **Package Lock:** The Gradle build automatically modifies `console-webapp/package-lock.json` via the `npmInstallDeps` task. ALWAYS revert this file (`git checkout console-webapp/package-lock.json`) before staging changes or finalizing a commit unless you explicitly modified NPM dependencies.
|
||||
- **PR Description Synchronization:** Whenever you amend or update a commit's description, you **MUST** check whether the corresponding GitHub PR description (if one exists) matches the previous commit description. If the PR description was not manually customized (i.e., it simply reflects the older commit description), you must automatically update it via `gh pr edit` to keep it in sync with your newly updated commit description—**strictly after the updated commit has been pushed to the remote GitHub PR branch**. Do not update the remote PR description when changes are only committed locally. **CRITICAL:** When updating the PR description, you must strictly preserve any automated review links or footer blocks (such as Reviewable link blocks: `<!-- Reviewable:start -->...<!-- Reviewable:end -->`) at the bottom of the description.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,8 +48,7 @@ task buildConsoleForAll() {}
|
||||
def createConsoleTask = { env ->
|
||||
project.tasks.register("buildConsoleFor${env.capitalize()}", Exec) {
|
||||
workingDir "${consoleDir}/"
|
||||
executable 'npx'
|
||||
args 'ng', 'build', '--base-href=/console/', "--configuration=${env}"
|
||||
commandLine 'sh', '-c', "npx ng build --base-href=/console/ --configuration=${env}"
|
||||
doFirst {
|
||||
println "Building console for environment: ${env}"
|
||||
}
|
||||
@@ -91,14 +86,12 @@ 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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<p>
|
||||
<mat-label for="password">Password: </mat-label>
|
||||
<mat-form-field name="password" appearance="outline">
|
||||
<input matInput type="text" formControlName="password" required />
|
||||
<input matInput type="password" formControlName="password" required />
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<p>
|
||||
@@ -60,7 +60,7 @@
|
||||
<p>
|
||||
<mat-label for="password">Password: </mat-label>
|
||||
<mat-form-field name="password" appearance="outline">
|
||||
<input matInput type="text" formControlName="password" required />
|
||||
<input matInput type="password" formControlName="password" required />
|
||||
</mat-form-field>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -226,18 +226,17 @@ public class SafeBrowsingTransforms {
|
||||
private void processResponse(
|
||||
CloseableHttpResponse response,
|
||||
ImmutableSet.Builder<KV<DomainNameInfo, ThreatMatch>> resultBuilder)
|
||||
throws JSONException, IOException {
|
||||
throws IOException {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
if (statusCode != SC_OK) {
|
||||
logger.atWarning().log("Got unexpected status code %s from response.", statusCode);
|
||||
} else {
|
||||
// Unpack the response body
|
||||
JSONObject responseBody =
|
||||
new JSONObject(
|
||||
CharStreams.toString(
|
||||
new InputStreamReader(response.getEntity().getContent(), UTF_8)));
|
||||
logger.atInfo().log("Got response: %s", responseBody);
|
||||
if (responseBody.length() == 0) {
|
||||
throw new IOException(
|
||||
String.format("Got unexpected status code %s from response.", statusCode));
|
||||
}
|
||||
// Unpack the response body
|
||||
try (InputStreamReader reader =
|
||||
new InputStreamReader(response.getEntity().getContent(), UTF_8)) {
|
||||
JSONObject responseBody = new JSONObject(CharStreams.toString(reader));
|
||||
if (responseBody.isEmpty()) {
|
||||
logger.atInfo().log("Response was empty, no threats detected.");
|
||||
} else {
|
||||
// Emit all DomainNameInfos with their API results.
|
||||
|
||||
@@ -38,6 +38,7 @@ import java.security.GeneralSecurityException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
@@ -106,12 +107,16 @@ public final class CacheModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
public static HostCache provideHostCache(
|
||||
Optional<SimplifiedJedisClient> jedisClient, CacheMetrics cacheMetrics) {
|
||||
Optional<SimplifiedJedisClient> jedisClient, Clock clock, CacheMetrics cacheMetrics) {
|
||||
if (jedisClient.isEmpty()) {
|
||||
return repoId ->
|
||||
Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId)));
|
||||
return repoId -> {
|
||||
Instant now = clock.now();
|
||||
return Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId)))
|
||||
.filter(host -> now.isBefore(host.getDeletionTime()))
|
||||
.map(host -> (Host) host.cloneProjectedAtTime(now));
|
||||
};
|
||||
}
|
||||
return new MultilayerHostCache(jedisClient.get(), cacheMetrics);
|
||||
return new MultilayerHostCache(jedisClient.get(), clock, cacheMetrics);
|
||||
}
|
||||
|
||||
private static SSLSocketFactory createValkeySslSocketFactory(String valkeyCertificateAuthority) {
|
||||
|
||||
@@ -19,7 +19,6 @@ import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.util.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -30,12 +29,9 @@ import java.util.Optional;
|
||||
public class MultilayerDomainCache extends MultilayerEppResourceCache<Domain>
|
||||
implements DomainCache {
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
public MultilayerDomainCache(
|
||||
SimplifiedJedisClient jedisClient, Clock clock, CacheMetrics cacheMetrics) {
|
||||
super(jedisClient, cacheMetrics);
|
||||
this.clock = clock;
|
||||
super(jedisClient, clock, cacheMetrics);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,15 +42,10 @@ public class MultilayerDomainCache extends MultilayerEppResourceCache<Domain>
|
||||
@Override
|
||||
protected Optional<Domain> loadFromDatabase(String domainName) {
|
||||
// Don't use the cache (avoid caching the same domain twice). Do use the replica SQL instance.
|
||||
Optional<Domain> possibleDomain =
|
||||
Optional.ofNullable(
|
||||
ForeignKeyUtils.loadMostRecentResourceObjects(
|
||||
Domain.class, ImmutableList.of(domainName), true)
|
||||
.get(domainName));
|
||||
Instant now = clock.now();
|
||||
return possibleDomain
|
||||
.filter(domain -> now.isBefore(domain.getDeletionTime()))
|
||||
.map(domain -> domain.cloneProjectedAtTime(now));
|
||||
return Optional.ofNullable(
|
||||
ForeignKeyUtils.loadMostRecentResourceObjects(
|
||||
Domain.class, ImmutableList.of(domainName), true)
|
||||
.get(domainName));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,7 +18,9 @@ import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.util.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -36,11 +38,13 @@ public abstract class MultilayerEppResourceCache<V extends EppResource> {
|
||||
.build();
|
||||
|
||||
private final SimplifiedJedisClient jedisClient;
|
||||
private final Clock clock;
|
||||
private final CacheMetrics cacheMetrics;
|
||||
|
||||
protected MultilayerEppResourceCache(
|
||||
SimplifiedJedisClient jedisClient, CacheMetrics cacheMetrics) {
|
||||
SimplifiedJedisClient jedisClient, Clock clock, CacheMetrics cacheMetrics) {
|
||||
this.jedisClient = jedisClient;
|
||||
this.clock = clock;
|
||||
this.cacheMetrics = cacheMetrics;
|
||||
}
|
||||
|
||||
@@ -50,7 +54,16 @@ public abstract class MultilayerEppResourceCache<V extends EppResource> {
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Optional<V> loadFromCaches(Class<V> clazz, String key) {
|
||||
Instant now = clock.now();
|
||||
return (Optional<V>)
|
||||
loadFromCachesInternal(clazz, key)
|
||||
.filter(v -> now.isBefore(v.getDeletionTime()))
|
||||
.map(v -> v.cloneProjectedAtTime(now));
|
||||
}
|
||||
|
||||
private Optional<V> loadFromCachesInternal(Class<V> clazz, String key) {
|
||||
// hopefully the resource is in the local cache
|
||||
Optional<V> possibleValue = Optional.ofNullable(localCache.getIfPresent(key));
|
||||
if (possibleValue.isPresent()) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
|
||||
|
||||
import google.registry.model.host.Host;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.Clock;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -27,8 +28,9 @@ import java.util.Optional;
|
||||
*/
|
||||
public class MultilayerHostCache extends MultilayerEppResourceCache<Host> implements HostCache {
|
||||
|
||||
public MultilayerHostCache(SimplifiedJedisClient jedisClient, CacheMetrics cacheMetrics) {
|
||||
super(jedisClient, cacheMetrics);
|
||||
public MultilayerHostCache(
|
||||
SimplifiedJedisClient jedisClient, Clock clock, CacheMetrics cacheMetrics) {
|
||||
super(jedisClient, clock, cacheMetrics);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -38,6 +38,7 @@ import google.registry.dns.ReadDnsRefreshRequestsAction;
|
||||
import google.registry.model.common.DnsRefreshRequest;
|
||||
import google.registry.mosapi.MosApiClient;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import google.registry.rde.RdeUploadUrl;
|
||||
import google.registry.request.Action.Service;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import google.registry.util.YamlUtils;
|
||||
@@ -173,6 +174,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
|
||||
*
|
||||
@@ -830,8 +843,8 @@ public final class RegistryConfig {
|
||||
*/
|
||||
@Provides
|
||||
@Config("rdeUploadUrl")
|
||||
public static URI provideRdeUploadUrl(RegistryConfigSettings config) {
|
||||
return URI.create(config.rde.uploadUrl);
|
||||
public static RdeUploadUrl provideRdeUploadUrl(RegistryConfigSettings config) {
|
||||
return RdeUploadUrl.create(URI.create(config.rde.uploadUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- <!– Queue for async actions that should be run at some point in the future. –>-->
|
||||
<!-- 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.
|
||||
|
||||
-10
@@ -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>
|
||||
|
||||
-10
@@ -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>
|
||||
|
||||
@@ -19,7 +19,6 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import google.registry.request.Response;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -55,7 +54,6 @@ public class CookieSessionMetadata extends SessionMetadata {
|
||||
Pattern.compile("serviceExtensionUris=([^,\\s}]+)?");
|
||||
private static final Pattern FAILED_LOGIN_ATTEMPTS_PATTERN =
|
||||
Pattern.compile("failedLoginAttempts=([^,\\s]+)?");
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private final Map<String, String> data = new HashMap<>();
|
||||
|
||||
@@ -66,7 +64,6 @@ public class CookieSessionMetadata extends SessionMetadata {
|
||||
Matcher matcher = COOKIE_PATTERN.matcher(cookie);
|
||||
if (matcher.find()) {
|
||||
String sessionInfo = decode(matcher.group(1));
|
||||
logger.atInfo().log("SESSION INFO: %s", sessionInfo);
|
||||
matcher = REGISTRAR_ID_PATTERN.matcher(sessionInfo);
|
||||
if (matcher.find()) {
|
||||
String registrarId = matcher.group(1);
|
||||
|
||||
@@ -68,9 +68,12 @@ public final class EppController {
|
||||
try {
|
||||
eppInput = unmarshalEpp(EppInput.class, inputXmlBytes);
|
||||
} catch (EppException e) {
|
||||
// Log the unmarshalling error, with the raw bytes (in base64) to help with debugging.
|
||||
// Log the unmarshalling error, with the sanitized bytes (in base64) to help with debugging.
|
||||
Optional<String> sanitizedXml = EppXmlSanitizer.sanitizeEppXmlIfValid(inputXmlBytes);
|
||||
String xmlBytesToLog =
|
||||
base64().encode(sanitizedXml.map(xml -> xml.getBytes(UTF_8)).orElse(inputXmlBytes));
|
||||
logger.atInfo().withCause(e).log(
|
||||
"EPP request XML unmarshalling failed - \"%s\":\n%s\n%s\n%s\n%s",
|
||||
"EPP request XML unmarshalling failed - \"%s\":\n%s\n%s",
|
||||
e.getMessage(),
|
||||
lazy(
|
||||
() ->
|
||||
@@ -83,12 +86,7 @@ public final class EppController {
|
||||
"resultMessage",
|
||||
e.getResult().getCode().msg,
|
||||
"xmlBytes",
|
||||
base64().encode(inputXmlBytes)))),
|
||||
LOG_SEPARATOR,
|
||||
lazy(
|
||||
() ->
|
||||
new String(inputXmlBytes, UTF_8)
|
||||
.trim()), // Charset decoding failures are swallowed.
|
||||
xmlBytesToLog))),
|
||||
LOG_SEPARATOR);
|
||||
// Return early by sending an error message, with no clTRID since we couldn't unmarshal it.
|
||||
eppMetricBuilder.setStatus(e.getResult().getCode());
|
||||
|
||||
@@ -87,16 +87,22 @@ public class EppXmlSanitizer {
|
||||
*
|
||||
* <p>Also, an empty element will be formatted as {@code <tag></tag>} instead of {@code <tag/>}.
|
||||
*/
|
||||
public static String sanitizeEppXml(byte[] inputXmlBytes) {
|
||||
public static Optional<String> sanitizeEppXmlIfValid(byte[] inputXmlBytes) {
|
||||
try {
|
||||
// Keep exactly one newline at end of sanitized string.
|
||||
return CharMatcher.whitespace().trimTrailingFrom(sanitizeAndEncode(inputXmlBytes)) + "\n";
|
||||
return Optional.of(
|
||||
CharMatcher.whitespace().trimTrailingFrom(sanitizeAndEncode(inputXmlBytes)) + "\n");
|
||||
} catch (XMLStreamException | UnsupportedEncodingException e) {
|
||||
logger.atWarning().withCause(e).log("Failed to sanitize EPP XML message.");
|
||||
return Base64.getMimeEncoder().encodeToString(inputXmlBytes);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public static String sanitizeEppXml(byte[] inputXmlBytes) {
|
||||
return sanitizeEppXmlIfValid(inputXmlBytes)
|
||||
.orElseGet(() -> Base64.getMimeEncoder().encodeToString(inputXmlBytes));
|
||||
}
|
||||
|
||||
private static String sanitizeAndEncode(byte[] inputXmlBytes)
|
||||
throws XMLStreamException, UnsupportedEncodingException {
|
||||
XMLEventReader xmlEventReader =
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,15 @@
|
||||
|
||||
package google.registry.gcs;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.Iterables.getLast;
|
||||
|
||||
import com.google.cloud.storage.Blob;
|
||||
import com.google.cloud.storage.BlobId;
|
||||
import com.google.cloud.storage.BlobInfo;
|
||||
import com.google.cloud.storage.Bucket;
|
||||
import com.google.cloud.storage.BucketInfo;
|
||||
import com.google.cloud.storage.Storage;
|
||||
import com.google.cloud.storage.Storage.BlobListOption;
|
||||
import com.google.cloud.storage.StorageException;
|
||||
@@ -33,12 +36,14 @@ import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
|
||||
import google.registry.util.GoogleCredentialsBundle;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import jakarta.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.nio.channels.Channels;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
|
||||
/**
|
||||
@@ -50,6 +55,8 @@ public class GcsUtils implements Serializable {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final ConcurrentHashMap<String, Boolean> PAP_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private static final ImmutableMap<String, MediaType> EXTENSIONS =
|
||||
new ImmutableMap.Builder<String, MediaType>()
|
||||
.put("ghostryde", MediaType.APPLICATION_BINARY)
|
||||
@@ -85,6 +92,7 @@ public class GcsUtils implements Serializable {
|
||||
/** Opens a GCS file for writing as an {@link OutputStream}, overwriting existing files. */
|
||||
@CheckReturnValue
|
||||
public OutputStream openOutputStream(BlobId blobId) {
|
||||
verifyPublicAccessPrevention(blobId.getBucket());
|
||||
return Channels.newOutputStream(storage().writer(createBlobInfo(blobId)));
|
||||
}
|
||||
|
||||
@@ -94,6 +102,7 @@ public class GcsUtils implements Serializable {
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public OutputStream openOutputStream(BlobId blobId, ImmutableMap<String, String> metadata) {
|
||||
verifyPublicAccessPrevention(blobId.getBucket());
|
||||
return Channels.newOutputStream(
|
||||
storage().writer(BlobInfo.newBuilder(blobId).setMetadata(metadata).build()));
|
||||
}
|
||||
@@ -105,6 +114,7 @@ public class GcsUtils implements Serializable {
|
||||
|
||||
/** Creates a GCS file with the given byte contents and metadata, overwriting existing files. */
|
||||
public void createFromBytes(BlobInfo blobInfo, byte[] bytes) throws StorageException {
|
||||
verifyPublicAccessPrevention(blobInfo.getBucket());
|
||||
storage().create(blobInfo, bytes);
|
||||
}
|
||||
|
||||
@@ -120,6 +130,7 @@ public class GcsUtils implements Serializable {
|
||||
|
||||
/** Update file content type on existing GCS file */
|
||||
public void updateContentType(BlobId blobId, String contentType) throws StorageException {
|
||||
verifyPublicAccessPrevention(blobId.getBucket());
|
||||
if (existsAndNotEmpty(blobId)) {
|
||||
Blob blob = storage().get(blobId);
|
||||
blob.toBuilder().setContentType(contentType).build().update();
|
||||
@@ -154,12 +165,6 @@ public class GcsUtils implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the user defined metadata of a GCS file if the file exists, or an empty map. */
|
||||
public ImmutableMap<String, String> getMetadata(BlobId blobId) throws StorageException {
|
||||
Blob blob = storage().get(blobId);
|
||||
return blob == null ? ImmutableMap.of() : ImmutableMap.copyOf(blob.getMetadata());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link BlobInfo} of the given GCS file.
|
||||
*
|
||||
@@ -179,6 +184,37 @@ public class GcsUtils implements Serializable {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that Public Access Prevention (PAP) is enforced on the GCS bucket.
|
||||
*
|
||||
* @throws IllegalStateException if PAP is not ENFORCED.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void verifyPublicAccessPrevention(String bucketName) {
|
||||
if (RegistryEnvironment.get() != RegistryEnvironment.PRODUCTION) {
|
||||
return;
|
||||
}
|
||||
PAP_CACHE.computeIfAbsent(
|
||||
bucketName,
|
||||
name -> {
|
||||
Bucket bucket = storage().get(name);
|
||||
checkState(bucket != null, "Bucket %s does not exist", name);
|
||||
BucketInfo.PublicAccessPrevention pap =
|
||||
bucket.getIamConfiguration().getPublicAccessPrevention();
|
||||
checkState(
|
||||
pap == BucketInfo.PublicAccessPrevention.ENFORCED,
|
||||
"Public Access Prevention is not enforced on bucket %s. Current state: %s",
|
||||
name,
|
||||
pap);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static void clearPapCache() {
|
||||
PAP_CACHE.clear();
|
||||
}
|
||||
|
||||
// These two methods are needed to check whether serialization is done correctly in tests.
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
@@ -176,11 +176,12 @@ public class OteStats {
|
||||
* Check if the {@link HistoryEntry} type matches as well as the {@link EppInput} if supplied.
|
||||
*/
|
||||
private boolean matches(HistoryEntry.Type historyType, Optional<EppInput> eppInput) {
|
||||
if (eppInputFilter.isPresent() && eppInput.isPresent()) {
|
||||
return typeFilter.test(historyType) && eppInputFilter.get().test(eppInput.get());
|
||||
} else {
|
||||
return typeFilter.test(historyType);
|
||||
if (!typeFilter.test(historyType)) {
|
||||
return false;
|
||||
}
|
||||
return eppInputFilter
|
||||
.map(filter -> eppInput.isPresent() && filter.test(eppInput.get()))
|
||||
.orElse(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,10 @@ public class BillingRecurrence extends BillingBase {
|
||||
^ instance.renewalPrice == null,
|
||||
"Renewal price can have a value if and only if the renewal price behavior is"
|
||||
+ " SPECIFIED");
|
||||
if (instance.renewalPrice != null) {
|
||||
checkArgument(
|
||||
instance.renewalPrice.isPositiveOrZero(), "SPECIFIED renewal price cannot be negative");
|
||||
}
|
||||
instance.recurrenceTimeOfYear = TimeOfYear.fromInstant(instance.eventTime);
|
||||
instance.recurrenceEndTime =
|
||||
Optional.ofNullable(instance.recurrenceEndTime).orElse(END_INSTANT);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -462,6 +462,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
|
||||
getInstance().renewalPriceBehavior.equals(RenewalPriceBehavior.SPECIFIED)
|
||||
== (getInstance().renewalPrice != null),
|
||||
"renewalPrice must be specified iff renewalPriceBehavior is SPECIFIED");
|
||||
if (getInstance().renewalPrice != null) {
|
||||
checkArgument(
|
||||
getInstance().renewalPrice.isPositiveOrZero(), "Renewal price cannot be negative");
|
||||
}
|
||||
|
||||
if (getInstance().tokenType.equals(TokenType.BULK_PRICING)) {
|
||||
checkArgument(
|
||||
|
||||
@@ -871,7 +871,10 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
}
|
||||
|
||||
public Builder setIpAddressAllowList(Iterable<CidrAddressBlock> ipAddressAllowList) {
|
||||
getInstance().ipAddressAllowList = ImmutableList.copyOf(ipAddressAllowList);
|
||||
getInstance().ipAddressAllowList =
|
||||
ipAddressAllowList == null
|
||||
? ImmutableList.of()
|
||||
: ImmutableList.copyOf(ipAddressAllowList);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1030,6 +1033,11 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
return tm().transact(() -> tm().loadAllOf(Registrar.class));
|
||||
}
|
||||
|
||||
/** Loads all registrar entities directly from the database, sorted by the given field names. */
|
||||
public static Iterable<Registrar> loadAllSorted(String... sortFields) {
|
||||
return tm().transact(() -> tm().loadAllOfSorted(Registrar.class, sortFields));
|
||||
}
|
||||
|
||||
/** Loads all registrar entities using an in-memory cache. */
|
||||
public static Iterable<Registrar> loadAllCached() {
|
||||
return CACHE_BY_REGISTRAR_ID.get().values();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -809,6 +814,55 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/** Checks the validity of the TLD object, for use during building or deserializing. */
|
||||
public void validateState() {
|
||||
checkArgument(tldStr != null, "No registry TLD specified");
|
||||
// Check for canonical form by converting to an InternetDomainName and then back.
|
||||
checkArgument(
|
||||
InternetDomainName.isValid(tldStr)
|
||||
&& tldStr.equals(InternetDomainName.from(tldStr).toString()),
|
||||
"Cannot create registry for TLD that is not a valid, canonical domain name");
|
||||
// Check the validity of all TimedTransitionProperties to ensure that they have values for
|
||||
// START_INSTANT. The setters above have already checked this for new values, but also check
|
||||
// here to catch cases where we loaded an invalid TimedTransitionProperty from the database
|
||||
// and cloned it into a new builder, to block re-building a Tld in an invalid state.
|
||||
tldStateTransitions.checkValidity();
|
||||
createBillingCostTransitions.checkValidity();
|
||||
renewBillingCostTransitions.checkValidity();
|
||||
eapFeeSchedule.checkValidity();
|
||||
// All costs must be in the expected currency.
|
||||
checkArgumentNotNull(getCurrency(), "Currency must be set");
|
||||
Predicate<Money> currencyCheck =
|
||||
(Money money) -> money.getCurrencyUnit().equals(currency) && money.isPositiveOrZero();
|
||||
checkArgument(
|
||||
currencyCheck.test(getRestoreBillingCost()),
|
||||
"Restore cost is negative or in the wrong currency");
|
||||
checkArgument(
|
||||
currencyCheck.test(getServerStatusChangeBillingCost()),
|
||||
"Server status change cost is negative or in the wrong currency");
|
||||
checkArgument(
|
||||
currencyCheck.test(getRegistryLockOrUnlockBillingCost()),
|
||||
"Registry lock/unlock cost is negative or in the wrong currency");
|
||||
checkArgument(
|
||||
getRenewBillingCostTransitions().values().stream().allMatch(currencyCheck),
|
||||
"Some renew cost(s) are negative or in the wrong currency");
|
||||
checkArgument(
|
||||
getCreateBillingCostTransitions().values().stream().allMatch(currencyCheck),
|
||||
"Some create cost(s) are negative or in the wrong currency");
|
||||
checkArgument(
|
||||
eapFeeSchedule.toValueMap().values().stream().allMatch(currencyCheck),
|
||||
"Some EAP fee cost(s) are negative or in the wrong currency'");
|
||||
checkArgumentNotNull(
|
||||
pricingEngineClassName, "All registries must have a configured pricing engine");
|
||||
checkArgument(
|
||||
dnsWriters != null && !dnsWriters.isEmpty(),
|
||||
"At least one DNS writer must be specified."
|
||||
+ " VoidDnsWriter can be used if DNS writing isn't desired");
|
||||
checkArgument(
|
||||
numDnsPublishLocks > 0,
|
||||
"Number of DNS publish locks must be positive. Use 1 for TLD-wide locks.");
|
||||
}
|
||||
|
||||
/** A builder for constructing {@link Tld} objects, since they are immutable. */
|
||||
public static class Builder extends Buildable.Builder<Tld> {
|
||||
public Builder() {}
|
||||
@@ -961,10 +1015,6 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
|
||||
public Builder setCreateBillingCostTransitions(
|
||||
ImmutableSortedMap<Instant, Money> createCostsMap) {
|
||||
checkArgumentNotNull(createCostsMap, "Create billing costs map cannot be null");
|
||||
checkArgument(
|
||||
createCostsMap.values().stream().allMatch(Money::isPositiveOrZero),
|
||||
"Create billing cost cannot be negative");
|
||||
getInstance().createBillingCostTransitions =
|
||||
TimedTransitionProperty.fromValueMap(createCostsMap);
|
||||
return this;
|
||||
@@ -1007,17 +1057,12 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
}
|
||||
|
||||
public Builder setRestoreBillingCost(Money amount) {
|
||||
checkArgument(amount.isPositiveOrZero(), "restoreBillingCost cannot be negative");
|
||||
getInstance().restoreBillingCost = amount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRenewBillingCostTransitions(
|
||||
ImmutableSortedMap<Instant, Money> renewCostsMap) {
|
||||
checkArgumentNotNull(renewCostsMap, "Renew billing costs map cannot be null");
|
||||
checkArgument(
|
||||
renewCostsMap.values().stream().allMatch(Money::isPositiveOrZero),
|
||||
"Renew billing cost cannot be negative");
|
||||
getInstance().renewBillingCostTransitions =
|
||||
TimedTransitionProperty.fromValueMap(renewCostsMap);
|
||||
return this;
|
||||
@@ -1025,10 +1070,6 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
|
||||
/** Sets the EAP fee schedule for the TLD. */
|
||||
public Builder setEapFeeSchedule(ImmutableSortedMap<Instant, Money> eapFeeSchedule) {
|
||||
checkArgumentNotNull(eapFeeSchedule, "EAP fee schedule cannot be null");
|
||||
checkArgument(
|
||||
eapFeeSchedule.values().stream().allMatch(Money::isPositiveOrZero),
|
||||
"EAP fee cannot be negative");
|
||||
getInstance().eapFeeSchedule = TimedTransitionProperty.fromValueMap(eapFeeSchedule);
|
||||
return this;
|
||||
}
|
||||
@@ -1046,14 +1087,11 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
}
|
||||
|
||||
public Builder setServerStatusChangeBillingCost(Money amount) {
|
||||
checkArgument(
|
||||
amount.isPositiveOrZero(), "Server status change billing cost cannot be negative");
|
||||
getInstance().serverStatusChangeBillingCost = amount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRegistryLockOrUnlockBillingCost(Money amount) {
|
||||
checkArgument(amount.isPositiveOrZero(), "Registry lock/unlock cost cannot be negative");
|
||||
getInstance().registryLockOrUnlockBillingCost = amount;
|
||||
return this;
|
||||
}
|
||||
@@ -1115,58 +1153,10 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
|
||||
@Override
|
||||
public Tld build() {
|
||||
final Tld instance = getInstance();
|
||||
// Pick up the name of the associated TLD from the instance object.
|
||||
String tldName = instance.tldStr;
|
||||
checkArgument(tldName != null, "No registry TLD specified");
|
||||
// Check for canonical form by converting to an InternetDomainName and then back.
|
||||
checkArgument(
|
||||
InternetDomainName.isValid(tldName)
|
||||
&& tldName.equals(InternetDomainName.from(tldName).toString()),
|
||||
"Cannot create registry for TLD that is not a valid, canonical domain name");
|
||||
// Check the validity of all TimedTransitionProperties to ensure that they have values for
|
||||
// START_INSTANT. The setters above have already checked this for new values, but also check
|
||||
// here to catch cases where we loaded an invalid TimedTransitionProperty from the database
|
||||
// and cloned it into a new builder, to block re-building a Tld in an invalid state.
|
||||
instance.tldStateTransitions.checkValidity();
|
||||
instance.createBillingCostTransitions.checkValidity();
|
||||
instance.renewBillingCostTransitions.checkValidity();
|
||||
instance.eapFeeSchedule.checkValidity();
|
||||
// All costs must be in the expected currency.
|
||||
checkArgumentNotNull(instance.getCurrency(), "Currency must be set");
|
||||
checkArgument(
|
||||
instance.getRestoreBillingCost().getCurrencyUnit().equals(instance.currency),
|
||||
"Restore cost must be in the TLD's currency");
|
||||
checkArgument(
|
||||
instance.getServerStatusChangeBillingCost().getCurrencyUnit().equals(instance.currency),
|
||||
"Server status change cost must be in the TLD's currency");
|
||||
checkArgument(
|
||||
instance.getRegistryLockOrUnlockBillingCost().getCurrencyUnit().equals(instance.currency),
|
||||
"Registry lock/unlock cost must be in the TLD's currency");
|
||||
Predicate<Money> currencyCheck =
|
||||
(Money money) -> money.getCurrencyUnit().equals(instance.currency);
|
||||
checkArgument(
|
||||
instance.getRenewBillingCostTransitions().values().stream().allMatch(currencyCheck),
|
||||
"Renew cost must be in the TLD's currency");
|
||||
checkArgument(
|
||||
instance.getCreateBillingCostTransitions().values().stream().allMatch(currencyCheck),
|
||||
"Create cost must be in the TLD's currency");
|
||||
checkArgument(
|
||||
instance.eapFeeSchedule.toValueMap().values().stream().allMatch(currencyCheck),
|
||||
"All EAP fees must be in the TLD's currency");
|
||||
checkArgumentNotNull(
|
||||
instance.pricingEngineClassName, "All registries must have a configured pricing engine");
|
||||
checkArgument(
|
||||
instance.dnsWriters != null && !instance.dnsWriters.isEmpty(),
|
||||
"At least one DNS writer must be specified."
|
||||
+ " VoidDnsWriter can be used if DNS writing isn't desired");
|
||||
// If not set explicitly, numDnsPublishLocks defaults to 1.
|
||||
instance.setDefaultNumDnsPublishLocks();
|
||||
checkArgument(
|
||||
instance.numDnsPublishLocks > 0,
|
||||
"Number of DNS publish locks must be positive. Use 1 for TLD-wide locks.");
|
||||
instance.tldStr = tldName;
|
||||
instance.tldUnicode = Idn.toUnicode(tldName);
|
||||
getInstance().setDefaultNumDnsPublishLocks();
|
||||
getInstance().validateState();
|
||||
getInstance().tldUnicode = Idn.toUnicode(getInstance().tldStr);
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,14 @@ public final class PremiumList extends BaseDomainLabelList<BigDecimal, PremiumEn
|
||||
getInstance().revisionId = revisionId;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PremiumEntry build() {
|
||||
checkArgument(getInstance().price != null, "Price must not be null");
|
||||
checkArgument(
|
||||
getInstance().price.compareTo(BigDecimal.ZERO) >= 0, "Price must not be negative");
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
+10
@@ -257,11 +257,21 @@ public class DelegatingReplicaJpaTransactionManager implements JpaTransactionMan
|
||||
return getReplica().loadAllOf(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ImmutableList<T> loadAllOfSorted(Class<T> clazz, String... sortFields) {
|
||||
return getReplica().loadAllOfSorted(clazz, sortFields);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Stream<T> loadAllOfStream(Class<T> clazz) {
|
||||
return getReplica().loadAllOfStream(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Stream<T> loadAllOfSortedStream(Class<T> clazz, String... sortFields) {
|
||||
return getReplica().loadAllOfSortedStream(clazz, sortFields);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> loadSingleton(Class<T> clazz) {
|
||||
return getReplica().loadSingleton(clazz);
|
||||
|
||||
+36
-2
@@ -76,6 +76,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -88,6 +89,16 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final Retrier retrier = new Retrier(new SystemSleeper(), 6);
|
||||
|
||||
/**
|
||||
* Strict allowlist regex for property/field names in dynamic JPQL ORDER BY clauses.
|
||||
*
|
||||
* <p>JPA and database engines forbid bind parameters (e.g. ? or :param) for schema identifiers or
|
||||
* property names in ORDER BY clauses. To prevent JPQL/SQL injection when dynamically constructing
|
||||
* sort queries, every sort field MUST be validated against this pattern before concatenation.
|
||||
*/
|
||||
private static final Pattern VALID_SORT_FIELD_PATTERN = Pattern.compile("^[a-zA-Z0-9_.]+$");
|
||||
|
||||
private static final String NESTED_TRANSACTION_MESSAGE =
|
||||
"Nested transaction detected. Try refactoring to avoid nested transactions. If unachievable,"
|
||||
+ " use reTransact() in nested transactions";
|
||||
@@ -528,15 +539,38 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
|
||||
@Override
|
||||
public <T> ImmutableList<T> loadAllOf(Class<T> clazz) {
|
||||
return loadAllOfStream(clazz).collect(toImmutableList());
|
||||
return loadAllOfSorted(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Stream<T> loadAllOfStream(Class<T> clazz) {
|
||||
return loadAllOfSortedStream(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ImmutableList<T> loadAllOfSorted(Class<T> clazz, String... sortFields) {
|
||||
return loadAllOfSortedStream(clazz, sortFields).collect(toImmutableList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Stream<T> loadAllOfSortedStream(Class<T> clazz, String... sortFields) {
|
||||
checkArgumentNotNull(clazz, "clazz must be specified");
|
||||
checkArgumentNotNull(sortFields, "sortFields must not be null");
|
||||
assertInTransaction();
|
||||
StringBuilder queryString =
|
||||
new StringBuilder(String.format("FROM %s", getEntityType(clazz).getName()));
|
||||
if (sortFields.length > 0) {
|
||||
for (String field : sortFields) {
|
||||
checkArgument(
|
||||
VALID_SORT_FIELD_PATTERN.matcher(field).matches(),
|
||||
"Invalid sort field name: %s",
|
||||
field);
|
||||
}
|
||||
queryString.append(" ORDER BY ");
|
||||
queryString.append(String.join(", ", sortFields));
|
||||
}
|
||||
return getEntityManager()
|
||||
.createQuery(String.format("FROM %s", getEntityType(clazz).getName()), clazz)
|
||||
.createQuery(queryString.toString(), clazz)
|
||||
.getResultStream()
|
||||
.map(this::detach);
|
||||
}
|
||||
|
||||
@@ -219,6 +219,14 @@ public interface TransactionManager {
|
||||
*/
|
||||
<T> ImmutableList<T> loadAllOf(Class<T> clazz);
|
||||
|
||||
/**
|
||||
* Returns a list of all entities of the given type that exist in the database, ordered by the
|
||||
* specified field names in ascending order.
|
||||
*
|
||||
* <p>The resulting list is empty if there are no entities of this type.
|
||||
*/
|
||||
<T> ImmutableList<T> loadAllOfSorted(Class<T> clazz, String... sortFields);
|
||||
|
||||
/**
|
||||
* Returns a stream of all entities of the given type that exist in the database.
|
||||
*
|
||||
@@ -226,6 +234,14 @@ public interface TransactionManager {
|
||||
*/
|
||||
<T> Stream<T> loadAllOfStream(Class<T> clazz);
|
||||
|
||||
/**
|
||||
* Returns a stream of all entities of the given type that exist in the database, ordered by the
|
||||
* specified field names in ascending order.
|
||||
*
|
||||
* <p>The resulting stream is empty if there are no entities of this type.
|
||||
*/
|
||||
<T> Stream<T> loadAllOfSortedStream(Class<T> clazz, String... sortFields);
|
||||
|
||||
/**
|
||||
* Loads the only instance of this particular class, or empty if none exists.
|
||||
*
|
||||
|
||||
@@ -24,7 +24,6 @@ import com.jcraft.jsch.SftpException;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import jakarta.inject.Inject;
|
||||
import java.io.Closeable;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
@@ -57,8 +56,7 @@ final class JSchSshSession implements Closeable {
|
||||
*
|
||||
* @throws JSchException if we fail to open the connection.
|
||||
*/
|
||||
JSchSshSession create(JSch jsch, URI uri) throws JSchException {
|
||||
RdeUploadUrl url = RdeUploadUrl.create(uri);
|
||||
JSchSshSession create(JSch jsch, RdeUploadUrl url) throws JSchException {
|
||||
logger.atInfo().log("Connecting to SSH endpoint: %s", url);
|
||||
Session session = jsch.getSession(
|
||||
url.getUser().orElse("domain-registry"),
|
||||
|
||||
@@ -312,7 +312,8 @@ public final class RdeStagingAction implements Runnable {
|
||||
jobRegion,
|
||||
new LaunchFlexTemplateRequest().setLaunchParameter(parameter))
|
||||
.execute();
|
||||
logger.atInfo().log("Got response: %s", launchResponse.getJob().toPrettyString());
|
||||
logger.atInfo().log(
|
||||
"Launched RDE staging Dataflow job: %s", launchResponse.getJob().getId());
|
||||
jobNameBuilder.add(launchResponse.getJob().getId());
|
||||
} catch (IOException e) {
|
||||
logger.atWarning().withCause(e).log("Pipeline Launch failed");
|
||||
|
||||
@@ -64,7 +64,6 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
@@ -116,11 +115,16 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
|
||||
@Inject @Config("rdeInterval") Duration interval;
|
||||
@Inject @Config("rdeUploadLockTimeout") Duration timeout;
|
||||
@Inject @Config("rdeUploadSftpCooldown") Duration sftpCooldown;
|
||||
@Inject @Config("rdeUploadUrl") URI uploadUrl;
|
||||
@Inject @Key("rdeReceiverKey") PGPPublicKey receiverKey;
|
||||
@Inject @Key("rdeSigningKey") PGPKeyPair signingKey;
|
||||
@Inject @Key("rdeStagingDecryptionKey") PGPPrivateKey stagingDecryptionKey;
|
||||
|
||||
@Inject
|
||||
@Config("rdeUploadUrl")
|
||||
RdeUploadUrl rdeUploadUrl;
|
||||
|
||||
@Inject RdeUploadAction() {}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
logger.atInfo().log("Attempting to acquire RDE upload lock for TLD '%s'.", tld);
|
||||
@@ -135,7 +139,7 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
|
||||
@Override
|
||||
public void runWithLock(Instant watermark) throws Exception {
|
||||
// If a prefix is not provided,try to determine the prefix. This should only happen when the RDE
|
||||
// upload cron job runs to catch up any un-retried (i. e. expected) RDE failures.
|
||||
// upload cron job runs to catch up any un-retried (i.e. expected) RDE failures.
|
||||
String actualPrefix =
|
||||
prefix.orElseGet(() -> findMostRecentPrefixForWatermark(watermark, bucket, tld, gcsUtils));
|
||||
logger.atInfo().log("Verifying readiness to upload the RDE deposit.");
|
||||
@@ -181,7 +185,7 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
|
||||
verifyFileExists(xmlFilename);
|
||||
verifyFileExists(xmlLengthFilename);
|
||||
verifyFileExists(reportFilename);
|
||||
logger.atInfo().log("Commencing RDE upload for TLD '%s' to '%s'.", tld, uploadUrl);
|
||||
logger.atInfo().log("Commencing RDE upload for TLD '%s' to '%s'.", tld, rdeUploadUrl);
|
||||
final long xmlLength = readXmlLength(xmlLengthFilename);
|
||||
retrier.callWithRetry(
|
||||
() -> upload(xmlFilename, xmlLength, watermark, name, nameWithoutPrefix),
|
||||
@@ -221,10 +225,10 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
|
||||
private void upload(
|
||||
BlobId xmlFile, long xmlLength, Instant watermark, String name, String nameWithoutPrefix)
|
||||
throws Exception {
|
||||
logger.atInfo().log("Uploading XML file '%s' to remote path '%s'.", xmlFile, uploadUrl);
|
||||
logger.atInfo().log("Uploading XML file '%s' to remote path '%s'.", xmlFile, rdeUploadUrl);
|
||||
try (InputStream gcsInput = gcsUtils.openInputStream(xmlFile);
|
||||
InputStream ghostrydeDecoder = Ghostryde.decoder(gcsInput, stagingDecryptionKey)) {
|
||||
try (JSchSshSession session = jschSshSessionFactory.create(lazyJsch.get(), uploadUrl);
|
||||
try (JSchSshSession session = jschSshSessionFactory.create(lazyJsch.get(), rdeUploadUrl);
|
||||
JSchSftpChannel ftpChan = session.openSftpChannel()) {
|
||||
ByteArrayOutputStream sigOut = new ByteArrayOutputStream();
|
||||
String rydeFilename = nameWithoutPrefix + ".ryde";
|
||||
|
||||
@@ -37,7 +37,7 @@ import javax.annotation.concurrent.Immutable;
|
||||
* @see RdeUploadAction
|
||||
*/
|
||||
@Immutable
|
||||
final class RdeUploadUrl implements Comparable<RdeUploadUrl> {
|
||||
public final class RdeUploadUrl implements Comparable<RdeUploadUrl> {
|
||||
|
||||
public static final Protocol SFTP = new Protocol("sftp", 22);
|
||||
private static final ImmutableMap<String, Protocol> ALLOWED_PROTOCOLS =
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -14,10 +14,14 @@
|
||||
|
||||
package google.registry.reporting.billing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static google.registry.beam.BeamUtils.createJobName;
|
||||
import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static java.time.ZoneOffset.UTC;
|
||||
|
||||
import com.google.api.services.dataflow.Dataflow;
|
||||
import com.google.api.services.dataflow.model.LaunchFlexTemplateParameter;
|
||||
@@ -29,6 +33,7 @@ import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.batch.CloudTasksUtils;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.common.Cursor;
|
||||
import google.registry.persistence.PersistenceModule;
|
||||
import google.registry.reporting.ReportingModule;
|
||||
import google.registry.request.Action;
|
||||
@@ -40,7 +45,9 @@ import google.registry.util.RegistryEnvironment;
|
||||
import jakarta.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.YearMonth;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Invokes the {@code InvoicingPipeline} beam template via the REST api, and enqueues the {@link
|
||||
@@ -107,6 +114,7 @@ public class GenerateInvoicesAction implements Runnable {
|
||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||
logger.atInfo().log("Launching invoicing pipeline for %s.", yearMonth);
|
||||
try {
|
||||
checkBillingRecurrenceCursor();
|
||||
LaunchFlexTemplateParameter parameter =
|
||||
new LaunchFlexTemplateParameter()
|
||||
.setJobName(createJobName("invoicing", clock))
|
||||
@@ -156,4 +164,20 @@ public class GenerateInvoicesAction implements Runnable {
|
||||
response.setPayload(String.format("Pipeline launch failed: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private void checkBillingRecurrenceCursor() {
|
||||
Optional<Cursor> previousCursor =
|
||||
tm().transact(() -> tm().loadByKeyIfPresent(Cursor.createGlobalVKey(RECURRING_BILLING)));
|
||||
checkState(
|
||||
previousCursor.isPresent(),
|
||||
"BillingRecurrence expansion cursor is not present. Run ExpandBillingRecurrencesAction.");
|
||||
Instant startOfNextMonth = yearMonth.plusMonths(1).atDay(1).atStartOfDay(UTC).toInstant();
|
||||
Instant previousCursorTime = previousCursor.get().getCursorTime();
|
||||
checkState(
|
||||
!previousCursorTime.isBefore(startOfNextMonth),
|
||||
"BillingRecurrence expansion cursor (%s) is before the start of the next month (%s). "
|
||||
+ "Run ExpandBillingRecurrencesAction.",
|
||||
previousCursorTime,
|
||||
startOfNextMonth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class GenerateSpec11ReportAction implements Runnable {
|
||||
jobRegion,
|
||||
new LaunchFlexTemplateRequest().setLaunchParameter(parameter))
|
||||
.execute();
|
||||
logger.atInfo().log("Got response: %s", launchResponse.getJob().toPrettyString());
|
||||
logger.atInfo().log("Launched Spec11 Dataflow job: %s", launchResponse.getJob().getId());
|
||||
String jobId = launchResponse.getJob().getId();
|
||||
if (sendEmail) {
|
||||
cloudTasksUtils.enqueue(
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+3
-4
@@ -40,10 +40,9 @@ import javax.annotation.Nullable;
|
||||
* An authentication mechanism that verifies the OIDC token.
|
||||
*
|
||||
* <p>Currently, two flavors are supported: one that checks for the OIDC token as a regular bearer
|
||||
* token, and another that checks for the OIDC token passed by IAP. In both cases, the {@link
|
||||
* AuthResult} with the highest {@link AuthLevel} possible is returned. So, if the email address for
|
||||
* which the token is minted exists both as a {@link User} and as a service account, the returned
|
||||
* {@link AuthResult} is at {@link AuthLevel#USER}.
|
||||
* token, and another that checks for the OIDC token passed by IAP. In the case where the email
|
||||
* address for which the token is minted exists both as a {link User} and as a service account, the
|
||||
* returned {@link AuthResult} is at {@link AuthLevel#APP} to avoid database lookups when possible.
|
||||
*
|
||||
* @see <a href="https://developers.google.com/identity/openid-connect/openid-connect">OpenID
|
||||
* Connect </a>
|
||||
|
||||
@@ -104,8 +104,7 @@ public final class XsrfTokenManager {
|
||||
// Reconstruct the token to verify validity.
|
||||
String reconstructedToken = encodeToken(ServerSecret.get().asBytes(), email, timestampMillis);
|
||||
if (!MessageDigest.isEqual(token.getBytes(UTF_8), reconstructedToken.getBytes(UTF_8))) {
|
||||
logger.atWarning().log(
|
||||
"Reconstructed XSRF mismatch (got != expected): %s != %s", token, reconstructedToken);
|
||||
logger.atWarning().log("Token %s didn't match expected reconstructed token", token);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -164,6 +164,8 @@ public class ConfigureTldCommand extends MutatingCommand {
|
||||
if (Boolean.TRUE.equals(breakGlass)) {
|
||||
newTld = newTld.asBuilder().setBreakglassMode(true).build();
|
||||
}
|
||||
// Enforce any restrictions, e.g. "no negative fees"
|
||||
newTld.validateState();
|
||||
stageEntityChange(oldTld, newTld);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.secdns.DomainDsData;
|
||||
import google.registry.model.host.Host;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.tld.Tlds;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.HttpException.BadRequestException;
|
||||
import google.registry.request.JsonActionRunner;
|
||||
@@ -119,6 +120,7 @@ public class GenerateZoneFilesAction implements Runnable, JsonActionRunner.JsonA
|
||||
public Map<String, Object> handleJsonRequest(Map<String, ?> json) {
|
||||
@SuppressWarnings("unchecked")
|
||||
ImmutableSet<String> tlds = ImmutableSet.copyOf((List<String>) json.get("tlds"));
|
||||
Tlds.assertTldsExist(tlds);
|
||||
Instant exportTime = Instant.parse(json.get("exportTime").toString());
|
||||
// We disallow exporting within the past 2 minutes because there might be outstanding writes.
|
||||
// We can only reliably call loadAtPointInTime at times that are UTC midnight and >
|
||||
@@ -233,15 +235,20 @@ public class GenerateZoneFilesAction implements Runnable, JsonActionRunner.JsonA
|
||||
String domainLabel = stripTld(domain.getDomainName(), domain.getTld());
|
||||
Tld tld = Tld.get(domain.getTld());
|
||||
for (Host nameserver : tm().loadByKeys(domain.getNameservers()).values()) {
|
||||
// Load the nameservers at the export time in case they've been renamed or deleted
|
||||
Host host = loadAtPointInTime(nameserver, exportTime);
|
||||
if (host == null) {
|
||||
log.atSevere().log(
|
||||
"Domain %s contained nameserver %s that didn't exist at time %s",
|
||||
domain.getRepoId(), nameserver.getRepoId(), exportTime);
|
||||
continue;
|
||||
}
|
||||
result.append(
|
||||
String.format(
|
||||
NS_FORMAT,
|
||||
domainLabel,
|
||||
tld.getDnsNsTtl()
|
||||
.orElse(dnsDefaultNsTtl)
|
||||
.toSeconds(),
|
||||
// Load the nameservers at the export time in case they've been renamed or deleted.
|
||||
loadAtPointInTime(nameserver, exportTime).getHostName()));
|
||||
tld.getDnsNsTtl().orElse(dnsDefaultNsTtl).toSeconds(),
|
||||
host.getHostName()));
|
||||
}
|
||||
for (DomainDsData dsData : domain.getDsData()) {
|
||||
result.append(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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.ui.server;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/** Filter to inject security headers, including CSP, for defense-in-depth. */
|
||||
public class CspFilter implements Filter {
|
||||
|
||||
private static final String CSP_POLICY =
|
||||
"default-src 'self'; object-src 'none'; base-uri 'self';";
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
if (response instanceof HttpServletResponse httpResponse) {
|
||||
httpResponse.setHeader("Content-Security-Policy", CSP_POLICY);
|
||||
httpResponse.setHeader("X-Content-Type-Options", "nosniff");
|
||||
httpResponse.setHeader("X-Frame-Options", "DENY");
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -101,7 +102,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
@Override
|
||||
protected void postHandler(User user) {
|
||||
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
|
||||
tm().transact(this::runPostInTransaction);
|
||||
tm().transact(() -> runPostInTransaction(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,25 +135,33 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
tm().transact(this::runDeleteInTransaction);
|
||||
}
|
||||
|
||||
private void runPostInTransaction() throws IOException {
|
||||
private void runPostInTransaction(User callingUser) throws IOException {
|
||||
validateRequestParams();
|
||||
if (!tm().exists(VKey.create(User.class, this.userData.get().emailAddress))) {
|
||||
this.runCreate();
|
||||
} else {
|
||||
this.runAppendUserToExistingRegistrar();
|
||||
this.runAppendUserToExistingRegistrar(callingUser);
|
||||
}
|
||||
}
|
||||
|
||||
private void runAppendUserToExistingRegistrar() {
|
||||
private void runAppendUserToExistingRegistrar(User callingUser) {
|
||||
ImmutableList<User> allRegistrarUsers = getAllRegistrarUsers(registrarId);
|
||||
if (allRegistrarUsers.size() >= 4) {
|
||||
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");
|
||||
}
|
||||
for (String existingRegistrarId : userToAppend.getUserRoles().getRegistrarRoles().keySet()) {
|
||||
checkPermission(callingUser, existingRegistrarId, ConsolePermission.MANAGE_USERS);
|
||||
}
|
||||
|
||||
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 +173,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()) {
|
||||
@@ -250,10 +266,15 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
return;
|
||||
}
|
||||
|
||||
User userToUpdate = verifyUserExists(this.userData.get().emailAddress);
|
||||
if (userToUpdate.getUserRoles().isAdmin()
|
||||
|| !userToUpdate.getUserRoles().getGlobalRole().equals(GlobalRole.NONE)) {
|
||||
throw new BadRequestException(
|
||||
"Cannot update a global administrator or user with a global role");
|
||||
}
|
||||
|
||||
updateUserRegistrarRoles(
|
||||
this.userData.get().emailAddress,
|
||||
registrarId,
|
||||
requestRoleToAllowedRoles(this.userData.get().role));
|
||||
userToUpdate, registrarId, requestRoleToAllowedRoles(this.userData.get().role));
|
||||
|
||||
sendConfirmationEmail(registrarId, this.userData.get().emailAddress, "Updated user");
|
||||
consoleApiParams.response().setStatus(SC_OK);
|
||||
@@ -297,9 +318,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()
|
||||
|
||||
+18
-7
@@ -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) {
|
||||
|
||||
+12
-2
@@ -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())) {
|
||||
|
||||
@@ -59,6 +59,7 @@ public class RegistrarsAction extends ConsoleApiAction {
|
||||
"""
|
||||
SELECT * FROM "Registrar"
|
||||
WHERE registrar_id in :registrarIds
|
||||
ORDER BY registrar_name ASC, registrar_id ASC
|
||||
""";
|
||||
static final String PATH = "/console-api/registrars";
|
||||
private final Optional<Registrar> registrar;
|
||||
@@ -83,7 +84,7 @@ public class RegistrarsAction extends ConsoleApiAction {
|
||||
ImmutableSet<Registrar.Type> allowedRegistrarTypes =
|
||||
user.getUserRoles().isAdmin() ? TYPES_ALLOWED_FOR_ADMINS : TYPES_ALLOWED_FOR_USERS;
|
||||
ImmutableList<Registrar> registrars =
|
||||
Streams.stream(Registrar.loadAll())
|
||||
Streams.stream(Registrar.loadAllSorted("registrarName", "registrarId"))
|
||||
.filter(r -> allowedRegistrarTypes.contains(r.getType()))
|
||||
.collect(ImmutableList.toImmutableList());
|
||||
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(registrars));
|
||||
|
||||
+12
@@ -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);
|
||||
|
||||
|
||||
+1
-1
@@ -87,6 +87,6 @@ public abstract class ConsoleDomainActionType {
|
||||
}
|
||||
|
||||
private String replaceValue(String xml, String key, String value) {
|
||||
return xml.replaceAll("%" + key + "%", XML_ESCAPER.escape(value));
|
||||
return xml.replace("%" + key + "%", XML_ESCAPER.escape(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ import javax.xml.XMLConstants;
|
||||
import javax.xml.stream.XMLInputFactory;
|
||||
import javax.xml.stream.XMLStreamException;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Result;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerConfigurationException;
|
||||
import javax.xml.transform.TransformerException;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
@@ -62,7 +62,7 @@ public class XmlTransformer {
|
||||
private static final String SYSTEM_ID = "<default system id>";
|
||||
|
||||
/** A transformer factory for the {@link #prettyPrint} method. */
|
||||
private static final TransformerFactory transformerFactory = TransformerFactory.newInstance();
|
||||
private static final TransformerFactory transformerFactory = createTransformerFactory();
|
||||
|
||||
/** A {@link JAXBContext} (thread-safe) to use for marshaling and unmarshaling. */
|
||||
private final JAXBContext jaxbContext;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -152,21 +156,24 @@ public class XmlTransformer {
|
||||
// Plain old parsing exceptions have a SAXParseException with no further cause.
|
||||
if (e.getLinkedException() instanceof SAXParseException sae
|
||||
&& e.getLinkedException().getCause() == null) {
|
||||
throw new XmlException(String.format(
|
||||
"Syntax error at line %d, column %d: %s",
|
||||
sae.getLineNumber(),
|
||||
sae.getColumnNumber(),
|
||||
nullToEmpty(sae.getMessage()).replaceAll(""", "")));
|
||||
throw new XmlException(
|
||||
String.format(
|
||||
"Syntax error at line %d, column %d: %s",
|
||||
sae.getLineNumber(),
|
||||
sae.getColumnNumber(),
|
||||
nullToEmpty(sae.getMessage()).replace(""", "")));
|
||||
}
|
||||
// These get thrown for attempted XXE attacks.
|
||||
if (e.getLinkedException() instanceof XMLStreamException xse) {
|
||||
throw new XmlException(String.format(
|
||||
"Syntax error at line %d, column %d: %s",
|
||||
xse.getLocation().getLineNumber(),
|
||||
xse.getLocation().getColumnNumber(),
|
||||
nullToEmpty(xse.getMessage())
|
||||
.replaceAll("^.*\nMessage: ", "") // Strip an ugly prefix from XMLStreamException.
|
||||
.replaceAll(""", "")));
|
||||
throw new XmlException(
|
||||
String.format(
|
||||
"Syntax error at line %d, column %d: %s",
|
||||
xse.getLocation().getLineNumber(),
|
||||
xse.getLocation().getColumnNumber(),
|
||||
nullToEmpty(xse.getMessage())
|
||||
.replaceAll(
|
||||
"^.*\nMessage: ", "") // Strip an ugly prefix from XMLStreamException.
|
||||
.replace(""", "")));
|
||||
}
|
||||
throw new XmlException(e);
|
||||
} catch (JAXBException | XMLStreamException | IOException e) {
|
||||
@@ -226,27 +233,6 @@ public class XmlTransformer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and streams {@code root} as characters, always using strict validation.
|
||||
*
|
||||
* <p>The root object must be annotated with {@link jakarta.xml.bind.annotation.XmlRootElement}.
|
||||
* This method will verify that your object strictly conforms to {@link #schema}. Because the
|
||||
* output is streamed, {@link XmlException} will most likely be thrown <i>after</i> output has
|
||||
* been written.
|
||||
*
|
||||
* @param root the object to write
|
||||
* @param result to write the output to
|
||||
* @throws XmlException to rethrow {@link JAXBException}.
|
||||
*/
|
||||
public void marshalStrict(Object root, Result result) throws XmlException {
|
||||
try {
|
||||
getMarshaller(schema, ImmutableMap.of())
|
||||
.marshal(checkNotNull(root, "root"), checkNotNull(result, "result"));
|
||||
} catch (JAXBException e) {
|
||||
throw new XmlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns new instance of {@link XmlFragmentMarshaller}. */
|
||||
public XmlFragmentMarshaller createFragmentMarshaller() {
|
||||
return new XmlFragmentMarshaller(jaxbContext, schema);
|
||||
@@ -323,4 +309,14 @@ public class XmlTransformer {
|
||||
public static String prettyPrint(byte[] xmlBytes) {
|
||||
return prettyPrint(new String(xmlBytes, UTF_8));
|
||||
}
|
||||
|
||||
private static TransformerFactory createTransformerFactory() {
|
||||
TransformerFactory transformerFactory = TransformerFactory.newInstance();
|
||||
try {
|
||||
transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
} catch (TransformerConfigurationException e) {
|
||||
throw new RuntimeException(e); // this should never happen
|
||||
}
|
||||
return transformerFactory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ JOIN Registrar r ON b.clientId = r.registrarId
|
||||
JOIN Domain d ON b.domainRepoId = d.repoId
|
||||
JOIN Tld t ON t.tldStr = d.tld
|
||||
LEFT JOIN BillingCancellation c ON b.id = c.billingEvent
|
||||
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence
|
||||
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence AND b.billingTime = cr.billingTime
|
||||
WHERE r.billingAccountMap IS NOT NULL
|
||||
AND r.type = 'REAL'
|
||||
AND t.invoicingEnabled IS TRUE
|
||||
|
||||
@@ -229,7 +229,22 @@ class InvoicingPipelineTest {
|
||||
3,
|
||||
"USD",
|
||||
20.5,
|
||||
""));
|
||||
""),
|
||||
google.registry.beam.billing.BillingEvent.create(
|
||||
17,
|
||||
Instant.parse("2017-10-04T00:00:00Z"),
|
||||
Instant.parse("2017-10-04T00:00:00Z"),
|
||||
"theRegistrar",
|
||||
"234",
|
||||
"",
|
||||
"test",
|
||||
"RENEW",
|
||||
"recurrence-collision.test",
|
||||
"REPO-ID",
|
||||
3,
|
||||
"USD",
|
||||
20.5,
|
||||
"SYNTHETIC"));
|
||||
|
||||
private static final ImmutableMap<String, ImmutableList<String>> EXPECTED_DETAILED_REPORT_MAP =
|
||||
ImmutableMap.of(
|
||||
@@ -239,6 +254,8 @@ class InvoicingPipelineTest {
|
||||
+ "test,RENEW,mydomain2.test,REPO-ID,3,USD,20.50,",
|
||||
"1,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,theRegistrar,234,,"
|
||||
+ "test,RENEW,mydomain.test,REPO-ID,3,USD,20.50,",
|
||||
"17,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,theRegistrar,234,,"
|
||||
+ "test,RENEW,recurrence-collision.test,REPO-ID,3,USD,20.50,",
|
||||
"7,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,theRegistrar,234,,"
|
||||
+ "test,SERVER_STATUS,update-prohibited.test,REPO-ID,0,USD,20.00,",
|
||||
"6,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,theRegistrar,234,,"
|
||||
@@ -264,7 +281,7 @@ class InvoicingPipelineTest {
|
||||
|
||||
private static final ImmutableList<String> EXPECTED_INVOICE_OUTPUT =
|
||||
ImmutableList.of(
|
||||
"2017-10-01,2020-09-30,234,61.50,USD,10125,1,PURCHASE,,3,"
|
||||
"2017-10-01,2020-09-30,234,82.00,USD,10125,1,PURCHASE,,4,"
|
||||
+ "RENEW | TLD: test | TERM: 3-year,20.50,USD,",
|
||||
"2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,,1,"
|
||||
+ "CREATE | TLD: hello | TERM: 5-year,70.00,JPY,",
|
||||
@@ -398,7 +415,8 @@ JOIN Registrar r ON b.clientId = r.registrarId
|
||||
JOIN Domain d ON b.domainRepoId = d.repoId
|
||||
JOIN Tld t ON t.tldStr = d.tld
|
||||
LEFT JOIN BillingCancellation c ON b.id = c.billingEvent
|
||||
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence
|
||||
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence AND \
|
||||
b.billingTime = cr.billingTime
|
||||
WHERE r.billingAccountMap IS NOT NULL
|
||||
AND r.type = 'REAL'
|
||||
AND t.invoicingEnabled IS TRUE
|
||||
@@ -607,6 +625,51 @@ AND cr.id IS NULL
|
||||
Instant.parse("2017-10-04T00:00:00.0Z"),
|
||||
Instant.parse("2017-10-02T00:00:00.0Z"));
|
||||
persistBillingEvent(16, domain15, registrar11, Reason.RENEW, 3, Money.of(USD, 20.5));
|
||||
|
||||
// Add a billing event in Year 1 and a cancellation in Year 2 for the same recurrence.
|
||||
// The Year 1 event should NOT be cancelled.
|
||||
Domain domain17 = persistActiveDomain("recurrence-collision.test");
|
||||
DomainHistory domainHistoryCollision = persistDomainHistory(domain17, registrar1);
|
||||
|
||||
BillingRecurrence billingRecurrenceCollision =
|
||||
new BillingRecurrence()
|
||||
.asBuilder()
|
||||
.setRegistrarId(registrar1.getRegistrarId())
|
||||
.setRecurrenceEndTime(END_INSTANT)
|
||||
.setId(100)
|
||||
.setDomainHistory(domainHistoryCollision)
|
||||
.setTargetId(domain17.getDomainName())
|
||||
.setEventTime(Instant.parse("2017-10-04T00:00:00.0Z"))
|
||||
.setReason(Reason.RENEW)
|
||||
.build();
|
||||
persistResource(billingRecurrenceCollision);
|
||||
|
||||
// Year 1 Billing Event (October 2017)
|
||||
BillingEvent billingEventYear1 =
|
||||
persistBillingEvent(17, domain17, registrar1, Reason.RENEW, 3, Money.of(USD, 20.5));
|
||||
billingEventYear1 =
|
||||
billingEventYear1
|
||||
.asBuilder()
|
||||
.setCancellationMatchingBillingEvent(billingRecurrenceCollision)
|
||||
.setFlags(ImmutableSet.of(Flag.SYNTHETIC))
|
||||
.setSyntheticCreationTime(Instant.parse("2017-10-03T00:00:00.0Z"))
|
||||
.build();
|
||||
persistResource(billingEventYear1);
|
||||
|
||||
// Year 2 Billing Cancellation (October 2018)
|
||||
BillingCancellation cancellationYear2 =
|
||||
new BillingCancellation()
|
||||
.asBuilder()
|
||||
.setId(101)
|
||||
.setRegistrarId(registrar1.getRegistrarId())
|
||||
.setEventTime(Instant.parse("2018-10-05T00:00:00.0Z"))
|
||||
.setBillingTime(Instant.parse("2018-10-04T00:00:00.0Z"))
|
||||
.setBillingRecurrence(billingRecurrenceCollision.createVKey())
|
||||
.setTargetId(domain17.getDomainName())
|
||||
.setReason(Reason.RENEW)
|
||||
.setDomainHistory(domainHistoryCollision)
|
||||
.build();
|
||||
persistResource(cancellationYear2);
|
||||
}
|
||||
|
||||
private static DomainHistory persistDomainHistory(Domain domain, Registrar registrar) {
|
||||
|
||||
@@ -24,11 +24,14 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import google.registry.testing.FakeClock;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -100,4 +103,39 @@ public class MultilayerDomainCacheTest {
|
||||
verify(cacheMetrics).recordLookup("Domain", CacheMetrics.CacheHitType.MISS_NONEXISTENT);
|
||||
verifyNoMoreInteractions(cacheMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_filtersOutDeletedDomain() {
|
||||
Domain domain =
|
||||
persistActiveDomain("example.tld")
|
||||
.asBuilder()
|
||||
.setDeletionTime(clock.now().plus(Duration.ofDays(1)))
|
||||
.build();
|
||||
when(jedisClient.get(Domain.class, "example.tld")).thenReturn(Optional.of(domain));
|
||||
assertThat(cache.loadByDomainName("example.tld")).hasValue(domain);
|
||||
|
||||
clock.advanceBy(Duration.ofDays(2));
|
||||
assertThat(cache.loadByDomainName("example.tld")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_projectsToCurrentTime() {
|
||||
Domain domain =
|
||||
persistActiveDomain("example.tld")
|
||||
.asBuilder()
|
||||
.addGracePeriod(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.ADD,
|
||||
"example.tld",
|
||||
clock.now().plus(Duration.ofDays(5)),
|
||||
"TheRegistrar",
|
||||
null))
|
||||
.build();
|
||||
when(jedisClient.get(Domain.class, "example.tld")).thenReturn(Optional.of(domain));
|
||||
assertThat(cache.loadByDomainName("example.tld").get().getGracePeriods())
|
||||
.containsExactlyElementsIn(domain.getGracePeriods());
|
||||
|
||||
clock.advanceBy(Duration.ofDays(10));
|
||||
assertThat(cache.loadByDomainName("example.tld").get().getGracePeriods()).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import google.registry.model.host.Host;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import google.registry.testing.FakeClock;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -38,12 +40,13 @@ public class MultilayerHostCacheTest {
|
||||
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
|
||||
|
||||
private final SimplifiedJedisClient jedisClient = mock(SimplifiedJedisClient.class);
|
||||
private final FakeClock clock = new FakeClock();
|
||||
private final CacheMetrics cacheMetrics = mock(CacheMetrics.class);
|
||||
private MultilayerHostCache cache;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
cache = new MultilayerHostCache(jedisClient, cacheMetrics);
|
||||
cache = new MultilayerHostCache(jedisClient, clock, cacheMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -80,4 +83,18 @@ public class MultilayerHostCacheTest {
|
||||
verify(cacheMetrics).recordLookup("Host", CacheMetrics.CacheHitType.MISS_NONEXISTENT);
|
||||
verifyNoMoreInteractions(cacheMetrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_filtersOutDeletedHost() {
|
||||
Host host =
|
||||
persistActiveHost("ns1.example.tld")
|
||||
.asBuilder()
|
||||
.setDeletionTime(clock.now().plus(Duration.ofDays(1)))
|
||||
.build();
|
||||
when(jedisClient.get(Host.class, host.getRepoId())).thenReturn(Optional.of(host));
|
||||
assertThat(cache.loadByRepoId(host.getRepoId())).hasValue(host);
|
||||
|
||||
clock.advanceBy(Duration.ofDays(2));
|
||||
assertThat(cache.loadByRepoId(host.getRepoId())).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,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;
|
||||
@@ -144,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;
|
||||
@@ -1937,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,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;
|
||||
@@ -90,6 +89,7 @@ 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;
|
||||
@@ -283,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 {
|
||||
|
||||
@@ -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>");
|
||||
}
|
||||
}
|
||||
@@ -15,27 +15,40 @@
|
||||
package google.registry.gcs;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.cloud.storage.BlobId;
|
||||
import com.google.cloud.storage.BlobInfo;
|
||||
import com.google.cloud.storage.Bucket;
|
||||
import com.google.cloud.storage.BucketInfo;
|
||||
import com.google.cloud.storage.Storage;
|
||||
import com.google.cloud.storage.StorageOptions;
|
||||
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.testing.SystemPropertyExtension;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit tests for {@link GcsUtilsTest}. */
|
||||
class GcsUtilsTest {
|
||||
|
||||
@RegisterExtension
|
||||
final SystemPropertyExtension systemPropertyExtension = new SystemPropertyExtension();
|
||||
|
||||
private GcsUtils gcsUtils = new GcsUtils(LocalStorageHelper.getOptions());
|
||||
|
||||
private String bucket = "my-bucket";
|
||||
@@ -43,9 +56,18 @@ class GcsUtilsTest {
|
||||
private BlobId blobId = BlobId.of(bucket, filename);
|
||||
private ImmutableMap<String, String> metadata = ImmutableMap.of("key1", "val1", "Key2", "val2");
|
||||
private final byte[] bytes = new byte[] {'a', 'b', 'c'};
|
||||
private RegistryEnvironment previousEnvironment;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {}
|
||||
void beforeEach() {
|
||||
previousEnvironment = RegistryEnvironment.get();
|
||||
GcsUtils.clearPapCache();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() {
|
||||
previousEnvironment.setup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerialization_testStorage() throws Exception {
|
||||
@@ -111,6 +133,88 @@ class GcsUtilsTest {
|
||||
assertThat(gcsUtils.existsAndNotEmpty(blobId)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifyPublicAccessPrevention_enforced() {
|
||||
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
|
||||
Storage mockStorage = mock(Storage.class);
|
||||
StorageOptions mockOptions = mock(StorageOptions.class);
|
||||
when(mockOptions.getService()).thenReturn(mockStorage);
|
||||
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockStorage.get("my-bucket")).thenReturn(mockBucket);
|
||||
|
||||
BucketInfo.IamConfiguration mockIamConfig = mock(BucketInfo.IamConfiguration.class);
|
||||
when(mockBucket.getIamConfiguration()).thenReturn(mockIamConfig);
|
||||
when(mockIamConfig.getPublicAccessPrevention())
|
||||
.thenReturn(BucketInfo.PublicAccessPrevention.ENFORCED);
|
||||
|
||||
GcsUtils utils = new GcsUtils(mockOptions);
|
||||
utils.verifyPublicAccessPrevention("my-bucket");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifyPublicAccessPrevention_notCheckedInNonProd() {
|
||||
RegistryEnvironment.SANDBOX.setup(systemPropertyExtension);
|
||||
Storage mockStorage = mock(Storage.class);
|
||||
StorageOptions mockOptions = mock(StorageOptions.class);
|
||||
when(mockOptions.getService()).thenReturn(mockStorage);
|
||||
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockStorage.get("my-bucket")).thenReturn(mockBucket);
|
||||
|
||||
BucketInfo.IamConfiguration mockIamConfig = mock(BucketInfo.IamConfiguration.class);
|
||||
when(mockBucket.getIamConfiguration()).thenReturn(mockIamConfig);
|
||||
when(mockIamConfig.getPublicAccessPrevention())
|
||||
.thenReturn(BucketInfo.PublicAccessPrevention.INHERITED);
|
||||
|
||||
GcsUtils utils = new GcsUtils(mockOptions);
|
||||
// no exception thrown even though PAP isn't enforced
|
||||
utils.verifyPublicAccessPrevention("my-bucket");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifyPublicAccessPrevention_notEnforced() {
|
||||
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
|
||||
Storage mockStorage = mock(Storage.class);
|
||||
StorageOptions mockOptions = mock(StorageOptions.class);
|
||||
when(mockOptions.getService()).thenReturn(mockStorage);
|
||||
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockStorage.get("my-bucket")).thenReturn(mockBucket);
|
||||
|
||||
BucketInfo.IamConfiguration mockIamConfig = mock(BucketInfo.IamConfiguration.class);
|
||||
when(mockBucket.getIamConfiguration()).thenReturn(mockIamConfig);
|
||||
when(mockIamConfig.getPublicAccessPrevention())
|
||||
.thenReturn(BucketInfo.PublicAccessPrevention.INHERITED);
|
||||
|
||||
GcsUtils utils = new GcsUtils(mockOptions);
|
||||
|
||||
IllegalStateException thrown =
|
||||
assertThrows(
|
||||
IllegalStateException.class, () -> utils.verifyPublicAccessPrevention("my-bucket"));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Public Access Prevention is not enforced on bucket my-bucket");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifyPublicAccessPrevention_nonexistentBucket() {
|
||||
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
|
||||
Storage mockStorage = mock(Storage.class);
|
||||
StorageOptions mockOptions = mock(StorageOptions.class);
|
||||
when(mockOptions.getService()).thenReturn(mockStorage);
|
||||
|
||||
when(mockStorage.get("nonexistent-bucket")).thenReturn(null);
|
||||
|
||||
GcsUtils utils = new GcsUtils(mockOptions);
|
||||
|
||||
IllegalStateException thrown =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> utils.verifyPublicAccessPrevention("nonexistent-bucket"));
|
||||
assertThat(thrown).hasMessageThat().contains("Bucket nonexistent-bucket does not exist");
|
||||
}
|
||||
|
||||
private static byte[] serialize(Object object) throws IOException {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
ObjectOutputStream oos = new ObjectOutputStream(baos);
|
||||
|
||||
@@ -16,10 +16,17 @@ package google.registry.model;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatabaseHelper.persistPremiumList;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static org.joda.money.CurrencyUnit.USD;
|
||||
|
||||
import google.registry.model.OteStats.StatType;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.reporting.HistoryEntry.Type;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import java.time.Instant;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
@@ -60,39 +67,40 @@ public final class OteStatsTest {
|
||||
OteStats stats = OteStats.getFromRegistrar("blobio");
|
||||
String expected =
|
||||
"""
|
||||
contact creates: 0
|
||||
contact deletes: 0
|
||||
contact transfer approves: 0
|
||||
contact transfer cancels: 0
|
||||
contact transfer rejects: 0
|
||||
contact transfer requests: 0
|
||||
contact updates: 0
|
||||
domain autorenews: 0
|
||||
domain creates: 5
|
||||
domain creates ascii: 4
|
||||
domain creates idn: 1
|
||||
domain creates start date sunrise: 1
|
||||
domain creates with claims notice: 1
|
||||
domain creates with fee: 1
|
||||
domain creates with sec dns: 1
|
||||
domain creates without sec dns: 4
|
||||
domain deletes: 1
|
||||
domain renews: 0
|
||||
domain restores: 1
|
||||
domain transfer approves: 1
|
||||
domain transfer cancels: 1
|
||||
domain transfer rejects: 1
|
||||
domain transfer requests: 1
|
||||
domain updates: 1
|
||||
domain updates with sec dns: 1
|
||||
domain updates without sec dns: 0
|
||||
host creates: 1
|
||||
host creates external: 0
|
||||
host creates subordinate: 1
|
||||
host deletes: 1
|
||||
host updates: 1
|
||||
unclassified flows: 0
|
||||
TOTAL: 30""";
|
||||
contact creates: 0
|
||||
contact deletes: 0
|
||||
contact transfer approves: 0
|
||||
contact transfer cancels: 0
|
||||
contact transfer rejects: 0
|
||||
contact transfer requests: 0
|
||||
contact updates: 0
|
||||
domain autorenews: 0
|
||||
domain creates: 5
|
||||
domain creates ascii: 4
|
||||
domain creates idn: 1
|
||||
domain creates start date sunrise: 1
|
||||
domain creates with claims notice: 1
|
||||
domain creates with fee: 1
|
||||
domain creates with sec dns: 1
|
||||
domain creates without sec dns: 4
|
||||
domain deletes: 1
|
||||
domain renews: 0
|
||||
domain restores: 1
|
||||
domain transfer approves: 1
|
||||
domain transfer cancels: 1
|
||||
domain transfer rejects: 1
|
||||
domain transfer requests: 1
|
||||
domain updates: 1
|
||||
domain updates with sec dns: 1
|
||||
domain updates without sec dns: 0
|
||||
host creates: 1
|
||||
host creates external: 0
|
||||
host creates subordinate: 1
|
||||
host deletes: 1
|
||||
host updates: 1
|
||||
unclassified flows: 0
|
||||
TOTAL: 30\
|
||||
""";
|
||||
assertThat(stats.toString()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@@ -102,39 +110,72 @@ public final class OteStatsTest {
|
||||
OteStats stats = OteStats.getFromRegistrar("blobio");
|
||||
String expected =
|
||||
"""
|
||||
contact creates: 0
|
||||
contact deletes: 0
|
||||
contact transfer approves: 0
|
||||
contact transfer cancels: 0
|
||||
contact transfer rejects: 0
|
||||
contact transfer requests: 0
|
||||
contact updates: 0
|
||||
domain autorenews: 0
|
||||
domain creates: 4
|
||||
domain creates ascii: 4
|
||||
domain creates idn: 0
|
||||
domain creates start date sunrise: 1
|
||||
domain creates with claims notice: 1
|
||||
domain creates with fee: 1
|
||||
domain creates with sec dns: 1
|
||||
domain creates without sec dns: 3
|
||||
domain deletes: 1
|
||||
domain renews: 0
|
||||
domain restores: 0
|
||||
domain transfer approves: 1
|
||||
domain transfer cancels: 1
|
||||
domain transfer rejects: 1
|
||||
domain transfer requests: 1
|
||||
domain updates: 1
|
||||
domain updates with sec dns: 1
|
||||
domain updates without sec dns: 0
|
||||
host creates: 1
|
||||
host creates external: 0
|
||||
host creates subordinate: 1
|
||||
host deletes: 0
|
||||
host updates: 10
|
||||
unclassified flows: 0
|
||||
TOTAL: 34""";
|
||||
contact creates: 0
|
||||
contact deletes: 0
|
||||
contact transfer approves: 0
|
||||
contact transfer cancels: 0
|
||||
contact transfer rejects: 0
|
||||
contact transfer requests: 0
|
||||
contact updates: 0
|
||||
domain autorenews: 0
|
||||
domain creates: 4
|
||||
domain creates ascii: 4
|
||||
domain creates idn: 0
|
||||
domain creates start date sunrise: 1
|
||||
domain creates with claims notice: 1
|
||||
domain creates with fee: 1
|
||||
domain creates with sec dns: 1
|
||||
domain creates without sec dns: 3
|
||||
domain deletes: 1
|
||||
domain renews: 0
|
||||
domain restores: 0
|
||||
domain transfer approves: 1
|
||||
domain transfer cancels: 1
|
||||
domain transfer rejects: 1
|
||||
domain transfer requests: 1
|
||||
domain updates: 1
|
||||
domain updates with sec dns: 1
|
||||
domain updates without sec dns: 0
|
||||
host creates: 1
|
||||
host creates external: 0
|
||||
host creates subordinate: 1
|
||||
host deletes: 0
|
||||
host updates: 10
|
||||
unclassified flows: 0
|
||||
TOTAL: 34\
|
||||
""";
|
||||
assertThat(stats.toString()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDomainCreateWithoutXmlBytes_doesNotSatisfyComplexRequirements() throws Exception {
|
||||
persistPremiumList("default_sandbox_list", USD, "sandbox,USD 1000");
|
||||
OteAccountBuilder.forRegistrarId("blobio").buildAndPersist();
|
||||
String oteAccount1 = "blobio-1";
|
||||
Instant now = Instant.parse("2026-06-25T10:00:00Z");
|
||||
|
||||
// Persist a DOMAIN_CREATE history entry without XML bytes
|
||||
persistResource(
|
||||
new DomainHistory.Builder()
|
||||
.setDomain(persistActiveDomain("example.tld"))
|
||||
.setRegistrarId(oteAccount1)
|
||||
.setType(Type.DOMAIN_CREATE)
|
||||
.setXmlBytes(null) // explicitly null
|
||||
.setModificationTime(now)
|
||||
.build());
|
||||
|
||||
OteStats stats = OteStats.getFromRegistrar("blobio");
|
||||
|
||||
// It should count towards the basic DOMAIN_CREATES
|
||||
assertThat(stats.getCount(StatType.DOMAIN_CREATES)).isEqualTo(1);
|
||||
|
||||
// It should NOT count towards any stat type that requires EPP input filtering
|
||||
assertThat(stats.getCount(StatType.DOMAIN_CREATES_ASCII)).isEqualTo(0);
|
||||
assertThat(stats.getCount(StatType.DOMAIN_CREATES_IDN)).isEqualTo(0);
|
||||
assertThat(stats.getCount(StatType.DOMAIN_CREATES_START_DATE_SUNRISE)).isEqualTo(0);
|
||||
assertThat(stats.getCount(StatType.DOMAIN_CREATES_WITH_CLAIMS_NOTICE)).isEqualTo(0);
|
||||
assertThat(stats.getCount(StatType.DOMAIN_CREATES_WITH_FEE)).isEqualTo(0);
|
||||
assertThat(stats.getCount(StatType.DOMAIN_CREATES_WITH_SEC_DNS)).isEqualTo(0);
|
||||
assertThat(stats.getCount(StatType.DOMAIN_CREATES_WITHOUT_SEC_DNS)).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,4 +890,23 @@ public class BillingBaseTest extends EntityTestCase {
|
||||
"Renewal price can have a value if and only if the "
|
||||
+ "renewal price behavior is SPECIFIED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_buildWithSpecifiedRenewalBehavior_negativeRenewalPrice() {
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
new BillingRecurrence.Builder()
|
||||
.setDomainHistory(domainHistory)
|
||||
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
|
||||
.setReason(Reason.RENEW)
|
||||
.setEventTime(plusYears(now, 1))
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.setRenewalPrice(Money.of(USD, -100))
|
||||
.setRecurrenceEndTime(END_INSTANT)
|
||||
.build()))
|
||||
.hasMessageThat()
|
||||
.isEqualTo("SPECIFIED renewal price cannot be negative");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -612,6 +612,22 @@ public class AllocationTokenTest extends EntityTestCase {
|
||||
.isEqualTo("renewalPrice must be specified iff renewalPriceBehavior is SPECIFIED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuild_negativeRenewalPrice() {
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
new AllocationToken.Builder()
|
||||
.setToken("abc")
|
||||
.setTokenType(SINGLE_USE)
|
||||
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
|
||||
.setRenewalPrice(Money.of(CurrencyUnit.USD, -5))
|
||||
.build()))
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Renewal price cannot be negative");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuild_registrationBehaviors() {
|
||||
createTld("tld");
|
||||
|
||||
@@ -257,7 +257,8 @@ public final class TldTest extends EntityTestCase {
|
||||
.asBuilder()
|
||||
.setCreateBillingCostTransitions(createCostTransitions)
|
||||
.build());
|
||||
assertThat(thrown.getMessage()).isEqualTo("Create billing cost cannot be negative");
|
||||
assertThat(thrown.getMessage())
|
||||
.isEqualTo("Some create cost(s) are negative or in the wrong currency");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -614,8 +615,11 @@ public final class TldTest extends EntityTestCase {
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setRenewBillingCostTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Money.of(USD, -42))));
|
||||
assertThat(thrown).hasMessageThat().contains("billing cost cannot be negative");
|
||||
ImmutableSortedMap.of(START_INSTANT, Money.of(USD, -42)))
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Some renew cost(s) are negative or in the wrong currency");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -623,8 +627,10 @@ public final class TldTest extends EntityTestCase {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> Tld.get("tld").asBuilder().setRestoreBillingCost(Money.of(USD, -42)));
|
||||
assertThat(thrown).hasMessageThat().contains("restoreBillingCost cannot be negative");
|
||||
() -> Tld.get("tld").asBuilder().setRestoreBillingCost(Money.of(USD, -42)).build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Restore cost is negative or in the wrong currency");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -652,8 +658,14 @@ public final class TldTest extends EntityTestCase {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> Tld.get("tld").asBuilder().setServerStatusChangeBillingCost(Money.of(USD, -42)));
|
||||
assertThat(thrown).hasMessageThat().contains("billing cost cannot be negative");
|
||||
() ->
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setServerStatusChangeBillingCost(Money.of(USD, -42))
|
||||
.build());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Server status change cost is negative or in the wrong currency");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -667,7 +679,9 @@ public final class TldTest extends EntityTestCase {
|
||||
.setRenewBillingCostTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Money.of(EUR, 42)))
|
||||
.build());
|
||||
assertThat(thrown).hasMessageThat().contains("cost must be in the TLD's currency");
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Some renew cost(s) are negative or in the wrong currency");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -681,7 +695,7 @@ public final class TldTest extends EntityTestCase {
|
||||
.setCreateBillingCostTransitions(
|
||||
ImmutableSortedMap.of(START_INSTANT, Money.of(EUR, 42)))
|
||||
.build());
|
||||
assertThat(thrown).hasMessageThat().contains("cost must be in the TLD's currency");
|
||||
assertThat(thrown).hasMessageThat().contains("negative or in the wrong currency");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -690,7 +704,7 @@ public final class TldTest extends EntityTestCase {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> Tld.get("tld").asBuilder().setRestoreBillingCost(Money.of(EUR, 42)).build());
|
||||
assertThat(thrown).hasMessageThat().contains("cost must be in the TLD's currency");
|
||||
assertThat(thrown).hasMessageThat().contains("negative or in the wrong currency");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -703,7 +717,7 @@ public final class TldTest extends EntityTestCase {
|
||||
.asBuilder()
|
||||
.setServerStatusChangeBillingCost(Money.of(EUR, 42))
|
||||
.build());
|
||||
assertThat(thrown).hasMessageThat().contains("cost must be in the TLD's currency");
|
||||
assertThat(thrown).hasMessageThat().contains("negative or in the wrong currency");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -742,7 +756,7 @@ public final class TldTest extends EntityTestCase {
|
||||
.asBuilder()
|
||||
.setEapFeeSchedule(ImmutableSortedMap.of(START_INSTANT, Money.zero(EUR)))
|
||||
.build());
|
||||
assertThat(thrown).hasMessageThat().contains("All EAP fees must be in the TLD's currency");
|
||||
assertThat(thrown).hasMessageThat().contains("negative or in the wrong currency");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -110,28 +110,44 @@ public class PremiumListTest {
|
||||
|
||||
@Test
|
||||
void testValidation_labelMustBeLowercase() {
|
||||
Exception e =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
new PremiumEntry.Builder()
|
||||
.setPrice(BigDecimal.valueOf(399))
|
||||
.setLabel("UPPER.tld")
|
||||
.build());
|
||||
assertThat(e).hasMessageThat().contains("must be in puny-coded, lower-case form");
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
new PremiumEntry.Builder()
|
||||
.setPrice(BigDecimal.valueOf(399))
|
||||
.setLabel("UPPER.tld")
|
||||
.build()))
|
||||
.hasMessageThat()
|
||||
.contains("must be in puny-coded, lower-case form");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidation_priceMustNotBeNegative() {
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
new PremiumEntry.Builder()
|
||||
.setPrice(BigDecimal.valueOf(-100))
|
||||
.setLabel("anchor")
|
||||
.build()))
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Price must not be negative");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidation_labelMustBePunyCoded() {
|
||||
Exception e =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
new PremiumEntry.Builder()
|
||||
.setPrice(BigDecimal.valueOf(399))
|
||||
.setLabel("lower.みんな")
|
||||
.build());
|
||||
assertThat(e).hasMessageThat().contains("must be in puny-coded, lower-case form");
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
new PremiumEntry.Builder()
|
||||
.setPrice(BigDecimal.valueOf(399))
|
||||
.setLabel("lower.みんな")
|
||||
.build()))
|
||||
.hasMessageThat()
|
||||
.contains("must be in puny-coded, lower-case form");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+33
@@ -404,6 +404,39 @@ class JpaTransactionManagerImplTest {
|
||||
.containsExactlyElementsIn(moreEntities);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadAllOfSorted() {
|
||||
TestEntity entityB = new TestEntity("b_entity", "gamma");
|
||||
TestEntity entityC = new TestEntity("c_entity", "alpha");
|
||||
TestEntity entityA = new TestEntity("a_entity", "beta");
|
||||
persistResources(ImmutableList.of(entityB, entityC, entityA));
|
||||
|
||||
assertThat(tm().transact(() -> tm().loadAllOfSorted(TestEntity.class, "name")))
|
||||
.containsExactly(entityA, entityB, entityC)
|
||||
.inOrder();
|
||||
|
||||
assertThat(tm().transact(() -> tm().loadAllOfSorted(TestEntity.class, "data")))
|
||||
.containsExactly(entityC, entityA, entityB)
|
||||
.inOrder();
|
||||
|
||||
assertThat(tm().transact(() -> tm().loadAllOfSorted(TestEntity.class, "data", "name")))
|
||||
.containsExactly(entityC, entityA, entityB)
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadAllOfSorted_invalidFieldName_throwsException() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().loadAllOfSorted(
|
||||
TestEntity.class, "name; DROP TABLE TestEntity;")));
|
||||
assertThat(thrown).hasMessageThat().contains("Invalid sort field name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveAllNew_rollsBackWhenFailure() {
|
||||
moreEntities.forEach(entity -> assertThat(tm().transact(() -> tm().exists(entity))).isFalse());
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -73,7 +73,10 @@ class RdapTestHelper {
|
||||
"We reserve the right to modify this agreement at any time.");
|
||||
rdapJsonFormatter.rdapTosStaticUrl = "https://www.example.tld/about/rdap/tos.html";
|
||||
rdapJsonFormatter.hostCache =
|
||||
(repoId) -> Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId)));
|
||||
(repoId) ->
|
||||
Optional.ofNullable(EppResource.loadByCache(VKey.create(Host.class, repoId)))
|
||||
.filter(host -> clock.now().isBefore(host.getDeletionTime()))
|
||||
.map(host -> (Host) host.cloneProjectedAtTime(clock.now()));
|
||||
return rdapJsonFormatter;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ public class RdeUploadActionTest {
|
||||
action.timeout = Duration.ofSeconds(23);
|
||||
action.tld = "tld";
|
||||
action.sftpCooldown = Duration.ofSeconds(7);
|
||||
action.uploadUrl = uploadUrl;
|
||||
action.rdeUploadUrl = RdeUploadUrl.create(uploadUrl);
|
||||
action.receiverKey = keyring.getRdeReceiverKey();
|
||||
action.signingKey = keyring.getRdeSigningKey();
|
||||
action.stagingDecryptionKey = keyring.getRdeStagingDecryptionKey();
|
||||
@@ -212,7 +212,7 @@ public class RdeUploadActionTest {
|
||||
@Test
|
||||
void testRun() {
|
||||
createTld("lol");
|
||||
RdeUploadAction action = createAction(null);
|
||||
RdeUploadAction action = createAction(URI.create("sftp://user:password@localhost:5432"));
|
||||
action.tld = "lol";
|
||||
action.run();
|
||||
verify(runner)
|
||||
@@ -231,7 +231,7 @@ public class RdeUploadActionTest {
|
||||
@Test
|
||||
void testRun_withPrefix() throws Exception {
|
||||
createTld("lol");
|
||||
RdeUploadAction action = createAction(null);
|
||||
RdeUploadAction action = createAction(URI.create("sftp://user:password@localhost:5432"));
|
||||
action.prefix = Optional.of("job-name/");
|
||||
action.tld = "lol";
|
||||
action.run();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,11 @@
|
||||
package google.registry.reporting.billing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.mockito.ArgumentMatchers.startsWith;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -25,6 +28,7 @@ import com.google.cloud.tasks.v2.HttpMethod;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.batch.CloudTasksUtils;
|
||||
import google.registry.beam.BeamActionTestBase;
|
||||
import google.registry.model.common.Cursor;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.reporting.ReportingModule;
|
||||
@@ -33,6 +37,7 @@ import google.registry.testing.CloudTasksHelper.TaskMatcher;
|
||||
import google.registry.testing.FakeClock;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.YearMonth;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
@@ -50,8 +55,13 @@ class GenerateInvoicesActionTest extends BeamActionTestBase {
|
||||
private CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
|
||||
private GenerateInvoicesAction action;
|
||||
|
||||
private void setCursor(Instant cursorTime) {
|
||||
persistResource(Cursor.createGlobal(RECURRING_BILLING, cursorTime));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLaunchTemplateJob_withPublish() throws Exception {
|
||||
setCursor(Instant.parse("2017-11-01T00:00:00Z"));
|
||||
action =
|
||||
new GenerateInvoicesAction(
|
||||
"test-project",
|
||||
@@ -84,6 +94,7 @@ class GenerateInvoicesActionTest extends BeamActionTestBase {
|
||||
|
||||
@Test
|
||||
void testLaunchTemplateJob_withoutPublish() throws Exception {
|
||||
setCursor(Instant.parse("2017-11-01T00:00:00Z"));
|
||||
action =
|
||||
new GenerateInvoicesAction(
|
||||
"test-project",
|
||||
@@ -107,6 +118,7 @@ class GenerateInvoicesActionTest extends BeamActionTestBase {
|
||||
|
||||
@Test
|
||||
void testCaughtIOException() throws IOException {
|
||||
setCursor(Instant.parse("2017-11-01T00:00:00Z"));
|
||||
when(launch.execute()).thenThrow(new IOException("Pipeline error"));
|
||||
action =
|
||||
new GenerateInvoicesAction(
|
||||
@@ -128,4 +140,58 @@ class GenerateInvoicesActionTest extends BeamActionTestBase {
|
||||
verify(emailUtils).sendAlertEmail("Pipeline Launch failed due to Pipeline error");
|
||||
cloudTasksHelper.assertNoTasksEnqueued("beam-reporting");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_cursorLagging() {
|
||||
setCursor(Instant.parse("2017-10-31T23:59:59.999Z"));
|
||||
action =
|
||||
new GenerateInvoicesAction(
|
||||
"test-project",
|
||||
"test-region",
|
||||
"staging_bucket",
|
||||
"billing_bucket",
|
||||
"REG-INV",
|
||||
false,
|
||||
YearMonth.of(2017, 10),
|
||||
emailUtils,
|
||||
cloudTasksUtils,
|
||||
clock,
|
||||
response,
|
||||
dataflow);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
|
||||
assertThat(response.getPayload()).contains("Pipeline launch failed");
|
||||
assertThat(response.getPayload()).contains("BillingRecurrence expansion cursor");
|
||||
verify(emailUtils)
|
||||
.sendAlertEmail(
|
||||
startsWith("Pipeline Launch failed due to BillingRecurrence expansion cursor"));
|
||||
cloudTasksHelper.assertNoTasksEnqueued("beam-reporting");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_cursorMissing() {
|
||||
// Do not set cursor, should default to START_INSTANT (1970)
|
||||
action =
|
||||
new GenerateInvoicesAction(
|
||||
"test-project",
|
||||
"test-region",
|
||||
"staging_bucket",
|
||||
"billing_bucket",
|
||||
"REG-INV",
|
||||
false,
|
||||
YearMonth.of(2017, 10),
|
||||
emailUtils,
|
||||
cloudTasksUtils,
|
||||
clock,
|
||||
response,
|
||||
dataflow);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
|
||||
assertThat(response.getPayload()).contains("Pipeline launch failed");
|
||||
assertThat(response.getPayload()).contains("BillingRecurrence expansion cursor");
|
||||
verify(emailUtils)
|
||||
.sendAlertEmail(
|
||||
startsWith("Pipeline Launch failed due to BillingRecurrence expansion cursor"));
|
||||
cloudTasksHelper.assertNoTasksEnqueued("beam-reporting");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user