From 401653ad4ad97a513df6c5b2617dbb39d13c8557 Mon Sep 17 00:00:00 2001 From: gbrodman Date: Tue, 10 Sep 2019 18:17:36 -0400 Subject: [PATCH] Add RegistryLock SQL schema (#243) * Add RegistryLock SQL schema * Refactor a bit * Move registrylock -> domain * Clearing up lock workflow * Add more docs and remove LockStatus * Responses to CR * Add repoId javadoc * Add registry lock to persistence xml file * Quote rather than backtick * Remove unnecessary check * File TODO * Remove uniqueness constraint on verification code * Remove import * add index * Add to SQL generation task * Move fields around to be the same order as Hibernate's generated sql --- .../registry/schema/domain/RegistryLock.java | 237 ++++++++++++++++++ .../tools/GenerateSqlSchemaCommand.java | 2 + .../main/resources/META-INF/persistence.xml | 1 + .../resources/sql/schema/registry_lock.sql | 30 +++ 4 files changed, 270 insertions(+) create mode 100644 core/src/main/java/google/registry/schema/domain/RegistryLock.java create mode 100644 db/src/main/resources/sql/schema/registry_lock.sql diff --git a/core/src/main/java/google/registry/schema/domain/RegistryLock.java b/core/src/main/java/google/registry/schema/domain/RegistryLock.java new file mode 100644 index 0000000000..a3f1ca6606 --- /dev/null +++ b/core/src/main/java/google/registry/schema/domain/RegistryLock.java @@ -0,0 +1,237 @@ +// 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.schema.domain; + +import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.util.DateTimeUtils.toJodaDateTime; +import static google.registry.util.DateTimeUtils.toZonedDateTime; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; + +import google.registry.model.Buildable; +import google.registry.model.ImmutableObject; +import java.time.ZonedDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.Table; +import org.joda.time.DateTime; + +/** + * Represents a registry lock/unlock object, meaning that the domain is locked on the registry + * level. + * + *

Registry locks must be requested through the registrar console by a lock-enabled contact, then + * confirmed through email within a certain length of time. Until that confirmation is processed, + * the completion time will remain null and the lock will have no effect. The same applies for + * unlock actions. + * + *

Note that there will be at most one row per domain with a null copmleted time -- this means + * that there is at most one pending action per domain. This is enforced at the logic level. + * + *

Note as well that in the case of a retry of a write after an unexpected success, the unique + * constraint on {@link #verificationCode} means that the second write will fail. + */ +@Entity +@Table( + // Unique constraint to get around Hibernate's failure to handle + // auto-increment field in composite primary key. + indexes = + @Index( + name = "idx_registry_lock_repo_id_revision_id", + columnList = "repo_id, revision_id", + unique = true)) +public final class RegistryLock extends ImmutableObject implements Buildable { + + /** Describes the action taken by the user. */ + public enum Action { + LOCK, + UNLOCK + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "revision_id", nullable = false) + private Long revisionId; + + /** EPP repo ID of the domain in question. */ + @Column(name = "repo_id", nullable = false) + private String repoId; + + // TODO (b/140568328): remove this when everything is in Cloud SQL and we can join on "domain" + @Column(name = "domain_name", nullable = false) + private String domainName; + + /** + * The ID of the registrar that performed the action -- this may be the admin ID if this action + * was performed by a superuser. + */ + @Column(name = "registrar_id", nullable = false) + private String registrarId; + + /** The POC that performed the action, or null if it was a superuser. */ + @Column(name = "registrar_poc_id") + private String registrarPocId; + + /** + * Lock action is immutable and describes whether the action performed was a lock or an unlock. + */ + @Enumerated(EnumType.STRING) + @Column(name = "action", nullable = false) + private Action action; + + /** Creation timestamp is when the lock/unlock is first requested. */ + @Column(name = "creation_timestamp", nullable = false) + private ZonedDateTime creationTimestamp; + + /** + * Completion timestamp is when the user has verified the lock/unlock, when this object de facto + * becomes immutable. If this field is null, it means that the lock has not been verified yet (and + * thus not been put into effect). + */ + @Column(name = "completion_timestamp") + private ZonedDateTime completionTimestamp; + + /** + * The user must provide the random verification code in order to complete the lock and move the + * status from PENDING to COMPLETED. + */ + @Column(name = "verification_code", nullable = false) + private String verificationCode; + + /** + * True iff this action was taken by a superuser, in response to something like a URS request. In + * this case, the action was performed by a registry admin rather than a registrar. + */ + @Column(name = "is_superuser", nullable = false) + private boolean isSuperuser; + + public String getRepoId() { + return repoId; + } + + public String getDomainName() { + return domainName; + } + + public String getRegistrarId() { + return registrarId; + } + + public String getRegistrarPocId() { + return registrarPocId; + } + + public Action getAction() { + return action; + } + + public DateTime getCreationTimestamp() { + return toJodaDateTime(creationTimestamp); + } + + public DateTime getCompletionTimestamp() { + return toJodaDateTime(completionTimestamp); + } + + public String getVerificationCode() { + return verificationCode; + } + + public boolean isSuperuser() { + return isSuperuser; + } + + public Long getRevisionId() { + return revisionId; + } + + @Override + public Builder asBuilder() { + return new Builder(clone(this)); + } + + /** Builder for {@link google.registry.schema.domain.RegistryLock}. */ + public static class Builder extends Buildable.Builder { + public Builder() {} + + private Builder(RegistryLock instance) { + super(instance); + } + + @Override + public RegistryLock build() { + checkArgumentNotNull(getInstance().repoId, "Repo ID cannot be null"); + checkArgumentNotNull(getInstance().domainName, "Domain name cannot be null"); + checkArgumentNotNull(getInstance().registrarId, "Registrar ID cannot be null"); + checkArgumentNotNull(getInstance().action, "Action cannot be null"); + checkArgumentNotNull(getInstance().creationTimestamp, "Creation timestamp cannot be null"); + checkArgumentNotNull(getInstance().verificationCode, "Verification codecannot be null"); + checkArgument( + getInstance().registrarPocId != null || getInstance().isSuperuser, + "Registrar POC ID must be provided if superuser is false"); + return super.build(); + } + + public Builder setRepoId(String repoId) { + getInstance().repoId = repoId; + return this; + } + + public Builder setDomainName(String domainName) { + getInstance().domainName = domainName; + return this; + } + + public Builder setRegistrarId(String registrarId) { + getInstance().registrarId = registrarId; + return this; + } + + public Builder setRegistrarPocId(String registrarPocId) { + getInstance().registrarPocId = registrarPocId; + return this; + } + + public Builder setAction(Action action) { + getInstance().action = action; + return this; + } + + public Builder setCreationTimestamp(DateTime creationTimestamp) { + getInstance().creationTimestamp = toZonedDateTime(creationTimestamp); + return this; + } + + public Builder setCompletionTimestamp(DateTime lockTimestamp) { + getInstance().completionTimestamp = toZonedDateTime(lockTimestamp); + return this; + } + + public Builder setVerificationCode(String verificationCode) { + getInstance().verificationCode = verificationCode; + return this; + } + + public Builder isSuperuser(boolean isSuperuser) { + getInstance().isSuperuser = isSuperuser; + return this; + } + } +} diff --git a/core/src/main/java/google/registry/tools/GenerateSqlSchemaCommand.java b/core/src/main/java/google/registry/tools/GenerateSqlSchemaCommand.java index b861cbe981..fe817840e3 100644 --- a/core/src/main/java/google/registry/tools/GenerateSqlSchemaCommand.java +++ b/core/src/main/java/google/registry/tools/GenerateSqlSchemaCommand.java @@ -28,6 +28,7 @@ import google.registry.model.transfer.BaseTransferObject; import google.registry.model.transfer.TransferData; import google.registry.persistence.NomulusNamingStrategy; +import google.registry.schema.domain.RegistryLock; import google.registry.schema.tld.PremiumList; import google.registry.schema.tmch.ClaimsList; import java.io.IOException; @@ -68,6 +69,7 @@ public class GenerateSqlSchemaCommand implements Command { GracePeriod.class, Period.class, PremiumList.class, + RegistryLock.class, TransferData.class, Trid.class); diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index fb8a86acb6..a2e256f324 100644 --- a/core/src/main/resources/META-INF/persistence.xml +++ b/core/src/main/resources/META-INF/persistence.xml @@ -20,6 +20,7 @@ * Use Hibernate's ServiceRegistry for bootstrapping (not JPA-compliant) --> google.registry.model.domain.DomainBase + google.registry.schema.domain.RegistryLock google.registry.schema.tmch.ClaimsList google.registry.model.transfer.BaseTransferObject google.registry.schema.tld.PremiumList diff --git a/db/src/main/resources/sql/schema/registry_lock.sql b/db/src/main/resources/sql/schema/registry_lock.sql new file mode 100644 index 0000000000..fa4457cc90 --- /dev/null +++ b/db/src/main/resources/sql/schema/registry_lock.sql @@ -0,0 +1,30 @@ +-- 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. + +CREATE TABLE "RegistryLock" ( + revision_id BIGSERIAL NOT NULL, + action TEXT NOT NULL, + completion_timestamp TIMESTAMPTZ, + creation_timestamp TIMESTAMPTZ NOT NULL, + domain_name TEXT NOT NULL, + is_superuser BOOLEAN NOT NULL, + registrar_id TEXT NOT NULL, + registrar_poc_id TEXT, + repo_id TEXT NOT NULL, + verification_code TEXT NOT NULL, + PRIMARY KEY (revision_id) +); + +ALTER TABLE IF EXISTS "RegistryLock" + ADD CONSTRAINT idx_registry_lock_repo_id_revision_id UNIQUE (repo_id, revision_id);