1
0
mirror of https://github.com/google/nomulus synced 2026-05-19 06:11:49 +00:00

Compare commits

...

13 Commits

Author SHA1 Message Date
Pavlo Tkach
35f95bbbe4 Add delete user to the console (#2603)
* Add delete user to the console

* Add delete user to the console

* Add delete user to the console
2024-11-08 18:20:01 +00:00
gbrodman
ae61cd443d Sometimes include deletion times in domain-list exports (#2602)
We only include the deletion time if the domain is in the 5-day
PENDING_DELETE period after the 30 day REDEMPTION period. For all other
domains, we just have an empty string as that field.

This is behind a feature flag so that we can control when it is enabled
2024-11-06 17:59:30 +00:00
gbrodman
cc20f7d76d Add a simple toString for TimedTransitionProperty (#2604)
this means that we can actually see the transitions when running
GetAllocationTokenCommand, for instance
2024-11-05 18:26:36 +00:00
Ben McIlwain
5603b91526 Make nomulus update_recurrence command only fail on pending transfers (#2605)
It was failing when any kind of transfer data was present, even completed
transfer data. Note that completed transfer data persists on a domain
indefinitely until/unless a new transfer is requested.

BUG= http://b/377328244
2024-11-04 21:16:11 +00:00
Pavlo Tkach
332f491ac7 Fix cut off status list on domains page (#2601) 2024-10-28 18:20:04 +00:00
Pavlo Tkach
4bd7c18fe9 Add console settings update progress status (#2596) 2024-10-25 22:23:22 +00:00
Pavlo Tkach
fdb0664841 Add admin.directory.user.security scope (#2597) 2024-10-25 21:24:15 +00:00
Lai Jiang
a9ba770bfa Add canary service to GKE (#2594) 2024-10-22 17:12:00 +00:00
Lai Jiang
4d96e5a6b1 Remove cap on soy (#2592)
We still need to cap the protobuf version that soy depends on, but the
rest of nomulus can use the latest version of protobuf.
2024-10-18 17:21:26 +00:00
Lai Jiang
1171c5cfcb Delete legacy console (#2579) 2024-10-17 20:48:10 +00:00
Pavlo Tkach
91e241374d Add required fields to API users().insert (#2593) 2024-10-17 19:45:12 +00:00
Weimin Yu
634202c0e9 A batch query utility to replace TransactionManager's loadAllOf methods (#2589)
* Replace  with batch query

* Addressing CR
2024-10-14 20:11:29 +00:00
Lai Jiang
020ed33003 Fix releases (#2591)
It seems like `/usr/bin/python` is no longer symlinked to the `python3`
binary in the `gcr.io/cloud-builders/git` image.

I've sent out a separate fix to upstream to change the shebang.

https://gerrit-review.git.corp.google.com/c/gcompute-tools/+/439501

But in the meantime, we need this temporary fix for the release to
build.
2024-10-14 15:42:15 +00:00
221 changed files with 2573 additions and 8834 deletions

View File

@@ -47,20 +47,9 @@ war {
if (project.path == ":services:default") {
war {
from("${coreResourcesDir}/google/registry/ui") {
include "registrar_bin.js"
if (environment != "production") {
include "registrar_bin.js.map"
}
into("assets/js")
}
from("${coreResourcesDir}/google/registry/ui/css") {
include "registrar*"
into("assets/css")
}
from("${coreResourcesDir}/google/registry/ui/assets/images") {
include "**/*"
into("assets/images")
from("${coreResourcesDir}/google/registry/ui/html") {
include "*.html"
into("registrar")
}
}
}
@@ -104,7 +93,6 @@ appengineDeployAll.finalizedBy ':deployCloudSchedulerAndQueue'
rootProject.deploy.dependsOn appengineDeployAll
rootProject.stage.dependsOn appengineStage
tasks['war'].dependsOn ':core:compileProdJS'
tasks['war'].dependsOn ':core:processResources'
tasks['war'].dependsOn ':core:jar'

View File

@@ -95,9 +95,9 @@
<ng-container matColumnDef="statuses">
<mat-header-cell *matHeaderCellDef>Statuses</mat-header-cell>
<mat-cell *matCellDef="let element">{{
element.statuses
}}</mat-cell>
<mat-cell *matCellDef="let element">
<span>{{ element.statuses?.join(", ") }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="registryLock">

View File

@@ -36,6 +36,11 @@
.mat-column-registryLock {
max-width: 150px;
}
.mat-column-statuses span {
padding: 10px 0;
overflow: hidden;
word-break: break-word;
}
}
&__domains-spinner {

View File

@@ -13,7 +13,7 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { switchMap } from 'rxjs';
import { switchMap, timeout } from 'rxjs';
import {
IpAllowListItem,
RegistrarService,
@@ -69,6 +69,7 @@ export class SecurityService {
uiToApiConverter(newSecuritySettings)
)
.pipe(
timeout(2000),
switchMap(() => {
return this.registrarService.loadRegistrars();
})

View File

@@ -10,6 +10,7 @@
<mat-form-field appearance="outline">
<input
matInput
[disabled]="isUpdating"
type="text"
[(ngModel)]="ip.value"
[ngModelOptions]="{ standalone: true }"
@@ -20,12 +21,19 @@
mat-icon-button
aria-label="Remove"
(click)="removeIpEntry(ip)"
[disabled]="isUpdating"
>
<mat-icon>close</mat-icon>
</button>
</div>
}
<button mat-button color="primary" (click)="createIpEntry()" type="button">
<button
mat-button
[disabled]="isUpdating"
color="primary"
(click)="createIpEntry()"
type="button"
>
+ Add IP
</button>
@@ -35,6 +43,7 @@
<textarea
class="console-app__clientCertificateValue"
matInput
[disabled]="isUpdating"
[(ngModel)]="dataSource.clientCertificate"
[ngModelOptions]="{ standalone: true }"
></textarea>
@@ -44,6 +53,7 @@
<mat-form-field appearance="outline">
<textarea
matInput
[disabled]="isUpdating"
[(ngModel)]="dataSource.failoverClientCertificate"
[ngModelOptions]="{ standalone: true }"
></textarea>
@@ -51,6 +61,7 @@
<button
mat-flat-button
color="primary"
[disabled]="isUpdating"
aria-label="Save security settings"
type="submit"
class="settings-security__edit-save"

View File

@@ -29,6 +29,7 @@ import { SecurityService, apiToUiConverter } from './security.service';
})
export default class SecurityEditComponent {
dataSource: SecuritySettings = {};
isUpdating = false;
constructor(
public securityService: SecurityService,
@@ -43,12 +44,15 @@ export default class SecurityEditComponent {
}
save() {
this.isUpdating = true;
this.securityService.saveChanges(this.dataSource).subscribe({
complete: () => {
this.isUpdating = false;
this.goBack();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
this._snackBar.open(err.error || err.message);
this.isUpdating = false;
},
});
}

View File

@@ -172,6 +172,14 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<User>(err)));
}
deleteUser(registrarId: string, emailAddress: string): Observable<any> {
return this.http
.delete<any>(`/console-api/users?registrarId=${registrarId}`, {
body: JSON.stringify({ emailAddress }),
})
.pipe(catchError((err) => this.errorCatcher<any>(err)));
}
getUserData(): Observable<UserData> {
return this.http
.get<UserData>('/console-api/userdata')

View File

@@ -0,0 +1,79 @@
<div class="console-app__user-details">
@if(isNewUser) {
<h1 class="mat-headline-4">
{{ userDetails.emailAddress + " succesfully created" }}
</h1>
} @else {
<h1 class="mat-headline-4">User details</h1>
}
<mat-divider></mat-divider>
<div>
<div class="console-app__user-details-controls">
<button
mat-icon-button
aria-label="Back to users list"
(click)="goBack()"
>
<mat-icon>arrow_back</mat-icon>
</button>
<div class="spacer"></div>
<button
mat-icon-button
aria-label="Delete User"
(click)="deleteUser()"
[disabled]="isLoading"
>
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<p *ngIf="isLoading">
<mat-progress-bar mode="query"></mat-progress-bar>
</p>
<mat-card appearance="outlined">
<mat-card-content>
<mat-list role="list">
<mat-list-item role="listitem">
<h2>User details</h2>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">User email</span>
<span class="console-app__list-value">{{
userDetails.emailAddress
}}</span>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">User role</span>
<span class="console-app__list-value">{{
roleToDescription(userDetails.role)
}}</span>
</mat-list-item>
@if (userDetails.password) {
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">Password</span>
<span
class="console-app__list-value console-app__user-details-password"
>
<input
[type]="isPasswordVisible ? 'text' : 'password'"
[value]="userDetails.password"
disabled
/>
<button
mat-button
aria-label="Show password"
(click)="isPasswordVisible = !isPasswordVisible"
>
{{ isPasswordVisible ? "Hide" : "View" }} password
</button>
</span>
</mat-list-item>
}
</mat-list>
</mat-card-content>
</mat-card>
</div>

View File

@@ -1,4 +1,4 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
// 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.
@@ -12,10 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
/**
* Marker interface for {@link google.registry.request.Action}s that serve GET requests and return
* JSON, rather than HTML.
*/
public interface JsonGetAction extends Runnable {}
.console-app {
&__user-details {
&-controls {
display: flex;
align-items: center;
margin: 20px 0;
}
&-password {
input {
border: none;
background: transparent;
}
}
max-width: 616px;
}
}

View File

@@ -0,0 +1,81 @@
// 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 { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { SelectedRegistrarModule } from '../app.module';
import { MaterialModule } from '../material.module';
import { RegistrarService } from '../registrar/registrar.service';
import { SnackBarModule } from '../snackbar.module';
import { User, UsersService, roleToDescription } from './users.service';
@Component({
selector: 'app-user-edit',
templateUrl: './userEdit.component.html',
styleUrls: ['./userEdit.component.scss'],
standalone: true,
imports: [
MaterialModule,
SnackBarModule,
CommonModule,
SelectedRegistrarModule,
],
providers: [],
})
export class UserEditComponent {
inEdit = false;
isPasswordVisible = false;
isNewUser = false;
isLoading = false;
userDetails: User;
constructor(
protected registrarService: RegistrarService,
protected usersService: UsersService,
private _snackBar: MatSnackBar
) {
this.userDetails = this.usersService
.users()
.filter(
(u) => u.emailAddress === this.usersService.currentlyOpenUserEmail()
)[0];
if (this.usersService.isNewUser) {
this.isNewUser = true;
this.usersService.isNewUser = false;
}
}
roleToDescription(role: string) {
return roleToDescription(role);
}
deleteUser() {
this.isLoading = true;
this.usersService.deleteUser(this.userDetails.emailAddress).subscribe({
error: (err) => {
this._snackBar.open(err.error || err.message);
this.isLoading = false;
},
complete: () => {
this.isLoading = false;
this.goBack();
},
});
}
goBack() {
this.usersService.currentlyOpenUserEmail.set('');
}
}

View File

@@ -1,5 +1,11 @@
<app-selected-registrar-wrapper>
@if (usersService.isLoaded) {
@if(isLoading) {
<div class="console-app__users-spinner">
<mat-spinner />
</div>
} @else if(usersService.currentlyOpenUserEmail()) {
<app-user-edit></app-user-edit>
} @else {
<div class="console-app__users">
<div class="console-app__users-header">
<h1 class="mat-headline-4">Users</h1>
@@ -10,7 +16,7 @@
aria-label="Create new user"
color="primary"
>
Create New User
Create a Viewer User
</button>
</div>
<mat-table
@@ -32,12 +38,11 @@
></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
(click)="openDetails(row.emailAddress)"
></mat-row>
</mat-table>
</div>
} @else {
<div class="console-app__users-spinner">
<mat-spinner />
</div>
}
</app-selected-registrar-wrapper>

View File

@@ -22,7 +22,8 @@ import { SelectedRegistrarModule } from '../app.module';
import { MaterialModule } from '../material.module';
import { RegistrarService } from '../registrar/registrar.service';
import { SnackBarModule } from '../snackbar.module';
import { User, UsersService } from './users.service';
import { UserEditComponent } from './userEdit.component';
import { roleToDescription, User, UsersService } from './users.service';
export const columns = [
{
@@ -33,7 +34,7 @@ export const columns = [
{
columnDef: 'role',
header: 'User role',
cell: (record: User) => `${record.role || ''}`,
cell: (record: User) => `${roleToDescription(record.role)}`,
},
];
@@ -47,6 +48,7 @@ export const columns = [
SnackBarModule,
CommonModule,
SelectedRegistrarModule,
UserEditComponent,
],
providers: [UsersService],
})
@@ -54,6 +56,7 @@ export class UsersComponent {
dataSource: MatTableDataSource<User>;
columns = columns;
displayedColumns = this.columns.map((c) => c.columnDef);
isLoading = false;
@ViewChild(MatSort) sort!: MatSort;
@@ -79,27 +82,32 @@ export class UsersComponent {
}
loadUsers() {
this.isLoading = true;
this.usersService.fetchUsers().subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
this.isLoading = false;
},
complete: () => {
this.isLoading = false;
},
});
}
createNewUser() {
this.isLoading = true;
this.usersService.createNewUser().subscribe({
next: (newUser) => {
this._snackBar.open(
`New user with email ${newUser.emailAddress} has been created.`,
'',
{
duration: 2000,
}
);
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
this.isLoading = false;
},
complete: () => {
this.isLoading = false;
},
});
}
openDetails(emailAddress: string) {
this.usersService.currentlyOpenUserEmail.set(emailAddress);
}
}

View File

@@ -17,20 +17,29 @@ import { tap } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
export const roleToDescription = (role: string) => {
if (!role) return 'N/A';
else if (role.toLowerCase().startsWith('account_manager')) {
return 'Viewer';
}
return 'Editor';
};
export interface CreateAutoTimestamp {
creationTime: string;
}
export interface User {
emailAddress: String;
role: String;
password?: String;
emailAddress: string;
role: string;
password?: string;
}
@Injectable()
export class UsersService {
isLoaded = false;
users = signal<User[]>([]);
currentlyOpenUserEmail = signal<string>('');
isNewUser: boolean = false;
constructor(
private backendService: BackendService,
@@ -42,7 +51,6 @@ export class UsersService {
.getUsers(this.registrarService.registrarId())
.pipe(
tap((users: User[]) => {
this.isLoaded = true;
this.users.set(users);
})
);
@@ -54,7 +62,15 @@ export class UsersService {
.pipe(
tap((newUser: User) => {
this.users.set([...this.users(), newUser]);
this.currentlyOpenUserEmail.set(newUser.emailAddress);
this.isNewUser = true;
})
);
}
deleteUser(emailAddress: string) {
return this.backendService
.deleteUser(this.registrarService.registrarId(), emailAddress)
.pipe(tap((_) => this.fetchUsers()));
}
}

View File

@@ -98,10 +98,8 @@ processTestResources {
}
configurations {
css
jaxb
soy
closureCompiler
devtool
nonprodImplementation.extendsFrom implementation
@@ -120,8 +118,6 @@ configurations {
all {
// servlet-api:3.1 pulled in but not used by soy compiler
exclude group: 'javax.servlet', module: 'javax.servlet-api'
// Jetty's servlet-api:2.5 implementation, pulled in by other Jetty jars
exclude group: 'org.mortbay.jetty', module: 'servlet-api'
}
}
@@ -179,7 +175,6 @@ dependencies {
implementation deps['com.google.oauth-client:google-oauth-client-java6']
implementation deps['com.google.oauth-client:google-oauth-client-jetty']
implementation deps['com.google.oauth-client:google-oauth-client-servlet']
implementation deps['com.google.protobuf:protobuf-java']
implementation deps['com.google.re2j:re2j']
implementation deps['com.google.template:soy']
implementation deps['com.googlecode.json-simple:json-simple']
@@ -297,22 +292,16 @@ dependencies {
jaxb deps['org.glassfish.jaxb:jaxb-xjc']
// Dependency needed for soy to java compilation.
soy deps['com.google.inject:guice']
soy deps['com.google.protobuf:protobuf-java']
// Read dependencies.gradle on why the protobuf version is pinned directly.
soy 'com.google.protobuf:protobuf-java:[3.25.5, 4.0.0)!!'
soy deps['com.google.template:soy']
// Dependencies needed for compiling stylesheets to javascript
css deps['com.google.closure-stylesheets:closure-stylesheets']
css deps['args4j:args4j']
// Tool dependencies. used for doc generation.
implementation files("${System.properties['java.home']}/../lib/tools.jar")
// Flyway classes needed to generate the golden file.
implementation deps['org.flywaydb:flyway-core']
implementation deps['org.flywaydb:flyway-database-postgresql']
closureCompiler deps['com.google.javascript:closure-compiler']
}
task jaxbToJava {
@@ -383,9 +372,8 @@ task soyToJava {
// Relative paths of soy directories.
def spec11SoyDir = "google/registry/reporting/spec11/soy"
def toolsSoyDir = "google/registry/tools/soy"
def registrarSoyDir = "google/registry/ui/soy/registrar"
def soyRelativeDirs = [spec11SoyDir, toolsSoyDir, registrarSoyDir]
def soyRelativeDirs = [spec11SoyDir, toolsSoyDir]
soyRelativeDirs.each {
inputs.dir "${resourcesSourceDir}/${it}"
outputs.dir "${generatedDir}/${it}"
@@ -398,9 +386,7 @@ task soyToJava {
args "--javaPackage", "${javaPackage}",
"--outputDirectory", "${outputDirectory}",
"--javaClassNameSource", "filename",
"--allowExternalCalls", "true",
"--srcs", "${soyFiles.join(',')}",
"--compileTimeGlobalsFile", "${resourcesSourceDir}/google/registry/ui/globals.txt"
"--srcs", "${soyFiles.join(',')}"
}
// Replace the "@link" tags after the "@deprecated" tags in the generated
@@ -427,12 +413,6 @@ task soyToJava {
dir: "${resourcesSourceDir}/${toolsSoyDir}",
include: ['**/*.soy']))
soyToJava('google.registry.ui.soy.registrar',
"${generatedDir}/${registrarSoyDir}",
fileTree(
dir: "${resourcesSourceDir}/${registrarSoyDir}",
include: ['**/*.soy']))
soyToJava('google.registry.reporting.spec11.soy',
"${generatedDir}/${spec11SoyDir}",
fileTree(
@@ -441,134 +421,8 @@ task soyToJava {
}
}
task soyToJS(type: JavaExec) {
def rootSoyDirectory = "${resourcesSourceDir}/google/registry/ui/soy/registrar"
def outputSoyDirectory = "${generatedDir}/google/registry/ui/soy/registrar"
inputs.dir rootSoyDirectory
outputs.dir outputSoyDirectory
def inputSoyFiles = files {
file("${rootSoyDirectory}").listFiles()
}.filter {
it.name.endsWith(".soy")
}
classpath configurations.soy
main = "com.google.template.soy.SoyToJsSrcCompiler"
args "--outputPathFormat", "${outputSoyDirectory}/{INPUT_FILE_NAME}.js",
"--allowExternalCalls", "false",
"--srcs", "${inputSoyFiles.join(',')}",
"--compileTimeGlobalsFile", "${resourcesSourceDir}/google/registry/ui/globals.txt"
}
task stylesheetsToJavascript {
def cssSourceDir = "${jsDir}/google/registry/ui/css"
def outputDir = "${resourcesDir}/google/registry/ui/css"
inputs.dir cssSourceDir
outputs.dir outputDir
ext.cssCompile = { outputName, debug, cssFiles ->
javaexec {
main = "com.google.common.css.compiler.commandline.ClosureCommandLineCompiler"
classpath configurations.css
def argsBuffer = [
"--output-file", "${outputName}.css",
"--output-source-map", "${outputName}.css.map",
"--input-orientation", "LTR",
"--output-orientation", "NOCHANGE",
"--output-renaming-map", "${outputName}.css.js",
"--output-renaming-map-format", "CLOSURE_COMPILED_SPLIT_HYPHENS"
]
if (debug) {
argsBuffer.addAll(["--rename", "DEBUG", "--pretty-print"])
} else {
argsBuffer.addAll(["--rename", "CLOSURE"])
}
argsBuffer.addAll(cssFiles)
args argsBuffer
}
}
doLast {
file("${outputDir}").mkdirs()
def ignoredFiles = ["demo_css.css", "registrar_imports_raw.css"]
def sourceFiles = []
// include all CSS files that we find except for the ones explicitly ignored
fileTree(cssSourceDir).each {
if (it.name.endsWith(".css") && !ignoredFiles.contains(it.name)) {
sourceFiles << (cssSourceDir + "/" + it.name)
}
}
// The css files have to be passed to the compiler in alphabetic order to
// avoid some flaky style issues
sourceFiles.sort()
cssCompile("${outputDir}/registrar_bin", false, sourceFiles)
cssCompile("${outputDir}/registrar_dbg", true, sourceFiles)
}
}
task compileProdJS(type: JavaExec) {
def outputDir = "${resourcesDir}/google/registry/ui"
def nodeModulesDir = "${rootDir}/node_modules"
def cssSourceDir = "${resourcesDir}/google/registry/ui/css"
def jsSourceDir = "${jsDir}/google/registry/ui/js"
def externsDir = "${jsDir}/google/registry/ui/externs"
def soySourceDir = "${generatedDir}/google/registry/ui/soy"
[nodeModulesDir, cssSourceDir, jsSourceDir, externsDir, soySourceDir].each {
inputs.dir "${it}"
}
outputs.dir outputDir
classpath configurations.closureCompiler
main = 'com.google.javascript.jscomp.CommandLineRunner'
def closureArgs = []
closureArgs << "--js_output_file=${outputDir}/registrar_bin.js"
// sourcemap-related configuration
closureArgs << "--create_source_map=${outputDir}/registrar_bin.js.map"
closureArgs << "--source_map_include_content=true"
closureArgs << "--source_map_location_mapping=${rootDir}/|"
closureArgs << "--output_wrapper=\"%output% //# sourceMappingURL=registrar_bin.js.map\""
// compilation options
closureArgs << "--compilation_level=ADVANCED"
closureArgs << "--entry_point=goog:registry.registrar.main"
closureArgs << "--generate_exports"
// manually include all the required js files
closureArgs << "--js=${nodeModulesDir}/google-closure-library/**/*.js"
closureArgs << "--js=${jsDir}/*.js"
closureArgs << "--js=${cssSourceDir}/registrar_bin.css.js"
closureArgs << "--js=${jsSourceDir}/**.js"
closureArgs << "--js=${externsDir}/json.js"
closureArgs << "--js=${soySourceDir}/**.js"
args closureArgs
}
compileProdJS.dependsOn soyToJava
compileJava.dependsOn jaxbToJava
compileJava.dependsOn soyToJava
// The Closure JS compiler does not support Windows. It is fine to disable it if
// all we want to do is to complile the Java code on Windows.
if (!System.properties['os.name'].toLowerCase().contains('windows')) {
compileJava.dependsOn compileProdJS
assemble.dependsOn compileProdJS
}
// stylesheetsToJavascript must happen after processResources, which wipes the
// resources folder before copying data into it.
stylesheetsToJavascript.dependsOn processResources
classes.dependsOn stylesheetsToJavascript
compileProdJS.dependsOn stylesheetsToJavascript
compileProdJS.dependsOn rootProject.npmInstall
compileProdJS.dependsOn processResources
compileProdJS.dependsOn processTestResources
compileProdJS.dependsOn soyToJS
// Make testing artifacts available to be depended up on by other projects.
// TODO: factor out google.registry.testing to be a separate project.
@@ -829,16 +683,7 @@ artifacts {
devtool devtool
}
task copyJsFilesForTestServer(dependsOn: assemble, type: Copy) {
// Unfortunately the test server relies on having some compiled JS/CSS
// in place, so copy it over here
from "${resourcesDir}/google/registry/ui/"
include '**/*.js'
include '**/*.css'
into "${project.projectDir}/src/main/resources/google/registry/ui/"
}
task runTestServer(dependsOn: copyJsFilesForTestServer, type: JavaExec) {
task runTestServer(type: JavaExec) {
main = 'google.registry.server.RegistryTestServerMain'
classpath = sourceSets.test.runtimeClasspath
}

View File

@@ -2,10 +2,7 @@
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
aopalliance:aopalliance:1.0=annotationProcessor,compileClasspath,deploy_jar,errorprone,nonprodAnnotationProcessor,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
args4j:args4j:2.0.23=soy
args4j:args4j:2.0.26=css
args4j:args4j:2.33=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
cglib:cglib-nodep:2.2=css
args4j:args4j:2.33=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
com.charleskorn.kaml:kaml:0.20.0=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.18.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.18.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -38,7 +35,7 @@ com.google.api-client:google-api-client-servlet:2.2.0=testRuntimeClasspath
com.google.api-client:google-api-client-servlet:2.7.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.api-client:google-api-client:2.7.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:gapic-google-cloud-storage-v2:2.32.1-alpha=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.api.grpc:gapic-google-cloud-storage-v2:2.43.1-beta=testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:gapic-google-cloud-storage-v2:2.43.2-beta=testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:grpc-google-cloud-bigquerystorage-v1:3.5.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:grpc-google-cloud-bigquerystorage-v1beta1:0.177.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:grpc-google-cloud-bigquerystorage-v1beta2:0.177.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -49,7 +46,7 @@ com.google.api.grpc:grpc-google-cloud-spanner-admin-database-v1:6.66.0=compileCl
com.google.api.grpc:grpc-google-cloud-spanner-admin-instance-v1:6.66.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:grpc-google-cloud-spanner-v1:6.66.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:grpc-google-cloud-storage-v2:2.32.1-alpha=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.api.grpc:grpc-google-cloud-storage-v2:2.43.1-beta=testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:grpc-google-cloud-storage-v2:2.43.2-beta=testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:grpc-google-common-protos:2.39.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-bigquerystorage-v1:3.5.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-bigquerystorage-v1beta1:0.177.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -62,27 +59,27 @@ com.google.api.grpc:proto-google-cloud-monitoring-v3:3.44.0=compileClasspath,dep
com.google.api.grpc:proto-google-cloud-pubsub-v1:1.111.3=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-pubsublite-v1:1.13.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-secretmanager-v1:2.29.0=testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-secretmanager-v1:2.51.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.api.grpc:proto-google-cloud-secretmanager-v1:2.52.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.api.grpc:proto-google-cloud-secretmanager-v1beta1:2.29.0=testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-secretmanager-v1beta2:2.51.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.api.grpc:proto-google-cloud-secretmanager-v1beta2:2.52.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.api.grpc:proto-google-cloud-spanner-admin-database-v1:6.66.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-spanner-admin-instance-v1:6.66.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-spanner-executor-v1:6.66.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-spanner-v1:6.66.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-storage-v2:2.32.1-alpha=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.api.grpc:proto-google-cloud-storage-v2:2.43.1-beta=testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-storage-v2:2.43.2-beta=testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-tasks-v2:2.29.0=testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-tasks-v2:2.51.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.api.grpc:proto-google-cloud-tasks-v2:2.52.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.api.grpc:proto-google-cloud-tasks-v2beta2:0.119.0=testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-tasks-v2beta2:0.141.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.api.grpc:proto-google-cloud-tasks-v2beta2:0.142.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.api.grpc:proto-google-cloud-tasks-v2beta3:0.119.0=testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-tasks-v2beta3:0.141.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.api.grpc:proto-google-common-protos:2.45.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-iam-v1:1.40.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:api-common:2.37.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:gax-grpc:2.54.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:gax-httpjson:2.54.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:gax:2.54.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-cloud-tasks-v2beta3:0.142.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.api.grpc:proto-google-common-protos:2.46.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:proto-google-iam-v1:1.41.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:api-common:2.38.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:gax-grpc:2.55.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:gax-httpjson:2.55.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:gax:2.55.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-admin-directory:directory_v1-rev20240924-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-bigquery:v2-rev20240919-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-cloudresourcemanager:v1-rev20240310-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -96,13 +93,12 @@ com.google.apis:google-api-services-iam:v2-rev20240530-2.0.0=compileClasspath,de
com.google.apis:google-api-services-iamcredentials:v1-rev20211203-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-monitoring:v3-rev20240929-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-pubsub:v1-rev20220904-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-sheets:v4-rev20240917-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-sheets:v4-rev20241001-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-sqladmin:v1beta4-rev20240925-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-storage:v1-rev20240319-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.apis:google-api-services-storage:v1-rev20240819-2.0.0=testRuntimeClasspath
com.google.apis:google-api-services-storage:v1-rev20240924-2.0.0=testCompileClasspath
com.google.auth:google-auth-library-credentials:1.27.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auth:google-auth-library-oauth2-http:1.27.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-storage:v1-rev20240924-2.0.0=testCompileClasspath,testRuntimeClasspath
com.google.auth:google-auth-library-credentials:1.28.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auth:google-auth-library-oauth2-http:1.28.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auto.service:auto-service-annotations:1.0.1=errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.auto.service:auto-service-annotations:1.1.1=annotationProcessor,compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auto.service:auto-service:1.1.1=annotationProcessor
@@ -111,7 +107,6 @@ com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,errorprone,
com.google.auto.value:auto-value:1.10.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auto.value:auto-value:1.11.0=annotationProcessor,testAnnotationProcessor
com.google.auto:auto-common:1.2.1=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.closure-stylesheets:closure-stylesheets:1.5.0=css
com.google.cloud.bigdataoss:gcsio:2.2.16=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud.bigdataoss:util:2.2.16=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud.bigtable:bigtable-client-core-config:1.28.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -125,62 +120,63 @@ com.google.cloud.sql:postgres-socket-factory:1.21.0=deploy_jar,runtimeClasspath,
com.google.cloud:google-cloud-bigquerystorage:3.5.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-bigtable:2.39.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-core-grpc:2.38.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.cloud:google-cloud-core-grpc:2.44.1=testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-core-grpc:2.45.0=testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-core-http:2.31.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.cloud:google-cloud-core-http:2.44.1=testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-core-http:2.45.0=testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-core:2.38.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.cloud:google-cloud-core:2.44.1=testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-core:2.45.0=testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-firestore:3.21.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-monitoring:3.44.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-nio:0.127.24=testCompileClasspath
com.google.cloud:google-cloud-nio:0.127.25=testCompileClasspath
com.google.cloud:google-cloud-nio:0.127.6=testRuntimeClasspath
com.google.cloud:google-cloud-pubsub:1.129.3=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-pubsublite:1.13.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-secretmanager:2.29.0=testRuntimeClasspath
com.google.cloud:google-cloud-secretmanager:2.51.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.cloud:google-cloud-secretmanager:2.52.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.cloud:google-cloud-spanner:6.66.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-storage:2.32.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.cloud:google-cloud-storage:2.43.1=testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-storage:2.43.2=testCompileClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-tasks:2.29.0=testRuntimeClasspath
com.google.cloud:google-cloud-tasks:2.51.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.cloud:google-cloud-tasks:2.52.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.cloud:grpc-gcp:1.5.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud:libraries-bom:26.26.0=testRuntimeClasspath
com.google.cloud:proto-google-cloud-firestore-bundle-v1:3.21.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.code.findbugs:jsr305:3.0.1=css
com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,checkstyle,compileClasspath,deploy_jar,errorprone,nonprodAnnotationProcessor,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
com.google.code.gson:gson:2.10.1=soy
com.google.code.gson:gson:2.11.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.code.gson:gson:2.7=css,soy
com.google.common.html.types:types:1.0.6=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
com.google.common.html.types:types:1.0.8=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
com.google.dagger:dagger-compiler:2.51.1=annotationProcessor,testAnnotationProcessor
com.google.dagger:dagger-spi:2.51.1=annotationProcessor,testAnnotationProcessor
com.google.dagger:dagger:2.51.1=annotationProcessor,compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
com.google.devtools.ksp:symbol-processing-api:1.9.20-1.0.14=annotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_annotation:2.23.0=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.20.0=soy
com.google.errorprone:error_prone_annotations:2.23.0=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.32.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.errorprone:error_prone_annotations:2.7.1=checkstyle,soy
com.google.errorprone:error_prone_annotations:2.33.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.errorprone:error_prone_annotations:2.7.1=checkstyle
com.google.errorprone:error_prone_check_api:2.23.0=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_core:2.23.0=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_type_annotations:2.23.0=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:javac-shaded:9-dev-r4023-3=annotationProcessor,testAnnotationProcessor
com.google.errorprone:javac:9+181-r4173-1=errorproneJavac
com.google.escapevelocity:escapevelocity:0.9.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
com.google.escapevelocity:escapevelocity:1.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
com.google.flatbuffers:flatbuffers-java:23.5.26=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.flogger:flogger-system-backend:0.7.4=soy
com.google.flogger:flogger-system-backend:0.8=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.flogger:flogger:0.7.4=soy
com.google.flogger:flogger:0.8=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.flogger:google-extensions:0.7.4=soy
com.google.flogger:google-extensions:0.8=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.googlejavaformat:google-java-format:1.5=annotationProcessor,testAnnotationProcessor
com.google.guava:failureaccess:1.0.1=checkstyle,errorprone,nonprodAnnotationProcessor,soy
com.google.guava:failureaccess:1.0.2=annotationProcessor,compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
com.google.guava:guava-parent:32.1.1-jre=errorprone,nonprodAnnotationProcessor
com.google.guava:guava-parent:32.1.1-jre=errorprone,nonprodAnnotationProcessor,soy
com.google.guava:guava-testlib:33.3.1-jre=testCompileClasspath,testRuntimeClasspath
com.google.guava:guava:20.0=css
com.google.guava:guava:31.0.1-jre=checkstyle,soy
com.google.guava:guava:32.1.1-jre=errorprone,nonprodAnnotationProcessor
com.google.guava:guava:31.0.1-jre=checkstyle
com.google.guava:guava:32.1.1-jre=errorprone,nonprodAnnotationProcessor,soy
com.google.guava:guava:33.0.0-jre=annotationProcessor,testAnnotationProcessor
com.google.guava:guava:33.3.1-android=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.guava:guava:33.3.1-jre=testCompileClasspath,testRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
com.google.guava:guava:33.3.1-jre=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
com.google.gwt:gwt-user:2.10.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-apache-v2:1.45.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-appengine:1.43.3=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
@@ -190,15 +186,10 @@ com.google.http-client:google-http-client-jackson2:1.43.3=compileClasspath,deplo
com.google.http-client:google-http-client-jackson2:1.45.0=testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-protobuf:1.44.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client:1.45.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.inject.extensions:guice-multibindings:4.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
com.google.inject:guice:4.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.inject:guice:5.1.0=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.inject:guice:7.0.0=soy
com.google.j2objc:j2objc-annotations:1.3=checkstyle,soy
com.google.inject:guice:7.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
com.google.j2objc:j2objc-annotations:1.3=checkstyle
com.google.j2objc:j2objc-annotations:3.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.javascript:closure-compiler-externs:v20160713=css
com.google.javascript:closure-compiler-unshaded:v20160713=css
com.google.javascript:closure-compiler:v20210505=closureCompiler
com.google.jsinterop:jsinterop-annotations:1.0.1=soy
com.google.jsinterop:jsinterop-annotations:2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.monitoring-client:contrib:1.0.7=testCompileClasspath,testRuntimeClasspath
@@ -211,15 +202,13 @@ com.google.oauth-client:google-oauth-client-servlet:1.34.1=testRuntimeClasspath
com.google.oauth-client:google-oauth-client-servlet:1.36.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.oauth-client:google-oauth-client:1.36.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.protobuf:protobuf-java-util:3.25.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.protobuf:protobuf-java:2.5.0=css
com.google.protobuf:protobuf-java:3.19.6=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.protobuf:protobuf-java:3.25.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
com.google.re2j:re2j:1.7=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.template:soy:2021-02-01=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
com.google.template:soy:2024-02-26=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
com.google.truth:truth:1.4.4=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.googlecode.json-simple:json-simple:1.1.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.ibm.icu:icu4j:57.1=soy
com.ibm.icu:icu4j:73.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.ibm.icu:icu4j:73.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
com.jcraft:jsch:0.1.55=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.lmax:disruptor:3.4.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.puppycrawl.tools:checkstyle:9.3=checkstyle
@@ -270,29 +259,29 @@ io.apicurio:apicurio-registry-protobuf-schema-utilities:3.0.0.M2=compileClasspat
io.github.classgraph:classgraph:4.8.162=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.github.eisop:dataflow-errorprone:3.34.0-eisop1=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,deploy_jar,errorprone,nonprodAnnotationProcessor,nonprodRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-alts:1.68.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-api:1.68.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-auth:1.68.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-alts:1.67.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-api:1.67.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-auth:1.67.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-census:1.62.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-context:1.68.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-core:1.68.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-googleapis:1.68.0=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.grpc:grpc-grpclb:1.68.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-inprocess:1.68.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-netty-shaded:1.68.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-context:1.67.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-core:1.67.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-googleapis:1.67.1=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.grpc:grpc-grpclb:1.67.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-inprocess:1.67.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-netty-shaded:1.67.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-netty:1.62.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-opentelemetry:1.68.0=testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-protobuf-lite:1.68.0=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-protobuf:1.68.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-opentelemetry:1.67.1=testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-protobuf-lite:1.67.1=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-protobuf:1.67.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-rls:1.62.2=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath
io.grpc:grpc-rls:1.68.0=testRuntimeClasspath
io.grpc:grpc-rls:1.67.1=testRuntimeClasspath
io.grpc:grpc-services:1.62.2=compileClasspath,nonprodCompileClasspath,testCompileClasspath
io.grpc:grpc-services:1.68.0=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.grpc:grpc-stub:1.68.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-services:1.67.1=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.grpc:grpc-stub:1.67.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-util:1.62.2=compileClasspath,nonprodCompileClasspath,testCompileClasspath
io.grpc:grpc-util:1.68.0=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.grpc:grpc-util:1.67.1=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.grpc:grpc-xds:1.62.2=compileClasspath,nonprodCompileClasspath,testCompileClasspath
io.grpc:grpc-xds:1.68.0=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.grpc:grpc-xds:1.67.1=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-buffer:4.1.100.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-http2:4.1.100.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-http:4.1.100.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -353,10 +342,9 @@ jakarta.servlet:jakarta.servlet-api:6.0.0=testCompileClasspath,testRuntimeClassp
jakarta.servlet:jakarta.servlet-api:6.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
jakarta.transaction:jakarta.transaction-api:2.0.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=compileClasspath,deploy_jar,jaxb,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
javacc:javacc:4.1=css
javax.annotation:javax.annotation-api:1.3.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
javax.annotation:jsr250-api:1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
javax.inject:javax.inject:1=annotationProcessor,compileClasspath,deploy_jar,errorprone,nonprodAnnotationProcessor,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
javax.inject:javax.inject:1=annotationProcessor,compileClasspath,deploy_jar,errorprone,nonprodAnnotationProcessor,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
javax.jdo:jdo2-api:2.3-20090302111651=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
javax.servlet:servlet-api:2.5=testRuntimeClasspath
javax.validation:validation-api:1.0.0.GA=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -367,7 +355,6 @@ net.bytebuddy:byte-buddy-agent:1.15.3=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.14.12=compileClasspath,nonprodCompileClasspath
net.bytebuddy:byte-buddy:1.14.15=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath
net.bytebuddy:byte-buddy:1.15.3=testCompileClasspath,testRuntimeClasspath
net.java.dev.javacc:javacc:4.1=css
net.java.dev.jna:jna:5.13.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
net.ltgt.gradle.incap:incap:0.2=annotationProcessor,testAnnotationProcessor
net.sf.saxon:Saxon-HE:10.6=checkstyle
@@ -415,22 +402,21 @@ org.apache.sshd:sshd-common:2.14.0=testCompileClasspath,testRuntimeClasspath
org.apache.sshd:sshd-core:2.14.0=testCompileClasspath,testRuntimeClasspath
org.apache.sshd:sshd-scp:2.14.0=testCompileClasspath,testRuntimeClasspath
org.apache.sshd:sshd-sftp:2.14.0=testCompileClasspath,testRuntimeClasspath
org.apache.tomcat:tomcat-annotations-api:11.0.0-M26=testCompileClasspath,testRuntimeClasspath
org.apache.tomcat:tomcat-annotations-api:11.0.0=testCompileClasspath,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
org.bouncycastle:bcpg-jdk18on:1.78.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.bouncycastle:bcpkix-jdk18on:1.78.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.bouncycastle:bcprov-jdk18on:1.78.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.bouncycastle:bcutil-jdk18on:1.78.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.checkerframework:checker-compat-qual:2.5.3=compileClasspath,nonprodCompileClasspath,testCompileClasspath
org.checkerframework:checker-compat-qual:2.5.3=compileClasspath,nonprodCompileClasspath,soy,testCompileClasspath
org.checkerframework:checker-compat-qual:2.5.5=annotationProcessor,testAnnotationProcessor
org.checkerframework:checker-compat-qual:2.5.6=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.checkerframework:checker-qual:3.12.0=checkstyle,soy
org.checkerframework:checker-qual:3.33.0=errorprone,nonprodAnnotationProcessor
org.checkerframework:checker-qual:3.12.0=checkstyle
org.checkerframework:checker-qual:3.33.0=errorprone,nonprodAnnotationProcessor,soy
org.checkerframework:checker-qual:3.41.0=annotationProcessor,testAnnotationProcessor
org.checkerframework:checker-qual:3.47.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.codehaus.mojo:animal-sniffer-annotations:1.24=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.conscrypt:conscrypt-openjdk-uber:2.5.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.easymock:easymock:3.0=css
org.eclipse.angus:angus-activation:2.0.2=deploy_jar,jaxb,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.eclipse.angus:jakarta.mail:2.0.3=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.eclipse.collections:eclipse-collections-api:11.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -457,7 +443,6 @@ org.glassfish.jaxb:txw2:4.0.2=deploy_jar,nonprodRuntimeClasspath,runtimeClasspat
org.glassfish.jaxb:txw2:4.0.5=jaxb
org.glassfish.jaxb:xsom:4.0.5=jaxb
org.gwtproject:gwt-user:2.10.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hamcrest:hamcrest-core:1.1=css
org.hamcrest:hamcrest-core:1.3=nonprodCompileClasspath,nonprodRuntimeClasspath
org.hamcrest:hamcrest-core:3.0=testCompileClasspath,testRuntimeClasspath
org.hamcrest:hamcrest-library:3.0=testCompileClasspath,testRuntimeClasspath
@@ -493,7 +478,7 @@ org.jetbrains:annotations:13.0=annotationProcessor,testAnnotationProcessor
org.jetbrains:annotations:17.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jline:jline:3.27.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.joda:joda-money:2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.json:json:20160212=soy
org.json:json:20230618=soy
org.json:json:20240303=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jsoup:jsoup:1.18.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jspecify:jspecify:0.3.0=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath
@@ -510,25 +495,18 @@ org.junit.platform:junit-platform-runner:1.11.2=testCompileClasspath,testRuntime
org.junit.platform:junit-platform-suite-api:1.11.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-suite-commons:1.11.2=testRuntimeClasspath
org.junit:junit-bom:5.11.2=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-core:1.10.19=css
org.mockito:mockito-core:5.14.1=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.14.1=testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:2.1=css
org.objenesis:objenesis:3.3=testRuntimeClasspath
org.ogce:xpp3:1.1.6=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-analysis:7.0=soy
org.ow2.asm:asm-analysis:9.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:7.0=soy
org.ow2.asm:asm-commons:9.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:9.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:9.7=jacocoAnt
org.ow2.asm:asm-tree:7.0=soy
org.ow2.asm:asm-tree:9.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-tree:9.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-tree:9.7=jacocoAnt
org.ow2.asm:asm-util:7.0=soy
org.ow2.asm:asm-util:9.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm:7.0=soy
org.ow2.asm:asm:9.2=compileClasspath,nonprodCompileClasspath
org.ow2.asm:asm-util:9.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm:9.5=compileClasspath,nonprodCompileClasspath,soy
org.ow2.asm:asm:9.7=deploy_jar,jacocoAnt,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.pcollections:pcollections:3.1.4=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
org.postgresql:postgresql:42.7.3=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath

View File

@@ -46,8 +46,6 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
@@ -138,22 +136,10 @@ public final class RegistryConfig {
return config.gcpProject.locationId;
}
/**
* The filename of the logo to be displayed in the header of the registrar console.
*
* @see google.registry.ui.server.registrar.ConsoleUiAction
*/
@Provides
@Config("logoFilename")
public static String provideLogoFilename(RegistryConfigSettings config) {
return config.registrarConsole.logoFilename;
}
/**
* The product name of this specific registry. Used throughout the registrar console.
*
* @see google.registry.ui.server.registrar.ConsoleUiAction
* @see google.registry.ui.server.console.ConsoleUserDataAction
*/
@Provides
@Config("productName")
@@ -180,23 +166,11 @@ public final class RegistryConfig {
return config.misc.isEmailSendingEnabled;
}
/**
* The e-mail address for questions about integrating with the registry. Used in the
* "contact-us" section of the registrar console.
*
* @see google.registry.ui.server.registrar.ConsoleUiAction
*/
@Provides
@Config("integrationEmail")
public static String provideIntegrationEmail(RegistryConfigSettings config) {
return config.registrarConsole.integrationEmailAddress;
}
/**
* The e-mail address for general support. Used in the "contact-us" section of the registrar
* console.
*
* @see google.registry.ui.server.registrar.ConsoleUiAction
* @see google.registry.ui.server.console.ConsoleUserDataAction
*/
@Provides
@Config("supportEmail")
@@ -204,18 +178,6 @@ public final class RegistryConfig {
return config.registrarConsole.supportEmailAddress;
}
/**
* The "From" e-mail address for announcements. Used in the "contact-us" section of the
* registrar console.
*
* @see google.registry.ui.server.registrar.ConsoleUiAction
*/
@Provides
@Config("announcementsEmail")
public static String provideAnnouncementsEmail(RegistryConfigSettings config) {
return config.registrarConsole.announcementsEmailAddress;
}
/**
* The DUM file name, used as a file name base for DUM csv file
*
@@ -230,7 +192,7 @@ public final class RegistryConfig {
/**
* The contact phone number. Used in the "contact-us" section of the registrar console.
*
* @see google.registry.ui.server.registrar.ConsoleUiAction
* @see google.registry.ui.server.console.ConsoleUserDataAction
*/
@Provides
@Config("supportPhoneNumber")
@@ -242,7 +204,7 @@ public final class RegistryConfig {
* The URL for technical support docs. Used in the "contact-us" section of the registrar
* console.
*
* @see google.registry.ui.server.registrar.ConsoleUiAction
* @see google.registry.ui.server.console.ConsoleUserDataAction
*/
@Provides
@Config("technicalDocsUrl")
@@ -250,22 +212,6 @@ public final class RegistryConfig {
return config.registrarConsole.technicalDocsUrl;
}
/**
* Configuration for analytics services installed in the web console.
*
* @see google.registry.ui.server.registrar.ConsoleUiAction
* @see google.registry.ui.soy.registrar.AnalyticsSoyInfo
*/
@Provides
@Config("analyticsConfig")
public static Map<String, Object> provideAnalyticsConfig(RegistryConfigSettings config) {
// Can't be an ImmutableMap because it may contain null values.
HashMap<String, Object> analyticsConfig = new HashMap<>();
analyticsConfig.put(
"googleAnalyticsId", config.registrarConsole.analyticsConfig.googleAnalyticsId);
return Collections.unmodifiableMap(analyticsConfig);
}
/**
* Returns the Google Cloud Storage bucket for storing zone files.
*
@@ -520,7 +466,7 @@ public final class RegistryConfig {
* Returns the email address(es) that notifications of registrar and/or registrar contact
* updates should be sent to, or the empty list if updates should not be sent.
*
* @see google.registry.ui.server.registrar.RegistrarSettingsAction
* @see google.registry.ui.server.SendEmailUtils
*/
@Provides
@Config("registrarChangesNotificationEmailAddresses")
@@ -914,17 +860,6 @@ public final class RegistryConfig {
return URI.create(config.rde.uploadUrl);
}
/**
* Whether or not the registrar console is enabled.
*
* @see google.registry.ui.server.registrar.ConsoleUiAction
*/
@Provides
@Config("registrarConsoleEnabled")
public static boolean provideRegistrarConsoleEnabled() {
return true;
}
/**
* Maximum amount of time for syncing a spreadsheet, before killing.
*

View File

@@ -189,18 +189,9 @@ public class RegistryConfigSettings {
/** Configuration for the web-based registrar console. */
public static class RegistrarConsole {
public String dumFileName;
public String logoFilename;
public String supportPhoneNumber;
public String supportEmailAddress;
public String announcementsEmailAddress;
public String integrationEmailAddress;
public String technicalDocsUrl;
public AnalyticsConfig analyticsConfig;
}
/** Configuration for analytics services installed in the registrar console */
public static class AnalyticsConfig {
public String googleAnalyticsId;
}
/** Configuration for monitoring. */

View File

@@ -374,6 +374,9 @@ credentialOAuth:
- https://www.googleapis.com/auth/admin.directory.group
# View and manage users in Google Workspace
- https://www.googleapis.com/auth/admin.directory.user
# Security scope which seems to be required to create users via API,
# based on https://github.com/googleapis/google-api-nodejs-client/issues/1884
- https://www.googleapis.com/auth/admin.directory.user.security
# View and manage group settings in Group Settings API.
- https://www.googleapis.com/auth/apps.groups.settings
# Send email through Gmail.
@@ -422,30 +425,15 @@ registrarConsole:
# DUM download file name, excluding the extension
dumFileName: dum_file_name
# Filename of the logo to use in the header of the console. This filename is
# relative to ui/assets/images/
logoFilename: logo.png
# Contact phone number for support with the registry.
supportPhoneNumber: +1 (888) 555 0123
# Contact email address for support with the registry.
supportEmailAddress: support@example.com
# From: email address used to send announcements from the registry.
announcementsEmailAddress: announcements@example.com
# Contact email address for questions about integrating with the registry.
integrationEmailAddress: integration@example.com
# URL linking to directory of technical support docs on the registry.
technicalDocsUrl: http://example.com/your_support_docs/
# Configuration for all analytics services installed in the web console
analyticsConfig:
# Google Analytics account where data on console use is sent, optional
googleAnalyticsId: null
monitoring:
# Max queries per second for the Google Cloud Monitoring V3 (aka Stackdriver)
# API. The limit can be adjusted by contacting Cloud Support.

View File

@@ -24,9 +24,7 @@
<static-files>
<include path="/*.html" expiration="1m"/>
<include path="/assets/js/**" expiration="1m"/>
<include path="/assets/css/**" expiration="1m"/>
<include path="/assets/images/**" expiration="1m"/>
<include path="/assets/sources/**" expiration="1m"/>
<include path="/registrar/*.html" expiration="1m"/>
</static-files>
</appengine-web-app>

View File

@@ -18,58 +18,6 @@
<url-pattern>/_dr/epp</url-pattern>
</servlet-mapping>
<!-- Registrar Console endpoint, which accepts EPP XHRs from GAE GAIA-authenticated sessions. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registrar-xhr</url-pattern>
</servlet-mapping>
<!-- Registrar Console. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registrar</url-pattern>
</servlet-mapping>
<!-- Registrar creation console. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registrar-create</url-pattern>
</servlet-mapping>
<!-- OT&E creation console. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registrar-ote-setup</url-pattern>
</servlet-mapping>
<!-- OT&E status console. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registrar-ote-status</url-pattern>
</servlet-mapping>
<!-- Registrar Self-serve Settings. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registrar-settings</url-pattern>
</servlet-mapping>
<!-- Registry lock get/post/verify. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registry-lock-get</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registry-lock-post</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registry-lock-verify</url-pattern>
</servlet-mapping>
<!-- Registrar console endpoints -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
@@ -84,47 +32,18 @@
Admin-only internal section. Requests for paths covered by the URL patterns below will be
checked for a logged-in user account that's allowed to access the AppEngine admin console
(NOTE: this includes Editor/Viewer permissions in addition to Owner and the new IAM
App Engine Admin role. See https://cloud.google.com/appengine/docs/java/access-control
App Engine Admin role. See https://cloud.google.com/appengine/docs/java/access-control
specifically the "Access handlers that have a login:admin restriction" line.)
TODO(b/28219927): lift some of these restrictions so that we can allow OAuth authentication
for endpoints that need to be accessed by open-source automated processes.
</description>
<!-- Internal AppEngine endpoints. The '_ah' is short for app hosting. -->
<url-pattern>/_ah/*</url-pattern>
<!-- Verbatim JavaScript sources (only visible to admins for debugging). -->
<url-pattern>/assets/sources/*</url-pattern>
<!-- TODO(b/26776367): Move these files to /assets/sources. -->
<url-pattern>/assets/js/registrar_bin.js.map</url-pattern>
<url-pattern>/assets/js/registrar_dbg.js</url-pattern>
<url-pattern>/assets/css/registrar_dbg.css</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
<!-- Repeated here since catch-all rule below is not inherited. -->
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Registrar console</web-resource-name>
<description>
Registrar console requires user login. This is in addition to the
code-level "requireLogin" configuration on individual @Actions.
</description>
<url-pattern>/registrar*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>*</role-name>
</auth-constraint>
<!-- Repeated here since catch-all rule below is not inherited. -->
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>

View File

@@ -24,9 +24,6 @@
<static-files>
<include path="/*.html" expiration="1m"/>
<include path="/assets/js/**" expiration="1m"/>
<include path="/assets/css/**" expiration="1m"/>
<include path="/assets/images/**" expiration="1m"/>
<include path="/assets/sources/**" expiration="1m"/>
<include path="/registrar/*.html" expiration="1m"/>
</static-files>
</appengine-web-app>

View File

@@ -28,16 +28,7 @@
<include path="/*.html">
<http-header name="Cache-Control" value="max-age=0,must-revalidate" />
</include>
<include path="/assets/js/**">
<http-header name="Cache-Control" value="max-age=0,must-revalidate" />
</include>
<include path="/assets/css/**">
<http-header name="Cache-Control" value="max-age=0,must-revalidate" />
</include>
<include path="/assets/images/**">
<http-header name="Cache-Control" value="max-age=0,must-revalidate" />
</include>
<include path="/assets/sources/**">
<include path="/registrar/*.html">
<http-header name="Cache-Control" value="max-age=0,must-revalidate" />
</include>
</static-files>

View File

@@ -23,10 +23,7 @@
<static-files>
<include path="/*.html" expiration="1d"/>
<include path="/assets/js/**" expiration="1d"/>
<include path="/assets/css/**" expiration="1d"/>
<include path="/assets/images/**" expiration="1d"/>
<include path="/assets/sources/**" expiration="1d"/>
<include path="/registrar/*.html" expiration="1d"/>
</static-files>
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->

View File

@@ -27,10 +27,7 @@
<static-files>
<include path="/*.html" expiration="1h"/>
<include path="/assets/js/**" expiration="1h"/>
<include path="/assets/css/**" expiration="1h"/>
<include path="/assets/images/**" expiration="1h"/>
<include path="/assets/sources/**" expiration="1h"/>
<include path="/registrar/*.html" expiration="1h"/>
</static-files>
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->

View File

@@ -23,10 +23,7 @@
<static-files>
<include path="/*.html" expiration="1d"/>
<include path="/assets/js/**" expiration="1d"/>
<include path="/assets/css/**" expiration="1d"/>
<include path="/assets/images/**" expiration="1d"/>
<include path="/assets/sources/**" expiration="1d"/>
<include path="/registrar/*.html" expiration="1d"/>
</static-files>
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->

View File

@@ -17,6 +17,7 @@ package google.registry.export;
import static com.google.common.base.Verify.verifyNotNull;
import static google.registry.model.tld.Tlds.getTldsOfType;
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -28,6 +29,9 @@ import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.gcs.GcsUtils;
import google.registry.model.common.FeatureFlag;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldType;
import google.registry.request.Action;
@@ -38,8 +42,13 @@ import google.registry.util.Clock;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.time.Instant;
import java.util.List;
import javax.inject.Inject;
import org.hibernate.query.NativeQuery;
import org.hibernate.query.TupleTransformer;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
/**
* An action that exports the list of active domains on all real TLDs to Google Drive and GCS.
@@ -55,7 +64,21 @@ import javax.inject.Inject;
public class ExportDomainListsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static final String REGISTERED_DOMAINS_FILENAME = "registered_domains.txt";
private static final String SELECT_DOMAINS_STATEMENT =
"SELECT domainName FROM Domain WHERE tld = :tld AND deletionTime > :now ORDER by domainName";
private static final String SELECT_DOMAINS_AND_DELETION_TIMES_STATEMENT =
"""
SELECT d.domain_name, d.deletion_time, d.statuses, gp.type FROM "Domain" d
LEFT JOIN (SELECT type, domain_repo_id FROM "GracePeriod"
WHERE type = 'REDEMPTION'
AND expiration_time > CAST(:now AS timestamptz)) AS gp
ON d.repo_id = gp.domain_repo_id
WHERE d.tld = :tld
AND d.deletion_time > CAST(:now AS timestamptz)
ORDER BY d.domain_name""";
// This may be a CSV, but it is uses a .txt file extension for back-compatibility
static final String REGISTERED_DOMAINS_FILENAME = "registered_domains.txt";
@Inject Clock clock;
@Inject DriveConnection driveConnection;
@@ -68,47 +91,50 @@ public class ExportDomainListsAction implements Runnable {
public void run() {
ImmutableSet<String> realTlds = getTldsOfType(TldType.REAL);
logger.atInfo().log("Exporting domain lists for TLDs %s.", realTlds);
boolean includeDeletionTimes =
tm().transact(
() ->
FeatureFlag.isActiveNowOrElse(
FeatureFlag.FeatureName.INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS, false));
realTlds.forEach(
tld -> {
List<String> domains =
tm().transact(
List<String> domainsList =
replicaTm()
.transact(
TRANSACTION_REPEATABLE_READ,
() ->
// Note that if we had "creationTime <= :now" in the condition (not
// necessary as there is no pending creation, the order of deletionTime
// and creationTime in the query would have been significant and it
// should come after deletionTime. When Hibernate substitutes "now" it
// will first validate that the **first** field that is to be compared
// with it (deletionTime) is assignable from the substituted Java object
// (click.nowUtc()). Since creationTime is a CreateAutoTimestamp, if it
// comes first, we will need to substitute "now" with
// CreateAutoTimestamp.create(clock.nowUtc()). This might look a bit
// strange as the Java object type is clearly incompatible between the
// two fields deletionTime (DateTime) and creationTime, yet they are
// compared with the same "now". It is actually OK because in the end
// Hibernate converts everything to SQL types (and Java field names to
// SQL column names) to run the query. Both CreateAutoTimestamp and
// DateTime are persisted as timestamp_z in SQL. It is only the
// validation that compares the Java types, and only with the first
// field that compares with the substituted value.
tm().query(
"SELECT domainName FROM Domain "
+ "WHERE tld = :tld "
+ "AND deletionTime > :now "
+ "ORDER by domainName ASC",
String.class)
() -> {
if (includeDeletionTimes) {
// We want to include deletion times, but only for domains in the 5-day
// PENDING_DELETE period after the REDEMPTION grace period. In order to
// accomplish this without loading the entire list of domains, we use a
// native query to join against the GracePeriod table to find
// PENDING_DELETE domains that don't have a REDEMPTION grace period.
return replicaTm()
.getEntityManager()
.createNativeQuery(SELECT_DOMAINS_AND_DELETION_TIMES_STATEMENT)
.unwrap(NativeQuery.class)
.setTupleTransformer(new DomainResultTransformer())
.setParameter("tld", tld)
.setParameter("now", clock.nowUtc())
.getResultList());
String domainsList = Joiner.on("\n").join(domains);
.setParameter("now", replicaTm().getTransactionTime().toString())
.getResultList();
} else {
return replicaTm()
.query(SELECT_DOMAINS_STATEMENT, String.class)
.setParameter("tld", tld)
.setParameter("now", replicaTm().getTransactionTime())
.getResultList();
}
});
logger.atInfo().log(
"Exporting %d domains for TLD %s to GCS and Drive.", domains.size(), tld);
exportToGcs(tld, domainsList, gcsBucket, gcsUtils);
exportToDrive(tld, domainsList, driveConnection);
"Exporting %d domains for TLD %s to GCS and Drive.", domainsList.size(), tld);
String domainsListOutput = Joiner.on('\n').join(domainsList);
exportToGcs(tld, domainsListOutput, gcsBucket, gcsUtils);
exportToDrive(tld, domainsListOutput, driveConnection);
});
}
protected static boolean exportToDrive(
protected static void exportToDrive(
String tldStr, String domains, DriveConnection driveConnection) {
verifyNotNull(driveConnection, "Expecting non-null driveConnection");
try {
@@ -131,12 +157,10 @@ public class ExportDomainListsAction implements Runnable {
} catch (Throwable e) {
logger.atSevere().withCause(e).log(
"Error exporting registered domains for TLD %s to Drive, skipping...", tldStr);
return false;
}
return true;
}
protected static boolean exportToGcs(
protected static void exportToGcs(
String tld, String domains, String gcsBucket, GcsUtils gcsUtils) {
BlobId blobId = BlobId.of(gcsBucket, tld + ".txt");
try (OutputStream gcsOutput = gcsUtils.openOutputStream(blobId);
@@ -145,8 +169,22 @@ public class ExportDomainListsAction implements Runnable {
} catch (Throwable e) {
logger.atSevere().withCause(e).log(
"Error exporting registered domains for TLD %s to GCS, skipping...", tld);
return false;
}
return true;
}
/** Transforms the multiple columns selected from SQL into the output line. */
private static class DomainResultTransformer implements TupleTransformer<String> {
@Override
public String transformTuple(Object[] domainResult, String[] strings) {
String domainName = (String) domainResult[0];
Instant deletionInstant = (Instant) domainResult[1];
DateTime deletionTime = new DateTime(deletionInstant.toEpochMilli(), DateTimeZone.UTC);
String[] domainStatuses = (String[]) domainResult[2];
String gracePeriodType = (String) domainResult[3];
boolean inPendingDelete =
ImmutableSet.copyOf(domainStatuses).contains(StatusValue.PENDING_DELETE.toString())
&& !GracePeriodStatus.REDEMPTION.toString().equals(gracePeriodType);
return String.format("%s,%s", domainName, inPendingDelete ? deletionTime : "");
}
}
}

View File

@@ -66,7 +66,7 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
TEST_FEATURE,
MINIMUM_DATASET_CONTACTS_OPTIONAL,
MINIMUM_DATASET_CONTACTS_PROHIBITED,
NEW_CONSOLE
INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS
}
/** The name of the flag/feature. */
@@ -155,6 +155,15 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
return status.getValueAtTime(time);
}
/** Returns if the flag is active, or the default value if the flag does not exist. */
public static boolean isActiveNowOrElse(FeatureName featureName, boolean defaultValue) {
tm().assertInTransaction();
return CACHE
.get(featureName)
.map(flag -> flag.getStatus(tm().getTransactionTime()).equals(ACTIVE))
.orElse(defaultValue);
}
/** Returns if the FeatureFlag with the given FeatureName is active now. */
public static boolean isActiveNow(FeatureName featureName) {
tm().assertInTransaction();

View File

@@ -185,4 +185,9 @@ public class TimedTransitionProperty<V extends Serializable> implements UnsafeSe
public int hashCode() {
return this.backingMap.hashCode();
}
@Override
public String toString() {
return this.backingMap.toString();
}
}

View File

@@ -40,14 +40,6 @@ import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.SecurityAction;
import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
import google.registry.ui.server.registrar.ConsoleOteSetupAction;
import google.registry.ui.server.registrar.ConsoleRegistrarCreatorAction;
import google.registry.ui.server.registrar.ConsoleUiAction;
import google.registry.ui.server.registrar.OteStatusAction;
import google.registry.ui.server.registrar.RegistrarSettingsAction;
import google.registry.ui.server.registrar.RegistryLockGetAction;
import google.registry.ui.server.registrar.RegistryLockPostAction;
import google.registry.ui.server.registrar.RegistryLockVerifyAction;
/** Dagger component with per-request lifetime for "default" App Engine module. */
@RequestScope
@@ -69,15 +61,10 @@ public interface FrontendRequestComponent {
ConsoleOteAction consoleOteAction();
ConsoleOteSetupAction consoleOteSetupAction();
ConsoleRegistrarCreatorAction consoleRegistrarCreatorAction();
ConsoleRegistryLockAction consoleRegistryLockAction();
ConsoleRegistryLockVerifyAction consoleRegistryLockVerifyAction();
ConsoleUiAction consoleUiAction();
ConsoleUpdateRegistrarAction consoleUpdateRegistrarAction();
ConsoleUserDataAction consoleUserDataAction();
@@ -89,18 +76,13 @@ public interface FrontendRequestComponent {
ContactAction contactAction();
EppTlsAction eppTlsAction();
FlowComponent.Builder flowComponentBuilder();
OteStatusAction oteStatusAction();
RegistrarsAction registrarsAction();
RegistrarSettingsAction registrarSettingsAction();
RegistryLockGetAction registryLockGetAction();
RegistryLockPostAction registryLockPostAction();
RegistryLockVerifyAction registryLockVerifyAction();
SecurityAction securityAction();
WhoisRegistrarFieldsAction whoisRegistrarFieldsAction();
@Subcomponent.Builder

View File

@@ -0,0 +1,154 @@
// 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.
package google.registry.persistence.transaction;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.common.collect.UnmodifiableIterator;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.metamodel.EntityType;
import java.util.Optional;
import java.util.stream.Stream;
/** Helper for querying large data sets in batches. */
public final class BatchedQueries {
private BatchedQueries() {}
private static final int DEFAULT_BATCH_SIZE = 500;
public static <T> Stream<ImmutableList<T>> loadAllOf(Class<T> entityType) {
return loadAllOf(entityType, DEFAULT_BATCH_SIZE);
}
public static <T> Stream<ImmutableList<T>> loadAllOf(Class<T> entityType, int batchSize) {
return loadAllOf(tm(), entityType, batchSize);
}
/**
* Loads all entities of type {@code T} in batches.
*
* <p>This method must not be nested in any transaction; same for the traversal of the returned
* {@link Stream}. Each batch is loaded in a separate transaction at the {@code
* TRANSACTION_REPEATABLE_READ} isolation level, and loads the snapshot of the batch at the
* batch's start time. New insertions or updates since then are not reflected in the result.
*/
public static <T> Stream<ImmutableList<T>> loadAllOf(
JpaTransactionManager jpaTm, Class<T> entityType, int batchSize) {
checkState(!jpaTm.inTransaction(), "loadAllOf cannot be nested in a transaction");
checkArgument(batchSize > 0, "batchSize must be positive");
EntityType<T> jpaEntityType = jpaTm.getMetaModel().entity(entityType);
if (!jpaEntityType.hasSingleIdAttribute()) {
// We should support multi-column primary keys on a case-by-case basis.
throw new UnsupportedOperationException(
"Types with multi-column primary key not supported yet");
}
return Streams.stream(
new BatchedIterator<>(new SingleColIdBatchQuery<>(jpaTm, jpaEntityType), batchSize));
}
public interface BatchQuery<T> {
ImmutableList<T> readBatch(Optional<T> lastRead, int batchSize);
}
private static class SingleColIdBatchQuery<T> implements BatchQuery<T> {
private final JpaTransactionManager jpaTm;
private final Class<T> entityType;
private final String initialJpqlQuery;
private final String subsequentJpqlTemplate;
private SingleColIdBatchQuery(JpaTransactionManager jpaTm, EntityType<T> jpaEntityType) {
checkArgument(
jpaEntityType.hasSingleIdAttribute(),
"%s must have a single ID attribute",
jpaEntityType.getJavaType().getSimpleName());
this.jpaTm = jpaTm;
this.entityType = jpaEntityType.getJavaType();
var idAttr = jpaEntityType.getId(jpaEntityType.getIdType().getJavaType());
this.initialJpqlQuery =
String.format("FROM %s ORDER BY %s", jpaEntityType.getName(), idAttr.getName());
this.subsequentJpqlTemplate =
String.format(
"FROM %1$s WHERE %2$s > :id ORDER BY %2$s",
jpaEntityType.getName(), idAttr.getName());
}
@Override
public ImmutableList<T> readBatch(Optional<T> lastRead, int batchSize) {
checkState(!jpaTm.inTransaction(), "Stream cannot be accessed in a transaction");
return jpaTm.transact(
TRANSACTION_REPEATABLE_READ,
() -> {
var entityManager = jpaTm.getEntityManager();
Optional<Object> lastReadId =
lastRead.map(
entityManager.getEntityManagerFactory().getPersistenceUnitUtil()
::getIdentifier);
TypedQuery<T> query =
lastRead.isEmpty()
? entityManager.createQuery(initialJpqlQuery, entityType)
: entityManager
.createQuery(subsequentJpqlTemplate, entityType)
.setParameter("id", lastReadId.get());
var results = ImmutableList.copyOf(query.setMaxResults(batchSize).getResultList());
results.forEach(entityManager::detach);
return results;
});
}
}
private static class BatchedIterator<T> extends UnmodifiableIterator<ImmutableList<T>> {
private final BatchQuery<T> batchQuery;
private final int batchSize;
private ImmutableList<T> cachedBatch = null;
private BatchedIterator(BatchQuery<T> batchQuery, int batchSize) {
this.batchQuery = batchQuery;
this.batchSize = batchSize;
this.cachedBatch = readNextBatch();
}
@Override
public boolean hasNext() {
return !cachedBatch.isEmpty();
}
@Override
public ImmutableList<T> next() {
var toReturn = cachedBatch;
cachedBatch = cachedBatch.size() < batchSize ? ImmutableList.of() : readNextBatch();
return toReturn;
}
private ImmutableList<T> readNextBatch() {
Optional<T> lastRead =
cachedBatch == null
? Optional.empty()
: Optional.ofNullable(Iterables.getLast(cachedBatch, null));
return batchQuery.readBatch(lastRead, batchSize);
}
}
}

View File

@@ -20,6 +20,7 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.metamodel.Metamodel;
/** Sub-interface of {@link TransactionManager} which defines JPA related methods. */
public interface JpaTransactionManager extends TransactionManager {
@@ -31,6 +32,9 @@ public interface JpaTransactionManager extends TransactionManager {
*/
EntityManager getStandaloneEntityManager();
/** Returns the JPA {@link Metamodel}. */
Metamodel getMetaModel();
/**
* Returns the {@link EntityManager} for the current request.
*

View File

@@ -55,6 +55,7 @@ import jakarta.persistence.TemporalType;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.Metamodel;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
@@ -116,6 +117,11 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return emf.createEntityManager();
}
@Override
public Metamodel getMetaModel() {
return this.emf.getMetamodel();
}
@Override
public EntityManager getEntityManager() {
assertInTransaction();

View File

@@ -33,7 +33,8 @@ public @interface Action {
enum Method {
GET,
HEAD,
POST
POST,
DELETE
}
interface Service {

View File

@@ -21,6 +21,7 @@ import com.beust.jcommander.IStringConverter;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.converters.IParameterSplitter;
import com.google.common.base.Ascii;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
@@ -103,7 +104,10 @@ class CurlCommand implements CommandWithConnection {
throw new IllegalArgumentException("You may not specify a body for a get method.");
}
Service service = useGke ? GkeService.valueOf(serviceName) : GaeService.valueOf(serviceName);
Service service =
useGke
? GkeService.valueOf(Ascii.toUpperCase(serviceName))
: GaeService.valueOf(Ascii.toUpperCase(serviceName));
ServiceConnection connectionToService = connection.withService(service, canary);
String response =

View File

@@ -14,7 +14,6 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Verify.verify;
@@ -35,7 +34,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.CharStreams;
import com.google.common.net.MediaType;
import com.google.re2j.Matcher;
import com.google.re2j.Pattern;
import google.registry.config.RegistryConfig.Config;
import google.registry.request.Action.GaeService;
@@ -60,6 +58,8 @@ public class ServiceConnection {
/** Pattern to heuristically extract title tag contents in HTML responses. */
protected static final Pattern HTML_TITLE_TAG_PATTERN = Pattern.compile("<title>(.*?)</title>");
private static final String CANARY_HEADER = "canary";
private final Service service;
private final boolean useCanary;
private final HttpRequestFactory requestFactory;
@@ -70,9 +70,6 @@ public class ServiceConnection {
}
private ServiceConnection(Service service, HttpRequestFactory requestFactory, boolean useCanary) {
// Currently, only GAE supports connecting to canary.
// TODO (jianglai): decide how to implement canary for GKE.
checkArgument(useCanary == false || service instanceof GaeService, "Canary is only for GAE");
this.service = service;
this.requestFactory = requestFactory;
this.useCanary = useCanary;
@@ -80,15 +77,17 @@ public class ServiceConnection {
/** Returns a copy of this connection that talks to a different service endpoint. */
public ServiceConnection withService(Service service, boolean useCanary) {
Class<? extends Service> oldServiceClazz = this.service.getClass();
Class<? extends Service> newServiceClazz = service.getClass();
if (oldServiceClazz != newServiceClazz) {
throw new IllegalArgumentException(
String.format(
"Cannot switch from %s to %s",
oldServiceClazz.getSimpleName(), newServiceClazz.getSimpleName()));
}
return new ServiceConnection(service, requestFactory, useCanary);
}
/** Returns the contents of the title tag in the given HTML, or null if not found. */
private static String extractHtmlTitle(String html) {
Matcher matcher = HTML_TITLE_TAG_PATTERN.matcher(html);
return (matcher.find() ? matcher.group(1) : null);
}
/** Returns the HTML from the connection error stream, if any, otherwise the empty string. */
private static String getErrorHtmlAsString(HttpResponse response) throws IOException {
return CharStreams.toString(new InputStreamReader(response.getContent(), UTF_8));
@@ -107,19 +106,22 @@ public class ServiceConnection {
HttpHeaders headers = request.getHeaders();
headers.setCacheControl("no-cache");
headers.put(X_REQUESTED_WITH, ImmutableList.of("RegistryTool"));
if (useCanary) {
headers.set(CANARY_HEADER, "true");
}
request.setHeaders(headers);
request.setFollowRedirects(false);
request.setThrowExceptionOnExecuteError(false);
request.setUnsuccessfulResponseHandler(
(request1, response, supportsRetry) -> {
String errorTitle = extractHtmlTitle(getErrorHtmlAsString(response));
String error = getErrorHtmlAsString(response);
throw new IOException(
String.format(
"Error from %s: %d %s%s",
request1.getUrl().toString(),
response.getStatusCode(),
response.getStatusMessage(),
(errorTitle == null ? "" : ": " + errorTitle)));
error));
});
HttpResponse response = null;
try {
@@ -135,8 +137,8 @@ public class ServiceConnection {
@VisibleForTesting
URL getServer() {
URL url = service.getServiceUrl();
if (useCanary) {
verify(!isNullOrEmpty(url.getHost()), "Null host in url");
verify(!isNullOrEmpty(url.getHost()), "Null host in url");
if (useCanary && service instanceof GaeService) {
url =
makeUrl(
String.format(

View File

@@ -30,6 +30,7 @@ import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.model.transfer.TransferStatus;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
@@ -174,7 +175,8 @@ public class UpdateRecurrenceCommand extends ConfirmingCommand {
"Domain %s has already had a deletion time set",
domainName);
checkArgument(
domain.getTransferData().isEmpty(),
domain.getTransferData().isEmpty()
|| domain.getTransferData().getTransferStatus() != TransferStatus.PENDING,
"Domain %s has a pending transfer: %s",
domainName,
domain.getTransferData());

View File

@@ -15,11 +15,12 @@
package google.registry.tools.server;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.BatchedQueries.loadAllOf;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static java.util.Comparator.comparing;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.model.EppResourceUtils;
import google.registry.model.host.Host;
@@ -51,7 +52,8 @@ public final class ListHostsAction extends ListObjectsAction<Host> {
@Override
public ImmutableSet<Host> loadObjects() {
final DateTime now = clock.nowUtc();
return tm().transact(() -> tm().loadAllOf(Host.class)).stream()
return loadAllOf(Host.class)
.flatMap(ImmutableList::stream)
.filter(host -> EppResourceUtils.isActive(host, now))
.collect(toImmutableSortedSet(comparing(Host::getHostName)));
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 B

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
x="0px" y="0px" width="21px" height="21px" viewBox="0 0 21 21" overflow="visible" enable-background="new 0 0 21 21"
xml:space="preserve">
<defs>
</defs>
<path fill="#FFFFFF" d="M17.646,16.501L14,12.855c0.566-0.811,0.903-1.792,0.903-2.855c0-2.762-2.238-5-5-5c-2.761,0-5,2.238-5,5
s2.239,5,5,5c1.064,0,2.045-0.337,2.856-0.903l3.646,3.646c0.343,0.343,0.898,0.344,1.241,0.001
C17.99,17.399,17.99,16.845,17.646,16.501z M9.903,13.2c-1.767,0-3.199-1.433-3.199-3.2s1.433-3.2,3.199-3.2
c1.77,0,3.199,1.433,3.199,3.2S11.673,13.2,9.903,13.2z"/>
<rect opacity="0" fill="#4387FD" width="21" height="21"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
x="0px" y="0px" width="21px" height="21px" viewBox="0 0 21 21" overflow="visible" enable-background="new 0 0 21 21"
xml:space="preserve">
<defs>
</defs>
<path fill="#FFFFFF" d="M17.646,16.501L14,12.855c0.566-0.811,0.903-1.792,0.903-2.855c0-2.762-2.238-5-5-5c-2.761,0-5,2.238-5,5
s2.239,5,5,5c1.064,0,2.045-0.337,2.856-0.903l3.646,3.646c0.343,0.343,0.898,0.344,1.241,0.001
C17.99,17.399,17.99,16.845,17.646,16.501z M9.903,13.2c-1.767,0-3.199-1.433-3.199-3.2s1.433-3.2,3.199-3.2
c1.77,0,3.199,1.433,3.199,3.2S11.673,13.2,9.903,13.2z"/>
<rect opacity="0" fill="#4387FD" width="21" height="21"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
x="0px" y="0px" width="21px" height="21px" viewBox="0 0 21 21" overflow="visible" enable-background="new 0 0 21 21"
xml:space="preserve">
<defs>
</defs>
<path d="M16.895,11.64C16.96,11.269,17,10.89,17,10.5s-0.04-0.77-0.106-1.14l1.535-1.129l0.183-0.683l-1.5-2.598l-0.684-0.184
l-1.738,0.762c-0.581-0.49-1.25-0.875-1.979-1.138L12.5,2.5L12,2H9L8.5,2.499L8.29,4.395C7.562,4.658,6.896,5.043,6.314,5.531
L4.571,4.768L3.889,4.951l-1.5,2.598l0.183,0.684l1.535,1.129C4.04,9.731,4,10.111,4,10.5s0.04,0.769,0.106,1.139l-1.535,1.129
l-0.183,0.684l1.5,2.598l0.682,0.184l1.744-0.764c0.58,0.488,1.248,0.873,1.975,1.137l0.21,1.896L9,19h3l0.5-0.5l0.21-1.892
c0.729-0.263,1.398-0.648,1.979-1.138l1.738,0.762l0.684-0.184l1.499-2.598l-0.181-0.683L16.895,11.64z M14.15,10.5
c0,2.016-1.635,3.65-3.65,3.65c-2.016,0-3.65-1.635-3.65-3.65s1.635-3.65,3.65-3.65C12.515,6.85,14.15,8.484,14.15,10.5z"/>
<rect opacity="0" fill="#4387FD" width="21" height="21"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 B

View File

@@ -1,14 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<info>
<contact:info
xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>sh8013</contact:id>
<contact:authInfo>
<contact:pw>2fooBAR</contact:pw>
</contact:authInfo>
</contact:info>
</info>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -1,14 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<transfer op="request">
<contact:transfer
xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>sh8013</contact:id>
<contact:authInfo>
<contact:pw>2fooBAR</contact:pw>
</contact:authInfo>
</contact:transfer>
</transfer>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -1,18 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<create>
<domain:create
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:period unit="y">2</domain:period>
<domain:registrant>jd1234</domain:registrant>
<domain:contact type="admin">sh8013</domain:contact>
<domain:contact type="tech">sh8013</domain:contact>
<domain:authInfo>
<domain:pw>2fooBAR</domain:pw>
</domain:authInfo>
</domain:create>
</create>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -1,11 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<delete>
<domain:delete
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
</domain:delete>
</delete>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -1,17 +0,0 @@
<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>example.tld</domain:name>
<domain:chg/>
</domain:update>
</update>
<extension>
<rgp:update xmlns:rgp="urn:ietf:params:xml:ns:rgp-1.0">
<rgp:restore op="request"/>
</rgp:update>
</extension>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -1,18 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<login>
<clID>NewRegistrar</clID>
<pw>foo-BAR2</pw>
<options>
<version>1.0</version>
<lang>en</lang>
</options>
<svcs>
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
</svcs>
</login>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -1,6 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<logout/>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -1,6 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<poll op="req"/>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -1,137 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server;
import static com.google.common.base.Suppliers.memoize;
import static com.google.common.io.Resources.asCharSource;
import static com.google.common.io.Resources.getResource;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Resources;
import com.google.re2j.Matcher;
import com.google.re2j.Pattern;
import com.google.template.soy.SoyFileSet;
import com.google.template.soy.SoyUtils;
import com.google.template.soy.parseinfo.SoyFileInfo;
import com.google.template.soy.shared.SoyCssRenamingMap;
import com.google.template.soy.tofu.SoyTofu;
import google.registry.ui.ConsoleDebug;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Helper methods for rendering Soy templates from Java code. */
public final class SoyTemplateUtils {
@VisibleForTesting
public static final Supplier<SoyCssRenamingMap> CSS_RENAMING_MAP_SUPPLIER =
SoyTemplateUtils.createCssRenamingMapSupplier(
Resources.getResource("google/registry/ui/css/registrar_bin.css.js"),
Resources.getResource("google/registry/ui/css/registrar_dbg.css.js"));
/** Returns a memoized supplier containing compiled tofu. */
public static Supplier<SoyTofu> createTofuSupplier(final SoyFileInfo... soyInfos) {
return memoize(
() -> {
ConsoleDebug debugMode = ConsoleDebug.get();
SoyFileSet.Builder builder = SoyFileSet.builder();
for (SoyFileInfo soyInfo : soyInfos) {
builder.add(getResource(soyInfo.getClass(), soyInfo.getFileName()));
}
Map<String, Object> globals;
try {
globals =
new HashMap<>(SoyUtils.parseCompileTimeGlobals(asCharSource(SOY_GLOBALS, UTF_8)));
} catch (IOException e) {
throw new RuntimeException("Failed to load soy globals", e);
}
globals.put("DEBUG", debugMode.ordinal());
builder.setCompileTimeGlobals(globals);
return builder.build().compileToTofu();
});
}
/** Returns a memoized supplier of the thing you pass to {@code setCssRenamingMap()}. */
public static Supplier<SoyCssRenamingMap> createCssRenamingMapSupplier(
final URL cssMap,
final URL cssMapDebug) {
return memoize(
() -> {
final ImmutableMap<String, String> renames = getCssRenames(cssMap, cssMapDebug);
return (cssClassName) -> {
List<String> result = new ArrayList<>();
for (String part : CSS_CLASS_SPLITTER.split(cssClassName)) {
result.add(renames.getOrDefault(part, part));
}
return CSS_CLASS_JOINER.join(result);
};
});
}
private static ImmutableMap<String, String> getCssRenames(URL cssMap, URL cssMapDebug) {
try {
return switch (ConsoleDebug.get()) {
case RAW -> ImmutableMap.of(); // See firstNonNull() above for clarification.
case DEBUG -> extractCssRenames(Resources.toString(cssMapDebug, UTF_8));
default -> extractCssRenames(Resources.toString(cssMap, UTF_8));
};
} catch (IOException e) {
throw new RuntimeException("Failed to load css map", e);
}
}
/**
* Extract class name rewrites from a {@code .css.js} mapping file.
*
* <p>This is the file created when you pass {@code --css_renaming_output_file} to the Closure
* Stylesheets compiler. In order for this to work, {@code --output_renaming_map_format} should
* be {@code CLOSURE_COMPILED} or {@code CLOSURE_UNCOMPILED}.
*
* <p>Here's an example of what the {@code .css.js} file looks like:<pre>
*
* goog.setCssNameMapping({
* "nonLatin": "a",
* "secondary": "b",
* "mobile": "c"
* });</pre>
*
* <p>This is a burden that's only necessary for tofu, since the closure compiler is smart enough
* to substitute CSS class names when soy is compiled to JavaScript.
*/
private static ImmutableMap<String, String> extractCssRenames(String json) {
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
Matcher matcher = KEY_VALUE_PATTERN.matcher(json);
while (matcher.find()) {
builder.put(matcher.group(1), matcher.group(2));
}
return builder.build();
}
private static final URL SOY_GLOBALS = getResource("google/registry/ui/globals.txt");
private static final Splitter CSS_CLASS_SPLITTER = Splitter.on('-');
private static final Joiner CSS_CLASS_JOINER = Joiner.on('-');
private static final Pattern KEY_VALUE_PATTERN =
Pattern.compile("['\"]([^'\"]+)['\"]: ['\"]([^'\"]+)['\"]");
private SoyTemplateUtils() {}
}

View File

@@ -16,10 +16,9 @@ package google.registry.ui.server.console;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.common.FeatureFlag.FeatureName.NEW_CONSOLE;
import static google.registry.model.common.FeatureFlag.isActiveNow;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.DELETE;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.request.Action.Method.POST;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
@@ -36,19 +35,15 @@ import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig;
import google.registry.export.sheet.SyncRegistrarsSheetAction;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.request.HttpException;
import google.registry.security.XsrfTokenManager;
import google.registry.ui.server.registrar.ConsoleUiAction;
import google.registry.util.DiffUtils;
import google.registry.util.RegistryEnvironment;
import jakarta.servlet.http.Cookie;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -82,32 +77,19 @@ public abstract class ConsoleApiAction implements Runnable {
return;
}
User user = consoleApiParams.authResult().user().get();
// This allows us to enable console to a selected cohort of users with release
// We can ignore it in tests
UserRoles userRoles = user.getUserRoles();
boolean hasGlobalOrTestingRole =
!GlobalRole.NONE.equals(userRoles.getGlobalRole())
|| userRoles.hasPermission(
registryAdminClientId, ConsolePermission.VIEW_REGISTRAR_DETAILS);
if (!hasGlobalOrTestingRole
&& RegistryEnvironment.get() != RegistryEnvironment.UNITTEST
&& tm().transact(() -> !isActiveNow(NEW_CONSOLE))) {
try {
consoleApiParams.response().sendRedirect(ConsoleUiAction.PATH);
return;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
String requestMethod = consoleApiParams.request().getMethod();
try {
if (consoleApiParams.request().getMethod().equals(GET.toString())) {
if (requestMethod.equals(GET.toString())) {
getHandler(user);
} else if (requestMethod.equals(HEAD.toString())) {
headHandler(user);
} else {
if (verifyXSRF(user)) {
postHandler(user);
if (requestMethod.equals(DELETE.toString())) {
deleteHandler(user);
} else {
postHandler(user);
}
}
}
} catch (ConsolePermissionForbiddenException e) {
@@ -139,6 +121,14 @@ public abstract class ConsoleApiAction implements Runnable {
throw new UnsupportedOperationException("Console API GET handler not implemented");
}
protected void deleteHandler(User user) {
throw new UnsupportedOperationException("Console API DELETE handler not implemented");
}
protected void headHandler(User user) {
throw new UnsupportedOperationException("Console API HEAD handler not implemented");
}
protected void setFailedResponse(String message, int code) {
consoleApiParams.response().setStatus(code);
consoleApiParams.response().setPayload(message);

View File

@@ -36,6 +36,7 @@ import google.registry.ui.server.SendEmailUtils;
import google.registry.ui.server.console.ConsoleEppPasswordAction.EppPasswordData;
import google.registry.ui.server.console.ConsoleOteAction.OteCreateData;
import google.registry.ui.server.console.ConsoleRegistryLockAction.ConsoleRegistryLockPostInput;
import google.registry.ui.server.console.ConsoleUsersAction.UserDeleteData;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.joda.time.DateTime;
@@ -245,6 +246,13 @@ public final class ConsoleModule {
return payload.map(s -> gson.fromJson(s, EppPasswordData.class));
}
@Provides
@Parameter("userDeleteData")
public static Optional<UserDeleteData> provideUserDeleteData(
Gson gson, @OptionalJsonPayload Optional<JsonElement> payload) {
return payload.map(s -> gson.fromJson(s, UserDeleteData.class));
}
@Provides
@Parameter("oteCreateData")
public static Optional<OteCreateData> provideOteCreateData(

View File

@@ -19,7 +19,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.ui.server.registrar.RegistryLockPostAction.VERIFICATION_EMAIL_TEMPLATE;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
@@ -64,6 +63,12 @@ import org.joda.time.Duration;
public class ConsoleRegistryLockAction extends ConsoleApiAction {
static final String PATH = "/console-api/registry-lock";
static final String VERIFICATION_EMAIL_TEMPLATE =
"""
Please click the link below to perform the lock / unlock action on domain %s. Note: this\
code will expire in one hour.
%s""";
private final DomainLockUtils domainLockUtils;
private final GmailClient gmailClient;

View File

@@ -14,20 +14,26 @@
package google.registry.ui.server.console;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.console.RegistrarRole.ACCOUNT_MANAGER;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.DELETE;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static jakarta.servlet.http.HttpServletResponse.SC_CREATED;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.api.services.directory.Directory;
import com.google.api.services.directory.model.UserName;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.annotations.Expose;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
@@ -37,11 +43,13 @@ import google.registry.request.Action.GkeService;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.tools.IamClient;
import google.registry.util.StringGenerator;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.IntStream;
import javax.inject.Inject;
import javax.inject.Named;
@@ -49,36 +57,42 @@ import javax.inject.Named;
service = Action.GaeService.DEFAULT,
gkeService = GkeService.CONSOLE,
path = ConsoleUsersAction.PATH,
method = {GET, POST},
method = {GET, POST, DELETE},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleUsersAction extends ConsoleApiAction {
static final String PATH = "/console-api/users";
private static final int PASSWORD_LENGTH = 16;
private static final Splitter EMAIL_SPLITTER = Splitter.on('@').trimResults();
private final Gson gson;
private final String registrarId;
private final Directory directory;
private final StringGenerator passwordGenerator;
private final Optional<UserDeleteData> userDeleteData;
private final Optional<String> maybeGroupEmailAddress;
private final IamClient iamClient;
private final String gSuiteDomainName;
@Inject
public ConsoleUsersAction(
ConsoleApiParams consoleApiParams,
Gson gson,
Directory directory,
IamClient iamClient,
@Config("gSuiteDomainName") String gSuiteDomainName,
@Config("gSuiteConsoleUserGroupEmailAddress") Optional<String> maybeGroupEmailAddress,
@Named("base58StringGenerator") StringGenerator passwordGenerator,
@Parameter("userDeleteData") Optional<UserDeleteData> userDeleteData,
@Parameter("registrarId") String registrarId) {
super(consoleApiParams);
this.gson = gson;
this.registrarId = registrarId;
this.directory = directory;
this.passwordGenerator = passwordGenerator;
}
private static String generateNewEmailAddress(User user, String increment) {
List<String> emailParts = EMAIL_SPLITTER.splitToList(user.getEmailAddress());
return String.format("%s-%s@%s", emailParts.get(0), increment, emailParts.get(1));
this.userDeleteData = userDeleteData;
this.maybeGroupEmailAddress = maybeGroupEmailAddress;
this.iamClient = iamClient;
this.gSuiteDomainName = gSuiteDomainName;
}
@Override
@@ -86,7 +100,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
// Temporary flag while testing
if (user.getUserRoles().isAdmin()) {
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
tm().transact(() -> runInTransaction(user));
tm().transact(() -> runCreateInTransaction());
} else {
consoleApiParams.response().setStatus(SC_FORBIDDEN);
}
@@ -96,8 +110,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
protected void getHandler(User user) {
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
List<ImmutableMap> users =
getAllUsers().stream()
.filter(u -> u.getUserRoles().getRegistrarRoles().containsKey(registrarId))
getAllRegistrarUsers(registrarId).stream()
.map(
u ->
ImmutableMap.of(
@@ -111,21 +124,71 @@ public class ConsoleUsersAction extends ConsoleApiAction {
consoleApiParams.response().setStatus(SC_OK);
}
private void runInTransaction(User user) throws IOException {
String nextAvailableIncrement =
Stream.of("1", "2", "3")
.filter(
increment ->
tm().loadByKeyIfPresent(
VKey.create(User.class, generateNewEmailAddress(user, increment)))
.isEmpty())
@Override
protected void deleteHandler(User user) {
// Temporary flag while testing
if (user.getUserRoles().isAdmin()) {
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
tm().transact(() -> runDeleteInTransaction());
} else {
consoleApiParams.response().setStatus(SC_FORBIDDEN);
}
}
private void runDeleteInTransaction() throws IOException {
if (userDeleteData.isEmpty() || isNullOrEmpty(userDeleteData.get().userEmail)) {
throw new BadRequestException("Missing user data param");
}
String email = userDeleteData.get().userEmail;
User userToDelete =
tm().loadByKeyIfPresent(VKey.create(User.class, email))
.orElseThrow(
() -> new BadRequestException(String.format("User %s doesn't exist", email)));
if (!userToDelete.getUserRoles().getRegistrarRoles().containsKey(registrarId)) {
setFailedResponse(
String.format("Can't delete user not associated with registrarId %s", registrarId),
SC_FORBIDDEN);
return;
}
try {
directory.users().delete(email).execute();
} catch (IOException e) {
setFailedResponse("Failed to delete the user workspace account", SC_INTERNAL_SERVER_ERROR);
throw e;
}
VKey<User> key = VKey.create(User.class, email);
tm().delete(key);
User.revokeIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, null, iamClient);
consoleApiParams.response().setStatus(SC_OK);
}
private void runCreateInTransaction() throws IOException {
ImmutableList<User> allRegistrarUsers = getAllRegistrarUsers(registrarId);
if (allRegistrarUsers.size() >= 4)
throw new BadRequestException("Total users amount per registrar is limited to 4");
String nextAvailableEmail =
IntStream.range(1, 5)
.mapToObj(i -> String.format("%s-user%s@%s", registrarId, i, gSuiteDomainName))
.filter(email -> tm().loadByKeyIfPresent(VKey.create(User.class, email)).isEmpty())
.findFirst()
.orElseThrow(() -> new BadRequestException("Extra users amount is limited to 3"));
// Can only happen if registrar cycled through 20 users, which is unlikely
.orElseThrow(
() -> new BadRequestException("Failed to find available increment for new user"));
com.google.api.services.directory.model.User newUser =
new com.google.api.services.directory.model.User();
newUser.setName(
new UserName()
.setFamilyName(registrarId)
.setGivenName(EMAIL_SPLITTER.splitToList(nextAvailableEmail).get(0)));
newUser.setPassword(passwordGenerator.createString(PASSWORD_LENGTH));
newUser.setPrimaryEmail(generateNewEmailAddress(user, nextAvailableIncrement));
newUser.setPrimaryEmail(nextAvailableEmail);
try {
directory.users().insert(newUser).execute();
@@ -142,8 +205,10 @@ public class ConsoleUsersAction extends ConsoleApiAction {
User.Builder builder =
new User.Builder().setUserRoles(userRoles).setEmailAddress(newUser.getPrimaryEmail());
tm().put(builder.build());
User.grantIapPermission(
nextAvailableEmail, maybeGroupEmailAddress, cloudTasksUtils, null, iamClient);
consoleApiParams.response().setStatus(SC_OK);
consoleApiParams.response().setStatus(SC_CREATED);
consoleApiParams
.response()
.setPayload(
@@ -157,11 +222,13 @@ public class ConsoleUsersAction extends ConsoleApiAction {
ACCOUNT_MANAGER)));
}
private ImmutableList<User> getAllUsers() {
private ImmutableList<User> getAllRegistrarUsers(String registrarId) {
return tm().transact(
() ->
tm().loadAllOf(User.class).stream()
.filter(u -> !u.getUserRoles().getRegistrarRoles().isEmpty())
.filter(u -> u.getUserRoles().getRegistrarRoles().containsKey(registrarId))
.collect(toImmutableList()));
}
public record UserDeleteData(@Expose String userEmail) {}
}

View File

@@ -16,19 +16,24 @@ package google.registry.ui.server.console.settings;
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.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.flogger.FluentLogger;
import com.google.gson.Gson;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase.Type;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
@@ -36,11 +41,14 @@ import google.registry.request.Action.GkeService;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.ui.forms.FormException;
import google.registry.ui.server.RegistrarFormFields;
import google.registry.ui.server.console.ConsoleApiAction;
import google.registry.ui.server.console.ConsoleApiParams;
import google.registry.ui.server.registrar.RegistrarSettingsAction;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
@Action(
@@ -97,18 +105,20 @@ public class ContactAction extends ConsoleApiAction {
String.format("Unknown registrar %s", registrarId)));
ImmutableSet<RegistrarPoc> oldContacts = registrar.getContacts();
// TODO: @ptkach - refactor out contacts update functionality after RegistrarSettingsAction is
// deprecated
ImmutableSet<RegistrarPoc> updatedContacts =
RegistrarSettingsAction.readContacts(
registrar,
oldContacts,
Collections.singletonMap(
"contacts",
contacts.get().stream().map(RegistrarPoc::toJsonMap).collect(toImmutableList())));
RegistrarFormFields.getRegistrarContactBuilders(
oldContacts,
Collections.singletonMap(
"contacts",
contacts.get().stream()
.map(RegistrarPoc::toJsonMap)
.collect(toImmutableList())))
.stream()
.map(builder -> builder.setRegistrar(registrar).build())
.collect(toImmutableSet());
try {
RegistrarSettingsAction.checkContactRequirements(oldContacts, updatedContacts);
checkContactRequirements(oldContacts, updatedContacts);
} catch (FormException e) {
logger.atWarning().withCause(e).log(
"Error processing contacts post request for registrar: %s", registrarId);
@@ -127,4 +137,165 @@ public class ContactAction extends ConsoleApiAction {
consoleApiParams.response().setStatus(SC_OK);
}
/**
* Enforces business logic checks on registrar contacts.
*
* @throws FormException if the checks fail.
*/
private static void checkContactRequirements(
ImmutableSet<RegistrarPoc> existingContacts, ImmutableSet<RegistrarPoc> updatedContacts) {
// Check that no two contacts use the same email address.
Set<String> emails = new HashSet<>();
for (RegistrarPoc contact : updatedContacts) {
if (!emails.add(contact.getEmailAddress())) {
throw new ContactRequirementException(
String.format(
"One email address (%s) cannot be used for multiple contacts",
contact.getEmailAddress()));
}
}
// Check that required contacts don't go away, once they are set.
Multimap<Type, RegistrarPoc> oldContactsByType = HashMultimap.create();
for (RegistrarPoc contact : existingContacts) {
for (Type t : contact.getTypes()) {
oldContactsByType.put(t, contact);
}
}
Multimap<Type, RegistrarPoc> newContactsByType = HashMultimap.create();
for (RegistrarPoc contact : updatedContacts) {
for (Type t : contact.getTypes()) {
newContactsByType.put(t, contact);
}
}
for (Type t : difference(oldContactsByType.keySet(), newContactsByType.keySet())) {
if (t.isRequired()) {
throw new ContactRequirementException(t);
}
}
ensurePhoneNumberNotRemovedForContactTypes(oldContactsByType, newContactsByType, Type.TECH);
Optional<RegistrarPoc> domainWhoisAbuseContact =
getDomainWhoisVisibleAbuseContact(updatedContacts);
// If the new set has a domain WHOIS abuse contact, it must have a phone number.
if (domainWhoisAbuseContact.isPresent()
&& domainWhoisAbuseContact.get().getPhoneNumber() == null) {
throw new ContactRequirementException(
"The abuse contact visible in domain WHOIS query must have a phone number");
}
// If there was a domain WHOIS abuse contact in the old set, the new set must have one.
if (getDomainWhoisVisibleAbuseContact(existingContacts).isPresent()
&& domainWhoisAbuseContact.isEmpty()) {
throw new ContactRequirementException(
"An abuse contact visible in domain WHOIS query must be designated");
}
checkContactRegistryLockRequirements(existingContacts, updatedContacts);
}
private static void checkContactRegistryLockRequirements(
ImmutableSet<RegistrarPoc> existingContacts, ImmutableSet<RegistrarPoc> updatedContacts) {
// Any contact(s) with new passwords must be allowed to set them
for (RegistrarPoc updatedContact : updatedContacts) {
if (updatedContact.isRegistryLockAllowed()
|| updatedContact.isAllowedToSetRegistryLockPassword()) {
RegistrarPoc existingContact =
existingContacts.stream()
.filter(
contact -> contact.getEmailAddress().equals(updatedContact.getEmailAddress()))
.findFirst()
.orElseThrow(
() ->
new FormException(
"Cannot set registry lock password directly on new contact"));
// Can't modify registry lock email address
if (!Objects.equals(
updatedContact.getRegistryLockEmailAddress(),
existingContact.getRegistryLockEmailAddress())) {
throw new FormException("Cannot modify registryLockEmailAddress through the UI");
}
if (updatedContact.isRegistryLockAllowed()) {
// the password must have been set before or the user was allowed to set it now
if (!existingContact.isAllowedToSetRegistryLockPassword()
&& !existingContact.isRegistryLockAllowed()) {
throw new FormException("Registrar contact not allowed to set registry lock password");
}
}
if (updatedContact.isAllowedToSetRegistryLockPassword()) {
if (!existingContact.isAllowedToSetRegistryLockPassword()) {
throw new FormException(
"Cannot modify isAllowedToSetRegistryLockPassword through the UI");
}
}
}
}
// Any previously-existing contacts with registry lock enabled cannot be deleted
existingContacts.stream()
.filter(RegistrarPoc::isRegistryLockAllowed)
.forEach(
contact -> {
Optional<RegistrarPoc> updatedContactOptional =
updatedContacts.stream()
.filter(
updatedContact ->
updatedContact.getEmailAddress().equals(contact.getEmailAddress()))
.findFirst();
if (updatedContactOptional.isEmpty()) {
throw new FormException(
String.format(
"Cannot delete the contact %s that has registry lock enabled",
contact.getEmailAddress()));
}
if (!updatedContactOptional.get().isRegistryLockAllowed()) {
throw new FormException(
String.format(
"Cannot remove the ability to use registry lock on the contact %s",
contact.getEmailAddress()));
}
});
}
/**
* Retrieves the registrar contact whose phone number and email address is visible in domain WHOIS
* query as abuse contact (if any).
*
* <p>Frontend processing ensures that only one contact can be set as abuse contact in domain
* WHOIS record.
*
* <p>Therefore, it is possible to return inside the loop once one such contact is found.
*/
private static Optional<RegistrarPoc> getDomainWhoisVisibleAbuseContact(
Set<RegistrarPoc> contacts) {
return contacts.stream().filter(RegistrarPoc::getVisibleInDomainWhoisAsAbuse).findFirst();
}
/**
* Ensure that for each given registrar type, a phone number is present after update, if there was
* one before.
*/
private static void ensurePhoneNumberNotRemovedForContactTypes(
Multimap<Type, RegistrarPoc> oldContactsByType,
Multimap<Type, RegistrarPoc> newContactsByType,
Type... types) {
for (Type type : types) {
if (oldContactsByType.get(type).stream().anyMatch(contact -> contact.getPhoneNumber() != null)
&& newContactsByType.get(type).stream()
.noneMatch(contact -> contact.getPhoneNumber() != null)) {
throw new ContactRequirementException(
String.format(
"Please provide a phone number for at least one %s contact",
type.getDisplayName()));
}
}
}
/** Thrown when a set of contacts doesn't meet certain constraints. */
private static class ContactRequirementException extends FormException {
ContactRequirementException(String msg) {
super(msg);
}
ContactRequirementException(Type type) {
super(String.format("Must have at least one %s contact", type.getDisplayName()));
}
}
}

View File

@@ -1,210 +0,0 @@
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.ui.server.SoyTemplateUtils.CSS_RENAMING_MAP_SUPPLIER;
import static google.registry.util.RegistryEnvironment.PRODUCTION;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import com.google.common.base.Ascii;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.template.soy.tofu.SoyTofu;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.OteAccountBuilder;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Action.Method;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.tools.IamClient;
import google.registry.ui.server.SendEmailUtils;
import google.registry.ui.server.SoyTemplateUtils;
import google.registry.ui.soy.registrar.OteSetupConsoleSoyInfo;
import google.registry.util.RegistryEnvironment;
import google.registry.util.StringGenerator;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
/**
* Action that serves OT&amp;E setup web page.
*
* <p>This Action does 2 things: - for GET, just returns the form that asks for the clientId and
* email. - for POST, receives the clientId and email and generates the OTE entities.
*
* <p>TODO(b/120201577): once we can have 2 different Actions with the same path (different
* Methods), separate this class to 2 Actions.
*/
@Action(
service = GaeService.DEFAULT,
path = ConsoleOteSetupAction.PATH,
method = {Method.POST, Method.GET},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public final class ConsoleOteSetupAction extends HtmlAction {
public static final String PATH = "/registrar-ote-setup";
private static final int PASSWORD_LENGTH = 16;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Supplier<SoyTofu> TOFU_SUPPLIER =
SoyTemplateUtils.createTofuSupplier(
google.registry.ui.soy.registrar.AnalyticsSoyInfo.getInstance(),
google.registry.ui.soy.registrar.ConsoleSoyInfo.getInstance(),
google.registry.ui.soy.registrar.ConsoleUtilsSoyInfo.getInstance(),
google.registry.ui.soy.registrar.FormsSoyInfo.getInstance(),
google.registry.ui.soy.registrar.OteSetupConsoleSoyInfo.getInstance());
@Inject AuthenticatedRegistrarAccessor registrarAccessor;
@Inject SendEmailUtils sendEmailUtils;
@Inject
@Named("base58StringGenerator")
StringGenerator passwordGenerator;
@Inject
@Parameter("consoleClientId")
Optional<String> clientId;
@Inject
@Parameter("email")
Optional<String> email;
@Inject
@Parameter("password")
Optional<String> optionalPassword;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject IamClient iamClient;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@Inject
ConsoleOteSetupAction() {}
@Override
public void runAfterLogin(Map<String, Object> data) {
checkState(
!RegistryEnvironment.get().equals(PRODUCTION), "Can't create OT&E in prod");
if (!registrarAccessor.isAdmin()) {
response.setStatus(SC_FORBIDDEN);
response.setPayload(
TOFU_SUPPLIER
.get()
.newRenderer(OteSetupConsoleSoyInfo.WHOAREYOU)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(data)
.render());
return;
}
switch (method) {
case POST -> runPost(data);
case GET -> runGet(data);
default ->
throw new BadRequestException(
String.format("Action cannot be called with method %s", method));
}
}
@Override
public String getPath() {
return PATH;
}
private void runPost(Map<String, Object> data) {
try {
checkState(clientId.isPresent() && email.isPresent(), "Must supply clientId and email");
data.put("baseClientId", clientId.get());
data.put("contactEmail", email.get());
String password = optionalPassword.orElse(passwordGenerator.createString(PASSWORD_LENGTH));
OteAccountBuilder oteAccountBuilder =
OteAccountBuilder.forRegistrarId(clientId.get())
.addUser(email.get())
.setPassword(password);
ImmutableMap<String, String> clientIdToTld = oteAccountBuilder.buildAndPersist();
oteAccountBuilder.grantIapPermission(maybeGroupEmailAddress, cloudTasksUtils, iamClient);
sendExternalUpdates(clientIdToTld);
data.put("clientIdToTld", clientIdToTld);
data.put("password", password);
response.setPayload(
TOFU_SUPPLIER
.get()
.newRenderer(OteSetupConsoleSoyInfo.RESULT_SUCCESS)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(data)
.render());
} catch (Throwable e) {
logger.atWarning().withCause(e).log(
"Failed to setup OT&E. clientId: %s, email: %s", clientId.get(), email.get());
data.put("errorMessage", e.getMessage());
response.setPayload(
TOFU_SUPPLIER
.get()
.newRenderer(OteSetupConsoleSoyInfo.FORM_PAGE)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(data)
.render());
}
}
private void runGet(Map<String, Object> data) {
// set the values to pre-fill, if given
data.put("baseClientId", clientId.orElse(null));
data.put("contactEmail", email.orElse(null));
response.setPayload(
TOFU_SUPPLIER
.get()
.newRenderer(OteSetupConsoleSoyInfo.FORM_PAGE)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(data)
.render());
}
private void sendExternalUpdates(ImmutableMap<String, String> clientIdToTld) {
if (!sendEmailUtils.hasRecipients()) {
return;
}
String environment = Ascii.toLowerCase(String.valueOf(RegistryEnvironment.get()));
StringBuilder builder = new StringBuilder();
builder.append(
String.format(
"The following entities were created in %s by %s:\n",
environment, registrarAccessor.userIdForLogging()));
clientIdToTld.forEach(
(clientId, tld) ->
builder.append(
String.format(" Registrar %s with access to TLD %s\n", clientId, tld)));
builder.append(String.format("Gave user %s web access to these Registrars\n", email.get()));
sendEmailUtils.sendEmail(
String.format("OT&E for registrar %s created in %s", clientId.get(), environment),
builder.toString());
}
}

View File

@@ -1,331 +0,0 @@
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.ui.server.SoyTemplateUtils.CSS_RENAMING_MAP_SUPPLIER;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import com.google.common.base.Ascii;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.template.soy.tofu.SoyTofu;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Action.Method;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.tools.IamClient;
import google.registry.ui.server.SendEmailUtils;
import google.registry.ui.server.SoyTemplateUtils;
import google.registry.ui.soy.registrar.AnalyticsSoyInfo;
import google.registry.ui.soy.registrar.ConsoleSoyInfo;
import google.registry.ui.soy.registrar.ConsoleUtilsSoyInfo;
import google.registry.ui.soy.registrar.FormsSoyInfo;
import google.registry.ui.soy.registrar.RegistrarCreateConsoleSoyInfo;
import google.registry.util.RegistryEnvironment;
import google.registry.util.StringGenerator;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;
import org.joda.money.CurrencyUnit;
/**
* Action that serves Registrar creation page.
*
* <p>This Action does 2 things: - for GET, just returns the form that asks for the required
* information. - for POST, receives the information and creates the Registrar.
*/
@Action(
service = GaeService.DEFAULT,
path = ConsoleRegistrarCreatorAction.PATH,
method = {Method.POST, Method.GET},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public final class ConsoleRegistrarCreatorAction extends HtmlAction {
private static final int PASSWORD_LENGTH = 16;
private static final int PASSCODE_LENGTH = 5;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static final String PATH = "/registrar-create";
private static final Supplier<SoyTofu> TOFU_SUPPLIER =
SoyTemplateUtils.createTofuSupplier(
AnalyticsSoyInfo.getInstance(),
ConsoleSoyInfo.getInstance(),
ConsoleUtilsSoyInfo.getInstance(),
FormsSoyInfo.getInstance(),
RegistrarCreateConsoleSoyInfo.getInstance());
@Inject AuthenticatedRegistrarAccessor registrarAccessor;
@Inject SendEmailUtils sendEmailUtils;
@Inject @Named("base58StringGenerator") StringGenerator passwordGenerator;
@Inject @Named("digitOnlyStringGenerator") StringGenerator passcodeGenerator;
@Inject
@Parameter("consoleClientId")
Optional<String> clientId;
@Inject
@Parameter("consoleName")
Optional<String> name;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject IamClient iamClient;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@Inject @Parameter("billingAccount") Optional<String> billingAccount;
@Inject @Parameter("ianaId") Optional<Integer> ianaId;
@Inject @Parameter("referralEmail") Optional<String> referralEmail;
@Inject @Parameter("driveId") Optional<String> driveId;
@Inject @Parameter("consoleUserEmail") Optional<String> consoleUserEmail;
// Address fields, some of which are required and others are optional.
@Inject @Parameter("street1") Optional<String> street1;
@Inject @Parameter("street2") Optional<String> optionalStreet2;
@Inject @Parameter("street3") Optional<String> optionalStreet3;
@Inject @Parameter("city") Optional<String> city;
@Inject @Parameter("state") Optional<String> optionalState;
@Inject @Parameter("zip") Optional<String> optionalZip;
@Inject @Parameter("countryCode") Optional<String> countryCode;
@Inject @Parameter("password") Optional<String> optionalPassword;
@Inject @Parameter("passcode") Optional<String> optionalPasscode;
@Inject ConsoleRegistrarCreatorAction() {}
@Override
public void runAfterLogin(Map<String, Object> data) {
if (!registrarAccessor.isAdmin()) {
response.setStatus(SC_FORBIDDEN);
response.setPayload(
TOFU_SUPPLIER
.get()
.newRenderer(RegistrarCreateConsoleSoyInfo.WHOAREYOU)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(data)
.render());
return;
}
switch (method) {
case POST -> runPost(data);
case GET -> runGet(data);
default ->
throw new BadRequestException(
String.format("Action cannot be called with method %s", method));
}
}
@Override
public String getPath() {
return PATH;
}
private static void checkPresent(Optional<?> value, String name) {
checkState(value.isPresent(), "Missing value for %s", name);
}
private static final Splitter LINE_SPLITTER =
Splitter.onPattern("\r?\n").trimResults().omitEmptyStrings();
private static final Splitter ENTRY_SPLITTER =
Splitter.on('=').trimResults().omitEmptyStrings().limit(2);
private static ImmutableMap<CurrencyUnit, String> parseBillingAccount(String billingAccount) {
try {
return LINE_SPLITTER.splitToList(billingAccount).stream()
.map(ENTRY_SPLITTER::splitToList)
.peek(
list ->
checkState(
list.size() == 2,
"Can't parse line %s. The format should be [currency]=[account ID]",
list))
.collect(
toImmutableMap(
list -> CurrencyUnit.of(Ascii.toUpperCase(list.getFirst())),
list -> list.get(1)));
} catch (Throwable e) {
throw new RuntimeException("Error parsing billing accounts - " + e.getMessage(), e);
}
}
private void runPost(Map<String, Object> data) {
try {
checkPresent(clientId, "clientId");
checkPresent(name, "name");
checkPresent(billingAccount, "billingAccount");
checkPresent(ianaId, "ianaId");
checkPresent(referralEmail, "referralEmail");
checkPresent(driveId, "driveId");
checkPresent(consoleUserEmail, "consoleUserEmail");
checkPresent(street1, "street");
checkPresent(city, "city");
checkPresent(countryCode, "countryCode");
data.put("clientId", clientId.get());
data.put("name", name.get());
data.put("ianaId", ianaId.get());
data.put("referralEmail", referralEmail.get());
data.put("billingAccount", billingAccount.get());
data.put("driveId", driveId.get());
data.put("consoleUserEmail", consoleUserEmail.get());
data.put("street1", street1.get());
optionalStreet2.ifPresent(street2 -> data.put("street2", street2));
optionalStreet3.ifPresent(street3 -> data.put("street3", street3));
data.put("city", city.get());
optionalState.ifPresent(state -> data.put("state", state));
optionalZip.ifPresent(zip -> data.put("zip", zip));
data.put("countryCode", countryCode.get());
String password = optionalPassword.orElse(passwordGenerator.createString(PASSWORD_LENGTH));
String phonePasscode =
optionalPasscode.orElse(passcodeGenerator.createString(PASSCODE_LENGTH));
Registrar registrar =
new Registrar.Builder()
.setRegistrarId(clientId.get())
.setRegistrarName(name.get())
.setBillingAccountMap(parseBillingAccount(billingAccount.get()))
.setIanaIdentifier(Long.valueOf(ianaId.get()))
.setIcannReferralEmail(referralEmail.get())
.setEmailAddress(referralEmail.get())
.setDriveFolderId(driveId.get())
.setType(Registrar.Type.REAL)
.setPassword(password)
.setPhonePasscode(phonePasscode)
.setState(State.PENDING)
.setLocalizedAddress(
new RegistrarAddress.Builder()
.setStreet(
Stream.of(street1, optionalStreet2, optionalStreet3)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toImmutableList()))
.setCity(city.get())
.setState(optionalState.orElse(null))
.setCountryCode(countryCode.get())
.setZip(optionalZip.orElse(null))
.build())
.build();
User user =
new User.Builder()
.setEmailAddress(consoleUserEmail.get())
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of(
registrar.getRegistrarId(), RegistrarRole.ACCOUNT_MANAGER))
.build())
.build();
tm().transact(
() -> {
checkState(
Registrar.loadByRegistrarId(registrar.getRegistrarId()).isEmpty(),
"Registrar with client ID %s already exists",
registrar.getRegistrarId());
tm().put(registrar);
tm().put(user);
});
User.grantIapPermission(
user.getEmailAddress(), maybeGroupEmailAddress, cloudTasksUtils, null, iamClient);
data.put("password", password);
data.put("passcode", phonePasscode);
sendExternalUpdates();
response.setPayload(
TOFU_SUPPLIER
.get()
.newRenderer(RegistrarCreateConsoleSoyInfo.RESULT_SUCCESS)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(data)
.render());
} catch (Throwable e) {
logger.atWarning().withCause(e).log(
"Failed to create registrar. clientId: %s, data: %s", clientId.get(), data);
data.put("errorMessage", e.getMessage());
response.setPayload(
TOFU_SUPPLIER
.get()
.newRenderer(RegistrarCreateConsoleSoyInfo.FORM_PAGE)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(data)
.render());
}
}
private void runGet(Map<String, Object> data) {
// set the values to pre-fill, if given
data.put("clientId", clientId.orElse(null));
data.put("name", name.orElse(null));
data.put("ianaId", ianaId.orElse(null));
data.put("referralEmail", referralEmail.orElse(null));
data.put("driveId", driveId.orElse(null));
data.put("consoleUserEmail", consoleUserEmail.orElse(null));
response.setPayload(
TOFU_SUPPLIER
.get()
.newRenderer(RegistrarCreateConsoleSoyInfo.FORM_PAGE)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(data)
.render());
}
private static String toEmailLine(Optional<?> value, String name) {
return String.format(" %s: %s\n", name, value.orElse(null));
}
private void sendExternalUpdates() {
if (!sendEmailUtils.hasRecipients()) {
return;
}
String environment = Ascii.toLowerCase(String.valueOf(RegistryEnvironment.get()));
String body =
String.format(
"The following registrar was created in %s by %s:\n",
environment, registrarAccessor.userIdForLogging())
+ toEmailLine(clientId, "clientId")
+ toEmailLine(name, "name")
+ toEmailLine(billingAccount, "billingAccount")
+ toEmailLine(ianaId, "ianaId")
+ toEmailLine(referralEmail, "referralEmail")
+ toEmailLine(driveId, "driveId")
+ String.format("Gave user %s web access to the registrar\n", consoleUserEmail.get());
sendEmailUtils.sendEmail(
String.format("Registrar %s created in %s", clientId.get(), environment), body);
}
}

View File

@@ -1,198 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.ADMIN;
import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.OWNER;
import static google.registry.ui.server.SoyTemplateUtils.CSS_RENAMING_MAP_SUPPLIER;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.template.soy.data.SoyMapData;
import com.google.template.soy.tofu.SoyTofu;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.common.FeatureFlag;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role;
import google.registry.ui.server.SoyTemplateUtils;
import google.registry.ui.soy.registrar.ConsoleSoyInfo;
import google.registry.util.RegistryEnvironment;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
/** Action that serves Registrar Console single HTML page (SPA). */
@Action(
service = GaeService.DEFAULT,
path = ConsoleUiAction.PATH,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public final class ConsoleUiAction extends HtmlAction {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static final String PATH = "/registrar";
private static final Supplier<SoyTofu> TOFU_SUPPLIER =
SoyTemplateUtils.createTofuSupplier(
google.registry.ui.soy.registrar.AnalyticsSoyInfo.getInstance(),
google.registry.ui.soy.registrar.ConsoleSoyInfo.getInstance(),
google.registry.ui.soy.registrar.ConsoleUtilsSoyInfo.getInstance());
@Inject RegistrarConsoleMetrics registrarConsoleMetrics;
@Inject AuthenticatedRegistrarAccessor registrarAccessor;
@Inject
@Config("integrationEmail")
String integrationEmail;
@Inject
@Config("supportEmail")
String supportEmail;
@Inject
@Config("announcementsEmail")
String announcementsEmail;
@Inject
@Config("supportPhoneNumber")
String supportPhoneNumber;
@Inject
@Config("technicalDocsUrl")
String technicalDocsUrl;
@Inject
@Config("registrarConsoleEnabled")
boolean enabled;
@Inject
@Parameter("consoleClientId")
Optional<String> paramClientId;
@Inject
ConsoleUiAction() {}
@Override
public void runAfterLogin(Map<String, Object> data) {
// This console is deprecated.
// Unless an explict "noredirect" URL parameter is included, it will redirect to the new
// console.
if (isNullOrEmpty(req.getParameter("noredirect"))) {
try {
response.sendRedirect("/console");
return;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
SoyMapData soyMapData = new SoyMapData();
data.forEach((key, value) -> soyMapData.put(key, value));
soyMapData.put("integrationEmail", integrationEmail);
soyMapData.put("supportEmail", supportEmail);
soyMapData.put("announcementsEmail", announcementsEmail);
soyMapData.put("supportPhoneNumber", supportPhoneNumber);
soyMapData.put("technicalDocsUrl", technicalDocsUrl);
if (!enabled) {
response.setStatus(SC_SERVICE_UNAVAILABLE);
response.setPayload(
TOFU_SUPPLIER
.get()
.newRenderer(ConsoleSoyInfo.DISABLED)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(soyMapData)
.render());
return;
}
ImmutableSetMultimap<String, Role> roleMap = registrarAccessor.getAllRegistrarIdsWithRoles();
soyMapData.put("allClientIds", roleMap.keySet());
soyMapData.put("environment", RegistryEnvironment.get().toString());
boolean newConsole =
tm().transact(
() ->
FeatureFlag.getUncached(FeatureFlag.FeatureName.NEW_CONSOLE)
.map(
flag ->
flag.getStatus(tm().getTransactionTime())
.equals(FeatureFlag.FeatureStatus.ACTIVE))
.orElse(false));
soyMapData.put("includeDeprecationWarning", newConsole);
// We set the initial value to the value that will show if guessClientId throws.
String clientId = "<null>";
try {
clientId = paramClientId.orElse(registrarAccessor.guessRegistrarId());
soyMapData.put("clientId", clientId);
soyMapData.put("isOwner", roleMap.containsEntry(clientId, OWNER));
soyMapData.put("isAdmin", roleMap.containsEntry(clientId, ADMIN));
// We want to load the registrar even if we won't use it later (even if we remove the
// requireFeeExtension) - to make sure the user indeed has access to the guessed registrar.
//
// Note that not doing so (and just passing the "clientId" as given) isn't a security issue
// since we double-check the access to the registrar on any read / update request. We have to
// - since the access might get revoked between the initial page load and the request! (also
// because the requests come from the browser, and can easily be faked)
registrarAccessor.getRegistrar(clientId);
} catch (RegistrarAccessDeniedException e) {
logger.atWarning().withCause(e).log(
"User %s doesn't have access to registrar console.", authResult.userIdForLogging());
response.setStatus(SC_FORBIDDEN);
response.setPayload(
TOFU_SUPPLIER
.get()
.newRenderer(ConsoleSoyInfo.WHOAREYOU)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(soyMapData)
.render());
registrarConsoleMetrics.registerConsoleRequest(
clientId, paramClientId.isPresent(), roleMap.get(clientId), "FORBIDDEN");
return;
} catch (Exception e) {
registrarConsoleMetrics.registerConsoleRequest(
clientId, paramClientId.isPresent(), roleMap.get(clientId), "UNEXPECTED ERROR");
throw e;
}
String payload =
TOFU_SUPPLIER
.get()
.newRenderer(ConsoleSoyInfo.MAIN)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(soyMapData)
.render();
response.setPayload(payload);
registrarConsoleMetrics.registerConsoleRequest(
clientId, paramClientId.isPresent(), roleMap.get(clientId), "SUCCESS");
}
@Override
public String getPath() {
return PATH;
}
}

View File

@@ -1,92 +0,0 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static com.google.common.net.HttpHeaders.X_FRAME_OPTIONS;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.request.Action;
import google.registry.request.RequestMethod;
import google.registry.request.Response;
import google.registry.request.auth.AuthResult;
import google.registry.security.XsrfTokenManager;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
/**
* Handles some of the nitty-gritty of responding to requests that should return HTML, including
* login, redirects, analytics, and some headers.
*
* <p>If the user is not logged in, this will redirect to the login URL.
*/
public abstract class HtmlAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject HttpServletRequest req;
@Inject Response response;
@Inject XsrfTokenManager xsrfTokenManager;
@Inject AuthResult authResult;
@Inject @RequestMethod Action.Method method;
@Inject
@Config("logoFilename")
String logoFilename;
@Inject
@Config("productName")
String productName;
@Inject
@Config("analyticsConfig")
Map<String, Object> analyticsConfig;
@Override
public void run() {
response.setHeader(X_FRAME_OPTIONS, "SAMEORIGIN"); // Disallow iframing.
response.setHeader("X-Ui-Compatible", "IE=edge"); // Ask IE not to be silly.
if (authResult.user().isEmpty()) {
response.setStatus(SC_UNAUTHORIZED);
return;
}
response.setContentType(MediaType.HTML_UTF_8);
User user = authResult.user().get();
// Using HashMap to allow null values
HashMap<String, Object> data = new HashMap<>();
data.put("logoFilename", logoFilename);
data.put("productName", productName);
data.put("username", user.getEmailAddress());
data.put("logoutUrl", "/registrar?gcp-iap-mode=CLEAR_LOGIN_COOKIE");
data.put("analyticsConfig", analyticsConfig);
data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmailAddress()));
logger.atInfo().log(
"User %s is accessing %s with method %s.",
authResult.userIdForLogging(), getClass().getName(), method);
runAfterLogin(data);
}
public abstract void runAfterLogin(Map<String, Object> data);
public abstract String getPath();
}

View File

@@ -1,126 +0,0 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.security.JsonResponseHelper.Status.ERROR;
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import google.registry.model.OteAccountBuilder;
import google.registry.model.OteStats;
import google.registry.model.OteStats.StatType;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase.Type;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.JsonActionRunner;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.security.JsonResponseHelper;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
/**
* Admin servlet that allows creating or updating a registrar. Deletes are not allowed so as to
* preserve history.
*/
@Action(
service = GaeService.DEFAULT,
path = OteStatusAction.PATH,
method = Action.Method.POST,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public final class OteStatusAction implements Runnable, JsonActionRunner.JsonAction {
public static final String PATH = "/registrar-ote-status";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String CLIENT_ID_PARAM = "clientId";
private static final String COMPLETED_PARAM = "completed";
private static final String DETAILS_PARAM = "details";
private static final String STAT_TYPE_DESCRIPTION_PARAM = "description";
private static final String STAT_TYPE_REQUIREMENT_PARAM = "requirement";
private static final String STAT_TYPE_TIMES_PERFORMED_PARAM = "timesPerformed";
@Inject AuthenticatedRegistrarAccessor registrarAccessor;
@Inject JsonActionRunner jsonActionRunner;
@Inject
OteStatusAction() {}
@Override
public void run() {
jsonActionRunner.run(this);
}
@Override
public Map<String, ?> handleJsonRequest(Map<String, ?> input) {
try {
checkArgument(input != null, "Malformed JSON");
String oteClientId = (String) input.get(CLIENT_ID_PARAM);
checkArgument(
!Strings.isNullOrEmpty(oteClientId), "Missing key for OT&E client: %s", CLIENT_ID_PARAM);
String baseClientId = OteAccountBuilder.getBaseRegistrarId(oteClientId);
Registrar oteRegistrar = registrarAccessor.getRegistrar(oteClientId);
verifyOteAccess(baseClientId);
checkArgument(
Type.OTE.equals(oteRegistrar.getType()),
"Registrar with ID %s is not an OT&E registrar",
oteClientId);
OteStats oteStats = OteStats.getFromRegistrar(baseClientId);
return JsonResponseHelper.create(
SUCCESS, "OT&E check completed successfully", convertOteStats(baseClientId, oteStats));
} catch (Throwable e) {
logger.atWarning().withCause(e).log(
"Failed to verify OT&E status for registrar with input: %s", input);
return JsonResponseHelper.create(
ERROR, Optional.ofNullable(e.getMessage()).orElse("Unspecified error"));
}
}
private void verifyOteAccess(String baseClientId) throws RegistrarAccessDeniedException {
for (String oteClientId : OteAccountBuilder.createRegistrarIdToTldMap(baseClientId).keySet()) {
registrarAccessor.verifyAccess(oteClientId);
}
}
private Map<String, Object> convertOteStats(String baseClientId, OteStats oteStats) {
return ImmutableMap.of(
CLIENT_ID_PARAM, baseClientId,
COMPLETED_PARAM, oteStats.getFailures().isEmpty(),
DETAILS_PARAM,
StatType.REQUIRED_STAT_TYPES.stream()
.map(statType -> convertSingleRequirement(statType, oteStats.getCount(statType)))
.collect(toImmutableList()));
}
private Map<String, Object> convertSingleRequirement(StatType statType, int count) {
int requirement = statType.getRequirement();
return ImmutableMap.of(
STAT_TYPE_DESCRIPTION_PARAM, statType.getDescription(),
STAT_TYPE_REQUIREMENT_PARAM, requirement,
STAT_TYPE_TIMES_PERFORMED_PARAM, count,
COMPLETED_PARAM, count >= requirement);
}
}

View File

@@ -1,69 +0,0 @@
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import com.google.common.collect.ImmutableSet;
import com.google.monitoring.metrics.IncrementableMetric;
import com.google.monitoring.metrics.LabelDescriptor;
import com.google.monitoring.metrics.MetricRegistryImpl;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role;
import javax.inject.Inject;
final class RegistrarConsoleMetrics {
private static final ImmutableSet<LabelDescriptor> CONSOLE_LABEL_DESCRIPTORS =
ImmutableSet.of(
LabelDescriptor.create("clientId", "target registrar client ID"),
LabelDescriptor.create("explicitClientId", "whether the client ID is set explicitly"),
LabelDescriptor.create("role", "Role[s] of the user making the request"),
LabelDescriptor.create("status", "whether the request is successful"));
static final IncrementableMetric consoleRequestMetric =
MetricRegistryImpl.getDefault()
.newIncrementableMetric(
"/console/registrar/console_requests",
"Count of /registrar requests",
"count",
CONSOLE_LABEL_DESCRIPTORS);
private static final ImmutableSet<LabelDescriptor> SETTINGS_LABEL_DESCRIPTORS =
ImmutableSet.of(
LabelDescriptor.create("clientId", "target registrar client ID"),
LabelDescriptor.create("action", "action performed"),
LabelDescriptor.create("role", "Role[s] of the user making the request"),
LabelDescriptor.create("status", "whether the request is successful"));
static final IncrementableMetric settingsRequestMetric =
MetricRegistryImpl.getDefault()
.newIncrementableMetric(
"/console/registrar/setting_requests",
"Count of /registrar-settings requests",
"count",
SETTINGS_LABEL_DESCRIPTORS);
@Inject
public RegistrarConsoleMetrics() {}
void registerConsoleRequest(
String clientId, boolean explicitClientId, ImmutableSet<Role> roles, String status) {
consoleRequestMetric.increment(
clientId, String.valueOf(explicitClientId), String.valueOf(roles), status);
}
void registerSettingsRequest(
String clientId, String action, ImmutableSet<Role> roles, String status) {
settingsRequestMetric.increment(clientId, action, String.valueOf(roles), status);
}
}

View File

@@ -1,641 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static 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.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.security.JsonResponseHelper.Status.ERROR;
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import static google.registry.util.RegistryEnvironment.PRODUCTION;
import com.google.common.base.Ascii;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import google.registry.batch.CloudTasksUtils;
import google.registry.export.sheet.SyncRegistrarsSheetAction;
import google.registry.flows.certs.CertificateChecker;
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase.Type;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.HttpException.ForbiddenException;
import google.registry.request.JsonActionRunner;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role;
import google.registry.security.JsonResponseHelper;
import google.registry.ui.forms.FormException;
import google.registry.ui.forms.FormFieldException;
import google.registry.ui.server.RegistrarFormFields;
import google.registry.ui.server.SendEmailUtils;
import google.registry.util.CollectionUtils;
import google.registry.util.DiffUtils;
import google.registry.util.RegistryEnvironment;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
import org.joda.time.DateTime;
/**
* Admin servlet that allows creating or updating a registrar. Deletes are not allowed so as to
* preserve history.
*/
@Action(
service = GaeService.DEFAULT,
path = RegistrarSettingsAction.PATH,
method = Action.Method.POST,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonAction {
public static final String PATH = "/registrar-settings";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
static final String OP_PARAM = "op";
static final String ARGS_PARAM = "args";
static final String ID_PARAM = "id";
@Inject JsonActionRunner jsonActionRunner;
@Inject RegistrarConsoleMetrics registrarConsoleMetrics;
@Inject SendEmailUtils sendEmailUtils;
@Inject AuthenticatedRegistrarAccessor registrarAccessor;
@Inject AuthResult authResult;
@Inject CertificateChecker certificateChecker;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject RegistrarSettingsAction() {}
private static boolean hasPhone(RegistrarPoc contact) {
return contact.getPhoneNumber() != null;
}
@Override
public void run() {
jsonActionRunner.run(this);
}
@Override
public Map<String, Object> handleJsonRequest(Map<String, ?> input) {
if (input == null) {
throw new BadRequestException("Malformed JSON");
}
String registrarId = (String) input.get(ID_PARAM);
if (Strings.isNullOrEmpty(registrarId)) {
throw new BadRequestException(String.format("Missing key for resource ID: %s", ID_PARAM));
}
// Process the operation. Though originally derived from a CRUD
// handler, registrar-settings really only supports read and update.
String op = Optional.ofNullable((String) input.get(OP_PARAM)).orElse("read");
@SuppressWarnings("unchecked")
Map<String, ?> args =
(Map<String, Object>)
Optional.<Object>ofNullable(input.get(ARGS_PARAM)).orElse(ImmutableMap.of());
logger.atInfo().log(
"Received request '%s' on registrar '%s' with args %s", op, registrarId, args);
String status = "SUCCESS";
try {
return switch (op) {
case "update" -> update(args, registrarId).toJsonResponse();
case "read" -> read(registrarId).toJsonResponse();
default -> throw new IllegalArgumentException("Unknown or unsupported operation: " + op);
};
} catch (Throwable e) {
logger.atWarning().withCause(e).log(
"Failed to perform operation '%s' on registrar '%s' for args %s", op, registrarId, args);
status = "ERROR: " + e.getClass().getSimpleName();
if (e instanceof FormFieldException formFieldException) {
return JsonResponseHelper.createFormFieldError(
formFieldException.getMessage(), formFieldException.getFieldName());
}
return JsonResponseHelper.create(
ERROR, Optional.ofNullable(e.getMessage()).orElse("Unspecified error"));
} finally {
registrarConsoleMetrics.registerSettingsRequest(
registrarId, op, registrarAccessor.getRolesForRegistrar(registrarId), status);
}
}
record RegistrarResult(String message, Registrar registrar) {
Map<String, Object> toJsonResponse() {
return JsonResponseHelper.create(SUCCESS, message(), registrar().toJsonMap());
}
static RegistrarResult create(String message, Registrar registrar) {
return new RegistrarResult(message, registrar);
}
}
record EmailInfo(
Registrar registrar,
Registrar updatedRegistrar,
ImmutableSet<RegistrarPoc> contacts,
ImmutableSet<RegistrarPoc> updatedContacts) {
static EmailInfo create(
Registrar registrar,
Registrar updatedRegistrar,
ImmutableSet<RegistrarPoc> contacts,
ImmutableSet<RegistrarPoc> updatedContacts) {
return new EmailInfo(registrar, updatedRegistrar, contacts, updatedContacts);
}
}
private RegistrarResult read(String registrarId) {
return RegistrarResult.create("Success", loadRegistrarUnchecked(registrarId));
}
private Registrar loadRegistrarUnchecked(String registrarId) {
try {
return registrarAccessor.getRegistrar(registrarId);
} catch (RegistrarAccessDeniedException e) {
throw new ForbiddenException(e.getMessage(), e);
}
}
private RegistrarResult update(final Map<String, ?> args, String registrarId) {
// Email the updates
sendExternalUpdatesIfNecessary(tm().transact(() -> saveUpdates(args, registrarId)));
// Reload the result outside the transaction to get the most recent version
return RegistrarResult.create("Saved " + registrarId, loadRegistrarUnchecked(registrarId));
}
/** Saves the updates and returns info needed for the update email */
private EmailInfo saveUpdates(final Map<String, ?> args, String registrarId) {
// We load the registrar here rather than outside the transaction - to make
// sure we have the latest version. This one is loaded inside the transaction, so it's
// guaranteed to not change before we update it.
Registrar registrar = loadRegistrarUnchecked(registrarId);
// Detach the registrar to avoid Hibernate object-updates, since we wish to email
// out the diffs between the existing and updated registrar objects
tm().getEntityManager().detach(registrar);
// Verify that the registrar hasn't been changed.
// To do that - we find the latest update time (or null if the registrar has been
// deleted) and compare to the update time from the args. The update time in the args
// comes from the read that gave the UI the data - if it's out of date, then the UI
// had out of date data.
DateTime latest = registrar.getLastUpdateTime();
DateTime latestFromArgs = RegistrarFormFields.LAST_UPDATE_TIME.extractUntyped(args).get();
if (!latestFromArgs.equals(latest)) {
logger.atWarning().log(
"Registrar changed since reading the data!"
+ " Last updated at %s, but args data last updated at %s.",
latest, latestFromArgs);
throw new IllegalStateException(
"Registrar has been changed by someone else. Please reload and retry.");
}
// Keep the current contacts, so we can later check that no required contact was
// removed, email the changes to the contacts
ImmutableSet<RegistrarPoc> contacts = registrar.getContacts();
Registrar updatedRegistrar = registrar;
// Do OWNER only updates to the registrar from the request.
updatedRegistrar = checkAndUpdateOwnerControlledFields(updatedRegistrar, args);
// Do ADMIN only updates to the registrar from the request.
updatedRegistrar = checkAndUpdateAdminControlledFields(updatedRegistrar, args);
// read the contacts from the request.
ImmutableSet<RegistrarPoc> updatedContacts = readContacts(registrar, contacts, args);
// Save the updated contacts
if (!updatedContacts.equals(contacts)) {
if (!registrarAccessor.hasRoleOnRegistrar(Role.OWNER, registrar.getRegistrarId())) {
throw new ForbiddenException("Only OWNERs can update the contacts");
}
checkContactRequirements(contacts, updatedContacts);
RegistrarPoc.updateContacts(updatedRegistrar, updatedContacts);
updatedRegistrar = updatedRegistrar.asBuilder().setContactsRequireSyncing(true).build();
}
// Save the updated registrar
if (!updatedRegistrar.equals(registrar)) {
tm().put(updatedRegistrar);
}
return EmailInfo.create(registrar, updatedRegistrar, contacts, updatedContacts);
}
private Map<String, Object> expandRegistrarWithContacts(
Iterable<RegistrarPoc> contacts, Registrar registrar) {
ImmutableSet<Map<String, Object>> expandedContacts =
Streams.stream(contacts)
.map(RegistrarPoc::toDiffableFieldMap)
// Note: per the javadoc, toDiffableFieldMap includes sensitive data, but we don't want
// to display it here
.peek(
map -> {
map.remove("registryLockPasswordHash");
map.remove("registryLockPasswordSalt");
})
.collect(toImmutableSet());
// Use LinkedHashMap here to preserve ordering; null values mean we can't use ImmutableMap.
LinkedHashMap<String, Object> result = new LinkedHashMap<>(registrar.toDiffableFieldMap());
result.put("contacts", expandedContacts);
return result;
}
/**
* Updates registrar with the OWNER-controlled args from the http request.
*
* <p>If any changes were made and the user isn't an OWNER - throws a {@link ForbiddenException}.
*/
private Registrar checkAndUpdateOwnerControlledFields(
Registrar initialRegistrar, Map<String, ?> args) {
Registrar.Builder builder = initialRegistrar.asBuilder();
// WHOIS
//
// Because of how whoisServer handles "default value", it's possible that setting the existing
// value will still change the Registrar. So we first check whether the value has changed.
//
// The problem is - if the Registrar has a "null" whoisServer value, the console gets the
// "default value" instead of the actual (null) value.
// This was done so we display the "default" value, but it also means that it always looks like
// the user updated the whoisServer value from "null" to the default value.
//
// TODO(b/119913848):once a null whoisServer value is sent to the console as "null", there's no
// need to check for equality before setting the value in the builder.
String updatedWhoisServer =
RegistrarFormFields.WHOIS_SERVER_FIELD.extractUntyped(args).orElse(null);
if (!Objects.equals(initialRegistrar.getWhoisServer(), updatedWhoisServer)) {
builder.setWhoisServer(updatedWhoisServer);
}
builder.setUrl(RegistrarFormFields.URL_FIELD.extractUntyped(args).orElse(null));
// If the email is already null / empty - we can keep it so. But if it's set - it's required to
// remain set.
(Strings.isNullOrEmpty(initialRegistrar.getEmailAddress())
? RegistrarFormFields.EMAIL_ADDRESS_FIELD_OPTIONAL
: RegistrarFormFields.EMAIL_ADDRESS_FIELD_REQUIRED)
.extractUntyped(args)
.ifPresent(builder::setEmailAddress);
builder.setPhoneNumber(
RegistrarFormFields.PHONE_NUMBER_FIELD.extractUntyped(args).orElse(null));
builder.setFaxNumber(RegistrarFormFields.FAX_NUMBER_FIELD.extractUntyped(args).orElse(null));
builder.setLocalizedAddress(
RegistrarFormFields.L10N_ADDRESS_FIELD.extractUntyped(args).orElse(null));
// Security
builder.setIpAddressAllowList(
RegistrarFormFields.IP_ADDRESS_ALLOW_LIST_FIELD
.extractUntyped(args)
.orElse(ImmutableList.of()));
Optional<String> certificateString =
RegistrarFormFields.CLIENT_CERTIFICATE_FIELD.extractUntyped(args);
if (certificateString.isPresent()) {
if (validateCertificate(initialRegistrar.getClientCertificate(), certificateString.get())) {
builder.setClientCertificate(certificateString.get(), tm().getTransactionTime());
}
}
Optional<String> failoverCertificateString =
RegistrarFormFields.FAILOVER_CLIENT_CERTIFICATE_FIELD.extractUntyped(args);
if (failoverCertificateString.isPresent()) {
if (validateCertificate(
initialRegistrar.getFailoverClientCertificate(), failoverCertificateString.get())) {
builder.setFailoverClientCertificate(
failoverCertificateString.get(), tm().getTransactionTime());
}
}
return checkNotChangedUnlessAllowed(builder, initialRegistrar, Role.OWNER);
}
/**
* Returns true if the registrar should accept the new certificate. Returns false if the
* certificate is already the one stored for the registrar.
*/
private boolean validateCertificate(
Optional<String> existingCertificate, String certificateString) {
if (existingCertificate.isEmpty() || !existingCertificate.get().equals(certificateString)) {
try {
certificateChecker.validateCertificate(certificateString);
} catch (InsecureCertificateException e) {
throw new IllegalArgumentException(e.getMessage());
}
return true;
}
return false;
}
/**
* Updates a registrar with the ADMIN-controlled args from the http request.
*
* <p>If any changes were made and the user isn't an ADMIN - throws a {@link ForbiddenException}.
*/
private Registrar checkAndUpdateAdminControlledFields(
Registrar initialRegistrar, Map<String, ?> args) {
Registrar.Builder builder = initialRegistrar.asBuilder();
Set<String> updatedAllowedTlds =
RegistrarFormFields.ALLOWED_TLDS_FIELD.extractUntyped(args).orElse(ImmutableSet.of());
// Temporarily block anyone from removing an allowed TLD.
// This is so we can start having Support users use the console in production before we finish
// implementing configurable access control.
// TODO(b/119549884): remove this code once configurable access control is implemented.
if (!Sets.difference(initialRegistrar.getAllowedTlds(), updatedAllowedTlds).isEmpty()) {
throw new ForbiddenException("Can't remove allowed TLDs using the console.");
}
if (!Sets.difference(updatedAllowedTlds, initialRegistrar.getAllowedTlds()).isEmpty()) {
// If a REAL registrar isn't in compliance with regard to having an abuse contact set,
// prevent addition of allowed TLDs until that's fixed.
if (Registrar.Type.REAL.equals(initialRegistrar.getType())
&& PRODUCTION.equals(RegistryEnvironment.get())) {
checkArgumentPresent(
initialRegistrar.getWhoisAbuseContact(),
"Cannot add allowed TLDs if there is no WHOIS abuse contact set.");
}
}
builder.setAllowedTlds(updatedAllowedTlds);
return checkNotChangedUnlessAllowed(builder, initialRegistrar, Role.ADMIN);
}
/**
* Makes sure {@code builder.build}is different from {@code originalRegistrar} only if we have the
* correct role.
*
* <p>On success, returns {@code builder.build()}.
*/
private Registrar checkNotChangedUnlessAllowed(
Registrar.Builder builder, Registrar originalRegistrar, Role allowedRole) {
Registrar updatedRegistrar = builder.build();
if (updatedRegistrar.equals(originalRegistrar)) {
return updatedRegistrar;
}
checkArgument(
updatedRegistrar.getRegistrarId().equals(originalRegistrar.getRegistrarId()),
"Can't change clientId (%s -> %s)",
originalRegistrar.getRegistrarId(),
updatedRegistrar.getRegistrarId());
if (registrarAccessor.hasRoleOnRegistrar(allowedRole, originalRegistrar.getRegistrarId())) {
return updatedRegistrar;
}
Map<?, ?> diffs =
DiffUtils.deepDiff(
originalRegistrar.toDiffableFieldMap(), updatedRegistrar.toDiffableFieldMap(), true);
// It's expected that the update timestamp will be changed, as it gets reset whenever we change
// nested collections. If it's the only change, just return the original registrar.
if (diffs.keySet().equals(ImmutableSet.of("lastUpdateTime"))) {
return originalRegistrar;
}
throw new ForbiddenException(
String.format("Unauthorized: only %s can change fields %s", allowedRole, diffs.keySet()));
}
/** Reads the contacts from the supplied args. */
public static ImmutableSet<RegistrarPoc> readContacts(
Registrar registrar, ImmutableSet<RegistrarPoc> existingContacts, Map<String, ?> args) {
return RegistrarFormFields.getRegistrarContactBuilders(existingContacts, args).stream()
.map(builder -> builder.setRegistrar(registrar).build())
.collect(toImmutableSet());
}
/**
* Enforces business logic checks on registrar contacts.
*
* @throws FormException if the checks fail.
*/
public static void checkContactRequirements(
ImmutableSet<RegistrarPoc> existingContacts, ImmutableSet<RegistrarPoc> updatedContacts) {
// Check that no two contacts use the same email address.
Set<String> emails = new HashSet<>();
for (RegistrarPoc contact : updatedContacts) {
if (!emails.add(contact.getEmailAddress())) {
throw new ContactRequirementException(
String.format(
"One email address (%s) cannot be used for multiple contacts",
contact.getEmailAddress()));
}
}
// Check that required contacts don't go away, once they are set.
Multimap<Type, RegistrarPoc> oldContactsByType = HashMultimap.create();
for (RegistrarPoc contact : existingContacts) {
for (Type t : contact.getTypes()) {
oldContactsByType.put(t, contact);
}
}
Multimap<Type, RegistrarPoc> newContactsByType = HashMultimap.create();
for (RegistrarPoc contact : updatedContacts) {
for (Type t : contact.getTypes()) {
newContactsByType.put(t, contact);
}
}
for (Type t : difference(oldContactsByType.keySet(), newContactsByType.keySet())) {
if (t.isRequired()) {
throw new ContactRequirementException(t);
}
}
ensurePhoneNumberNotRemovedForContactTypes(oldContactsByType, newContactsByType, Type.TECH);
Optional<RegistrarPoc> domainWhoisAbuseContact =
getDomainWhoisVisibleAbuseContact(updatedContacts);
// If the new set has a domain WHOIS abuse contact, it must have a phone number.
if (domainWhoisAbuseContact.isPresent()
&& domainWhoisAbuseContact.get().getPhoneNumber() == null) {
throw new ContactRequirementException(
"The abuse contact visible in domain WHOIS query must have a phone number");
}
// If there was a domain WHOIS abuse contact in the old set, the new set must have one.
if (getDomainWhoisVisibleAbuseContact(existingContacts).isPresent()
&& domainWhoisAbuseContact.isEmpty()) {
throw new ContactRequirementException(
"An abuse contact visible in domain WHOIS query must be designated");
}
checkContactRegistryLockRequirements(existingContacts, updatedContacts);
}
private static void checkContactRegistryLockRequirements(
ImmutableSet<RegistrarPoc> existingContacts, ImmutableSet<RegistrarPoc> updatedContacts) {
// Any contact(s) with new passwords must be allowed to set them
for (RegistrarPoc updatedContact : updatedContacts) {
if (updatedContact.isRegistryLockAllowed()
|| updatedContact.isAllowedToSetRegistryLockPassword()) {
RegistrarPoc existingContact =
existingContacts.stream()
.filter(
contact -> contact.getEmailAddress().equals(updatedContact.getEmailAddress()))
.findFirst()
.orElseThrow(
() ->
new FormException(
"Cannot set registry lock password directly on new contact"));
// Can't modify registry lock email address
if (!Objects.equals(
updatedContact.getRegistryLockEmailAddress(),
existingContact.getRegistryLockEmailAddress())) {
throw new FormException("Cannot modify registryLockEmailAddress through the UI");
}
if (updatedContact.isRegistryLockAllowed()) {
// the password must have been set before or the user was allowed to set it now
if (!existingContact.isAllowedToSetRegistryLockPassword()
&& !existingContact.isRegistryLockAllowed()) {
throw new FormException("Registrar contact not allowed to set registry lock password");
}
}
if (updatedContact.isAllowedToSetRegistryLockPassword()) {
if (!existingContact.isAllowedToSetRegistryLockPassword()) {
throw new FormException(
"Cannot modify isAllowedToSetRegistryLockPassword through the UI");
}
}
}
}
// Any previously-existing contacts with registry lock enabled cannot be deleted
existingContacts.stream()
.filter(RegistrarPoc::isRegistryLockAllowed)
.forEach(
contact -> {
Optional<RegistrarPoc> updatedContactOptional =
updatedContacts.stream()
.filter(
updatedContact ->
updatedContact.getEmailAddress().equals(contact.getEmailAddress()))
.findFirst();
if (updatedContactOptional.isEmpty()) {
throw new FormException(
String.format(
"Cannot delete the contact %s that has registry lock enabled",
contact.getEmailAddress()));
}
if (!updatedContactOptional.get().isRegistryLockAllowed()) {
throw new FormException(
String.format(
"Cannot remove the ability to use registry lock on the contact %s",
contact.getEmailAddress()));
}
});
}
/**
* Ensure that for each given registrar type, a phone number is present after update, if there was
* one before.
*/
private static void ensurePhoneNumberNotRemovedForContactTypes(
Multimap<Type, RegistrarPoc> oldContactsByType,
Multimap<Type, RegistrarPoc> newContactsByType,
Type... types) {
for (Type type : types) {
if (oldContactsByType.get(type).stream().anyMatch(RegistrarSettingsAction::hasPhone)
&& newContactsByType.get(type).stream().noneMatch(RegistrarSettingsAction::hasPhone)) {
throw new ContactRequirementException(
String.format(
"Please provide a phone number for at least one %s contact",
type.getDisplayName()));
}
}
}
/**
* Retrieves the registrar contact whose phone number and email address is visible in domain WHOIS
* query as abuse contact (if any).
*
* <p>Frontend processing ensures that only one contact can be set as abuse contact in domain
* WHOIS record. Therefore, it is possible to return inside the loop once one such contact is
* found.
*/
private static Optional<RegistrarPoc> getDomainWhoisVisibleAbuseContact(
Set<RegistrarPoc> contacts) {
return contacts.stream().filter(RegistrarPoc::getVisibleInDomainWhoisAsAbuse).findFirst();
}
/**
* Determines if any changes were made to the registrar besides the lastUpdateTime, and if so,
* sends an email with a diff of the changes to the configured notification email address and all
* contact addresses and enqueues a task to re-sync the registrar sheet.
*/
private void sendExternalUpdatesIfNecessary(EmailInfo emailInfo) {
ImmutableSet<RegistrarPoc> existingContacts = emailInfo.contacts();
if (!sendEmailUtils.hasRecipients() && existingContacts.isEmpty()) {
return;
}
Registrar existingRegistrar = emailInfo.registrar();
Map<?, ?> diffs =
DiffUtils.deepDiff(
expandRegistrarWithContacts(existingContacts, existingRegistrar),
expandRegistrarWithContacts(emailInfo.updatedContacts(), emailInfo.updatedRegistrar()),
true);
@SuppressWarnings("unchecked")
Set<String> changedKeys = (Set<String>) diffs.keySet();
if (CollectionUtils.difference(changedKeys, "lastUpdateTime").isEmpty()) {
return;
}
if (!RegistryEnvironment.isInTestServer()) {
// Enqueues a sync registrar sheet task if enqueuing is not triggered by console tests and
// there's an update besides the lastUpdateTime
cloudTasksUtils.enqueue(
SyncRegistrarsSheetAction.QUEUE,
cloudTasksUtils.createTask(
SyncRegistrarsSheetAction.class, Action.Method.POST, ImmutableMultimap.of()));
}
String environment = Ascii.toLowerCase(String.valueOf(RegistryEnvironment.get()));
sendEmailUtils.sendEmail(
String.format(
"Registrar %s (%s) updated in registry %s environment",
existingRegistrar.getRegistrarName(), existingRegistrar.getRegistrarId(), environment),
String.format(
"""
The following changes were made in registry %s environment to the registrar %s by\
%s:
%s""",
environment,
existingRegistrar.getRegistrarId(),
authResult.userIdForLogging(),
DiffUtils.prettyPrintDiffedMap(diffs, null)),
existingContacts.stream()
.filter(c -> c.getTypes().contains(Type.ADMIN))
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableList()));
}
/** Thrown when a set of contacts doesn't meet certain constraints. */
private static class ContactRequirementException extends FormException {
ContactRequirementException(String msg) {
super(msg);
}
ContactRequirementException(Type type) {
super(String.format("Must have at least one %s contact", type.getDisplayName()));
}
}
}

View File

@@ -1,182 +0,0 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
import static google.registry.ui.server.console.ConsoleModule.PARAM_CLIENT_ID;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.domain.RegistryLock;
import google.registry.model.registrar.Registrar;
import google.registry.model.tld.RegistryLockDao;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Action.Method;
import google.registry.request.Parameter;
import google.registry.request.RequestMethod;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.security.JsonResponseHelper;
import java.util.Optional;
import javax.inject.Inject;
import org.joda.time.DateTime;
/**
* Action that allows for getting locks for a particular registrar.
*
* <p>Note: at the moment we have no mechanism for JSON GET/POSTs in the same class or at the same
* URL, which is why this is distinct from the {@link RegistryLockPostAction}.
*/
@Action(
service = GaeService.DEFAULT,
path = RegistryLockGetAction.PATH,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public final class RegistryLockGetAction implements JsonGetAction {
public static final String PATH = "/registry-lock-get";
private static final String LOCK_ENABLED_FOR_CONTACT_PARAM = "lockEnabledForContact";
private static final String EMAIL_PARAM = "email";
private static final String LOCKS_PARAM = "locks";
private static final String DOMAIN_NAME_PARAM = "domainName";
private static final String LOCKED_TIME_PARAM = "lockedTime";
private static final String LOCKED_BY_PARAM = "lockedBy";
private static final String IS_LOCK_PENDING_PARAM = "isLockPending";
private static final String IS_UNLOCK_PENDING_PARAM = "isUnlockPending";
private static final String USER_CAN_UNLOCK_PARAM = "userCanUnlock";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Gson GSON = new Gson();
@VisibleForTesting Method method;
private final Response response;
@VisibleForTesting AuthenticatedRegistrarAccessor registrarAccessor;
@VisibleForTesting AuthResult authResult;
@VisibleForTesting Optional<String> paramClientId;
@Inject
RegistryLockGetAction(
@RequestMethod Method method,
Response response,
AuthenticatedRegistrarAccessor registrarAccessor,
AuthResult authResult,
@Parameter("consoleClientId") Optional<String> paramClientId) {
this.method = method;
this.response = response;
this.registrarAccessor = registrarAccessor;
this.authResult = authResult;
this.paramClientId = paramClientId;
}
@Override
public void run() {
checkArgument(Method.GET.equals(method), "Only GET requests allowed");
checkArgument(authResult.user().isPresent(), "User must be present");
checkArgument(paramClientId.isPresent(), "clientId must be present");
response.setContentType(MediaType.JSON_UTF_8);
try {
ImmutableMap<String, ?> resultMap = getLockedDomainsMap(paramClientId.get());
ImmutableMap<String, ?> payload =
JsonResponseHelper.create(SUCCESS, "Successful locks retrieval", resultMap);
response.setPayload(GSON.toJson(payload));
} catch (RegistrarAccessDeniedException e) {
logger.atWarning().withCause(e).log(
"User %s doesn't have access to this registrar.", authResult.userIdForLogging());
response.setStatus(SC_FORBIDDEN);
} catch (Exception e) {
logger.atWarning().withCause(e).log(
"Unexpected error when retrieving locks for a registrar.");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
}
}
static void verifyLockAccess(
AuthenticatedRegistrarAccessor registrarAccessor, String clientId, boolean isAdmin)
throws RegistrarAccessDeniedException {
Registrar registrar = registrarAccessor.getRegistrar(clientId);
checkArgument(
isAdmin || registrar.isRegistryLockAllowed(),
"Registry lock not allowed for registrar %s",
clientId);
}
private ImmutableMap<String, ?> getLockedDomainsMap(String registrarId)
throws RegistrarAccessDeniedException {
// Note: admins always have access to the locks page
checkArgument(authResult.user().isPresent(), "User must be present");
boolean isAdmin = registrarAccessor.isAdmin();
verifyLockAccess(registrarAccessor, registrarId, isAdmin);
User user = authResult.user().get();
// Split logic depending on whether we are using the old auth system or the new one
boolean isRegistryLockAllowed;
isRegistryLockAllowed =
user.getUserRoles().hasPermission(registrarId, ConsolePermission.REGISTRY_LOCK);
String relevantEmail = user.getRegistryLockEmailAddress().orElse(user.getEmailAddress());
// Use the contact's registry lock email if it's present, else use the login email (for admins)
return ImmutableMap.of(
LOCK_ENABLED_FOR_CONTACT_PARAM,
isRegistryLockAllowed,
EMAIL_PARAM,
relevantEmail,
PARAM_CLIENT_ID,
registrarId,
LOCKS_PARAM,
getLockedDomains(registrarId, isAdmin));
}
private static ImmutableList<ImmutableMap<String, ?>> getLockedDomains(
String registrarId, boolean isAdmin) {
return tm().transact(
() ->
RegistryLockDao.getLocksByRegistrarId(registrarId).stream()
.filter(lock -> !lock.isLockRequestExpired(tm().getTransactionTime()))
.map(lock -> lockToMap(lock, isAdmin))
.collect(toImmutableList()));
}
private static ImmutableMap<String, ?> lockToMap(RegistryLock lock, boolean isAdmin) {
DateTime now = tm().getTransactionTime();
return new ImmutableMap.Builder<String, Object>()
.put(DOMAIN_NAME_PARAM, lock.getDomainName())
.put(LOCKED_TIME_PARAM, lock.getLockCompletionTime().map(DateTime::toString).orElse(""))
.put(LOCKED_BY_PARAM, lock.isSuperuser() ? "admin" : lock.getRegistrarPocId())
.put(IS_LOCK_PENDING_PARAM, lock.getLockCompletionTime().isEmpty())
.put(
IS_UNLOCK_PENDING_PARAM,
lock.getUnlockRequestTime().isPresent()
&& lock.getUnlockCompletionTime().isEmpty()
&& !lock.isUnlockRequestExpired(now))
.put(USER_CAN_UNLOCK_PARAM, isAdmin || !lock.isSuperuser())
.build();
}
}

View File

@@ -1,197 +0,0 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.security.JsonResponseHelper.Status.ERROR;
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
import static google.registry.ui.server.registrar.RegistryLockGetAction.verifyLockAccess;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gson.Gson;
import google.registry.flows.domain.DomainFlowUtils;
import google.registry.groups.GmailClient;
import google.registry.model.console.User;
import google.registry.model.domain.RegistryLock;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Action.Method;
import google.registry.request.HttpException.ForbiddenException;
import google.registry.request.JsonActionRunner;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.security.JsonResponseHelper;
import google.registry.tools.DomainLockUtils;
import google.registry.util.EmailMessage;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import org.apache.http.client.utils.URIBuilder;
import org.joda.time.Duration;
/**
* UI action that allows for creating registry locks. Locks / unlocks must be verified separately
* before they are written permanently.
*
* <p>Note: at the moment we have no mechanism for JSON GET/POSTs in the same class or at the same
* URL, which is why this is distinct from the {@link RegistryLockGetAction}.
*/
@Action(
service = GaeService.DEFAULT,
path = RegistryLockPostAction.PATH,
method = Method.POST,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAction {
public static final String PATH = "/registry-lock-post";
public static final String VERIFICATION_EMAIL_TEMPLATE =
"""
Please click the link below to perform the lock / unlock action on domain %s. Note: this\
code will expire in one hour.
%s""";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Gson GSON = new Gson();
private final HttpServletRequest req;
private final JsonActionRunner jsonActionRunner;
private final AuthResult authResult;
private final AuthenticatedRegistrarAccessor registrarAccessor;
private final GmailClient gmailClient;
private final DomainLockUtils domainLockUtils;
@Inject
RegistryLockPostAction(
HttpServletRequest req,
JsonActionRunner jsonActionRunner,
AuthResult authResult,
AuthenticatedRegistrarAccessor registrarAccessor,
GmailClient gmailClient,
DomainLockUtils domainLockUtils) {
this.req = req;
this.jsonActionRunner = jsonActionRunner;
this.authResult = authResult;
this.registrarAccessor = registrarAccessor;
this.gmailClient = gmailClient;
this.domainLockUtils = domainLockUtils;
}
@Override
public void run() {
jsonActionRunner.run(this);
}
@Override
public Map<String, ?> handleJsonRequest(Map<String, ?> input) {
try {
checkArgumentNotNull(input, "Null JSON");
RegistryLockPostInput postInput =
GSON.fromJson(GSON.toJsonTree(input), RegistryLockPostInput.class);
String registrarId = postInput.registrarId;
checkArgument(!Strings.isNullOrEmpty(registrarId), "Missing key for registrarId");
checkArgument(!Strings.isNullOrEmpty(postInput.domainName), "Missing key for domainName");
DomainFlowUtils.validateDomainName(postInput.domainName);
checkNotNull(postInput.isLock, "Missing key for isLock");
User user =
authResult.user().orElseThrow(() -> new ForbiddenException("User is not logged in"));
// TODO: Move this line to the transaction below during nested transaction refactoring.
String userEmail = verifyPasswordAndGetEmail(user, postInput);
tm().transact(
() -> {
RegistryLock registryLock =
postInput.isLock
? domainLockUtils.saveNewRegistryLockRequest(
postInput.domainName,
registrarId,
userEmail,
registrarAccessor.isAdmin())
: domainLockUtils.saveNewRegistryUnlockRequest(
postInput.domainName,
registrarId,
registrarAccessor.isAdmin(),
Optional.ofNullable(postInput.relockDurationMillis).map(Duration::new));
sendVerificationEmail(registryLock, userEmail, postInput.isLock);
});
String action = postInput.isLock ? "lock" : "unlock";
return JsonResponseHelper.create(SUCCESS, String.format("Successful %s", action));
} catch (Throwable e) {
logger.atWarning().withCause(e).log("Failed to lock/unlock domain.");
return JsonResponseHelper.create(
ERROR,
Optional.ofNullable(Throwables.getRootCause(e).getMessage()).orElse("Unspecified error"));
}
}
private void sendVerificationEmail(RegistryLock lock, String userEmail, boolean isLock) {
try {
String url =
new URIBuilder()
.setScheme("https")
.setHost(req.getServerName())
.setPath("registry-lock-verify")
.setParameter("lockVerificationCode", lock.getVerificationCode())
.build()
.toString();
String body = String.format(VERIFICATION_EMAIL_TEMPLATE, lock.getDomainName(), url);
ImmutableList<InternetAddress> recipients =
ImmutableList.of(new InternetAddress(userEmail, true));
String action = isLock ? "lock" : "unlock";
gmailClient.sendEmail(
EmailMessage.newBuilder()
.setBody(body)
.setSubject(String.format("Registry %s verification", action))
.setRecipients(recipients)
.build());
} catch (AddressException | URISyntaxException e) {
throw new RuntimeException(e); // caught above -- this is so we can run in a transaction
}
}
private String verifyPasswordAndGetEmail(User user, RegistryLockPostInput postInput)
throws RegistrarAccessDeniedException {
if (registrarAccessor.isAdmin()) {
return user.getEmailAddress();
}
// Verify that the registrar has locking enabled
verifyLockAccess(registrarAccessor, postInput.registrarId, false);
checkArgument(
user.verifyRegistryLockPassword(postInput.password),
"Incorrect registry lock password for user");
return user.getRegistryLockEmailAddress()
.orElseThrow(() -> new IllegalArgumentException("User has no registry lock email address"));
}
/** Value class that represents the expected input body from the UI request. */
private static class RegistryLockPostInput {
private String registrarId;
private String domainName;
private Boolean isLock;
private String password;
private Long relockDurationMillis;
}
}

View File

@@ -1,91 +0,0 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static google.registry.ui.server.SoyTemplateUtils.CSS_RENAMING_MAP_SUPPLIER;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.flogger.FluentLogger;
import com.google.template.soy.tofu.SoyTofu;
import google.registry.model.domain.RegistryLock;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.tools.DomainLockUtils;
import google.registry.ui.server.SoyTemplateUtils;
import google.registry.ui.soy.registrar.RegistryLockVerificationSoyInfo;
import java.util.Map;
import javax.inject.Inject;
/** Action that allows for verification of registry lock / unlock requests */
@Action(
service = GaeService.DEFAULT,
path = RegistryLockVerifyAction.PATH,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public final class RegistryLockVerifyAction extends HtmlAction {
public static final String PATH = "/registry-lock-verify";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Supplier<SoyTofu> TOFU_SUPPLIER =
SoyTemplateUtils.createTofuSupplier(
google.registry.ui.soy.registrar.AnalyticsSoyInfo.getInstance(),
google.registry.ui.soy.registrar.ConsoleSoyInfo.getInstance(),
google.registry.ui.soy.registrar.ConsoleUtilsSoyInfo.getInstance(),
google.registry.ui.soy.registrar.RegistryLockVerificationSoyInfo.getInstance());
private final DomainLockUtils domainLockUtils;
private final String lockVerificationCode;
@Inject
public RegistryLockVerifyAction(
DomainLockUtils domainLockUtils,
@Parameter("lockVerificationCode") String lockVerificationCode) {
this.domainLockUtils = domainLockUtils;
this.lockVerificationCode = lockVerificationCode;
}
@Override
public void runAfterLogin(Map<String, Object> data) {
try {
boolean isAdmin = authResult.user().get().getUserRoles().isAdmin();
RegistryLock resultLock =
domainLockUtils.verifyVerificationCode(lockVerificationCode, isAdmin);
data.put("isLock", resultLock.getUnlockCompletionTime().isEmpty());
data.put("success", true);
data.put("domainName", resultLock.getDomainName());
} catch (Throwable t) {
logger.atWarning().withCause(t).log(
"Error when verifying verification code '%s'.", lockVerificationCode);
data.put("success", false);
data.put("errorMessage", Throwables.getRootCause(t).getMessage());
}
response.setPayload(
TOFU_SUPPLIER
.get()
.newRenderer(RegistryLockVerificationSoyInfo.VERIFICATION_PAGE)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(data)
.render());
}
@Override
public String getPath() {
return PATH;
}
}

View File

@@ -1,16 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
@javax.annotation.ParametersAreNonnullByDefault
package google.registry.ui.server.registrar;

View File

@@ -17,7 +17,7 @@
/**
* Template for the content of the monthly spec11 email
*/
{template .monthlySpec11Email}
{template monthlySpec11Email}
{@param threats: list<map<string, string>>}
{@param resources: list<string>}
{@param registry: string}
@@ -32,7 +32,7 @@
security concerns. This may be because the registrants have not completed the requisite steps
to mitigate the potential security abuse and/or have it reviewed and delisted.</p>
{call .threatMatchTable}
{call threatMatchTable}
{param threats: $threats /}
{/call}
@@ -42,7 +42,7 @@
please <a href="https://safebrowsing.google.com/safebrowsing/report_error/?hl=en">submit a
request</a> to have the site reviewed.</p>
{call .resourceList}
{call resourceList}
{param resources: $resources /}
{/call}
@@ -56,7 +56,7 @@
/**
* Template for the content of the daily spec11 email
*/
{template .dailySpec11Email}
{template dailySpec11Email}
{@param threats: list<map<string, string>>}
{@param resources: list<string>}
{@param date: string}
@@ -69,14 +69,14 @@
identify potential security concerns. On {$date}, the following domains that your
registrar manages were flagged for potential security concerns:</p>
{call .threatMatchTable}
{call threatMatchTable}
{param threats: $threats /}
{/call}
<p><b>Please communicate these findings to the registrant and work with the
registrant to mitigate any security issues and have the domains delisted.</b></p>
{call .resourceList}
{call resourceList}
{param resources: $resources /}
{/call}
@@ -98,7 +98,7 @@
/**
* Template for the list of potentially-useful resources
*/
{template .resourceList}
{template resourceList}
{@param resources: list<string>}
{if length($resources) > 0}
Some helpful resources for getting off a blocked list include:
@@ -113,7 +113,7 @@
/**
* Template for the table containing the threats themselves
*/
{template .threatMatchTable}
{template threatMatchTable}
{@param threats: list<map<string, string>>}
<table>
<tr>
@@ -122,8 +122,8 @@
</tr>
{for $threat in $threats}
<tr>
<td>{$threat['domainName']}</td>
<td>{$threat['threatType']}</td>
<td>{$threat.get('domainName')}</td>
<td>{$threat.get('threatType')}</td>
</tr>
{/for}
</table>

View File

@@ -12,22 +12,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.contact_create}
/**
* Create contact
*/
{template .contactcreate stricthtml="false"}
{@param? id: string}
{@param? name: string}
{@param? org: string}
{@param? street: list<string>}
{@param? city: string}
{@param? state: string}
{@param? zip: string}
{@param? cc: string}
{@param? phone: string}
{@param? fax: string}
{@param? email: string}
{template contactcreate stricthtml="false"}
{@param? id: string|null}
{@param? name: string|null}
{@param? org: string|null}
{@param? street: list<string>|null}
{@param? city: string|null}
{@param? state: string|null}
{@param? zip: string|null}
{@param? cc: string|null}
{@param? phone: string|null}
{@param? fax: string|null}
{@param? email: string|null}
{@param password: string}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

View File

@@ -12,19 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.create_anchor_tenant}
/**
* Create anchor tenant domain
*/
{template .createanchortenant stricthtml="false"}
{template createanchortenant stricthtml="false"}
{@param domainName: string}
{@param contactId: string}
{@param password: string}
{@param period: int}
{@param? reason: string}
{@param? feeCurrency: string}
{@param? fee: string}
{@param? reason: string|null}
{@param? feeCurrency: string|null}
{@param? fee: string|null}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>

View File

@@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.domain_check}
/**
* Domain check request
*/
{template .domaincheck stricthtml="false"}
{template domaincheck stricthtml="false"}
{@param domainNames: list<string>}
{@param? allocationToken: string|null}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
@@ -40,7 +40,7 @@
</fee:domain>
{/for}
</fee:check>
{if isNonnull($allocationToken)}
{if $allocationToken}
<allocationToken:allocationToken
xmlns:allocationToken="urn:ietf:params:xml:ns:allocationToken-1.0">
{$allocationToken}

View File

@@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.domain_check_claims}
/**
* Domain check claims request
*/
{template .domaincheckclaims stricthtml="false"}
{template domaincheckclaims stricthtml="false"}
{@param domainNames: list<string>}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">

View File

@@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.domain_create}
/**
* Create domain
*/
{template .domaincreate stricthtml="false"}
{template domaincreate stricthtml="false"}
{@param domain: string}
{@param period: int}
{@param nameservers: list<string>}
@@ -24,12 +24,12 @@
{@param admins: list<string>}
{@param techs: list<string>}
{@param password: string}
{@param? currency: string}
{@param? price: string}
{@param? currency: string|null}
{@param? price: string|null}
{@param dsRecords: list<[keyTag:int, alg:int, digestType:int, digest:string]>}
{@param? reason: string}
{@param? requestedByRegistrar: string}
{@param? allocationToken: string}
{@param? reason: string|null}
{@param? requestedByRegistrar: string|null}
{@param? allocationToken: string|null}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
@@ -57,7 +57,7 @@
</domain:authInfo>
</domain:create>
</create>
{if length($dsRecords) > 0 or $price != null or $reason or $requestedByRegistrar or $allocationToken}
{if length($dsRecords) > 0 || $price != null || $reason || $requestedByRegistrar || $allocationToken}
<extension>
{if $price != null}
<fee:create xmlns:fee="urn:ietf:params:xml:ns:fee-0.12">
@@ -77,7 +77,7 @@
{/for}
</secDNS:create>
{/if}
{if $reason or $requestedByRegistrar}
{if $reason || $requestedByRegistrar}
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
{if $reason}
<metadata:reason>{$reason}</metadata:reason>

View File

@@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.domain_delete}
/**
* Delete domain request
*/
{template .deletedomain stricthtml="false"}
{template deletedomain stricthtml="false"}
{@param domainName: string}
{@param immediately: bool}
{@param reason: string}

View File

@@ -12,17 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.domain_renew}
/**
* Renew domain request
*/
{template .renewdomain stricthtml="false"}
{template renewdomain stricthtml="false"}
{@param domainName: string}
{@param expirationDate: string}
{@param period: string}
{@param? reason: string}
{@param? requestedByRegistrar: string}
{@param? reason: string|null}
{@param? requestedByRegistrar: string|null}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
@@ -34,7 +34,7 @@
<domain:period unit="y">{$period}</domain:period>
</domain:renew>
</renew>
{if $reason or $requestedByRegistrar}
{if $reason || $requestedByRegistrar}
<extension>
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
{if $reason}

View File

@@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.domain_update}
/**
* Update domain
*/
{template .domainupdate stricthtml="false"}
{template domainupdate stricthtml="false"}
{@param domain: string}
{@param add: bool}
{@param addNameservers: list<string>}
@@ -29,15 +29,15 @@
{@param removeTechs: list<string>}
{@param removeStatuses: list<string>}
{@param change: bool}
{@param? registrant: string}
{@param? password: string}
{@param? registrant: string|null}
{@param? password: string|null}
{@param secdns: bool}
{@param addDsRecords: list<[keyTag:int, alg:int, digestType:int, digest:string]>}
{@param removeDsRecords: list<[keyTag:int, alg:int, digestType:int, digest:string]>}
{@param removeAllDsRecords: bool}
{@param? autorenews: string}
{@param? reason: string}
{@param? requestedByRegistrar: string}
{@param? autorenews: string|null}
{@param? reason: string|null}
{@param? requestedByRegistrar: string|null}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
@@ -99,7 +99,7 @@
{/if}
</domain:update>
</update>
{if $secdns or $autorenews or $reason or $requestedByRegistrar}
{if $secdns || $autorenews || $reason || $requestedByRegistrar}
<extension>
{if $secdns}
<secDNS:update xmlns:secDNS="urn:ietf:params:xml:ns:secDNS-1.1">
@@ -139,7 +139,7 @@
<superuser:autorenews>{$autorenews}</superuser:autorenews>
</superuser:domainUpdate>
{/if}
{if $reason or $requestedByRegistrar}
{if $reason || $requestedByRegistrar}
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
{if $reason}
<metadata:reason>{$reason}</metadata:reason>

View File

@@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.host_create}
/**
* Create host
*/
{template .hostcreate stricthtml="false"}
{template hostcreate stricthtml="false"}
{@param hostname: string}
{@param? ipv4addresses: list<string>}
{@param? ipv6addresses: list<string>}
{@param? ipv4addresses: list<string>|null}
{@param? ipv6addresses: list<string>|null}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">

View File

@@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.host_delete}
/**
* Delete host request
*/
{template .deletehost stricthtml="false"}
{template deletehost stricthtml="false"}
{@param hostName: string}
{@param reason: string}
{@param requestedByRegistrar: any}

View File

@@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.remove_ip_address}
/**
* Request to remove IP addresses.
*/
{template .remove_ip_address stricthtml="false"}
{template remove_ip_address stricthtml="false"}
{@param name: string}
{@param ipAddresses: list<legacy_object_map<string, string>>}
{@param requestedByRegistrar: string}

View File

@@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
{namespace domain.registry.tools}
{namespace domain.registry.tools.uniform_rapid_suspension}
/**
* Uniform Rapid Suspension
*/
{template .uniformrapidsuspension stricthtml="false"}
{template uniformrapidsuspension stricthtml="false"}
{@param domainName: string}
{@param hostsToAdd: list<string>}
{@param hostsToRemove: list<string>}

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