1
0
mirror of https://github.com/google/nomulus synced 2026-05-25 09:10:51 +00:00

Compare commits

...

11 Commits

Author SHA1 Message Date
gbrodman
149fb66ac5 Add cache for deletion times of existing domains (#2840)
This should help in instances of popular domains dropping, since we
won't need to do an additional two database loads every time (assuming
the deletion time is in the future).
2025-10-09 17:22:24 +00:00
gbrodman
8c96940a27 Only load from ClaimsList once when filling the cache (#2843) 2025-10-09 16:57:21 +00:00
Ben McIlwain
9c5510f05d Add a rate limiter to remove all domain contacts action (#2838)
The maximum QPS defaults to 10, but can also be specified at runtime through
use of a query-string parameter.

BUG = http://b/439636188
2025-10-02 22:15:19 +00:00
gbrodman
84884de77b Verify existence of TLDs and registrars for tokens (#2837)
Just in case someone makes a typo when running the commands
2025-10-02 20:10:58 +00:00
Ben McIlwain
d6c35df9bc Ignore single domain failures in remove contacts from all domains action (#2836)
When running the action in sandbox on 1.5M domains, it failed a few times
updating individual domains (requiring a manual restart of the entire action).
It's better to just log the individual failures for manual inspection and then
otherwise continue running the action to process the vast majority of other
updates that won't fail.

BUG = http://b/439636188
2025-10-02 18:58:23 +00:00
Juan Celhay
7caa0ec9d6 Add environment configuration files to .gitignore (#2830)
* Add environment configuration files to .gitignore

* Delete config files from repo

* Refactor release cb file to delete config file lines from gitignore

* Reorder env files

* Add README for config files
2025-10-02 18:36:43 +00:00
Weimin Yu
ee3866ec4a Allow top level tld creation in Sandbox (#2835)
Add a flag to the CreateCdnsTld command to bypass the dns name format
check in Sandbox (limiting names to `*.test.`). With this flag, we
can create TLDs for RST testing in Sandbox.

Note that if the new flag is wrongly set for a disallowed name, the
request to the Cloud DNS API will fail. The format check in the command
just provides a user-friendly error message.
2025-10-01 14:20:33 +00:00
gbrodman
97d0b7680f Add hash indexes for common use cases (#2834)
I went through all the SQL statements generated by some sample
DomainCreateFlow and DomainDeleteFlow cases to find situations where we
were either SELECTing from, or UPDATEing, tables with a direct "field =
value" format. These are the situations that I found where we can add
hash indexes. This does two things:

1. Makes these queries slight faster, since these are usually queries on
   columns that are either unique or very close to unique, and O(1) is
   faster than O(log(n))
2. Spreads around the optimistic predicate locks on the previously-used
   btree indexes. Many of our serialization errors came from the fact
   that we were autogenerating incrementing ID values for various
   tables, meaning that SELECTs, INSERTs, and UPDATEs would all try to
   take predicate locks out on the same page of the btree index. Using a
   hash index means that the page locks will be spread out to various
   index pages, rather than conflicting with each other.

Running load tests on alpha I see significant improvements in speed and
error rates. Speed is hard to quantify due to the nature of the way the
load tests distribute tasks among the queues but it could be more than
50% improvement, and serialization errors in the logs drop by more than
90%.
2025-09-29 22:16:24 +00:00
Pavlo Tkach
5700a008d6 Add console history frontend (#2832) 2025-09-26 21:25:03 +00:00
Ben McIlwain
dc9f5b99bc Add a batch action to remove all contacts from domains (#2827)
This implements the first part of Minimum Data Set phase 3, wherein we delete
all contact data. This action is necessary to leave a permanent record on the
domain (in the form of a domain history entry) documenting when the contacts
were removed by the administrative user.

Then, after this has finished removing all contact assocations, we can simply
empty out or drop the Contact/ContactHistory tables and associated join tables.
2025-09-25 20:47:17 +00:00
Ben McIlwain
d3c6de7a38 Modify the base Latin LGR with our intended changes to improve security (#2829) 2025-09-24 21:04:37 +00:00
65 changed files with 1832 additions and 432 deletions

7
.gitignore vendored
View File

@@ -18,6 +18,13 @@ gjf.out
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# Environment-specific configuration files
core/src/main/java/google/registry/config/files/nomulus-config-alpha.yaml
core/src/main/java/google/registry/config/files/nomulus-config-crash.yaml
core/src/main/java/google/registry/config/files/nomulus-config-production.yaml
core/src/main/java/google/registry/config/files/nomulus-config-qa.yaml
core/src/main/java/google/registry/config/files/nomulus-config-sandbox.yaml
######################################################################
# Eclipse Ignores

View File

@@ -26,6 +26,7 @@ import SecurityComponent from './settings/security/security.component';
import { SettingsComponent } from './settings/settings.component';
import { SupportComponent } from './support/support.component';
import RdapComponent from './settings/rdap/rdap.component';
import { HistoryComponent } from './history/history.component';
import { PasswordResetVerifyComponent } from './shared/components/passwordReset/passwordResetVerify.component';
export interface RouteWithIcon extends Route {
@@ -64,13 +65,18 @@ export const routes: RouteWithIcon[] = [
title: 'Dashboard',
iconName: 'view_comfy_alt',
},
// { path: 'tlds', component: TldsComponent, title: "TLDs", iconName: "event_list" },
{
path: DomainListComponent.PATH,
component: DomainListComponent,
title: 'Domains',
iconName: 'view_list',
},
{
path: HistoryComponent.PATH,
component: HistoryComponent,
// title: 'History',
// iconName: 'history',
},
{
path: SettingsComponent.PATH,
component: SettingsComponent,

View File

@@ -56,13 +56,14 @@ import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { UserDataService } from './shared/services/userData.service';
import { SnackBarModule } from './snackbar.module';
import { SupportComponent } from './support/support.component';
import { TldsComponent } from './tlds/tlds.component';
import { ForceFocusDirective } from './shared/directives/forceFocus.directive';
import RdapComponent from './settings/rdap/rdap.component';
import RdapEditComponent from './settings/rdap/rdapEdit.component';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
import { PasswordResetVerifyComponent } from './shared/components/passwordReset/passwordResetVerify.component';
import { PasswordInputForm } from './shared/components/passwordReset/passwordInputForm.component';
import { HistoryComponent } from './history/history.component';
import { HistoryListComponent } from './history/historyList.component';
@NgModule({
declarations: [SelectedRegistrarWrapper],
@@ -81,6 +82,8 @@ export class SelectedRegistrarModule {}
EppPasswordEditComponent,
ForceFocusDirective,
HeaderComponent,
HistoryComponent,
HistoryListComponent,
HomeComponent,
LocationBackDirective,
NavigationComponent,
@@ -104,7 +107,6 @@ export class SelectedRegistrarModule {}
SettingsComponent,
SettingsContactComponent,
SupportComponent,
TldsComponent,
UserLevelVisibility,
],
bootstrap: [AppComponent],

View File

@@ -0,0 +1,62 @@
<app-selected-registrar-wrapper>
<div class="history-log">
<h1 class="mat-headline-4" forceFocus>
Registrar Console Activity History
</h1>
<mat-tab-group
[elementId]="getElementIdForUserLog()"
class="history-log__tabs"
>
<mat-tab label="Registrar Activity">
<div class="spacer"></div>
<app-history-list
[historyRecords]="historyService.historyRecordsRegistrar()"
[isLoading]="isLoading"
/>
</mat-tab>
<mat-tab label="User Activity">
<div class="spacer"></div>
<form (ngSubmit)="loadHistory()" #form="ngForm">
<section>
<mat-form-field appearance="outline">
<mat-label>Console User Email: </mat-label>
<input
matInput
id="email"
type="email"
name="consoleUserEmail"
required
email
[(ngModel)]="consoleUserEmail"
#emailControl="ngModel"
/>
</mat-form-field>
</section>
<div class="spacer"></div>
<button
mat-flat-button
color="primary"
type="submit"
aria-label="Search user history"
[disabled]="!form.valid"
>
Search
</button>
</form>
<div class="spacer"></div>
<app-history-list
[historyRecords]="historyService.historyRecordsUser()"
[isLoading]="isLoading"
/>
</mat-tab>
</mat-tab-group>
</div>
<app-history-list
[elementId]="getElementIdForUserLog()"
[isReverse]="true"
[historyRecords]="historyService.historyRecordsUser()"
[isLoading]="isLoading"
/>
</app-selected-registrar-wrapper>

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,17 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
.console-tlds {
&__cards {
display: flex;
border-top: 1px solid #ddd;
padding: 1rem;
}
&__card {
max-width: 300px;
}
&__card-links {
display: flex;
flex-direction: column;
.history-log {
font-family: "Roboto", sans-serif;
max-width: 760px;
.spacer {
margin: 20px 0;
}
}

View File

@@ -0,0 +1,80 @@
// Copyright 2025 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.
import { Component, effect } from '@angular/core';
import { UserDataService } from '../shared/services/userData.service';
import { BackendService } from '../shared/services/backend.service';
import { RegistrarService } from '../registrar/registrar.service';
import { HistoryService } from './history.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
GlobalLoader,
GlobalLoaderService,
} from '../shared/services/globalLoader.service';
import { HttpErrorResponse } from '@angular/common/http';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
@Component({
selector: 'app-history',
templateUrl: './history.component.html',
styleUrls: ['./history.component.scss'],
providers: [HistoryService],
standalone: false,
})
export class HistoryComponent implements GlobalLoader {
public static PATH = 'history';
consoleUserEmail: string = '';
isLoading: boolean = false;
constructor(
private backendService: BackendService,
private registrarService: RegistrarService,
protected historyService: HistoryService,
protected globalLoader: GlobalLoaderService,
protected userDataService: UserDataService,
private _snackBar: MatSnackBar
) {
effect(() => {
if (registrarService.registrarId()) {
this.loadHistory();
}
});
}
getElementIdForUserLog() {
return RESTRICTED_ELEMENTS.ACTIVITY_PER_USER;
}
loadingTimeout() {
this._snackBar.open('Timeout loading records history');
}
loadHistory() {
this.globalLoader.startGlobalLoader(this);
this.isLoading = true;
this.historyService
.getHistoryLog(this.registrarService.registrarId(), this.consoleUserEmail)
.subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
this.isLoading = false;
},
next: () => {
this.globalLoader.stopGlobalLoader(this);
this.isLoading = false;
},
});
}
}

View File

@@ -0,0 +1,46 @@
// Copyright 2025 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.
import { Injectable, signal } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
import { tap } from 'rxjs';
export interface HistoryRecord {
modificationTime: string;
type: string;
description: string;
actingUser: {
emailAddress: string;
};
}
@Injectable()
export class HistoryService {
historyRecordsRegistrar = signal<HistoryRecord[]>([]);
historyRecordsUser = signal<HistoryRecord[]>([]);
constructor(private backendService: BackendService) {}
getHistoryLog(registrarId: string, userEmail?: string) {
return this.backendService.getHistoryLog(registrarId, userEmail).pipe(
tap((historyRecords: HistoryRecord[]) => {
if (userEmail) {
this.historyRecordsUser.set(historyRecords);
} else {
this.historyRecordsRegistrar.set(historyRecords);
}
})
);
}
}

View File

@@ -0,0 +1,50 @@
@if (!isLoading && historyRecords.length == 0) {
<div class="history-list__no-records">
<mat-icon class="history-list__no-records-icon secondary-text"
>apps_outage</mat-icon
>
<h1>No records found</h1>
</div>
} @else {
<mat-card>
<mat-card-content>
<mat-list role="list">
<ng-container *ngFor="let item of historyRecords; let last = last">
<mat-list-item class="history-list__item">
<mat-icon
[ngClass]="getIconClass(item.type)"
class="history-list__icon"
>
{{ getIconForType(item.type) }}
</mat-icon>
<div class="history-list__content">
<div class="history-list__description">
<span class="history-list__description--main">{{
item.type
}}</span>
<div>
<mat-chip
*ngIf="parseDescription(item.description).detail"
class="history-list__chip"
>
{{ parseDescription(item.description).detail }}
</mat-chip>
</div>
</div>
<div class="history-list__user">
<b>User - {{ item.actingUser.emailAddress }}</b>
</div>
</div>
<span class="history-list__timestamp">
{{ item.modificationTime | date : "MMM d, y, h:mm a" }}
</span>
</mat-list-item>
<mat-divider *ngIf="!last"></mat-divider>
</ng-container>
</mat-list>
</mat-card-content>
</mat-card>
}

View File

@@ -0,0 +1,81 @@
// Copyright 2025 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.
.history-list {
font-family: "Roboto", sans-serif;
&__item {
display: flex;
align-items: center;
// Override default mat-list-item height to fit content
height: auto !important;
padding: 16px 0;
}
&__no-records {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
&__no-records-icon {
width: 4rem;
height: 4rem;
font-size: 4rem;
margin-top: 1.5rem;
}
&__icon {
margin-right: 16px;
&--update {
color: #1976d2;
}
&--security {
color: #d32f2f;
}
}
&__description {
&--main {
font-size: 1rem;
font-weight: 500;
color: rgba(0, 0, 0, 0.87);
margin-bottom: 1em;
}
}
&__content {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 4px;
margin-right: 16px;
}
&__chip {
margin: 0.5rem 0;
}
&__user {
font-size: 0.9rem;
color: rgba(0, 0, 0, 0.6);
}
&__timestamp {
color: rgba(0, 0, 0, 0.6);
white-space: nowrap;
text-align: right;
}
}

View File

@@ -0,0 +1,66 @@
// Copyright 2025 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.
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { HistoryRecord } from './history.service';
@Component({
selector: 'app-history-list',
templateUrl: './historyList.component.html',
styleUrls: ['./historyList.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class HistoryListComponent {
@Input() historyRecords: HistoryRecord[] = [];
@Input() isLoading: boolean = false;
getIconForType(type: string): string {
switch (type) {
case 'REGISTRAR_UPDATE':
return 'edit';
case 'REGISTRAR_SECURITY_UPDATE':
return 'security';
default:
return 'history'; // A fallback icon
}
}
getIconClass(type: string): string {
switch (type) {
case 'REGISTRAR_UPDATE':
return 'history-log__icon--update';
case 'REGISTRAR_SECURITY_UPDATE':
return 'history-log__icon--security';
default:
return '';
}
}
parseDescription(description: string): {
main: string;
detail: string | null;
} {
if (!description) {
return { main: 'N/A', detail: null };
}
const parts = description.split('|');
const detail = parts.length > 1 ? parts[1].replace(/_/g, ' ') : null;
return {
main: parts[0],
detail: detail,
};
}
}

View File

@@ -16,6 +16,7 @@ import { Directive, ElementRef, Input, effect } from '@angular/core';
import { UserDataService } from '../services/userData.service';
export enum RESTRICTED_ELEMENTS {
ACTIVITY_PER_USER,
REGISTRAR_ELEMENT,
OTE,
USERS,
@@ -28,9 +29,10 @@ export const DISABLED_ELEMENTS_PER_ROLE = {
RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT,
RESTRICTED_ELEMENTS.OTE,
RESTRICTED_ELEMENTS.SUSPEND,
RESTRICTED_ELEMENTS.ACTIVITY_PER_USER,
],
SUPPORT_LEAD: [],
SUPPORT_AGENT: [],
SUPPORT_AGENT: [RESTRICTED_ELEMENTS.ACTIVITY_PER_USER],
};
@Directive({
@@ -40,6 +42,8 @@ export const DISABLED_ELEMENTS_PER_ROLE = {
export class UserLevelVisibility {
@Input() elementId!: RESTRICTED_ELEMENTS | null;
@Input() isReverse: boolean = false;
constructor(
private userDataService: UserDataService,
private el: ElementRef
@@ -56,9 +60,9 @@ export class UserLevelVisibility {
// @ts-ignore
(DISABLED_ELEMENTS_PER_ROLE[globalRole] || []).includes(this.elementId)
) {
this.el.nativeElement.style.display = 'none';
this.el.nativeElement.style.display = this.isReverse ? '' : 'none';
} else {
this.el.nativeElement.style.display = '';
this.el.nativeElement.style.display = this.isReverse ? 'none' : '';
}
}
}

View File

@@ -31,6 +31,7 @@ import { Contact } from '../../settings/contact/contact.service';
import { EppPasswordBackendModel } from '../../settings/security/security.service';
import { UserData } from './userData.service';
import { PasswordResetVerifyResponse } from '../components/passwordReset/passwordResetVerify.component';
import { HistoryRecord } from '../../history/history.service';
@Injectable()
export class BackendService {
@@ -123,6 +124,16 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<DomainListResult>(err)));
}
getHistoryLog(registrarId: string, userEmail?: string) {
return this.http
.get<HistoryRecord[]>(
userEmail
? `/console-api/history?registrarId=${registrarId}&consoleUserEmail=${userEmail}`
: `/console-api/history?registrarId=${registrarId}`
)
.pipe(catchError((err) => this.errorCatcher<HistoryRecord[]>(err)));
}
getRegistrars(): Observable<Registrar[]> {
return this.http
.get<Registrar[]>('/console-api/registrars')

View File

@@ -1 +0,0 @@
<div class="console-tlds__cards"></div>

View File

@@ -1,38 +0,0 @@
// Copyright 2024 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.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TldsComponent } from './tlds.component';
import { MaterialModule } from '../material.module';
describe('TldsComponent', () => {
let component: TldsComponent;
let fixture: ComponentFixture<TldsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MaterialModule],
declarations: [TldsComponent],
}).compileComponents();
fixture = TestBed.createComponent(TldsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -29,9 +29,11 @@ import static google.registry.request.RequestParameters.extractRequiredParameter
import static google.registry.request.RequestParameters.extractSetOfDatetimeParameters;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.RateLimiter;
import dagger.Module;
import dagger.Provides;
import google.registry.request.Parameter;
import jakarta.inject.Named;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.joda.time.DateTime;
@@ -137,4 +139,18 @@ public class BatchModule {
static boolean provideIsFast(HttpServletRequest req) {
return extractBooleanParameter(req, PARAM_FAST);
}
private static final int DEFAULT_MAX_QPS = 10;
@Provides
@Parameter("maxQps")
static int provideMaxQps(HttpServletRequest req) {
return extractOptionalIntParameter(req, "maxQps").orElse(DEFAULT_MAX_QPS);
}
@Provides
@Named("removeAllDomainContacts")
static RateLimiter provideRemoveAllDomainContactsRateLimiter(@Parameter("maxQps") int maxQps) {
return RateLimiter.create(maxQps);
}
}

View File

@@ -0,0 +1,246 @@
// Copyright 2025 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.batch;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static google.registry.flows.FlowUtils.marshalWithLenientRetry;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.RateLimiter;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
import google.registry.persistence.VKey;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import javax.annotation.Nullable;
import org.joda.time.Duration;
/**
* An action that removes all contacts from all active (non-deleted) domains.
*
* <p>This implements part 1 of phase 3 of the Minimum Dataset migration, wherein we remove all uses
* of contact objects in preparation for later removing all contact data from the system.
*
* <p>This runs as a singly threaded, resumable action that loads batches of domains still
* containing contacts, and runs a superuser domain update on each one to remove the contacts,
* leaving behind a record recording that update.
*/
@Action(
service = GaeService.BACKEND,
path = RemoveAllDomainContactsAction.PATH,
method = Action.Method.POST,
auth = Auth.AUTH_ADMIN)
public class RemoveAllDomainContactsAction implements Runnable {
public static final String PATH = "/_dr/task/removeAllDomainContacts";
private static final String LOCK_NAME = "Remove all domain contacts";
private static final String CONTACT_FMT = "<domain:contact type=\"%s\">%s</domain:contact>";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final EppController eppController;
private final String registryAdminClientId;
private final LockHandler lockHandler;
private final RateLimiter rateLimiter;
private final Response response;
private final String updateDomainXml;
private int successes = 0;
private int failures = 0;
private static final int BATCH_SIZE = 10000;
@Inject
RemoveAllDomainContactsAction(
EppController eppController,
@Config("registryAdminClientId") String registryAdminClientId,
LockHandler lockHandler,
@Named("removeAllDomainContacts") RateLimiter rateLimiter,
Response response) {
this.eppController = eppController;
this.registryAdminClientId = registryAdminClientId;
this.lockHandler = lockHandler;
this.rateLimiter = rateLimiter;
this.response = response;
this.updateDomainXml =
readResourceUtf8(RemoveAllDomainContactsAction.class, "domain_remove_contacts.xml");
}
@Override
public void run() {
checkState(
tm().transact(() -> FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)),
"Minimum dataset migration must be completed prior to running this action");
response.setContentType(PLAIN_TEXT_UTF_8);
Callable<Void> runner =
() -> {
try {
runLocked();
response.setStatus(SC_OK);
} catch (Exception e) {
logger.atSevere().withCause(e).log("Errored out during execution.");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload(String.format("Errored out with cause: %s", e));
}
return null;
};
if (!lockHandler.executeWithLocks(runner, null, Duration.standardHours(1), LOCK_NAME)) {
// Send a 200-series status code to prevent this conflicting action from retrying.
response.setStatus(SC_NO_CONTENT);
response.setPayload("Could not acquire lock; already running?");
}
}
private void runLocked() {
logger.atInfo().log("Removing contacts on all active domains.");
List<String> domainRepoIdsBatch;
do {
domainRepoIdsBatch =
tm().<List<String>>transact(
() ->
tm().getEntityManager()
.createQuery(
"""
SELECT repoId FROM Domain WHERE deletionTime = :end_of_time AND NOT (
adminContact IS NULL AND billingContact IS NULL
AND registrantContact IS NULL AND techContact IS NULL)
""")
.setParameter("end_of_time", END_OF_TIME)
.setMaxResults(BATCH_SIZE)
.getResultList());
for (String domainRepoId : domainRepoIdsBatch) {
rateLimiter.acquire();
runDomainUpdateFlow(domainRepoId);
}
} while (!domainRepoIdsBatch.isEmpty());
String msg =
String.format(
"Finished; %d domains were successfully updated and %d errored out.",
successes, failures);
logger.at(failures == 0 ? Level.INFO : Level.WARNING).log(msg);
response.setPayload(msg);
}
private void runDomainUpdateFlow(String repoId) {
// Create a new transaction that the flow's execution will be enlisted in that loads the domain
// transactionally. This way we can ensure that nothing else has modified the domain in question
// in the intervening period since the query above found it. If a single domain update fails
// permanently, log it and move on to not block processing all the other domains.
try {
boolean success = tm().transact(() -> runDomainUpdateFlowInner(repoId));
if (success) {
successes++;
} else {
failures++;
}
} catch (Throwable t) {
logger.atWarning().withCause(t).log(
"Failed updating domain with repoId %s; skipping.", repoId);
}
}
/**
* Runs the actual domain update flow and returns whether the contact removals were successful.
*/
private boolean runDomainUpdateFlowInner(String repoId) {
Domain domain = tm().loadByKey(VKey.create(Domain.class, repoId));
if (!domain.getDeletionTime().equals(END_OF_TIME)) {
// Domain has been deleted since the action began running; nothing further to be
// done here.
logger.atInfo().log("Nothing to process for deleted domain '%s'.", domain.getDomainName());
return false;
}
logger.atInfo().log("Attempting to remove contacts on domain '%s'.", domain.getDomainName());
StringBuilder sb = new StringBuilder();
ImmutableMap<VKey<? extends Contact>, Contact> contacts =
tm().loadByKeys(
domain.getContacts().stream()
.map(DesignatedContact::getContactKey)
.collect(ImmutableSet.toImmutableSet()));
// Collect all the (non-registrant) contacts referenced by the domain and compile an EPP XML
// string that removes each one.
for (DesignatedContact designatedContact : domain.getContacts()) {
@Nullable Contact contact = contacts.get(designatedContact.getContactKey());
if (contact == null) {
logger.atWarning().log(
"Domain '%s' referenced contact with repo ID '%s' that couldn't be" + " loaded.",
domain.getDomainName(), designatedContact.getContactKey().getKey());
continue;
}
sb.append(
String.format(
CONTACT_FMT,
Ascii.toLowerCase(designatedContact.getType().name()),
contact.getContactId()))
.append("\n");
}
String compiledXml =
updateDomainXml
.replace("%DOMAIN%", domain.getDomainName())
.replace("%CONTACTS%", sb.toString());
EppOutput output =
eppController.handleEppCommand(
new StatelessRequestSessionMetadata(
registryAdminClientId, ProtocolDefinition.getVisibleServiceExtensionUris()),
new PasswordOnlyTransportCredentials(),
EppRequestSource.BACKEND,
false,
true,
compiledXml.getBytes(US_ASCII));
if (output.isSuccess()) {
logger.atInfo().log(
"Successfully removed contacts from domain '%s'.", domain.getDomainName());
} else {
logger.atWarning().log(
"Failed removing contacts from domain '%s' with error %s.",
domain.getDomainName(), new String(marshalWithLenientRetry(output), US_ASCII));
}
return output.isSuccess();
}
}

View File

@@ -0,0 +1,13 @@
# Nomulus Environment Configuration
The configuration files for the different Nomulus environments are not included in this repository. To configure and run a specific environment, you will need to create the corresponding YAML configuration file in this directory.
The following is a list of the environment configuration files that you may need to create:
* `nomulus-config-alpha.yaml`
* `nomulus-config-crash.yaml`
* `nomulus-config-qa.yaml`
* `nomulus-config-sandbox.yaml`
* `nomulus-config-production.yaml`
Please create the relevant file for the environment you intend to use and populate it with the necessary configuration details.

View File

@@ -1 +0,0 @@
# Add environment-specific configuration here.

View File

@@ -1 +0,0 @@
# Add environment-specific configuration here.

View File

@@ -1 +0,0 @@
# Add environment-specific configuration here.

View File

@@ -1 +0,0 @@
# Add environment-specific configuration here.

View File

@@ -1 +0,0 @@
# Add environment-specific configuration here.

View File

@@ -18,7 +18,6 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
import static google.registry.flows.FlowUtils.persistEntityChanges;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist;
import static google.registry.flows.domain.DomainFlowUtils.COLLISION_MESSAGE;
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static google.registry.flows.domain.DomainFlowUtils.checkHasBillingAccount;
@@ -224,6 +223,7 @@ public final class DomainCreateFlow implements MutatingFlow {
@Inject DomainCreateFlowCustomLogic flowCustomLogic;
@Inject DomainFlowTmchUtils tmchUtils;
@Inject DomainPricingLogic pricingLogic;
@Inject DomainDeletionTimeCache domainDeletionTimeCache;
@Inject DomainCreateFlow() {}
@@ -239,13 +239,13 @@ public final class DomainCreateFlow implements MutatingFlow {
validateRegistrarIsLoggedIn(registrarId);
verifyRegistrarIsActive(registrarId);
extensionManager.validate();
verifyDomainDoesNotExist();
DateTime now = tm().getTransactionTime();
DomainCommand.Create command = cloneAndLinkReferences((Create) resourceCommand, now);
Period period = command.getPeriod();
verifyUnitIsYears(period);
int years = period.getValue();
validateRegistrationPeriod(years);
verifyResourceDoesNotExist(Domain.class, targetId, now, registrarId);
// Validate that this is actually a legal domain name on a TLD that the registrar has access to.
InternetDomainName domainName = validateDomainName(command.getDomainName());
String domainLabel = domainName.parts().getFirst();
@@ -649,6 +649,15 @@ public final class DomainCreateFlow implements MutatingFlow {
.build();
}
private void verifyDomainDoesNotExist() throws ResourceCreateContentionException {
Optional<DateTime> previousDeletionTime =
domainDeletionTimeCache.getDeletionTimeForDomain(targetId);
if (previousDeletionTime.isPresent()
&& !tm().getTransactionTime().isAfter(previousDeletionTime.get())) {
throw new ResourceCreateContentionException(targetId);
}
}
private static BillingEvent createEapBillingEvent(
FeesAndCredits feesAndCredits, BillingEvent createBillingEvent) {
return new BillingEvent.Builder()

View File

@@ -0,0 +1,122 @@
// Copyright 2025 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.domain;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Expiry;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.collect.ImmutableSet;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* Functionally-static loading cache that keeps track of deletion (AKA drop) times for domains.
*
* <p>Some domain names may have many create requests issued shortly before (and directly after) the
* name is released due to a previous registrant deleting it. In those cases, caching the deletion
* time of the existing domain allows us to short-circuit the request and avoid any load on the
* database checking the existing domain (at least, in cases where the request hits a particular
* node more than once).
*
* <p>The cache is fairly short-lived (as we're concerned about many requests at basically the same
* time), and entries also expire when the drop actually happens. If the domain is re-created after
* a drop, the next load attempt will populate the cache with a deletion time of END_OF_TIME, which
* will be read from the cache by subsequent attempts.
*
* <p>We take advantage of the fact that Caffeine caches don't store nulls returned from the
* CacheLoader, so a null result (meaning the domain doesn't exist) won't affect future calls (this
* avoids a stale-cache situation where the cache "thinks" the domain doesn't exist, but it does).
* Put another way, if a domain really doesn't exist, we'll re-attempt the database load every time.
*
* <p>We don't explicitly set the cache inside domain create/delete flows, in case the transaction
* fails at commit time. It's better to have stale data, or to require an additional database load,
* than to have incorrect data.
*
* <p>Note: this should be injected as a singleton -- it's essentially static, but we have it as a
* non-static object for concurrent testing purposes.
*/
public class DomainDeletionTimeCache {
// Max expiry time is ten minutes
private static final int MAX_EXPIRY_MILLIS = 10 * 60 * 1000;
private static final int MAX_ENTRIES = 500;
private static final int NANOS_IN_ONE_MILLISECOND = 100000;
/**
* Expire after the max duration, or after the domain is set to drop (whichever comes first).
*
* <p>If the domain has already been deleted (the deletion time is <= now), the entry will
* immediately be expired/removed.
*
* <p>NB: the Expiry class requires the return value in <b>nanoseconds</b>, not milliseconds
*/
private static final Expiry<String, DateTime> EXPIRY_POLICY =
new Expiry<>() {
@Override
public long expireAfterCreate(String key, DateTime value, long currentTime) {
long millisUntilDeletion = value.getMillis() - tm().getTransactionTime().getMillis();
return NANOS_IN_ONE_MILLISECOND
* Math.max(0L, Math.min(MAX_EXPIRY_MILLIS, millisUntilDeletion));
}
/** Reset the time entirely on update, as if we were creating the entry anew. */
@Override
public long expireAfterUpdate(
String key, DateTime value, long currentTime, long currentDuration) {
return expireAfterCreate(key, value, currentTime);
}
/** Reads do not change the expiry duration. */
@Override
public long expireAfterRead(
String key, DateTime value, long currentTime, long currentDuration) {
return currentDuration;
}
};
/** Attempt to load the domain's deletion time if the domain exists. */
private static final CacheLoader<String, DateTime> CACHE_LOADER =
(domainName) -> {
ForeignKeyUtils.MostRecentResource mostRecentResource =
ForeignKeyUtils.loadMostRecentResources(
Domain.class, ImmutableSet.of(domainName), false)
.get(domainName);
return mostRecentResource == null ? null : mostRecentResource.deletionTime();
};
public static DomainDeletionTimeCache create() {
return new DomainDeletionTimeCache(
Caffeine.newBuilder()
.expireAfter(EXPIRY_POLICY)
.maximumSize(MAX_ENTRIES)
.build(CACHE_LOADER));
}
private final LoadingCache<String, DateTime> cache;
private DomainDeletionTimeCache(LoadingCache<String, DateTime> cache) {
this.cache = cache;
}
/** Returns the domain's deletion time, or null if it doesn't currently exist. */
public Optional<DateTime> getDeletionTimeForDomain(String domainName) {
return Optional.ofNullable(cache.get(domainName));
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,12 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
package google.registry.flows.domain;
@Component({
selector: 'app-tlds',
templateUrl: './tlds.component.html',
styleUrls: ['./tlds.component.scss'],
standalone: false,
})
export class TldsComponent {}
import dagger.Module;
import dagger.Provides;
import jakarta.inject.Singleton;
/** Dagger module to provide the {@link DomainDeletionTimeCache}. */
@Module
public class DomainDeletionTimeCacheModule {
@Provides
@Singleton
public static DomainDeletionTimeCache provideDomainDeletionTimeCache() {
return DomainDeletionTimeCache.create();
}
}

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<lgr xmlns="urn:ietf:params:xml:ns:lgr-1.0">
<meta>
<version comment="Second Level Reference LGR">1</version>
<date>2024-10-25</date>
<version comment="Latin LGR">1</version>
<date>2025-10-01</date>
<language>und-Latn</language>
<unicode-version>11.0.0</unicode-version>
<unicode-version>2</unicode-version>
<description type="text/html"><![CDATA[
<div class="instructions">
<h2>INSTRUCTIONS</h2>
@@ -35,22 +35,21 @@
<p>Note: version numbers start at 1. RFC 7940 recommends using simple integers. The version comment is optional,
please replace or delete the default comment. Version comments may be used by some tools as part of the page header.</p>
<p><code>&lt;version comment=&quot;</code>[Please replace (or delete) the optional comment]<code>&quot;&gt;</code>[Please fill in version number, starting at 1]<code>&lt;/version&gt;</code></p>
<p><code>&lt;date&gt;</code>[Please fill in with publication date, in YYYY-MM-DD format]<code>&lt;/date&gt;</code></p>
<p><code>&lt;validity-start&gt;</code>[Please fill in effective date, in YYYY-MM-DD format]<code>&lt;/validity-start&gt;</code></p>
<p><code>&lt;date&gt;</code>2025-10-01<code>&lt;/date&gt;</code></p>
<p><code>&lt;validity-start&gt;</code>2025-10-01<code>&lt;/validity-start&gt;</code></p>
<p>Note: the scope element may be repeated, so that the same document can serve for multiple domains.</p>
<p><code>&lt;scope type=&quot;domain&quot;&gt;</code>[Please provide, in &quot;.domain&quot; format]<code>&lt;/scope&gt;</code></p>
<p><strong>Registry Contact Information:</strong></p>
<p>Please fill in the <a href="#registry_contact_details">Registry Contact Details</a>.</p>
<p><strong>Change History</strong></p>
<p>If you made technical modifications to the LGR, please summarize them in the <a href="#change_history">Change History</a> (and also note the details in the appropriate section of the description).</p>
<p>PLEASE DELETE THESE INSTRUCTIONS BEFORE DEPOSITING THE DOCUMENT</p></div>
<section id="registry_contact_details">
<h2>Registry Contact Details</h2>
<ul style="list-style:none;">
<li><b>Contact Name:</b> [Please fill in Contact Name]</li>
<li><b>Email address:</b> [Please fill in Email address]</li>
<li><b>Phone Number:</b> [Please fill in optional Phone Number]</li>
<li><b>Contact Name:</b> Ben McIlwain</li>
<li><b>Email address:</b> nomulus-discuss@google.com</li>
</ul>
</section>
@@ -67,17 +66,11 @@
<h2>Repertoire</h2>
<p>The repertoire contains the 197 letters needed to write hundreds of languages in the Latin script.
An additional 7 combining diacritical marks are available as part of 21 explicitly defined combining sequences.
<p>The repertoire contains the 164 letters needed to write hundreds of languages in the Latin script.
The repertoire is a subset of [Unicode 11.0.0]. For details, see Section 5, “Repertoire” in [Proposal-Latin].
(The proposal cited has been adopted for the Latin script portion of the Root Zone LGR.)</p>
<p>
Compared to that source, an additional language is supported by adding the code point for the Middle Dot used
in the Catalan Ela Geminada: U+006C U+00B7 U+006C. Context rules limit U+00B7 MIDDLE DOT to being bracketed
by the letter “l”. (See also [280])</p>
<p>For the second level, the repertoire has been augmented with the ASCII digits, U+0030 to U+0039, plus U+002D HYPHEN-MINUS, for a total of 231 repertoire elements.</p>
<p>For the second level, the repertoire has been augmented with the ASCII digits, U+0030 to U+0039, plus U+002D HYPHEN-MINUS, for a total of 175 repertoire elements.</p>
<p>Any code points outside the Latin Script repertoire that are targets for
out-of-repertoire variants would be included here only if the variant is listed
@@ -142,28 +135,6 @@
<ul>
<li><p><b>U+00B7 MIDDLE DOT and U+002D HYPHEN-MINUS</b> &mdash;
the use of the hyphen as fallback for the middle dot in the Catalan Ela Geminada follows registry practice, see [281].
The variant is limited to an Ela Geminada context.</p></li>
<li><b>U+00DF LATIN SMALL LETTER SHARP S and the sequence of two letters “ss” (U+0073 U+0073)</b> &mdash;
IDNA2003 Compatibility: in IDNA2003, U+00DF LATIN SMALL LETTER SHARP S is mapped into “ss” (U+0073 U+0073).
Note: the fallback is also used outside domain names, but also used in locale variants of German and the
standard spelling.</li>
<li><b>U+0131 LATIN SMALL LETTER DOTLESS I and U+0069 LATIN SMALL LETTER I</b> &mdash;
IDNA2003 Compatibility: in IDNA2003, U+0131 LATIN SMALL LETTER DOTLESS I is mapped into
U+0069 LATIN SMALL LETTER I.</li>
</ul>
<p>Some second level LGRs provide ASCII fallback variants for some or all accented Latin characters.
Likewise the U+0153 Small OE Ligature and U+00E6 Small AE ligature have ASCII fallbacks consisting of the
non-ligated “oe” and “ae” sequences. None of these fallbacks have been added to the current version of the LGR.</p>
<p><b>Overlapped Variant Sequence:</b> Both “ss” and “s” coexist in the repertoire and “s” has variant
relationships on its own. These variants thus overlap: making the variant set well-behaved for
index variant calculation requires that the sequence “ss” also be given variants to all permutations of
variants for the letter s followed by itself, as well as all transitive variants due to other variants
for U+00DF.</p>
<h3>In-script Variant Mapping Types</h3>
<p>In each of the fallback variant pairs defined above, the mapping type from the first element to the second is of type
“fallback”, while the variant type for the other direction is “blocked”. In addition, the first element of each pair uses the
@@ -194,17 +165,6 @@
<h3>Latin-specific Rules</h3>
<p>The following context rule applies to U+00B7 MIDDLE DOT and its variants.
It ensures that the middle dot is part of an Ela Geminada sequence and variants between it and HYPHEN-MINUS are only defined in that context.</p>
<ul>
<li><b>surrounded-by-L</b> &mdash; code points are invalid and variants undefined when not surrounded by “l”</li>
</ul>
<p>The following WLE rule invalidates labels in which two Ela Geminada sequences overlap.</p>
<ul>
<li><b>dot-L-dot</b> &mdash; labels sharing a single “l” with two different middle dots are invalid</li>
</ul>
<h2>Actions</h2>
<h3>Default Actions</h3>
@@ -213,27 +173,6 @@
invalidate labels with misplaced combining marks. They are marked with &#x235F;.
For a description see [RFC 7940].</p>
<p>Because this LGR defines allocatable fallback variants the following default actions are applicable.</p>
<ul>
<li><b>blocked</b> &mdash; a variant label containing a blocked variant will receive a disposition of “blocked”.</li>
<li><b>r-original</b> &mdash; a label containing one or more of this reflexive variant type
and no others represents an original label
and receives a disposition of “valid”.</li>
<li><b>fallback</b> &mdash; a label containing one or more of these variant types and no others
represents a label that contains only fallback variants
and receives a disposition of “allocatable”.</li>
<li><b>fallback plus other</b> &mdash; any label remaining containing both this variant type and any others
receives a disposition of “blocked”.</li>
</ul>
<p>These actions resolve as “allocatable” any label where all variants are of type “fallback”, and as “valid” any label
where all variants are of type “r-original”. Labels with a mix of variant types are resolved as “blocked”.</p>
<p>To account for original code points in a permuted variant, reflexive variant
mappings with an “r-” prefix are used. (See [RFC 7940]).
In particular, the mapping type “r-original” is given to any code point that has a fallback mapping,
but that appears in its non-fallback form in the original label, and thus “maps to itself”.</p>
<p>Default actions that are
triggered by the LGR-specific variant types described above limit the “allocatable” variant
labels to those containing only “ss”, dotted “i” or hyphen variants, while
@@ -258,7 +197,7 @@
<section id="change_history">
<h3>Changes from Version Dated 25 October 2024</h3>
<p>Adopted from the Second Level Reference LGR for the Latin Script [Ref-LGR-und-Latn] without normative changes.</p>
<p>Adopted from the Second Level Reference LGR for the Latin Script [Ref-LGR-und-Latn] with security improvements implemented by removing confusable variants.</p>
</section>
<h2>References</h2>
@@ -496,9 +435,7 @@
</references>
</meta>
<data>
<char cp="002D" not-when="hyphen-minus-disallowed" tag="sc:Zyyy" ref="0" comment="HYPHEN-MINUS; &#x235F;">
<var cp="00B7" when="surrounded-by-L" type="blocked" ref="281" comment="Middle dot in Ela Geminada" />
</char>
<char cp="002D" not-when="hyphen-minus-disallowed" tag="sc:Zyyy" ref="0" comment="HYPHEN-MINUS; &#x235F;" />
<char cp="0030" tag="Common-digit sc:Zyyy" ref="0" comment="DIGIT ZERO; &#x235F;" />
<char cp="0031" tag="Common-digit sc:Zyyy" ref="0" comment="DIGIT ONE; &#x235F;" />
<char cp="0032" tag="Common-digit sc:Zyyy" ref="0" comment="DIGIT TWO; &#x235F;" />
@@ -509,89 +446,35 @@
<char cp="0037" tag="Common-digit sc:Zyyy" ref="0" comment="DIGIT SEVEN; &#x235F;" />
<char cp="0038" tag="Common-digit sc:Zyyy" ref="0" comment="DIGIT EIGHT; &#x235F;" />
<char cp="0039" tag="Common-digit sc:Zyyy" ref="0" comment="DIGIT NINE; &#x235F;" />
<char cp="0061" tag="sc:Latn" ref="0 99" comment="Basic Latin">
<var cp="00E1" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="0061 0331" ref="129 146" comment="Nuer (4)" />
<char cp="0061" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0062" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0063" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0064" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0065" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0065 0331" ref="146" comment="Nuer (4)" />
<char cp="0066" tag="sc:Latn" ref="0 99" comment="Basic Latin">
<var cp="0192" type="blocked" comment="Generally acceptable alternate glyph" />
</char>
<char cp="0066" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0067" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0067 0303" ref="142 143" comment="Guarani (1)">
<var cp="1E21" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0068" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0069" tag="sc:Latn" ref="0 99" comment="Basic Latin">
<var cp="00ED" type="blocked" comment="Required for use with Common LGR" />
<var cp="00EF" type="blocked" comment="Required for use with Common LGR" />
<var cp="0131" type="blocked" comment="IDNA2003 Compatibility" />
<var cp="0269" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
<var cp="1EC9" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0069 0331" ref="146" comment="Nuer (4)" />
<char cp="0069" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="006A" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="006B" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="006C" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="006D" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="006D 0327" ref="136 213 214" comment="Marshallese (1)" />
<char cp="006E" tag="sc:Latn" ref="0 99" comment="Basic Latin">
<var cp="0144" type="blocked" comment="Required for use with Common LGR" />
<var cp="014B" type="blocked" comment="Required for use with Common LGR" />
<var cp="1E45" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="006E 0304" ref="136 200 213" comment="Raga (Hano) (3), Marshallese (1)">
<var cp="00F1" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="006E 0308" ref="276" comment="Malagasy (1)" />
<char cp="006F" tag="sc:Latn" ref="0 99" comment="Basic Latin">
<var cp="00F3" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="006F 0327" ref="136" comment="Marshallese (1)" />
<char cp="006F 0331" ref="129 146" comment="Nuer (4)" />
<char cp="006E" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="006F" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0070" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0071" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0072" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0072 0303" ref="147" comment="Hausa (2)" />
<char cp="0073" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0073 0073" comment="Sequence added for variant mapping">
<var cp="00DF" type="blocked" comment="IDNA2003 Compatibility" />
</char>
<char cp="0074" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0075" tag="sc:Latn" ref="0 99" comment="Basic Latin">
<var cp="00FA" type="blocked" comment="Required for use with Common LGR" />
<var cp="00FC" type="blocked" comment="Required for use with Common LGR" />
<var cp="028B" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="0075" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0076" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0077" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0078" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="0079" tag="sc:Latn" ref="0 99" comment="Basic Latin">
<var cp="0263" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="0079" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="007A" tag="sc:Latn" ref="0 99" comment="Basic Latin" />
<char cp="00B7" when="surrounded-by-L" tag="sc:Zyyy" ref="0 100 279 280" comment="Catalan (2) L·L - CONTEXTO for MIDDLE DOT in RFC 5892">
<var cp="002D" when="surrounded-by-L" type="fallback" comment="Fallback" />
<var cp="00B7" when="surrounded-by-L" type="r-original" comment="Middle Dot in the original label" />
</char>
<char cp="00DF" tag="sc:Latn" ref="0 119" comment="German (1)">
<var cp="0073 0073" type="fallback" comment="IDNA2003 Compatibility" />
<var cp="00DF" type="r-original" comment="Eszett in the original label" />
</char>
<char cp="00E0" tag="sc:Latn" ref="0 106 114 130 131 132" comment="Italian (1), French (1), Galician (2), Wolof (4)">
<var cp="1EA3" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="00E1" tag="sc:Latn" ref="0 100 101 102 103 105 106 107 108" comment="Spanish (1), Czech (1), Icelandic (1), Faroese (2), Chuukese (2), Galician (2), Lule Sami (2), Northern Sami (2)">
<var cp="0061" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="00E0" tag="sc:Latn" ref="0 106 114 130 131 132" comment="Italian (1), French (1), Galician (2), Wolof (4)" />
<char cp="00E2" tag="sc:Latn" ref="0 106 109 110 113 114 115 116 117 275" comment="Vietnamese (1), Romanian (1), Skolt Sami (2), French (1), Galician (2), West Frisian (1), Friulian (4), Xavante (4)" />
<char cp="00E3" tag="sc:Latn" ref="0 141 142 143 144 145" comment="Umbundu (3), Guarani (1), Nauruan (3), Khoekhoe (4)">
<var cp="0101" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="00E3" tag="sc:Latn" ref="0 141 142 143 144 145" comment="Umbundu (3), Guarani (1), Nauruan (3), Khoekhoe (4)" />
<char cp="00E4" tag="sc:Latn" ref="0 107 119 120 121 122 123 124 125 126 127 128 129" comment="German (1), Finnish (1), Turkmen (1), Estonian (1), Swedish (1), Lule Sami (2), Yapese (2), Dinka (4), Kaqchikel (4), Bashkir (4), Alsatian (5), Nuer (4)" />
<char cp="00E5" tag="sc:Latn" ref="0 107 120 123 139 140" comment="Danish (1), Finnish (1), Chamorro (1), Swedish (1), Lule Sami (2)" />
<char cp="00E6" tag="sc:Latn" ref="0 102 103 139" comment="Danish (1), Icelandic (1), Faroese (2)" />
@@ -601,126 +484,44 @@
<char cp="00EA" tag="sc:Latn" ref="0 109 114 115 116 158 173 174 175" comment="French (1), Tswana (1), Afrikaans (1), Vietnamese (1), Kurdish (2), West Frisian (2), Friulian (4)" />
<char cp="00EB" tag="sc:Latn" ref="0 114 115 124 126 129 132 175 176 177 179 180" comment="Afrikaans (1), Albanian (1), French (1), Uyghur (2), Yapese (2), Wolof (4), Drehu (4), Kaqchikel (4), West Frisian (2), Nuer (4)" />
<char cp="00EC" tag="sc:Latn" ref="0 130 206 208" comment="Italian (1)" />
<char cp="00ED" tag="sc:Latn" ref="0 100 101 102 103 106 127" comment="Spanish (1), Czech (1), Icelandic (1), Faroese (2), Galician (2), Bashkir (4)">
<var cp="0069" type="blocked" comment="Required for use with Common LGR" />
<var cp="00EF" type="blocked" comment="Required for use with Common LGR" />
<var cp="0131" type="blocked" comment="Required for use with Common LGR" />
<var cp="0269" type="blocked" comment="Required for use with Common LGR" />
<var cp="1EC9" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="00EE" tag="sc:Latn" ref="0 110 114 116 158 175" comment="Afrikaans (1), Romanian (1), Kurdish (2), French (1), Friulian (4)" />
<char cp="00EF" tag="sc:Latn" ref="0 114 115 125 126 175" comment="Afrikaans (1), French (1), Kaqchikel (4), Dinka (4), West Frisian (2)">
<var cp="0069" type="blocked" comment="Required for use with Common LGR" />
<var cp="00ED" type="blocked" comment="Required for use with Common LGR" />
<var cp="0131" type="blocked" comment="Required for use with Common LGR" />
<var cp="0269" type="blocked" comment="Required for use with Common LGR" />
<var cp="1EC9" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="00F0" tag="sc:Latn" ref="0 102 103" comment="Faroese (2), Icelandic (1)" />
<char cp="00F1" tag="sc:Latn" ref="0 106 127 132 136 142 143 144 149 160 197 205 221 222 223 224 225 226 227 228 229" comment="Spanish (1), Fula (3), Chamorro (1), Filipino (1), Guarani (1), Chavacano (4), Basque (1), Galician (2), Iloco (3), Quechua (3), Cape Verdean Creole (4), Waray-Waray (3), Wolof (4), Nauruan (3), Lozi (4), Bashkir (4), Marshallese (1), Mandinka (5), Igbo (2)">
<var cp="006E 0304" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="00F2" tag="sc:Latn" ref="0 130 182 183" comment="Italian (1), Haitian Creole (1)">
<var cp="1ECF" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="00F3" tag="sc:Latn" ref="0 100 101 102 105 106 132 152" comment="Spanish (1), Polish (1), Czech (1), Icelandic (1), Chuukese (2), Galician (2), Wolof (4)">
<var cp="006F" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="00F2" tag="sc:Latn" ref="0 130 182 183" comment="Italian (1), Haitian Creole (1)" />
<char cp="00F4" tag="sc:Latn" ref="0 106 109 114 115 116 117 173 174 175 230 275" comment="Tswana (1), Afrikaans (1), Vietnamese (1), French (1), Northern Sotho (1), West Frisian (2), Galician (2), Friulian (4), Xavante (4)" />
<char cp="00F5" tag="sc:Latn" ref="0 113 117 122 141 142 143 144 145 275" comment="Estonian (1), Skolt Sami (2), Umbundu (3), Guarani (1), Nauruan (3), Xavante (4), Khoekhoe (4)">
<var cp="014D" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="00F5" tag="sc:Latn" ref="0 113 117 122 141 142 143 144 145 275" comment="Estonian (1), Skolt Sami (2), Umbundu (3), Guarani (1), Nauruan (3), Xavante (4), Khoekhoe (4)" />
<char cp="00F6" tag="sc:Latn" ref="0 115 119 120 123 124 125 126 127 129 157 175 179 180 232" comment="German (1), Finnish (1), Afrikaans (1), Turkish (1), Swedish (1), Uygur (2), Yapese (2), Drehu (4), Kaqchikel (4), Dinka (4), Bashkir (4), Chechen (2), 1992 Version, West Frisian (2), Nuer (4)" />
<char cp="00F8" tag="sc:Latn" ref="0 103 139" comment="Danish (1), Faroese (2)" />
<char cp="00F9" tag="sc:Latn" ref="0 114 130 206 245 246 253" comment="Italian (1), French (1), Papiamento (1)">
<var cp="1EE7" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="00FA" tag="sc:Latn" ref="0 100 101 102 103 105 106 115" comment="Spanish (1), Czech (1), Icelandic (1), Faroese (2), Chuukese (2), West Frisian (2), Galician (2)">
<var cp="0075" type="blocked" comment="Required for use with Common LGR" />
<var cp="00FC" type="blocked" comment="Required for use with Common LGR" />
<var cp="028B" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="00F9" tag="sc:Latn" ref="0 114 130 206 245 246 253" comment="Italian (1), French (1), Papiamento (1)" />
<char cp="00FB" tag="sc:Latn" ref="0 114 115 116 158 175 202 243" comment="Afrikaans (1), Kurdish (2), French (1), Miskito (2), West Frisian (2), Friulian (4), Zazaki (4)" />
<char cp="00FC" tag="sc:Latn" ref="0 100 106 114 119 123 126 127 157 159 161 175 179" comment="German (1), Spanish (1), Afrikaans (1), Turkish (1), Swedish (1), French (1), Azeri (1), Basque (1), Galician (2), Uygur (2), Kaqchikel (4), Bashkir (4)">
<var cp="0075" type="blocked" comment="Required for use with Common LGR" />
<var cp="00FA" type="blocked" comment="Required for use with Common LGR" />
<var cp="028B" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="00FD" tag="sc:Latn" ref="0 101 102 103 121 142 143" comment="Turkmen (1), Czech (1), Icelandic (1), Faroese (2), Guarani (1)">
<var cp="1EF3" type="blocked" comment="Variant due to transitivity" />
<var cp="1EF7" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="00FD" tag="sc:Latn" ref="0 101 102 103 121 142 143" comment="Turkmen (1), Czech (1), Icelandic (1), Faroese (2), Guarani (1)" />
<char cp="00FE" tag="sc:Latn" ref="0 102" comment="Icelandic (1)" />
<char cp="00FF" tag="sc:Latn" ref="0 114 253 257" comment="French (1)" />
<char cp="0101" tag="sc:Latn" ref="0 133 134 135 136" comment="Latvian (1), Tongan (1), Hawaiian (2), Marshallese (1)">
<var cp="00E3" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0103" tag="sc:Latn" ref="0 109 110" comment="Vietnamese (1), Romanian (1)" />
<char cp="0105" tag="sc:Latn" ref="0 137 138" comment="Polish (1), Lithuanian (1)" />
<char cp="0107" tag="sc:Latn" ref="0 150 151 152" comment="Croatian (1), Serbian (1), Polish (1)">
<var cp="010B" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0107" tag="sc:Latn" ref="0 150 151 152" comment="Croatian (1), Serbian (1), Polish (1)" />
<char cp="0109" tag="sc:Latn" ref="0 255" comment="Esperanto (3)" />
<char cp="010B" tag="sc:Latn" ref="0 163" comment="Maltese (1)">
<var cp="0107" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="010D" tag="sc:Latn" ref="0 108 133 150 151 153 154" comment="Croatian (1), Serbian (1), Latvian (1), Slovak (1), Northern Sami (2), Lithuanian (1)" />
<char cp="010F" tag="sc:Latn" ref="0 101 153" comment="Czech (1), Slovak (1)" />
<char cp="0111" tag="sc:Latn" ref="0 108 109 150 151 168" comment="Croatian (1), Serbian (1), Vietnamese (1), Northern Sami (2), Brahui (5)" />
<char cp="0113" tag="sc:Latn" ref="0 133 134 135 184" comment="Latvian (1), Hawaiian (2), Tongan (1), Minangkabau (5)">
<var cp="1EBD" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0113" tag="sc:Latn" ref="0 133 134 135 184" comment="Latvian (1), Hawaiian (2), Tongan (1), Minangkabau (5)" />
<char cp="0117" tag="sc:Latn" ref="0 138 154" comment="Lithuanian (1)" />
<char cp="0119" tag="sc:Latn" ref="0 138 152 154 185" comment="Polish (1), Palauan (2), Lithuanian (1)" />
<char cp="011B" tag="sc:Latn" ref="0 101 172" comment="Czech (1), Sorbian (4)" />
<char cp="011D" tag="sc:Latn" ref="0 255" comment="Esperanto (3)" />
<char cp="011F" tag="sc:Latn" ref="0 127 157 159 201 202" comment="Turkish (1), Tatar (2), Azeri (1), Bashkir (4), Zaza (5)">
<var cp="01E7" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0121" tag="sc:Latn" ref="0 163" comment="Maltese (1)">
<var cp="0123" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0123" tag="sc:Latn" ref="0 133 168" comment="Latvian (1), Brahui (5)">
<var cp="0121" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="011F" tag="sc:Latn" ref="0 127 157 159 201 202" comment="Turkish (1), Tatar (2), Azeri (1), Bashkir (4), Zaza (5)" />
<char cp="0123" tag="sc:Latn" ref="0 133 168" comment="Latvian (1), Brahui (5)" />
<char cp="0125" tag="sc:Latn" ref="0 255" comment="Esperanto (3)" />
<char cp="0127" tag="sc:Latn" ref="0 163" comment="Maltese (1)" />
<char cp="0129" tag="sc:Latn" ref="0 142 143 145 186 209" comment="Guarani (1), Cubeo (3), Khoekhoe (4), Kikuyu (5)">
<var cp="012B" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="012B" tag="sc:Latn" ref="0 133 134 135 138" comment="Latvian (1), Lithuanian (1), Hawaiian (2), Tongan (1)">
<var cp="0129" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="012B" tag="sc:Latn" ref="0 133 134 135 138" comment="Latvian (1), Lithuanian (1), Hawaiian (2), Tongan (1)" />
<char cp="012F" tag="sc:Latn" ref="0 154" comment="Lithuanian (1)" />
<char cp="0131" tag="sc:Latn" ref="0 157 159 201 203" comment="Turkish (1), Tatar (2), Azeri (1)">
<var cp="0069" type="fallback" comment="IDNA2003 Compatibility" />
<var cp="00ED" type="blocked" comment="Required for use with Common LGR" />
<var cp="00EF" type="blocked" comment="Required for use with Common LGR" />
<var cp="0131" type="r-original" comment="Dotless form in the original label" />
<var cp="0269" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
<var cp="1EC9" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0135" tag="sc:Latn" ref="0 255" comment="Esperanto (3)" />
<char cp="0137" tag="sc:Latn" ref="0 133" comment="Latvian (1)" />
<char cp="013A" tag="sc:Latn" ref="0 153" comment="Slovak (1)" />
<char cp="013C" tag="sc:Latn" ref="0 133 168 213 214" comment="Latvian (1), Marshallese (1), Brahui (5)" />
<char cp="013E" tag="sc:Latn" ref="0 153" comment="Slovak (1)" />
<char cp="0142" tag="sc:Latn" ref="0 152" comment="Polish (1)" />
<char cp="0144" tag="sc:Latn" ref="0 107 152 168 172" comment="Polish (1), Lule Sami (2), Sorbian (4), Brahui (5)">
<var cp="006E" type="blocked" comment="Required for use with Common LGR" />
<var cp="014B" type="blocked" comment="Required for use with Common LGR" />
<var cp="1E45" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0146" tag="sc:Latn" ref="0 133 136" comment="Latvian (1), Marshallese (1)" />
<char cp="0148" tag="sc:Latn" ref="0 101 121 153" comment="Turkmen (1), Czech (1), Slovak (1)" />
<char cp="014B" tag="sc:Latn" ref="0 108 125 129 132 146 148 170 188 189 190 191 192 193 194 195 196 197 198 199" comment="Inari Saami (2), Dagaare - Burkina Faso (4), Dagbani (Dagomba), (4), Northern Sami (2), Ewondo (3), Luganda (3), Wolof (4), Adzera (4), Nuer (4), Ga (4), Dinka (4), Duala (3), Ewe (3), Soga (5), Alur (5), Mandinka (5), Acholi (5), Bambara (4), Nuer (4)">
<var cp="006E" type="blocked" comment="Required for use with Common LGR" />
<var cp="0144" type="blocked" comment="Required for use with Common LGR" />
<var cp="1E45" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="014D" tag="sc:Latn" ref="0 134 135 136" comment="Hawaiian (2), Marshallese (1), Tongan (1)">
<var cp="00F5" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0151" tag="sc:Latn" ref="0 233 234" comment="Hungarian (1)" />
<char cp="0153" tag="sc:Latn" ref="0 114 253" comment="French (1)" />
<char cp="0155" tag="sc:Latn" ref="0 153 168" comment="Slovak (1), Brahui (5)" />
@@ -731,41 +532,22 @@
<char cp="0161" tag="sc:Latn" ref="0 108 133 150 151 154 174 230" comment="Tswana (1), Croatian (1), Serbian (1), Latvian (1), Northern Sotho (1), Northern Sami (2), Lithuanian (1)" />
<char cp="0165" tag="sc:Latn" ref="0 101 153" comment="Czech (1), Slovak (1)" />
<char cp="0167" tag="sc:Latn" ref="0 108 168" comment="Northern Sami (2), Brahui (5)" />
<char cp="0169" tag="sc:Latn" ref="0 141 142 143 144 145 209" comment="Umbundu (3), Guarani (1), Nauruan (3), Khoekhoe (4), Kikuyu (5)">
<var cp="016B" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="016B" tag="sc:Latn" ref="0 133 134 135 136 138 154" comment="Latvian (1), Hawaiian (2), Lithuanian (1), Marshallese (1), Tongan (1)">
<var cp="0169" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="016B" tag="sc:Latn" ref="0 133 134 135 136 138 154" comment="Latvian (1), Hawaiian (2), Lithuanian (1), Marshallese (1), Tongan (1)" />
<char cp="016D" tag="sc:Latn" ref="0 255" comment="Esperanto (3)" />
<char cp="016F" tag="sc:Latn" ref="0 101" comment="Czech (1)" />
<char cp="0171" tag="sc:Latn" ref="0 233 234" comment="Hungarian (1)" />
<char cp="0173" tag="sc:Latn" ref="0 138 154" comment="Lithuanian (1)" />
<char cp="0175" tag="sc:Latn" ref="0 247 256" comment="Chichewa (3), Welsh (2)" />
<char cp="0177" tag="sc:Latn" ref="0 256" comment="Welsh (2)" />
<char cp="017A" tag="sc:Latn" ref="0 152 168 172 252 258" comment="Polish (1), Brahui (5), Sorbian (4), Montenegrin (1)">
<var cp="017C" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="017C" tag="sc:Latn" ref="0 152 163" comment="Polish (1), Maltese (1)">
<var cp="017A" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="017A" tag="sc:Latn" ref="0 152 168 172 252 258" comment="Polish (1), Brahui (5), Sorbian (4), Montenegrin (1)" />
<char cp="017E" tag="sc:Latn" ref="0 108 121 133 150 151 153 154 232" comment="Lithuanian (1), Croatian (1), Serbian (1), Turkmen (1), Latvian (1), Slovak (1), Northern Sami (2), Chechen (2) 1925 Version" />
<char cp="0188" tag="sc:Latn" ref="0 277" comment="Serer (5)" />
<char cp="0192" tag="sc:Latn" ref="0 170" comment="Ewe (3)">
<var cp="0066" type="blocked" comment="Generally acceptable alternate glyph" />
</char>
<char cp="0199" tag="sc:Latn" ref="0 147" comment="Hausa (2)" />
<char cp="01A1" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="01A5" tag="sc:Latn" ref="0 277" comment="Serer (5)" />
<char cp="01AD" tag="sc:Latn" ref="0 277" comment="Serer (5)" />
<char cp="01B0" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="01B4" tag="sc:Latn" ref="0 148 149 251" comment="Dagaare - Burkina Faso (4), Fula (3)" />
<char cp="01DD" tag="sc:Latn" ref="0 240" comment="Kanuri (3)">
<var cp="0259" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="01E7" tag="sc:Latn" ref="0 113" comment="Skolt Sami (2)">
<var cp="011F" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="01E9" tag="sc:Latn" ref="0 113" comment="Skolt Sami (2)" />
<char cp="01EF" tag="sc:Latn" ref="0 113" comment="Skolt Sami (2)" />
<char cp="0219" tag="sc:Latn" ref="3 110" comment="Romanian (1)" />
@@ -773,49 +555,17 @@
<char cp="024D" tag="sc:Latn" ref="8 240" comment="Kanuri (3)" />
<char cp="0253" tag="sc:Latn" ref="0 147 148 250" comment="Hausa (2), Dagaare - Burkina Faso (4), Pulaar (3)" />
<char cp="0254" tag="sc:Latn" ref="0 129 146 148 169 170 189 190 193 194 236 237" comment="Dagaare - Burkina Faso (4), Dagbani (Dagomba) (4), Lingala (2), Akan (3), Ewondo (3), Fon (3), Nuer (4), Ga (4), Duala (3), EWE (3), Nuer (4)" />
<char cp="0254 0308" ref="125" comment="DINKA (4)" />
<char cp="0254 0331" ref="129 146" comment="Nuer (4)" />
<char cp="0256" tag="sc:Latn" ref="0 169 170" comment="Fon (3), Ewe (3)" />
<char cp="0257" tag="sc:Latn" ref="0 147 149 250" comment="Hausa (2), Fula (3)" />
<char cp="0259" tag="sc:Latn" ref="0 159 170 190 241" comment="Azeri, Azerbaijani (1), Ewondo (3), Ewe (3), Bugis (3)">
<var cp="01DD" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0259" tag="sc:Latn" ref="0 159 170 190 241" comment="Azeri, Azerbaijani (1), Ewondo (3), Ewe (3), Bugis (3)" />
<char cp="025B" tag="sc:Latn" ref="0 129 148 169 170 189 190 193 194 199 212 236 237 238" comment="Dagaare - Burkina Faso (4), Lingala (2), Akan (3), Ewondo (3), Dagbani (Dagomba), (4), Fon (3), Mossi (3), Ga (4), Ewe (3), Duala (3), Bambara (4), Nuer (4)" />
<char cp="025B 0308" ref="125 129 146 239" comment="Nuer (4), Dinka (4)" />
<char cp="025B 0331" ref="129 146 239" comment="Nuer (4)" />
<char cp="025B 0331 0308" ref="129 146 239" comment="Nuer (4)" />
<char cp="0260" tag="sc:Latn" ref="0 278" comment="Kpelle (5)" />
<char cp="0263" tag="sc:Latn" ref="0 125 129 146 170 189" comment="Dagbani (Dagomba) (4), Nuer (4), Dinka (4), Ewe (3), Nuer (4)">
<var cp="0079" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="0268" tag="sc:Latn" ref="0 186 189 210 211" comment="Cubeo (3), Dagbani (Dagomba) (4), HIxkaryána (4), Maasai (5)" />
<char cp="0268 0303" ref="186" comment="Cubeo (3)" />
<char cp="0269" tag="sc:Latn" ref="0 148 212" comment="Dagaare - Burkina Faso (4), Mossi (3)">
<var cp="0069" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
<var cp="00ED" type="blocked" comment="Required for use with Common LGR" />
<var cp="00EF" type="blocked" comment="Required for use with Common LGR" />
<var cp="0131" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
<var cp="1EC9" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="0272" tag="sc:Latn" ref="0 199 218 219" comment="Susu (4), Zarma (4), Bambara (4)" />
<char cp="0289" tag="sc:Latn" ref="0 186 187 211" comment="Cubeo (3), Maasai (5)" />
<char cp="0289 0303" ref="186 187" comment="Cubeo (3)" />
<char cp="028B" tag="sc:Latn" ref="0 148 170 212 238" comment="Dagaare - Burkina Faso (4), Mossi (3), Ewe (3)">
<var cp="0075" type="blocked" comment="Required for use with Common LGR" />
<var cp="00FA" type="blocked" comment="Required for use with Common LGR" />
<var cp="00FC" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="0292" tag="sc:Latn" ref="0 113 189" comment="Skolt Sami (2), Dagbani (Dagomba) (4)" />
<char cp="1E13" tag="sc:Latn" ref="0 164 257" comment="Venda (1)" />
<char cp="1E21" tag="sc:Latn" ref="0 200" comment="Raga (Hano) (3)">
<var cp="0067 0303" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="1E3D" tag="sc:Latn" ref="0 164 257" comment="Venda (1)" />
<char cp="1E45" tag="sc:Latn" ref="0 164 257" comment="Venda (1)">
<var cp="006E" type="blocked" comment="Required for use with Common LGR" />
<var cp="0144" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
<var cp="014B" type="blocked" comment="Required for use with Common LGR" />
</char>
<char cp="1E49" tag="sc:Latn" ref="0 220" comment="Pitjantjatjara (4)" />
<char cp="1E4B" tag="sc:Latn" ref="0 164 257" comment="Venda (1)" />
<char cp="1E63" tag="sc:Latn" ref="0 254" comment="Yoruba (2)" />
@@ -823,9 +573,6 @@
<char cp="1E71" tag="sc:Latn" ref="0 164 257" comment="Venda (1)" />
<char cp="1E8D" tag="sc:Latn" ref="0 248 249" comment="Mam (4)" />
<char cp="1EA1" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EA3" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)">
<var cp="00E0" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="1EA5" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EA7" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EA9" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
@@ -837,31 +584,14 @@
<char cp="1EB5" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EB7" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EB9" tag="sc:Latn" ref="0 254" comment="Yoruba (2)" />
<char cp="1EB9 0300" ref="254" comment="Yoruba (2)" />
<char cp="1EB9 0301" ref="254" comment="Yoruba (2)" />
<char cp="1EBB" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EBD" tag="sc:Latn" ref="0 117 141 142 143 186 187 275" comment="Umbundu (3), Guarani (1), Cubeo (3), Xavante (4)">
<var cp="0113" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="1EBF" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EC1" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EC3" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EC5" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EC7" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EC9" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)">
<var cp="0069" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
<var cp="00ED" type="blocked" comment="Required for use with Common LGR" />
<var cp="00EF" type="blocked" comment="Required for use with Common LGR" />
<var cp="0131" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
<var cp="0269" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="1ECB" tag="sc:Latn" ref="0 205" comment="Igbo (2)" />
<char cp="1ECD" tag="sc:Latn" ref="0 136 204 205 215 216 254" comment="Igbo (2), Yoruba (2), Marshallese (1)" />
<char cp="1ECD 0300" ref="254" comment="Yoruba (2)" />
<char cp="1ECD 0301" ref="254" comment="Yoruba (2)" />
<char cp="1ECF" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)">
<var cp="00F2" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="1ED1" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1ED3" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1ED5" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
@@ -873,23 +603,12 @@
<char cp="1EE1" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EE3" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EE5" tag="sc:Latn" ref="0 109 204 205" comment="Vietnamese (1), Igbo (2)" />
<char cp="1EE7" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)">
<var cp="00F9" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="1EE9" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EEB" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EED" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EEF" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EF1" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EF3" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)">
<var cp="00FD" type="blocked" comment="Variant due to transitivity" />
<var cp="1EF7" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="1EF5" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)" />
<char cp="1EF7" tag="sc:Latn" ref="0 109" comment="Vietnamese (1)">
<var cp="00FD" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
<var cp="1EF3" type="blocked" comment="Glyphs either homoglyph or nearly identical" />
</char>
<char cp="1EF9" tag="sc:Latn" ref="0 109 142" comment="Vietnamese (1), Guarani (1)" />
</data>
<!--Rules section goes here-->
@@ -928,18 +647,6 @@
</rule>
</choice>
</rule>
<rule name="surrounded-by-L" comment="code point both follows and precedes L, required context for Ela Geminada &#x235F;">
<look-behind>
<char cp="006C" />
</look-behind>
<anchor />
<look-ahead>
<char cp="006C" />
</look-ahead>
</rule>
<rule name="dot-L-dot" comment="labels with one L sharing two middle dots are invalid &#x235F;">
<char cp="00B7 006C 00B7" />
</rule>
<!--Action elements go here - order defines precedence-->
<action disp="invalid" match="leading-combining-mark" comment="labels with leading combining marks are invalid &#x235F;" />
<action disp="invalid" any-variant="out-of-repertoire-var" comment="any variant label with a code point out of repertoire is invalid &#x235F;" />

View File

@@ -86,7 +86,7 @@ public final class ForeignKeyUtils {
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> load(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
return load(clazz, foreignKeys, false).entrySet().stream()
return loadMostRecentResources(clazz, foreignKeys, false).entrySet().stream()
.filter(e -> now.isBefore(e.getValue().deletionTime()))
.collect(toImmutableMap(Entry::getKey, e -> VKey.create(clazz, e.getValue().repoId())));
}
@@ -104,8 +104,9 @@ public final class ForeignKeyUtils {
* same max {@code deleteTime}, usually {@code END_OF_TIME}, lest this method throws an error due
* to duplicate keys.
*/
private static <E extends EppResource> ImmutableMap<String, MostRecentResource> load(
Class<E> clazz, Collection<String> foreignKeys, boolean useReplicaTm) {
public static <E extends EppResource>
ImmutableMap<String, MostRecentResource> loadMostRecentResources(
Class<E> clazz, Collection<String> foreignKeys, boolean useReplicaTm) {
String fkProperty = RESOURCE_TYPE_TO_FK_PROPERTY.get(clazz);
JpaTransactionManager tmToUse = useReplicaTm ? replicaTm() : tm();
return tmToUse.reTransact(
@@ -148,7 +149,7 @@ public final class ForeignKeyUtils {
ImmutableList<String> foreignKeys =
keys.stream().map(key -> (String) key.getKey()).collect(toImmutableList());
ImmutableMap<String, MostRecentResource> existingKeys =
ForeignKeyUtils.load(clazz, foreignKeys, true);
ForeignKeyUtils.loadMostRecentResources(clazz, foreignKeys, true);
// The above map only contains keys that exist in the database, so we re-add the
// missing ones with Optional.empty() values for caching.
return Maps.asMap(
@@ -234,7 +235,7 @@ public final class ForeignKeyUtils {
e -> VKey.create(clazz, e.getValue().get().repoId())));
}
record MostRecentResource(String repoId, DateTime deletionTime) {
public record MostRecentResource(String repoId, DateTime deletionTime) {
static MostRecentResource create(String repoId, DateTime deletionTime) {
return new MostRecentResource(repoId, deletionTime);

View File

@@ -15,7 +15,6 @@
package google.registry.model.tmch;
import static google.registry.config.RegistryConfig.getClaimsListCacheDuration;
import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -79,14 +78,11 @@ public class ClaimsListDao {
*/
private static ClaimsList getUncached() {
return tm().reTransact(
() -> {
Long revisionId =
tm().query("SELECT MAX(revisionId) FROM ClaimsList", Long.class)
.getSingleResult();
return tm().createQueryComposer(ClaimsList.class)
.where("revisionId", EQ, revisionId)
.first();
})
() ->
tm().query("FROM ClaimsList ORDER BY revisionId DESC", ClaimsList.class)
.setMaxResults(1)
.getResultStream()
.findFirst())
.orElse(ClaimsList.create(START_OF_TIME, ImmutableMap.of()));
}

View File

@@ -30,6 +30,7 @@ import google.registry.export.DriveModule;
import google.registry.export.sheet.SheetsServiceModule;
import google.registry.flows.ServerTridProviderModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
import google.registry.groups.DirectoryModule;
import google.registry.groups.GmailModule;
import google.registry.groups.GroupsModule;
@@ -66,6 +67,7 @@ import jakarta.inject.Singleton;
CredentialModule.class,
CustomLogicFactoryModule.class,
DirectoryModule.class,
DomainDeletionTimeCacheModule.class,
DriveModule.class,
GmailModule.class,
GroupsModule.class,

View File

@@ -23,6 +23,7 @@ import google.registry.batch.DeleteLoadTestDataAction;
import google.registry.batch.DeleteProberDataAction;
import google.registry.batch.ExpandBillingRecurrencesAction;
import google.registry.batch.RelockDomainAction;
import google.registry.batch.RemoveAllDomainContactsAction;
import google.registry.batch.ResaveAllEppResourcesPipelineAction;
import google.registry.batch.ResaveEntityAction;
import google.registry.batch.SendExpiringCertificateNotificationEmailAction;
@@ -270,6 +271,8 @@ interface RequestComponent {
ReadinessProbeActionFrontend readinessProbeActionFrontend();
RemoveAllDomainContactsAction removeAllDomainContactsAction();
RdapAutnumAction rdapAutnumAction();
RdapDomainAction rdapDomainAction();

View File

@@ -26,6 +26,7 @@ import google.registry.export.DriveModule;
import google.registry.export.sheet.SheetsServiceModule;
import google.registry.flows.ServerTridProviderModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
import google.registry.groups.DirectoryModule;
import google.registry.groups.GmailModule;
import google.registry.groups.GroupsModule;
@@ -56,6 +57,7 @@ import jakarta.inject.Singleton;
CloudTasksUtilsModule.class,
CredentialModule.class,
CustomLogicFactoryModule.class,
DomainDeletionTimeCacheModule.class,
DirectoryModule.class,
DriveModule.class,
GmailModule.class,

View File

@@ -23,6 +23,7 @@ import google.registry.batch.DeleteLoadTestDataAction;
import google.registry.batch.DeleteProberDataAction;
import google.registry.batch.ExpandBillingRecurrencesAction;
import google.registry.batch.RelockDomainAction;
import google.registry.batch.RemoveAllDomainContactsAction;
import google.registry.batch.ResaveAllEppResourcesPipelineAction;
import google.registry.batch.ResaveEntityAction;
import google.registry.batch.SendExpiringCertificateNotificationEmailAction;
@@ -153,6 +154,8 @@ public interface BackendRequestComponent {
RelockDomainAction relockDomainAction();
RemoveAllDomainContactsAction removeAllDomainContactsAction();
ResaveAllEppResourcesPipelineAction resaveAllEppResourcesPipelineAction();
ResaveEntityAction resaveEntityAction();

View File

@@ -22,6 +22,7 @@ import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.flows.ServerTridProviderModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
import google.registry.groups.DirectoryModule;
import google.registry.groups.GmailModule;
import google.registry.groups.GroupsModule;
@@ -50,6 +51,7 @@ import jakarta.inject.Singleton;
CustomLogicFactoryModule.class,
CloudTasksUtilsModule.class,
DirectoryModule.class,
DomainDeletionTimeCacheModule.class,
FrontendRequestComponentModule.class,
GmailModule.class,
GroupsModule.class,

View File

@@ -23,6 +23,7 @@ import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.export.DriveModule;
import google.registry.flows.ServerTridProviderModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
import google.registry.groups.DirectoryModule;
import google.registry.groups.GroupsModule;
import google.registry.groups.GroupssettingsModule;
@@ -47,6 +48,7 @@ import jakarta.inject.Singleton;
CustomLogicFactoryModule.class,
CloudTasksUtilsModule.class,
DirectoryModule.class,
DomainDeletionTimeCacheModule.class,
DriveModule.class,
GroupsModule.class,
GroupssettingsModule.class,

View File

@@ -47,6 +47,11 @@ final class CreateCdnsTld extends ConfirmingCommand {
)
String name;
@Parameter(
names = "--skip_sandbox_tld_check",
description = "In Sandbox, skip the dns_name format check.")
boolean skipSandboxTldCheck;
@Inject
@Config("projectId")
String projectId;
@@ -61,10 +66,15 @@ final class CreateCdnsTld extends ConfirmingCommand {
protected void init() {
// Sandbox talks to production Cloud DNS. As a result, we can't configure any domains with a
// suffix that might be used by customers on the same nameserver set. Limit the user to setting
// up *.test TLDs.
if (RegistryToolEnvironment.get() == RegistryToolEnvironment.SANDBOX
&& !dnsName.endsWith(".test.")) {
throw new IllegalArgumentException("Sandbox TLDs must be of the form \"*.test.\"");
// up *.test TLDs unless the user declares that the name is approved.
//
// The name format check simply provides a user-friendly error message. If the user wrongly
// declares name approval, the request to the Cloud DNS API will still fail.
if (RegistryToolEnvironment.get() == RegistryToolEnvironment.SANDBOX) {
if (!skipSandboxTldCheck && !dnsName.endsWith(".test.")) {
throw new IllegalArgumentException(
"Sandbox TLDs must be approved or in the form \"*.test.\"");
}
}
managedZone =

View File

@@ -15,6 +15,7 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Sets.difference;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEFAULT;
@@ -46,6 +47,9 @@ import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.RegistrationBehavior;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.model.registrar.Registrar;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tlds;
import google.registry.persistence.VKey;
import google.registry.tools.params.MoneyParameter;
import google.registry.tools.params.TransitionListParameter.TokenStatusTransitions;
@@ -291,10 +295,12 @@ class GenerateAllocationTokensCommand implements Command {
!ImmutableList.of("").equals(allowedClientIds),
"Either omit --allowed_client_ids if all registrars are allowed, or include a"
+ " comma-separated list");
verifyAllRegistrarIdsExist(allowedClientIds);
checkArgument(
!ImmutableList.of("").equals(allowedTlds),
"Either omit --allowed_tlds if all TLDs are allowed, or include a comma-separated list");
verifyAllTldsExist(allowedTlds);
if (ImmutableList.of("").equals(allowedEppActions)) {
allowedEppActions = ImmutableList.of();
@@ -326,6 +332,34 @@ class GenerateAllocationTokensCommand implements Command {
}
}
static void verifyAllRegistrarIdsExist(@Nullable List<String> allowedClientIds) {
// a null/empty list means that all registrars are allowed
if (isNullOrEmpty(allowedClientIds)) {
return;
}
ImmutableSet<String> allRegistrarIds =
Registrar.loadAllKeysCached().stream()
.map(VKey::getKey)
.map(Object::toString)
.collect(toImmutableSet());
ImmutableList<String> badRegistrarIds =
allowedClientIds.stream()
.filter(id -> !allRegistrarIds.contains(id))
.collect(toImmutableList());
checkArgument(badRegistrarIds.isEmpty(), "Unknown registrar ID(s) %s", badRegistrarIds);
}
static void verifyAllTldsExist(@Nullable List<String> allowedTlds) {
// a null/empty list means that all TLDs are allowed
if (isNullOrEmpty(allowedTlds)) {
return;
}
ImmutableSet<String> allTlds = Tlds.getTldsOfType(Tld.TldType.REAL);
ImmutableList<String> badTlds =
allowedTlds.stream().filter(tld -> !allTlds.contains(tld)).collect(toImmutableList());
checkArgument(badTlds.isEmpty(), "Unknown REAL TLD(s) %s", badTlds);
}
private void verifyTokenStringsDoNotExist() {
ImmutableSet<String> existingTokenStrings =
getExistingTokenStrings(ImmutableSet.copyOf(tokenStrings));

View File

@@ -157,6 +157,9 @@ final class UpdateAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
endToken = true;
}
GenerateAllocationTokensCommand.verifyAllRegistrarIdsExist(allowedClientIds);
GenerateAllocationTokensCommand.verifyAllTldsExist(allowedTlds);
tokensToSave =
tm().transact(
() ->

View File

@@ -0,0 +1,22 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<update>
<domain:update xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN%</domain:name>
<domain:rem>
%CONTACTS%
</domain:rem>
<domain:chg>
<domain:registrant/>
</domain:chg>
</domain:update>
</update>
<extension>
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>Registry minimum data set phase 3: Removed all contacts from domain.</metadata:reason>
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -0,0 +1,119 @@
// Copyright 2025 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.batch;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.newDomain;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.mockito.Mockito.mock;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.util.concurrent.RateLimiter;
import google.registry.flows.DaggerEppTestComponent;
import google.registry.flows.EppController;
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeLockHandler;
import google.registry.testing.FakeResponse;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link RemoveAllDomainContactsAction}. */
class RemoveAllDomainContactsActionTest {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final FakeResponse response = new FakeResponse();
private final RateLimiter rateLimiter = mock(RateLimiter.class);
private RemoveAllDomainContactsAction action;
@BeforeEach
void beforeEach() {
createTld("tld");
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, ACTIVE))
.build());
EppController eppController =
DaggerEppTestComponent.builder()
.fakesAndMocksModule(FakesAndMocksModule.create(new FakeClock()))
.build()
.startRequest()
.eppController();
action =
new RemoveAllDomainContactsAction(
eppController, "NewRegistrar", new FakeLockHandler(true), rateLimiter, response);
}
@Test
void test_removesAllContactsFromMultipleDomains_andDoesntModifyDomainThatHasNoContacts() {
Contact c1 = persistActiveContact("contact12345");
Domain d1 = persistResource(newDomain("foo.tld", c1));
assertThat(d1.getAllContacts()).hasSize(3);
Contact c2 = persistActiveContact("contact23456");
Domain d2 = persistResource(newDomain("bar.tld", c2));
assertThat(d2.getAllContacts()).hasSize(3);
Domain d3 =
persistResource(
newDomain("baz.tld")
.asBuilder()
.setRegistrant(Optional.empty())
.setContacts(ImmutableSet.of())
.build());
assertThat(d3.getAllContacts()).isEmpty();
DateTime lastUpdate = d3.getUpdateTimestamp().getTimestamp();
action.run();
assertThat(loadByEntity(d1).getAllContacts()).isEmpty();
assertThat(loadByEntity(d2).getAllContacts()).isEmpty();
assertThat(loadByEntity(d3).getUpdateTimestamp().getTimestamp()).isEqualTo(lastUpdate);
}
@Test
void test_removesContacts_onDomainsThatOnlyPartiallyHaveContacts() {
Contact c1 = persistActiveContact("contact12345");
Domain d1 =
persistResource(
newDomain("foo.tld", c1).asBuilder().setContacts(ImmutableSet.of()).build());
assertThat(d1.getAllContacts()).hasSize(1);
Contact c2 = persistActiveContact("contact23456");
Domain d2 =
persistResource(
newDomain("bar.tld", c2).asBuilder().setRegistrant(Optional.empty()).build());
assertThat(d2.getAllContacts()).hasSize(2);
action.run();
assertThat(loadByEntity(d1).getAllContacts()).isEmpty();
assertThat(loadByEntity(d2).getAllContacts()).isEmpty();
}
}

View File

@@ -25,6 +25,7 @@ import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.config.RegistryConfig.ConfigModule.TmchCaMode;
import google.registry.flows.custom.CustomLogicFactory;
import google.registry.flows.custom.TestCustomLogicFactory;
import google.registry.flows.domain.DomainDeletionTimeCache;
import google.registry.flows.domain.DomainFlowTmchUtils;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.request.RequestScope;
@@ -126,6 +127,11 @@ public interface EppTestComponent {
ServerTridProvider provideServerTridProvider() {
return new FakeServerTridProvider();
}
@Provides
DomainDeletionTimeCache provideDomainDeletionTimeCache() {
return DomainDeletionTimeCache.create();
}
}
class FakeServerTridProvider implements ServerTridProvider {

View File

@@ -149,7 +149,6 @@ import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeem
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.ResourceAlreadyExistsForThisClientException;
import google.registry.flows.exceptions.ResourceCreateContentionException;
import google.registry.model.billing.BillingBase;
import google.registry.model.billing.BillingBase.Flag;
@@ -1238,8 +1237,8 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
void testFailure_alreadyExists() throws Exception {
persistContactsAndHosts();
persistActiveDomain(getUniqueIdFromCommand());
ResourceAlreadyExistsForThisClientException thrown =
assertThrows(ResourceAlreadyExistsForThisClientException.class, this::runFlow);
ResourceCreateContentionException thrown =
assertThrows(ResourceCreateContentionException.class, this::runFlow);
assertAboutEppExceptions()
.that(thrown)
.marshalsToXml()

View File

@@ -0,0 +1,85 @@
// Copyright 2025 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.domain;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistDomainAsDeleted;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import google.registry.model.domain.Domain;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link DomainDeletionTimeCache}. */
public class DomainDeletionTimeCacheTest {
private final FakeClock clock = new FakeClock(DateTime.parse("2025-10-01T00:00:00.000Z"));
private final DomainDeletionTimeCache cache = DomainDeletionTimeCache.create();
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
@BeforeEach
void beforeEach() {
DatabaseHelper.createTld("tld");
}
@Test
void testDomainAvailable_null() {
assertThat(getDeletionTimeFromCache("nonexistent.tld")).isEmpty();
}
@Test
void testDomainNotAvailable_notDeleted() {
persistActiveDomain("active.tld");
assertThat(getDeletionTimeFromCache("active.tld")).hasValue(END_OF_TIME);
}
@Test
void testDomainAvailable_deletedInFuture() {
persistDomainAsDeleted(persistActiveDomain("domain.tld"), clock.nowUtc().plusDays(1));
assertThat(getDeletionTimeFromCache("domain.tld")).hasValue(clock.nowUtc().plusDays(1));
}
@Test
void testCache_returnsOldData() {
Domain domain = persistActiveDomain("domain.tld");
assertThat(getDeletionTimeFromCache("domain.tld")).hasValue(END_OF_TIME);
persistDomainAsDeleted(domain, clock.nowUtc().plusDays(1));
// Without intervention, the cache should have the old data
assertThat(getDeletionTimeFromCache("domain.tld")).hasValue(END_OF_TIME);
}
@Test
void testCache_returnsNewDataAfterDomainCreate() {
// Null deletion dates (meaning an avilable domain) shouldn't be cached
assertThat(getDeletionTimeFromCache("domain.tld")).isEmpty();
persistDomainAsDeleted(persistActiveDomain("domain.tld"), clock.nowUtc().plusDays(1));
assertThat(getDeletionTimeFromCache("domain.tld")).hasValue(clock.nowUtc().plusDays(1));
}
private Optional<DateTime> getDeletionTimeFromCache(String domainName) {
return tm().transact(() -> cache.getDeletionTimeForDomain(domainName));
}
}

View File

@@ -20,6 +20,7 @@ import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig;
import google.registry.flows.ServerTridProviderModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.domain.DomainDeletionTimeCacheModule;
import google.registry.groups.GmailModule;
import google.registry.groups.GroupsModule;
import google.registry.groups.GroupssettingsModule;
@@ -43,6 +44,7 @@ import jakarta.inject.Singleton;
CredentialModule.class,
CustomLogicFactoryModule.class,
CloudTasksUtilsModule.class,
DomainDeletionTimeCacheModule.class,
FrontendRequestComponent.FrontendRequestComponentModule.class,
GmailModule.class,
GroupsModule.class,

View File

@@ -76,11 +76,37 @@ class CreateCdnsTldTest extends CommandTestCase<CreateCdnsTld> {
@Test
@MockitoSettings(strictness = Strictness.LENIENT)
void testSandboxTldRestrictions() {
void testSandboxTldRestrictions_Disallowed() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> runCommandInEnvironment(RegistryToolEnvironment.SANDBOX, "--dns_name=foobar."));
assertThat(thrown).hasMessageThat().contains("Sandbox TLDs must be of the form \"*.test.\"");
() ->
runCommandInEnvironment(
RegistryToolEnvironment.SANDBOX,
"--dns_name=foobar.",
"--description=test run",
"--force"));
assertThat(thrown)
.hasMessageThat()
.contains("Sandbox TLDs must be approved or in the form \"*.test.\"");
}
@Test
void testSandboxTldRestrictions_tldCheckSkipped() throws Exception {
runCommandInEnvironment(
RegistryToolEnvironment.SANDBOX,
"--dns_name=foobar.",
"--description=test run",
"--force",
"--skip_sandbox_tld_check");
}
@Test
void testSandboxTldRestrictions_testTld() throws Exception {
runCommandInEnvironment(
RegistryToolEnvironment.SANDBOX,
"--dns_name=abc.test.",
"--description=test run",
"--force");
}
}

View File

@@ -39,6 +39,7 @@ import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.DeterministicStringGenerator.Rule;
import google.registry.util.StringGenerator.Alphabets;
@@ -56,6 +57,7 @@ class GenerateAllocationTokensCommandTest extends CommandTestCase<GenerateAlloca
@BeforeEach
void beforeEach() {
DatabaseHelper.createTlds("tld", "example");
command.stringGenerator = new DeterministicStringGenerator(Alphabets.BASE_58);
}
@@ -531,6 +533,26 @@ class GenerateAllocationTokensCommandTest extends CommandTestCase<GenerateAlloca
.isEqualTo("For DEFAULT_PROMO tokens, must specify --token_status_transitions");
}
@Test
void testFailure_badTld() {
assertThat(
assertThrows(
IllegalArgumentException.class,
() -> runCommand("--number", "10", "--allowed_tlds", "badtld")))
.hasMessageThat()
.isEqualTo("Unknown REAL TLD(s) [badtld]");
}
@Test
void testFailure_badRegistrar() {
assertThat(
assertThrows(
IllegalArgumentException.class,
() -> runCommand("--number", "10", "--allowed_client_ids", "badregistrar")))
.hasMessageThat()
.isEqualTo("Unknown registrar ID(s) [badregistrar]");
}
private AllocationToken createToken(
String token,
@Nullable HistoryEntryId redemptionHistoryEntryId,

View File

@@ -40,14 +40,21 @@ import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.RegistrationBehavior;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.testing.DatabaseHelper;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link UpdateAllocationTokensCommand}. */
class UpdateAllocationTokensCommandTest extends CommandTestCase<UpdateAllocationTokensCommand> {
@BeforeEach
void beforeEach() {
DatabaseHelper.createTlds("tld", "example");
}
@Test
void testUpdateTlds_setTlds() throws Exception {
AllocationToken token =
@@ -64,14 +71,24 @@ class UpdateAllocationTokensCommandTest extends CommandTestCase<UpdateAllocation
assertThat(reloadResource(token).getAllowedTlds()).isEmpty();
}
@Test
void testUpdateTlds_badTlds() {
persistResource(builderWithPromo().build());
assertThat(
assertThrows(
IllegalArgumentException.class, () -> runCommandForced("--allowed_tlds=badtld")))
.hasMessageThat()
.isEqualTo("Unknown REAL TLD(s) [badtld]");
}
@Test
void testUpdateClientIds_setClientIds() throws Exception {
AllocationToken token =
persistResource(
builderWithPromo().setAllowedRegistrarIds(ImmutableSet.of("toRemove")).build());
runCommandForced("--prefix", "token", "--allowed_client_ids", "clientone,clienttwo");
runCommandForced("--prefix", "token", "--allowed_client_ids", "TheRegistrar,NewRegistrar");
assertThat(reloadResource(token).getAllowedRegistrarIds())
.containsExactly("clientone", "clienttwo");
.containsExactly("TheRegistrar", "NewRegistrar");
}
@Test
@@ -83,6 +100,17 @@ class UpdateAllocationTokensCommandTest extends CommandTestCase<UpdateAllocation
assertThat(reloadResource(token).getAllowedRegistrarIds()).isEmpty();
}
@Test
void testUpdateClientIds_badClientId() {
persistResource(builderWithPromo().build());
assertThat(
assertThrows(
IllegalArgumentException.class,
() -> runCommandForced("--allowed_client_ids=badregistrar")))
.hasMessageThat()
.isEqualTo("Unknown registrar ID(s) [badregistrar]");
}
@Test
void testUpdateEppActions_setEppActions() throws Exception {
AllocationToken token =

View File

@@ -26,6 +26,7 @@ BACKEND /_dr/task/rdeUpload RdeUploadAction
BACKEND /_dr/task/readDnsRefreshRequests ReadDnsRefreshRequestsAction POST y APP ADMIN
BACKEND /_dr/task/refreshDnsOnHostRename RefreshDnsOnHostRenameAction POST n APP ADMIN
BACKEND /_dr/task/relockDomain RelockDomainAction POST y APP ADMIN
BACKEND /_dr/task/removeAllDomainContacts RemoveAllDomainContactsAction POST n APP ADMIN
BACKEND /_dr/task/resaveAllEppResourcesPipeline ResaveAllEppResourcesPipelineAction GET n APP ADMIN
BACKEND /_dr/task/resaveEntity ResaveEntityAction POST n APP ADMIN
BACKEND /_dr/task/sendExpiringCertificateNotificationEmail SendExpiringCertificateNotificationEmailAction GET n APP ADMIN

View File

@@ -44,6 +44,7 @@ BACKEND /_dr/task/readDnsRefreshRequests ReadDnsRefreshReques
BACKEND /_dr/task/refreshDnsForAllDomains RefreshDnsForAllDomainsAction GET n APP ADMIN
BACKEND /_dr/task/refreshDnsOnHostRename RefreshDnsOnHostRenameAction POST n APP ADMIN
BACKEND /_dr/task/relockDomain RelockDomainAction POST y APP ADMIN
BACKEND /_dr/task/removeAllDomainContacts RemoveAllDomainContactsAction POST n APP ADMIN
BACKEND /_dr/task/resaveAllEppResourcesPipeline ResaveAllEppResourcesPipelineAction GET n APP ADMIN
BACKEND /_dr/task/resaveEntity ResaveEntityAction POST n APP ADMIN
BACKEND /_dr/task/sendExpiringCertificateNotificationEmail SendExpiringCertificateNotificationEmailAction GET n APP ADMIN

View File

@@ -261,11 +261,11 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2025-09-05 16:11:29</td>
<td class="property_value">2025-09-29 21:19:42</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V197__poc_rlock_drop_not_null.sql</td>
<td id="lastFlywayFile" class="property_value">V209__poll_message_hash.sql</td>
</tr>
</tbody>
</table>
@@ -273,7 +273,7 @@ td.section {
<p>&nbsp;</p>
<svg viewBox="0.00 0.00 4903.00 3732.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 3728)">
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="transparent" points="-4,4 -4,-3728 4899,-3728 4899,4 -4,4" /> <text text-anchor="start" x="4655" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text text-anchor="start" x="4738" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.27.1</text> <text text-anchor="start" x="4654" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text text-anchor="start" x="4738" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-09-05 16:11:29</text> <polygon fill="none" stroke="#888888" points="4651,-4 4651,-44 4887,-44 4887,-4 4651,-4" /> <!-- allocationtoken_a08ccbef -->
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="transparent" points="-4,4 -4,-3728 4899,-3728 4899,4 -4,4" /> <text text-anchor="start" x="4655" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text text-anchor="start" x="4738" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.27.1</text> <text text-anchor="start" x="4654" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text text-anchor="start" x="4738" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-09-29 21:19:42</text> <polygon fill="none" stroke="#888888" points="4651,-4 4651,-44 4887,-44 4887,-4 4651,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
<title>allocationtoken_a08ccbef</title> <polygon fill="#e9c2f2" stroke="transparent" points="481.5,-978 481.5,-997 667.5,-997 667.5,-978 481.5,-978" /> <text text-anchor="start" x="483.5" y="-984.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."AllocationToken"</text> <polygon fill="#e9c2f2" stroke="transparent" points="667.5,-978 667.5,-997 741.5,-997 741.5,-978 667.5,-978" /> <text text-anchor="start" x="702.5" y="-983.8" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text text-anchor="start" x="483.5" y="-965.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">token</text> <text text-anchor="start" x="661.5" y="-964.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-964.8" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text text-anchor="start" x="483.5" y="-945.8" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text> <text text-anchor="start" x="661.5" y="-945.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-945.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text text-anchor="start" x="483.5" y="-926.8" font-family="Helvetica,sans-Serif" font-size="14.00">redemption_domain_repo_id</text> <text text-anchor="start" x="661.5" y="-926.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-926.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text text-anchor="start" x="483.5" y="-907.8" font-family="Helvetica,sans-Serif" font-size="14.00">token_type</text> <text text-anchor="start" x="661.5" y="-907.8" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text text-anchor="start" x="669.5" y="-907.8" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <polygon fill="none" stroke="#888888" points="480.5,-901.5 480.5,-998.5 742.5,-998.5 742.5,-901.5 480.5,-901.5" />
</g>

File diff suppressed because one or more lines are too long

View File

@@ -195,3 +195,15 @@ V194__password_reset_request_registrar.sql
V195__registrar_poc_id.sql
V196__tld_expiry_access_period_enabled.sql
V197__poc_rlock_drop_not_null.sql
V198__billing_cancellation_hash.sql
V199__billing_event_hash.sql
V200__billing_recurrence_hash.sql
V201__domain_hash.sql
V202__delegation_signer_data_hash.sql
V203__domain_history_hash.sql
V204__domain_host_hash.sql
V205__domain_transaction_record_hash.sql
V206__grace_period_hash.sql
V207__grace_period_history_hash.sql
V208__host_hash.sql
V209__poll_message_hash.sql

View File

@@ -0,0 +1,16 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS billingcancellation_billing_cancellation_id_hash ON "BillingCancellation" USING hash (billing_cancellation_id);

View File

@@ -0,0 +1,16 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS billingevent_billing_event_id_hash ON "BillingEvent" USING hash (billing_event_id);

View File

@@ -0,0 +1,16 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS billingrecurrence_billing_recurrence_id_hash ON "BillingRecurrence" USING hash (billing_recurrence_id);

View File

@@ -0,0 +1,17 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS domain_domain_name_hash ON "Domain" USING hash (domain_name);
CREATE INDEX CONCURRENTLY IF NOT EXISTS domain_domain_repo_id_hash ON "Domain" USING hash (repo_id);

View File

@@ -0,0 +1,16 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS delegationsignerdata_domain_repo_id_hash ON "DelegationSignerData" USING hash (domain_repo_id);

View File

@@ -0,0 +1,17 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS domainhistory_domain_repo_id_hash ON "DomainHistory" USING hash (domain_repo_id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS domainhistory_history_revision_id_hash ON "DomainHistory" USING hash (history_revision_id);

View File

@@ -0,0 +1,16 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS domainhost_domain_repo_id_hash ON "DomainHost" USING hash (domain_repo_id);

View File

@@ -0,0 +1,17 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS domaintransactionrecord_id_hash ON "DomainTransactionRecord" USING hash (id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS domaintransactionrecord_domain_history_revision_id_hash ON "DomainTransactionRecord" USING hash (history_revision_id);

View File

@@ -0,0 +1,17 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS graceperiod_grace_period_id_hash ON "GracePeriod" USING hash (grace_period_id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS graceperiod_domain_repo_id_hash ON "GracePeriod" USING hash (domain_repo_id);

View File

@@ -0,0 +1,16 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS graceperiodhistory_grace_period_history_revision_id_hash ON "GracePeriodHistory" USING hash (grace_period_history_revision_id);

View File

@@ -0,0 +1,17 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS host_host_name_hash ON "Host" USING hash (host_name);
CREATE INDEX CONCURRENTLY IF NOT EXISTS host_repo_id_hash ON "Host" USING hash (repo_id);

View File

@@ -0,0 +1,16 @@
-- Copyright 2025 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.
-- Add hash indexes on columns that are commonly queried with a direct equals
CREATE INDEX CONCURRENTLY IF NOT EXISTS pollmessage_poll_message_id_hash ON "PollMessage" USING hash (poll_message_id);

View File

@@ -1924,6 +1924,48 @@ ALTER TABLE ONLY public."User"
CREATE INDEX allocation_token_domain_name_idx ON public."AllocationToken" USING btree (domain_name);
--
-- Name: billingcancellation_billing_cancellation_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX billingcancellation_billing_cancellation_id_hash ON public."BillingCancellation" USING hash (billing_cancellation_id);
--
-- Name: billingevent_billing_event_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX billingevent_billing_event_id_hash ON public."BillingEvent" USING hash (billing_event_id);
--
-- Name: billingrecurrence_billing_recurrence_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX billingrecurrence_billing_recurrence_id_hash ON public."BillingRecurrence" USING hash (billing_recurrence_id);
--
-- Name: delegationsignerdata_domain_repo_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX delegationsignerdata_domain_repo_id_hash ON public."DelegationSignerData" USING hash (domain_repo_id);
--
-- Name: domain_domain_name_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX domain_domain_name_hash ON public."Domain" USING hash (domain_name);
--
-- Name: domain_domain_repo_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX domain_domain_repo_id_hash ON public."Domain" USING hash (repo_id);
--
-- Name: domain_history_to_ds_data_history_idx; Type: INDEX; Schema: public; Owner: -
--
@@ -1938,6 +1980,76 @@ CREATE INDEX domain_history_to_ds_data_history_idx ON public."DomainDsDataHistor
CREATE INDEX domain_history_to_transaction_record_idx ON public."DomainTransactionRecord" USING btree (domain_repo_id, history_revision_id);
--
-- Name: domainhistory_domain_repo_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX domainhistory_domain_repo_id_hash ON public."DomainHistory" USING hash (domain_repo_id);
--
-- Name: domainhistory_history_revision_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX domainhistory_history_revision_id_hash ON public."DomainHistory" USING hash (history_revision_id);
--
-- Name: domainhost_domain_repo_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX domainhost_domain_repo_id_hash ON public."DomainHost" USING hash (domain_repo_id);
--
-- Name: domaintransactionrecord_domain_history_revision_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX domaintransactionrecord_domain_history_revision_id_hash ON public."DomainTransactionRecord" USING hash (history_revision_id);
--
-- Name: domaintransactionrecord_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX domaintransactionrecord_id_hash ON public."DomainTransactionRecord" USING hash (id);
--
-- Name: graceperiod_domain_repo_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX graceperiod_domain_repo_id_hash ON public."GracePeriod" USING hash (domain_repo_id);
--
-- Name: graceperiod_grace_period_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX graceperiod_grace_period_id_hash ON public."GracePeriod" USING hash (grace_period_id);
--
-- Name: graceperiodhistory_grace_period_history_revision_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX graceperiodhistory_grace_period_history_revision_id_hash ON public."GracePeriodHistory" USING hash (grace_period_history_revision_id);
--
-- Name: host_host_name_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX host_host_name_hash ON public."Host" USING hash (host_name);
--
-- Name: host_repo_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX host_repo_id_hash ON public."Host" USING hash (repo_id);
--
-- Name: idx1dyqmqb61xbnj7mt7bk27ds25; Type: INDEX; Schema: public; Owner: -
--
@@ -2617,6 +2729,13 @@ CREATE INDEX idxtmlqd31dpvvd2g1h9i7erw6aj ON public."AllocationToken" USING btre
CREATE INDEX idxy98mebut8ix1v07fjxxdkqcx ON public."Host" USING btree (creation_time);
--
-- Name: pollmessage_poll_message_id_hash; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX pollmessage_poll_message_id_hash ON public."PollMessage" USING hash (poll_message_id);
--
-- Name: premiumlist_name_idx; Type: INDEX; Schema: public; Owner: -
--

View File

@@ -42,6 +42,20 @@ steps:
rm -rf .git && rm -rf nomulus-internal/.git
cp -rf nomulus-internal/* .
rm -rf nomulus-internal
# Remove environment configs from .gitignore
- name: 'gcr.io/cloud-builders/git'
entrypoint: /bin/bash
args:
- -c
- |
set -e
sed -i \
-e '\#core/src/main/java/google/registry/config/files/nomulus-config-alpha.yaml#d' \
-e '\#core/src/main/java/google/registry/config/files/nomulus-config-crash.yaml#d' \
-e '\#core/src/main/java/google/registry/config/files/nomulus-config-production.yaml#d' \
-e '\#core/src/main/java/google/registry/config/files/nomulus-config-qa.yaml#d' \
-e '\#core/src/main/java/google/registry/config/files/nomulus-config-sandbox.yaml#d' \
.gitignore
# Build the builder image and pull the base images, them upload them to GCR.
- name: 'gcr.io/cloud-builders/docker'
entrypoint: /bin/bash