Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package google.registry.model.console;

import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;

import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.persistence.WithVKey;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import java.util.Optional;
import java.util.UUID;
import org.joda.time.DateTime;

/**
* Represents a password reset request of some type.
*
* <p>Password reset requests must be performed within an hour of the time that they were requested,
* as well as requiring that the requester and the fulfiller have the proper respective permissions.
*/
@Entity
@WithVKey(String.class)
public class PasswordResetRequest extends ImmutableObject implements Buildable {

Check failure

Code scanning / CodeQL

No clone method Error

No clone method, yet implements Cloneable.

public enum Type {
EPP,
REGISTRY_LOCK
}

@Id private String verificationCode;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
Type type;

@AttributeOverrides({
@AttributeOverride(
name = "creationTime",
column = @Column(name = "requestTime", nullable = false))
})
CreateAutoTimestamp requestTime = CreateAutoTimestamp.create(null);

@Column(nullable = false)
String requester;

@Column DateTime fulfillmentTime;

@Column(nullable = false)
String destinationEmail;

@Column(nullable = false)
String registrarId;

public String getVerificationCode() {
return verificationCode;
}

public Type getType() {
return type;
}

public DateTime getRequestTime() {
return requestTime.getTimestamp();
}

public String getRequester() {
return requester;
}

public Optional<DateTime> getFulfillmentTime() {
return Optional.ofNullable(fulfillmentTime);
}

public String getDestinationEmail() {
return destinationEmail;
}

public String getRegistrarId() {
return registrarId;
}

@Override
public Builder asBuilder() {
return new Builder(clone(this));
}

/** Builder for constructing immutable {@link PasswordResetRequest} objects. */
public static class Builder extends Buildable.Builder<PasswordResetRequest> {

public Builder() {}

private Builder(PasswordResetRequest instance) {
super(instance);
}

@Override
public PasswordResetRequest build() {
checkArgumentNotNull(getInstance().type, "Type must be specified");
checkArgumentNotNull(getInstance().requester, "Requester must be specified");
checkArgumentNotNull(getInstance().destinationEmail, "Destination email must be specified");
checkArgumentNotNull(getInstance().registrarId, "Registrar ID must be specified");
getInstance().verificationCode = UUID.randomUUID().toString();
return super.build();
}

public Builder setType(Type type) {
getInstance().type = type;
return this;
}

public Builder setRequester(String requester) {
getInstance().requester = requester;
return this;
}

public Builder setDestinationEmail(String destinationEmail) {
getInstance().destinationEmail = destinationEmail;
return this;
}

public Builder setRegistrarId(String registrarId) {
getInstance().registrarId = registrarId;
return this;
}

public Builder setFulfillmentTime(DateTime fulfillmentTime) {
getInstance().fulfillmentTime = fulfillmentTime;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -600,13 +600,8 @@ public Optional<RegistrarPoc> getWhoisAbuseContact() {
return getContacts().stream().filter(RegistrarPoc::getVisibleInDomainWhoisAsAbuse).findFirst();
}

private ImmutableSet<RegistrarPoc> getContactPocs() {
return tm().transact(
() ->
tm().query("FROM RegistrarPoc WHERE registrarId = :registrarId", RegistrarPoc.class)
.setParameter("registrarId", registrarId)
.getResultStream()
.collect(toImmutableSet()));
private ImmutableList<RegistrarPoc> getContactPocs() {
return tm().transact(() -> RegistrarPoc.loadForRegistrar(registrarId));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import static java.util.stream.Collectors.joining;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.gson.annotations.Expose;
Expand All @@ -36,6 +37,7 @@
import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.QueryComposer;
import google.registry.util.PasswordUtils;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand Down Expand Up @@ -432,6 +434,12 @@ public B setRegistryLockPassword(String registryLockPassword) {
}
}

public static ImmutableList<RegistrarPoc> loadForRegistrar(String registrarId) {
return tm().createQueryComposer(RegistrarPoc.class)
.where("registrarId", QueryComposer.Comparator.EQ, registrarId)
.list();
}

/** Class to represent the composite primary key for {@link RegistrarPoc} entity. */
@VisibleForTesting
public static class RegistrarPocId extends ImmutableObject implements Serializable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
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;
Expand All @@ -35,7 +34,6 @@
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPoc.Type;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Action.GkeService;
Expand Down Expand Up @@ -77,14 +75,7 @@ public ContactAction(
protected void getHandler(User user) {
checkPermission(user, registrarId, ConsolePermission.VIEW_REGISTRAR_DETAILS);
ImmutableList<RegistrarPoc> contacts =
tm().transact(
() ->
tm()
.createQueryComposer(RegistrarPoc.class)
.where("registrarId", Comparator.EQ, registrarId)
.stream()
.collect(toImmutableList()));

tm().transact(() -> RegistrarPoc.loadForRegistrar(registrarId));
consoleApiParams.response().setStatus(SC_OK);
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(contacts));
}
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/resources/META-INF/persistence.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@
<class>google.registry.model.billing.BillingRecurrence</class>
<class>google.registry.model.common.Cursor</class>
<class>google.registry.model.common.DnsRefreshRequest</class>
<class>google.registry.model.common.FeatureFlag</class>
<class>google.registry.model.console.ConsoleUpdateHistory</class>
<class>google.registry.model.console.PasswordResetRequest</class>
<class>google.registry.model.console.User</class>
<class>google.registry.model.contact.ContactHistory</class>
<class>google.registry.model.contact.Contact</class>
<class>google.registry.model.domain.Domain</class>
<class>google.registry.model.domain.DomainHistory</class>
<class>google.registry.model.common.FeatureFlag</class>
<class>google.registry.model.domain.GracePeriod</class>
<class>google.registry.model.domain.GracePeriod$GracePeriodHistory</class>
<class>google.registry.model.domain.secdns.DomainDsData</class>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package google.registry.model.console;

import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.junit.Assert.assertThrows;

import google.registry.model.EntityTestCase;
import google.registry.persistence.VKey;
import google.registry.testing.DatabaseHelper;
import org.junit.jupiter.api.Test;

/** Tests for {@link PasswordResetRequest}. */
public class PasswordResetRequestTest extends EntityTestCase {

PasswordResetRequestTest() {
super(JpaEntityCoverageCheck.ENABLED);
}

@Test
void testSuccess_persistence() {
PasswordResetRequest request =
new PasswordResetRequest.Builder()
.setRequester("requestor@email.tld")
.setDestinationEmail("destination@email.tld")
.setType(PasswordResetRequest.Type.EPP)
.setRegistrarId("TheRegistrar")
.build();
String verificationCode = request.getVerificationCode();
assertThat(verificationCode).isNotEmpty();
persistResource(request);
PasswordResetRequest fromDatabase =
DatabaseHelper.loadByKey(VKey.create(PasswordResetRequest.class, verificationCode));
assertAboutImmutableObjects().that(fromDatabase).isEqualExceptFields(request, "requestTime");
assertThat(fromDatabase.getRequestTime()).isEqualTo(fakeClock.nowUtc());
}

@Test
void testFailure_nullFields() {
PasswordResetRequest.Builder builder = new PasswordResetRequest.Builder();
assertThrows(IllegalArgumentException.class, builder::build);
builder.setType(PasswordResetRequest.Type.EPP);
assertThrows(IllegalArgumentException.class, builder::build);
builder.setRequester("foobar@email.tld");
assertThrows(IllegalArgumentException.class, builder::build);
builder.setDestinationEmail("email@email.tld");
assertThrows(IllegalArgumentException.class, builder::build);
builder.setRegistrarId("TheRegistrar");
builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import google.registry.model.common.DnsRefreshRequestTest;
import google.registry.model.common.FeatureFlagTest;
import google.registry.model.console.ConsoleUpdateHistoryTest;
import google.registry.model.console.PasswordResetRequestTest;
import google.registry.model.console.UserTest;
import google.registry.model.contact.ContactTest;
import google.registry.model.domain.DomainSqlTest;
Expand Down Expand Up @@ -104,6 +105,7 @@
FeatureFlagTest.class,
HostHistoryTest.class,
LockTest.class,
PasswordResetRequestTest.class,
PollMessageTest.class,
PremiumListDaoTest.class,
RdeRevisionTest.class,
Expand Down
6 changes: 3 additions & 3 deletions db/src/main/resources/sql/er_diagram/brief_er_diagram.html
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ <h2>System Information</h2>
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2025-06-02 14:41:34</td>
<td class="property_value">2025-06-04 18:53:06</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
Expand All @@ -280,7 +280,7 @@ <h2>System Information</h2>
<text text-anchor="start" x="4655" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text>
<text text-anchor="start" x="4738" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.25.2</text>
<text text-anchor="start" x="4654" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text>
<text text-anchor="start" x="4738" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-06-02 14:41:34</text>
<text text-anchor="start" x="4738" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-06-04 18:53:06</text>
<polygon fill="none" stroke="#888888" points="4651,-4 4651,-44 4887,-44 4887,-4 4651,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
<title>
Expand Down Expand Up @@ -2702,7 +2702,7 @@ <h2>Tables</h2>
<tr>
<td class="spacer"></td>
<td class="minwidth"></td>
<td class="minwidth">default '2021-06-01 00:00:00+00'::timestamp with time zone</td>
<td class="minwidth">default '2021-05-31 20:00:00-04'::timestamp with time zone</td>
</tr>
<tr>
<td colspan="3"></td>
Expand Down
6 changes: 3 additions & 3 deletions db/src/main/resources/sql/er_diagram/full_er_diagram.html
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ <h2>System Information</h2>
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2025-06-02 14:41:30</td>
<td class="property_value">2025-06-04 18:53:03</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
Expand All @@ -280,7 +280,7 @@ <h2>System Information</h2>
<text text-anchor="start" x="5435" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text>
<text text-anchor="start" x="5518" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.25.2</text>
<text text-anchor="start" x="5434" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text>
<text text-anchor="start" x="5518" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-06-02 14:41:30</text>
<text text-anchor="start" x="5518" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-06-04 18:53:03</text>
<polygon fill="none" stroke="#888888" points="5431,-4 5431,-44 5667,-44 5667,-4 5431,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
<title>
Expand Down Expand Up @@ -4806,7 +4806,7 @@ <h2>Tables</h2>
<tr>
<td class="spacer"></td>
<td class="minwidth"></td>
<td class="minwidth">default '2021-06-01 00:00:00+00'::timestamp with time zone</td>
<td class="minwidth">default '2021-05-31 20:00:00-04'::timestamp with time zone</td>
</tr>
<tr>
<td colspan="3"></td>
Expand Down
11 changes: 11 additions & 0 deletions db/src/main/resources/sql/schema/db-schema.sql.generated
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,17 @@
primary key (package_promotion_id)
);

create table "PasswordResetRequest" (
verification_code text not null,
destination_email text not null,
fulfillment_time timestamp(6) with time zone,
registrar_id text not null,
request_time timestamp(6) with time zone not null,
requester text not null,
type text not null check (type in ('EPP','REGISTRY_LOCK')),
primary key (verification_code)
);

create table "PollMessage" (
type text not null,
poll_message_id bigint not null,
Expand Down