From f6aefd2664e4d17a47493f5fb4454d4d833da023 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Fri, 2 May 2025 12:31:00 -0400 Subject: [PATCH 01/49] add scaffolding for PowerDNS writer --- .../writer/powerdns/PowerDnsConfigModule.java | 24 + .../dns/writer/powerdns/PowerDnsWriter.java | 65 + .../writer/powerdns/PowerDnsWriterModule.java | 27 + .../powerdns/client/PowerDNSClient.java | 183 +++ .../writer/powerdns/client/model/Comment.java | 38 + .../writer/powerdns/client/model/RRSet.java | 77 + .../powerdns/client/model/RecordObject.java | 27 + .../writer/powerdns/client/model/Server.java | 82 + .../writer/powerdns/client/model/Zone.java | 267 ++++ .../openapi/authoritative-api-swagger.yaml | 1398 +++++++++++++++++ .../registry/module/RequestComponent.java | 4 + .../backend/BackendRequestComponent.java | 4 + .../registry/tools/RegistryToolComponent.java | 4 + 13 files changed, 2200 insertions(+) create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriterModule.java create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/client/openapi/authoritative-api-swagger.yaml diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java new file mode 100644 index 00000000000..130e31f7844 --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java @@ -0,0 +1,24 @@ +package google.registry.dns.writer.powerdns; + +import dagger.Module; +import dagger.Provides; +import google.registry.config.RegistryConfig.Config; + +/** Dagger module that provides DNS configuration settings. */ +@Module +public class PowerDnsConfigModule { + + /** Host of the PowerDNS server. */ + @Provides + @Config("powerDnsHost") + public static String providePowerDnsHost() { + return "localhost"; + } + + /** API key for the PowerDNS server. */ + @Provides + @Config("powerDnsApiKey") + public static String providePowerDnsApiKey() { + return "dummy-api-key"; + } +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java new file mode 100644 index 00000000000..ba5dc725330 --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -0,0 +1,65 @@ +package google.registry.dns.writer.powerdns; + +import com.google.common.flogger.FluentLogger; +import google.registry.config.RegistryConfig.Config; +import google.registry.dns.writer.BaseDnsWriter; +import google.registry.dns.writer.DnsWriterZone; +import google.registry.dns.writer.powerdns.client.PowerDNSClient; +import google.registry.util.Clock; +import jakarta.inject.Inject; +import java.util.HashSet; +import java.util.Set; + +/** A DnsWriter that implements the PowerDNS API. */ +public class PowerDnsWriter extends BaseDnsWriter { + public static final String NAME = "PowerDnsWriter"; + + private final Clock clock; + private final PowerDNSClient powerDnsClient; + private final String zoneName; + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final Set names = new HashSet<>(); + + /** + * Class constructor. + * + * @param zoneName the name of the zone to write to + * @param powerDnsHost the host of the PowerDNS server + * @param powerDnsApiKey the API key for the PowerDNS server + * @param clock the clock to use for the PowerDNS writer + */ + @Inject + public PowerDnsWriter( + @DnsWriterZone String zoneName, + @Config("powerDnsHost") String powerDnsHost, + @Config("powerDnsApiKey") String powerDnsApiKey, + Clock clock) { + + // Initialize the PowerDNS client and zone name + this.powerDnsClient = new PowerDNSClient(powerDnsHost, powerDnsApiKey); + this.zoneName = zoneName; + this.clock = clock; + } + + @Override + public void publishDomain(String domainName) { + // TODO: Implement the logic to stage the domain zone files to PowerDNS for commit + names.add(domainName); + } + + @Override + public void publishHost(String hostName) { + // TODO: Implement the logic to stage the host glue records to PowerDNS for commit + names.add(hostName); + } + + @Override + protected void commitUnchecked() { + // TODO: Call the PowerDNS API to commit the changes + logger.atWarning().log( + "PowerDnsWriter for server ID %s not yet implemented; ignoring %s names to commit: %s at" + + " %s", + powerDnsClient.getServerId(), zoneName, names, clock.nowUtc()); + } +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriterModule.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriterModule.java new file mode 100644 index 00000000000..2c2de487828 --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriterModule.java @@ -0,0 +1,27 @@ +package google.registry.dns.writer.powerdns; + +import dagger.Binds; +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.IntoMap; +import dagger.multibindings.IntoSet; +import dagger.multibindings.StringKey; +import google.registry.dns.writer.DnsWriter; +import jakarta.inject.Named; + +/** Dagger module that provides a PowerDnsWriter. */ +@Module +public abstract class PowerDnsWriterModule { + + @Binds + @IntoMap + @StringKey(PowerDnsWriter.NAME) + abstract DnsWriter provideWriter(PowerDnsWriter writer); + + @Provides + @IntoSet + @Named("dnsWriterNames") + static String provideWriterName() { + return PowerDnsWriter.NAME; + } +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java new file mode 100644 index 00000000000..adad1e40bf8 --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java @@ -0,0 +1,183 @@ +package google.registry.dns.writer.powerdns.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import google.registry.dns.writer.powerdns.client.model.Server; +import google.registry.dns.writer.powerdns.client.model.Zone; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class PowerDNSClient { + // static fields + private final OkHttpClient httpClient; + private final ObjectMapper objectMapper; + private final String baseUrl; + private final String apiKey; + + // dynamic fields + private String serverId; + + public PowerDNSClient(String baseUrl, String apiKey) { + // initialize the base URL, API key, and HTTP client + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.httpClient = new OkHttpClient(); + this.objectMapper = new ObjectMapper(); + + // initialize the Server ID by querying the server list and choosing + // the first entry + try { + List servers = listServers(); + if (servers.isEmpty()) { + throw new IOException("No servers found"); + } + this.serverId = servers.get(0).getId(); + } catch (IOException e) { + this.serverId = "unknown-server-id"; + } + } + + public List listServers() throws IOException { + Request request = + new Request.Builder().url(baseUrl + "/servers").header("X-API-Key", apiKey).get().build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Failed to list servers: " + response); + } + return objectMapper.readValue( + Objects.requireNonNull(response.body()).string(), + objectMapper.getTypeFactory().constructCollectionType(List.class, Server.class)); + } + } + + public Server getServer() throws IOException { + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId) + .header("X-API-Key", apiKey) + .get() + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Failed to get server: " + response); + } + return objectMapper.readValue(Objects.requireNonNull(response.body()).string(), Server.class); + } + } + + public String getServerId() { + return serverId; + } + + public void setServerId(String serverId) { + this.serverId = serverId; + } + + public List listZones() throws IOException { + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones") + .header("X-API-Key", apiKey) + .get() + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Failed to list zones: " + response); + } + return objectMapper.readValue( + Objects.requireNonNull(response.body()).string(), + objectMapper.getTypeFactory().constructCollectionType(List.class, Zone.class)); + } + } + + public Zone getZone(String zoneId) throws IOException { + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones/" + zoneId) + .header("X-API-Key", apiKey) + .get() + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Failed to get zone: " + response); + } + return objectMapper.readValue(Objects.requireNonNull(response.body()).string(), Zone.class); + } + } + + public Zone createZone(Zone zone) throws IOException { + String json = objectMapper.writeValueAsString(zone); + RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); + + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones") + .header("X-API-Key", apiKey) + .post(body) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Failed to create zone: " + response); + } + return objectMapper.readValue(Objects.requireNonNull(response.body()).string(), Zone.class); + } + } + + public void deleteZone(String zoneId) throws IOException { + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones/" + zoneId) + .header("X-API-Key", apiKey) + .delete() + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Failed to delete zone: " + response); + } + } + } + + public void patchZone(String zoneId, Zone zone) throws IOException { + String json = objectMapper.writeValueAsString(zone); + RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); + + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones/" + zoneId) + .header("X-API-Key", apiKey) + .patch(body) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Failed to patch zone: " + response); + } + } + } + + public void notifyZone(String zoneId) throws IOException { + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones/" + zoneId + "/notify") + .header("X-API-Key", apiKey) + .put(RequestBody.create("", MediaType.parse("application/json"))) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Failed to notify zone: " + response); + } + } + } +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java new file mode 100644 index 00000000000..32d9cb87db3 --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java @@ -0,0 +1,38 @@ +package google.registry.dns.writer.powerdns.client.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Comment { + @JsonProperty("content") + private String content; + + @JsonProperty("account") + private String account; + + @JsonProperty("modified_at") + private Long modifiedAt; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getAccount() { + return account; + } + + public void setAccount(String account) { + this.account = account; + } + + public Long getModifiedAt() { + return modifiedAt; + } + + public void setModifiedAt(Long modifiedAt) { + this.modifiedAt = modifiedAt; + } +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java new file mode 100644 index 00000000000..42ea5ad735a --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java @@ -0,0 +1,77 @@ +package google.registry.dns.writer.powerdns.client.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public class RRSet { + @JsonProperty("name") + private String name; + + @JsonProperty("type") + private String type; + + @JsonProperty("ttl") + private Integer ttl; + + @JsonProperty("changetype") + private ChangeType changetype; + + @JsonProperty("records") + private List records; + + @JsonProperty("comments") + private List comments; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Integer getTtl() { + return ttl; + } + + public void setTtl(Integer ttl) { + this.ttl = ttl; + } + + public ChangeType getChangetype() { + return changetype; + } + + public void setChangetype(ChangeType changetype) { + this.changetype = changetype; + } + + public List getRecords() { + return records; + } + + public void setRecords(List records) { + this.records = records; + } + + public List getComments() { + return comments; + } + + public void setComments(List comments) { + this.comments = comments; + } + + public enum ChangeType { + REPLACE, + DELETE + } +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java new file mode 100644 index 00000000000..ffe3c2e40cf --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java @@ -0,0 +1,27 @@ +package google.registry.dns.writer.powerdns.client.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class RecordObject { + @JsonProperty("content") + private String content; + + @JsonProperty("disabled") + private Boolean disabled; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Boolean getDisabled() { + return disabled; + } + + public void setDisabled(Boolean disabled) { + this.disabled = disabled; + } +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java new file mode 100644 index 00000000000..135f488a4ae --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java @@ -0,0 +1,82 @@ +package google.registry.dns.writer.powerdns.client.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Server { + @JsonProperty("type") + private String type; + + @JsonProperty("id") + private String id; + + @JsonProperty("daemon_type") + private String daemonType; + + @JsonProperty("version") + private String version; + + @JsonProperty("url") + private String url; + + @JsonProperty("config_url") + private String configUrl; + + @JsonProperty("zones_url") + private String zonesUrl; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDaemonType() { + return daemonType; + } + + public void setDaemonType(String daemonType) { + this.daemonType = daemonType; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getConfigUrl() { + return configUrl; + } + + public void setConfigUrl(String configUrl) { + this.configUrl = configUrl; + } + + public String getZonesUrl() { + return zonesUrl; + } + + public void setZonesUrl(String zonesUrl) { + this.zonesUrl = zonesUrl; + } +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java new file mode 100644 index 00000000000..08a96c18fe4 --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java @@ -0,0 +1,267 @@ +package google.registry.dns.writer.powerdns.client.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public class Zone { + @JsonProperty("id") + private String id; + + @JsonProperty("name") + private String name; + + @JsonProperty("type") + private String type; + + @JsonProperty("url") + private String url; + + @JsonProperty("kind") + private ZoneKind kind; + + @JsonProperty("rrsets") + private List rrsets; + + @JsonProperty("serial") + private Integer serial; + + @JsonProperty("notified_serial") + private Integer notifiedSerial; + + @JsonProperty("edited_serial") + private Integer editedSerial; + + @JsonProperty("masters") + private List masters; + + @JsonProperty("dnssec") + private Boolean dnssec; + + @JsonProperty("nsec3param") + private String nsec3param; + + @JsonProperty("nsec3narrow") + private Boolean nsec3narrow; + + @JsonProperty("presigned") + private Boolean presigned; + + @JsonProperty("soa_edit") + private String soaEdit; + + @JsonProperty("soa_edit_api") + private String soaEditApi; + + @JsonProperty("api_rectify") + private Boolean apiRectify; + + @JsonProperty("zone") + private String zone; + + @JsonProperty("catalog") + private String catalog; + + @JsonProperty("account") + private String account; + + @JsonProperty("nameservers") + private List nameservers; + + @JsonProperty("master_tsig_key_ids") + private List masterTsigKeyIds; + + @JsonProperty("slave_tsig_key_ids") + private List slaveTsigKeyIds; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public ZoneKind getKind() { + return kind; + } + + public void setKind(ZoneKind kind) { + this.kind = kind; + } + + public List getRrsets() { + return rrsets; + } + + public void setRrsets(List rrsets) { + this.rrsets = rrsets; + } + + public Integer getSerial() { + return serial; + } + + public void setSerial(Integer serial) { + this.serial = serial; + } + + public Integer getNotifiedSerial() { + return notifiedSerial; + } + + public void setNotifiedSerial(Integer notifiedSerial) { + this.notifiedSerial = notifiedSerial; + } + + public Integer getEditedSerial() { + return editedSerial; + } + + public void setEditedSerial(Integer editedSerial) { + this.editedSerial = editedSerial; + } + + public List getMasters() { + return masters; + } + + public void setMasters(List masters) { + this.masters = masters; + } + + public Boolean getDnssec() { + return dnssec; + } + + public void setDnssec(Boolean dnssec) { + this.dnssec = dnssec; + } + + public String getNsec3param() { + return nsec3param; + } + + public void setNsec3param(String nsec3param) { + this.nsec3param = nsec3param; + } + + public Boolean getNsec3narrow() { + return nsec3narrow; + } + + public void setNsec3narrow(Boolean nsec3narrow) { + this.nsec3narrow = nsec3narrow; + } + + public Boolean getPresigned() { + return presigned; + } + + public void setPresigned(Boolean presigned) { + this.presigned = presigned; + } + + public String getSoaEdit() { + return soaEdit; + } + + public void setSoaEdit(String soaEdit) { + this.soaEdit = soaEdit; + } + + public String getSoaEditApi() { + return soaEditApi; + } + + public void setSoaEditApi(String soaEditApi) { + this.soaEditApi = soaEditApi; + } + + public Boolean getApiRectify() { + return apiRectify; + } + + public void setApiRectify(Boolean apiRectify) { + this.apiRectify = apiRectify; + } + + public String getZone() { + return zone; + } + + public void setZone(String zone) { + this.zone = zone; + } + + public String getCatalog() { + return catalog; + } + + public void setCatalog(String catalog) { + this.catalog = catalog; + } + + public String getAccount() { + return account; + } + + public void setAccount(String account) { + this.account = account; + } + + public List getNameservers() { + return nameservers; + } + + public void setNameservers(List nameservers) { + this.nameservers = nameservers; + } + + public List getMasterTsigKeyIds() { + return masterTsigKeyIds; + } + + public void setMasterTsigKeyIds(List masterTsigKeyIds) { + this.masterTsigKeyIds = masterTsigKeyIds; + } + + public List getSlaveTsigKeyIds() { + return slaveTsigKeyIds; + } + + public void setSlaveTsigKeyIds(List slaveTsigKeyIds) { + this.slaveTsigKeyIds = slaveTsigKeyIds; + } + + public enum ZoneKind { + Native, + Master, + Slave, + Producer, + Consumer + } +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/openapi/authoritative-api-swagger.yaml b/core/src/main/java/google/registry/dns/writer/powerdns/client/openapi/authoritative-api-swagger.yaml new file mode 100644 index 00000000000..555fe70c2b7 --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/openapi/authoritative-api-swagger.yaml @@ -0,0 +1,1398 @@ +swagger: '2.0' +info: + version: "0.0.15" + title: PowerDNS Authoritative HTTP API + license: + name: MIT +basePath: /api/v1 +consumes: + - application/json +produces: + - application/json +securityDefinitions: + # X-API-Key: abcdef12345 + APIKeyHeader: + type: apiKey + in: header + name: X-API-Key +security: + - APIKeyHeader: [] + +# Overall TODOS: +# TODO: Return types are not consistent across documentation +# We need to look at the code and figure out the default HTTP response +# codes and adjust docs accordingly. +paths: + '/error': + get: + summary: Will always generate an error + operationId: error + responses: &commonErrors + '400': + description: The supplied request was not valid + schema: + $ref: '#/definitions/Error' + '404': + description: Requested item was not found + schema: + $ref: '#/definitions/Error' + '422': + description: The input to the operation was not valid + schema: + $ref: '#/definitions/Error' + '500': + description: Internal server error + schema: + $ref: '#/definitions/Error' + + '/servers': + get: + summary: List all servers + operationId: listServers + tags: + - servers + responses: + '200': + description: An array of servers + schema: + type: array + items: + $ref: '#/definitions/Server' + <<: *commonErrors + + '/servers/{server_id}': + get: + summary: List a server + operationId: listServer + tags: + - servers + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + responses: + '200': + description: An server + schema: + $ref: '#/definitions/Server' + <<: *commonErrors + + '/servers/{server_id}/cache/flush': + put: + summary: Flush a cache-entry by name + operationId: cacheFlushByName + tags: + - servers + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: domain + in: query + required: true + description: The domain name to flush from the cache + type: string + responses: + '200': + description: Flush successful + schema: + $ref: '#/definitions/CacheFlushResult' + <<: *commonErrors + + '/servers/{server_id}/zones': + get: + summary: List all Zones in a server + operationId: listZones + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone + in: query + required: false + type: string + description: | + When set to the name of a zone, only this zone is returned. + If no zone with that name exists, the response is an empty array. + This can e.g. be used to check if a zone exists in the database without having to guess/encode the zone's id or to check if a zone exists. + - name: dnssec + in: query + required: false + type: boolean + default: true + description: '“true” (default) or “false”, whether to include the “dnssec” and ”edited_serial” fields in the Zone objects. Setting this to ”false” will make the query a lot faster.' + responses: + '200': + description: An array of Zones + schema: + type: array + items: + $ref: '#/definitions/Zone' + <<: *commonErrors + post: + summary: Creates a new domain, returns the Zone on creation. + operationId: createZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: rrsets + in: query + description: '“true” (default) or “false”, whether to include the “rrsets” in the response Zone object.' + type: boolean + default: true + - name: zone_struct + description: The zone struct to patch with + required: true + in: body + schema: + $ref: '#/definitions/Zone' + responses: + '201': + description: A zone + schema: + $ref: '#/definitions/Zone' + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}': + get: + summary: zone managed by a server + operationId: listZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + - name: rrsets + in: query + description: '“true” (default) or “false”, whether to include the “rrsets” in the response Zone object.' + type: boolean + default: true + - name: rrset_name + in: query + description: Limit output to RRsets for this name. + type: string + - name: rrset_type + in: query + description: Limit output to the RRset of this type. Can only be used together with rrset_name. + type: string + - name: include_disabled + in: query + description: '“true” (default) or “false”, whether to include disabled RRsets in the response.' + type: boolean + responses: + '200': + description: A Zone + schema: + $ref: '#/definitions/Zone' + <<: *commonErrors + delete: + summary: Deletes this zone, all attached metadata and rrsets. + operationId: deleteZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '204': + description: 'Returns 204 No Content on success.' + <<: *commonErrors + patch: + summary: 'Creates/modifies/deletes RRsets present in the payload and their comments. Returns 204 No Content on success.' + operationId: patchZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: zone_struct + description: The zone struct to patch with + required: true + in: body + schema: + $ref: '#/definitions/Zone' + responses: + '204': + description: 'Returns 204 No Content on success.' + <<: *commonErrors + + put: + summary: Modifies basic zone data. + description: 'The only fields in the zone structure which can be modified are: kind, masters, catalog, account, soa_edit, soa_edit_api, api_rectify, dnssec, and nsec3param. All other fields are ignored.' + operationId: putZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: zone_struct + description: The zone struct to patch with + required: true + in: body + schema: + $ref: '#/definitions/Zone' + responses: + '204': + description: 'Returns 204 No Content on success.' + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/notify': + put: + summary: Send a DNS NOTIFY to all slaves. + description: 'Fails when zone kind is not Master or Slave, or master and slave are disabled in the configuration. Only works for Slave if renotify is on. Clients MUST NOT send a body.' + operationId: notifyZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: OK + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/axfr-retrieve': + put: + summary: Retrieve slave zone from its master. + description: 'Fails when zone kind is not Slave, or slave is disabled in the configuration. Clients MUST NOT send a body.' + operationId: axfrRetrieveZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: OK + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/export': + get: + summary: 'Returns the zone in AXFR format.' + operationId: axfrExportZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: OK + schema: + type: string + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/rectify': + put: + summary: 'Rectify the zone data.' + description: 'This does not take into account the API-RECTIFY metadata. Fails on slave zones and zones that do not have DNSSEC.' + operationId: rectifyZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: OK + schema: + type: string + <<: *commonErrors + + '/servers/{server_id}/config': + get: + summary: 'Returns all ConfigSettings for a single server' + operationId: getConfig + tags: + - config + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + responses: + '200': + description: List of config values + schema: + type: array + items: + $ref: '#/definitions/ConfigSetting' + <<: *commonErrors + + '/servers/{server_id}/config/{config_setting_name}': + get: + summary: 'Returns a specific ConfigSetting for a single server' + description: 'NOT IMPLEMENTED' + operationId: getConfigSetting + tags: + - config + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: config_setting_name + in: path + required: true + description: The name of the setting to retrieve + type: string + responses: + '200': + description: List of config values + schema: + $ref: '#/definitions/ConfigSetting' + <<: *commonErrors + + '/servers/{server_id}/statistics': + get: + summary: 'Query statistics.' + description: 'Query PowerDNS internal statistics.' + operationId: getStats + tags: + - stats + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: statistic + in: query + required: false + type: string + description: | + When set to the name of a specific statistic, only this value is returned. + If no statistic with that name exists, the response has a 422 status and an error message. + - name: includerings + in: query + required: false + type: boolean + default: true + description: '“true” (default) or “false”, whether to include the Ring items, which can contain thousands of log messages or queried domains. Setting this to ”false” may make the response a lot smaller.' + responses: + '200': + description: List of Statistic Items + schema: + type: array + items: + - $ref: '#/definitions/StatisticItem' + - $ref: '#/definitions/MapStatisticItem' + - $ref: '#/definitions/RingStatisticItem' + '422': + description: 'Returned when a non-existing statistic name has been requested. Contains an error message' + <<: *commonErrors + + '/servers/{server_id}/search-data': + get: + summary: 'Search the data inside PowerDNS' + description: 'Search the data inside PowerDNS for search_term and return at most max_results. This includes zones, records and comments. The * character can be used in search_term as a wildcard character and the ? character can be used as a wildcard for a single character.' + operationId: searchData + tags: + - search + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: q + in: query + required: true + description: 'The string to search for' + type: string + - name: max + in: query + required: true + description: 'Maximum number of entries to return' + type: integer + - name: object_type + in: query + required: false + description: 'Type of data to search for, one of “all”, “zone”, “record”, “comment”' + type: string + responses: + '200': + description: Returns a JSON array with results + schema: + $ref: '#/definitions/SearchResults' + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/metadata': + get: + summary: 'Get all the Metadata associated with the zone.' + operationId: listMetadata + tags: + - zonemetadata + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: List of Metadata objects + schema: + type: array + items: + $ref: '#/definitions/Metadata' + <<: *commonErrors + post: + summary: 'Creates a set of metadata entries' + description: 'Creates a set of metadata entries of given kind for the zone. Existing metadata entries for the zone with the same kind are not overwritten.' + operationId: createMetadata + tags: + - zonemetadata + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: metadata + description: Metadata object with list of values to create + required: true + in: body + schema: + $ref: '#/definitions/Metadata' + responses: + '204': + description: OK + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/metadata/{metadata_kind}': + get: + summary: 'Get the content of a single kind of domain metadata as a Metadata object.' + operationId: getMetadata + tags: + - zonemetadata + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + - name: metadata_kind + type: string + in: path + required: true + description: The kind of metadata + responses: + '200': + description: Metadata object with list of values + schema: + $ref: '#/definitions/Metadata' + <<: *commonErrors + put: + summary: 'Replace the content of a single kind of domain metadata.' + description: 'Creates a set of metadata entries of given kind for the zone. Existing metadata entries for the zone with the same kind are removed.' + operationId: modifyMetadata + tags: + - zonemetadata + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: metadata_kind + description: The kind of metadata + required: true + type: string + in: path + - name: metadata + description: metadata to add/create + required: true + in: body + schema: + $ref: '#/definitions/Metadata' + responses: + '200': + description: Metadata object with list of values + schema: + $ref: '#/definitions/Metadata' + <<: *commonErrors + delete: + summary: 'Delete all items of a single kind of domain metadata.' + operationId: deleteMetadata + tags: + - zonemetadata + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + - name: metadata_kind + type: string + in: path + required: true + description: The kind of metadata + responses: + '204': + description: OK + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/cryptokeys': + get: + summary: 'Get all CryptoKeys for a zone, except the privatekey' + operationId: listCryptokeys + tags: + - zonecryptokey + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: List of Cryptokey objects + schema: + type: array + items: + $ref: '#/definitions/Cryptokey' + <<: *commonErrors + post: + summary: 'Creates a Cryptokey' + description: 'This method adds a new key to a zone. The key can either be generated or imported by supplying the content parameter. if content, bits and algo are null, a key will be generated based on the default-ksk-algorithm and default-ksk-size settings for a KSK and the default-zsk-algorithm and default-zsk-size options for a ZSK.' + operationId: createCryptokey + tags: + - zonecryptokey + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: cryptokey + description: Add a Cryptokey + required: true + in: body + schema: + $ref: '#/definitions/Cryptokey' + responses: + '201': + description: Created + schema: + $ref: '#/definitions/Cryptokey' + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/cryptokeys/{cryptokey_id}': + get: + summary: 'Returns all data about the CryptoKey, including the privatekey.' + operationId: getCryptokey + tags: + - zonecryptokey + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + - name: cryptokey_id + type: string + in: path + required: true + description: 'The id value of the CryptoKey' + responses: + '200': + description: Cryptokey + schema: + $ref: '#/definitions/Cryptokey' + <<: *commonErrors + put: + summary: 'This method (de)activates a key from zone_name specified by cryptokey_id' + operationId: modifyCryptokey + tags: + - zonecryptokey + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: cryptokey_id + description: Cryptokey to manipulate + required: true + in: path + type: string + - name: cryptokey + description: the Cryptokey + required: true + in: body + schema: + $ref: '#/definitions/Cryptokey' + responses: + '204': + description: OK + <<: *commonErrors + delete: + summary: 'This method deletes a key specified by cryptokey_id.' + operationId: deleteCryptokey + tags: + - zonecryptokey + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + - name: cryptokey_id + type: string + in: path + required: true + description: 'The id value of the Cryptokey' + responses: + '204': + description: OK + <<: *commonErrors + + '/servers/{server_id}/tsigkeys': + parameters: + - name: server_id + in: path + required: true + description: 'The id of the server' + type: string + get: + summary: 'Get all TSIGKeys on the server, except the actual key' + operationId: listTSIGKeys + tags: + - tsigkey + responses: + '200': + description: List of TSIGKey objects + schema: + type: array + items: + $ref: '#/definitions/TSIGKey' + <<: *commonErrors + post: + summary: 'Add a TSIG key' + description: 'This methods add a new TSIGKey. The actual key can be generated by the server or be provided by the client' + operationId: createTSIGKey + tags: + - tsigkey + parameters: + - name: tsigkey + description: The TSIGKey to add + required: true + in: body + schema: + $ref: '#/definitions/TSIGKey' + responses: + '201': + description: Created + schema: + $ref: '#/definitions/TSIGKey' + '409': + description: An item with this name already exists + schema: + $ref: '#/definitions/Error' + <<: *commonErrors + + '/servers/{server_id}/tsigkeys/{tsigkey_id}': + parameters: + - name: server_id + in: path + required: true + description: 'The id of the server to retrieve the key from' + type: string + - name: tsigkey_id + in: path + required: true + description: 'The id of the TSIGkey. Should match the "id" field in the TSIGKey object' + type: string + get: + summary: 'Get a specific TSIGKeys on the server, including the actual key' + operationId: getTSIGKey + tags: + - tsigkey + responses: + '200': + description: OK. + schema: + $ref: '#/definitions/TSIGKey' + <<: *commonErrors + put: + description: | + The TSIGKey at tsigkey_id can be changed in multiple ways: + * Changing the Name, this will remove the key with tsigkey_id after adding. + * Changing the Algorithm + * Changing the Key + + Only the relevant fields have to be provided in the request body. + operationId: putTSIGKey + tags: + - tsigkey + parameters: + - name: tsigkey + description: A (possibly stripped down) TSIGKey object with the new values + schema: + $ref: '#/definitions/TSIGKey' + in: body + required: true + responses: + '200': + description: OK. TSIGKey is changed. + schema: + $ref: '#/definitions/TSIGKey' + '409': + description: An item with this name already exists + schema: + $ref: '#/definitions/Error' + <<: *commonErrors + delete: + summary: 'Delete the TSIGKey with tsigkey_id' + operationId: deleteTSIGKey + tags: + - tsigkey + responses: + '204': + description: 'OK, key was deleted' + <<: *commonErrors + + '/servers/{server_id}/autoprimaries': + parameters: + - name: server_id + in: path + required: true + description: 'The id of the server to manage the list of autoprimaries on' + type: string + get: + summary: 'Get a list of autoprimaries' + operationId: getAutoprimaries + tags: + - autoprimary + responses: + '200': + description: OK. + schema: + $ref: '#/definitions/Autoprimary' + <<: *commonErrors + post: + summary: 'Add an autoprimary' + description: 'This methods add a new autoprimary server.' + operationId: createAutoprimary + tags: + - autoprimary + parameters: + - name: autoprimary + description: autoprimary entry to add + required: true + in: body + schema: + $ref: '#/definitions/Autoprimary' + responses: + '201': + description: Created + <<: *commonErrors + + '/servers/{server_id}/autoprimaries/{ip}/{nameserver}': + parameters: + - name: server_id + in: path + required: true + description: 'The id of the server to delete the autoprimary from' + type: string + - name: ip + in: path + required: true + description: 'IP address of autoprimary' + type: string + - name: nameserver + in: path + required: true + description: 'DNS name of the autoprimary' + type: string + delete: + summary: 'Delete the autoprimary entry' + operationId: deleteAutoprimary + tags: + - autoprimary + responses: + '204': + description: 'OK, key was deleted' + <<: *commonErrors + +definitions: + Server: + title: Server + properties: + type: + type: string + description: 'Set to “Server”' + id: + type: string + description: 'The id of the server, “localhost”' + daemon_type: + type: string + description: '“recursor” for the PowerDNS Recursor and “authoritative” for the Authoritative Server' + version: + type: string + description: 'The version of the server software' + url: + type: string + description: 'The API endpoint for this server' + config_url: + type: string + description: 'The API endpoint for this server’s configuration' + zones_url: + type: string + description: 'The API endpoint for this server’s zones' + + Servers: + type: array + items: + $ref: '#/definitions/Server' + + Zone: + title: Zone + description: This represents an authoritative DNS Zone. + properties: + id: + type: string + description: 'Opaque zone id (string), assigned by the server, should not be interpreted by the application. Guaranteed to be safe for embedding in URLs.' + name: + type: string + description: 'Name of the zone (e.g. “example.com.”) MUST have a trailing dot' + type: + type: string + description: 'Set to “Zone”' + url: + type: string + description: 'API endpoint for this zone' + kind: + type: string + enum: + - 'Native' + - 'Master' + - 'Slave' + - 'Producer' + - 'Consumer' + description: 'Zone kind, one of “Native”, “Master”, “Slave”, “Producer”, “Consumer”' + rrsets: + type: array + items: + $ref: '#/definitions/RRSet' + description: 'RRSets in this zone (for zones/{zone_id} endpoint only; omitted during GET on the .../zones list endpoint)' + serial: + type: integer + description: 'The SOA serial number' + notified_serial: + type: integer + description: 'The SOA serial notifications have been sent out for' + edited_serial: + type: integer + description: 'The SOA serial as seen in query responses. Calculated using the SOA-EDIT metadata, default-soa-edit and default-soa-edit-signed settings' + masters: + type: array + items: + type: string + description: ' List of IP addresses configured as a master for this zone (“Slave” type zones only)' + dnssec: + type: boolean + description: 'Whether or not this zone is DNSSEC signed (inferred from presigned being true XOR presence of at least one cryptokey with active being true)' + nsec3param: + type: string + description: 'The NSEC3PARAM record' + nsec3narrow: + type: boolean + description: 'Whether or not the zone uses NSEC3 narrow' + presigned: + type: boolean + description: 'Whether or not the zone is pre-signed' + soa_edit: + type: string + description: 'The SOA-EDIT metadata item' + soa_edit_api: + type: string + description: 'The SOA-EDIT-API metadata item' + api_rectify: + type: boolean + description: 'Whether or not the zone will be rectified on data changes via the API' + zone: + type: string + description: 'MAY contain a BIND-style zone file when creating a zone' + catalog: + type: string + description: 'The catalog this zone is a member of' + account: + type: string + description: 'MAY be set. Its value is defined by local policy' + nameservers: + type: array + items: + type: string + description: 'MAY be sent in client bodies during creation, and MUST NOT be sent by the server. Simple list of strings of nameserver names, including the trailing dot. Not required for slave zones.' + master_tsig_key_ids: + type: array + items: + type: string + description: 'The id of the TSIG keys used for master operation in this zone' + externalDocs: + url: 'https://doc.powerdns.com/authoritative/tsig.html#provisioning-outbound-axfr-access' + slave_tsig_key_ids: + type: array + items: + type: string + description: 'The id of the TSIG keys used for slave operation in this zone' + externalDocs: + url: 'https://doc.powerdns.com/authoritative/tsig.html#provisioning-signed-notification-and-axfr-requests' + + Zones: + type: array + items: + $ref: '#/definitions/Zone' + + RRSet: + title: RRSet + description: This represents a Resource Record Set (all records with the same name and type). + required: + - name + - type + - ttl + - changetype + - records + properties: + name: + type: string + description: 'Name for record set (e.g. “www.powerdns.com.”)' + type: + type: string + description: 'Type of this record (e.g. “A”, “PTR”, “MX”)' + ttl: + type: integer + description: 'DNS TTL of the records, in seconds. MUST NOT be included when changetype is set to “DELETE”.' + changetype: + type: string + description: 'MUST be added when updating the RRSet. Must be REPLACE or DELETE. With DELETE, all existing RRs matching name and type will be deleted, including all comments. With REPLACE: when records is present, all existing RRs matching name and type will be deleted, and then new records given in records will be created. If no records are left, any existing comments will be deleted as well. When comments is present, all existing comments for the RRs matching name and type will be deleted, and then new comments given in comments will be created.' + records: + type: array + description: 'All records in this RRSet. When updating Records, this is the list of new records (replacing the old ones). Must be empty when changetype is set to DELETE. An empty list results in deletion of all records (and comments).' + items: + $ref: '#/definitions/Record' + comments: + type: array + description: 'List of Comment. Must be empty when changetype is set to DELETE. An empty list results in deletion of all comments. modified_at is optional and defaults to the current server time.' + items: + $ref: '#/definitions/Comment' + + Record: + title: Record + description: The RREntry object represents a single record. + required: + - content + properties: + content: + type: string + description: 'The content of this record' + disabled: + type: boolean + description: 'Whether or not this record is disabled. When unset, the record is not disabled' + + Comment: + title: Comment + description: A comment about an RRSet. + properties: + content: + type: string + description: 'The actual comment' + account: + type: string + description: 'Name of an account that added the comment' + modified_at: + type: integer + description: 'Timestamp of the last change to the comment' + + TSIGKey: + title: TSIGKey + description: A TSIG key that can be used to authenticate NOTIFY, AXFR, and DNSUPDATE queries. + properties: + name: + type: string + description: 'The name of the key' + id: + type: string + description: 'The ID for this key, used in the TSIGkey URL endpoint.' + readOnly: true + algorithm: + type: string + description: 'The algorithm of the TSIG key' + key: + type: string + description: 'The Base64 encoded secret key, empty when listing keys. MAY be empty when POSTing to have the server generate the key material' + type: + type: string + description: 'Set to "TSIGKey"' + readOnly: true + + Autoprimary: + title: Autoprimary server + description: An autoprimary server that can provision new domains. + properties: + ip: + type: string + description: "IP address of the autoprimary server" + nameserver: + type: string + description: "DNS name of the autoprimary server" + account: + type: string + description: "Account name for the autoprimary server" + + ConfigSetting: + title: ConfigSetting + properties: + name: + type: string + description: 'set to "ConfigSetting"' + type: + type: string + description: 'The name of this setting (e.g. ‘webserver-port’)' + value: + type: string + description: 'The value of setting name' + + SimpleStatisticItem: + title: SimpleStatisticItem + type: object + properties: + name: + type: string + description: 'Item name' + value: + type: string + description: 'Item value' + + StatisticItem: + title: StatisticItem + properties: + name: + type: string + description: 'Item name' + type: + type: string + description: 'set to "StatisticItem"' + value: + type: string + description: 'Item value' + + MapStatisticItem: + title: MapStatisticItem + properties: + name: + type: string + description: 'Item name' + type: + type: string + description: 'Set to "MapStatisticItem"' + value: + type: array + description: 'Named values' + items: + $ref: '#/definitions/SimpleStatisticItem' + + RingStatisticItem: + title: RingStatisticItem + properties: + name: + type: string + description: 'Item name' + type: + type: string + description: 'Set to "RingStatisticItem"' + size: + type: integer + description: 'Ring size' + value: + type: array + description: 'Named values' + items: + $ref: '#/definitions/SimpleStatisticItem' + + SearchResultZone: + title: SearchResultZone + properties: + name: + type: string + object_type: + type: string + description: 'set to "zone"' + zone_id: + type: string + + SearchResultRecord: + title: SearchResultRecord + properties: + content: + type: string + disabled: + type: boolean + name: + type: string + object_type: + type: string + description: 'set to "record"' + zone_id: + type: string + zone: + type: string + type: + type: string + ttl: + type: integer + + SearchResultComment: + title: SearchResultComment + properties: + content: + type: string + name: + type: string + object_type: + type: string + description: 'set to "comment"' + zone_id: + type: string + zone: + type: string + +# FIXME: This is problematic at the moment, because swagger doesn't support this type of mixed response +# SearchResult: +# anyOf: +# - $ref: '#/definitions/SearchResultZone' +# - $ref: '#/definitions/SearchResultRecord' +# - $ref: '#/definitions/SearchResultComment' + +# Since we can't do 'anyOf' at the moment, we create a 'superset object' + SearchResult: + title: SearchResult + properties: + content: + type: string + disabled: + type: boolean + name: + type: string + object_type: + type: string + description: 'set to one of "record, zone, comment"' + zone_id: + type: string + zone: + type: string + type: + type: string + ttl: + type: integer + + SearchResults: + type: array + items: + $ref: '#/definitions/SearchResult' + + Metadata: + title: Metadata + description: Represents zone metadata + properties: + kind: + type: string + description: 'Name of the metadata' + metadata: + type: array + items: + type: string + description: 'Array with all values for this metadata kind.' + + Cryptokey: + title: Cryptokey + description: 'Describes a DNSSEC cryptographic key' + properties: + type: + type: string + description: 'set to "Cryptokey"' + id: + type: integer + description: 'The internal identifier, read only' + keytype: + type: string + enum: [ksk, zsk, csk] + active: + type: boolean + description: 'Whether or not the key is in active use' + published: + type: boolean + description: 'Whether or not the DNSKEY record is published in the zone' + dnskey: + type: string + description: 'The DNSKEY record for this key' + ds: + type: array + items: + type: string + description: 'An array of DS records for this key' + cds: + type: array + items: + type: string + description: 'An array of DS records for this key, filtered by CDS publication settings' + privatekey: + type: string + description: 'The private key in ISC format' + algorithm: + type: string + description: 'The name of the algorithm of the key, should be a mnemonic' + bits: + type: integer + description: 'The size of the key' + + Error: + title: Error + description: 'Returned when the server encounters an error, either in client input or internally' + properties: + error: + type: string + description: 'A human readable error message' + errors: + type: array + items: + type: string + description: 'Optional array of multiple errors encountered during processing' + required: + - error + + CacheFlushResult: + title: CacheFlushResult + description: 'The result of a cache-flush' + properties: + count: + type: number + description: 'Amount of entries flushed' + result: + type: string + description: 'A message about the result like "Flushed cache"' \ No newline at end of file diff --git a/core/src/main/java/google/registry/module/RequestComponent.java b/core/src/main/java/google/registry/module/RequestComponent.java index b865edd43a8..96f912dbd07 100644 --- a/core/src/main/java/google/registry/module/RequestComponent.java +++ b/core/src/main/java/google/registry/module/RequestComponent.java @@ -42,6 +42,8 @@ import google.registry.dns.writer.clouddns.CloudDnsWriterModule; import google.registry.dns.writer.dnsupdate.DnsUpdateConfigModule; import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; +import google.registry.dns.writer.powerdns.PowerDnsConfigModule; +import google.registry.dns.writer.powerdns.PowerDnsWriterModule; import google.registry.export.ExportDomainListsAction; import google.registry.export.ExportPremiumTermsAction; import google.registry.export.ExportReservedTermsAction; @@ -148,6 +150,8 @@ DnsModule.class, DnsUpdateConfigModule.class, DnsUpdateWriterModule.class, + PowerDnsConfigModule.class, + PowerDnsWriterModule.class, EppTlsModule.class, EppToolModule.class, IcannReportingModule.class, diff --git a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java index 91a9863a98c..4125bce0655 100644 --- a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java +++ b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java @@ -38,6 +38,8 @@ import google.registry.dns.writer.clouddns.CloudDnsWriterModule; import google.registry.dns.writer.dnsupdate.DnsUpdateConfigModule; import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; +import google.registry.dns.writer.powerdns.PowerDnsConfigModule; +import google.registry.dns.writer.powerdns.PowerDnsWriterModule; import google.registry.export.ExportDomainListsAction; import google.registry.export.ExportPremiumTermsAction; import google.registry.export.ExportReservedTermsAction; @@ -89,6 +91,8 @@ DnsModule.class, DnsUpdateConfigModule.class, DnsUpdateWriterModule.class, + PowerDnsConfigModule.class, + PowerDnsWriterModule.class, IcannReportingModule.class, RdeModule.class, ReportingModule.class, diff --git a/core/src/main/java/google/registry/tools/RegistryToolComponent.java b/core/src/main/java/google/registry/tools/RegistryToolComponent.java index fd4b6ddbf38..5aef100085a 100644 --- a/core/src/main/java/google/registry/tools/RegistryToolComponent.java +++ b/core/src/main/java/google/registry/tools/RegistryToolComponent.java @@ -26,6 +26,8 @@ import google.registry.dns.writer.VoidDnsWriterModule; import google.registry.dns.writer.clouddns.CloudDnsWriterModule; import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; +import google.registry.dns.writer.powerdns.PowerDnsConfigModule; +import google.registry.dns.writer.powerdns.PowerDnsWriterModule; import google.registry.keyring.KeyringModule; import google.registry.keyring.api.KeyModule; import google.registry.model.ModelModule; @@ -59,6 +61,8 @@ CloudDnsWriterModule.class, CloudTasksUtilsModule.class, DnsUpdateWriterModule.class, + PowerDnsConfigModule.class, + PowerDnsWriterModule.class, GsonModule.class, KeyModule.class, KeyringModule.class, From 4333a55edb409c588d41c35f71a76a58ba51daf9 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Sat, 3 May 2025 09:02:23 -0400 Subject: [PATCH 02/49] inherit base staging logic from DnsUpdateWriter --- .../dns/writer/dnsupdate/DnsUpdateWriter.java | 6 +- .../dns/writer/powerdns/PowerDnsWriter.java | 59 +++++++++++++------ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/google/registry/dns/writer/dnsupdate/DnsUpdateWriter.java b/core/src/main/java/google/registry/dns/writer/dnsupdate/DnsUpdateWriter.java index 86489f3d107..85e2d5cd3ad 100644 --- a/core/src/main/java/google/registry/dns/writer/dnsupdate/DnsUpdateWriter.java +++ b/core/src/main/java/google/registry/dns/writer/dnsupdate/DnsUpdateWriter.java @@ -90,9 +90,9 @@ public class DnsUpdateWriter extends BaseDnsWriter { private final Duration dnsDefaultNsTtl; private final Duration dnsDefaultDsTtl; private final DnsMessageTransport transport; - private final Clock clock; - private final Update update; - private final String zoneName; + protected final Clock clock; + protected final Update update; + protected final String zoneName; /** * Class constructor. diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index ba5dc725330..28279dcb174 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -2,29 +2,31 @@ import com.google.common.flogger.FluentLogger; import google.registry.config.RegistryConfig.Config; -import google.registry.dns.writer.BaseDnsWriter; import google.registry.dns.writer.DnsWriterZone; +import google.registry.dns.writer.dnsupdate.DnsUpdateWriter; import google.registry.dns.writer.powerdns.client.PowerDNSClient; import google.registry.util.Clock; import jakarta.inject.Inject; -import java.util.HashSet; -import java.util.Set; +import org.joda.time.Duration; -/** A DnsWriter that implements the PowerDNS API. */ -public class PowerDnsWriter extends BaseDnsWriter { +/** + * A DnsWriter that sends updates to a PowerDNS backend server. Extends the peer DnsUpdateWriter + * class, which already handles the logic for aggregating DNS changes into a single update request. + * This request is then converted into a PowerDNS Zone object and sent to the PowerDNS API. + */ +public class PowerDnsWriter extends DnsUpdateWriter { public static final String NAME = "PowerDnsWriter"; - private final Clock clock; private final PowerDNSClient powerDnsClient; - private final String zoneName; - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private final Set names = new HashSet<>(); /** * Class constructor. * * @param zoneName the name of the zone to write to + * @param dnsDefaultATtl the default TTL for A records + * @param dnsDefaultNsTtl the default TTL for NS records + * @param dnsDefaultDsTtl the default TTL for DS records * @param powerDnsHost the host of the PowerDNS server * @param powerDnsApiKey the API key for the PowerDNS server * @param clock the clock to use for the PowerDNS writer @@ -32,34 +34,53 @@ public class PowerDnsWriter extends BaseDnsWriter { @Inject public PowerDnsWriter( @DnsWriterZone String zoneName, + @Config("dnsDefaultATtl") Duration dnsDefaultATtl, + @Config("dnsDefaultNsTtl") Duration dnsDefaultNsTtl, + @Config("dnsDefaultDsTtl") Duration dnsDefaultDsTtl, @Config("powerDnsHost") String powerDnsHost, @Config("powerDnsApiKey") String powerDnsApiKey, Clock clock) { - // Initialize the PowerDNS client and zone name + // call the DnsUpdateWriter constructor, omitting the transport parameter + // since we don't need it for PowerDNS + super(zoneName, dnsDefaultATtl, dnsDefaultNsTtl, dnsDefaultDsTtl, null, clock); + + // Initialize the PowerDNS client this.powerDnsClient = new PowerDNSClient(powerDnsHost, powerDnsApiKey); - this.zoneName = zoneName; - this.clock = clock; } + /** + * Prepare a domain for staging in PowerDNS. Handles the logic to clean up orphaned glue records + * and adds the domain records to the update. + * + * @param domainName the fully qualified domain name, with no trailing dot + */ @Override public void publishDomain(String domainName) { - // TODO: Implement the logic to stage the domain zone files to PowerDNS for commit - names.add(domainName); + logger.atInfo().log("Staging domain %s for PowerDNS", domainName); + super.publishDomain(domainName); } + /** + * Determine whether the host should be published as a glue record for this zone. If so, add the + * host records to the update. + * + * @param hostName the fully qualified host name, with no trailing dot + */ @Override public void publishHost(String hostName) { - // TODO: Implement the logic to stage the host glue records to PowerDNS for commit - names.add(hostName); + logger.atInfo().log("Staging host %s for PowerDNS", hostName); + super.publishHost(hostName); } @Override protected void commitUnchecked() { + // TODO: Convert the parent class's update object (org.xbill.DNS.Update) into + // a PowerDNS Zone object (google.registry.dns.writer.powerdns.client.Zone) + // TODO: Call the PowerDNS API to commit the changes logger.atWarning().log( - "PowerDnsWriter for server ID %s not yet implemented; ignoring %s names to commit: %s at" - + " %s", - powerDnsClient.getServerId(), zoneName, names, clock.nowUtc()); + "PowerDnsWriter for server ID %s not yet implemented; ignoring zone %s", + powerDnsClient.getServerId(), zoneName); } } From d32c1bcea9585cbdf0497d0bbd49580144a88073 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Sat, 3 May 2025 13:37:23 -0400 Subject: [PATCH 03/49] initial conversion of DNS changes for PowerDNS API usage --- .../dns/writer/powerdns/PowerDnsWriter.java | 105 +++++++++++++++++- .../writer/powerdns/client/model/RRSet.java | 8 +- 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 28279dcb174..3977abb3166 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -5,9 +5,19 @@ import google.registry.dns.writer.DnsWriterZone; import google.registry.dns.writer.dnsupdate.DnsUpdateWriter; import google.registry.dns.writer.powerdns.client.PowerDNSClient; +import google.registry.dns.writer.powerdns.client.model.RRSet; +import google.registry.dns.writer.powerdns.client.model.RecordObject; +import google.registry.dns.writer.powerdns.client.model.Zone; import google.registry.util.Clock; import jakarta.inject.Inject; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import org.joda.time.Duration; +import org.xbill.DNS.Record; +import org.xbill.DNS.Section; +import org.xbill.DNS.Type; +import org.xbill.DNS.Update; /** * A DnsWriter that sends updates to a PowerDNS backend server. Extends the peer DnsUpdateWriter @@ -75,12 +85,95 @@ public void publishHost(String hostName) { @Override protected void commitUnchecked() { - // TODO: Convert the parent class's update object (org.xbill.DNS.Update) into - // a PowerDNS Zone object (google.registry.dns.writer.powerdns.client.Zone) + try { + // persist staged changes to PowerDNS + logger.atInfo().log( + "Committing updates to PowerDNS for zone %s on server %s", + zoneName, powerDnsClient.getServerId()); - // TODO: Call the PowerDNS API to commit the changes - logger.atWarning().log( - "PowerDnsWriter for server ID %s not yet implemented; ignoring zone %s", - powerDnsClient.getServerId(), zoneName); + // convert the update to a PowerDNS Zone object + Zone zone = convertUpdateToZone(update); + + // call the PowerDNS API to commit the changes + powerDnsClient.patchZone(zone.getId(), zone); + } catch (IOException e) { + throw new RuntimeException("publishDomain failed for zone: " + zoneName, e); + } + } + + /** + * Convert the parent class's update object (org.xbill.DNS.Update) into a PowerDNS Zone object + * (google.registry.dns.writer.powerdns.client.Zone) + * + * @param update the update object to convert + * @return the PowerDNS Zone object + * @throws IOException if the zone is not found + */ + private Zone convertUpdateToZone(Update update) throws IOException { + // Iterate the update records and prepare them as PowerDNS RRSet objects, referencing the + // following source code to determine the usage of the org.xbill.DNS.Record object: + // + // https://www.javadoc.io/doc/dnsjava/dnsjava/3.2.1/org/xbill/DNS/Record.html + // https://github.com/dnsjava/dnsjava/blob/master/src/main/java/org/xbill/DNS/Record.java#L324-L350 + ArrayList updatedRRSets = new ArrayList(); + for (Record r : update.getSection(Section.UPDATE)) { + logger.atInfo().log("Processing zone update record: %s", r); + + // create a PowerDNS RRSet object + RRSet record = new RRSet(); + record.setName(r.getName().toString()); + record.setTtl(r.getTTL()); + record.setType(Type.string(r.getType())); + + // add the record content + RecordObject recordObject = new RecordObject(); + recordObject.setContent(r.rdataToString()); + recordObject.setDisabled(false); + record.setRecords(new ArrayList(Arrays.asList(recordObject))); + + // TODO: need to figure out how to handle the change type of + // the record set. How to handle new and deleted records? + record.setChangeType(RRSet.ChangeType.REPLACE); + + // add the RRSet to the list of updated RRSets + updatedRRSets.add(record); + } + + // retrieve the zone by name and check that it exists + Zone zone = getZoneByName(); + + // prepare the zone for updates + Zone preparedZone = prepareZoneForUpdates(zone); + preparedZone.setRrsets(updatedRRSets); + + // return the prepared zone + return preparedZone; + } + + /** + * Prepare the zone for updates by clearing the RRSets and incrementing the serial number. + * + * @param zone the zone to prepare + * @return the prepared zone + */ + private Zone prepareZoneForUpdates(Zone zone) { + zone.setRrsets(new ArrayList()); + zone.setEditedSerial(zone.getSerial() + 1); + return zone; + } + + /** + * Get the zone by name. + * + * @return the zone + * @throws IOException if the zone is not found + */ + private Zone getZoneByName() throws IOException { + for (Zone zone : powerDnsClient.listZones()) { + if (zone.getName().equals(zoneName)) { + return zone; + } + } + throw new IOException("Zone not found: " + zoneName); } } diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java index 42ea5ad735a..488cf7bd109 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java @@ -11,7 +11,7 @@ public class RRSet { private String type; @JsonProperty("ttl") - private Integer ttl; + private long ttl; @JsonProperty("changetype") private ChangeType changetype; @@ -38,11 +38,11 @@ public void setType(String type) { this.type = type; } - public Integer getTtl() { + public long getTtl() { return ttl; } - public void setTtl(Integer ttl) { + public void setTtl(long ttl) { this.ttl = ttl; } @@ -50,7 +50,7 @@ public ChangeType getChangetype() { return changetype; } - public void setChangetype(ChangeType changetype) { + public void setChangeType(ChangeType changetype) { this.changetype = changetype; } From 5639b21b2bd1be56ba178b9b5205c02761a93aa0 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Mon, 5 May 2025 13:10:52 -0400 Subject: [PATCH 04/49] handle record updates and deletes --- .../dns/writer/powerdns/PowerDnsWriter.java | 114 +++++++++++------- .../writer/powerdns/client/model/RRSet.java | 2 +- .../writer/powerdns/client/model/Zone.java | 10 ++ 3 files changed, 83 insertions(+), 43 deletions(-) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 3977abb3166..8d874378506 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.joda.time.Duration; import org.xbill.DNS.Record; import org.xbill.DNS.Section; @@ -27,13 +28,14 @@ public class PowerDnsWriter extends DnsUpdateWriter { public static final String NAME = "PowerDnsWriter"; + private final String tldZoneName; private final PowerDNSClient powerDnsClient; private static final FluentLogger logger = FluentLogger.forEnclosingClass(); /** * Class constructor. * - * @param zoneName the name of the zone to write to + * @param tldZoneName the name of the TLD associated with the update * @param dnsDefaultATtl the default TTL for A records * @param dnsDefaultNsTtl the default TTL for NS records * @param dnsDefaultDsTtl the default TTL for DS records @@ -43,7 +45,7 @@ public class PowerDnsWriter extends DnsUpdateWriter { */ @Inject public PowerDnsWriter( - @DnsWriterZone String zoneName, + @DnsWriterZone String tldZoneName, @Config("dnsDefaultATtl") Duration dnsDefaultATtl, @Config("dnsDefaultNsTtl") Duration dnsDefaultNsTtl, @Config("dnsDefaultDsTtl") Duration dnsDefaultDsTtl, @@ -53,9 +55,10 @@ public PowerDnsWriter( // call the DnsUpdateWriter constructor, omitting the transport parameter // since we don't need it for PowerDNS - super(zoneName, dnsDefaultATtl, dnsDefaultNsTtl, dnsDefaultDsTtl, null, clock); + super(tldZoneName, dnsDefaultATtl, dnsDefaultNsTtl, dnsDefaultDsTtl, null, clock); // Initialize the PowerDNS client + this.tldZoneName = tldZoneName; this.powerDnsClient = new PowerDNSClient(powerDnsHost, powerDnsApiKey); } @@ -88,8 +91,8 @@ protected void commitUnchecked() { try { // persist staged changes to PowerDNS logger.atInfo().log( - "Committing updates to PowerDNS for zone %s on server %s", - zoneName, powerDnsClient.getServerId()); + "Committing updates to PowerDNS for TLD %s on server %s", + tldZoneName, powerDnsClient.getServerId()); // convert the update to a PowerDNS Zone object Zone zone = convertUpdateToZone(update); @@ -97,7 +100,7 @@ protected void commitUnchecked() { // call the PowerDNS API to commit the changes powerDnsClient.patchZone(zone.getId(), zone); } catch (IOException e) { - throw new RuntimeException("publishDomain failed for zone: " + zoneName, e); + throw new RuntimeException("publishDomain failed for TLD: " + tldZoneName, e); } } @@ -115,65 +118,92 @@ private Zone convertUpdateToZone(Update update) throws IOException { // // https://www.javadoc.io/doc/dnsjava/dnsjava/3.2.1/org/xbill/DNS/Record.html // https://github.com/dnsjava/dnsjava/blob/master/src/main/java/org/xbill/DNS/Record.java#L324-L350 - ArrayList updatedRRSets = new ArrayList(); + ArrayList allRRSets = new ArrayList(); + ArrayList filteredRRSets = new ArrayList(); for (Record r : update.getSection(Section.UPDATE)) { - logger.atInfo().log("Processing zone update record: %s", r); + logger.atInfo().log("Processing TLD zone %s update record: %s", tldZoneName, r); - // create a PowerDNS RRSet object + // create the base PowerDNS RRSet object RRSet record = new RRSet(); record.setName(r.getName().toString()); record.setTtl(r.getTTL()); record.setType(Type.string(r.getType())); - // add the record content - RecordObject recordObject = new RecordObject(); - recordObject.setContent(r.rdataToString()); - recordObject.setDisabled(false); - record.setRecords(new ArrayList(Arrays.asList(recordObject))); - - // TODO: need to figure out how to handle the change type of - // the record set. How to handle new and deleted records? - record.setChangeType(RRSet.ChangeType.REPLACE); + // determine if this is a record update or a record deletion + Boolean isDelete = r.getTTL() == 0 && r.rdataToString().equals(""); + + // handle record updates and deletions + if (isDelete) { + // indicate that this is a record deletion + record.setChangeType(RRSet.ChangeType.DELETE); + } else { + // add the record content + RecordObject recordObject = new RecordObject(); + recordObject.setContent(r.rdataToString()); + recordObject.setDisabled(false); + record.setRecords(new ArrayList(Arrays.asList(recordObject))); + + // indicate that this is a record update + record.setChangeType(RRSet.ChangeType.REPLACE); + } - // add the RRSet to the list of updated RRSets - updatedRRSets.add(record); + // Add record to lists of all and filtered RRSets. The first list is used to track all RRSets + // for the TLD zone, while the second list is used to track the RRSets that will be sent to + // the PowerDNS API. By default, there is a deletion record created by the parent class for + // every domain name and record type combination. However, PowerDNS only expects to see a + // deletion record if the record should be removed from the TLD zone. + allRRSets.add(record); + filteredRRSets.add(record); } - // retrieve the zone by name and check that it exists - Zone zone = getZoneByName(); - - // prepare the zone for updates - Zone preparedZone = prepareZoneForUpdates(zone); - preparedZone.setRrsets(updatedRRSets); - - // return the prepared zone - return preparedZone; + // remove deletion records for a domain if there is a subsequent update enqueued + // for the same domain name and record type combination + allRRSets.stream() + .filter(r -> r.getChangeType() == RRSet.ChangeType.REPLACE) + .forEach( + r -> { + filteredRRSets.removeIf( + fr -> + fr.getName().equals(r.getName()) + && fr.getType().equals(r.getType()) + && fr.getChangeType() == RRSet.ChangeType.DELETE); + }); + + // retrieve the TLD zone by name and prepare it for update using the filtered set of + // RRSet records that will be sent to the PowerDNS API + Zone tldZone = getTldZoneByName(); + Zone preparedTldZone = prepareTldZoneForUpdates(tldZone, filteredRRSets); + + // return the prepared TLD zone + logger.atInfo().log("Prepared TLD zone %s for PowerDNS: %s", tldZoneName, preparedTldZone); + return preparedTldZone; } /** - * Prepare the zone for updates by clearing the RRSets and incrementing the serial number. + * Prepare the TLD zone for updates by clearing the RRSets and incrementing the serial number. * - * @param zone the zone to prepare - * @return the prepared zone + * @param tldZone the TLD zone to prepare + * @param records the set of RRSet records that will be sent to the PowerDNS API + * @return the prepared TLD zone */ - private Zone prepareZoneForUpdates(Zone zone) { - zone.setRrsets(new ArrayList()); - zone.setEditedSerial(zone.getSerial() + 1); - return zone; + private Zone prepareTldZoneForUpdates(Zone tldZone, List records) { + tldZone.setRrsets(records); + tldZone.setEditedSerial(tldZone.getSerial() + 1); + return tldZone; } /** - * Get the zone by name. + * Get the TLD zone by name. * - * @return the zone - * @throws IOException if the zone is not found + * @return the TLD zone + * @throws IOException if the TLD zone is not found */ - private Zone getZoneByName() throws IOException { + private Zone getTldZoneByName() throws IOException { for (Zone zone : powerDnsClient.listZones()) { - if (zone.getName().equals(zoneName)) { + if (zone.getName().equals(tldZoneName)) { return zone; } } - throw new IOException("Zone not found: " + zoneName); + throw new IOException("TLD zone not found: " + tldZoneName); } } diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java index 488cf7bd109..3dba7bd1998 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java @@ -46,7 +46,7 @@ public void setTtl(long ttl) { this.ttl = ttl; } - public ChangeType getChangetype() { + public ChangeType getChangeType() { return changetype; } diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java index 08a96c18fe4..a3c581879c0 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java @@ -257,6 +257,16 @@ public void setSlaveTsigKeyIds(List slaveTsigKeyIds) { this.slaveTsigKeyIds = slaveTsigKeyIds; } + @Override + public String toString() { + long deletedCount = + rrsets.stream().filter(rrset -> rrset.getChangeType() == RRSet.ChangeType.DELETE).count(); + long updatedCount = + rrsets.stream().filter(rrset -> rrset.getChangeType() == RRSet.ChangeType.REPLACE).count(); + return String.format( + "{id:%s,name:%s,deleted:%d,updated:%d}", id, name, deletedCount, updatedCount); + } + public enum ZoneKind { Native, Master, From d0bc866380dd8ddb55454b74e7ff32357ee587ba Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Mon, 5 May 2025 13:45:54 -0400 Subject: [PATCH 05/49] add powerDNS API logging --- .../dns/writer/powerdns/PowerDnsWriter.java | 2 +- .../powerdns/client/PowerDNSClient.java | 41 ++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 8d874378506..88a8e7c3d78 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -98,7 +98,7 @@ protected void commitUnchecked() { Zone zone = convertUpdateToZone(update); // call the PowerDNS API to commit the changes - powerDnsClient.patchZone(zone.getId(), zone); + powerDnsClient.patchZone(zone); } catch (IOException e) { throw new RuntimeException("publishDomain failed for TLD: " + tldZoneName, e); } diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java index adad1e40bf8..22f6a273869 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java @@ -1,6 +1,7 @@ package google.registry.dns.writer.powerdns.client; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.flogger.FluentLogger; import google.registry.dns.writer.powerdns.client.model.Server; import google.registry.dns.writer.powerdns.client.model.Zone; import java.io.IOException; @@ -14,6 +15,7 @@ public class PowerDNSClient { // static fields + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final OkHttpClient httpClient; private final ObjectMapper objectMapper; private final String baseUrl; @@ -42,11 +44,30 @@ public PowerDNSClient(String baseUrl, String apiKey) { } } + private Response logAndExecuteRequest(Request request) throws IOException { + // log the request and create timestamp for the start time + logger.atInfo().log("Executing PowerDNS request: %s, body: %s", request, request.body()); + long startTime = System.currentTimeMillis(); + + // execute the request and log the response + Response response = httpClient.newCall(request).execute(); + logger.atInfo().log("PowerDNS response: %s", response); + + // log the response time and response code + long endTime = System.currentTimeMillis(); + logger.atInfo().log( + "Completed PowerDNS request in %d ms, success: %s, response code: %d", + endTime - startTime, response.isSuccessful(), response.code()); + + // return the response + return response; + } + public List listServers() throws IOException { Request request = new Request.Builder().url(baseUrl + "/servers").header("X-API-Key", apiKey).get().build(); - try (Response response = httpClient.newCall(request).execute()) { + try (Response response = logAndExecuteRequest(request)) { if (!response.isSuccessful()) { throw new IOException("Failed to list servers: " + response); } @@ -64,7 +85,7 @@ public Server getServer() throws IOException { .get() .build(); - try (Response response = httpClient.newCall(request).execute()) { + try (Response response = logAndExecuteRequest(request)) { if (!response.isSuccessful()) { throw new IOException("Failed to get server: " + response); } @@ -88,7 +109,7 @@ public List listZones() throws IOException { .get() .build(); - try (Response response = httpClient.newCall(request).execute()) { + try (Response response = logAndExecuteRequest(request)) { if (!response.isSuccessful()) { throw new IOException("Failed to list zones: " + response); } @@ -106,7 +127,7 @@ public Zone getZone(String zoneId) throws IOException { .get() .build(); - try (Response response = httpClient.newCall(request).execute()) { + try (Response response = logAndExecuteRequest(request)) { if (!response.isSuccessful()) { throw new IOException("Failed to get zone: " + response); } @@ -125,7 +146,7 @@ public Zone createZone(Zone zone) throws IOException { .post(body) .build(); - try (Response response = httpClient.newCall(request).execute()) { + try (Response response = logAndExecuteRequest(request)) { if (!response.isSuccessful()) { throw new IOException("Failed to create zone: " + response); } @@ -141,25 +162,25 @@ public void deleteZone(String zoneId) throws IOException { .delete() .build(); - try (Response response = httpClient.newCall(request).execute()) { + try (Response response = logAndExecuteRequest(request)) { if (!response.isSuccessful()) { throw new IOException("Failed to delete zone: " + response); } } } - public void patchZone(String zoneId, Zone zone) throws IOException { + public void patchZone(Zone zone) throws IOException { String json = objectMapper.writeValueAsString(zone); RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); Request request = new Request.Builder() - .url(baseUrl + "/servers/" + serverId + "/zones/" + zoneId) + .url(baseUrl + "/servers/" + serverId + "/zones/" + zone.getId()) .header("X-API-Key", apiKey) .patch(body) .build(); - try (Response response = httpClient.newCall(request).execute()) { + try (Response response = logAndExecuteRequest(request)) { if (!response.isSuccessful()) { throw new IOException("Failed to patch zone: " + response); } @@ -174,7 +195,7 @@ public void notifyZone(String zoneId) throws IOException { .put(RequestBody.create("", MediaType.parse("application/json"))) .build(); - try (Response response = httpClient.newCall(request).execute()) { + try (Response response = logAndExecuteRequest(request)) { if (!response.isSuccessful()) { throw new IOException("Failed to notify zone: " + response); } From 4a45d9e5d01d654da6ac4381028de35d6cdca9d5 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Mon, 5 May 2025 15:00:28 -0400 Subject: [PATCH 06/49] add license headers --- .../writer/powerdns/PowerDnsConfigModule.java | 14 + .../dns/writer/powerdns/PowerDnsWriter.java | 14 + .../writer/powerdns/PowerDnsWriterModule.java | 14 + .../powerdns/client/PowerDNSClient.java | 14 + .../writer/powerdns/client/model/Comment.java | 14 + .../writer/powerdns/client/model/RRSet.java | 14 + .../powerdns/client/model/RecordObject.java | 14 + .../writer/powerdns/client/model/Server.java | 14 + .../writer/powerdns/client/model/Zone.java | 14 + .../openapi/authoritative-api-swagger.yaml | 1398 ----------------- 10 files changed, 126 insertions(+), 1398 deletions(-) delete mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/client/openapi/authoritative-api-swagger.yaml diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java index 130e31f7844..b9eb16d6a01 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java @@ -1,3 +1,17 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package google.registry.dns.writer.powerdns; import dagger.Module; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 88a8e7c3d78..8ae1a48008f 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -1,3 +1,17 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package google.registry.dns.writer.powerdns; import com.google.common.flogger.FluentLogger; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriterModule.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriterModule.java index 2c2de487828..ffa99b2b6ef 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriterModule.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriterModule.java @@ -1,3 +1,17 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package google.registry.dns.writer.powerdns; import dagger.Binds; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java index 22f6a273869..3cfa23e52a0 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java @@ -1,3 +1,17 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package google.registry.dns.writer.powerdns.client; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java index 32d9cb87db3..ba49afa7d01 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java @@ -1,3 +1,17 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package google.registry.dns.writer.powerdns.client.model; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java index 3dba7bd1998..0620fb757fd 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java @@ -1,3 +1,17 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package google.registry.dns.writer.powerdns.client.model; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java index ffe3c2e40cf..1ad33e4f209 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java @@ -1,3 +1,17 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package google.registry.dns.writer.powerdns.client.model; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java index 135f488a4ae..024ee40946a 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java @@ -1,3 +1,17 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package google.registry.dns.writer.powerdns.client.model; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java index a3c581879c0..ddc406510ad 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java @@ -1,3 +1,17 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package google.registry.dns.writer.powerdns.client.model; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/openapi/authoritative-api-swagger.yaml b/core/src/main/java/google/registry/dns/writer/powerdns/client/openapi/authoritative-api-swagger.yaml deleted file mode 100644 index 555fe70c2b7..00000000000 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/openapi/authoritative-api-swagger.yaml +++ /dev/null @@ -1,1398 +0,0 @@ -swagger: '2.0' -info: - version: "0.0.15" - title: PowerDNS Authoritative HTTP API - license: - name: MIT -basePath: /api/v1 -consumes: - - application/json -produces: - - application/json -securityDefinitions: - # X-API-Key: abcdef12345 - APIKeyHeader: - type: apiKey - in: header - name: X-API-Key -security: - - APIKeyHeader: [] - -# Overall TODOS: -# TODO: Return types are not consistent across documentation -# We need to look at the code and figure out the default HTTP response -# codes and adjust docs accordingly. -paths: - '/error': - get: - summary: Will always generate an error - operationId: error - responses: &commonErrors - '400': - description: The supplied request was not valid - schema: - $ref: '#/definitions/Error' - '404': - description: Requested item was not found - schema: - $ref: '#/definitions/Error' - '422': - description: The input to the operation was not valid - schema: - $ref: '#/definitions/Error' - '500': - description: Internal server error - schema: - $ref: '#/definitions/Error' - - '/servers': - get: - summary: List all servers - operationId: listServers - tags: - - servers - responses: - '200': - description: An array of servers - schema: - type: array - items: - $ref: '#/definitions/Server' - <<: *commonErrors - - '/servers/{server_id}': - get: - summary: List a server - operationId: listServer - tags: - - servers - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - responses: - '200': - description: An server - schema: - $ref: '#/definitions/Server' - <<: *commonErrors - - '/servers/{server_id}/cache/flush': - put: - summary: Flush a cache-entry by name - operationId: cacheFlushByName - tags: - - servers - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: domain - in: query - required: true - description: The domain name to flush from the cache - type: string - responses: - '200': - description: Flush successful - schema: - $ref: '#/definitions/CacheFlushResult' - <<: *commonErrors - - '/servers/{server_id}/zones': - get: - summary: List all Zones in a server - operationId: listZones - tags: - - zones - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone - in: query - required: false - type: string - description: | - When set to the name of a zone, only this zone is returned. - If no zone with that name exists, the response is an empty array. - This can e.g. be used to check if a zone exists in the database without having to guess/encode the zone's id or to check if a zone exists. - - name: dnssec - in: query - required: false - type: boolean - default: true - description: '“true” (default) or “false”, whether to include the “dnssec” and ”edited_serial” fields in the Zone objects. Setting this to ”false” will make the query a lot faster.' - responses: - '200': - description: An array of Zones - schema: - type: array - items: - $ref: '#/definitions/Zone' - <<: *commonErrors - post: - summary: Creates a new domain, returns the Zone on creation. - operationId: createZone - tags: - - zones - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: rrsets - in: query - description: '“true” (default) or “false”, whether to include the “rrsets” in the response Zone object.' - type: boolean - default: true - - name: zone_struct - description: The zone struct to patch with - required: true - in: body - schema: - $ref: '#/definitions/Zone' - responses: - '201': - description: A zone - schema: - $ref: '#/definitions/Zone' - <<: *commonErrors - - '/servers/{server_id}/zones/{zone_id}': - get: - summary: zone managed by a server - operationId: listZone - tags: - - zones - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - - name: rrsets - in: query - description: '“true” (default) or “false”, whether to include the “rrsets” in the response Zone object.' - type: boolean - default: true - - name: rrset_name - in: query - description: Limit output to RRsets for this name. - type: string - - name: rrset_type - in: query - description: Limit output to the RRset of this type. Can only be used together with rrset_name. - type: string - - name: include_disabled - in: query - description: '“true” (default) or “false”, whether to include disabled RRsets in the response.' - type: boolean - responses: - '200': - description: A Zone - schema: - $ref: '#/definitions/Zone' - <<: *commonErrors - delete: - summary: Deletes this zone, all attached metadata and rrsets. - operationId: deleteZone - tags: - - zones - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - responses: - '204': - description: 'Returns 204 No Content on success.' - <<: *commonErrors - patch: - summary: 'Creates/modifies/deletes RRsets present in the payload and their comments. Returns 204 No Content on success.' - operationId: patchZone - tags: - - zones - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - - name: zone_struct - description: The zone struct to patch with - required: true - in: body - schema: - $ref: '#/definitions/Zone' - responses: - '204': - description: 'Returns 204 No Content on success.' - <<: *commonErrors - - put: - summary: Modifies basic zone data. - description: 'The only fields in the zone structure which can be modified are: kind, masters, catalog, account, soa_edit, soa_edit_api, api_rectify, dnssec, and nsec3param. All other fields are ignored.' - operationId: putZone - tags: - - zones - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - - name: zone_struct - description: The zone struct to patch with - required: true - in: body - schema: - $ref: '#/definitions/Zone' - responses: - '204': - description: 'Returns 204 No Content on success.' - <<: *commonErrors - - '/servers/{server_id}/zones/{zone_id}/notify': - put: - summary: Send a DNS NOTIFY to all slaves. - description: 'Fails when zone kind is not Master or Slave, or master and slave are disabled in the configuration. Only works for Slave if renotify is on. Clients MUST NOT send a body.' - operationId: notifyZone - tags: - - zones - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - responses: - '200': - description: OK - <<: *commonErrors - - '/servers/{server_id}/zones/{zone_id}/axfr-retrieve': - put: - summary: Retrieve slave zone from its master. - description: 'Fails when zone kind is not Slave, or slave is disabled in the configuration. Clients MUST NOT send a body.' - operationId: axfrRetrieveZone - tags: - - zones - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - responses: - '200': - description: OK - <<: *commonErrors - - '/servers/{server_id}/zones/{zone_id}/export': - get: - summary: 'Returns the zone in AXFR format.' - operationId: axfrExportZone - tags: - - zones - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - responses: - '200': - description: OK - schema: - type: string - <<: *commonErrors - - '/servers/{server_id}/zones/{zone_id}/rectify': - put: - summary: 'Rectify the zone data.' - description: 'This does not take into account the API-RECTIFY metadata. Fails on slave zones and zones that do not have DNSSEC.' - operationId: rectifyZone - tags: - - zones - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - responses: - '200': - description: OK - schema: - type: string - <<: *commonErrors - - '/servers/{server_id}/config': - get: - summary: 'Returns all ConfigSettings for a single server' - operationId: getConfig - tags: - - config - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - responses: - '200': - description: List of config values - schema: - type: array - items: - $ref: '#/definitions/ConfigSetting' - <<: *commonErrors - - '/servers/{server_id}/config/{config_setting_name}': - get: - summary: 'Returns a specific ConfigSetting for a single server' - description: 'NOT IMPLEMENTED' - operationId: getConfigSetting - tags: - - config - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: config_setting_name - in: path - required: true - description: The name of the setting to retrieve - type: string - responses: - '200': - description: List of config values - schema: - $ref: '#/definitions/ConfigSetting' - <<: *commonErrors - - '/servers/{server_id}/statistics': - get: - summary: 'Query statistics.' - description: 'Query PowerDNS internal statistics.' - operationId: getStats - tags: - - stats - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: statistic - in: query - required: false - type: string - description: | - When set to the name of a specific statistic, only this value is returned. - If no statistic with that name exists, the response has a 422 status and an error message. - - name: includerings - in: query - required: false - type: boolean - default: true - description: '“true” (default) or “false”, whether to include the Ring items, which can contain thousands of log messages or queried domains. Setting this to ”false” may make the response a lot smaller.' - responses: - '200': - description: List of Statistic Items - schema: - type: array - items: - - $ref: '#/definitions/StatisticItem' - - $ref: '#/definitions/MapStatisticItem' - - $ref: '#/definitions/RingStatisticItem' - '422': - description: 'Returned when a non-existing statistic name has been requested. Contains an error message' - <<: *commonErrors - - '/servers/{server_id}/search-data': - get: - summary: 'Search the data inside PowerDNS' - description: 'Search the data inside PowerDNS for search_term and return at most max_results. This includes zones, records and comments. The * character can be used in search_term as a wildcard character and the ? character can be used as a wildcard for a single character.' - operationId: searchData - tags: - - search - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: q - in: query - required: true - description: 'The string to search for' - type: string - - name: max - in: query - required: true - description: 'Maximum number of entries to return' - type: integer - - name: object_type - in: query - required: false - description: 'Type of data to search for, one of “all”, “zone”, “record”, “comment”' - type: string - responses: - '200': - description: Returns a JSON array with results - schema: - $ref: '#/definitions/SearchResults' - <<: *commonErrors - - '/servers/{server_id}/zones/{zone_id}/metadata': - get: - summary: 'Get all the Metadata associated with the zone.' - operationId: listMetadata - tags: - - zonemetadata - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - responses: - '200': - description: List of Metadata objects - schema: - type: array - items: - $ref: '#/definitions/Metadata' - <<: *commonErrors - post: - summary: 'Creates a set of metadata entries' - description: 'Creates a set of metadata entries of given kind for the zone. Existing metadata entries for the zone with the same kind are not overwritten.' - operationId: createMetadata - tags: - - zonemetadata - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - - name: metadata - description: Metadata object with list of values to create - required: true - in: body - schema: - $ref: '#/definitions/Metadata' - responses: - '204': - description: OK - <<: *commonErrors - - '/servers/{server_id}/zones/{zone_id}/metadata/{metadata_kind}': - get: - summary: 'Get the content of a single kind of domain metadata as a Metadata object.' - operationId: getMetadata - tags: - - zonemetadata - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - - name: metadata_kind - type: string - in: path - required: true - description: The kind of metadata - responses: - '200': - description: Metadata object with list of values - schema: - $ref: '#/definitions/Metadata' - <<: *commonErrors - put: - summary: 'Replace the content of a single kind of domain metadata.' - description: 'Creates a set of metadata entries of given kind for the zone. Existing metadata entries for the zone with the same kind are removed.' - operationId: modifyMetadata - tags: - - zonemetadata - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - - name: metadata_kind - description: The kind of metadata - required: true - type: string - in: path - - name: metadata - description: metadata to add/create - required: true - in: body - schema: - $ref: '#/definitions/Metadata' - responses: - '200': - description: Metadata object with list of values - schema: - $ref: '#/definitions/Metadata' - <<: *commonErrors - delete: - summary: 'Delete all items of a single kind of domain metadata.' - operationId: deleteMetadata - tags: - - zonemetadata - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - - name: metadata_kind - type: string - in: path - required: true - description: The kind of metadata - responses: - '204': - description: OK - <<: *commonErrors - - '/servers/{server_id}/zones/{zone_id}/cryptokeys': - get: - summary: 'Get all CryptoKeys for a zone, except the privatekey' - operationId: listCryptokeys - tags: - - zonecryptokey - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - responses: - '200': - description: List of Cryptokey objects - schema: - type: array - items: - $ref: '#/definitions/Cryptokey' - <<: *commonErrors - post: - summary: 'Creates a Cryptokey' - description: 'This method adds a new key to a zone. The key can either be generated or imported by supplying the content parameter. if content, bits and algo are null, a key will be generated based on the default-ksk-algorithm and default-ksk-size settings for a KSK and the default-zsk-algorithm and default-zsk-size options for a ZSK.' - operationId: createCryptokey - tags: - - zonecryptokey - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - - name: cryptokey - description: Add a Cryptokey - required: true - in: body - schema: - $ref: '#/definitions/Cryptokey' - responses: - '201': - description: Created - schema: - $ref: '#/definitions/Cryptokey' - <<: *commonErrors - - '/servers/{server_id}/zones/{zone_id}/cryptokeys/{cryptokey_id}': - get: - summary: 'Returns all data about the CryptoKey, including the privatekey.' - operationId: getCryptokey - tags: - - zonecryptokey - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - - name: cryptokey_id - type: string - in: path - required: true - description: 'The id value of the CryptoKey' - responses: - '200': - description: Cryptokey - schema: - $ref: '#/definitions/Cryptokey' - <<: *commonErrors - put: - summary: 'This method (de)activates a key from zone_name specified by cryptokey_id' - operationId: modifyCryptokey - tags: - - zonecryptokey - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - - name: cryptokey_id - description: Cryptokey to manipulate - required: true - in: path - type: string - - name: cryptokey - description: the Cryptokey - required: true - in: body - schema: - $ref: '#/definitions/Cryptokey' - responses: - '204': - description: OK - <<: *commonErrors - delete: - summary: 'This method deletes a key specified by cryptokey_id.' - operationId: deleteCryptokey - tags: - - zonecryptokey - parameters: - - name: server_id - in: path - required: true - description: The id of the server to retrieve - type: string - - name: zone_id - type: string - in: path - required: true - description: The id of the zone to retrieve - - name: cryptokey_id - type: string - in: path - required: true - description: 'The id value of the Cryptokey' - responses: - '204': - description: OK - <<: *commonErrors - - '/servers/{server_id}/tsigkeys': - parameters: - - name: server_id - in: path - required: true - description: 'The id of the server' - type: string - get: - summary: 'Get all TSIGKeys on the server, except the actual key' - operationId: listTSIGKeys - tags: - - tsigkey - responses: - '200': - description: List of TSIGKey objects - schema: - type: array - items: - $ref: '#/definitions/TSIGKey' - <<: *commonErrors - post: - summary: 'Add a TSIG key' - description: 'This methods add a new TSIGKey. The actual key can be generated by the server or be provided by the client' - operationId: createTSIGKey - tags: - - tsigkey - parameters: - - name: tsigkey - description: The TSIGKey to add - required: true - in: body - schema: - $ref: '#/definitions/TSIGKey' - responses: - '201': - description: Created - schema: - $ref: '#/definitions/TSIGKey' - '409': - description: An item with this name already exists - schema: - $ref: '#/definitions/Error' - <<: *commonErrors - - '/servers/{server_id}/tsigkeys/{tsigkey_id}': - parameters: - - name: server_id - in: path - required: true - description: 'The id of the server to retrieve the key from' - type: string - - name: tsigkey_id - in: path - required: true - description: 'The id of the TSIGkey. Should match the "id" field in the TSIGKey object' - type: string - get: - summary: 'Get a specific TSIGKeys on the server, including the actual key' - operationId: getTSIGKey - tags: - - tsigkey - responses: - '200': - description: OK. - schema: - $ref: '#/definitions/TSIGKey' - <<: *commonErrors - put: - description: | - The TSIGKey at tsigkey_id can be changed in multiple ways: - * Changing the Name, this will remove the key with tsigkey_id after adding. - * Changing the Algorithm - * Changing the Key - - Only the relevant fields have to be provided in the request body. - operationId: putTSIGKey - tags: - - tsigkey - parameters: - - name: tsigkey - description: A (possibly stripped down) TSIGKey object with the new values - schema: - $ref: '#/definitions/TSIGKey' - in: body - required: true - responses: - '200': - description: OK. TSIGKey is changed. - schema: - $ref: '#/definitions/TSIGKey' - '409': - description: An item with this name already exists - schema: - $ref: '#/definitions/Error' - <<: *commonErrors - delete: - summary: 'Delete the TSIGKey with tsigkey_id' - operationId: deleteTSIGKey - tags: - - tsigkey - responses: - '204': - description: 'OK, key was deleted' - <<: *commonErrors - - '/servers/{server_id}/autoprimaries': - parameters: - - name: server_id - in: path - required: true - description: 'The id of the server to manage the list of autoprimaries on' - type: string - get: - summary: 'Get a list of autoprimaries' - operationId: getAutoprimaries - tags: - - autoprimary - responses: - '200': - description: OK. - schema: - $ref: '#/definitions/Autoprimary' - <<: *commonErrors - post: - summary: 'Add an autoprimary' - description: 'This methods add a new autoprimary server.' - operationId: createAutoprimary - tags: - - autoprimary - parameters: - - name: autoprimary - description: autoprimary entry to add - required: true - in: body - schema: - $ref: '#/definitions/Autoprimary' - responses: - '201': - description: Created - <<: *commonErrors - - '/servers/{server_id}/autoprimaries/{ip}/{nameserver}': - parameters: - - name: server_id - in: path - required: true - description: 'The id of the server to delete the autoprimary from' - type: string - - name: ip - in: path - required: true - description: 'IP address of autoprimary' - type: string - - name: nameserver - in: path - required: true - description: 'DNS name of the autoprimary' - type: string - delete: - summary: 'Delete the autoprimary entry' - operationId: deleteAutoprimary - tags: - - autoprimary - responses: - '204': - description: 'OK, key was deleted' - <<: *commonErrors - -definitions: - Server: - title: Server - properties: - type: - type: string - description: 'Set to “Server”' - id: - type: string - description: 'The id of the server, “localhost”' - daemon_type: - type: string - description: '“recursor” for the PowerDNS Recursor and “authoritative” for the Authoritative Server' - version: - type: string - description: 'The version of the server software' - url: - type: string - description: 'The API endpoint for this server' - config_url: - type: string - description: 'The API endpoint for this server’s configuration' - zones_url: - type: string - description: 'The API endpoint for this server’s zones' - - Servers: - type: array - items: - $ref: '#/definitions/Server' - - Zone: - title: Zone - description: This represents an authoritative DNS Zone. - properties: - id: - type: string - description: 'Opaque zone id (string), assigned by the server, should not be interpreted by the application. Guaranteed to be safe for embedding in URLs.' - name: - type: string - description: 'Name of the zone (e.g. “example.com.”) MUST have a trailing dot' - type: - type: string - description: 'Set to “Zone”' - url: - type: string - description: 'API endpoint for this zone' - kind: - type: string - enum: - - 'Native' - - 'Master' - - 'Slave' - - 'Producer' - - 'Consumer' - description: 'Zone kind, one of “Native”, “Master”, “Slave”, “Producer”, “Consumer”' - rrsets: - type: array - items: - $ref: '#/definitions/RRSet' - description: 'RRSets in this zone (for zones/{zone_id} endpoint only; omitted during GET on the .../zones list endpoint)' - serial: - type: integer - description: 'The SOA serial number' - notified_serial: - type: integer - description: 'The SOA serial notifications have been sent out for' - edited_serial: - type: integer - description: 'The SOA serial as seen in query responses. Calculated using the SOA-EDIT metadata, default-soa-edit and default-soa-edit-signed settings' - masters: - type: array - items: - type: string - description: ' List of IP addresses configured as a master for this zone (“Slave” type zones only)' - dnssec: - type: boolean - description: 'Whether or not this zone is DNSSEC signed (inferred from presigned being true XOR presence of at least one cryptokey with active being true)' - nsec3param: - type: string - description: 'The NSEC3PARAM record' - nsec3narrow: - type: boolean - description: 'Whether or not the zone uses NSEC3 narrow' - presigned: - type: boolean - description: 'Whether or not the zone is pre-signed' - soa_edit: - type: string - description: 'The SOA-EDIT metadata item' - soa_edit_api: - type: string - description: 'The SOA-EDIT-API metadata item' - api_rectify: - type: boolean - description: 'Whether or not the zone will be rectified on data changes via the API' - zone: - type: string - description: 'MAY contain a BIND-style zone file when creating a zone' - catalog: - type: string - description: 'The catalog this zone is a member of' - account: - type: string - description: 'MAY be set. Its value is defined by local policy' - nameservers: - type: array - items: - type: string - description: 'MAY be sent in client bodies during creation, and MUST NOT be sent by the server. Simple list of strings of nameserver names, including the trailing dot. Not required for slave zones.' - master_tsig_key_ids: - type: array - items: - type: string - description: 'The id of the TSIG keys used for master operation in this zone' - externalDocs: - url: 'https://doc.powerdns.com/authoritative/tsig.html#provisioning-outbound-axfr-access' - slave_tsig_key_ids: - type: array - items: - type: string - description: 'The id of the TSIG keys used for slave operation in this zone' - externalDocs: - url: 'https://doc.powerdns.com/authoritative/tsig.html#provisioning-signed-notification-and-axfr-requests' - - Zones: - type: array - items: - $ref: '#/definitions/Zone' - - RRSet: - title: RRSet - description: This represents a Resource Record Set (all records with the same name and type). - required: - - name - - type - - ttl - - changetype - - records - properties: - name: - type: string - description: 'Name for record set (e.g. “www.powerdns.com.”)' - type: - type: string - description: 'Type of this record (e.g. “A”, “PTR”, “MX”)' - ttl: - type: integer - description: 'DNS TTL of the records, in seconds. MUST NOT be included when changetype is set to “DELETE”.' - changetype: - type: string - description: 'MUST be added when updating the RRSet. Must be REPLACE or DELETE. With DELETE, all existing RRs matching name and type will be deleted, including all comments. With REPLACE: when records is present, all existing RRs matching name and type will be deleted, and then new records given in records will be created. If no records are left, any existing comments will be deleted as well. When comments is present, all existing comments for the RRs matching name and type will be deleted, and then new comments given in comments will be created.' - records: - type: array - description: 'All records in this RRSet. When updating Records, this is the list of new records (replacing the old ones). Must be empty when changetype is set to DELETE. An empty list results in deletion of all records (and comments).' - items: - $ref: '#/definitions/Record' - comments: - type: array - description: 'List of Comment. Must be empty when changetype is set to DELETE. An empty list results in deletion of all comments. modified_at is optional and defaults to the current server time.' - items: - $ref: '#/definitions/Comment' - - Record: - title: Record - description: The RREntry object represents a single record. - required: - - content - properties: - content: - type: string - description: 'The content of this record' - disabled: - type: boolean - description: 'Whether or not this record is disabled. When unset, the record is not disabled' - - Comment: - title: Comment - description: A comment about an RRSet. - properties: - content: - type: string - description: 'The actual comment' - account: - type: string - description: 'Name of an account that added the comment' - modified_at: - type: integer - description: 'Timestamp of the last change to the comment' - - TSIGKey: - title: TSIGKey - description: A TSIG key that can be used to authenticate NOTIFY, AXFR, and DNSUPDATE queries. - properties: - name: - type: string - description: 'The name of the key' - id: - type: string - description: 'The ID for this key, used in the TSIGkey URL endpoint.' - readOnly: true - algorithm: - type: string - description: 'The algorithm of the TSIG key' - key: - type: string - description: 'The Base64 encoded secret key, empty when listing keys. MAY be empty when POSTing to have the server generate the key material' - type: - type: string - description: 'Set to "TSIGKey"' - readOnly: true - - Autoprimary: - title: Autoprimary server - description: An autoprimary server that can provision new domains. - properties: - ip: - type: string - description: "IP address of the autoprimary server" - nameserver: - type: string - description: "DNS name of the autoprimary server" - account: - type: string - description: "Account name for the autoprimary server" - - ConfigSetting: - title: ConfigSetting - properties: - name: - type: string - description: 'set to "ConfigSetting"' - type: - type: string - description: 'The name of this setting (e.g. ‘webserver-port’)' - value: - type: string - description: 'The value of setting name' - - SimpleStatisticItem: - title: SimpleStatisticItem - type: object - properties: - name: - type: string - description: 'Item name' - value: - type: string - description: 'Item value' - - StatisticItem: - title: StatisticItem - properties: - name: - type: string - description: 'Item name' - type: - type: string - description: 'set to "StatisticItem"' - value: - type: string - description: 'Item value' - - MapStatisticItem: - title: MapStatisticItem - properties: - name: - type: string - description: 'Item name' - type: - type: string - description: 'Set to "MapStatisticItem"' - value: - type: array - description: 'Named values' - items: - $ref: '#/definitions/SimpleStatisticItem' - - RingStatisticItem: - title: RingStatisticItem - properties: - name: - type: string - description: 'Item name' - type: - type: string - description: 'Set to "RingStatisticItem"' - size: - type: integer - description: 'Ring size' - value: - type: array - description: 'Named values' - items: - $ref: '#/definitions/SimpleStatisticItem' - - SearchResultZone: - title: SearchResultZone - properties: - name: - type: string - object_type: - type: string - description: 'set to "zone"' - zone_id: - type: string - - SearchResultRecord: - title: SearchResultRecord - properties: - content: - type: string - disabled: - type: boolean - name: - type: string - object_type: - type: string - description: 'set to "record"' - zone_id: - type: string - zone: - type: string - type: - type: string - ttl: - type: integer - - SearchResultComment: - title: SearchResultComment - properties: - content: - type: string - name: - type: string - object_type: - type: string - description: 'set to "comment"' - zone_id: - type: string - zone: - type: string - -# FIXME: This is problematic at the moment, because swagger doesn't support this type of mixed response -# SearchResult: -# anyOf: -# - $ref: '#/definitions/SearchResultZone' -# - $ref: '#/definitions/SearchResultRecord' -# - $ref: '#/definitions/SearchResultComment' - -# Since we can't do 'anyOf' at the moment, we create a 'superset object' - SearchResult: - title: SearchResult - properties: - content: - type: string - disabled: - type: boolean - name: - type: string - object_type: - type: string - description: 'set to one of "record, zone, comment"' - zone_id: - type: string - zone: - type: string - type: - type: string - ttl: - type: integer - - SearchResults: - type: array - items: - $ref: '#/definitions/SearchResult' - - Metadata: - title: Metadata - description: Represents zone metadata - properties: - kind: - type: string - description: 'Name of the metadata' - metadata: - type: array - items: - type: string - description: 'Array with all values for this metadata kind.' - - Cryptokey: - title: Cryptokey - description: 'Describes a DNSSEC cryptographic key' - properties: - type: - type: string - description: 'set to "Cryptokey"' - id: - type: integer - description: 'The internal identifier, read only' - keytype: - type: string - enum: [ksk, zsk, csk] - active: - type: boolean - description: 'Whether or not the key is in active use' - published: - type: boolean - description: 'Whether or not the DNSKEY record is published in the zone' - dnskey: - type: string - description: 'The DNSKEY record for this key' - ds: - type: array - items: - type: string - description: 'An array of DS records for this key' - cds: - type: array - items: - type: string - description: 'An array of DS records for this key, filtered by CDS publication settings' - privatekey: - type: string - description: 'The private key in ISC format' - algorithm: - type: string - description: 'The name of the algorithm of the key, should be a mnemonic' - bits: - type: integer - description: 'The size of the key' - - Error: - title: Error - description: 'Returned when the server encounters an error, either in client input or internally' - properties: - error: - type: string - description: 'A human readable error message' - errors: - type: array - items: - type: string - description: 'Optional array of multiple errors encountered during processing' - required: - - error - - CacheFlushResult: - title: CacheFlushResult - description: 'The result of a cache-flush' - properties: - count: - type: number - description: 'Amount of entries flushed' - result: - type: string - description: 'A message about the result like "Flushed cache"' \ No newline at end of file From 4fc4a7212cdbeda04c6f1cefda70c5d747eed01e Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Tue, 6 May 2025 09:06:20 -0400 Subject: [PATCH 07/49] only fetch zone details when necessary --- .../dns/writer/powerdns/PowerDnsWriter.java | 35 +++++++++++++++---- .../powerdns/client/PowerDNSClient.java | 5 ++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 8ae1a48008f..b2fe76d00a8 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; import org.joda.time.Duration; import org.xbill.DNS.Record; import org.xbill.DNS.Section; @@ -45,6 +46,7 @@ public class PowerDnsWriter extends DnsUpdateWriter { private final String tldZoneName; private final PowerDNSClient powerDnsClient; private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final ConcurrentHashMap zoneIdCache = new ConcurrentHashMap<>(); /** * Class constructor. @@ -183,10 +185,8 @@ private Zone convertUpdateToZone(Update update) throws IOException { && fr.getChangeType() == RRSet.ChangeType.DELETE); }); - // retrieve the TLD zone by name and prepare it for update using the filtered set of - // RRSet records that will be sent to the PowerDNS API - Zone tldZone = getTldZoneByName(); - Zone preparedTldZone = prepareTldZoneForUpdates(tldZone, filteredRRSets); + // prepare a PowerDNS zone object containing the TLD record updates + Zone preparedTldZone = getTldZoneForUpdate(filteredRRSets); // return the prepared TLD zone logger.atInfo().log("Prepared TLD zone %s for PowerDNS: %s", tldZoneName, preparedTldZone); @@ -196,13 +196,13 @@ private Zone convertUpdateToZone(Update update) throws IOException { /** * Prepare the TLD zone for updates by clearing the RRSets and incrementing the serial number. * - * @param tldZone the TLD zone to prepare * @param records the set of RRSet records that will be sent to the PowerDNS API * @return the prepared TLD zone */ - private Zone prepareTldZoneForUpdates(Zone tldZone, List records) { + private Zone getTldZoneForUpdate(List records) { + Zone tldZone = new Zone(); + tldZone.setId(getTldZoneId()); tldZone.setRrsets(records); - tldZone.setEditedSerial(tldZone.getSerial() + 1); return tldZone; } @@ -220,4 +220,25 @@ private Zone getTldZoneByName() throws IOException { } throw new IOException("TLD zone not found: " + tldZoneName); } + + /** + * Get the TLD zone ID for the given TLD zone name from the cache, or compute it if it is not + * present in the cache. + * + * @return the ID of the TLD zone + */ + private String getTldZoneId() { + return zoneIdCache.computeIfAbsent( + tldZoneName, + key -> { + try { + return getTldZoneByName().getId(); + } catch (IOException e) { + // TODO: throw this exception once PowerDNS is available, but for now we are just + // going to return a dummy ID + logger.atWarning().log("Failed to get TLD zone ID for %s: %s", tldZoneName, e); + return String.format("dummy-zone-id-%s", tldZoneName); + } + }); + } } diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java index 3cfa23e52a0..d794978a340 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java @@ -54,7 +54,10 @@ public PowerDNSClient(String baseUrl, String apiKey) { } this.serverId = servers.get(0).getId(); } catch (IOException e) { - this.serverId = "unknown-server-id"; + // TODO: throw this exception once PowerDNS is available, but for now we are just + // going to return a dummy ID + logger.atWarning().log("Failed to get server ID: %s", e); + this.serverId = "dummy-server-id"; } } From 456345403665504ce5cc050d43e5c9f41df540a1 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Tue, 6 May 2025 11:08:51 -0400 Subject: [PATCH 08/49] add reference to PowerDNS OpenAPI spec --- .../dns/writer/powerdns/client/PowerDNSClient.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java index d794978a340..76bb4a39dcc 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java @@ -27,6 +27,18 @@ import okhttp3.RequestBody; import okhttp3.Response; +/** + * A client for the PowerDNS API. + * + *

This class is used to interact with the PowerDNS API. It provides methods to list servers, get + * a server, list zones, get a zone, create a zone, delete a zone, and patch a zone. Based on the PowerDNS + * API spec. + * + *

The server ID is retrieved from the server list and is used to make all subsequent requests. + * + *

The API key is retrieved from the environment variable {@code POWERDNS_API_KEY}. + */ public class PowerDNSClient { // static fields private static final FluentLogger logger = FluentLogger.forEnclosingClass(); From b9e5ff83bc14d37808d610b7f386eba1300fb6f4 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Tue, 6 May 2025 16:53:30 -0400 Subject: [PATCH 09/49] automatically create default TLD definition --- .../writer/powerdns/PowerDnsConfigModule.java | 16 +++- .../dns/writer/powerdns/PowerDnsWriter.java | 76 +++++++++++++++++-- .../powerdns/client/PowerDNSClient.java | 6 +- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java index b9eb16d6a01..e5a1654c607 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java @@ -18,7 +18,7 @@ import dagger.Provides; import google.registry.config.RegistryConfig.Config; -/** Dagger module that provides DNS configuration settings. */ +/** Dagger module that provides PowerDNS configuration settings. */ @Module public class PowerDnsConfigModule { @@ -35,4 +35,18 @@ public static String providePowerDnsHost() { public static String providePowerDnsApiKey() { return "dummy-api-key"; } + + /** Default SOA MNAME for the TLD zone. */ + @Provides + @Config("powerDnsDefaultSoaMName") + public static String providePowerDnsDefaultSoaMName() { + return "a.gtld-servers.net."; + } + + /** Default SOA RNAME for the TLD zone. */ + @Provides + @Config("powerDnsDefaultSoaRName") + public static String providePowerDnsDefaultSoaRName() { + return "nstld.verisign-grs.com."; + } } diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index b2fe76d00a8..5efef768989 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; import org.joda.time.Duration; import org.xbill.DNS.Record; @@ -44,6 +45,8 @@ public class PowerDnsWriter extends DnsUpdateWriter { public static final String NAME = "PowerDnsWriter"; private final String tldZoneName; + private final String powerDnsDefaultSoaMName; + private final String powerDnsDefaultSoaRName; private final PowerDNSClient powerDnsClient; private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final ConcurrentHashMap zoneIdCache = new ConcurrentHashMap<>(); @@ -67,6 +70,8 @@ public PowerDnsWriter( @Config("dnsDefaultDsTtl") Duration dnsDefaultDsTtl, @Config("powerDnsHost") String powerDnsHost, @Config("powerDnsApiKey") String powerDnsApiKey, + @Config("powerDnsDefaultSoaMName") String powerDnsDefaultSoaMName, + @Config("powerDnsDefaultSoaRName") String powerDnsDefaultSoaRName, Clock clock) { // call the DnsUpdateWriter constructor, omitting the transport parameter @@ -74,7 +79,9 @@ public PowerDnsWriter( super(tldZoneName, dnsDefaultATtl, dnsDefaultNsTtl, dnsDefaultDsTtl, null, clock); // Initialize the PowerDNS client - this.tldZoneName = tldZoneName; + this.tldZoneName = getCanonicalHostName(tldZoneName); + this.powerDnsDefaultSoaMName = powerDnsDefaultSoaMName; + this.powerDnsDefaultSoaRName = powerDnsDefaultSoaRName; this.powerDnsClient = new PowerDNSClient(powerDnsHost, powerDnsApiKey); } @@ -86,8 +93,9 @@ public PowerDnsWriter( */ @Override public void publishDomain(String domainName) { - logger.atInfo().log("Staging domain %s for PowerDNS", domainName); - super.publishDomain(domainName); + String normalizedDomainName = getCanonicalHostName(domainName); + logger.atInfo().log("Staging domain %s for PowerDNS", normalizedDomainName); + super.publishDomain(normalizedDomainName); } /** @@ -98,8 +106,9 @@ public void publishDomain(String domainName) { */ @Override public void publishHost(String hostName) { - logger.atInfo().log("Staging host %s for PowerDNS", hostName); - super.publishHost(hostName); + String normalizedHostName = getCanonicalHostName(hostName); + logger.atInfo().log("Staging host %s for PowerDNS", normalizedHostName); + super.publishHost(normalizedHostName); } @Override @@ -115,7 +124,7 @@ protected void commitUnchecked() { // call the PowerDNS API to commit the changes powerDnsClient.patchZone(zone); - } catch (IOException e) { + } catch (Exception e) { throw new RuntimeException("publishDomain failed for TLD: " + tldZoneName, e); } } @@ -193,6 +202,20 @@ private Zone convertUpdateToZone(Update update) throws IOException { return preparedTldZone; } + /** + * Returns the presentation format ending in a dot used for an given hostname. + * + * @param hostName the fully qualified hostname + */ + private String getCanonicalHostName(String hostName) { + String normalizedHostName = hostName.endsWith(".") ? hostName : hostName + '.'; + String canonicalHostName = + tldZoneName == null || normalizedHostName.endsWith(tldZoneName) + ? normalizedHostName + : normalizedHostName + tldZoneName; + return canonicalHostName.toLowerCase(Locale.US); + } + /** * Prepare the TLD zone for updates by clearing the RRSets and incrementing the serial number. * @@ -213,11 +236,50 @@ private Zone getTldZoneForUpdate(List records) { * @throws IOException if the TLD zone is not found */ private Zone getTldZoneByName() throws IOException { + // retrieve an existing TLD zone by name for (Zone zone : powerDnsClient.listZones()) { if (zone.getName().equals(tldZoneName)) { return zone; } } + + // attempt to create a new TLD zone + try { + // base TLD zone object + logger.atInfo().log("Creating new TLD zone %s", tldZoneName); + Zone newTldZone = new Zone(); + newTldZone.setName(tldZoneName); + newTldZone.setKind(Zone.ZoneKind.Native); + + // create an initial SOA record, which may be modified later by an administrator + // or an automated onboarding process + RRSet soaRecord = new RRSet(); + soaRecord.setChangeType(RRSet.ChangeType.REPLACE); + soaRecord.setName(tldZoneName); + soaRecord.setTtl(3600); + soaRecord.setType("SOA"); + + // add content to the SOA record content from default configuration + RecordObject soaRecordContent = new RecordObject(); + soaRecordContent.setContent( + String.format( + "%s %s 1 900 1800 6048000 3600", powerDnsDefaultSoaMName, powerDnsDefaultSoaRName)); + soaRecordContent.setDisabled(false); + soaRecord.setRecords(new ArrayList(Arrays.asList(soaRecordContent))); + + // add the SOA record to the new TLD zone + newTldZone.setRrsets(new ArrayList(Arrays.asList(soaRecord))); + + // create the TLD zone and log the result + Zone createdTldZone = powerDnsClient.createZone(newTldZone); + logger.atInfo().log("Successfully created TLD zone %s", tldZoneName); + return createdTldZone; + } catch (Exception e) { + // log the error and continue + logger.atWarning().log("Failed to create TLD zone %s: %s", tldZoneName, e); + } + + // otherwise, throw an exception throw new IOException("TLD zone not found: " + tldZoneName); } @@ -233,7 +295,7 @@ private String getTldZoneId() { key -> { try { return getTldZoneByName().getId(); - } catch (IOException e) { + } catch (Exception e) { // TODO: throw this exception once PowerDNS is available, but for now we are just // going to return a dummy ID logger.atWarning().log("Failed to get TLD zone ID for %s: %s", tldZoneName, e); diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java index 76bb4a39dcc..f0c74e3ba89 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java @@ -51,9 +51,13 @@ public class PowerDNSClient { private String serverId; public PowerDNSClient(String baseUrl, String apiKey) { - // initialize the base URL, API key, and HTTP client + // initialize the base URL and API key. The base URL should be of the form + // https:///api/v1. An example of a valid API call to the + // localhost to list servers is http://localhost:8081/api/v1/servers this.baseUrl = baseUrl; this.apiKey = apiKey; + + // initialize the base URL, API key, and HTTP client this.httpClient = new OkHttpClient(); this.objectMapper = new ObjectMapper(); From e3ba1ce3f77cae6fab93139992a4a5c535dfb378 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Wed, 7 May 2025 14:42:21 -0400 Subject: [PATCH 10/49] use primary zone configuration and add powerDNS resource files --- .../dns/writer/powerdns/PowerDnsWriter.java | 6 +- .../writer/powerdns/resources/management.sh | 106 ++ .../writer/powerdns/resources/openAPI.yaml | 1398 +++++++++++++++++ .../dns/writer/powerdns/resources/pdns.conf | 42 + 4 files changed, 1550 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/resources/openAPI.yaml create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/resources/pdns.conf diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 5efef768989..3a0c19ba5ad 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -243,13 +243,15 @@ private Zone getTldZoneByName() throws IOException { } } - // attempt to create a new TLD zone + // Attempt to create a new TLD zone if it does not exist. The zone will have a + // basic SOA record, but will not have DNSSEC enabled. Adding DNSSEC is a follow + // up step using pdnsutil command line tool. try { // base TLD zone object logger.atInfo().log("Creating new TLD zone %s", tldZoneName); Zone newTldZone = new Zone(); newTldZone.setName(tldZoneName); - newTldZone.setKind(Zone.ZoneKind.Native); + newTldZone.setKind(Zone.ZoneKind.Master); // create an initial SOA record, which may be modified later by an administrator // or an automated onboarding process diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh b/core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh new file mode 100644 index 00000000000..cfacb716ab7 --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh @@ -0,0 +1,106 @@ +## Create a new zone +# +# The following commands are used to create a new zone in PowerDNS. + +# Specify a TLD name on which to operate. In this example, the TLD is "tldtest1", +# which specifies a zone for SLDs like "mydomain.tldtest1". +ZONE=tldtest1 + +./pdnsutil create-zone $ZONE +./pdnsutil set-kind $ZONE primary +./pdnsutil add-record $ZONE SOA "a.gtld-servers.net. nstld.verisign-grs.com. 1 900 1800 6048000 3600" + +## DNSSEC Configuration +# +# The following commands are used to setup DNSSEC for a given zone. Assumes the +# user is running commands directly on the PowerDNS server. +# + +./pdnsutil add-zone-key $ZONE ksk 2048 active published rsasha256 +./pdnsutil add-zone-key $ZONE zsk 1024 active published rsasha256 +./pdnsutil set-meta $ZONE SOA-EDIT INCREMENT-WEEKS +./pdnsutil set-publish-cdnskey $ZONE +./pdnsutil rectify-zone $ZONE +./pdnsutil show-zone $ZONE + +## Example dig output after DNSSEC enabled +# +# ➜ dig +dnssec tldtest1 @127.0.0.1 +# +# ; <<>> DiG 9.10.6 <<>> +dnssec tldtest1 @127.0.0.1 +# ;; global options: +cmd +# ;; Got answer: +# ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20034 +# ;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1 +# ;; WARNING: recursion requested but not available +# +# ;; OPT PSEUDOSECTION: +# ; EDNS: version: 0, flags: do; udp: 1232 +# ;; QUESTION SECTION: +# ;tldtest1. IN A +# +# ;; AUTHORITY SECTION: +# tldtest1. 3600 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 2025053493 10800 3600 604800 3600 +# tldtest1. 3600 IN RRSIG SOA 8 1 3600 20250515000000 20250424000000 14128 tldtest1. I8E5RB2yADcYJIORd6ZwgBKRiNW7kMcZqO/xA6gTHbOY/SVLgib6wVe5 ohEk6n7VFTHQKz+Yv1VV5yIwI+ctGE2er3lW/r+jPMZ0AGduTUF59s94 Bsz/5Zzzq6gZZTscOtezOBiKjO4V42h99hQxA4x3jIKYs/rO+ijaNmy4 c7A= +# tldtest1. 3600 IN NSEC domain1.tldtest1. SOA RRSIG NSEC DNSKEY CDNSKEY +# tldtest1. 3600 IN RRSIG NSEC 8 1 3600 20250515000000 20250424000000 14128 tldtest1. eld/e7tESam0faZJMyJRR8ldMqPmAkOOnhz3sLUUpfmY6KJKwQWVIBn1 xs0jMhmOXqzFWcukEGT9tDUUGlA5RyhY+ihorwMntu18sHewTKWjTFeb VyNkfze4nExBzCfrgbyl6O9W//68QDKIfB29yf1rOrPszdOwhR90Ko0o BzQ= +# +# ;; Query time: 4 msec +# ;; SERVER: 127.0.0.1#53(127.0.0.1) +# ;; WHEN: Wed May 07 09:42:22 EDT 2025 +# ;; MSG SIZE rcvd: 489 + +## DNSSEC ZSK key rotation (monthly) +# +# The following commands are used to rotate the ZSK for a given zone. +# https://doc.powerdns.com/authoritative/guides/zskroll.html + +# Create an INACTIVE/PUBLISHED key that can begin propagating +./pdnsutil add-zone-key $ZONE zsk 1024 inactive published rsasha256 + +# Show the settings and make note of the key IDs for both the old and +# new ZSKs to be used later. +./pdnsutil show-zone $ZONE + +# Validate with dig that the DNSKEY is available, making note of the TTL +# for this record. Need to wait for both old/new DNSKEY records to be shown +# on secondary DNS servers before proceeding. +dig DNSKEY $ZONE @127.0.0.1 + +# Change activation status for the two ZSKs. The key IDs are found in the +# previous output from show-zone. At this point, the new ZSK will be used +# for signing, but the old key is still available for verification. +./pdnsutil activate-zone-key $ZONE NEW-ZSK-ID +./pdnsutil deactivate-zone-key $ZONE OLD-ZSK-ID + +# Wait for the longest TTL to ensure all secondaries have latest values, +# and then remove the old key. +./pdnsutil remove-zone-key $ZONE OLD-ZSK-ID + +## DNSSEC KSK key rotation (annually) +# +# The following commands are used to rotate the KSK for a given zone. +# https://doc.powerdns.com/authoritative/guides/kskroll.html + +# Create an ACTIVE/PUBLISHED key that can begin propagating +./pdnsutil add-zone-key $ZONE ksk 2048 active published rsasha256 + +# Show the settings and make note of the key IDs for both the old and +# new KSKs to be used later. +./pdnsutil show-zone $ZONE + +# Validate with dig that the DNSKEY is available, making note of the TTL +# for this record. Need to wait for both old/new DNSKEY records to be shown +# on secondary DNS servers before proceeding. +dig DNSKEY $ZONE @127.0.0.1 + +# Now that both old/new KSK values are being used to sign (both are active), +# we can communicate with the gTLD registry to give them a new DS value to +# publish in the parent DNS zone. The output of DS or DNSKEY for the new KSK +# can be provided to the registry, depending on what format they need. Some +# registries may handle this automatically by reading our DNSKEY records, but +# need to verify on a per-registry basis if this is the case. +./pdnsutil show-zone $ZONE + +# After propagation, the old key may be removed. +./pdnsutil remove-zone-key $ZONE OLD-KSK-ID diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/resources/openAPI.yaml b/core/src/main/java/google/registry/dns/writer/powerdns/resources/openAPI.yaml new file mode 100644 index 00000000000..555fe70c2b7 --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/resources/openAPI.yaml @@ -0,0 +1,1398 @@ +swagger: '2.0' +info: + version: "0.0.15" + title: PowerDNS Authoritative HTTP API + license: + name: MIT +basePath: /api/v1 +consumes: + - application/json +produces: + - application/json +securityDefinitions: + # X-API-Key: abcdef12345 + APIKeyHeader: + type: apiKey + in: header + name: X-API-Key +security: + - APIKeyHeader: [] + +# Overall TODOS: +# TODO: Return types are not consistent across documentation +# We need to look at the code and figure out the default HTTP response +# codes and adjust docs accordingly. +paths: + '/error': + get: + summary: Will always generate an error + operationId: error + responses: &commonErrors + '400': + description: The supplied request was not valid + schema: + $ref: '#/definitions/Error' + '404': + description: Requested item was not found + schema: + $ref: '#/definitions/Error' + '422': + description: The input to the operation was not valid + schema: + $ref: '#/definitions/Error' + '500': + description: Internal server error + schema: + $ref: '#/definitions/Error' + + '/servers': + get: + summary: List all servers + operationId: listServers + tags: + - servers + responses: + '200': + description: An array of servers + schema: + type: array + items: + $ref: '#/definitions/Server' + <<: *commonErrors + + '/servers/{server_id}': + get: + summary: List a server + operationId: listServer + tags: + - servers + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + responses: + '200': + description: An server + schema: + $ref: '#/definitions/Server' + <<: *commonErrors + + '/servers/{server_id}/cache/flush': + put: + summary: Flush a cache-entry by name + operationId: cacheFlushByName + tags: + - servers + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: domain + in: query + required: true + description: The domain name to flush from the cache + type: string + responses: + '200': + description: Flush successful + schema: + $ref: '#/definitions/CacheFlushResult' + <<: *commonErrors + + '/servers/{server_id}/zones': + get: + summary: List all Zones in a server + operationId: listZones + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone + in: query + required: false + type: string + description: | + When set to the name of a zone, only this zone is returned. + If no zone with that name exists, the response is an empty array. + This can e.g. be used to check if a zone exists in the database without having to guess/encode the zone's id or to check if a zone exists. + - name: dnssec + in: query + required: false + type: boolean + default: true + description: '“true” (default) or “false”, whether to include the “dnssec” and ”edited_serial” fields in the Zone objects. Setting this to ”false” will make the query a lot faster.' + responses: + '200': + description: An array of Zones + schema: + type: array + items: + $ref: '#/definitions/Zone' + <<: *commonErrors + post: + summary: Creates a new domain, returns the Zone on creation. + operationId: createZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: rrsets + in: query + description: '“true” (default) or “false”, whether to include the “rrsets” in the response Zone object.' + type: boolean + default: true + - name: zone_struct + description: The zone struct to patch with + required: true + in: body + schema: + $ref: '#/definitions/Zone' + responses: + '201': + description: A zone + schema: + $ref: '#/definitions/Zone' + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}': + get: + summary: zone managed by a server + operationId: listZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + - name: rrsets + in: query + description: '“true” (default) or “false”, whether to include the “rrsets” in the response Zone object.' + type: boolean + default: true + - name: rrset_name + in: query + description: Limit output to RRsets for this name. + type: string + - name: rrset_type + in: query + description: Limit output to the RRset of this type. Can only be used together with rrset_name. + type: string + - name: include_disabled + in: query + description: '“true” (default) or “false”, whether to include disabled RRsets in the response.' + type: boolean + responses: + '200': + description: A Zone + schema: + $ref: '#/definitions/Zone' + <<: *commonErrors + delete: + summary: Deletes this zone, all attached metadata and rrsets. + operationId: deleteZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '204': + description: 'Returns 204 No Content on success.' + <<: *commonErrors + patch: + summary: 'Creates/modifies/deletes RRsets present in the payload and their comments. Returns 204 No Content on success.' + operationId: patchZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: zone_struct + description: The zone struct to patch with + required: true + in: body + schema: + $ref: '#/definitions/Zone' + responses: + '204': + description: 'Returns 204 No Content on success.' + <<: *commonErrors + + put: + summary: Modifies basic zone data. + description: 'The only fields in the zone structure which can be modified are: kind, masters, catalog, account, soa_edit, soa_edit_api, api_rectify, dnssec, and nsec3param. All other fields are ignored.' + operationId: putZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: zone_struct + description: The zone struct to patch with + required: true + in: body + schema: + $ref: '#/definitions/Zone' + responses: + '204': + description: 'Returns 204 No Content on success.' + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/notify': + put: + summary: Send a DNS NOTIFY to all slaves. + description: 'Fails when zone kind is not Master or Slave, or master and slave are disabled in the configuration. Only works for Slave if renotify is on. Clients MUST NOT send a body.' + operationId: notifyZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: OK + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/axfr-retrieve': + put: + summary: Retrieve slave zone from its master. + description: 'Fails when zone kind is not Slave, or slave is disabled in the configuration. Clients MUST NOT send a body.' + operationId: axfrRetrieveZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: OK + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/export': + get: + summary: 'Returns the zone in AXFR format.' + operationId: axfrExportZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: OK + schema: + type: string + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/rectify': + put: + summary: 'Rectify the zone data.' + description: 'This does not take into account the API-RECTIFY metadata. Fails on slave zones and zones that do not have DNSSEC.' + operationId: rectifyZone + tags: + - zones + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: OK + schema: + type: string + <<: *commonErrors + + '/servers/{server_id}/config': + get: + summary: 'Returns all ConfigSettings for a single server' + operationId: getConfig + tags: + - config + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + responses: + '200': + description: List of config values + schema: + type: array + items: + $ref: '#/definitions/ConfigSetting' + <<: *commonErrors + + '/servers/{server_id}/config/{config_setting_name}': + get: + summary: 'Returns a specific ConfigSetting for a single server' + description: 'NOT IMPLEMENTED' + operationId: getConfigSetting + tags: + - config + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: config_setting_name + in: path + required: true + description: The name of the setting to retrieve + type: string + responses: + '200': + description: List of config values + schema: + $ref: '#/definitions/ConfigSetting' + <<: *commonErrors + + '/servers/{server_id}/statistics': + get: + summary: 'Query statistics.' + description: 'Query PowerDNS internal statistics.' + operationId: getStats + tags: + - stats + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: statistic + in: query + required: false + type: string + description: | + When set to the name of a specific statistic, only this value is returned. + If no statistic with that name exists, the response has a 422 status and an error message. + - name: includerings + in: query + required: false + type: boolean + default: true + description: '“true” (default) or “false”, whether to include the Ring items, which can contain thousands of log messages or queried domains. Setting this to ”false” may make the response a lot smaller.' + responses: + '200': + description: List of Statistic Items + schema: + type: array + items: + - $ref: '#/definitions/StatisticItem' + - $ref: '#/definitions/MapStatisticItem' + - $ref: '#/definitions/RingStatisticItem' + '422': + description: 'Returned when a non-existing statistic name has been requested. Contains an error message' + <<: *commonErrors + + '/servers/{server_id}/search-data': + get: + summary: 'Search the data inside PowerDNS' + description: 'Search the data inside PowerDNS for search_term and return at most max_results. This includes zones, records and comments. The * character can be used in search_term as a wildcard character and the ? character can be used as a wildcard for a single character.' + operationId: searchData + tags: + - search + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: q + in: query + required: true + description: 'The string to search for' + type: string + - name: max + in: query + required: true + description: 'Maximum number of entries to return' + type: integer + - name: object_type + in: query + required: false + description: 'Type of data to search for, one of “all”, “zone”, “record”, “comment”' + type: string + responses: + '200': + description: Returns a JSON array with results + schema: + $ref: '#/definitions/SearchResults' + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/metadata': + get: + summary: 'Get all the Metadata associated with the zone.' + operationId: listMetadata + tags: + - zonemetadata + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: List of Metadata objects + schema: + type: array + items: + $ref: '#/definitions/Metadata' + <<: *commonErrors + post: + summary: 'Creates a set of metadata entries' + description: 'Creates a set of metadata entries of given kind for the zone. Existing metadata entries for the zone with the same kind are not overwritten.' + operationId: createMetadata + tags: + - zonemetadata + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: metadata + description: Metadata object with list of values to create + required: true + in: body + schema: + $ref: '#/definitions/Metadata' + responses: + '204': + description: OK + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/metadata/{metadata_kind}': + get: + summary: 'Get the content of a single kind of domain metadata as a Metadata object.' + operationId: getMetadata + tags: + - zonemetadata + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + - name: metadata_kind + type: string + in: path + required: true + description: The kind of metadata + responses: + '200': + description: Metadata object with list of values + schema: + $ref: '#/definitions/Metadata' + <<: *commonErrors + put: + summary: 'Replace the content of a single kind of domain metadata.' + description: 'Creates a set of metadata entries of given kind for the zone. Existing metadata entries for the zone with the same kind are removed.' + operationId: modifyMetadata + tags: + - zonemetadata + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: metadata_kind + description: The kind of metadata + required: true + type: string + in: path + - name: metadata + description: metadata to add/create + required: true + in: body + schema: + $ref: '#/definitions/Metadata' + responses: + '200': + description: Metadata object with list of values + schema: + $ref: '#/definitions/Metadata' + <<: *commonErrors + delete: + summary: 'Delete all items of a single kind of domain metadata.' + operationId: deleteMetadata + tags: + - zonemetadata + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + - name: metadata_kind + type: string + in: path + required: true + description: The kind of metadata + responses: + '204': + description: OK + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/cryptokeys': + get: + summary: 'Get all CryptoKeys for a zone, except the privatekey' + operationId: listCryptokeys + tags: + - zonecryptokey + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + responses: + '200': + description: List of Cryptokey objects + schema: + type: array + items: + $ref: '#/definitions/Cryptokey' + <<: *commonErrors + post: + summary: 'Creates a Cryptokey' + description: 'This method adds a new key to a zone. The key can either be generated or imported by supplying the content parameter. if content, bits and algo are null, a key will be generated based on the default-ksk-algorithm and default-ksk-size settings for a KSK and the default-zsk-algorithm and default-zsk-size options for a ZSK.' + operationId: createCryptokey + tags: + - zonecryptokey + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: cryptokey + description: Add a Cryptokey + required: true + in: body + schema: + $ref: '#/definitions/Cryptokey' + responses: + '201': + description: Created + schema: + $ref: '#/definitions/Cryptokey' + <<: *commonErrors + + '/servers/{server_id}/zones/{zone_id}/cryptokeys/{cryptokey_id}': + get: + summary: 'Returns all data about the CryptoKey, including the privatekey.' + operationId: getCryptokey + tags: + - zonecryptokey + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + - name: cryptokey_id + type: string + in: path + required: true + description: 'The id value of the CryptoKey' + responses: + '200': + description: Cryptokey + schema: + $ref: '#/definitions/Cryptokey' + <<: *commonErrors + put: + summary: 'This method (de)activates a key from zone_name specified by cryptokey_id' + operationId: modifyCryptokey + tags: + - zonecryptokey + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + - name: cryptokey_id + description: Cryptokey to manipulate + required: true + in: path + type: string + - name: cryptokey + description: the Cryptokey + required: true + in: body + schema: + $ref: '#/definitions/Cryptokey' + responses: + '204': + description: OK + <<: *commonErrors + delete: + summary: 'This method deletes a key specified by cryptokey_id.' + operationId: deleteCryptokey + tags: + - zonecryptokey + parameters: + - name: server_id + in: path + required: true + description: The id of the server to retrieve + type: string + - name: zone_id + type: string + in: path + required: true + description: The id of the zone to retrieve + - name: cryptokey_id + type: string + in: path + required: true + description: 'The id value of the Cryptokey' + responses: + '204': + description: OK + <<: *commonErrors + + '/servers/{server_id}/tsigkeys': + parameters: + - name: server_id + in: path + required: true + description: 'The id of the server' + type: string + get: + summary: 'Get all TSIGKeys on the server, except the actual key' + operationId: listTSIGKeys + tags: + - tsigkey + responses: + '200': + description: List of TSIGKey objects + schema: + type: array + items: + $ref: '#/definitions/TSIGKey' + <<: *commonErrors + post: + summary: 'Add a TSIG key' + description: 'This methods add a new TSIGKey. The actual key can be generated by the server or be provided by the client' + operationId: createTSIGKey + tags: + - tsigkey + parameters: + - name: tsigkey + description: The TSIGKey to add + required: true + in: body + schema: + $ref: '#/definitions/TSIGKey' + responses: + '201': + description: Created + schema: + $ref: '#/definitions/TSIGKey' + '409': + description: An item with this name already exists + schema: + $ref: '#/definitions/Error' + <<: *commonErrors + + '/servers/{server_id}/tsigkeys/{tsigkey_id}': + parameters: + - name: server_id + in: path + required: true + description: 'The id of the server to retrieve the key from' + type: string + - name: tsigkey_id + in: path + required: true + description: 'The id of the TSIGkey. Should match the "id" field in the TSIGKey object' + type: string + get: + summary: 'Get a specific TSIGKeys on the server, including the actual key' + operationId: getTSIGKey + tags: + - tsigkey + responses: + '200': + description: OK. + schema: + $ref: '#/definitions/TSIGKey' + <<: *commonErrors + put: + description: | + The TSIGKey at tsigkey_id can be changed in multiple ways: + * Changing the Name, this will remove the key with tsigkey_id after adding. + * Changing the Algorithm + * Changing the Key + + Only the relevant fields have to be provided in the request body. + operationId: putTSIGKey + tags: + - tsigkey + parameters: + - name: tsigkey + description: A (possibly stripped down) TSIGKey object with the new values + schema: + $ref: '#/definitions/TSIGKey' + in: body + required: true + responses: + '200': + description: OK. TSIGKey is changed. + schema: + $ref: '#/definitions/TSIGKey' + '409': + description: An item with this name already exists + schema: + $ref: '#/definitions/Error' + <<: *commonErrors + delete: + summary: 'Delete the TSIGKey with tsigkey_id' + operationId: deleteTSIGKey + tags: + - tsigkey + responses: + '204': + description: 'OK, key was deleted' + <<: *commonErrors + + '/servers/{server_id}/autoprimaries': + parameters: + - name: server_id + in: path + required: true + description: 'The id of the server to manage the list of autoprimaries on' + type: string + get: + summary: 'Get a list of autoprimaries' + operationId: getAutoprimaries + tags: + - autoprimary + responses: + '200': + description: OK. + schema: + $ref: '#/definitions/Autoprimary' + <<: *commonErrors + post: + summary: 'Add an autoprimary' + description: 'This methods add a new autoprimary server.' + operationId: createAutoprimary + tags: + - autoprimary + parameters: + - name: autoprimary + description: autoprimary entry to add + required: true + in: body + schema: + $ref: '#/definitions/Autoprimary' + responses: + '201': + description: Created + <<: *commonErrors + + '/servers/{server_id}/autoprimaries/{ip}/{nameserver}': + parameters: + - name: server_id + in: path + required: true + description: 'The id of the server to delete the autoprimary from' + type: string + - name: ip + in: path + required: true + description: 'IP address of autoprimary' + type: string + - name: nameserver + in: path + required: true + description: 'DNS name of the autoprimary' + type: string + delete: + summary: 'Delete the autoprimary entry' + operationId: deleteAutoprimary + tags: + - autoprimary + responses: + '204': + description: 'OK, key was deleted' + <<: *commonErrors + +definitions: + Server: + title: Server + properties: + type: + type: string + description: 'Set to “Server”' + id: + type: string + description: 'The id of the server, “localhost”' + daemon_type: + type: string + description: '“recursor” for the PowerDNS Recursor and “authoritative” for the Authoritative Server' + version: + type: string + description: 'The version of the server software' + url: + type: string + description: 'The API endpoint for this server' + config_url: + type: string + description: 'The API endpoint for this server’s configuration' + zones_url: + type: string + description: 'The API endpoint for this server’s zones' + + Servers: + type: array + items: + $ref: '#/definitions/Server' + + Zone: + title: Zone + description: This represents an authoritative DNS Zone. + properties: + id: + type: string + description: 'Opaque zone id (string), assigned by the server, should not be interpreted by the application. Guaranteed to be safe for embedding in URLs.' + name: + type: string + description: 'Name of the zone (e.g. “example.com.”) MUST have a trailing dot' + type: + type: string + description: 'Set to “Zone”' + url: + type: string + description: 'API endpoint for this zone' + kind: + type: string + enum: + - 'Native' + - 'Master' + - 'Slave' + - 'Producer' + - 'Consumer' + description: 'Zone kind, one of “Native”, “Master”, “Slave”, “Producer”, “Consumer”' + rrsets: + type: array + items: + $ref: '#/definitions/RRSet' + description: 'RRSets in this zone (for zones/{zone_id} endpoint only; omitted during GET on the .../zones list endpoint)' + serial: + type: integer + description: 'The SOA serial number' + notified_serial: + type: integer + description: 'The SOA serial notifications have been sent out for' + edited_serial: + type: integer + description: 'The SOA serial as seen in query responses. Calculated using the SOA-EDIT metadata, default-soa-edit and default-soa-edit-signed settings' + masters: + type: array + items: + type: string + description: ' List of IP addresses configured as a master for this zone (“Slave” type zones only)' + dnssec: + type: boolean + description: 'Whether or not this zone is DNSSEC signed (inferred from presigned being true XOR presence of at least one cryptokey with active being true)' + nsec3param: + type: string + description: 'The NSEC3PARAM record' + nsec3narrow: + type: boolean + description: 'Whether or not the zone uses NSEC3 narrow' + presigned: + type: boolean + description: 'Whether or not the zone is pre-signed' + soa_edit: + type: string + description: 'The SOA-EDIT metadata item' + soa_edit_api: + type: string + description: 'The SOA-EDIT-API metadata item' + api_rectify: + type: boolean + description: 'Whether or not the zone will be rectified on data changes via the API' + zone: + type: string + description: 'MAY contain a BIND-style zone file when creating a zone' + catalog: + type: string + description: 'The catalog this zone is a member of' + account: + type: string + description: 'MAY be set. Its value is defined by local policy' + nameservers: + type: array + items: + type: string + description: 'MAY be sent in client bodies during creation, and MUST NOT be sent by the server. Simple list of strings of nameserver names, including the trailing dot. Not required for slave zones.' + master_tsig_key_ids: + type: array + items: + type: string + description: 'The id of the TSIG keys used for master operation in this zone' + externalDocs: + url: 'https://doc.powerdns.com/authoritative/tsig.html#provisioning-outbound-axfr-access' + slave_tsig_key_ids: + type: array + items: + type: string + description: 'The id of the TSIG keys used for slave operation in this zone' + externalDocs: + url: 'https://doc.powerdns.com/authoritative/tsig.html#provisioning-signed-notification-and-axfr-requests' + + Zones: + type: array + items: + $ref: '#/definitions/Zone' + + RRSet: + title: RRSet + description: This represents a Resource Record Set (all records with the same name and type). + required: + - name + - type + - ttl + - changetype + - records + properties: + name: + type: string + description: 'Name for record set (e.g. “www.powerdns.com.”)' + type: + type: string + description: 'Type of this record (e.g. “A”, “PTR”, “MX”)' + ttl: + type: integer + description: 'DNS TTL of the records, in seconds. MUST NOT be included when changetype is set to “DELETE”.' + changetype: + type: string + description: 'MUST be added when updating the RRSet. Must be REPLACE or DELETE. With DELETE, all existing RRs matching name and type will be deleted, including all comments. With REPLACE: when records is present, all existing RRs matching name and type will be deleted, and then new records given in records will be created. If no records are left, any existing comments will be deleted as well. When comments is present, all existing comments for the RRs matching name and type will be deleted, and then new comments given in comments will be created.' + records: + type: array + description: 'All records in this RRSet. When updating Records, this is the list of new records (replacing the old ones). Must be empty when changetype is set to DELETE. An empty list results in deletion of all records (and comments).' + items: + $ref: '#/definitions/Record' + comments: + type: array + description: 'List of Comment. Must be empty when changetype is set to DELETE. An empty list results in deletion of all comments. modified_at is optional and defaults to the current server time.' + items: + $ref: '#/definitions/Comment' + + Record: + title: Record + description: The RREntry object represents a single record. + required: + - content + properties: + content: + type: string + description: 'The content of this record' + disabled: + type: boolean + description: 'Whether or not this record is disabled. When unset, the record is not disabled' + + Comment: + title: Comment + description: A comment about an RRSet. + properties: + content: + type: string + description: 'The actual comment' + account: + type: string + description: 'Name of an account that added the comment' + modified_at: + type: integer + description: 'Timestamp of the last change to the comment' + + TSIGKey: + title: TSIGKey + description: A TSIG key that can be used to authenticate NOTIFY, AXFR, and DNSUPDATE queries. + properties: + name: + type: string + description: 'The name of the key' + id: + type: string + description: 'The ID for this key, used in the TSIGkey URL endpoint.' + readOnly: true + algorithm: + type: string + description: 'The algorithm of the TSIG key' + key: + type: string + description: 'The Base64 encoded secret key, empty when listing keys. MAY be empty when POSTing to have the server generate the key material' + type: + type: string + description: 'Set to "TSIGKey"' + readOnly: true + + Autoprimary: + title: Autoprimary server + description: An autoprimary server that can provision new domains. + properties: + ip: + type: string + description: "IP address of the autoprimary server" + nameserver: + type: string + description: "DNS name of the autoprimary server" + account: + type: string + description: "Account name for the autoprimary server" + + ConfigSetting: + title: ConfigSetting + properties: + name: + type: string + description: 'set to "ConfigSetting"' + type: + type: string + description: 'The name of this setting (e.g. ‘webserver-port’)' + value: + type: string + description: 'The value of setting name' + + SimpleStatisticItem: + title: SimpleStatisticItem + type: object + properties: + name: + type: string + description: 'Item name' + value: + type: string + description: 'Item value' + + StatisticItem: + title: StatisticItem + properties: + name: + type: string + description: 'Item name' + type: + type: string + description: 'set to "StatisticItem"' + value: + type: string + description: 'Item value' + + MapStatisticItem: + title: MapStatisticItem + properties: + name: + type: string + description: 'Item name' + type: + type: string + description: 'Set to "MapStatisticItem"' + value: + type: array + description: 'Named values' + items: + $ref: '#/definitions/SimpleStatisticItem' + + RingStatisticItem: + title: RingStatisticItem + properties: + name: + type: string + description: 'Item name' + type: + type: string + description: 'Set to "RingStatisticItem"' + size: + type: integer + description: 'Ring size' + value: + type: array + description: 'Named values' + items: + $ref: '#/definitions/SimpleStatisticItem' + + SearchResultZone: + title: SearchResultZone + properties: + name: + type: string + object_type: + type: string + description: 'set to "zone"' + zone_id: + type: string + + SearchResultRecord: + title: SearchResultRecord + properties: + content: + type: string + disabled: + type: boolean + name: + type: string + object_type: + type: string + description: 'set to "record"' + zone_id: + type: string + zone: + type: string + type: + type: string + ttl: + type: integer + + SearchResultComment: + title: SearchResultComment + properties: + content: + type: string + name: + type: string + object_type: + type: string + description: 'set to "comment"' + zone_id: + type: string + zone: + type: string + +# FIXME: This is problematic at the moment, because swagger doesn't support this type of mixed response +# SearchResult: +# anyOf: +# - $ref: '#/definitions/SearchResultZone' +# - $ref: '#/definitions/SearchResultRecord' +# - $ref: '#/definitions/SearchResultComment' + +# Since we can't do 'anyOf' at the moment, we create a 'superset object' + SearchResult: + title: SearchResult + properties: + content: + type: string + disabled: + type: boolean + name: + type: string + object_type: + type: string + description: 'set to one of "record, zone, comment"' + zone_id: + type: string + zone: + type: string + type: + type: string + ttl: + type: integer + + SearchResults: + type: array + items: + $ref: '#/definitions/SearchResult' + + Metadata: + title: Metadata + description: Represents zone metadata + properties: + kind: + type: string + description: 'Name of the metadata' + metadata: + type: array + items: + type: string + description: 'Array with all values for this metadata kind.' + + Cryptokey: + title: Cryptokey + description: 'Describes a DNSSEC cryptographic key' + properties: + type: + type: string + description: 'set to "Cryptokey"' + id: + type: integer + description: 'The internal identifier, read only' + keytype: + type: string + enum: [ksk, zsk, csk] + active: + type: boolean + description: 'Whether or not the key is in active use' + published: + type: boolean + description: 'Whether or not the DNSKEY record is published in the zone' + dnskey: + type: string + description: 'The DNSKEY record for this key' + ds: + type: array + items: + type: string + description: 'An array of DS records for this key' + cds: + type: array + items: + type: string + description: 'An array of DS records for this key, filtered by CDS publication settings' + privatekey: + type: string + description: 'The private key in ISC format' + algorithm: + type: string + description: 'The name of the algorithm of the key, should be a mnemonic' + bits: + type: integer + description: 'The size of the key' + + Error: + title: Error + description: 'Returned when the server encounters an error, either in client input or internally' + properties: + error: + type: string + description: 'A human readable error message' + errors: + type: array + items: + type: string + description: 'Optional array of multiple errors encountered during processing' + required: + - error + + CacheFlushResult: + title: CacheFlushResult + description: 'The result of a cache-flush' + properties: + count: + type: number + description: 'Amount of entries flushed' + result: + type: string + description: 'A message about the result like "Flushed cache"' \ No newline at end of file diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/resources/pdns.conf b/core/src/main/java/google/registry/dns/writer/powerdns/resources/pdns.conf new file mode 100644 index 00000000000..a2fb0d169e4 --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/resources/pdns.conf @@ -0,0 +1,42 @@ +# Minimal PowerDNS config to support DNSSEC using a PostgreSQL backend. A +# production ready configuration would need to customize IP addresses, ports, +# and keys, in addition to any other settings. + +################################# +# PROCESS CONFIG +# +guardian=yes +launch=gpgsql +local-address=127.0.0.1 +local-port=53 + +################################# +# BASE OPERATING MODE CONFIG +# +primary=yes +secondary=no + +################################# +# WEB SERVER CONFIG +# +webserver=yes +webserver-address=127.0.0.1 +webserver-allow-from=127.0.0.1,::1 +webserver-password=dummy-website-password +webserver-port=8081 + +################################# +# API CONFIG +# +api=yes +api-key=dummy-api-key + +################################# +# POSTGRES CONNECTION INFO +# +gpgsql-host=localhost +gpgsql-port=5432 +gpgsql-dbname=powerdns-dev +gpgsql-user=postgres +gpgsql-password= +gpgsql-dnssec=yes From 45d2cf756534de159488587e8aabd396441d0317 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Thu, 8 May 2025 13:50:42 -0400 Subject: [PATCH 11/49] add zone xfer settings --- .../registry/dns/writer/powerdns/resources/pdns.conf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/resources/pdns.conf b/core/src/main/java/google/registry/dns/writer/powerdns/resources/pdns.conf index a2fb0d169e4..1980c0b34d2 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/resources/pdns.conf +++ b/core/src/main/java/google/registry/dns/writer/powerdns/resources/pdns.conf @@ -10,6 +10,14 @@ launch=gpgsql local-address=127.0.0.1 local-port=53 +################################ +# ZONE TRANSFER CONFIG +# +allow-axfr-ips=127.0.0.0/8,::1 +disable-axfr=no +disable-axfr-rectify=no +also-notify=127.0.0.1:54 + ################################# # BASE OPERATING MODE CONFIG # From 38ae70a3df1ba09cca270f6db47eeeaf221f9073 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Tue, 13 May 2025 12:12:31 -0400 Subject: [PATCH 12/49] PowerDNS client updates for dynamic config --- .../registry/config/RegistryConfig.java | 40 +++++++++++ .../config/RegistryConfigSettings.java | 9 +++ .../registry/config/files/default-config.yaml | 6 ++ .../writer/powerdns/PowerDnsConfigModule.java | 52 -------------- .../dns/writer/powerdns/PowerDnsWriter.java | 69 +++++++++---------- .../powerdns/client/PowerDNSClient.java | 63 ++++++++++------- .../writer/powerdns/client/model/Comment.java | 2 + .../writer/powerdns/client/model/RRSet.java | 2 + .../powerdns/client/model/RecordObject.java | 2 + .../writer/powerdns/client/model/Server.java | 2 + .../writer/powerdns/client/model/Zone.java | 5 +- .../registry/module/RequestComponent.java | 2 - .../backend/BackendRequestComponent.java | 2 - .../registry/tools/RegistryToolComponent.java | 2 - 14 files changed, 138 insertions(+), 120 deletions(-) delete mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index f5421975d10..1ca12b52fca 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -130,6 +130,46 @@ public static String provideLocationId(RegistryConfigSettings config) { return config.gcpProject.locationId; } + /** Base URL of the PowerDNS server. */ + @Provides + @Config("powerDnsBaseUrl") + public static String providePowerDnsBaseUrl(RegistryConfigSettings config) { + if (config.powerDns != null && config.powerDns.baseUrl != null) { + return config.powerDns.baseUrl; + } + return "http://localhost:8081/api/v1"; + } + + /** API key for the PowerDNS server. */ + @Provides + @Config("powerDnsApiKey") + public static String providePowerDnsApiKey(RegistryConfigSettings config) { + if (config.powerDns != null && config.powerDns.apiKey != null) { + return config.powerDns.apiKey; + } + return "dummy-api-key"; + } + + /** Default SOA MNAME for the TLD zone. */ + @Provides + @Config("powerDnsDefaultSoaMName") + public static String providePowerDnsDefaultSoaMName(RegistryConfigSettings config) { + if (config.powerDns != null && config.powerDns.defaultSoaMName != null) { + return config.powerDns.defaultSoaMName; + } + return "a.gtld-servers.net."; + } + + /** Default SOA RNAME for the TLD zone. */ + @Provides + @Config("powerDnsDefaultSoaRName") + public static String providePowerDnsDefaultSoaRName(RegistryConfigSettings config) { + if (config.powerDns != null && config.powerDns.defaultSoaRName != null) { + return config.powerDns.defaultSoaRName; + } + return "nstld.verisign-grs.com."; + } + /** * The product name of this specific registry. Used throughout the registrar console. * diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index 32dd08ee84d..b912c268f0f 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -43,6 +43,7 @@ public class RegistryConfigSettings { public DnsUpdate dnsUpdate; public BulkPricingPackageMonitoring bulkPricingPackageMonitoring; public Bsa bsa; + public PowerDns powerDns; /** Configuration options that apply to the entire GCP project. */ public static class GcpProject { @@ -58,6 +59,14 @@ public static class GcpProject { public String baseDomain; } + /** Configuration options for PowerDNS. */ + public static class PowerDns { + public String baseUrl; + public String apiKey; + public String defaultSoaMName; + public String defaultSoaRName; + } + /** Configuration options for authenticating users. */ public static class Auth { public List allowedServiceAccountEmails; diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index 16f5bbd63c9..c580b53132f 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -27,6 +27,12 @@ gcpProject: # The base domain name of the registry service. Services are reachable at [service].baseDomain. baseDomain: registry.test +powerDns: + baseUrl: http://localhost:8081/api/v1 + apiKey: example-api-key + defaultSoaMName: a.example.com. + defaultSoaRName: nstld.example.com. + gSuite: # Publicly accessible domain name of the running G Suite instance. domainName: domain-registry.example diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java deleted file mode 100644 index e5a1654c607..00000000000 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigModule.java +++ /dev/null @@ -1,52 +0,0 @@ -// 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.dns.writer.powerdns; - -import dagger.Module; -import dagger.Provides; -import google.registry.config.RegistryConfig.Config; - -/** Dagger module that provides PowerDNS configuration settings. */ -@Module -public class PowerDnsConfigModule { - - /** Host of the PowerDNS server. */ - @Provides - @Config("powerDnsHost") - public static String providePowerDnsHost() { - return "localhost"; - } - - /** API key for the PowerDNS server. */ - @Provides - @Config("powerDnsApiKey") - public static String providePowerDnsApiKey() { - return "dummy-api-key"; - } - - /** Default SOA MNAME for the TLD zone. */ - @Provides - @Config("powerDnsDefaultSoaMName") - public static String providePowerDnsDefaultSoaMName() { - return "a.gtld-servers.net."; - } - - /** Default SOA RNAME for the TLD zone. */ - @Provides - @Config("powerDnsDefaultSoaRName") - public static String providePowerDnsDefaultSoaRName() { - return "nstld.verisign-grs.com."; - } -} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 3a0c19ba5ad..2b9666a220d 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -58,7 +58,7 @@ public class PowerDnsWriter extends DnsUpdateWriter { * @param dnsDefaultATtl the default TTL for A records * @param dnsDefaultNsTtl the default TTL for NS records * @param dnsDefaultDsTtl the default TTL for DS records - * @param powerDnsHost the host of the PowerDNS server + * @param powerDnsBaseUrl the base URL of the PowerDNS server * @param powerDnsApiKey the API key for the PowerDNS server * @param clock the clock to use for the PowerDNS writer */ @@ -68,7 +68,7 @@ public PowerDnsWriter( @Config("dnsDefaultATtl") Duration dnsDefaultATtl, @Config("dnsDefaultNsTtl") Duration dnsDefaultNsTtl, @Config("dnsDefaultDsTtl") Duration dnsDefaultDsTtl, - @Config("powerDnsHost") String powerDnsHost, + @Config("powerDnsBaseUrl") String powerDnsBaseUrl, @Config("powerDnsApiKey") String powerDnsApiKey, @Config("powerDnsDefaultSoaMName") String powerDnsDefaultSoaMName, @Config("powerDnsDefaultSoaRName") String powerDnsDefaultSoaRName, @@ -82,7 +82,7 @@ public PowerDnsWriter( this.tldZoneName = getCanonicalHostName(tldZoneName); this.powerDnsDefaultSoaMName = powerDnsDefaultSoaMName; this.powerDnsDefaultSoaRName = powerDnsDefaultSoaRName; - this.powerDnsClient = new PowerDNSClient(powerDnsHost, powerDnsApiKey); + this.powerDnsClient = new PowerDNSClient(powerDnsBaseUrl, powerDnsApiKey); } /** @@ -93,7 +93,7 @@ public PowerDnsWriter( */ @Override public void publishDomain(String domainName) { - String normalizedDomainName = getCanonicalHostName(domainName); + String normalizedDomainName = getSanitizedHostName(domainName); logger.atInfo().log("Staging domain %s for PowerDNS", normalizedDomainName); super.publishDomain(normalizedDomainName); } @@ -106,7 +106,7 @@ public void publishDomain(String domainName) { */ @Override public void publishHost(String hostName) { - String normalizedHostName = getCanonicalHostName(hostName); + String normalizedHostName = getSanitizedHostName(hostName); logger.atInfo().log("Staging host %s for PowerDNS", normalizedHostName); super.publishHost(normalizedHostName); } @@ -115,9 +115,7 @@ public void publishHost(String hostName) { protected void commitUnchecked() { try { // persist staged changes to PowerDNS - logger.atInfo().log( - "Committing updates to PowerDNS for TLD %s on server %s", - tldZoneName, powerDnsClient.getServerId()); + logger.atInfo().log("Committing updates to PowerDNS for TLD %s", tldZoneName); // convert the update to a PowerDNS Zone object Zone zone = convertUpdateToZone(update); @@ -125,6 +123,7 @@ protected void commitUnchecked() { // call the PowerDNS API to commit the changes powerDnsClient.patchZone(zone); } catch (Exception e) { + logger.atSevere().withCause(e).log("Commit to PowerDNS failed for TLD: %s", tldZoneName); throw new RuntimeException("publishDomain failed for TLD: " + tldZoneName, e); } } @@ -138,15 +137,17 @@ protected void commitUnchecked() { * @throws IOException if the zone is not found */ private Zone convertUpdateToZone(Update update) throws IOException { + // Convert the Update object to a Zone object + logger.atInfo().log("Converting PowerDNS TLD zone %s update: %s", tldZoneName, update); + // Iterate the update records and prepare them as PowerDNS RRSet objects, referencing the // following source code to determine the usage of the org.xbill.DNS.Record object: // // https://www.javadoc.io/doc/dnsjava/dnsjava/3.2.1/org/xbill/DNS/Record.html // https://github.com/dnsjava/dnsjava/blob/master/src/main/java/org/xbill/DNS/Record.java#L324-L350 ArrayList allRRSets = new ArrayList(); - ArrayList filteredRRSets = new ArrayList(); for (Record r : update.getSection(Section.UPDATE)) { - logger.atInfo().log("Processing TLD zone %s update record: %s", tldZoneName, r); + logger.atInfo().log("Processing PowerDNS TLD zone %s update record: %s", tldZoneName, r); // create the base PowerDNS RRSet object RRSet record = new RRSet(); @@ -172,33 +173,17 @@ private Zone convertUpdateToZone(Update update) throws IOException { record.setChangeType(RRSet.ChangeType.REPLACE); } - // Add record to lists of all and filtered RRSets. The first list is used to track all RRSets - // for the TLD zone, while the second list is used to track the RRSets that will be sent to - // the PowerDNS API. By default, there is a deletion record created by the parent class for - // every domain name and record type combination. However, PowerDNS only expects to see a - // deletion record if the record should be removed from the TLD zone. + // Add record to lists of RRSets allRRSets.add(record); - filteredRRSets.add(record); } - // remove deletion records for a domain if there is a subsequent update enqueued - // for the same domain name and record type combination - allRRSets.stream() - .filter(r -> r.getChangeType() == RRSet.ChangeType.REPLACE) - .forEach( - r -> { - filteredRRSets.removeIf( - fr -> - fr.getName().equals(r.getName()) - && fr.getType().equals(r.getType()) - && fr.getChangeType() == RRSet.ChangeType.DELETE); - }); - // prepare a PowerDNS zone object containing the TLD record updates - Zone preparedTldZone = getTldZoneForUpdate(filteredRRSets); + Zone preparedTldZone = getTldZoneForUpdate(allRRSets); // return the prepared TLD zone - logger.atInfo().log("Prepared TLD zone %s for PowerDNS: %s", tldZoneName, preparedTldZone); + logger.atInfo().log( + "Successfully processed PowerDNS TLD zone %s update record: %s", + tldZoneName, preparedTldZone); return preparedTldZone; } @@ -216,6 +201,17 @@ private String getCanonicalHostName(String hostName) { return canonicalHostName.toLowerCase(Locale.US); } + /** + * Returns the sanitized host name, which is the host name without the trailing dot. + * + * @param hostName the fully qualified hostname + * @return the sanitized host name + */ + private String getSanitizedHostName(String hostName) { + // return the host name without the trailing dot + return hostName.endsWith(".") ? hostName.substring(0, hostName.length() - 1) : hostName; + } + /** * Prepare the TLD zone for updates by clearing the RRSets and incrementing the serial number. * @@ -225,6 +221,7 @@ private String getCanonicalHostName(String hostName) { private Zone getTldZoneForUpdate(List records) { Zone tldZone = new Zone(); tldZone.setId(getTldZoneId()); + tldZone.setName(getSanitizedHostName(tldZoneName)); tldZone.setRrsets(records); return tldZone; } @@ -248,7 +245,7 @@ private Zone getTldZoneByName() throws IOException { // up step using pdnsutil command line tool. try { // base TLD zone object - logger.atInfo().log("Creating new TLD zone %s", tldZoneName); + logger.atInfo().log("Creating new PowerDNS TLD zone %s", tldZoneName); Zone newTldZone = new Zone(); newTldZone.setName(tldZoneName); newTldZone.setKind(Zone.ZoneKind.Master); @@ -274,11 +271,11 @@ private Zone getTldZoneByName() throws IOException { // create the TLD zone and log the result Zone createdTldZone = powerDnsClient.createZone(newTldZone); - logger.atInfo().log("Successfully created TLD zone %s", tldZoneName); + logger.atInfo().log("Successfully created PowerDNS TLD zone %s", tldZoneName); return createdTldZone; } catch (Exception e) { // log the error and continue - logger.atWarning().log("Failed to create TLD zone %s: %s", tldZoneName, e); + logger.atWarning().log("Failed to create PowerDNS TLD zone %s: %s", tldZoneName, e); } // otherwise, throw an exception @@ -300,8 +297,8 @@ private String getTldZoneId() { } catch (Exception e) { // TODO: throw this exception once PowerDNS is available, but for now we are just // going to return a dummy ID - logger.atWarning().log("Failed to get TLD zone ID for %s: %s", tldZoneName, e); - return String.format("dummy-zone-id-%s", tldZoneName); + logger.atWarning().log("Failed to get PowerDNS TLD zone ID for %s: %s", tldZoneName, e); + return tldZoneName; } }); } diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java index f0c74e3ba89..3b4e529fb81 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java @@ -26,6 +26,7 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import okio.Buffer; /** * A client for the PowerDNS API. @@ -40,15 +41,15 @@ *

The API key is retrieved from the environment variable {@code POWERDNS_API_KEY}. */ public class PowerDNSClient { - // static fields - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + // class variables private final OkHttpClient httpClient; private final ObjectMapper objectMapper; private final String baseUrl; private final String apiKey; - // dynamic fields - private String serverId; + // static fields + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static String serverId; public PowerDNSClient(String baseUrl, String apiKey) { // initialize the base URL and API key. The base URL should be of the form @@ -61,27 +62,47 @@ public PowerDNSClient(String baseUrl, String apiKey) { this.httpClient = new OkHttpClient(); this.objectMapper = new ObjectMapper(); - // initialize the Server ID by querying the server list and choosing - // the first entry - try { - List servers = listServers(); - if (servers.isEmpty()) { - throw new IOException("No servers found"); + // initialize the Server ID + initializeServerId(); + } + + private synchronized void initializeServerId() { + if (serverId == null) { + try { + // list the servers and throw an exception if no servers are found + List servers = listServers(); + if (servers.isEmpty()) { + throw new IOException("No servers found"); + } + + // set the server ID to the first server in the list + serverId = servers.get(0).getId(); + } catch (Exception e) { + logger.atWarning().withCause(e).log("Failed to get PowerDNS server ID"); } - this.serverId = servers.get(0).getId(); - } catch (IOException e) { - // TODO: throw this exception once PowerDNS is available, but for now we are just - // going to return a dummy ID - logger.atWarning().log("Failed to get server ID: %s", e); - this.serverId = "dummy-server-id"; + } + } + + private String bodyToString(final RequestBody requestBody) throws IOException { + try (Buffer buffer = new Buffer()) { + if (requestBody != null) requestBody.writeTo(buffer); + else return ""; + return buffer.readUtf8(); } } private Response logAndExecuteRequest(Request request) throws IOException { // log the request and create timestamp for the start time - logger.atInfo().log("Executing PowerDNS request: %s, body: %s", request, request.body()); + logger.atInfo().log( + "Executing PowerDNS request: %s, body: %s", + request, request.body() != null ? bodyToString(request.body()) : null); long startTime = System.currentTimeMillis(); + // validate the server ID is initialized + if (request.url().toString().contains("/servers/null")) { + throw new IOException("Server ID is not initialized"); + } + // execute the request and log the response Response response = httpClient.newCall(request).execute(); logger.atInfo().log("PowerDNS response: %s", response); @@ -126,14 +147,6 @@ public Server getServer() throws IOException { } } - public String getServerId() { - return serverId; - } - - public void setServerId(String serverId) { - this.serverId = serverId; - } - public List listZones() throws IOException { Request request = new Request.Builder() diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java index ba49afa7d01..b10af9ab08b 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Comment.java @@ -14,8 +14,10 @@ package google.registry.dns.writer.powerdns.client.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +@JsonIgnoreProperties(ignoreUnknown = true) public class Comment { @JsonProperty("content") private String content; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java index 0620fb757fd..97d46412cfe 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RRSet.java @@ -14,9 +14,11 @@ package google.registry.dns.writer.powerdns.client.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +@JsonIgnoreProperties(ignoreUnknown = true) public class RRSet { @JsonProperty("name") private String name; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java index 1ad33e4f209..7f3ced1ecf7 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/RecordObject.java @@ -14,8 +14,10 @@ package google.registry.dns.writer.powerdns.client.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +@JsonIgnoreProperties(ignoreUnknown = true) public class RecordObject { @JsonProperty("content") private String content; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java index 024ee40946a..518be6aa120 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Server.java @@ -14,8 +14,10 @@ package google.registry.dns.writer.powerdns.client.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +@JsonIgnoreProperties(ignoreUnknown = true) public class Server { @JsonProperty("type") private String type; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java index ddc406510ad..688981beb3b 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Zone.java @@ -14,9 +14,11 @@ package google.registry.dns.writer.powerdns.client.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +@JsonIgnoreProperties(ignoreUnknown = true) public class Zone { @JsonProperty("id") private String id; @@ -278,7 +280,8 @@ public String toString() { long updatedCount = rrsets.stream().filter(rrset -> rrset.getChangeType() == RRSet.ChangeType.REPLACE).count(); return String.format( - "{id:%s,name:%s,deleted:%d,updated:%d}", id, name, deletedCount, updatedCount); + "{id:%s,name:%s,deleted:%d,updated:%d,total:%d}", + id, name, deletedCount, updatedCount, rrsets.size()); } public enum ZoneKind { diff --git a/core/src/main/java/google/registry/module/RequestComponent.java b/core/src/main/java/google/registry/module/RequestComponent.java index 96f912dbd07..5ea879796a6 100644 --- a/core/src/main/java/google/registry/module/RequestComponent.java +++ b/core/src/main/java/google/registry/module/RequestComponent.java @@ -42,7 +42,6 @@ import google.registry.dns.writer.clouddns.CloudDnsWriterModule; import google.registry.dns.writer.dnsupdate.DnsUpdateConfigModule; import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; -import google.registry.dns.writer.powerdns.PowerDnsConfigModule; import google.registry.dns.writer.powerdns.PowerDnsWriterModule; import google.registry.export.ExportDomainListsAction; import google.registry.export.ExportPremiumTermsAction; @@ -150,7 +149,6 @@ DnsModule.class, DnsUpdateConfigModule.class, DnsUpdateWriterModule.class, - PowerDnsConfigModule.class, PowerDnsWriterModule.class, EppTlsModule.class, EppToolModule.class, diff --git a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java index 4125bce0655..af041fef322 100644 --- a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java +++ b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java @@ -38,7 +38,6 @@ import google.registry.dns.writer.clouddns.CloudDnsWriterModule; import google.registry.dns.writer.dnsupdate.DnsUpdateConfigModule; import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; -import google.registry.dns.writer.powerdns.PowerDnsConfigModule; import google.registry.dns.writer.powerdns.PowerDnsWriterModule; import google.registry.export.ExportDomainListsAction; import google.registry.export.ExportPremiumTermsAction; @@ -91,7 +90,6 @@ DnsModule.class, DnsUpdateConfigModule.class, DnsUpdateWriterModule.class, - PowerDnsConfigModule.class, PowerDnsWriterModule.class, IcannReportingModule.class, RdeModule.class, diff --git a/core/src/main/java/google/registry/tools/RegistryToolComponent.java b/core/src/main/java/google/registry/tools/RegistryToolComponent.java index 5aef100085a..e9215e54266 100644 --- a/core/src/main/java/google/registry/tools/RegistryToolComponent.java +++ b/core/src/main/java/google/registry/tools/RegistryToolComponent.java @@ -26,7 +26,6 @@ import google.registry.dns.writer.VoidDnsWriterModule; import google.registry.dns.writer.clouddns.CloudDnsWriterModule; import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; -import google.registry.dns.writer.powerdns.PowerDnsConfigModule; import google.registry.dns.writer.powerdns.PowerDnsWriterModule; import google.registry.keyring.KeyringModule; import google.registry.keyring.api.KeyModule; @@ -61,7 +60,6 @@ CloudDnsWriterModule.class, CloudTasksUtilsModule.class, DnsUpdateWriterModule.class, - PowerDnsConfigModule.class, PowerDnsWriterModule.class, GsonModule.class, KeyModule.class, From 0a28a124d0212a99b8ef97b4da721a903b600615 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Tue, 13 May 2025 14:10:04 -0400 Subject: [PATCH 13/49] adjust PowerDNS zone update body format --- .../dns/writer/powerdns/PowerDnsWriter.java | 62 ++++++++++++++----- .../powerdns/client/PowerDNSClient.java | 6 +- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 2b9666a220d..0a7a51898e3 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import org.joda.time.Duration; import org.xbill.DNS.Record; import org.xbill.DNS.Section; @@ -149,11 +150,18 @@ private Zone convertUpdateToZone(Update update) throws IOException { for (Record r : update.getSection(Section.UPDATE)) { logger.atInfo().log("Processing PowerDNS TLD zone %s update record: %s", tldZoneName, r); - // create the base PowerDNS RRSet object - RRSet record = new RRSet(); - record.setName(r.getName().toString()); - record.setTtl(r.getTTL()); - record.setType(Type.string(r.getType())); + // find an existing RRSET matching record name and type, or create a new one + // if an existing RRSET is not found + RRSet rrSet = + allRRSets.stream() + .filter( + rrset -> + rrset.getName().equals(r.getName().toString()) + && rrset.getType().equals(Type.string(r.getType()))) + .findFirst() + .orElse( + appendRRSet( + allRRSets, r.getName().toString(), Type.string(r.getType()), r.getTTL())); // determine if this is a record update or a record deletion Boolean isDelete = r.getTTL() == 0 && r.rdataToString().equals(""); @@ -161,24 +169,26 @@ private Zone convertUpdateToZone(Update update) throws IOException { // handle record updates and deletions if (isDelete) { // indicate that this is a record deletion - record.setChangeType(RRSet.ChangeType.DELETE); + rrSet.setChangeType(RRSet.ChangeType.DELETE); } else { + // indicate that this is a record update + rrSet.setChangeType(RRSet.ChangeType.REPLACE); + // add the record content RecordObject recordObject = new RecordObject(); recordObject.setContent(r.rdataToString()); recordObject.setDisabled(false); - record.setRecords(new ArrayList(Arrays.asList(recordObject))); - // indicate that this is a record update - record.setChangeType(RRSet.ChangeType.REPLACE); + // append the record to the RRSet + rrSet.getRecords().add(recordObject); } - - // Add record to lists of RRSets - allRRSets.add(record); } - // prepare a PowerDNS zone object containing the TLD record updates - Zone preparedTldZone = getTldZoneForUpdate(allRRSets); + // prepare a PowerDNS zone object containing the TLD record updates using the RRSet objects + // that have a valid change type + Zone preparedTldZone = + getTldZoneForUpdate( + allRRSets.stream().filter(v -> v.getChangeType() != null).collect(Collectors.toList())); // return the prepared TLD zone logger.atInfo().log( @@ -187,6 +197,30 @@ private Zone convertUpdateToZone(Update update) throws IOException { return preparedTldZone; } + /** + * Create a new RRSet object. + * + * @param rrsets the list of RRSets + * @param name the name of the RRSet + * @param type the type of the RRSet + * @param ttl the TTL of the RRSet + * @return the new RRSet object + */ + private RRSet appendRRSet(List rrsets, String name, String type, long ttl) { + // create the base PowerDNS RRSet object + RRSet rrset = new RRSet(); + rrset.setName(name); + rrset.setType(type); + rrset.setTtl(ttl); + rrset.setRecords(new ArrayList()); + + // add the RRSet to the list of RRSets + rrsets.add(rrset); + + // return the new RRSet object + return rrset; + } + /** * Returns the presentation format ending in a dot used for an given hostname. * diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java index 3b4e529fb81..d185e56d75e 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java @@ -94,8 +94,10 @@ private String bodyToString(final RequestBody requestBody) throws IOException { private Response logAndExecuteRequest(Request request) throws IOException { // log the request and create timestamp for the start time logger.atInfo().log( - "Executing PowerDNS request: %s, body: %s", - request, request.body() != null ? bodyToString(request.body()) : null); + "Executing PowerDNS request: %s, url: %s, body: %s", + request.method(), + request.url(), + request.body() != null ? bodyToString(request.body()) : null); long startTime = System.currentTimeMillis(); // validate the server ID is initialized From c49477d255ce16a658a3b88b59c11cb08c460ade Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Tue, 13 May 2025 16:25:00 -0400 Subject: [PATCH 14/49] only process delete change types when no other updates for a given domain --- .../dns/writer/powerdns/PowerDnsWriter.java | 58 +++++++++++++++++-- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 0a7a51898e3..f97db533ac6 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -49,7 +49,10 @@ public class PowerDnsWriter extends DnsUpdateWriter { private final String powerDnsDefaultSoaMName; private final String powerDnsDefaultSoaRName; private final PowerDNSClient powerDnsClient; + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final ArrayList supportedRecordTypes = + new ArrayList<>(Arrays.asList("DS", "NS")); private static final ConcurrentHashMap zoneIdCache = new ConcurrentHashMap<>(); /** @@ -141,31 +144,64 @@ private Zone convertUpdateToZone(Update update) throws IOException { // Convert the Update object to a Zone object logger.atInfo().log("Converting PowerDNS TLD zone %s update: %s", tldZoneName, update); + // generate a list of records to process + List updateRecordsToProcess = new ArrayList<>(); + for (Record r : update.getSection(Section.UPDATE)) { + // determine if any updates exist for this domain + Boolean isAnyDomainUpdate = + update.getSection(Section.UPDATE).stream() + .anyMatch(record -> record.getName().equals(r.getName()) && !isDeleteRecord(record)); + + // special processing for ANY record deletions + if (isDeleteRecord(r) && Type.string(r.getType()).equals("ANY")) { + // only add a deletion record if there are no other updates for this domain + if (!isAnyDomainUpdate) { + // add a delete record for each of the supported record types + for (String recordType : supportedRecordTypes) { + Record deleteRecord = + Record.newRecord(r.getName(), Type.value(recordType), r.getDClass(), r.getTTL()); + updateRecordsToProcess.add(deleteRecord); + } + } + } else { + // add the record to the list of records to process + updateRecordsToProcess.add(r); + } + } + // Iterate the update records and prepare them as PowerDNS RRSet objects, referencing the // following source code to determine the usage of the org.xbill.DNS.Record object: // // https://www.javadoc.io/doc/dnsjava/dnsjava/3.2.1/org/xbill/DNS/Record.html // https://github.com/dnsjava/dnsjava/blob/master/src/main/java/org/xbill/DNS/Record.java#L324-L350 ArrayList allRRSets = new ArrayList(); - for (Record r : update.getSection(Section.UPDATE)) { - logger.atInfo().log("Processing PowerDNS TLD zone %s update record: %s", tldZoneName, r); + for (Record r : updateRecordsToProcess) { + // skip unsupported record types + if (!supportedRecordTypes.contains(Type.string(r.getType()))) { + logger.atInfo().log( + "Skipping unsupported PowerDNS update record type: %s", Type.string(r.getType())); + continue; + } + + // determine if this is a record update or a record deletion + Boolean isDelete = isDeleteRecord(r); // find an existing RRSET matching record name and type, or create a new one // if an existing RRSET is not found + logger.atInfo().log("Processing PowerDNS TLD zone %s update record: %s", tldZoneName, r); RRSet rrSet = allRRSets.stream() .filter( rrset -> rrset.getName().equals(r.getName().toString()) - && rrset.getType().equals(Type.string(r.getType()))) + && rrset.getType().equals(Type.string(r.getType())) + && ((isDelete && rrset.getChangeType() == RRSet.ChangeType.DELETE) + || (!isDelete && rrset.getChangeType() == RRSet.ChangeType.REPLACE))) .findFirst() .orElse( appendRRSet( allRRSets, r.getName().toString(), Type.string(r.getType()), r.getTTL())); - // determine if this is a record update or a record deletion - Boolean isDelete = r.getTTL() == 0 && r.rdataToString().equals(""); - // handle record updates and deletions if (isDelete) { // indicate that this is a record deletion @@ -336,4 +372,14 @@ private String getTldZoneId() { } }); } + + /** + * Determine if a record is a delete record. + * + * @param r the record to check + * @return true if the record is a delete record, false otherwise + */ + private Boolean isDeleteRecord(Record r) { + return r.getTTL() == 0 && r.rdataToString().equals(""); + } } From 8fb99bc490f893437d42ecb0dc53a951c25daf17 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Tue, 13 May 2025 20:54:37 -0400 Subject: [PATCH 15/49] support A/AAAA glue records --- .../google/registry/dns/writer/powerdns/PowerDnsWriter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index f97db533ac6..f7163359429 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -52,7 +52,7 @@ public class PowerDnsWriter extends DnsUpdateWriter { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final ArrayList supportedRecordTypes = - new ArrayList<>(Arrays.asList("DS", "NS")); + new ArrayList<>(Arrays.asList("A", "AAAA", "DS", "NS")); private static final ConcurrentHashMap zoneIdCache = new ConcurrentHashMap<>(); /** From 5e71300cbbea4c827c1582f625bc6909ac81dce0 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Thu, 15 May 2025 09:49:57 -0400 Subject: [PATCH 16/49] automatic ZSK rotation --- .../registry/config/RegistryConfig.java | 20 +- .../config/RegistryConfigSettings.java | 1 + .../registry/config/files/default-config.yaml | 1 + .../dns/writer/powerdns/PowerDnsWriter.java | 346 +++++++++++++++--- .../powerdns/client/PowerDNSClient.java | 246 ++++++++++++- .../powerdns/client/model/Cryptokey.java | 168 +++++++++ .../powerdns/client/model/Metadata.java | 57 +++ 7 files changed, 778 insertions(+), 61 deletions(-) create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/client/model/Cryptokey.java create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/client/model/Metadata.java diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 1ca12b52fca..9a6cb10037a 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -134,7 +134,7 @@ public static String provideLocationId(RegistryConfigSettings config) { @Provides @Config("powerDnsBaseUrl") public static String providePowerDnsBaseUrl(RegistryConfigSettings config) { - if (config.powerDns != null && config.powerDns.baseUrl != null) { + if (config.powerDns.baseUrl != null) { return config.powerDns.baseUrl; } return "http://localhost:8081/api/v1"; @@ -144,17 +144,27 @@ public static String providePowerDnsBaseUrl(RegistryConfigSettings config) { @Provides @Config("powerDnsApiKey") public static String providePowerDnsApiKey(RegistryConfigSettings config) { - if (config.powerDns != null && config.powerDns.apiKey != null) { + if (config.powerDns.apiKey != null) { return config.powerDns.apiKey; } - return "dummy-api-key"; + return "example-api-key"; + } + + /** Whether DNSSEC is enabled for the PowerDNS server. */ + @Provides + @Config("powerDnsDnssecEnabled") + public static Boolean providePowerDnsDnssecEnabled(RegistryConfigSettings config) { + if (config.powerDns.dnssecEnabled != null) { + return config.powerDns.dnssecEnabled; + } + return false; } /** Default SOA MNAME for the TLD zone. */ @Provides @Config("powerDnsDefaultSoaMName") public static String providePowerDnsDefaultSoaMName(RegistryConfigSettings config) { - if (config.powerDns != null && config.powerDns.defaultSoaMName != null) { + if (config.powerDns.defaultSoaMName != null) { return config.powerDns.defaultSoaMName; } return "a.gtld-servers.net."; @@ -164,7 +174,7 @@ public static String providePowerDnsDefaultSoaMName(RegistryConfigSettings confi @Provides @Config("powerDnsDefaultSoaRName") public static String providePowerDnsDefaultSoaRName(RegistryConfigSettings config) { - if (config.powerDns != null && config.powerDns.defaultSoaRName != null) { + if (config.powerDns.defaultSoaRName != null) { return config.powerDns.defaultSoaRName; } return "nstld.verisign-grs.com."; diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index b912c268f0f..dd1d6c841ac 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -63,6 +63,7 @@ public static class GcpProject { public static class PowerDns { public String baseUrl; public String apiKey; + public Boolean dnssecEnabled; public String defaultSoaMName; public String defaultSoaRName; } diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index c580b53132f..16796d267d0 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -30,6 +30,7 @@ gcpProject: powerDns: baseUrl: http://localhost:8081/api/v1 apiKey: example-api-key + dnssecEnabled: false defaultSoaMName: a.example.com. defaultSoaRName: nstld.example.com. diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index f7163359429..d91ddbd4819 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -14,11 +14,16 @@ package google.registry.dns.writer.powerdns; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; import com.google.common.flogger.FluentLogger; import google.registry.config.RegistryConfig.Config; import google.registry.dns.writer.DnsWriterZone; import google.registry.dns.writer.dnsupdate.DnsUpdateWriter; import google.registry.dns.writer.powerdns.client.PowerDNSClient; +import google.registry.dns.writer.powerdns.client.model.Cryptokey; +import google.registry.dns.writer.powerdns.client.model.Cryptokey.KeyType; +import google.registry.dns.writer.powerdns.client.model.Metadata; import google.registry.dns.writer.powerdns.client.model.RRSet; import google.registry.dns.writer.powerdns.client.model.RecordObject; import google.registry.dns.writer.powerdns.client.model.Zone; @@ -43,17 +48,36 @@ * This request is then converted into a PowerDNS Zone object and sent to the PowerDNS API. */ public class PowerDnsWriter extends DnsUpdateWriter { + // Class configuration public static final String NAME = "PowerDnsWriter"; + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + // PowerDNS configuration private final String tldZoneName; - private final String powerDnsDefaultSoaMName; - private final String powerDnsDefaultSoaRName; + private final String defaultSoaMName; + private final String defaultSoaRName; + private final Boolean dnssecEnabled; private final PowerDNSClient powerDnsClient; - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + // Supported record types to synchronize with PowerDNS private static final ArrayList supportedRecordTypes = new ArrayList<>(Arrays.asList("A", "AAAA", "DS", "NS")); + + // Zone ID cache configuration private static final ConcurrentHashMap zoneIdCache = new ConcurrentHashMap<>(); + private static long zoneIdCacheExpiration = 0; + private static int defaultZoneTtl = 3600; // 1 hour in seconds + + // DNSSEC configuration + private static final String DNSSEC_ALGORITHM = "rsasha256"; + private static final String DNSSEC_SOA_EDIT = "INCREMENT-WEEKS"; + private static final int DNSSEC_KSK_BITS = 2048; + private static final int DNSSEC_ZSK_BITS = 1024; + private static final long DNSSEC_ZSK_EXPIRY_MS = 30L * 24 * 60 * 60 * 1000; // 30 days + private static final long DNSSEC_ZSK_ACTIVATION_MS = + 1000L * 2 * defaultZoneTtl; // twice the default zone TTL in milliseconds + private static final String DNSSEC_ZSK_EXPIRE_FLAG = "DNSSEC-ZSK-EXPIRE-DATE"; + private static final String DNSSEC_ZSK_ACTIVATION_FLAG = "DNSSEC-ZSK-ACTIVATION-DATE"; /** * Class constructor. @@ -76,6 +100,7 @@ public PowerDnsWriter( @Config("powerDnsApiKey") String powerDnsApiKey, @Config("powerDnsDefaultSoaMName") String powerDnsDefaultSoaMName, @Config("powerDnsDefaultSoaRName") String powerDnsDefaultSoaRName, + @Config("powerDnsDnssecEnabled") Boolean powerDnsDnssecEnabled, Clock clock) { // call the DnsUpdateWriter constructor, omitting the transport parameter @@ -84,8 +109,9 @@ public PowerDnsWriter( // Initialize the PowerDNS client this.tldZoneName = getCanonicalHostName(tldZoneName); - this.powerDnsDefaultSoaMName = powerDnsDefaultSoaMName; - this.powerDnsDefaultSoaRName = powerDnsDefaultSoaRName; + this.defaultSoaMName = powerDnsDefaultSoaMName; + this.defaultSoaRName = powerDnsDefaultSoaRName; + this.dnssecEnabled = powerDnsDnssecEnabled; this.powerDnsClient = new PowerDNSClient(powerDnsBaseUrl, powerDnsApiKey); } @@ -257,6 +283,215 @@ private RRSet appendRRSet(List rrsets, String name, String type, long ttl return rrset; } + /** + * Create a new TLD zone for the TLD associated with the PowerDnsWriter. The zone will be created + * with a basic SOA record and not yet configured with DNSSEC. + * + * @return the new TLD zone + * @throws IOException if the TLD zone is not found + */ + private Zone createZone() throws IOException { + // base TLD zone object + logger.atInfo().log("Creating new PowerDNS TLD zone %s", tldZoneName); + Zone newTldZone = new Zone(); + newTldZone.setName(tldZoneName); + newTldZone.setKind(Zone.ZoneKind.Master); + + // create an initial SOA record, which may be modified later by an administrator + // or an automated onboarding process + RRSet soaRecord = new RRSet(); + soaRecord.setChangeType(RRSet.ChangeType.REPLACE); + soaRecord.setName(tldZoneName); + soaRecord.setTtl(defaultZoneTtl); + soaRecord.setType("SOA"); + + // add content to the SOA record content from default configuration + RecordObject soaRecordContent = new RecordObject(); + soaRecordContent.setContent( + String.format( + "%s %s 1 900 1800 6048000 %s", defaultSoaMName, defaultSoaRName, defaultZoneTtl)); + soaRecordContent.setDisabled(false); + soaRecord.setRecords(new ArrayList(Arrays.asList(soaRecordContent))); + + // add the SOA record to the new TLD zone + newTldZone.setRrsets(new ArrayList(Arrays.asList(soaRecord))); + + // create the TLD zone and log the result + Zone createdTldZone = powerDnsClient.createZone(newTldZone); + logger.atInfo().log("Successfully created PowerDNS TLD zone %s", tldZoneName); + + // return the created TLD zone + return createdTldZone; + } + + /** + * Validate the DNSSEC configuration for the TLD zone. If DNSSEC is not enabled, it will be + * enabled and the KSK and ZSK entries will be created. If DNSSEC is enabled, the ZSK expiration + * date will be checked and the ZSK will be rolled over if it has expired. + * + * @param zone the TLD zone to validate + */ + private void validateDnssecConfig(Zone zone) { + // check if DNSSEC configuration is required + if (!dnssecEnabled) { + logger.atInfo().log( + "DNSSEC validation is not required for PowerDNS TLD zone %s", zone.getName()); + return; + } + + try { + // check if DNSSEC is already enabled for the TLD zone + if (!zone.getDnssec()) { + // DNSSEC is not enabled, so we need to enable it + logger.atInfo().log("Enabling DNSSEC for PowerDNS TLD zone %s", zone.getName()); + + // create the KSK and ZSK entries for the TLD zone + powerDnsClient.createCryptokey( + zone.getId(), + Cryptokey.createCryptokey(KeyType.ksk, DNSSEC_KSK_BITS, true, true, DNSSEC_ALGORITHM)); + powerDnsClient.createCryptokey( + zone.getId(), + Cryptokey.createCryptokey(KeyType.zsk, DNSSEC_ZSK_BITS, true, true, DNSSEC_ALGORITHM)); + + // create the SOA-EDIT metadata entry for the TLD zone + powerDnsClient.createMetadata( + zone.getId(), Metadata.createMetadata("SOA-EDIT", Arrays.asList(DNSSEC_SOA_EDIT))); + + // update the zone account field with the expiration timestamp + Zone updatedZone = new Zone(); + updatedZone.setId(zone.getId()); + updatedZone.setApiRectify(true); + updatedZone.setAccount( + String.format( + "%s:%s", + DNSSEC_ZSK_EXPIRE_FLAG, System.currentTimeMillis() + DNSSEC_ZSK_EXPIRY_MS)); + powerDnsClient.putZone(updatedZone); + + // attempt to manually rectify the TLD zone + try { + logger.atInfo().log("Rectifying PowerDNS TLD zone %s", zone.getName()); + powerDnsClient.rectifyZone(zone.getId()); + } catch (Exception rectifyException) { + logger.atWarning().withCause(rectifyException).log( + "Failed to complete rectification of PowerDNS TLD zone %s", zone.getName()); + } + + // retrieve the zone and print the new DS values + logger.atInfo().log("Successfully enabled DNSSEC for PowerDNS TLD zone %s", zone.getName()); + } else { + // DNSSEC is enabled, so we need to validate the configuration + logger.atInfo().log( + "Validating existing DNSSEC configuration for PowerDNS TLD zone %s", zone.getName()); + + // check for a ZSK expiration flag + if (zone.getAccount().contains(DNSSEC_ZSK_EXPIRE_FLAG)) { + // check for an expired ZSK expiration date + String dnssecZskExpireDate = Iterables.get(Splitter.on(':').split(zone.getAccount()), 1); + if (System.currentTimeMillis() > Long.parseLong(dnssecZskExpireDate)) { + // start a ZSK rollover + logger.atInfo().log( + "ZSK has expired, starting rollover for PowerDNS TLD zone %s", zone.getName()); + + // create a new inactive ZSK + powerDnsClient.createCryptokey( + zone.getId(), + Cryptokey.createCryptokey( + KeyType.zsk, DNSSEC_ZSK_BITS, false, true, DNSSEC_ALGORITHM)); + + // update the zone account field with the activation timestamp + Zone updatedZone = new Zone(); + updatedZone.setId(zone.getId()); + updatedZone.setAccount( + String.format( + "%s:%s", + DNSSEC_ZSK_ACTIVATION_FLAG, + System.currentTimeMillis() + DNSSEC_ZSK_ACTIVATION_MS)); + powerDnsClient.putZone(updatedZone); + + // log the rollover event + logger.atInfo().log( + "Successfully started ZSK rollover for PowerDNS TLD zone %s", zone.getName()); + } else { + // ZSK is not expired, so we need to log the current ZSK activation date + logger.atInfo().log( + "DNSSEC configuration for PowerDNS TLD zone %s is valid for another %s seconds", + zone.getName(), + (Long.parseLong(dnssecZskExpireDate) - System.currentTimeMillis()) / 1000); + } + } + + // check for a ZSK rollover key activation flag + else if (zone.getAccount().contains(DNSSEC_ZSK_ACTIVATION_FLAG)) { + // check for a ZSK activation date + String dnssecZskActivationDate = + Iterables.get(Splitter.on(':').split(zone.getAccount()), 1); + if (System.currentTimeMillis() > Long.parseLong(dnssecZskActivationDate)) { + // ZSK activation window has elapsed, so we need to activate the ZSK + logger.atInfo().log( + "ZSK activation window has elapsed, activating ZSK for PowerDNS TLD zone %s", + zone.getName()); + + // list all crypto keys for the TLD zone + List cryptokeys = powerDnsClient.listCryptokeys(zone.getId()); + + // identify the active and inactive ZSKs + Cryptokey activeZsk = + cryptokeys.stream() + .filter(c -> c.getActive() && c.getKeytype() == KeyType.zsk) + .findFirst() + .orElse(null); + Cryptokey inactiveZsk = + cryptokeys.stream() + .filter(c -> !c.getActive() && c.getKeytype() == KeyType.zsk) + .findFirst() + .orElse(null); + + // if both keys are found, complete the ZSK rollover + if (activeZsk != null && inactiveZsk != null) { + // activate the inactive ZSK + inactiveZsk.setActive(true); + powerDnsClient.modifyCryptokey(zone.getId(), inactiveZsk); + + // delete the active ZSK + powerDnsClient.deleteCryptokey(zone.getId(), activeZsk.getId()); + + // update the zone account field with the expiration timestamp + Zone updatedZone = new Zone(); + updatedZone.setId(zone.getId()); + updatedZone.setAccount( + String.format( + "%s:%s", + DNSSEC_ZSK_EXPIRE_FLAG, System.currentTimeMillis() + DNSSEC_ZSK_EXPIRY_MS)); + powerDnsClient.putZone(updatedZone); + + // log the ZSK rollover event + logger.atInfo().log( + "Successfully completed ZSK rollover for PowerDNS TLD zone %s", zone.getName()); + } else { + // unable to complete the ZSK rollover + logger.atSevere().log( + "Unable to locate active and inactive ZSKs for PowerDNS TLD zone %s. Manual" + + " intervention required to complete the ZSK rollover.", + zone.getName()); + return; + } + } else { + // ZSK activation date has not yet elapsed, so we need to log the current ZSK activation + // date + logger.atInfo().log( + "ZSK rollover for PowerDNS TLD zone %s is in progress for another %s seconds", + zone.getName(), + (Long.parseLong(dnssecZskActivationDate) - System.currentTimeMillis()) / 1000); + } + } + } + } catch (Exception e) { + // log the error gracefully and allow processing to continue + logger.atSevere().withCause(e).log( + "Failed to validate DNSSEC configuration for PowerDNS TLD zone %s", zone.getName()); + } + } + /** * Returns the presentation format ending in a dot used for an given hostname. * @@ -288,7 +523,7 @@ private String getSanitizedHostName(String hostName) { * @param records the set of RRSet records that will be sent to the PowerDNS API * @return the prepared TLD zone */ - private Zone getTldZoneForUpdate(List records) { + private Zone getTldZoneForUpdate(List records) throws IOException { Zone tldZone = new Zone(); tldZone.setId(getTldZoneId()); tldZone.setName(getSanitizedHostName(tldZoneName)); @@ -297,52 +532,29 @@ private Zone getTldZoneForUpdate(List records) { } /** - * Get the TLD zone by name. + * Get the TLD zone by name and validate the zone's configuration before returning. * * @return the TLD zone * @throws IOException if the TLD zone is not found */ - private Zone getTldZoneByName() throws IOException { + private Zone getAndValidateTldZoneByName() throws IOException { // retrieve an existing TLD zone by name for (Zone zone : powerDnsClient.listZones()) { - if (zone.getName().equals(tldZoneName)) { + if (getSanitizedHostName(zone.getName()).equals(getSanitizedHostName(tldZoneName))) { + // validate the DNSSEC configuration and return the TLD zone + validateDnssecConfig(zone); return zone; } } - // Attempt to create a new TLD zone if it does not exist. The zone will have a - // basic SOA record, but will not have DNSSEC enabled. Adding DNSSEC is a follow - // up step using pdnsutil command line tool. + // attempt to create a new TLD zone if it does not exist try { - // base TLD zone object - logger.atInfo().log("Creating new PowerDNS TLD zone %s", tldZoneName); - Zone newTldZone = new Zone(); - newTldZone.setName(tldZoneName); - newTldZone.setKind(Zone.ZoneKind.Master); - - // create an initial SOA record, which may be modified later by an administrator - // or an automated onboarding process - RRSet soaRecord = new RRSet(); - soaRecord.setChangeType(RRSet.ChangeType.REPLACE); - soaRecord.setName(tldZoneName); - soaRecord.setTtl(3600); - soaRecord.setType("SOA"); - - // add content to the SOA record content from default configuration - RecordObject soaRecordContent = new RecordObject(); - soaRecordContent.setContent( - String.format( - "%s %s 1 900 1800 6048000 3600", powerDnsDefaultSoaMName, powerDnsDefaultSoaRName)); - soaRecordContent.setDisabled(false); - soaRecord.setRecords(new ArrayList(Arrays.asList(soaRecordContent))); - - // add the SOA record to the new TLD zone - newTldZone.setRrsets(new ArrayList(Arrays.asList(soaRecord))); - - // create the TLD zone and log the result - Zone createdTldZone = powerDnsClient.createZone(newTldZone); - logger.atInfo().log("Successfully created PowerDNS TLD zone %s", tldZoneName); - return createdTldZone; + // create a new TLD zone + Zone zone = createZone(); + + // configure DNSSEC and return the TLD zone + validateDnssecConfig(zone); + return zone; } catch (Exception e) { // log the error and continue logger.atWarning().log("Failed to create PowerDNS TLD zone %s: %s", tldZoneName, e); @@ -354,23 +566,47 @@ private Zone getTldZoneByName() throws IOException { /** * Get the TLD zone ID for the given TLD zone name from the cache, or compute it if it is not - * present in the cache. + * present in the cache. This method is synchronized since it may result in a new TLD zone being + * created and DNSSEC being configured, and this should only happen once. * * @return the ID of the TLD zone */ - private String getTldZoneId() { - return zoneIdCache.computeIfAbsent( - tldZoneName, - key -> { - try { - return getTldZoneByName().getId(); - } catch (Exception e) { - // TODO: throw this exception once PowerDNS is available, but for now we are just - // going to return a dummy ID - logger.atWarning().log("Failed to get PowerDNS TLD zone ID for %s: %s", tldZoneName, e); - return tldZoneName; - } - }); + private synchronized String getTldZoneId() throws IOException { + // clear the cache if it has expired + if (zoneIdCacheExpiration < System.currentTimeMillis()) { + logger.atInfo().log("Clearing PowerDNS TLD zone ID cache"); + zoneIdCache.clear(); + zoneIdCacheExpiration = System.currentTimeMillis() + 1000 * 60 * 60; // 1 hour + } + + // retrieve the TLD zone ID from the cache or retrieve it from the PowerDNS API + // if not available in the cache + String zoneId = + zoneIdCache.computeIfAbsent( + tldZoneName, + key -> { + try { + // retrieve the TLD zone by name, which may result from an existing zone or + // be dynamically created if the zone does not exist + Zone tldZone = getAndValidateTldZoneByName(); + + // return the TLD zone ID, which will be cached for the next hour + return tldZone.getId(); + } catch (IOException e) { + // log the error and return a null value to indicate failure + logger.atWarning().log( + "Failed to get PowerDNS TLD zone ID for %s: %s", tldZoneName, e); + return null; + } + }); + + // if the TLD zone ID is not found, throw an exception + if (zoneId == null) { + throw new IOException("TLD zone not found: " + tldZoneName); + } + + // return the TLD zone ID + return zoneId; } /** diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java index d185e56d75e..07bbff1e8bd 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java @@ -16,11 +16,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.flogger.FluentLogger; +import google.registry.dns.writer.powerdns.client.model.Cryptokey; +import google.registry.dns.writer.powerdns.client.model.Metadata; import google.registry.dns.writer.powerdns.client.model.Server; import google.registry.dns.writer.powerdns.client.model.Zone; import java.io.IOException; import java.util.List; import java.util.Objects; +import java.util.concurrent.TimeUnit; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -59,7 +62,11 @@ public PowerDNSClient(String baseUrl, String apiKey) { this.apiKey = apiKey; // initialize the base URL, API key, and HTTP client - this.httpClient = new OkHttpClient(); + this.httpClient = + new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(300, TimeUnit.SECONDS) + .build(); this.objectMapper = new ObjectMapper(); // initialize the Server ID @@ -119,6 +126,7 @@ private Response logAndExecuteRequest(Request request) throws IOException { return response; } + /** ZONE AND SERVER MANAGEMENT */ public List listServers() throws IOException { Request request = new Request.Builder().url(baseUrl + "/servers").header("X-API-Key", apiKey).get().build(); @@ -235,6 +243,24 @@ public void patchZone(Zone zone) throws IOException { } } + public void putZone(Zone zone) throws IOException { + String json = objectMapper.writeValueAsString(zone); + RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); + + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones/" + zone.getId()) + .header("X-API-Key", apiKey) + .put(body) + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to patch zone: " + response); + } + } + } + public void notifyZone(String zoneId) throws IOException { Request request = new Request.Builder() @@ -249,4 +275,222 @@ public void notifyZone(String zoneId) throws IOException { } } } + + public void rectifyZone(String zoneId) throws IOException { + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones/" + zoneId + "/rectify") + .header("X-API-Key", apiKey) + .put(RequestBody.create("", MediaType.parse("application/json"))) + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to notify zone: " + response); + } + } + } + + /** DNSSEC key management */ + public List listCryptokeys(String zoneId) throws IOException { + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones/" + zoneId + "/cryptokeys") + .header("X-API-Key", apiKey) + .get() + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to list cryptokeys: " + response); + } + return objectMapper.readValue( + Objects.requireNonNull(response.body()).string(), + objectMapper.getTypeFactory().constructCollectionType(List.class, Cryptokey.class)); + } + } + + public Cryptokey createCryptokey(String zoneId, Cryptokey cryptokey) throws IOException { + String json = objectMapper.writeValueAsString(cryptokey); + RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); + + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones/" + zoneId + "/cryptokeys") + .header("X-API-Key", apiKey) + .post(body) + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to create cryptokey: " + response); + } + return objectMapper.readValue( + Objects.requireNonNull(response.body()).string(), Cryptokey.class); + } + } + + public Cryptokey getCryptokey(String zoneId, int cryptokeyId) throws IOException { + Request request = + new Request.Builder() + .url( + baseUrl + + "/servers/" + + serverId + + "/zones/" + + zoneId + + "/cryptokeys/" + + cryptokeyId) + .header("X-API-Key", apiKey) + .get() + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to get cryptokey: " + response); + } + return objectMapper.readValue( + Objects.requireNonNull(response.body()).string(), Cryptokey.class); + } + } + + public void modifyCryptokey(String zoneId, Cryptokey cryptokey) throws IOException { + String json = objectMapper.writeValueAsString(cryptokey); + RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); + + Request request = + new Request.Builder() + .url( + baseUrl + + "/servers/" + + serverId + + "/zones/" + + zoneId + + "/cryptokeys/" + + cryptokey.getId()) + .header("X-API-Key", apiKey) + .put(body) + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to modify cryptokey: " + response); + } + } + } + + public void deleteCryptokey(String zoneId, int cryptokeyId) throws IOException { + Request request = + new Request.Builder() + .url( + baseUrl + + "/servers/" + + serverId + + "/zones/" + + zoneId + + "/cryptokeys/" + + cryptokeyId) + .header("X-API-Key", apiKey) + .delete() + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to delete cryptokey: " + response); + } + } + } + + /** ZONE METADATA MANAGEMENT */ + public List listMetadata(String zoneId) throws IOException { + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones/" + zoneId + "/metadata") + .header("X-API-Key", apiKey) + .get() + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to list metadata: " + response); + } + return objectMapper.readValue( + Objects.requireNonNull(response.body()).string(), + objectMapper.getTypeFactory().constructCollectionType(List.class, Metadata.class)); + } + } + + public void createMetadata(String zoneId, Metadata metadata) throws IOException { + String json = objectMapper.writeValueAsString(metadata); + RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); + + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/zones/" + zoneId + "/metadata") + .header("X-API-Key", apiKey) + .post(body) + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to create metadata: " + response); + } + } + } + + public Metadata getMetadata(String zoneId, String metadataKind) throws IOException { + Request request = + new Request.Builder() + .url( + baseUrl + "/servers/" + serverId + "/zones/" + zoneId + "/metadata/" + metadataKind) + .header("X-API-Key", apiKey) + .get() + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to get metadata: " + response); + } + return objectMapper.readValue( + Objects.requireNonNull(response.body()).string(), Metadata.class); + } + } + + public Metadata modifyMetadata(String zoneId, String metadataKind, Metadata metadata) + throws IOException { + String json = objectMapper.writeValueAsString(metadata); + RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); + + Request request = + new Request.Builder() + .url( + baseUrl + "/servers/" + serverId + "/zones/" + zoneId + "/metadata/" + metadataKind) + .header("X-API-Key", apiKey) + .put(body) + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to modify metadata: " + response); + } + return objectMapper.readValue( + Objects.requireNonNull(response.body()).string(), Metadata.class); + } + } + + public void deleteMetadata(String zoneId, String metadataKind) throws IOException { + Request request = + new Request.Builder() + .url( + baseUrl + "/servers/" + serverId + "/zones/" + zoneId + "/metadata/" + metadataKind) + .header("X-API-Key", apiKey) + .delete() + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to delete metadata: " + response); + } + } + } } diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Cryptokey.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Cryptokey.java new file mode 100644 index 00000000000..ddc4b4facda --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Cryptokey.java @@ -0,0 +1,168 @@ +// 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.dns.writer.powerdns.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Cryptokey { + + public static Cryptokey createCryptokey( + KeyType keytype, int bits, Boolean active, Boolean published, String algorithm) { + Cryptokey k = new Cryptokey(); + k.setKeytype(keytype); + k.setBits(bits); + k.setAlgorithm(algorithm); + k.setActive(active); + k.setPublished(published); + return k; + } + + @JsonProperty("type") + private String type; + + @JsonProperty("id") + private Integer id; + + @JsonProperty("keytype") + private KeyType keytype; + + @JsonProperty("active") + private Boolean active; + + @JsonProperty("published") + private Boolean published; + + @JsonProperty("dnskey") + private String dnskey; + + @JsonProperty("ds") + private List ds; + + @JsonProperty("cds") + private List cds; + + @JsonProperty("privatekey") + private String privatekey; + + @JsonProperty("algorithm") + private String algorithm; + + @JsonProperty("bits") + private Integer bits; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public KeyType getKeytype() { + return keytype; + } + + public void setKeytype(KeyType keytype) { + this.keytype = keytype; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public Boolean getPublished() { + return published; + } + + public void setPublished(Boolean published) { + this.published = published; + } + + public String getDnskey() { + return dnskey; + } + + public void setDnskey(String dnskey) { + this.dnskey = dnskey; + } + + public List getDs() { + return ds; + } + + public void setDs(List ds) { + this.ds = ds; + } + + public List getCds() { + return cds; + } + + public void setCds(List cds) { + this.cds = cds; + } + + public String getPrivatekey() { + return privatekey; + } + + public void setPrivatekey(String privatekey) { + this.privatekey = privatekey; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public Integer getBits() { + return bits; + } + + public void setBits(Integer bits) { + this.bits = bits; + } + + @Override + public String toString() { + return String.format( + "{id:%s,keytype:%s,active:%s,published:%s,algorithm:%s,bits:%s}", + id, keytype, active, published, algorithm, bits); + } + + public enum KeyType { + ksk, + zsk, + csk + } +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Metadata.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Metadata.java new file mode 100644 index 00000000000..1e17f2a81db --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/Metadata.java @@ -0,0 +1,57 @@ +// 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.dns.writer.powerdns.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Metadata { + + public static Metadata createMetadata(String kind, List metadata) { + Metadata m = new Metadata(); + m.setKind(kind); + m.setMetadata(metadata); + return m; + } + + @JsonProperty("kind") + private String kind; + + @JsonProperty("metadata") + private List metadata; + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public List getMetadata() { + return metadata; + } + + public void setMetadata(List metadata) { + this.metadata = metadata; + } + + @Override + public String toString() { + return String.format("{kind:%s,metadata:%s}", kind, metadata); + } +} From db534d36c68623eabdae2f4f100b2e1b18d96b95 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Thu, 15 May 2025 11:35:25 -0400 Subject: [PATCH 17/49] add license header to management example script --- .../dns/writer/powerdns/resources/management.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh b/core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh index cfacb716ab7..c055d7760a3 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh +++ b/core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh @@ -1,3 +1,17 @@ +# Copyright 2025 The Nomulus Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + ## Create a new zone # # The following commands are used to create a new zone in PowerDNS. From fa247e4a3b6bf5f3bb7d2e8bb29bbb2cfaef2deb Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Thu, 15 May 2025 16:30:21 -0400 Subject: [PATCH 18/49] log DS values for DNSSEC config --- .../dns/writer/powerdns/PowerDnsWriter.java | 58 ++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index d91ddbd4819..5f47f731b56 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -346,9 +346,11 @@ private void validateDnssecConfig(Zone zone) { logger.atInfo().log("Enabling DNSSEC for PowerDNS TLD zone %s", zone.getName()); // create the KSK and ZSK entries for the TLD zone - powerDnsClient.createCryptokey( - zone.getId(), - Cryptokey.createCryptokey(KeyType.ksk, DNSSEC_KSK_BITS, true, true, DNSSEC_ALGORITHM)); + Cryptokey newKsk = + powerDnsClient.createCryptokey( + zone.getId(), + Cryptokey.createCryptokey( + KeyType.ksk, DNSSEC_KSK_BITS, true, true, DNSSEC_ALGORITHM)); powerDnsClient.createCryptokey( zone.getId(), Cryptokey.createCryptokey(KeyType.zsk, DNSSEC_ZSK_BITS, true, true, DNSSEC_ALGORITHM)); @@ -377,12 +379,50 @@ private void validateDnssecConfig(Zone zone) { } // retrieve the zone and print the new DS values - logger.atInfo().log("Successfully enabled DNSSEC for PowerDNS TLD zone %s", zone.getName()); + logger.atInfo().log( + "Successfully enabled DNSSEC for PowerDNS TLD zone %s, DS=%s", + zone.getName(), + newKsk.getDs().stream() + .map(ds -> String.format("IN DS %s", ds)) + .collect(Collectors.toList())); } else { // DNSSEC is enabled, so we need to validate the configuration logger.atInfo().log( "Validating existing DNSSEC configuration for PowerDNS TLD zone %s", zone.getName()); + // list all crypto keys for the TLD zone + List cryptokeys = powerDnsClient.listCryptokeys(zone.getId()); + + // identify the KSK and ZSK records + Cryptokey activeZsk = + cryptokeys.stream() + .filter(c -> c.getActive() && c.getKeytype() == KeyType.zsk) + .findFirst() + .orElse(null); + Cryptokey activeKsk = + cryptokeys.stream() + .filter(c -> c.getActive() && c.getKeytype() == KeyType.ksk) + .findFirst() + .orElse(null); + + // validate the KSK and ZSK records are present + if (activeKsk == null || activeZsk == null) { + // log the error and continue + logger.atWarning().log( + "Unable to validate DNSSEC configuration with active KSK and ZSK records for PowerDNS" + + " TLD zone %s", + zone.getName()); + return; + } + + // log the DS records associated with the KSK record + logger.atInfo().log( + "Validated KSK and ZSK records for PowerDNS TLD zone %s, parent DS=%s", + zone.getName(), + activeKsk.getDs().stream() + .map(ds -> String.format("IN DS %s", ds)) + .collect(Collectors.toList())); + // check for a ZSK expiration flag if (zone.getAccount().contains(DNSSEC_ZSK_EXPIRE_FLAG)) { // check for an expired ZSK expiration date @@ -431,15 +471,7 @@ else if (zone.getAccount().contains(DNSSEC_ZSK_ACTIVATION_FLAG)) { "ZSK activation window has elapsed, activating ZSK for PowerDNS TLD zone %s", zone.getName()); - // list all crypto keys for the TLD zone - List cryptokeys = powerDnsClient.listCryptokeys(zone.getId()); - - // identify the active and inactive ZSKs - Cryptokey activeZsk = - cryptokeys.stream() - .filter(c -> c.getActive() && c.getKeytype() == KeyType.zsk) - .findFirst() - .orElse(null); + // identify the inactive ZSK Cryptokey inactiveZsk = cryptokeys.stream() .filter(c -> !c.getActive() && c.getKeytype() == KeyType.zsk) From cfc1b7fdebf8eb483f3b9275c46d109c00a97c91 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Thu, 15 May 2025 16:49:15 -0400 Subject: [PATCH 19/49] clarify DS logging entries --- .../google/registry/dns/writer/powerdns/PowerDnsWriter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 5f47f731b56..5307ec154fc 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -380,7 +380,7 @@ private void validateDnssecConfig(Zone zone) { // retrieve the zone and print the new DS values logger.atInfo().log( - "Successfully enabled DNSSEC for PowerDNS TLD zone %s, DS=%s", + "Successfully enabled DNSSEC for PowerDNS TLD zone %s, expected root DS=%s", zone.getName(), newKsk.getDs().stream() .map(ds -> String.format("IN DS %s", ds)) @@ -417,7 +417,7 @@ private void validateDnssecConfig(Zone zone) { // log the DS records associated with the KSK record logger.atInfo().log( - "Validated KSK and ZSK records for PowerDNS TLD zone %s, parent DS=%s", + "Validated KSK and ZSK records for PowerDNS TLD zone %s, expected root DS=%s", zone.getName(), activeKsk.getDs().stream() .map(ds -> String.format("IN DS %s", ds)) From 50ddac7b122953f89332b0a4350edfd7e028c703 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Fri, 16 May 2025 17:30:51 -0400 Subject: [PATCH 20/49] manage TSIG configuration --- .../dns/writer/powerdns/PowerDnsWriter.java | 78 +++++++++++++++- .../powerdns/client/PowerDNSClient.java | 72 +++++++++++++++ .../writer/powerdns/client/model/TSIGKey.java | 88 +++++++++++++++++++ .../writer/powerdns/resources/management.sh | 17 ++++ 4 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/client/model/TSIGKey.java diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 5307ec154fc..c7d95541abb 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -26,6 +26,7 @@ import google.registry.dns.writer.powerdns.client.model.Metadata; import google.registry.dns.writer.powerdns.client.model.RRSet; import google.registry.dns.writer.powerdns.client.model.RecordObject; +import google.registry.dns.writer.powerdns.client.model.TSIGKey; import google.registry.dns.writer.powerdns.client.model.Zone; import google.registry.util.Clock; import jakarta.inject.Inject; @@ -79,6 +80,10 @@ public class PowerDnsWriter extends DnsUpdateWriter { private static final String DNSSEC_ZSK_EXPIRE_FLAG = "DNSSEC-ZSK-EXPIRE-DATE"; private static final String DNSSEC_ZSK_ACTIVATION_FLAG = "DNSSEC-ZSK-ACTIVATION-DATE"; + // TSIG key configuration + private static final String TSIG_KEY_NAME = "axfr-key"; + private static final String TSIG_KEY_ALGORITHM = "hmac-sha256"; + /** * Class constructor. * @@ -313,8 +318,21 @@ private Zone createZone() throws IOException { soaRecordContent.setDisabled(false); soaRecord.setRecords(new ArrayList(Arrays.asList(soaRecordContent))); - // add the SOA record to the new TLD zone - newTldZone.setRrsets(new ArrayList(Arrays.asList(soaRecord))); + // create NS records, which may be modified later by an administrator + RRSet nsRecord = new RRSet(); + nsRecord.setChangeType(RRSet.ChangeType.REPLACE); + nsRecord.setName(tldZoneName); + nsRecord.setTtl(defaultZoneTtl); + nsRecord.setType("NS"); + + // add content to the NS record content from default configuration + RecordObject nsRecordContent = new RecordObject(); + nsRecordContent.setContent(defaultSoaMName); + nsRecordContent.setDisabled(false); + nsRecord.setRecords(new ArrayList(Arrays.asList(nsRecordContent))); + + // add the SOA and NS record to the new TLD zone + newTldZone.setRrsets(new ArrayList(Arrays.asList(soaRecord, nsRecord))); // create the TLD zone and log the result Zone createdTldZone = powerDnsClient.createZone(newTldZone); @@ -324,6 +342,56 @@ private Zone createZone() throws IOException { return createdTldZone; } + /** + * Validate the TSIG key configuration for the PowerDNS server. Ensures a TSIG key associated with + * the TLD zone is available for use, and detects whether the TLD zone has been configured to use + * the TSIG key during AXFR replication. Instructions are provided in the logs on how to configure + * both the primary and secondary DNS servers with the expected TSIG key. + * + * @param zone the TLD zone to validate + */ + private void validateTsigConfig(Zone zone) throws IOException { + // calculate the zone TSIG key name + logger.atInfo().log("Validating TSIG configuration for PowerDNS TLD zone %s", zone.getName()); + String zoneTsigKeyName = + String.format("%s-%s", TSIG_KEY_NAME, getSanitizedHostName(zone.getName())); + + // validate the named TSIG key is present in the PowerDNS server + try { + // check for existing TSIG key, which throws an exception if it is not found + powerDnsClient.getTSIGKey(zoneTsigKeyName); + } catch (Exception e) { + // create the TSIG key + logger.atInfo().log( + "Creating TSIG key '%s' for PowerDNS TLD zone %s", zoneTsigKeyName, zone.getName()); + powerDnsClient.createTSIGKey(TSIGKey.createTSIGKey(zoneTsigKeyName, TSIG_KEY_ALGORITHM)); + } + logger.atInfo().log( + "Validated TSIG key '%s' (%s) is available for AXFR replication to secondary servers for" + + " TLD zone %s. Retrieve the key using 'pdnsutil list-tsig-keys' in a secure" + + " environment and apply the key to the secondary server configuration.", + zoneTsigKeyName, TSIG_KEY_ALGORITHM, zone.getName()); + + // ensure the TSIG-ALLOW-AXFR metadata is set to the current TSIG key name + try { + Metadata metadata = powerDnsClient.getMetadata(zone.getId(), "TSIG-ALLOW-AXFR"); + // validate the metadata contains the expected TSIG key name + if (!metadata.getMetadata().contains(zoneTsigKeyName)) { + throw new IOException("missing expected TSIG-ALLOW-AXFR value"); + } + logger.atInfo().log( + "Validated PowerDNS TLD zone %s is ready for AXFR replication using TSIG key '%s'", + zone.getName(), zoneTsigKeyName); + } catch (IOException e) { + // log the missing metadata with instructions on how to configure it + logger.atSevere().log( + "PowerDNS TLD zone %s is not configured for AXFR replication using TSIG key '%s'." + + " Configure the replication using 'pdnsutil activate-tsig-key %s %s primary' in a" + + " secure environment.", + zoneTsigKeyName, zone.getName(), zone.getName(), zoneTsigKeyName); + } + } + /** * Validate the DNSSEC configuration for the TLD zone. If DNSSEC is not enabled, it will be * enabled and the KSK and ZSK entries will be created. If DNSSEC is enabled, the ZSK expiration @@ -573,6 +641,9 @@ private Zone getAndValidateTldZoneByName() throws IOException { // retrieve an existing TLD zone by name for (Zone zone : powerDnsClient.listZones()) { if (getSanitizedHostName(zone.getName()).equals(getSanitizedHostName(tldZoneName))) { + // validate the zone's TSIG key configuration + validateTsigConfig(zone); + // validate the DNSSEC configuration and return the TLD zone validateDnssecConfig(zone); return zone; @@ -584,6 +655,9 @@ private Zone getAndValidateTldZoneByName() throws IOException { // create a new TLD zone Zone zone = createZone(); + // validate the zone's TSIG key configuration + validateTsigConfig(zone); + // configure DNSSEC and return the TLD zone validateDnssecConfig(zone); return zone; diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java index 07bbff1e8bd..379d0926c1e 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java @@ -19,6 +19,7 @@ import google.registry.dns.writer.powerdns.client.model.Cryptokey; import google.registry.dns.writer.powerdns.client.model.Metadata; import google.registry.dns.writer.powerdns.client.model.Server; +import google.registry.dns.writer.powerdns.client.model.TSIGKey; import google.registry.dns.writer.powerdns.client.model.Zone; import java.io.IOException; import java.util.List; @@ -493,4 +494,75 @@ public void deleteMetadata(String zoneId, String metadataKind) throws IOExceptio } } } + + /** TSIG KEY MANAGEMENT */ + public List listTSIGKeys() throws IOException { + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/tsigkeys") + .header("X-API-Key", apiKey) + .get() + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to list TSIG keys: " + response); + } + return objectMapper.readValue( + Objects.requireNonNull(response.body()).string(), + objectMapper.getTypeFactory().constructCollectionType(List.class, TSIGKey.class)); + } + } + + public TSIGKey createTSIGKey(TSIGKey tsigKey) throws IOException { + String json = objectMapper.writeValueAsString(tsigKey); + RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); + + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/tsigkeys") + .header("X-API-Key", apiKey) + .post(body) + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to create TSIG key: " + response); + } + return objectMapper.readValue( + Objects.requireNonNull(response.body()).string(), TSIGKey.class); + } + } + + public TSIGKey getTSIGKey(String tsigKeyId) throws IOException { + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/tsigkeys/" + tsigKeyId) + .header("X-API-Key", apiKey) + .get() + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to get TSIG key: " + response); + } + return objectMapper.readValue( + Objects.requireNonNull(response.body()).string(), TSIGKey.class); + } + } + + public void deleteTSIGKey(String tsigKeyId) throws IOException { + Request request = + new Request.Builder() + .url(baseUrl + "/servers/" + serverId + "/tsigkeys/" + tsigKeyId) + .header("X-API-Key", apiKey) + .delete() + .build(); + + try (Response response = logAndExecuteRequest(request)) { + if (!response.isSuccessful()) { + throw new IOException("Failed to delete TSIG key: " + response); + } + } + } } diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/model/TSIGKey.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/TSIGKey.java new file mode 100644 index 00000000000..ac4e5b6680b --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/model/TSIGKey.java @@ -0,0 +1,88 @@ +// 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.dns.writer.powerdns.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class TSIGKey { + @JsonProperty("name") + private String name; + + @JsonProperty("id") + private String id; + + @JsonProperty("algorithm") + private String algorithm; + + @JsonProperty("key") + private String key; + + @JsonProperty("type") + private String type; + + public static TSIGKey createTSIGKey(String name, String algorithm) { + TSIGKey tsigKey = new TSIGKey(); + tsigKey.setName(name); + tsigKey.setAlgorithm(algorithm); + return tsigKey; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public String toString() { + return String.format("{name:%s,id:%s,algorithm:%s,type:%s}", name, id, algorithm, type); + } +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh b/core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh index c055d7760a3..4c805f7a9f1 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh +++ b/core/src/main/java/google/registry/dns/writer/powerdns/resources/management.sh @@ -118,3 +118,20 @@ dig DNSKEY $ZONE @127.0.0.1 # After propagation, the old key may be removed. ./pdnsutil remove-zone-key $ZONE OLD-KSK-ID + +## AXFR Replication Setup on the Primary PowerDNS Server +# +# The following commands are used to configure AXFR replication for a secondary +# PowerDNS server. + +# Assumes the TSIG was already created by PowerDnsWriter +./pdnsutil activate-tsig-key $ZONE axfr-key-$ZONE primary + +## AXFR Replication Setup on the Secondary PowerDNS Server +# +# The following commands are used to configure AXFR replication for a secondary +# PowerDNS server. + +./pdnsutil create-secondary-zone $ZONE $PRIMARY_IP:$PRIMARY_PORT +./pdnsutil import-tsig-key axfr-key-$ZONE hmac-sha256 $TSIG_SHARED_SECRET +./pdnsutil activate-tsig-key $ZONE axfr-key-$ZONE secondary From be6a2b0b1cf09a7ce20d00bea95fe47b809dddfd Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Fri, 16 May 2025 23:35:47 -0400 Subject: [PATCH 21/49] handle root NS server configuration for new TLDs --- .../registry/config/RegistryConfig.java | 34 ++++++------------- .../config/RegistryConfigSettings.java | 4 +-- .../registry/config/files/default-config.yaml | 6 ++-- .../dns/writer/powerdns/PowerDnsWriter.java | 30 ++++++++++------ 4 files changed, 35 insertions(+), 39 deletions(-) diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 9a6cb10037a..cdca443adfd 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -134,50 +134,36 @@ public static String provideLocationId(RegistryConfigSettings config) { @Provides @Config("powerDnsBaseUrl") public static String providePowerDnsBaseUrl(RegistryConfigSettings config) { - if (config.powerDns.baseUrl != null) { - return config.powerDns.baseUrl; - } - return "http://localhost:8081/api/v1"; + return config.powerDns.baseUrl; } /** API key for the PowerDNS server. */ @Provides @Config("powerDnsApiKey") public static String providePowerDnsApiKey(RegistryConfigSettings config) { - if (config.powerDns.apiKey != null) { - return config.powerDns.apiKey; - } - return "example-api-key"; + return config.powerDns.apiKey; } /** Whether DNSSEC is enabled for the PowerDNS server. */ @Provides @Config("powerDnsDnssecEnabled") public static Boolean providePowerDnsDnssecEnabled(RegistryConfigSettings config) { - if (config.powerDns.dnssecEnabled != null) { - return config.powerDns.dnssecEnabled; - } - return false; + return config.powerDns.dnssecEnabled; } /** Default SOA MNAME for the TLD zone. */ @Provides - @Config("powerDnsDefaultSoaMName") - public static String providePowerDnsDefaultSoaMName(RegistryConfigSettings config) { - if (config.powerDns.defaultSoaMName != null) { - return config.powerDns.defaultSoaMName; - } - return "a.gtld-servers.net."; + @Config("powerDnsRootNameServers") + public static ImmutableList providePowerDnsRootNameServers( + RegistryConfigSettings config) { + return ImmutableList.copyOf(config.powerDns.rootNameServers); } /** Default SOA RNAME for the TLD zone. */ @Provides - @Config("powerDnsDefaultSoaRName") - public static String providePowerDnsDefaultSoaRName(RegistryConfigSettings config) { - if (config.powerDns.defaultSoaRName != null) { - return config.powerDns.defaultSoaRName; - } - return "nstld.verisign-grs.com."; + @Config("powerDnsSoaName") + public static String providePowerDnsSoaName(RegistryConfigSettings config) { + return config.powerDns.soaName; } /** diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index dd1d6c841ac..9e27de3e9d5 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -64,8 +64,8 @@ public static class PowerDns { public String baseUrl; public String apiKey; public Boolean dnssecEnabled; - public String defaultSoaMName; - public String defaultSoaRName; + public List rootNameServers; + public String soaName; } /** Configuration options for authenticating users. */ diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index 16796d267d0..4b1dadf8daf 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -31,8 +31,10 @@ powerDns: baseUrl: http://localhost:8081/api/v1 apiKey: example-api-key dnssecEnabled: false - defaultSoaMName: a.example.com. - defaultSoaRName: nstld.example.com. + rootNameServers: + - ns1.example.com. + - ns2.example.com. + soaName: nstld.example.com. gSuite: # Publicly accessible domain name of the running G Suite instance. diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index c7d95541abb..83f430dd65f 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -15,6 +15,7 @@ package google.registry.dns.writer.powerdns; import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.flogger.FluentLogger; import google.registry.config.RegistryConfig.Config; @@ -55,8 +56,8 @@ public class PowerDnsWriter extends DnsUpdateWriter { // PowerDNS configuration private final String tldZoneName; - private final String defaultSoaMName; - private final String defaultSoaRName; + private final ImmutableList rootNameServers; + private final String soaName; private final Boolean dnssecEnabled; private final PowerDNSClient powerDnsClient; @@ -103,8 +104,8 @@ public PowerDnsWriter( @Config("dnsDefaultDsTtl") Duration dnsDefaultDsTtl, @Config("powerDnsBaseUrl") String powerDnsBaseUrl, @Config("powerDnsApiKey") String powerDnsApiKey, - @Config("powerDnsDefaultSoaMName") String powerDnsDefaultSoaMName, - @Config("powerDnsDefaultSoaRName") String powerDnsDefaultSoaRName, + @Config("powerDnsRootNameServers") ImmutableList powerDnsRootNameServers, + @Config("powerDnsSoaName") String powerDnsSoaName, @Config("powerDnsDnssecEnabled") Boolean powerDnsDnssecEnabled, Clock clock) { @@ -114,8 +115,8 @@ public PowerDnsWriter( // Initialize the PowerDNS client this.tldZoneName = getCanonicalHostName(tldZoneName); - this.defaultSoaMName = powerDnsDefaultSoaMName; - this.defaultSoaRName = powerDnsDefaultSoaRName; + this.rootNameServers = powerDnsRootNameServers; + this.soaName = powerDnsSoaName; this.dnssecEnabled = powerDnsDnssecEnabled; this.powerDnsClient = new PowerDNSClient(powerDnsBaseUrl, powerDnsApiKey); } @@ -314,7 +315,7 @@ private Zone createZone() throws IOException { RecordObject soaRecordContent = new RecordObject(); soaRecordContent.setContent( String.format( - "%s %s 1 900 1800 6048000 %s", defaultSoaMName, defaultSoaRName, defaultZoneTtl)); + "%s %s 1 900 1800 6048000 %s", rootNameServers.get(0), soaName, defaultZoneTtl)); soaRecordContent.setDisabled(false); soaRecord.setRecords(new ArrayList(Arrays.asList(soaRecordContent))); @@ -326,10 +327,17 @@ private Zone createZone() throws IOException { nsRecord.setType("NS"); // add content to the NS record content from default configuration - RecordObject nsRecordContent = new RecordObject(); - nsRecordContent.setContent(defaultSoaMName); - nsRecordContent.setDisabled(false); - nsRecord.setRecords(new ArrayList(Arrays.asList(nsRecordContent))); + nsRecord.setRecords( + new ArrayList( + rootNameServers.stream() + .map( + ns -> { + RecordObject nsRecordContent = new RecordObject(); + nsRecordContent.setContent(ns); + nsRecordContent.setDisabled(false); + return nsRecordContent; + }) + .collect(Collectors.toList()))); // add the SOA and NS record to the new TLD zone newTldZone.setRrsets(new ArrayList(Arrays.asList(soaRecord, nsRecord))); From 765454b1f8007eee78f3b40f0042f7f7ed5dc93f Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Mon, 19 May 2025 10:13:28 -0400 Subject: [PATCH 22/49] optional TSIG configuration flag --- .../google/registry/config/RegistryConfig.java | 7 +++++++ .../registry/config/RegistryConfigSettings.java | 1 + .../registry/config/files/default-config.yaml | 1 + .../dns/writer/powerdns/PowerDnsWriter.java | 14 ++++++++++++-- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index cdca443adfd..4e779bd9ef9 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -151,6 +151,13 @@ public static Boolean providePowerDnsDnssecEnabled(RegistryConfigSettings config return config.powerDns.dnssecEnabled; } + /** Whether TSIG is enabled for the PowerDNS server. */ + @Provides + @Config("powerDnsTsigEnabled") + public static Boolean providePowerDnsTsigEnabled(RegistryConfigSettings config) { + return config.powerDns.tsigEnabled; + } + /** Default SOA MNAME for the TLD zone. */ @Provides @Config("powerDnsRootNameServers") diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index 9e27de3e9d5..2d139db779d 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -64,6 +64,7 @@ public static class PowerDns { public String baseUrl; public String apiKey; public Boolean dnssecEnabled; + public Boolean tsigEnabled; public List rootNameServers; public String soaName; } diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index 4b1dadf8daf..43626bcd3f9 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -31,6 +31,7 @@ powerDns: baseUrl: http://localhost:8081/api/v1 apiKey: example-api-key dnssecEnabled: false + tsigEnabled: true rootNameServers: - ns1.example.com. - ns2.example.com. diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index 83f430dd65f..ddb18603738 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -59,6 +59,7 @@ public class PowerDnsWriter extends DnsUpdateWriter { private final ImmutableList rootNameServers; private final String soaName; private final Boolean dnssecEnabled; + private final Boolean tsigEnabled; private final PowerDNSClient powerDnsClient; // Supported record types to synchronize with PowerDNS @@ -82,7 +83,7 @@ public class PowerDnsWriter extends DnsUpdateWriter { private static final String DNSSEC_ZSK_ACTIVATION_FLAG = "DNSSEC-ZSK-ACTIVATION-DATE"; // TSIG key configuration - private static final String TSIG_KEY_NAME = "axfr-key"; + private static final String TSIG_KEY_NAME = "tsig"; private static final String TSIG_KEY_ALGORITHM = "hmac-sha256"; /** @@ -107,6 +108,7 @@ public PowerDnsWriter( @Config("powerDnsRootNameServers") ImmutableList powerDnsRootNameServers, @Config("powerDnsSoaName") String powerDnsSoaName, @Config("powerDnsDnssecEnabled") Boolean powerDnsDnssecEnabled, + @Config("powerDnsTsigEnabled") Boolean powerDnsTsigEnabled, Clock clock) { // call the DnsUpdateWriter constructor, omitting the transport parameter @@ -118,6 +120,7 @@ public PowerDnsWriter( this.rootNameServers = powerDnsRootNameServers; this.soaName = powerDnsSoaName; this.dnssecEnabled = powerDnsDnssecEnabled; + this.tsigEnabled = powerDnsTsigEnabled; this.powerDnsClient = new PowerDNSClient(powerDnsBaseUrl, powerDnsApiKey); } @@ -359,10 +362,17 @@ private Zone createZone() throws IOException { * @param zone the TLD zone to validate */ private void validateTsigConfig(Zone zone) throws IOException { + // check if TSIG configuration is required + if (!tsigEnabled) { + logger.atInfo().log( + "TSIG validation is not required for PowerDNS TLD zone %s", zone.getName()); + return; + } + // calculate the zone TSIG key name logger.atInfo().log("Validating TSIG configuration for PowerDNS TLD zone %s", zone.getName()); String zoneTsigKeyName = - String.format("%s-%s", TSIG_KEY_NAME, getSanitizedHostName(zone.getName())); + String.format("%s-%s", getSanitizedHostName(zone.getName()), TSIG_KEY_NAME); // validate the named TSIG key is present in the PowerDNS server try { From 72dc3b6ac931b7b4d97ca65effa69d35dbcdfb43 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Fri, 30 May 2025 16:48:40 -0400 Subject: [PATCH 23/49] modularize PowerDNS config settings --- .../registry/config/RegistryConfig.java | 43 ------ .../config/RegistryConfigSettings.java | 11 -- .../registry/config/files/default-config.yaml | 10 -- .../config/files/power-dns/default.yaml | 10 ++ .../config/files/power-dns/env-alpha.yaml | 1 + .../config/files/power-dns/env-crash.yaml | 1 + .../config/files/power-dns/env-local.yaml | 1 + .../files/power-dns/env-production.yaml | 1 + .../config/files/power-dns/env-qa.yaml | 1 + .../config/files/power-dns/env-sandbox.yaml | 1 + .../config/files/power-dns/env-unittest.yaml | 10 ++ .../dns/writer/powerdns/PowerDnsConfig.java | 129 ++++++++++++++++++ .../powerdns/PowerDnsConfigSettings.java | 32 +++++ .../registry/module/RegistryComponent.java | 2 + .../module/backend/BackendComponent.java | 2 + 15 files changed, 191 insertions(+), 64 deletions(-) create mode 100644 core/src/main/java/google/registry/config/files/power-dns/default.yaml create mode 100644 core/src/main/java/google/registry/config/files/power-dns/env-alpha.yaml create mode 100644 core/src/main/java/google/registry/config/files/power-dns/env-crash.yaml create mode 100644 core/src/main/java/google/registry/config/files/power-dns/env-local.yaml create mode 100644 core/src/main/java/google/registry/config/files/power-dns/env-production.yaml create mode 100644 core/src/main/java/google/registry/config/files/power-dns/env-qa.yaml create mode 100644 core/src/main/java/google/registry/config/files/power-dns/env-sandbox.yaml create mode 100644 core/src/main/java/google/registry/config/files/power-dns/env-unittest.yaml create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfig.java create mode 100644 core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigSettings.java diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 4e779bd9ef9..f5421975d10 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -130,49 +130,6 @@ public static String provideLocationId(RegistryConfigSettings config) { return config.gcpProject.locationId; } - /** Base URL of the PowerDNS server. */ - @Provides - @Config("powerDnsBaseUrl") - public static String providePowerDnsBaseUrl(RegistryConfigSettings config) { - return config.powerDns.baseUrl; - } - - /** API key for the PowerDNS server. */ - @Provides - @Config("powerDnsApiKey") - public static String providePowerDnsApiKey(RegistryConfigSettings config) { - return config.powerDns.apiKey; - } - - /** Whether DNSSEC is enabled for the PowerDNS server. */ - @Provides - @Config("powerDnsDnssecEnabled") - public static Boolean providePowerDnsDnssecEnabled(RegistryConfigSettings config) { - return config.powerDns.dnssecEnabled; - } - - /** Whether TSIG is enabled for the PowerDNS server. */ - @Provides - @Config("powerDnsTsigEnabled") - public static Boolean providePowerDnsTsigEnabled(RegistryConfigSettings config) { - return config.powerDns.tsigEnabled; - } - - /** Default SOA MNAME for the TLD zone. */ - @Provides - @Config("powerDnsRootNameServers") - public static ImmutableList providePowerDnsRootNameServers( - RegistryConfigSettings config) { - return ImmutableList.copyOf(config.powerDns.rootNameServers); - } - - /** Default SOA RNAME for the TLD zone. */ - @Provides - @Config("powerDnsSoaName") - public static String providePowerDnsSoaName(RegistryConfigSettings config) { - return config.powerDns.soaName; - } - /** * The product name of this specific registry. Used throughout the registrar console. * diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index 2d139db779d..32dd08ee84d 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -43,7 +43,6 @@ public class RegistryConfigSettings { public DnsUpdate dnsUpdate; public BulkPricingPackageMonitoring bulkPricingPackageMonitoring; public Bsa bsa; - public PowerDns powerDns; /** Configuration options that apply to the entire GCP project. */ public static class GcpProject { @@ -59,16 +58,6 @@ public static class GcpProject { public String baseDomain; } - /** Configuration options for PowerDNS. */ - public static class PowerDns { - public String baseUrl; - public String apiKey; - public Boolean dnssecEnabled; - public Boolean tsigEnabled; - public List rootNameServers; - public String soaName; - } - /** Configuration options for authenticating users. */ public static class Auth { public List allowedServiceAccountEmails; diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index 43626bcd3f9..16f5bbd63c9 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -27,16 +27,6 @@ gcpProject: # The base domain name of the registry service. Services are reachable at [service].baseDomain. baseDomain: registry.test -powerDns: - baseUrl: http://localhost:8081/api/v1 - apiKey: example-api-key - dnssecEnabled: false - tsigEnabled: true - rootNameServers: - - ns1.example.com. - - ns2.example.com. - soaName: nstld.example.com. - gSuite: # Publicly accessible domain name of the running G Suite instance. domainName: domain-registry.example diff --git a/core/src/main/java/google/registry/config/files/power-dns/default.yaml b/core/src/main/java/google/registry/config/files/power-dns/default.yaml new file mode 100644 index 00000000000..f2328fb16fc --- /dev/null +++ b/core/src/main/java/google/registry/config/files/power-dns/default.yaml @@ -0,0 +1,10 @@ +# Default configuration for PowerDNS +powerDns: + baseUrl: http://localhost:8081/api/v1 + apiKey: example-api-key + dnssecEnabled: false + tsigEnabled: true + rootNameServers: + - ns1.example.com. + - ns2.example.com. + soaName: nstld.example.com. diff --git a/core/src/main/java/google/registry/config/files/power-dns/env-alpha.yaml b/core/src/main/java/google/registry/config/files/power-dns/env-alpha.yaml new file mode 100644 index 00000000000..15350061751 --- /dev/null +++ b/core/src/main/java/google/registry/config/files/power-dns/env-alpha.yaml @@ -0,0 +1 @@ +# Add environment-specific configuration here. \ No newline at end of file diff --git a/core/src/main/java/google/registry/config/files/power-dns/env-crash.yaml b/core/src/main/java/google/registry/config/files/power-dns/env-crash.yaml new file mode 100644 index 00000000000..15350061751 --- /dev/null +++ b/core/src/main/java/google/registry/config/files/power-dns/env-crash.yaml @@ -0,0 +1 @@ +# Add environment-specific configuration here. \ No newline at end of file diff --git a/core/src/main/java/google/registry/config/files/power-dns/env-local.yaml b/core/src/main/java/google/registry/config/files/power-dns/env-local.yaml new file mode 100644 index 00000000000..15350061751 --- /dev/null +++ b/core/src/main/java/google/registry/config/files/power-dns/env-local.yaml @@ -0,0 +1 @@ +# Add environment-specific configuration here. \ No newline at end of file diff --git a/core/src/main/java/google/registry/config/files/power-dns/env-production.yaml b/core/src/main/java/google/registry/config/files/power-dns/env-production.yaml new file mode 100644 index 00000000000..15350061751 --- /dev/null +++ b/core/src/main/java/google/registry/config/files/power-dns/env-production.yaml @@ -0,0 +1 @@ +# Add environment-specific configuration here. \ No newline at end of file diff --git a/core/src/main/java/google/registry/config/files/power-dns/env-qa.yaml b/core/src/main/java/google/registry/config/files/power-dns/env-qa.yaml new file mode 100644 index 00000000000..15350061751 --- /dev/null +++ b/core/src/main/java/google/registry/config/files/power-dns/env-qa.yaml @@ -0,0 +1 @@ +# Add environment-specific configuration here. \ No newline at end of file diff --git a/core/src/main/java/google/registry/config/files/power-dns/env-sandbox.yaml b/core/src/main/java/google/registry/config/files/power-dns/env-sandbox.yaml new file mode 100644 index 00000000000..15350061751 --- /dev/null +++ b/core/src/main/java/google/registry/config/files/power-dns/env-sandbox.yaml @@ -0,0 +1 @@ +# Add environment-specific configuration here. \ No newline at end of file diff --git a/core/src/main/java/google/registry/config/files/power-dns/env-unittest.yaml b/core/src/main/java/google/registry/config/files/power-dns/env-unittest.yaml new file mode 100644 index 00000000000..f42d029b031 --- /dev/null +++ b/core/src/main/java/google/registry/config/files/power-dns/env-unittest.yaml @@ -0,0 +1,10 @@ +# Add environment-specific configuration here. +powerDns: + baseUrl: http://unittest:8081/api/v1 + apiKey: unittest-api-key + dnssecEnabled: true + tsigEnabled: true + rootNameServers: + - ns1.unittest.com. + - ns2.unittest.com. + soaName: nstld.unittest.com. diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfig.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfig.java new file mode 100644 index 00000000000..d98282a9cdb --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfig.java @@ -0,0 +1,129 @@ +// 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.dns.writer.powerdns; + +import static com.google.common.base.Suppliers.memoize; +import static google.registry.util.ResourceUtils.readResourceUtf8; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; +import dagger.Module; +import dagger.Provides; +import google.registry.config.RegistryConfig; +import google.registry.config.RegistryConfig.Config; +import google.registry.util.RegistryEnvironment; +import google.registry.util.YamlUtils; +import jakarta.inject.Singleton; +import java.util.function.Supplier; + +/** + * Configuration manager for PowerDNS settings. + * + *

This class is responsible for loading the PowerDNS configuration from the YAML files, found in + * the {@code files/power-dns} directory. + */ +public final class PowerDnsConfig { + + // expected config file locations for PowerDNS + private static final String ENVIRONMENT_CONFIG_FORMAT = "files/power-dns/env-%s.yaml"; + private static final String YAML_CONFIG_PROD = + readResourceUtf8(RegistryConfig.class, "files/power-dns/default.yaml"); + + /** + * Loads the {@link PowerDnsConfigSettings} POJO from the YAML configuration files. + * + *

The {@code _default.yaml} file in this directory is loaded first, and a fatal error is + * thrown if it cannot be found or if there is an error parsing it. Separately, the + * environment-specific config file named {@code ENVIRONMENT.yaml} is also loaded and those values + * merged into the POJO. + */ + static PowerDnsConfigSettings getConfigSettings() { + String configFilePath = + String.format( + ENVIRONMENT_CONFIG_FORMAT, Ascii.toLowerCase(RegistryEnvironment.get().name())); + String customYaml = readResourceUtf8(RegistryConfig.class, configFilePath); + return YamlUtils.getConfigSettings(YAML_CONFIG_PROD, customYaml, PowerDnsConfigSettings.class); + } + + /** Dagger module that provides DNS configuration settings. */ + @Module + public static final class PowerDnsConfigModule { + + /** Parsed PowerDNS configuration settings. */ + @Singleton + @Provides + static PowerDnsConfigSettings providePowerDnsConfigSettings() { + return CONFIG_SETTINGS.get(); + } + + /** Base URL of the PowerDNS server. */ + @Provides + @Config("powerDnsBaseUrl") + public static String providePowerDnsBaseUrl(PowerDnsConfigSettings config) { + return config.powerDns.baseUrl; + } + + /** API key for the PowerDNS server. */ + @Provides + @Config("powerDnsApiKey") + public static String providePowerDnsApiKey(PowerDnsConfigSettings config) { + return config.powerDns.apiKey; + } + + /** Whether DNSSEC is enabled for the PowerDNS server. */ + @Provides + @Config("powerDnsDnssecEnabled") + public static Boolean providePowerDnsDnssecEnabled(PowerDnsConfigSettings config) { + return config.powerDns.dnssecEnabled; + } + + /** Whether TSIG is enabled for the PowerDNS server. */ + @Provides + @Config("powerDnsTsigEnabled") + public static Boolean providePowerDnsTsigEnabled(PowerDnsConfigSettings config) { + return config.powerDns.tsigEnabled; + } + + /** Default SOA MNAME for the TLD zone. */ + @Provides + @Config("powerDnsRootNameServers") + public static ImmutableList providePowerDnsRootNameServers( + PowerDnsConfigSettings config) { + return ImmutableList.copyOf(config.powerDns.rootNameServers); + } + + /** Default SOA RNAME for the TLD zone. */ + @Provides + @Config("powerDnsSoaName") + public static String providePowerDnsSoaName(PowerDnsConfigSettings config) { + return config.powerDns.soaName; + } + + private PowerDnsConfigModule() {} + } + + /** + * Memoizes loading of the {@link PowerDnsConfigSettings} POJO. + * + *

Memoizing without cache expiration is used because the app must be re-deployed in order to + * change the contents of the YAML config files. + */ + @VisibleForTesting + public static final Supplier CONFIG_SETTINGS = + memoize(PowerDnsConfig::getConfigSettings); + + private PowerDnsConfig() {} +} diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigSettings.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigSettings.java new file mode 100644 index 00000000000..5b14a9e026f --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfigSettings.java @@ -0,0 +1,32 @@ +// 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.dns.writer.powerdns; + +import java.util.List; + +/** The POJO that PowerDNS YAML config files are deserialized into. */ +public class PowerDnsConfigSettings { + + public PowerDns powerDns; + + public static class PowerDns { + public String baseUrl; + public String apiKey; + public Boolean dnssecEnabled; + public Boolean tsigEnabled; + public List rootNameServers; + public String soaName; + } +} diff --git a/core/src/main/java/google/registry/module/RegistryComponent.java b/core/src/main/java/google/registry/module/RegistryComponent.java index afd01c12b2f..6d53d31804e 100644 --- a/core/src/main/java/google/registry/module/RegistryComponent.java +++ b/core/src/main/java/google/registry/module/RegistryComponent.java @@ -26,6 +26,7 @@ import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.ConfigModule; import google.registry.dns.writer.VoidDnsWriterModule; +import google.registry.dns.writer.powerdns.PowerDnsConfig.PowerDnsConfigModule; import google.registry.export.DriveModule; import google.registry.export.sheet.SheetsServiceModule; import google.registry.flows.ServerTridProviderModule; @@ -62,6 +63,7 @@ BigqueryModule.class, CloudTasksUtilsModule.class, ConfigModule.class, + PowerDnsConfigModule.class, ConsoleConfigModule.class, CredentialModule.class, CustomLogicFactoryModule.class, diff --git a/core/src/main/java/google/registry/module/backend/BackendComponent.java b/core/src/main/java/google/registry/module/backend/BackendComponent.java index e698b86d8f0..da1bf752ff4 100644 --- a/core/src/main/java/google/registry/module/backend/BackendComponent.java +++ b/core/src/main/java/google/registry/module/backend/BackendComponent.java @@ -23,6 +23,7 @@ import google.registry.config.CredentialModule; import google.registry.config.RegistryConfig.ConfigModule; import google.registry.dns.writer.VoidDnsWriterModule; +import google.registry.dns.writer.powerdns.PowerDnsConfig.PowerDnsConfigModule; import google.registry.export.DriveModule; import google.registry.export.sheet.SheetsServiceModule; import google.registry.flows.ServerTridProviderModule; @@ -54,6 +55,7 @@ BatchModule.class, BigqueryModule.class, ConfigModule.class, + PowerDnsConfigModule.class, CloudTasksUtilsModule.class, CredentialModule.class, CustomLogicFactoryModule.class, From e7385bfa8bc3f6af411ffda7a2eadf5aec2e8b35 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Sat, 31 May 2025 11:31:26 -0400 Subject: [PATCH 24/49] refactor and reuse config merge from RegistryConfig class --- .../registry/config/RegistryConfig.java | 26 +++++++++-------- .../dns/writer/powerdns/PowerDnsConfig.java | 28 ++++--------------- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index f5421975d10..cfec7473db2 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -72,8 +72,8 @@ public final class RegistryConfig { public static final String CANARY_HEADER = "canary"; - private static final String ENVIRONMENT_CONFIG_FORMAT = "files/nomulus-config-%s.yaml"; - private static final String YAML_CONFIG_PROD = + private static final String YAML_CONFIG_ENV_TEMPLATE = "files/nomulus-config-%s.yaml"; + private static final String YAML_CONFIG_DEFAULT = readResourceUtf8(RegistryConfig.class, "files/default-config.yaml"); /** Dagger qualifier for configuration settings. */ @@ -85,19 +85,18 @@ public final class RegistryConfig { } /** - * Loads the {@link RegistryConfigSettings} POJO from the YAML configuration files. + * Loads a generic typed POJO from the YAML configuration files. * - *

The {@code default-config.yaml} file in this directory is loaded first, and a fatal error is - * thrown if it cannot be found or if there is an error parsing it. Separately, the - * environment-specific config file named {@code nomulus-config-ENVIRONMENT.yaml} is also loaded - * and those values merged into the POJO. + *

The {@code defaultYaml} file is loaded first, and a fatal error is thrown if it cannot be + * found or if there is an error parsing it. Separately, the environment-specific config file + * template {@code customYamlTemplate} is also loaded and those values merged into the POJO. */ - static RegistryConfigSettings getConfigSettings() { + public static T getEnvironmentConfigSettings( + String defaultYaml, String customYamlTemplate, Class clazz) { String configFilePath = - String.format( - ENVIRONMENT_CONFIG_FORMAT, Ascii.toLowerCase(RegistryEnvironment.get().name())); + String.format(customYamlTemplate, Ascii.toLowerCase(RegistryEnvironment.get().name())); String customYaml = readResourceUtf8(RegistryConfig.class, configFilePath); - return YamlUtils.getConfigSettings(YAML_CONFIG_PROD, customYaml, RegistryConfigSettings.class); + return YamlUtils.getConfigSettings(defaultYaml, customYaml, clazz); } /** Dagger module for providing configuration settings. */ @@ -1723,7 +1722,10 @@ public static ImmutableSet getNoPollMessageOnDeletionRegistrarIds() { */ @VisibleForTesting public static final Supplier CONFIG_SETTINGS = - memoize(RegistryConfig::getConfigSettings); + memoize( + () -> + RegistryConfig.getEnvironmentConfigSettings( + YAML_CONFIG_DEFAULT, YAML_CONFIG_ENV_TEMPLATE, RegistryConfigSettings.class)); private static InternetAddress parseEmailAddress(String email) { try { diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfig.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfig.java index d98282a9cdb..6eb0d89c80b 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfig.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsConfig.java @@ -18,14 +18,11 @@ import static google.registry.util.ResourceUtils.readResourceUtf8; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; import dagger.Module; import dagger.Provides; import google.registry.config.RegistryConfig; import google.registry.config.RegistryConfig.Config; -import google.registry.util.RegistryEnvironment; -import google.registry.util.YamlUtils; import jakarta.inject.Singleton; import java.util.function.Supplier; @@ -38,26 +35,10 @@ public final class PowerDnsConfig { // expected config file locations for PowerDNS - private static final String ENVIRONMENT_CONFIG_FORMAT = "files/power-dns/env-%s.yaml"; - private static final String YAML_CONFIG_PROD = + private static final String YAML_CONFIG_ENV_TEMPLATE = "files/power-dns/env-%s.yaml"; + private static final String YAML_CONFIG_DEFAULT = readResourceUtf8(RegistryConfig.class, "files/power-dns/default.yaml"); - /** - * Loads the {@link PowerDnsConfigSettings} POJO from the YAML configuration files. - * - *

The {@code _default.yaml} file in this directory is loaded first, and a fatal error is - * thrown if it cannot be found or if there is an error parsing it. Separately, the - * environment-specific config file named {@code ENVIRONMENT.yaml} is also loaded and those values - * merged into the POJO. - */ - static PowerDnsConfigSettings getConfigSettings() { - String configFilePath = - String.format( - ENVIRONMENT_CONFIG_FORMAT, Ascii.toLowerCase(RegistryEnvironment.get().name())); - String customYaml = readResourceUtf8(RegistryConfig.class, configFilePath); - return YamlUtils.getConfigSettings(YAML_CONFIG_PROD, customYaml, PowerDnsConfigSettings.class); - } - /** Dagger module that provides DNS configuration settings. */ @Module public static final class PowerDnsConfigModule { @@ -123,7 +104,10 @@ private PowerDnsConfigModule() {} */ @VisibleForTesting public static final Supplier CONFIG_SETTINGS = - memoize(PowerDnsConfig::getConfigSettings); + memoize( + () -> + RegistryConfig.getEnvironmentConfigSettings( + YAML_CONFIG_DEFAULT, YAML_CONFIG_ENV_TEMPLATE, PowerDnsConfigSettings.class)); private PowerDnsConfig() {} } From 2691faee634e578cb3f91cd90deea29e92cbdaea Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Tue, 3 Jun 2025 10:54:27 -0400 Subject: [PATCH 25/49] add validation for SOA and NS records --- .../dns/writer/powerdns/PowerDnsWriter.java | 232 +++++++++++++++--- 1 file changed, 198 insertions(+), 34 deletions(-) diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java index ddb18603738..fac8fc59050 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/PowerDnsWriter.java @@ -116,7 +116,7 @@ public PowerDnsWriter( super(tldZoneName, dnsDefaultATtl, dnsDefaultNsTtl, dnsDefaultDsTtl, null, clock); // Initialize the PowerDNS client - this.tldZoneName = getCanonicalHostName(tldZoneName); + this.tldZoneName = getHostNameWithTrailingDot(tldZoneName); this.rootNameServers = powerDnsRootNameServers; this.soaName = powerDnsSoaName; this.dnssecEnabled = powerDnsDnssecEnabled; @@ -132,7 +132,7 @@ public PowerDnsWriter( */ @Override public void publishDomain(String domainName) { - String normalizedDomainName = getSanitizedHostName(domainName); + String normalizedDomainName = getHostNameWithoutTrailingDot(domainName); logger.atInfo().log("Staging domain %s for PowerDNS", normalizedDomainName); super.publishDomain(normalizedDomainName); } @@ -145,7 +145,7 @@ public void publishDomain(String domainName) { */ @Override public void publishHost(String hostName) { - String normalizedHostName = getSanitizedHostName(hostName); + String normalizedHostName = getHostNameWithoutTrailingDot(hostName); logger.atInfo().log("Staging host %s for PowerDNS", normalizedHostName); super.publishHost(normalizedHostName); } @@ -280,7 +280,7 @@ private Zone convertUpdateToZone(Update update) throws IOException { private RRSet appendRRSet(List rrsets, String name, String type, long ttl) { // create the base PowerDNS RRSet object RRSet rrset = new RRSet(); - rrset.setName(name); + rrset.setName(getHostNameWithTrailingDot(name)); rrset.setType(type); rrset.setTtl(ttl); rrset.setRecords(new ArrayList()); @@ -303,14 +303,14 @@ private Zone createZone() throws IOException { // base TLD zone object logger.atInfo().log("Creating new PowerDNS TLD zone %s", tldZoneName); Zone newTldZone = new Zone(); - newTldZone.setName(tldZoneName); + newTldZone.setName(getHostNameWithTrailingDot(tldZoneName)); newTldZone.setKind(Zone.ZoneKind.Master); // create an initial SOA record, which may be modified later by an administrator // or an automated onboarding process RRSet soaRecord = new RRSet(); soaRecord.setChangeType(RRSet.ChangeType.REPLACE); - soaRecord.setName(tldZoneName); + soaRecord.setName(getHostNameWithTrailingDot(tldZoneName)); soaRecord.setTtl(defaultZoneTtl); soaRecord.setType("SOA"); @@ -318,14 +318,17 @@ private Zone createZone() throws IOException { RecordObject soaRecordContent = new RecordObject(); soaRecordContent.setContent( String.format( - "%s %s 1 900 1800 6048000 %s", rootNameServers.get(0), soaName, defaultZoneTtl)); + "%s %s 1 900 1800 6048000 %s", + getHostNameWithTrailingDot(rootNameServers.get(0)), + getHostNameWithTrailingDot(soaName), + defaultZoneTtl)); soaRecordContent.setDisabled(false); soaRecord.setRecords(new ArrayList(Arrays.asList(soaRecordContent))); // create NS records, which may be modified later by an administrator RRSet nsRecord = new RRSet(); nsRecord.setChangeType(RRSet.ChangeType.REPLACE); - nsRecord.setName(tldZoneName); + nsRecord.setName(getHostNameWithTrailingDot(tldZoneName)); nsRecord.setTtl(defaultZoneTtl); nsRecord.setType("NS"); @@ -336,7 +339,7 @@ private Zone createZone() throws IOException { .map( ns -> { RecordObject nsRecordContent = new RecordObject(); - nsRecordContent.setContent(ns); + nsRecordContent.setContent(getHostNameWithTrailingDot(ns)); nsRecordContent.setDisabled(false); return nsRecordContent; }) @@ -353,6 +356,169 @@ private Zone createZone() throws IOException { return createdTldZone; } + /** + * Validate and synchronize zone configuration for the provided TLD zone. includes SOA, NS, TSIG, + * and DNSSEC configuration. + * + * @param zone the TLD zone to validate + */ + private void validateZoneConfig(Zone zone) throws IOException { + // validate the SOA and root NS records + validateSoaConfig(zone); + + // validate the NS records + validateNsConfig(zone); + + // validate the TSIG key configuration + validateTsigConfig(zone); + + // validate the DNSSEC configuration + validateDnssecConfig(zone); + } + + /** + * Validate the SOA record for the TLD zone. + * + * @param zone the TLD zone to validate + */ + private void validateSoaConfig(Zone zone) throws IOException { + // retrieve the existing SOA record + logger.atInfo().log("Validating SOA record for PowerDNS TLD zone %s", zone.getName()); + RRSet soaRecord = + zone.getRrsets().stream() + .filter(rrset -> rrset.getType().equals("SOA")) + .findFirst() + .orElse(null); + if (soaRecord == null || soaRecord.getRecords() == null) { + throw new IOException("Invalid SOA record state for PowerDNS TLD zone " + zone.getName()); + } + + // retrieve the SOA record RRSet content exists + RecordObject soaRecordContent = soaRecord.getRecords().get(0); + if (soaRecordContent == null || soaRecordContent.getContent() == null) { + throw new IOException( + "Invalid SOA record content state for PowerDNS TLD zone " + zone.getName()); + } + + // validate the SOA record content exists + String soaRecordContentString = soaRecordContent.getContent(); + if (soaRecordContentString == null) { + throw new IOException( + "Invalid SOA record content data for PowerDNS TLD zone " + zone.getName()); + } + + // validate the SOA string starts with the first root name server and the SOA contact + // name found in the registry configuration + if (soaRecordContentString.startsWith( + String.format( + "%s %s ", + getHostNameWithTrailingDot(rootNameServers.get(0)), + getHostNameWithTrailingDot(soaName)))) { + logger.atInfo().log( + "Successfully validated SOA record for PowerDNS TLD zone %s", zone.getName()); + return; + } + + // update the SOA record to the expected value + logger.atWarning().log("Updating SOA record for PowerDNS TLD zone %s", zone.getName()); + RRSet newSoaRecord = new RRSet(); + newSoaRecord.setChangeType(RRSet.ChangeType.REPLACE); + newSoaRecord.setName(getHostNameWithTrailingDot(zone.getName())); + newSoaRecord.setTtl(defaultZoneTtl); + newSoaRecord.setType("SOA"); + + // add content to the SOA record content from default configuration + RecordObject newSoaRecordContent = new RecordObject(); + newSoaRecordContent.setContent( + String.format( + "%s %s %s 900 1800 6048000 %s", + getHostNameWithTrailingDot(rootNameServers.get(0)), + getHostNameWithTrailingDot(soaName), + zone.getSerial(), + defaultZoneTtl)); + newSoaRecordContent.setDisabled(false); + newSoaRecord.setRecords(new ArrayList(Arrays.asList(newSoaRecordContent))); + + // add the SOA to the TLD zone + zone.setRrsets(new ArrayList(Arrays.asList(newSoaRecord))); + + // call the PowerDNS API to commit the changes + powerDnsClient.patchZone(zone); + logger.atInfo().log("Successfully updated SOA record for PowerDNS TLD zone %s", zone.getName()); + } + + /** + * Validate the NS records for the TLD zone. + * + * @param zone the TLD zone to validate + */ + private void validateNsConfig(Zone zone) throws IOException { + // retrieve the existing NS records + logger.atInfo().log("Validating NS records for PowerDNS TLD zone %s", zone.getName()); + RRSet nsRecord = + zone.getRrsets().stream() + .filter( + rrset -> + rrset.getType().equals("NS") + && getHostNameWithoutTrailingDot(rrset.getName()) + .equals(getHostNameWithoutTrailingDot(zone.getName()))) + .findFirst() + .orElse(null); + if (nsRecord == null || nsRecord.getRecords() == null) { + throw new IOException("Invalid NS record state for PowerDNS TLD zone " + zone.getName()); + } + + // retrieve normalized list of existing NS record content + List existingNsRecords = + nsRecord.getRecords().stream() + .map(r -> getHostNameWithoutTrailingDot(r.getContent())) + .collect(Collectors.toList()); + + // make normalized list of expected NS record content + List expectedNsRecords = + rootNameServers.stream() + .map(r -> getHostNameWithoutTrailingDot(r)) + .collect(Collectors.toList()); + + // validate the existing NS record array elements match the expected elements + // found in the root name servers list + if (existingNsRecords.equals(expectedNsRecords)) { + logger.atInfo().log( + "Successfully validated NS records for PowerDNS TLD zone %s", zone.getName()); + return; + } + + // update the NS records to the expected value + logger.atWarning().log( + "Updating NS records for PowerDNS TLD zone %s. Existing=%s, New=%s", + zone.getName(), existingNsRecords, expectedNsRecords); + RRSet newNsRecord = new RRSet(); + newNsRecord.setChangeType(RRSet.ChangeType.REPLACE); + newNsRecord.setName(getHostNameWithTrailingDot(zone.getName())); + newNsRecord.setTtl(defaultZoneTtl); + newNsRecord.setType("NS"); + + // add content to the NS record content from default configuration + newNsRecord.setRecords( + new ArrayList( + rootNameServers.stream() + .map( + ns -> { + RecordObject nsRecordContent = new RecordObject(); + nsRecordContent.setContent(getHostNameWithTrailingDot(ns)); + nsRecordContent.setDisabled(false); + return nsRecordContent; + }) + .collect(Collectors.toList()))); + + // add the NS record to the TLD zone + zone.setRrsets(new ArrayList(Arrays.asList(newNsRecord))); + + // call the PowerDNS API to commit the changes + powerDnsClient.patchZone(zone); + logger.atInfo().log("Successfully updated NS records for PowerDNS TLD zone %s", zone.getName()); + } + /** * Validate the TSIG key configuration for the PowerDNS server. Ensures a TSIG key associated with * the TLD zone is available for use, and detects whether the TLD zone has been configured to use @@ -372,7 +538,7 @@ private void validateTsigConfig(Zone zone) throws IOException { // calculate the zone TSIG key name logger.atInfo().log("Validating TSIG configuration for PowerDNS TLD zone %s", zone.getName()); String zoneTsigKeyName = - String.format("%s-%s", getSanitizedHostName(zone.getName()), TSIG_KEY_NAME); + String.format("%s-%s", getHostNameWithoutTrailingDot(zone.getName()), TSIG_KEY_NAME); // validate the named TSIG key is present in the PowerDNS server try { @@ -611,28 +777,28 @@ else if (zone.getAccount().contains(DNSSEC_ZSK_ACTIVATION_FLAG)) { } /** - * Returns the presentation format ending in a dot used for an given hostname. + * Returns the host name with a trailing dot. * * @param hostName the fully qualified hostname + * @return the host name with a trailing dot */ - private String getCanonicalHostName(String hostName) { - String normalizedHostName = hostName.endsWith(".") ? hostName : hostName + '.'; - String canonicalHostName = - tldZoneName == null || normalizedHostName.endsWith(tldZoneName) - ? normalizedHostName - : normalizedHostName + tldZoneName; - return canonicalHostName.toLowerCase(Locale.US); + private String getHostNameWithTrailingDot(String hostName) { + String normalizedHostName = hostName.toLowerCase(Locale.US).trim(); + return normalizedHostName.endsWith(".") ? normalizedHostName : normalizedHostName + '.'; } /** - * Returns the sanitized host name, which is the host name without the trailing dot. + * Returns the host name without the trailing dot. * * @param hostName the fully qualified hostname * @return the sanitized host name */ - private String getSanitizedHostName(String hostName) { + private String getHostNameWithoutTrailingDot(String hostName) { // return the host name without the trailing dot - return hostName.endsWith(".") ? hostName.substring(0, hostName.length() - 1) : hostName; + String normalizedHostName = hostName.toLowerCase(Locale.US).trim(); + return normalizedHostName.endsWith(".") + ? normalizedHostName.substring(0, normalizedHostName.length() - 1) + : normalizedHostName; } /** @@ -644,7 +810,7 @@ private String getSanitizedHostName(String hostName) { private Zone getTldZoneForUpdate(List records) throws IOException { Zone tldZone = new Zone(); tldZone.setId(getTldZoneId()); - tldZone.setName(getSanitizedHostName(tldZoneName)); + tldZone.setName(getHostNameWithoutTrailingDot(tldZoneName)); tldZone.setRrsets(records); return tldZone; } @@ -658,13 +824,14 @@ private Zone getTldZoneForUpdate(List records) throws IOException { private Zone getAndValidateTldZoneByName() throws IOException { // retrieve an existing TLD zone by name for (Zone zone : powerDnsClient.listZones()) { - if (getSanitizedHostName(zone.getName()).equals(getSanitizedHostName(tldZoneName))) { - // validate the zone's TSIG key configuration - validateTsigConfig(zone); - - // validate the DNSSEC configuration and return the TLD zone - validateDnssecConfig(zone); - return zone; + if (getHostNameWithoutTrailingDot(zone.getName()) + .equals(getHostNameWithoutTrailingDot(tldZoneName))) { + // retrieve full zone details + Zone fullZone = powerDnsClient.getZone(zone.getId()); + + // validate the zone's configuration + validateZoneConfig(fullZone); + return fullZone; } } @@ -673,11 +840,8 @@ private Zone getAndValidateTldZoneByName() throws IOException { // create a new TLD zone Zone zone = createZone(); - // validate the zone's TSIG key configuration - validateTsigConfig(zone); - - // configure DNSSEC and return the TLD zone - validateDnssecConfig(zone); + // validate the zone's configuration + validateZoneConfig(zone); return zone; } catch (Exception e) { // log the error and continue From abe77678f02d4ed13cec83c367eb98a72ee29b12 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Thu, 26 Jun 2025 15:32:30 -0400 Subject: [PATCH 26/49] update configuration mgmt from review comments --- .../java/google/registry/config/RegistryConfig.java | 11 +++++++---- .../dns/writer/powerdns/client/PowerDNSClient.java | 7 +++++-- .../dns/writer/powerdns/resources/openAPI.yaml | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index cfec7473db2..11a43eadbc0 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -99,6 +99,12 @@ public static T getEnvironmentConfigSettings( return YamlUtils.getConfigSettings(defaultYaml, customYaml, clazz); } + /** Shorthand method to retrieve the main registry config settings for the current environment. */ + static RegistryConfigSettings getConfigSettings() { + return RegistryConfig.getEnvironmentConfigSettings( + YAML_CONFIG_DEFAULT, YAML_CONFIG_ENV_TEMPLATE, RegistryConfigSettings.class); + } + /** Dagger module for providing configuration settings. */ @Module public static final class ConfigModule { @@ -1722,10 +1728,7 @@ public static ImmutableSet getNoPollMessageOnDeletionRegistrarIds() { */ @VisibleForTesting public static final Supplier CONFIG_SETTINGS = - memoize( - () -> - RegistryConfig.getEnvironmentConfigSettings( - YAML_CONFIG_DEFAULT, YAML_CONFIG_ENV_TEMPLATE, RegistryConfigSettings.class)); + memoize(RegistryConfig::getConfigSettings); private static InternetAddress parseEmailAddress(String email) { try { diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java index 379d0926c1e..56f4ce1f476 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java +++ b/core/src/main/java/google/registry/dns/writer/powerdns/client/PowerDNSClient.java @@ -93,8 +93,11 @@ private synchronized void initializeServerId() { private String bodyToString(final RequestBody requestBody) throws IOException { try (Buffer buffer = new Buffer()) { - if (requestBody != null) requestBody.writeTo(buffer); - else return ""; + if (requestBody != null) { + requestBody.writeTo(buffer); + } else { + return ""; + } return buffer.readUtf8(); } } diff --git a/core/src/main/java/google/registry/dns/writer/powerdns/resources/openAPI.yaml b/core/src/main/java/google/registry/dns/writer/powerdns/resources/openAPI.yaml index 555fe70c2b7..b820599f7ba 100644 --- a/core/src/main/java/google/registry/dns/writer/powerdns/resources/openAPI.yaml +++ b/core/src/main/java/google/registry/dns/writer/powerdns/resources/openAPI.yaml @@ -1395,4 +1395,4 @@ definitions: description: 'Amount of entries flushed' result: type: string - description: 'A message about the result like "Flushed cache"' \ No newline at end of file + description: 'A message about the result like "Flushed cache"' From ec0d63e48c1c5b857469f294988012c917d80cc0 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Thu, 26 Jun 2025 15:37:55 -0400 Subject: [PATCH 27/49] Update core/src/main/java/google/registry/config/RegistryConfig.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/src/main/java/google/registry/config/RegistryConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 11a43eadbc0..83e9f8432d6 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -100,7 +100,7 @@ public static T getEnvironmentConfigSettings( } /** Shorthand method to retrieve the main registry config settings for the current environment. */ - static RegistryConfigSettings getConfigSettings() { + private static RegistryConfigSettings getConfigSettings() { return RegistryConfig.getEnvironmentConfigSettings( YAML_CONFIG_DEFAULT, YAML_CONFIG_ENV_TEMPLATE, RegistryConfigSettings.class); } From 81bc11fb59fb2f12f61fec4e2b71fd0bf04c0dcc Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Thu, 26 Jun 2025 15:40:03 -0400 Subject: [PATCH 28/49] add comment to getConfigSettings method --- .../src/main/java/google/registry/config/RegistryConfig.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 83e9f8432d6..59003b173d5 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -99,7 +99,10 @@ public static T getEnvironmentConfigSettings( return YamlUtils.getConfigSettings(defaultYaml, customYaml, clazz); } - /** Shorthand method to retrieve the main registry config settings for the current environment. */ + /** + * Shorthand method to retrieve the main registry config settings for the current environment. Can + * be changed to public scope if needed by other packages. + */ private static RegistryConfigSettings getConfigSettings() { return RegistryConfig.getEnvironmentConfigSettings( YAML_CONFIG_DEFAULT, YAML_CONFIG_ENV_TEMPLATE, RegistryConfigSettings.class); From b3351338c516cf348b9c1fb74fd2d12869a62147 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Thu, 26 Jun 2025 15:40:03 -0400 Subject: [PATCH 29/49] add comment to getConfigSettings method --- .../main/java/google/registry/config/RegistryConfig.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 11a43eadbc0..d4051ed95f0 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -99,8 +99,11 @@ public static T getEnvironmentConfigSettings( return YamlUtils.getConfigSettings(defaultYaml, customYaml, clazz); } - /** Shorthand method to retrieve the main registry config settings for the current environment. */ - static RegistryConfigSettings getConfigSettings() { + /** + * Shorthand method to retrieve the main registry config settings for the current environment. Can + * be changed to public scope if needed by other packages. + */ + private static RegistryConfigSettings getConfigSettings() { return RegistryConfig.getEnvironmentConfigSettings( YAML_CONFIG_DEFAULT, YAML_CONFIG_ENV_TEMPLATE, RegistryConfigSettings.class); } @@ -1739,4 +1742,4 @@ private static InternetAddress parseEmailAddress(String email) { } private RegistryConfig() {} -} +} \ No newline at end of file From dd487e808215306e34ce4d061f59ac0431578dbd Mon Sep 17 00:00:00 2001 From: Weimin Yu Date: Thu, 8 May 2025 13:29:30 -0400 Subject: [PATCH 30/49] Hardcode beam pipelines to use GKE for tasks (#2753) --- core/build.gradle | 1 + .../RegistryPipelineWorkerInitializer.java | 2 ++ ...RegistryPipelineWorkerInitializerTest.java | 36 +++++++++++++++++++ .../registry/util/RegistryEnvironment.java | 6 ++-- 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 core/src/test/java/google/registry/beam/common/RegistryPipelineWorkerInitializerTest.java diff --git a/core/build.gradle b/core/build.gradle index c715f748cd7..cdb1d318825 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -61,6 +61,7 @@ def fragileTestPatterns = [ // Currently changes a global configuration parameter that for some reason // results in timestamp inversions for other tests. TODO(mmuller): fix. "google/registry/flows/host/HostInfoFlowTest.*", + "google/registry/beam/common/RegistryPipelineWorkerInitializerTest.*", ] + dockerIncompatibleTestPatterns sourceSets { diff --git a/core/src/main/java/google/registry/beam/common/RegistryPipelineWorkerInitializer.java b/core/src/main/java/google/registry/beam/common/RegistryPipelineWorkerInitializer.java index 5dd54019b85..7b98135f64b 100644 --- a/core/src/main/java/google/registry/beam/common/RegistryPipelineWorkerInitializer.java +++ b/core/src/main/java/google/registry/beam/common/RegistryPipelineWorkerInitializer.java @@ -40,6 +40,8 @@ public class RegistryPipelineWorkerInitializer implements JvmInitializer { @Override public void beforeProcessing(PipelineOptions options) { + // TODO(b/416299900): remove next line after GAE is removed. + System.setProperty("google.registry.jetty", "true"); RegistryPipelineOptions registryOptions = options.as(RegistryPipelineOptions.class); RegistryEnvironment environment = registryOptions.getRegistryEnvironment(); if (environment == null || environment.equals(RegistryEnvironment.UNITTEST)) { diff --git a/core/src/test/java/google/registry/beam/common/RegistryPipelineWorkerInitializerTest.java b/core/src/test/java/google/registry/beam/common/RegistryPipelineWorkerInitializerTest.java new file mode 100644 index 00000000000..16a084ece99 --- /dev/null +++ b/core/src/test/java/google/registry/beam/common/RegistryPipelineWorkerInitializerTest.java @@ -0,0 +1,36 @@ +// 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.beam.common; + +import static com.google.common.truth.Truth.assertThat; + +import google.registry.util.RegistryEnvironment; +import org.apache.beam.sdk.options.PipelineOptionsFactory; +import org.junit.jupiter.api.Test; + +public class RegistryPipelineWorkerInitializerTest { + + @Test + void test() { + RegistryPipelineOptions options = + PipelineOptionsFactory.fromArgs( + "--registryEnvironment=ALPHA", "--isolationOverride=TRANSACTION_SERIALIZABLE") + .withValidation() + .as(RegistryPipelineOptions.class); + new RegistryPipelineWorkerInitializer().beforeProcessing(options); + assertThat(RegistryEnvironment.isOnJetty()).isTrue(); + System.clearProperty("google.registry.jetty"); + } +} diff --git a/util/src/main/java/google/registry/util/RegistryEnvironment.java b/util/src/main/java/google/registry/util/RegistryEnvironment.java index 2af902ad638..819efaa0bb8 100644 --- a/util/src/main/java/google/registry/util/RegistryEnvironment.java +++ b/util/src/main/java/google/registry/util/RegistryEnvironment.java @@ -61,9 +61,6 @@ public enum RegistryEnvironment { /** Name of the environmental variable of the container name. */ private static final String CONTAINER_ENV = "CONTAINER_NAME"; - private static final boolean ON_JETTY = - Boolean.parseBoolean(System.getProperty(JETTY_PROPERTY, "false")); - private static final boolean IS_CANARY = System.getenv().getOrDefault(CONTAINER_ENV, "").endsWith("-canary"); @@ -100,8 +97,9 @@ public static RegistryEnvironment get() { return valueOf(Ascii.toUpperCase(System.getProperty(PROPERTY, UNITTEST.name()))); } + // TODO(b/416299900): remove method after GAE is removed. public static boolean isOnJetty() { - return ON_JETTY; + return Boolean.parseBoolean(System.getProperty(JETTY_PROPERTY, "false")); } public static boolean isCanary() { From d06bcf8ca1f80e87220c9e7637ee617004262b6a Mon Sep 17 00:00:00 2001 From: gbrodman Date: Thu, 8 May 2025 15:16:08 -0400 Subject: [PATCH 31/49] Add SQL table for password resets (#2751) We plan on using this for EPP password resets and registry lock password resets for now. --- .../sql/er_diagram/brief_er_diagram.html | 464 ++++++----- .../sql/er_diagram/full_er_diagram.html | 756 ++++++++++-------- db/src/main/resources/sql/flyway.txt | 1 + .../flyway/V193__password_reset_request.sql | 23 + .../resources/sql/schema/nomulus.golden.sql | 22 + 5 files changed, 729 insertions(+), 537 deletions(-) create mode 100644 db/src/main/resources/sql/flyway/V193__password_reset_request.sql diff --git a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html index 85611136578..519144e3fb6 100644 --- a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html @@ -261,26 +261,26 @@

System Information

generated on - 2025-04-18 20:02:20 + 2025-04-30 16:04:48 last flyway file - V192__add_last_poc_verification_date.sql + V193__password_reset_request.sql

 

 

- - + + SchemaCrawler_Diagram - + generated by SchemaCrawler 16.25.2 generated on - 2025-04-18 20:02:20 + 2025-04-30 16:04:48 @@ -404,7 +404,7 @@

System Information

fk_billing_event_cancellation_matching_billing_recurrence_id - + registrar_6e1503e3 @@ -765,7 +765,7 @@

System Information

fk_domain_transfer_losing_registrar_id
- + tld_f1fa57e2 @@ -1153,7 +1153,7 @@

System Information

text not null
- + user_f2216f01 @@ -1900,74 +1900,87 @@

System Information

text not null -
+
+ + passwordresetrequest_8484e7b1 + + + public."PasswordResetRequest" + + [table] + verification_code + + text not null + + + premiumentry_b0060b91 - - public."PremiumEntry" - - [table] - revision_id - - int8 not null - domain_label - - text not null - + + public."PremiumEntry" + + [table] + revision_id + + int8 not null + domain_label + + text not null + - + premiumlist_7c3ea68b - - public."PremiumList" - - [table] - revision_id - - bigserial not null - - auto-incremented - name - - text not null - + + public."PremiumList" + + [table] + revision_id + + bigserial not null + + auto-incremented + name + + text not null + premiumentry_b0060b91:w->premiumlist_7c3ea68b:e - - - - - - - - fko0gw90lpo1tuee56l0nb6y6g5 + + + + + + + + fko0gw90lpo1tuee56l0nb6y6g5 - + rderevision_83396864 - - public."RdeRevision" - - [table] - tld - - text not null - mode - - text not null - "date" - - date not null - + + public."RdeRevision" + + [table] + tld + + text not null + mode + + text not null + "date" + + date not null + - + registrarpoc_ab47054d @@ -1996,7 +2009,7 @@

System Information

fk_registrar_poc_registrar_id
- + registrarupdatehistory_8a38bed4 @@ -2028,7 +2041,7 @@

System Information

fkregistrarupdatehistoryregistrarid
- + registrarpocupdatehistory_31e5d9aa @@ -2076,205 +2089,205 @@

System Information

fkregistrarpocupdatehistoryemailaddress
- + registrylock_ac88663e - - public."RegistryLock" - - [table] - revision_id - - bigserial not null - - auto-incremented - registrar_id - - text not null - repo_id - - text not null - verification_code - - text not null - relock_revision_id - - int8 - + + public."RegistryLock" + + [table] + revision_id + + bigserial not null + + auto-incremented + registrar_id + + text not null + repo_id + + text not null + verification_code + + text not null + relock_revision_id + + int8 + registrylock_ac88663e:w->registrylock_ac88663e:e - - - - - - - - fk2lhcwpxlnqijr96irylrh1707 + + + + + + + + fk2lhcwpxlnqijr96irylrh1707 - + reservedentry_1a7b8520 - - public."ReservedEntry" - - [table] - revision_id - - int8 not null - domain_label - - text not null - + + public."ReservedEntry" + + [table] + revision_id + + int8 not null + domain_label + + text not null + - + reservedlist_b97c3f1c - - public."ReservedList" - - [table] - revision_id - - bigserial not null - - auto-incremented - name - - text not null - + + public."ReservedList" + + [table] + revision_id + + bigserial not null + + auto-incremented + name + + text not null + reservedentry_1a7b8520:w->reservedlist_b97c3f1c:e - - - - - - - - fkgq03rk0bt1hb915dnyvd3vnfc + + + + + + + + fkgq03rk0bt1hb915dnyvd3vnfc - + serversecret_6cc90f09 - - public."ServerSecret" - - [table] - id - - int8 not null - + + public."ServerSecret" + + [table] + id + + int8 not null + - + signedmarkrevocationentry_99c39721 - - public."SignedMarkRevocationEntry" - - [table] - revision_id - - int8 not null - smd_id - - text not null - + + public."SignedMarkRevocationEntry" + + [table] + revision_id + + int8 not null + smd_id + + text not null + - + signedmarkrevocationlist_c5d968fb - - public."SignedMarkRevocationList" - - [table] - revision_id - - bigserial not null - - auto-incremented - + + public."SignedMarkRevocationList" + + [table] + revision_id + + bigserial not null + + auto-incremented + signedmarkrevocationentry_99c39721:w->signedmarkrevocationlist_c5d968fb:e - - - - - - - - fk5ivlhvs3121yx2li5tqh54u4 + + + + + + + + fk5ivlhvs3121yx2li5tqh54u4 - + spec11threatmatch_a61228a6 - - public."Spec11ThreatMatch" - - [table] - id - - bigserial not null - - auto-incremented - check_date - - date not null - registrar_id - - text not null - tld - - text not null - + + public."Spec11ThreatMatch" + + [table] + id + + bigserial not null + + auto-incremented + check_date + + date not null + registrar_id + + text not null + tld + + text not null + - + tmchcrl_d282355 - - public."TmchCrl" - - [table] - id - - int8 not null - + + public."TmchCrl" + + [table] + id + + int8 not null + - + userupdatehistory_24efd476 - - public."UserUpdateHistory" - - [table] - history_revision_id - - int8 not null - email_address - - text not null - history_acting_user - - text not null - + + public."UserUpdateHistory" + + [table] + history_revision_id + + int8 not null + email_address + + text not null + history_acting_user + + text not null + @@ -4982,6 +4995,37 @@

Tables

 

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ public."PasswordResetRequest" [table] +
verification_codetext not null
Primary Key
"PasswordResetRequest_pkey"[primary key]
verification_code
+

 

- + - +
public."PollMessage" [table] diff --git a/db/src/main/resources/sql/er_diagram/full_er_diagram.html b/db/src/main/resources/sql/er_diagram/full_er_diagram.html index 9799bd04c62..3edced83304 100644 --- a/db/src/main/resources/sql/er_diagram/full_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/full_er_diagram.html @@ -261,26 +261,26 @@

System Information

generated on2025-04-18 20:02:172025-04-30 16:04:45
last flyway fileV192__add_last_poc_verification_date.sqlV193__password_reset_request.sql

 

 

- - + + SchemaCrawler_Diagram - + generated by SchemaCrawler 16.25.2 generated on - 2025-04-18 20:02:17 + 2025-04-30 16:04:45 @@ -488,7 +488,7 @@

System Information

fk_billing_event_cancellation_matching_billing_recurrence_id - + registrar_6e1503e3 @@ -1233,7 +1233,7 @@

System Information

fk_domain_transfer_losing_registrar_id
- + tld_f1fa57e2 @@ -1999,7 +1999,7 @@

System Information

text not null
- + user_f2216f01 @@ -3163,92 +3163,120 @@

System Information

text not null -
+
+ + passwordresetrequest_8484e7b1 + + + public."PasswordResetRequest" + + [table] + type + + text not null + request_time + + timestamptz not null + requester + + text not null + fulfillment_time + + timestamptz + destination_email + + text not null + verification_code + + text not null + + + premiumentry_b0060b91 - - public."PremiumEntry" - - [table] - revision_id - - int8 not null - price - - numeric(19, 2) not null - domain_label - - text not null - + + public."PremiumEntry" + + [table] + revision_id + + int8 not null + price + + numeric(19, 2) not null + domain_label + + text not null + - + premiumlist_7c3ea68b - - public."PremiumList" - - [table] - revision_id - - bigserial not null - - auto-incremented - creation_timestamp - - timestamptz - name - - text not null - bloom_filter - - bytea not null - currency - - text not null - + + public."PremiumList" + + [table] + revision_id + + bigserial not null + + auto-incremented + creation_timestamp + + timestamptz + name + + text not null + bloom_filter + + bytea not null + currency + + text not null + premiumentry_b0060b91:w->premiumlist_7c3ea68b:e - - - - - - - - fko0gw90lpo1tuee56l0nb6y6g5 + + + + + + + + fko0gw90lpo1tuee56l0nb6y6g5 - + rderevision_83396864 - - public."RdeRevision" - - [table] - tld - - text not null - mode - - text not null - "date" - - date not null - update_timestamp - - timestamptz - revision - - int4 not null - + + public."RdeRevision" + + [table] + tld + + text not null + mode + + text not null + "date" + + date not null + update_timestamp + + timestamptz + revision + + int4 not null + - + registrarpoc_ab47054d @@ -3310,7 +3338,7 @@

System Information

fk_registrar_poc_registrar_id
- + registrarupdatehistory_8a38bed4 @@ -3492,7 +3520,7 @@

System Information

fkregistrarupdatehistoryregistrarid
- + registrarpocupdatehistory_31e5d9aa @@ -3591,304 +3619,304 @@

System Information

fkregistrarpocupdatehistoryemailaddress
- + registrylock_ac88663e - - public."RegistryLock" - - [table] - revision_id - - bigserial not null - - auto-incremented - lock_completion_time - - timestamptz - lock_request_time - - timestamptz not null - domain_name - - text not null - is_superuser - - bool not null - registrar_id - - text not null - registrar_poc_id - - text - repo_id - - text not null - verification_code - - text not null - unlock_request_time - - timestamptz - unlock_completion_time - - timestamptz - last_update_time - - timestamptz not null - relock_revision_id - - int8 - relock_duration - - interval - + + public."RegistryLock" + + [table] + revision_id + + bigserial not null + + auto-incremented + lock_completion_time + + timestamptz + lock_request_time + + timestamptz not null + domain_name + + text not null + is_superuser + + bool not null + registrar_id + + text not null + registrar_poc_id + + text + repo_id + + text not null + verification_code + + text not null + unlock_request_time + + timestamptz + unlock_completion_time + + timestamptz + last_update_time + + timestamptz not null + relock_revision_id + + int8 + relock_duration + + interval + registrylock_ac88663e:w->registrylock_ac88663e:e - - - - - - - - fk2lhcwpxlnqijr96irylrh1707 + + + + + + + + fk2lhcwpxlnqijr96irylrh1707 - + reservedentry_1a7b8520 - - public."ReservedEntry" - - [table] - revision_id - - int8 not null - comment - - text - reservation_type - - int4 not null - domain_label - - text not null - + + public."ReservedEntry" + + [table] + revision_id + + int8 not null + comment + + text + reservation_type + + int4 not null + domain_label + + text not null + - + reservedlist_b97c3f1c - - public."ReservedList" - - [table] - revision_id - - bigserial not null - - auto-incremented - creation_timestamp - - timestamptz not null - name - - text not null - + + public."ReservedList" + + [table] + revision_id + + bigserial not null + + auto-incremented + creation_timestamp + + timestamptz not null + name + + text not null + reservedentry_1a7b8520:w->reservedlist_b97c3f1c:e - - - - - - - - fkgq03rk0bt1hb915dnyvd3vnfc + + + + + + + + fkgq03rk0bt1hb915dnyvd3vnfc - + serversecret_6cc90f09 - - public."ServerSecret" - - [table] - secret - - uuid not null - id - - int8 not null - + + public."ServerSecret" + + [table] + secret + + uuid not null + id + + int8 not null + - + signedmarkrevocationentry_99c39721 - - public."SignedMarkRevocationEntry" - - [table] - revision_id - - int8 not null - revocation_time - - timestamptz not null - smd_id - - text not null - + + public."SignedMarkRevocationEntry" + + [table] + revision_id + + int8 not null + revocation_time + + timestamptz not null + smd_id + + text not null + - + signedmarkrevocationlist_c5d968fb - - public."SignedMarkRevocationList" - - [table] - revision_id - - bigserial not null - - auto-incremented - creation_time - - timestamptz - + + public."SignedMarkRevocationList" + + [table] + revision_id + + bigserial not null + + auto-incremented + creation_time + + timestamptz + signedmarkrevocationentry_99c39721:w->signedmarkrevocationlist_c5d968fb:e - - - - - - - - fk5ivlhvs3121yx2li5tqh54u4 + + + + + + + + fk5ivlhvs3121yx2li5tqh54u4 - + spec11threatmatch_a61228a6 - - public."Spec11ThreatMatch" - - [table] - id - - bigserial not null - - auto-incremented - check_date - - date not null - domain_name - - text not null - domain_repo_id - - text not null - registrar_id - - text not null - threat_types - - _text not null - tld - - text not null - + + public."Spec11ThreatMatch" + + [table] + id + + bigserial not null + + auto-incremented + check_date + + date not null + domain_name + + text not null + domain_repo_id + + text not null + registrar_id + + text not null + threat_types + + _text not null + tld + + text not null + - + tmchcrl_d282355 - - public."TmchCrl" - - [table] - certificate_revocations - - text not null - update_timestamp - - timestamptz not null - url - - text not null - id - - int8 not null - + + public."TmchCrl" + + [table] + certificate_revocations + + text not null + update_timestamp + + timestamptz not null + url + + text not null + id + + int8 not null + - + userupdatehistory_24efd476 - - public."UserUpdateHistory" - - [table] - history_revision_id - - int8 not null - history_modification_time - - timestamptz not null - history_method - - text not null - history_request_body - - text - history_type - - text not null - history_url - - text not null - email_address - - text not null - registry_lock_password_hash - - text - registry_lock_password_salt - - text - global_role - - text not null - is_admin - - bool not null - registrar_roles - - hstore - update_timestamp - - timestamptz - history_acting_user - - text not null - registry_lock_email_address - - text - + + public."UserUpdateHistory" + + [table] + history_revision_id + + int8 not null + history_modification_time + + timestamptz not null + history_method + + text not null + history_request_body + + text + history_type + + text not null + history_url + + text not null + email_address + + text not null + registry_lock_password_hash + + text + registry_lock_password_salt + + text + global_role + + text not null + is_admin + + bool not null + registrar_roles + + hstore + update_timestamp + + timestamptz + history_acting_user + + text not null + registry_lock_email_address + + text + @@ -9870,6 +9898,80 @@

Tables

 

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ public."PasswordResetRequest" [table] +
typetext not null
request_timetimestamptz not null
requestertext not null
fulfillment_timetimestamptz
destination_emailtext not null
verification_codetext not null
Primary Key
"PasswordResetRequest_pkey"[primary key]
verification_code
Indexes
"PasswordResetRequest_pkey"[unique index]
verification_codeascending
+

 

- + - +
public."PollMessage" [table] diff --git a/db/src/main/resources/sql/flyway.txt b/db/src/main/resources/sql/flyway.txt index 7a24901bb6c..92c8eb0c702 100644 --- a/db/src/main/resources/sql/flyway.txt +++ b/db/src/main/resources/sql/flyway.txt @@ -190,3 +190,4 @@ V189__remove_fk_consoleeppactionhistory.sql V190__remove_fk_registrarupdatehistory.sql V191__remove_fk_registrarpocupdatehistory.sql V192__add_last_poc_verification_date.sql +V193__password_reset_request.sql diff --git a/db/src/main/resources/sql/flyway/V193__password_reset_request.sql b/db/src/main/resources/sql/flyway/V193__password_reset_request.sql new file mode 100644 index 00000000000..737df821aa4 --- /dev/null +++ b/db/src/main/resources/sql/flyway/V193__password_reset_request.sql @@ -0,0 +1,23 @@ +-- 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. + +CREATE TABLE "PasswordResetRequest" ( + type text NOT NULL, + request_time timestamptz NOT NULL, + requester text NOT NULL, + fulfillment_time timestamptz, + destination_email text NOT NULL, + verification_code text NOT NULL, + PRIMARY KEY (verification_code) +); diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql index 6dff607ea10..f8962b232c5 100644 --- a/db/src/main/resources/sql/schema/nomulus.golden.sql +++ b/db/src/main/resources/sql/schema/nomulus.golden.sql @@ -842,6 +842,20 @@ CREATE SEQUENCE public."Package_promotion_id_seq" ALTER SEQUENCE public."Package_promotion_id_seq" OWNED BY public."PackagePromotion".package_promotion_id; +-- +-- Name: PasswordResetRequest; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public."PasswordResetRequest" ( + type text NOT NULL, + request_time timestamp with time zone NOT NULL, + requester text NOT NULL, + fulfillment_time timestamp with time zone, + destination_email text NOT NULL, + verification_code text NOT NULL +); + + -- -- Name: PollMessage; Type: TABLE; Schema: public; Owner: - -- @@ -1682,6 +1696,14 @@ ALTER TABLE ONLY public."PackagePromotion" ADD CONSTRAINT "PackagePromotion_pkey" PRIMARY KEY (package_promotion_id); +-- +-- Name: PasswordResetRequest PasswordResetRequest_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."PasswordResetRequest" + ADD CONSTRAINT "PasswordResetRequest_pkey" PRIMARY KEY (verification_code); + + -- -- Name: PollMessage PollMessage_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- From f5816323ec4f659f7b93aaad6cbb7cadac3b57b4 Mon Sep 17 00:00:00 2001 From: Pavlo Tkach <3469726+ptkach@users.noreply.github.com> Date: Mon, 12 May 2025 16:14:56 -0400 Subject: [PATCH 32/49] Add Console POC reminder front-end (#2754) --- console-webapp/src/app/app.component.spec.ts | 108 ++++++++++++++++-- console-webapp/src/app/app.component.ts | 28 ++++- console-webapp/src/app/app.module.ts | 2 + .../app/navigation/navigation.component.ts | 2 +- .../src/app/registrar/registrar.service.ts | 1 + .../pocReminder/pocReminder.component.html | 14 +++ .../pocReminder/pocReminder.component.scss | 5 + .../pocReminder/pocReminder.component.ts | 53 +++++++++ .../console/ConsoleUpdateRegistrarAction.java | 35 ++++-- .../ConsoleUpdateRegistrarActionTest.java | 49 ++++++-- .../webdriver/ConsoleScreenshotTest.java | 8 ++ 11 files changed, 273 insertions(+), 32 deletions(-) create mode 100644 console-webapp/src/app/shared/components/pocReminder/pocReminder.component.html create mode 100644 console-webapp/src/app/shared/components/pocReminder/pocReminder.component.scss create mode 100644 console-webapp/src/app/shared/components/pocReminder/pocReminder.component.ts diff --git a/console-webapp/src/app/app.component.spec.ts b/console-webapp/src/app/app.component.spec.ts index 235724aea80..ed224c7bd71 100644 --- a/console-webapp/src/app/app.component.spec.ts +++ b/console-webapp/src/app/app.component.spec.ts @@ -14,30 +14,71 @@ import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { AppComponent } from './app.component'; -import { MaterialModule } from './material.module'; -import { BackendService } from './shared/services/backend.service'; -import { AppRoutingModule } from './app-routing.module'; +import { routes } from './app-routing.module'; import { AppModule } from './app.module'; +import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component'; +import { RouterModule } from '@angular/router'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { UserData, UserDataService } from './shared/services/userData.service'; +import { Registrar, RegistrarService } from './registrar/registrar.service'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { signal, WritableSignal } from '@angular/core'; describe('AppComponent', () => { + let component: AppComponent; + let fixture: ComponentFixture; + let mockRegistrarService: { + registrar: WritableSignal | null | undefined>; + registrarId: WritableSignal; + registrars: WritableSignal>>; + }; + let mockUserDataService: { userData: WritableSignal> }; + let mockSnackBar: jasmine.SpyObj; + + const dummyPocReminderComponent = class {}; // Dummy class for type checking + beforeEach(async () => { + mockRegistrarService = { + registrar: signal(undefined), + registrarId: signal('123'), + registrars: signal([]), + }; + + mockUserDataService = { + userData: signal({ + globalRole: 'NONE', + }), + }; + + mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']); + await TestBed.configureTestingModule({ - declarations: [AppComponent], imports: [ - MaterialModule, - BrowserAnimationsModule, - AppRoutingModule, + MatSidenavModule, + NoopAnimationsModule, + MatSnackBarModule, AppModule, + RouterModule.forRoot(routes), ], providers: [ - BackendService, + { provide: RegistrarService, useValue: mockRegistrarService }, + { provide: UserDataService, useValue: mockUserDataService }, + { provide: MatSnackBar, useValue: mockSnackBar }, + { provide: PocReminderComponent, useClass: dummyPocReminderComponent }, provideHttpClient(), provideHttpClientTesting(), ], }).compileComponents(); + + fixture = TestBed.createComponent(AppComponent); + component = fixture.componentInstance; + }); + + afterEach(() => { + jasmine.clock().uninstall(); }); it('should create the app', () => { @@ -46,4 +87,51 @@ describe('AppComponent', () => { const app = fixture.componentInstance; expect(app).toBeTruthy(); }); + + describe('PoC Verification Reminder', () => { + beforeEach(() => { + jasmine.clock().install(); + }); + + it('should open snackbar if lastPocVerificationDate is older than one year', fakeAsync(() => { + const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z'); + jasmine.clock().mockDate(MOCK_TODAY); + + const twoYearsAgo = new Date(MOCK_TODAY); + twoYearsAgo.setFullYear(MOCK_TODAY.getFullYear() - 2); + + mockRegistrarService.registrar.set({ + lastPocVerificationDate: twoYearsAgo.toISOString(), + }); + + fixture.detectChanges(); + TestBed.flushEffects(); + + expect(mockSnackBar.openFromComponent).toHaveBeenCalledWith( + PocReminderComponent, + { + horizontalPosition: 'center', + verticalPosition: 'top', + duration: 1000000000, + } + ); + })); + + it('should NOT open snackbar if lastPocVerificationDate is within last year', fakeAsync(() => { + const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z'); + jasmine.clock().mockDate(MOCK_TODAY); + + const sixMonthsAgo = new Date(MOCK_TODAY); + sixMonthsAgo.setMonth(MOCK_TODAY.getMonth() - 6); + + mockRegistrarService.registrar.set({ + lastPocVerificationDate: sixMonthsAgo.toISOString(), + }); + + fixture.detectChanges(); + TestBed.flushEffects(); + + expect(mockSnackBar.openFromComponent).not.toHaveBeenCalled(); + })); + }); }); diff --git a/console-webapp/src/app/app.component.ts b/console-webapp/src/app/app.component.ts index 6433260c5d4..429544ff487 100644 --- a/console-webapp/src/app/app.component.ts +++ b/console-webapp/src/app/app.component.ts @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { AfterViewInit, Component, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, effect, ViewChild } from '@angular/core'; import { MatSidenav } from '@angular/material/sidenav'; import { NavigationEnd, Router } from '@angular/router'; import { RegistrarService } from './registrar/registrar.service'; import { BreakPointObserverService } from './shared/services/breakPoint.service'; import { GlobalLoaderService } from './shared/services/globalLoader.service'; import { UserDataService } from './shared/services/userData.service'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component'; @Component({ selector: 'app-root', @@ -35,8 +37,28 @@ export class AppComponent implements AfterViewInit { protected userDataService: UserDataService, protected globalLoader: GlobalLoaderService, protected breakpointObserver: BreakPointObserverService, - private router: Router - ) {} + private router: Router, + private _snackBar: MatSnackBar + ) { + effect(() => { + const registrar = this.registrarService.registrar(); + const oneYearAgo = new Date(); + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); + oneYearAgo.setHours(0, 0, 0, 0); + if ( + registrar && + registrar.lastPocVerificationDate && + new Date(registrar.lastPocVerificationDate) < oneYearAgo && + this.userDataService?.userData()?.globalRole === 'NONE' + ) { + this._snackBar.openFromComponent(PocReminderComponent, { + horizontalPosition: 'center', + verticalPosition: 'top', + duration: 1000000000, + }); + } + }); + } ngAfterViewInit() { this.router.events.subscribe((event) => { diff --git a/console-webapp/src/app/app.module.ts b/console-webapp/src/app/app.module.ts index 6e4341f839f..45cc67cd761 100644 --- a/console-webapp/src/app/app.module.ts +++ b/console-webapp/src/app/app.module.ts @@ -60,6 +60,7 @@ import { TldsComponent } from './tlds/tlds.component'; import { ForceFocusDirective } from './shared/directives/forceFocus.directive'; import RdapComponent from './settings/rdap/rdap.component'; import RdapEditComponent from './settings/rdap/rdapEdit.component'; +import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component'; @NgModule({ declarations: [SelectedRegistrarWrapper], @@ -86,6 +87,7 @@ export class SelectedRegistrarModule {} RdapComponent, RdapEditComponent, ReasonDialogComponent, + PocReminderComponent, RegistrarComponent, RegistrarDetailsComponent, RegistrarSelectorComponent, diff --git a/console-webapp/src/app/navigation/navigation.component.ts b/console-webapp/src/app/navigation/navigation.component.ts index 0315da596fb..9562eaf0658 100644 --- a/console-webapp/src/app/navigation/navigation.component.ts +++ b/console-webapp/src/app/navigation/navigation.component.ts @@ -57,7 +57,7 @@ export class NavigationComponent { } ngOnDestroy() { - this.subscription.unsubscribe(); + this.subscription && this.subscription.unsubscribe(); } getElementId(node: RouteWithIcon) { diff --git a/console-webapp/src/app/registrar/registrar.service.ts b/console-webapp/src/app/registrar/registrar.service.ts index 27bd7052580..e3a0708d800 100644 --- a/console-webapp/src/app/registrar/registrar.service.ts +++ b/console-webapp/src/app/registrar/registrar.service.ts @@ -71,6 +71,7 @@ export interface Registrar registrarName: string; registryLockAllowed?: boolean; type?: string; + lastPocVerificationDate?: string; } @Injectable({ diff --git a/console-webapp/src/app/shared/components/pocReminder/pocReminder.component.html b/console-webapp/src/app/shared/components/pocReminder/pocReminder.component.html new file mode 100644 index 00000000000..d9915cc9ba6 --- /dev/null +++ b/console-webapp/src/app/shared/components/pocReminder/pocReminder.component.html @@ -0,0 +1,14 @@ +
+

+ Please take a moment to complete annual review of + contacts. +

+ + + + +
diff --git a/console-webapp/src/app/shared/components/pocReminder/pocReminder.component.scss b/console-webapp/src/app/shared/components/pocReminder/pocReminder.component.scss new file mode 100644 index 00000000000..39194098d08 --- /dev/null +++ b/console-webapp/src/app/shared/components/pocReminder/pocReminder.component.scss @@ -0,0 +1,5 @@ +.console-app__pocReminder { + a { + color: white !important; + } +} diff --git a/console-webapp/src/app/shared/components/pocReminder/pocReminder.component.ts b/console-webapp/src/app/shared/components/pocReminder/pocReminder.component.ts new file mode 100644 index 00000000000..1e3339ccf34 --- /dev/null +++ b/console-webapp/src/app/shared/components/pocReminder/pocReminder.component.ts @@ -0,0 +1,53 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component } from '@angular/core'; +import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar'; +import { RegistrarService } from '../../../registrar/registrar.service'; +import { HttpErrorResponse } from '@angular/common/http'; + +@Component({ + selector: 'app-poc-reminder', + templateUrl: './pocReminder.component.html', + styleUrls: ['./pocReminder.component.scss'], + standalone: false, +}) +export class PocReminderComponent { + constructor( + public snackBarRef: MatSnackBarRef, + private registrarService: RegistrarService, + private _snackBar: MatSnackBar + ) {} + + confirmReviewed() { + if (this.registrarService.registrar()) { + const todayMidnight = new Date(); + todayMidnight.setHours(0, 0, 0, 0); + this.registrarService + // @ts-ignore - if check above won't allow empty object to be submitted + .updateRegistrar({ + ...this.registrarService.registrar(), + lastPocVerificationDate: todayMidnight.toISOString(), + }) + .subscribe({ + error: (err: HttpErrorResponse) => { + this._snackBar.open(err.error || err.message); + }, + next: () => { + this.snackBarRef.dismiss(); + }, + }); + } + } +} diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java index c3d5cfdba01..6f72372eb25 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java @@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.request.Action.Method.POST; +import static google.registry.util.DateTimeUtils.START_OF_TIME; import static google.registry.util.PreconditionsUtils.checkArgumentPresent; import static org.apache.http.HttpStatus.SC_OK; @@ -37,6 +38,7 @@ import jakarta.inject.Inject; import java.util.Optional; import java.util.stream.Collectors; +import org.joda.time.DateTime; @Action( service = GaeService.DEFAULT, @@ -88,18 +90,35 @@ protected void postHandler(User user) { } } - Registrar updatedRegistrar = + DateTime now = tm().getTransactionTime(); + DateTime newLastPocVerificationDate = + registrarParam.getLastPocVerificationDate() == null + ? START_OF_TIME + : registrarParam.getLastPocVerificationDate(); + + checkArgument( + newLastPocVerificationDate.isBefore(now), + "Invalid value of LastPocVerificationDate - value is in the future"); + + var updatedRegistrarBuilder = existingRegistrar .get() .asBuilder() - .setAllowedTlds( - registrarParam.getAllowedTlds().stream() - .map(DomainNameUtils::canonicalizeHostname) - .collect(Collectors.toSet())) - .setRegistryLockAllowed(registrarParam.isRegistryLockAllowed()) - .setLastPocVerificationDate(registrarParam.getLastPocVerificationDate()) - .build(); + .setLastPocVerificationDate(newLastPocVerificationDate); + + if (user.getUserRoles() + .hasGlobalPermission(ConsolePermission.EDIT_REGISTRAR_DETAILS)) { + updatedRegistrarBuilder = + updatedRegistrarBuilder + .setAllowedTlds( + registrarParam.getAllowedTlds().stream() + .map(DomainNameUtils::canonicalizeHostname) + .collect(Collectors.toSet())) + .setRegistryLockAllowed(registrarParam.isRegistryLockAllowed()) + .setLastPocVerificationDate(newLastPocVerificationDate); + } + var updatedRegistrar = updatedRegistrarBuilder.build(); tm().put(updatedRegistrar); finishAndPersistConsoleUpdateHistory( new ConsoleUpdateHistory.Builder() diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java index 56f12577241..3ff07068446 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java @@ -40,6 +40,7 @@ import google.registry.request.RequestModule; import google.registry.request.auth.AuthResult; import google.registry.testing.ConsoleApiParamsUtils; +import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.SystemPropertyExtension; import google.registry.tools.GsonUtils; @@ -51,6 +52,7 @@ import java.io.IOException; import java.io.StringReader; import java.util.Optional; +import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -59,7 +61,7 @@ /** Tests for {@link google.registry.ui.server.console.ConsoleUpdateRegistrarAction}. */ class ConsoleUpdateRegistrarActionTest { private static final Gson GSON = GsonUtils.provideGson(); - + private final FakeClock clock = new FakeClock(DateTime.parse("2025-01-01T00:00:00.000Z")); private ConsoleApiParams consoleApiParams; private FakeResponse response; @@ -75,6 +77,10 @@ class ConsoleUpdateRegistrarActionTest { @Order(Integer.MAX_VALUE) final SystemPropertyExtension systemPropertyExtension = new SystemPropertyExtension(); + @RegisterExtension + final JpaTestExtensions.JpaIntegrationTestExtension jpa = + new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension(); + @BeforeEach void beforeEach() throws Exception { createTlds("app", "dev"); @@ -95,10 +101,6 @@ void beforeEach() throws Exception { consoleApiParams = createParams(); } - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().buildIntegrationTestExtension(); - @Test void testSuccess_updatesRegistrar() throws IOException { var action = @@ -108,7 +110,7 @@ void testSuccess_updatesRegistrar() throws IOException { "TheRegistrar", "app, dev", false, - "\"2025-01-01T00:00:00.000Z\"")); + "\"2024-12-12T00:00:00.000Z\"")); action.run(); Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get(); assertThat(newRegistrar.getAllowedTlds()).containsExactly("app", "dev"); @@ -119,6 +121,33 @@ void testSuccess_updatesRegistrar() throws IOException { assertThat(history.getDescription()).hasValue("TheRegistrar"); } + @Test + void testSuccess_updatesNullPocVerificationDate() throws IOException { + var action = + createAction( + String.format(registrarPostData, "TheRegistrar", "app, dev", false, "\"null\"")); + action.run(); + Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get(); + assertThat(newRegistrar.getLastPocVerificationDate()) + .isEqualTo(DateTime.parse("1970-01-01T00:00:00.000Z")); + } + + @Test + void testFailure_pocVerificationInTheFuture() throws IOException { + var action = + createAction( + String.format( + registrarPostData, + "TheRegistrar", + "app, dev", + false, + "\"2025-02-01T00:00:00.000Z\"")); + action.run(); + assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat((String) ((FakeResponse) consoleApiParams.response()).getPayload()) + .contains("Invalid value of LastPocVerificationDate - value is in the future"); + } + @Test void testFails_missingWhoisContact() throws IOException { RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension); @@ -129,7 +158,7 @@ void testFails_missingWhoisContact() throws IOException { "TheRegistrar", "app, dev", false, - "\"2025-01-01T00:00:00.000Z\"")); + "\"2024-12-12T00:00:00.000Z\"")); action.run(); assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); assertThat((String) ((FakeResponse) consoleApiParams.response()).getPayload()) @@ -159,7 +188,7 @@ void testSuccess_presentWhoisContact() throws IOException { "TheRegistrar", "app, dev", false, - "\"2025-01-01T00:00:00.000Z\"")); + "\"2024-12-12T00:00:00.000Z\"")); action.run(); Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get(); assertThat(newRegistrar.getAllowedTlds()).containsExactly("app", "dev"); @@ -176,7 +205,7 @@ void testSuccess_sendsEmail() throws AddressException, IOException { "TheRegistrar", "app, dev", false, - "\"2025-01-01T00:00:00.000Z\"")); + "\"2024-12-12T00:00:00.000Z\"")); action.run(); verify(consoleApiParams.sendEmailUtils().gmailClient, times(1)) .sendEmail( @@ -190,7 +219,7 @@ void testSuccess_sendsEmail() throws AddressException, IOException { + "\n" + "allowedTlds: null -> [app, dev]\n" + "lastPocVerificationDate: 1970-01-01T00:00:00.000Z ->" - + " 2025-01-01T00:00:00.000Z\n") + + " 2024-12-12T00:00:00.000Z\n") .setRecipients(ImmutableList.of(new InternetAddress("notification@test.example"))) .build()); } diff --git a/core/src/test/java/google/registry/webdriver/ConsoleScreenshotTest.java b/core/src/test/java/google/registry/webdriver/ConsoleScreenshotTest.java index 679eeb0384e..b6981c51e53 100644 --- a/core/src/test/java/google/registry/webdriver/ConsoleScreenshotTest.java +++ b/core/src/test/java/google/registry/webdriver/ConsoleScreenshotTest.java @@ -16,12 +16,16 @@ import static com.google.common.truth.Truth.assertThat; import static google.registry.server.Fixture.BASIC; +import static google.registry.testing.DatabaseHelper.persistResource; import com.google.common.collect.ImmutableMap; import google.registry.model.console.GlobalRole; import google.registry.model.console.RegistrarRole; +import google.registry.model.registrar.Registrar; import google.registry.server.RegistryTestServer; import java.util.List; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; @@ -74,6 +78,10 @@ public class ConsoleScreenshotTest { @BeforeEach void beforeEach() throws Exception { server.setRegistrarRoles(ImmutableMap.of("TheRegistrar", RegistrarRole.ACCOUNT_MANAGER)); + Registrar registrar = Registrar.loadByRegistrarId("TheRegistrar").get(); + registrar = + registrar.asBuilder().setLastPocVerificationDate(DateTime.now(DateTimeZone.UTC)).build(); + persistResource(registrar); loadHomePage(); } From f50a51a91053f5416a7130fb44a479987db2bc95 Mon Sep 17 00:00:00 2001 From: gbrodman Date: Tue, 13 May 2025 14:00:30 -0400 Subject: [PATCH 33/49] Use the primary DB for DomainInfoFlow (#2750) This avoids potential replication lag issues when requesting info on domains that were just created. --- .../registry/flows/domain/DomainInfoFlow.java | 14 +++++++++++--- .../java/google/registry/flows/FlowModuleTest.java | 2 +- .../registry/flows/domain/DomainInfoFlowTest.java | 2 +- .../google/registry/flows/domain_check.xml | 11 +++++++++++ 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 core/src/test/resources/google/registry/flows/domain_check.xml diff --git a/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java b/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java index fee8622fc7a..01c5398913e 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainInfoFlow.java @@ -31,6 +31,7 @@ import google.registry.flows.FlowModule.RegistrarId; import google.registry.flows.FlowModule.Superuser; import google.registry.flows.FlowModule.TargetId; +import google.registry.flows.MutatingFlow; import google.registry.flows.TransactionalFlow; import google.registry.flows.annotations.ReportingSpec; import google.registry.flows.custom.DomainInfoFlowCustomLogic; @@ -53,6 +54,8 @@ import google.registry.model.eppoutput.EppResponse; import google.registry.model.eppoutput.EppResponse.ResponseExtension; import google.registry.model.reporting.IcannReportingTypes.ActivityReportField; +import google.registry.persistence.IsolationLevel; +import google.registry.persistence.PersistenceModule; import google.registry.util.Clock; import jakarta.inject.Inject; import java.util.Optional; @@ -62,8 +65,12 @@ * An EPP flow that returns information about a domain. * *

The registrar that owns the domain, and any registrar presenting a valid authInfo for the - * domain, will get a rich result with all of the domain's fields. All other requests will be - * answered with a minimal result containing only basic information about the domain. + * domain, will get a rich result with all the domain's fields. All other requests will be answered + * with a minimal result containing only basic information about the domain. + * + *

This implements {@link MutatingFlow} instead of {@link TransactionalFlow} as a workaround so + * that the common workflow of "create domain, then immediately get domain info" does not run into + * replication lag issues where the info command claims the domain does not exist. * * @error {@link google.registry.flows.FlowUtils.NotLoggedInException} * @error {@link google.registry.flows.FlowUtils.UnknownCurrencyEppException} @@ -76,7 +83,8 @@ * @error {@link DomainFlowUtils.TransfersAreAlwaysForOneYearException} */ @ReportingSpec(ActivityReportField.DOMAIN_INFO) -public final class DomainInfoFlow implements TransactionalFlow { +@IsolationLevel(PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ) +public final class DomainInfoFlow implements MutatingFlow { @Inject ExtensionManager extensionManager; @Inject ResourceCommand resourceCommand; diff --git a/core/src/test/java/google/registry/flows/FlowModuleTest.java b/core/src/test/java/google/registry/flows/FlowModuleTest.java index 331dd4aec05..1c8b209e237 100644 --- a/core/src/test/java/google/registry/flows/FlowModuleTest.java +++ b/core/src/test/java/google/registry/flows/FlowModuleTest.java @@ -58,7 +58,7 @@ void givenMutatingFlow_thenPrimaryTmIsUsed() throws EppException { @Test void givenNonMutatingFlow_thenReplicaTmIsUsed() throws EppException { - String eppInputXmlFilename = "domain_info.xml"; + String eppInputXmlFilename = "domain_check.xml"; FlowModule flowModule = new FlowModule.Builder().setEppInput(getEppInput(eppInputXmlFilename)).build(); JpaTransactionManager tm = diff --git a/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java index be04be30e05..5584faf3d70 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java @@ -179,7 +179,7 @@ private void doSuccessfulTest( ImmutableMap substitutions, boolean expectHistoryAndBilling) throws Exception { - assertMutatingFlow(false); + assertMutatingFlow(true); String expected = loadFile(expectedXmlFilename, updateSubstitutions(substitutions, "ROID", "2FF-TLD")); if (inactive) { diff --git a/core/src/test/resources/google/registry/flows/domain_check.xml b/core/src/test/resources/google/registry/flows/domain_check.xml new file mode 100644 index 00000000000..199bc1ddacd --- /dev/null +++ b/core/src/test/resources/google/registry/flows/domain_check.xml @@ -0,0 +1,11 @@ + + + + + %DOMAIN% + + + ABC-12345 + + From 442ee6482ed06b85d7e916fffba67951902b3aad Mon Sep 17 00:00:00 2001 From: Juan Celhay Date: Tue, 13 May 2025 16:29:25 -0400 Subject: [PATCH 34/49] Remove registrar id from invoice grouping key (#2749) * Remove registrar id from invoice grouping key * Fix formatting issues * Update BillingEventTests --- .../registry/beam/billing/BillingEvent.java | 2 +- .../beam/billing/BillingEventTest.java | 6 +- .../beam/billing/InvoicingPipelineTest.java | 102 ++++++++++++++---- 3 files changed, 85 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/google/registry/beam/billing/BillingEvent.java b/core/src/main/java/google/registry/beam/billing/BillingEvent.java index c8c936b58cd..5d2e3e4470e 100644 --- a/core/src/main/java/google/registry/beam/billing/BillingEvent.java +++ b/core/src/main/java/google/registry/beam/billing/BillingEvent.java @@ -172,7 +172,7 @@ InvoiceGroupingKey getInvoiceGroupingKey() { .minusDays(1) .toString(), billingId(), - registrarId(), + "", String.format("%s | TLD: %s | TERM: %d-year", action(), tld(), years()), amount(), currency(), diff --git a/core/src/test/java/google/registry/beam/billing/BillingEventTest.java b/core/src/test/java/google/registry/beam/billing/BillingEventTest.java index 4b01682c127..11dbb88d705 100644 --- a/core/src/test/java/google/registry/beam/billing/BillingEventTest.java +++ b/core/src/test/java/google/registry/beam/billing/BillingEventTest.java @@ -85,7 +85,7 @@ void testGetInvoiceGroupingKey_fromBillingEvent() { assertThat(invoiceKey.startDate()).isEqualTo("2017-10-01"); assertThat(invoiceKey.endDate()).isEqualTo("2022-09-30"); assertThat(invoiceKey.productAccountKey()).isEqualTo("12345-CRRHELLO"); - assertThat(invoiceKey.usageGroupingKey()).isEqualTo("myRegistrar"); + assertThat(invoiceKey.usageGroupingKey()).isEqualTo(""); assertThat(invoiceKey.description()).isEqualTo("RENEW | TLD: test | TERM: 5-year"); assertThat(invoiceKey.unitPrice()).isEqualTo(20.5); assertThat(invoiceKey.unitPriceCurrency()).isEqualTo("USD"); @@ -106,7 +106,7 @@ void testConvertInvoiceGroupingKey_toCsv() { assertThat(invoiceKey.toCsv(3L)) .isEqualTo( "2017-10-01,2022-09-30,12345-CRRHELLO,61.50,USD,10125,1,PURCHASE," - + "myRegistrar,3,RENEW | TLD: test | TERM: 5-year,20.50,USD,"); + + ",3,RENEW | TLD: test | TERM: 5-year,20.50,USD,"); } @Test @@ -116,7 +116,7 @@ void testConvertInvoiceGroupingKey_zeroYears_toCsv() { assertThat(invoiceKey.toCsv(3L)) .isEqualTo( "2017-10-01,,12345-CRRHELLO,61.50,USD,10125,1,PURCHASE," - + "myRegistrar,3,RENEW | TLD: test | TERM: 0-year,20.50,USD,"); + + ",3,RENEW | TLD: test | TERM: 0-year,20.50,USD,"); } @Test diff --git a/core/src/test/java/google/registry/beam/billing/InvoicingPipelineTest.java b/core/src/test/java/google/registry/beam/billing/InvoicingPipelineTest.java index 30b75e4e03d..6cad4a92842 100644 --- a/core/src/test/java/google/registry/beam/billing/InvoicingPipelineTest.java +++ b/core/src/test/java/google/registry/beam/billing/InvoicingPipelineTest.java @@ -199,6 +199,36 @@ class InvoicingPipelineTest { 0, "USD", 20.0, + ""), + google.registry.beam.billing.BillingEvent.create( + 15, + DateTime.parse("2017-10-02T00:00:00.0Z"), + DateTime.parse("2017-10-04T00:00:00.0Z"), + "theRegistrarCopy", + "234", + "", + "test", + "CREATE", + "mydomainfromanotherclient.test", + "REPO-ID", + 5, + "JPY", + 70.0, + ""), + google.registry.beam.billing.BillingEvent.create( + 16, + DateTime.parse("2017-10-04T00:00:00Z"), + DateTime.parse("2017-10-04T00:00:00Z"), + "theRegistrarCopy", + "234", + "", + "test", + "RENEW", + "mydomain2fromanotherclient.test", + "REPO-ID", + 3, + "USD", + 20.5, "")); private static final ImmutableMap> EXPECTED_DETAILED_REPORT_MAP = @@ -224,18 +254,26 @@ class InvoicingPipelineTest { "invoice_details_2017-10_anotherRegistrar_test.csv", ImmutableList.of( "5,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,anotherRegistrar,789,," - + "test,CREATE,mydomain5.test,REPO-ID,1,USD,0.00,SUNRISE ANCHOR_TENANT")); + + "test,CREATE,mydomain5.test,REPO-ID,1,USD,0.00,SUNRISE ANCHOR_TENANT"), + "invoice_details_2017-10_theRegistrarCopy_test.csv", + ImmutableList.of( + "15,2017-10-02 00:00:00 UTC,2017-10-04 00:00:00" + + " UTC,theRegistrarCopy,234,,test,CREATE,mydomainfromanotherclient.test,REPO-ID,5,JPY,70.00,", + "16,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00" + + " UTC,theRegistrarCopy,234,,test,RENEW,mydomain2fromanotherclient.test,REPO-ID,3,USD,20.50,")); private static final ImmutableList EXPECTED_INVOICE_OUTPUT = ImmutableList.of( - "2017-10-01,2020-09-30,234,41.00,USD,10125,1,PURCHASE,theRegistrar,2," + "2017-10-01,2020-09-30,234,61.50,USD,10125,1,PURCHASE,,3," + "RENEW | TLD: test | TERM: 3-year,20.50,USD,", - "2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,theRegistrar,1," + "2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,,1," + "CREATE | TLD: hello | TERM: 5-year,70.00,JPY,", - "2017-10-01,,234,20.00,USD,10125,1,PURCHASE,theRegistrar,1," + "2017-10-01,,234,20.00,USD,10125,1,PURCHASE,,1," + "SERVER_STATUS | TLD: test | TERM: 0-year,20.00,USD,", - "2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,bestdomains,1," - + "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688"); + "2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,,1," + + "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688", + "2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,,1,CREATE | TLD: test | TERM:" + + " 5-year,70.00,JPY,"); private final InvoicingPipelineOptions options = PipelineOptionsFactory.create().as(InvoicingPipelineOptions.class); @@ -355,21 +393,21 @@ void testSuccess_makeCloudSqlQuery() throws Exception { .isEqualTo( """ - SELECT b, r FROM BillingEvent b - JOIN Registrar r ON b.clientId = r.registrarId - JOIN Domain d ON b.domainRepoId = d.repoId - JOIN Tld t ON t.tldStr = d.tld - LEFT JOIN BillingCancellation c ON b.id = c.billingEvent - LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence - WHERE r.billingAccountMap IS NOT NULL - AND r.type = 'REAL' - AND t.invoicingEnabled IS TRUE - AND CAST(b.billingTime AS timestamp) - BETWEEN CAST('2017-10-01T00:00:00Z' AS timestamp) - AND CAST('2017-11-01T00:00:00Z' AS timestamp) - AND c.id IS NULL - AND cr.id IS NULL - """); +SELECT b, r FROM BillingEvent b +JOIN Registrar r ON b.clientId = r.registrarId +JOIN Domain d ON b.domainRepoId = d.repoId +JOIN Tld t ON t.tldStr = d.tld +LEFT JOIN BillingCancellation c ON b.id = c.billingEvent +LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence +WHERE r.billingAccountMap IS NOT NULL +AND r.type = 'REAL' +AND t.invoicingEnabled IS TRUE +AND CAST(b.billingTime AS timestamp) + BETWEEN CAST('2017-10-01T00:00:00Z' AS timestamp) + AND CAST('2017-11-01T00:00:00Z' AS timestamp) +AND c.id IS NULL +AND cr.id IS NULL +"""); } /** Returns the text contents of a file under the beamBucket/results directory. */ @@ -391,6 +429,13 @@ private static void setupCloudSql() { .setBillingAccountMap(ImmutableMap.of(JPY, "234", USD, "234")) .build(); persistResource(registrar1); + Registrar registrar11 = persistNewRegistrar("theRegistrarCopy"); + registrar11 = + registrar11 + .asBuilder() + .setBillingAccountMap(ImmutableMap.of(JPY, "234", USD, "234")) + .build(); + persistResource(registrar11); Registrar registrar2 = persistNewRegistrar("bestdomains"); registrar2 = registrar2 @@ -547,6 +592,21 @@ private static void setupCloudSql() { .setDomainHistory(domainHistoryRecurrence) .build(); persistResource(cancellationRecurrence); + + // Domains created for registrar with = key but != client id. + Domain domain14 = persistActiveDomain("mydomainfromanotherclient.test"); + Domain domain15 = persistActiveDomain("mydomain2fromanotherclient.test"); + + persistBillingEvent( + 15, + domain14, + registrar11, + Reason.CREATE, + 5, + Money.ofMajor(JPY, 70), + DateTime.parse("2017-10-04T00:00:00.0Z"), + DateTime.parse("2017-10-02T00:00:00.0Z")); + persistBillingEvent(16, domain15, registrar11, Reason.RENEW, 3, Money.of(USD, 20.5)); } private static DomainHistory persistDomainHistory(Domain domain, Registrar registrar) { From f1f75885998effc674eda60937ff69b54e8adf10 Mon Sep 17 00:00:00 2001 From: gbrodman Date: Thu, 15 May 2025 16:13:27 -0400 Subject: [PATCH 35/49] Add registrar_id col to password reset requests (#2756) This is just so that we can add an additional layer of security on verification --- .../sql/er_diagram/brief_er_diagram.html | 6 +- .../sql/er_diagram/full_er_diagram.html | 656 +++++++++--------- db/src/main/resources/sql/flyway.txt | 1 + ...V194__password_reset_request_registrar.sql | 17 + .../resources/sql/schema/nomulus.golden.sql | 3 +- 5 files changed, 355 insertions(+), 328 deletions(-) create mode 100644 db/src/main/resources/sql/flyway/V194__password_reset_request_registrar.sql diff --git a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html index 519144e3fb6..9afdc32450f 100644 --- a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html @@ -261,11 +261,11 @@

System Information

generated on2025-04-30 16:04:482025-05-15 19:22:21
last flyway fileV193__password_reset_request.sqlV194__password_reset_request_registrar.sql
@@ -280,7 +280,7 @@

System Information

generated by SchemaCrawler 16.25.2 generated on - 2025-04-30 16:04:48 + 2025-05-15 19:22:21 diff --git a/db/src/main/resources/sql/er_diagram/full_er_diagram.html b/db/src/main/resources/sql/er_diagram/full_er_diagram.html index 3edced83304..0e05a2c0875 100644 --- a/db/src/main/resources/sql/er_diagram/full_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/full_er_diagram.html @@ -261,26 +261,26 @@ <h2>System Information</h2> </tr> <tr> <td class="property_name">generated on</td> - <td class="property_value">2025-04-30 16:04:45</td> + <td class="property_value">2025-05-15 19:22:16</td> </tr> <tr> <td class="property_name">last flyway file</td> - <td id="lastFlywayFile" class="property_value">V193__password_reset_request.sql</td> + <td id="lastFlywayFile" class="property_value">V194__password_reset_request_registrar.sql</td> </tr> </tbody> </table> <p> </p> <p> </p> - <svg viewBox="0.00 0.00 5683.00 8128.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px"> - <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 8124)"> + <svg viewBox="0.00 0.00 5683.00 8146.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px"> + <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 8142)"> <title> SchemaCrawler_Diagram - + generated by SchemaCrawler 16.25.2 generated on - 2025-04-30 16:04:45 + 2025-05-15 19:22:16 @@ -3168,113 +3168,116 @@ <h2>System Information</h2> <title> passwordresetrequest_8484e7b1 - - public."PasswordResetRequest" - - [table] - type + + public."PasswordResetRequest" + + [table] + type + + text not null + request_time - text not null - request_time + timestamptz not null + requester - timestamptz not null - requester + text not null + fulfillment_time - text not null - fulfillment_time + timestamptz + destination_email - timestamptz - destination_email + text not null + verification_code text not null - verification_code + registrar_id text not null - + premiumentry_b0060b91 - - public."PremiumEntry" - - [table] - revision_id - - int8 not null - price - - numeric(19, 2) not null - domain_label - - text not null - + + public."PremiumEntry" + + [table] + revision_id + + int8 not null + price + + numeric(19, 2) not null + domain_label + + text not null + premiumlist_7c3ea68b - - public."PremiumList" - - [table] - revision_id - - bigserial not null - - auto-incremented - creation_timestamp - - timestamptz - name - - text not null - bloom_filter - - bytea not null - currency - - text not null - + + public."PremiumList" + + [table] + revision_id + + bigserial not null + + auto-incremented + creation_timestamp + + timestamptz + name + + text not null + bloom_filter + + bytea not null + currency + + text not null + premiumentry_b0060b91:w->premiumlist_7c3ea68b:e - - - - - - - - fko0gw90lpo1tuee56l0nb6y6g5 + + + + + + + + fko0gw90lpo1tuee56l0nb6y6g5 rderevision_83396864 - - public."RdeRevision" - - [table] - tld - - text not null - mode - - text not null - "date" - - date not null - update_timestamp - - timestamptz - revision - - int4 not null - + + public."RdeRevision" + + [table] + tld + + text not null + mode + + text not null + "date" + + date not null + update_timestamp + + timestamptz + revision + + int4 not null + @@ -3623,300 +3626,300 @@ <h2>System Information</h2> <title> registrylock_ac88663e - - public."RegistryLock" - - [table] - revision_id - - bigserial not null - - auto-incremented - lock_completion_time - - timestamptz - lock_request_time - - timestamptz not null - domain_name - - text not null - is_superuser - - bool not null - registrar_id - - text not null - registrar_poc_id - - text - repo_id - - text not null - verification_code - - text not null - unlock_request_time - - timestamptz - unlock_completion_time - - timestamptz - last_update_time - - timestamptz not null - relock_revision_id - - int8 - relock_duration - - interval - + + public."RegistryLock" + + [table] + revision_id + + bigserial not null + + auto-incremented + lock_completion_time + + timestamptz + lock_request_time + + timestamptz not null + domain_name + + text not null + is_superuser + + bool not null + registrar_id + + text not null + registrar_poc_id + + text + repo_id + + text not null + verification_code + + text not null + unlock_request_time + + timestamptz + unlock_completion_time + + timestamptz + last_update_time + + timestamptz not null + relock_revision_id + + int8 + relock_duration + + interval + registrylock_ac88663e:w->registrylock_ac88663e:e - - - - - - - - fk2lhcwpxlnqijr96irylrh1707 + + + + + + + + fk2lhcwpxlnqijr96irylrh1707 reservedentry_1a7b8520 - - public."ReservedEntry" - - [table] - revision_id - - int8 not null - comment - - text - reservation_type - - int4 not null - domain_label - - text not null - + + public."ReservedEntry" + + [table] + revision_id + + int8 not null + comment + + text + reservation_type + + int4 not null + domain_label + + text not null + reservedlist_b97c3f1c - - public."ReservedList" - - [table] - revision_id - - bigserial not null - - auto-incremented - creation_timestamp - - timestamptz not null - name - - text not null - + + public."ReservedList" + + [table] + revision_id + + bigserial not null + + auto-incremented + creation_timestamp + + timestamptz not null + name + + text not null + reservedentry_1a7b8520:w->reservedlist_b97c3f1c:e - - - - - - - - fkgq03rk0bt1hb915dnyvd3vnfc + + + + + + + + fkgq03rk0bt1hb915dnyvd3vnfc serversecret_6cc90f09 - - public."ServerSecret" - - [table] - secret - - uuid not null - id - - int8 not null - + + public."ServerSecret" + + [table] + secret + + uuid not null + id + + int8 not null + signedmarkrevocationentry_99c39721 - - public."SignedMarkRevocationEntry" - - [table] - revision_id - - int8 not null - revocation_time - - timestamptz not null - smd_id - - text not null - + + public."SignedMarkRevocationEntry" + + [table] + revision_id + + int8 not null + revocation_time + + timestamptz not null + smd_id + + text not null + signedmarkrevocationlist_c5d968fb - - public."SignedMarkRevocationList" - - [table] - revision_id - - bigserial not null - - auto-incremented - creation_time - - timestamptz - + + public."SignedMarkRevocationList" + + [table] + revision_id + + bigserial not null + + auto-incremented + creation_time + + timestamptz + signedmarkrevocationentry_99c39721:w->signedmarkrevocationlist_c5d968fb:e - - - - - - - - fk5ivlhvs3121yx2li5tqh54u4 + + + + + + + + fk5ivlhvs3121yx2li5tqh54u4 spec11threatmatch_a61228a6 - - public."Spec11ThreatMatch" - - [table] - id - - bigserial not null - - auto-incremented - check_date - - date not null - domain_name - - text not null - domain_repo_id - - text not null - registrar_id - - text not null - threat_types - - _text not null - tld - - text not null - + + public."Spec11ThreatMatch" + + [table] + id + + bigserial not null + + auto-incremented + check_date + + date not null + domain_name + + text not null + domain_repo_id + + text not null + registrar_id + + text not null + threat_types + + _text not null + tld + + text not null + tmchcrl_d282355 - - public."TmchCrl" - - [table] - certificate_revocations - - text not null - update_timestamp - - timestamptz not null - url - - text not null - id - - int8 not null - + + public."TmchCrl" + + [table] + certificate_revocations + + text not null + update_timestamp + + timestamptz not null + url + + text not null + id + + int8 not null + userupdatehistory_24efd476 - - public."UserUpdateHistory" - - [table] - history_revision_id - - int8 not null - history_modification_time - - timestamptz not null - history_method - - text not null - history_request_body - - text - history_type - - text not null - history_url - - text not null - email_address - - text not null - registry_lock_password_hash - - text - registry_lock_password_salt - - text - global_role - - text not null - is_admin - - bool not null - registrar_roles - - hstore - update_timestamp - - timestamptz - history_acting_user - - text not null - registry_lock_email_address - - text - + + public."UserUpdateHistory" + + [table] + history_revision_id + + int8 not null + history_modification_time + + timestamptz not null + history_method + + text not null + history_request_body + + text + history_type + + text not null + history_url + + text not null + email_address + + text not null + registry_lock_password_hash + + text + registry_lock_password_salt + + text + global_role + + text not null + is_admin + + bool not null + registrar_roles + + hstore + update_timestamp + + timestamptz + history_acting_user + + text not null + registry_lock_email_address + + text + @@ -9933,6 +9936,11 @@

Tables

verification_code text not null + + + registrar_id + text not null + diff --git a/db/src/main/resources/sql/flyway.txt b/db/src/main/resources/sql/flyway.txt index 92c8eb0c702..6904f7abb3d 100644 --- a/db/src/main/resources/sql/flyway.txt +++ b/db/src/main/resources/sql/flyway.txt @@ -191,3 +191,4 @@ V190__remove_fk_registrarupdatehistory.sql V191__remove_fk_registrarpocupdatehistory.sql V192__add_last_poc_verification_date.sql V193__password_reset_request.sql +V194__password_reset_request_registrar.sql diff --git a/db/src/main/resources/sql/flyway/V194__password_reset_request_registrar.sql b/db/src/main/resources/sql/flyway/V194__password_reset_request_registrar.sql new file mode 100644 index 00000000000..a2a8f240bc8 --- /dev/null +++ b/db/src/main/resources/sql/flyway/V194__password_reset_request_registrar.sql @@ -0,0 +1,17 @@ +-- Copyright 2025 The Nomulus Authors. All Rights Reserved. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +ALTER TABLE "PasswordResetRequest" ADD COLUMN registrar_id text; +UPDATE "PasswordResetRequest" SET registrar_id = '' WHERE registrar_id IS NULL; +ALTER TABLE "PasswordResetRequest" ALTER COLUMN registrar_id SET NOT NULL; diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql index f8962b232c5..809413e1519 100644 --- a/db/src/main/resources/sql/schema/nomulus.golden.sql +++ b/db/src/main/resources/sql/schema/nomulus.golden.sql @@ -852,7 +852,8 @@ CREATE TABLE public."PasswordResetRequest" ( requester text NOT NULL, fulfillment_time timestamp with time zone, destination_email text NOT NULL, - verification_code text NOT NULL + verification_code text NOT NULL, + registrar_id text NOT NULL ); From 829866dc8a01cb33b9c092906253acca4c734cb1 Mon Sep 17 00:00:00 2001 From: gbrodman Date: Thu, 15 May 2025 16:22:07 -0400 Subject: [PATCH 36/49] Don't always require contacts in CreateDomainCommand (#2755) If contacts are optional, they should be optional in the command too. --- .../registry/tools/CreateDomainCommand.java | 14 +++++-- .../registry/tools/soy/DomainCreate.soy | 26 ++++++++----- .../tools/CreateDomainCommandTest.java | 39 +++++++++++++++---- .../tools/server/domain_create_contacts.xml | 19 +++++++++ ...abc.xml => domain_create_contacts_abc.xml} | 0 .../tools/server/domain_create_minimal.xml | 3 -- 6 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 core/src/test/resources/google/registry/tools/server/domain_create_contacts.xml rename core/src/test/resources/google/registry/tools/server/{domain_create_minimal_abc.xml => domain_create_contacts_abc.xml} (100%) diff --git a/core/src/main/java/google/registry/tools/CreateDomainCommand.java b/core/src/main/java/google/registry/tools/CreateDomainCommand.java index 2c896c38b3a..dc408f0cf34 100644 --- a/core/src/main/java/google/registry/tools/CreateDomainCommand.java +++ b/core/src/main/java/google/registry/tools/CreateDomainCommand.java @@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; import static org.joda.time.DateTimeZone.UTC; @@ -23,6 +24,7 @@ import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.google.template.soy.data.SoyMapData; +import google.registry.model.common.FeatureFlag; import google.registry.model.pricing.PremiumPricingEngine.DomainPrices; import google.registry.tools.soy.DomainCreateSoyInfo; import google.registry.util.StringGenerator; @@ -58,9 +60,15 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand { @Override protected void initMutatingEppToolCommand() { - checkArgumentNotNull(registrant, "Registrant must be specified"); - checkArgument(!admins.isEmpty(), "At least one admin must be specified"); - checkArgument(!techs.isEmpty(), "At least one tech must be specified"); + tm().transact( + () -> { + if (!FeatureFlag.isActiveNowOrElse( + FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL, false)) { + checkArgumentNotNull(registrant, "Registrant must be specified"); + checkArgument(!admins.isEmpty(), "At least one admin must be specified"); + checkArgument(!techs.isEmpty(), "At least one tech must be specified"); + } + }); if (isNullOrEmpty(password)) { password = passwordGenerator.createString(PASSWORD_LENGTH); } diff --git a/core/src/main/resources/google/registry/tools/soy/DomainCreate.soy b/core/src/main/resources/google/registry/tools/soy/DomainCreate.soy index b7eeb825891..9b20b85d61e 100644 --- a/core/src/main/resources/google/registry/tools/soy/DomainCreate.soy +++ b/core/src/main/resources/google/registry/tools/soy/DomainCreate.soy @@ -20,9 +20,9 @@ {@param domain: string} {@param period: int} {@param nameservers: list} - {@param registrant: string} - {@param admins: list} - {@param techs: list} + {@param? registrant: string|null} + {@param? admins: list|null} + {@param? techs: list|null} {@param password: string} {@param? currency: string|null} {@param? price: string|null} @@ -45,13 +45,19 @@ {/for} {/if} - {$registrant} - {for $admin in $admins} - {$admin} - {/for} - {for $tech in $techs} - {$tech} - {/for} + {if $registrant != null} + {$registrant} + {/if} + {if $admins != null} + {for $admin in $admins} + {$admin} + {/for} + {/if} + {if $techs != null} + {for $tech in $techs} + {$tech} + {/for} + {/if} {$password} diff --git a/core/src/test/java/google/registry/tools/CreateDomainCommandTest.java b/core/src/test/java/google/registry/tools/CreateDomainCommandTest.java index a05c557e71d..000486cae6a 100644 --- a/core/src/test/java/google/registry/tools/CreateDomainCommandTest.java +++ b/core/src/test/java/google/registry/tools/CreateDomainCommandTest.java @@ -15,16 +15,21 @@ package google.registry.tools; import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL; +import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.persistPremiumList; import static google.registry.testing.DatabaseHelper.persistResource; +import static google.registry.util.DateTimeUtils.START_OF_TIME; import static org.joda.money.CurrencyUnit.JPY; import static org.junit.jupiter.api.Assertions.assertThrows; import com.beust.jcommander.ParameterException; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedMap; import google.registry.dns.writer.VoidDnsWriter; +import google.registry.model.common.FeatureFlag; import google.registry.model.pricing.StaticPremiumListPricingEngine; import google.registry.model.tld.Tld; import google.registry.model.tld.label.PremiumListDao; @@ -111,12 +116,15 @@ void testSuccess_completeWithSquareBracketsAndCanonicalization() throws Exceptio @Test void testSuccess_minimal() throws Exception { + persistResource( + new FeatureFlag() + .asBuilder() + .setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL) + .setStatusMap(ImmutableSortedMap.of(START_OF_TIME, ACTIVE)) + .build()); // Test that each optional field can be omitted. Also tests the auto-gen password. runCommandForced( "--client=NewRegistrar", - "--registrant=crr-admin", - "--admins=crr-admin", - "--techs=crr-tech", "example.tld"); eppVerifier.verifySent("domain_create_minimal.xml"); } @@ -131,7 +139,9 @@ void testSuccess_multipleDomains() throws Exception { "--techs=crr-tech", "example.tld", "example.abc"); - eppVerifier.verifySent("domain_create_minimal.xml").verifySent("domain_create_minimal_abc.xml"); + eppVerifier + .verifySent("domain_create_contacts.xml") + .verifySent("domain_create_contacts_abc.xml"); } @Test @@ -152,8 +162,8 @@ void testSuccess_premiumListNull() throws Exception { "example.tld", "example.abc"); eppVerifier - .verifySent("domain_create_minimal.xml") - .verifySent("domain_create_minimal_abc.xml"); + .verifySent("domain_create_contacts.xml") + .verifySent("domain_create_contacts_abc.xml"); } @Test @@ -192,9 +202,9 @@ void testSuccess_multipleDomainsWithPremium() throws Exception { "palladium.tld", "example.abc"); eppVerifier - .verifySent("domain_create_minimal.xml") + .verifySent("domain_create_contacts.xml") .verifySent("domain_create_palladium.xml") - .verifySent("domain_create_minimal_abc.xml"); + .verifySent("domain_create_contacts_abc.xml"); assertInStdout( "palladium.tld is premium at USD 877.00 per year; " + "sending total cost for 1 year(s) of USD 877.00."); @@ -227,6 +237,19 @@ void testSuccess_allocationToken() throws Exception { eppVerifier.verifySent("domain_create_token.xml"); } + @Test + void testSuccess_contactsStillRequired() throws Exception { + // Verify that if contacts are still required, the minimum+contacts request is sent + createTld("tld"); + runCommandForced( + "--client=NewRegistrar", + "--registrant=crr-admin", + "--admins=crr-admin", + "--techs=crr-tech", + "example.tld"); + eppVerifier.verifySent("domain_create_contacts.xml"); + } + @Test void testFailure_duplicateDomains() { IllegalArgumentException thrown = diff --git a/core/src/test/resources/google/registry/tools/server/domain_create_contacts.xml b/core/src/test/resources/google/registry/tools/server/domain_create_contacts.xml new file mode 100644 index 00000000000..c04c1e1a83a --- /dev/null +++ b/core/src/test/resources/google/registry/tools/server/domain_create_contacts.xml @@ -0,0 +1,19 @@ + + + + + + example.tld + 1 + crr-admin + crr-admin + crr-tech + + abcdefghijklmnop + + + + RegistryTool + + diff --git a/core/src/test/resources/google/registry/tools/server/domain_create_minimal_abc.xml b/core/src/test/resources/google/registry/tools/server/domain_create_contacts_abc.xml similarity index 100% rename from core/src/test/resources/google/registry/tools/server/domain_create_minimal_abc.xml rename to core/src/test/resources/google/registry/tools/server/domain_create_contacts_abc.xml diff --git a/core/src/test/resources/google/registry/tools/server/domain_create_minimal.xml b/core/src/test/resources/google/registry/tools/server/domain_create_minimal.xml index c04c1e1a83a..8f6bedcfb30 100644 --- a/core/src/test/resources/google/registry/tools/server/domain_create_minimal.xml +++ b/core/src/test/resources/google/registry/tools/server/domain_create_minimal.xml @@ -6,9 +6,6 @@ xmlns:domain="urn:ietf:params:xml:ns:domain-1.0"> example.tld 1 - crr-admin - crr-admin - crr-tech abcdefghijklmnop From c2ea4b34a51ed3b4b5af648448ce3f225a970a98 Mon Sep 17 00:00:00 2001 From: Pavlo Tkach <3469726+ptkach@users.noreply.github.com> Date: Mon, 2 Jun 2025 11:43:03 -0400 Subject: [PATCH 37/49] Add RegistrarPoc id column (#2761) --- .../sql/er_diagram/brief_er_diagram.html | 8 +- .../sql/er_diagram/full_er_diagram.html | 4216 +++++++++-------- db/src/main/resources/sql/flyway.txt | 1 + .../sql/flyway/V195__registrar_poc_id.sql | 15 + .../resources/sql/schema/nomulus.golden.sql | 29 +- 5 files changed, 2166 insertions(+), 2103 deletions(-) create mode 100644 db/src/main/resources/sql/flyway/V195__registrar_poc_id.sql diff --git a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html index 9afdc32450f..ff8167034ef 100644 --- a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html @@ -261,11 +261,11 @@

System Information

generated on - 2025-05-15 19:22:21 + 2025-06-02 14:41:34 last flyway file - V194__password_reset_request_registrar.sql + V195__registrar_poc_id.sql @@ -280,7 +280,7 @@

System Information

generated by SchemaCrawler 16.25.2 generated on - 2025-05-15 19:22:21 + 2025-06-02 14:41:34 @@ -2702,7 +2702,7 @@ <h2>Tables</h2> <tr> <td class="spacer"></td> <td class="minwidth"></td> - <td class="minwidth">default '2021-05-31 20:00:00-04'::timestamp with time zone</td> + <td class="minwidth">default '2021-06-01 00:00:00+00'::timestamp with time zone</td> </tr> <tr> <td colspan="3"></td> diff --git a/db/src/main/resources/sql/er_diagram/full_er_diagram.html b/db/src/main/resources/sql/er_diagram/full_er_diagram.html index 0e05a2c0875..d40163df17a 100644 --- a/db/src/main/resources/sql/er_diagram/full_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/full_er_diagram.html @@ -261,3085 +261,3090 @@ <h2>System Information</h2> </tr> <tr> <td class="property_name">generated on</td> - <td class="property_value">2025-05-15 19:22:16</td> + <td class="property_value">2025-06-02 14:41:30</td> </tr> <tr> <td class="property_name">last flyway file</td> - <td id="lastFlywayFile" class="property_value">V194__password_reset_request_registrar.sql</td> + <td id="lastFlywayFile" class="property_value">V195__registrar_poc_id.sql</td> </tr> </tbody> </table> <p> </p> <p> </p> - <svg viewBox="0.00 0.00 5683.00 8146.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px"> - <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 8142)"> + <svg viewBox="0.00 0.00 5683.00 8184.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px"> + <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 8180)"> <title> SchemaCrawler_Diagram - + generated by SchemaCrawler 16.25.2 generated on - 2025-05-15 19:22:16 + 2025-06-02 14:41:30 allocationtoken_a08ccbef - - public."AllocationToken" - - [table] - token + + public."AllocationToken" + + [table] + token + + text not null + update_timestamp + + timestamptz + allowed_registrar_ids - text not null - update_timestamp + _text + allowed_tlds - timestamptz - allowed_registrar_ids + _text + creation_time - _text - allowed_tlds + timestamptz not null + discount_fraction - _text - creation_time + float8(17, 17) not null + discount_premiums - timestamptz not null - discount_fraction + bool not null + discount_years - float8(17, 17) not null - discount_premiums + int4 not null + domain_name - bool not null - discount_years + text + redemption_domain_repo_id - int4 not null - domain_name + text + token_status_transitions - text - redemption_domain_repo_id + hstore + token_type text - token_status_transitions + redemption_domain_history_id - hstore - token_type + int8 + renewal_price_behavior - text - redemption_domain_history_id + text not null + registration_behavior - int8 - renewal_price_behavior + text not null + allowed_epp_actions - text not null - registration_behavior + _text + renewal_price_amount - text not null - allowed_epp_actions + numeric(19, 2) + renewal_price_currency - _text - renewal_price_amount + text + discount_price_amount numeric(19, 2) - renewal_price_currency + discount_price_currency text - discount_price_amount - - numeric(19, 2) - discount_price_currency - - text - + billingevent_a57d1815 - - public."BillingEvent" - - [table] - billing_event_id + + public."BillingEvent" + + [table] + billing_event_id + + int8 not null + registrar_id + + text not null + domain_history_revision_id int8 not null - registrar_id + domain_repo_id text not null - domain_history_revision_id + event_time - int8 not null - domain_repo_id + timestamptz not null + flags - text not null - event_time + _text + reason - timestamptz not null - flags + text not null + domain_name - _text - reason + text not null + allocation_token - text not null - domain_name + text + billing_time - text not null - allocation_token + timestamptz + cancellation_matching_billing_recurrence_id - text - billing_time + int8 + cost_amount - timestamptz - cancellation_matching_billing_recurrence_id + numeric(19, 2) + cost_currency - int8 - cost_amount + text + period_years - numeric(19, 2) - cost_currency + int4 + synthetic_creation_time - text - period_years + timestamptz + recurrence_history_revision_id - int4 - synthetic_creation_time - - timestamptz - recurrence_history_revision_id - - int8 - + int8 + billingevent_a57d1815:w->allocationtoken_a08ccbef:e - - - - - - - - fk_billing_event_allocation_token + + + + + + + + fk_billing_event_allocation_token billingrecurrence_5fa2cb01 - - public."BillingRecurrence" - - [table] - billing_recurrence_id + + public."BillingRecurrence" + + [table] + billing_recurrence_id + + int8 not null + registrar_id + + text not null + domain_history_revision_id int8 not null - registrar_id + domain_repo_id text not null - domain_history_revision_id + event_time - int8 not null - domain_repo_id + timestamptz not null + flags - text not null - event_time + _text + reason - timestamptz not null - flags + text not null + domain_name - _text - reason + text not null + recurrence_end_time - text not null - domain_name + timestamptz + recurrence_time_of_year - text not null - recurrence_end_time + text + renewal_price_behavior - timestamptz - recurrence_time_of_year + text not null + renewal_price_currency text - renewal_price_behavior + renewal_price_amount - text not null - renewal_price_currency + numeric(19, 2) + recurrence_last_expansion - text - renewal_price_amount - - numeric(19, 2) - recurrence_last_expansion - - timestamptz not null - + timestamptz not null + billingevent_a57d1815:w->billingrecurrence_5fa2cb01:e - - - - - - - - fk_billing_event_cancellation_matching_billing_recurrence_id + + + + + + + + fk_billing_event_cancellation_matching_billing_recurrence_id registrar_6e1503e3 - - public."Registrar" - - [table] - registrar_id + + public."Registrar" + + [table] + registrar_id + + text not null + allowed_tlds + + _text + billing_account_map - text not null - allowed_tlds + hstore + block_premium_names - _text - billing_account_map + bool not null + client_certificate - hstore - block_premium_names + text + client_certificate_hash - bool not null - client_certificate + text + contacts_require_syncing - text - client_certificate_hash + bool not null + creation_time - text - contacts_require_syncing + timestamptz not null + drive_folder_id - bool not null - creation_time + text + email_address - timestamptz not null - drive_folder_id + text + failover_client_certificate text - email_address + failover_client_certificate_hash text - failover_client_certificate + fax_number text - failover_client_certificate_hash + iana_identifier - text - fax_number + int8 + icann_referral_email text - iana_identifier + i18n_address_city - int8 - icann_referral_email + text + i18n_address_country_code text - i18n_address_city + i18n_address_state text - i18n_address_country_code + i18n_address_street_line1 text - i18n_address_state + i18n_address_street_line2 text - i18n_address_street_line1 + i18n_address_street_line3 text - i18n_address_street_line2 + i18n_address_zip text - i18n_address_street_line3 + ip_address_allow_list - text - i18n_address_zip + _text + last_certificate_update_time - text - ip_address_allow_list + timestamptz + last_update_time - _text - last_certificate_update_time + timestamptz not null + localized_address_city - timestamptz - last_update_time + text + localized_address_country_code - timestamptz not null - localized_address_city + text + localized_address_state text - localized_address_country_code + localized_address_street_line1 text - localized_address_state + localized_address_street_line2 text - localized_address_street_line1 + localized_address_street_line3 text - localized_address_street_line2 + localized_address_zip text - localized_address_street_line3 + password_hash text - localized_address_zip + phone_number text - password_hash + phone_passcode text - phone_number + po_number text - phone_passcode + rdap_base_urls - text - po_number + _text + registrar_name - text - rdap_base_urls + text not null + registry_lock_allowed - _text - registrar_name + bool not null + password_salt - text not null - registry_lock_allowed + text + state - bool not null - password_salt + text + type - text - state + text not null + url text - type + whois_server - text not null - url + text + last_expiring_cert_notification_sent_date - text - whois_server + timestamptz + last_expiring_failover_cert_notification_sent_date - text - last_expiring_cert_notification_sent_date + timestamptz + last_poc_verification_date timestamptz - last_expiring_failover_cert_notification_sent_date - - timestamptz - last_poc_verification_date - - timestamptz - + billingevent_a57d1815:w->registrar_6e1503e3:e - - - - - - - - fk_billing_event_registrar_id + + + + + + + + fk_billing_event_registrar_id domain_6c51cffa - - public."Domain" - - [table] - repo_id + + public."Domain" + + [table] + repo_id + + text not null + creation_registrar_id + + text not null + creation_time - text not null - creation_registrar_id + timestamptz not null + current_sponsor_registrar_id text not null - creation_time + deletion_time - timestamptz not null - current_sponsor_registrar_id + timestamptz + last_epp_update_registrar_id - text not null - deletion_time + text + last_epp_update_time timestamptz - last_epp_update_registrar_id + statuses - text - last_epp_update_time + _text + auth_info_repo_id - timestamptz - statuses + text + auth_info_value - _text - auth_info_repo_id + text + domain_name text - auth_info_value + idn_table_name text - domain_name + last_transfer_time - text - idn_table_name + timestamptz + launch_notice_accepted_time - text - last_transfer_time + timestamptz + launch_notice_expiration_time timestamptz - launch_notice_accepted_time + launch_notice_tcn_id - timestamptz - launch_notice_expiration_time + text + launch_notice_validator_id - timestamptz - launch_notice_tcn_id + text + registration_expiration_time - text - launch_notice_validator_id + timestamptz + smd_id text - registration_expiration_time + subordinate_hosts - timestamptz - smd_id + _text + tld text - subordinate_hosts + admin_contact - _text - tld + text + billing_contact text - admin_contact + registrant_contact text - billing_contact + tech_contact text - registrant_contact + transfer_poll_message_id_1 - text - tech_contact + int8 + transfer_poll_message_id_2 - text - transfer_poll_message_id_1 + int8 + transfer_billing_cancellation_id int8 - transfer_poll_message_id_2 + transfer_billing_event_id int8 - transfer_billing_cancellation_id + transfer_billing_recurrence_id int8 - transfer_billing_event_id + transfer_autorenew_poll_message_id int8 - transfer_billing_recurrence_id + transfer_renew_period_unit - int8 - transfer_autorenew_poll_message_id + text + transfer_renew_period_value - int8 - transfer_renew_period_unit + int4 + transfer_client_txn_id text - transfer_renew_period_value + transfer_server_txn_id - int4 - transfer_client_txn_id + text + transfer_registration_expiration_time - text - transfer_server_txn_id + timestamptz + transfer_gaining_registrar_id text - transfer_registration_expiration_time + transfer_losing_registrar_id - timestamptz - transfer_gaining_registrar_id + text + transfer_pending_expiration_time - text - transfer_losing_registrar_id + timestamptz + transfer_request_time - text - transfer_pending_expiration_time + timestamptz + transfer_status - timestamptz - transfer_request_time + text + update_timestamp timestamptz - transfer_status + billing_recurrence_id - text - update_timestamp + int8 + autorenew_poll_message_id - timestamptz - billing_recurrence_id + int8 + deletion_poll_message_id int8 - autorenew_poll_message_id + autorenew_end_time - int8 - deletion_poll_message_id + timestamptz + transfer_autorenew_poll_message_history_id int8 - autorenew_end_time + transfer_history_entry_id - timestamptz - transfer_autorenew_poll_message_history_id + int8 + transfer_repo_id - int8 - transfer_history_entry_id + text + transfer_poll_message_id_3 int8 - transfer_repo_id + current_package_token text - transfer_poll_message_id_3 + lordn_phase - int8 - current_package_token + text not null + last_update_time_via_epp - text - lordn_phase - - text not null - last_update_time_via_epp - - timestamptz - + timestamptz + domain_6c51cffa:w->allocationtoken_a08ccbef:e - - - - - - - - fk_domain_current_package_token + + + + + + + + fk_domain_current_package_token domain_6c51cffa:w->billingevent_a57d1815:e - - - - - - - - fk_domain_transfer_billing_event_id + + + + + + + + fk_domain_transfer_billing_event_id billingcancellation_6eedf614 - - public."BillingCancellation" - - [table] - billing_cancellation_id + + public."BillingCancellation" + + [table] + billing_cancellation_id + + int8 not null + registrar_id + + text not null + domain_history_revision_id int8 not null - registrar_id + domain_repo_id text not null - domain_history_revision_id + event_time - int8 not null - domain_repo_id + timestamptz not null + flags - text not null - event_time + _text + reason - timestamptz not null - flags + text not null + domain_name - _text - reason + text not null + billing_time - text not null - domain_name + timestamptz + billing_event_id - text not null - billing_time + int8 + billing_recurrence_id - timestamptz - billing_event_id - - int8 - billing_recurrence_id - - int8 - + int8 + domain_6c51cffa:w->billingcancellation_6eedf614:e - - - - - - - - fk_domain_transfer_billing_cancellation_id + + + + + + + + fk_domain_transfer_billing_cancellation_id domain_6c51cffa:w->billingrecurrence_5fa2cb01:e - - - - - - - - fk_domain_billing_recurrence_id + + + + + + + + fk_domain_billing_recurrence_id domain_6c51cffa:w->billingrecurrence_5fa2cb01:e - - - - - - - - fk_domain_transfer_billing_recurrence_id + + + + + + + + fk_domain_transfer_billing_recurrence_id contact_8de8cb16 - - public."Contact" - - [table] - repo_id + + public."Contact" + + [table] + repo_id + + text not null + creation_registrar_id + + text not null + creation_time - text not null - creation_registrar_id + timestamptz not null + current_sponsor_registrar_id text not null - creation_time + deletion_time - timestamptz not null - current_sponsor_registrar_id + timestamptz + last_epp_update_registrar_id - text not null - deletion_time + text + last_epp_update_time timestamptz - last_epp_update_registrar_id + statuses - text - last_epp_update_time + _text + auth_info_repo_id - timestamptz - statuses + text + auth_info_value - _text - auth_info_repo_id + text + contact_id text - auth_info_value + disclose_types_addr - text - contact_id + _text + disclose_show_email - text - disclose_types_addr + bool + disclose_show_fax - _text - disclose_show_email + bool + disclose_mode_flag bool - disclose_show_fax + disclose_types_name - bool - disclose_mode_flag + _text + disclose_types_org - bool - disclose_types_name + _text + disclose_show_voice - _text - disclose_types_org + bool + email - _text - disclose_show_voice + text + fax_phone_extension - bool - email + text + fax_phone_number text - fax_phone_extension + addr_i18n_city text - fax_phone_number + addr_i18n_country_code text - addr_i18n_city + addr_i18n_state text - addr_i18n_country_code + addr_i18n_street_line1 text - addr_i18n_state + addr_i18n_street_line2 text - addr_i18n_street_line1 + addr_i18n_street_line3 text - addr_i18n_street_line2 + addr_i18n_zip text - addr_i18n_street_line3 + addr_i18n_name text - addr_i18n_zip + addr_i18n_org text - addr_i18n_name + addr_i18n_type text - addr_i18n_org + last_transfer_time - text - addr_i18n_type + timestamptz + addr_local_city text - last_transfer_time + addr_local_country_code - timestamptz - addr_local_city + text + addr_local_state text - addr_local_country_code + addr_local_street_line1 text - addr_local_state + addr_local_street_line2 text - addr_local_street_line1 + addr_local_street_line3 text - addr_local_street_line2 + addr_local_zip text - addr_local_street_line3 + addr_local_name text - addr_local_zip + addr_local_org text - addr_local_name + addr_local_type text - addr_local_org + search_name text - addr_local_type + voice_phone_extension text - search_name + voice_phone_number text - voice_phone_extension + transfer_poll_message_id_1 - text - voice_phone_number + int8 + transfer_poll_message_id_2 - text - transfer_poll_message_id_1 + int8 + transfer_client_txn_id - int8 - transfer_poll_message_id_2 + text + transfer_server_txn_id - int8 - transfer_client_txn_id + text + transfer_gaining_registrar_id text - transfer_server_txn_id + transfer_losing_registrar_id text - transfer_gaining_registrar_id + transfer_pending_expiration_time - text - transfer_losing_registrar_id + timestamptz + transfer_request_time - text - transfer_pending_expiration_time + timestamptz + transfer_status - timestamptz - transfer_request_time + text + update_timestamp timestamptz - transfer_status + transfer_history_entry_id - text - update_timestamp + int8 + transfer_repo_id - timestamptz - transfer_history_entry_id + text + transfer_poll_message_id_3 int8 - transfer_repo_id + last_update_time_via_epp - text - transfer_poll_message_id_3 - - int8 - last_update_time_via_epp - - timestamptz - + timestamptz + domain_6c51cffa:w->contact_8de8cb16:e - - - - - - - - fk_domain_admin_contact + + + + + + + + fk_domain_admin_contact domain_6c51cffa:w->contact_8de8cb16:e - - - - - - - - fk_domain_billing_contact + + + + + + + + fk_domain_billing_contact domain_6c51cffa:w->contact_8de8cb16:e - - - - - - - - fk_domain_registrant_contact + + + + + + + + fk_domain_registrant_contact domain_6c51cffa:w->contact_8de8cb16:e - - - - - - - - fk_domain_tech_contact + + + + + + + + fk_domain_tech_contact domain_6c51cffa:w->registrar_6e1503e3:e - - - - - - - - fk2jc69qyg2tv9hhnmif6oa1cx1 + + + + + + + + fk2jc69qyg2tv9hhnmif6oa1cx1 domain_6c51cffa:w->registrar_6e1503e3:e - - - - - - - - fk2u3srsfbei272093m3b3xwj23 + + + + + + + + fk2u3srsfbei272093m3b3xwj23 domain_6c51cffa:w->registrar_6e1503e3:e - - - - - - - - fkjc0r9r5y1lfbt4gpbqw4wsuvq + + + + + + + + fkjc0r9r5y1lfbt4gpbqw4wsuvq domain_6c51cffa:w->registrar_6e1503e3:e - - - - - - - - fk_domain_transfer_gaining_registrar_id + + + + + + + + fk_domain_transfer_gaining_registrar_id domain_6c51cffa:w->registrar_6e1503e3:e - - - - - - - - fk_domain_transfer_losing_registrar_id + + + + + + + + fk_domain_transfer_losing_registrar_id tld_f1fa57e2 - - public."Tld" - - [table] - tld_name + + public."Tld" + + [table] + tld_name + + text not null + add_grace_period_length + + interval not null + allowed_fully_qualified_host_names - text not null - add_grace_period_length + _text + allowed_registrant_contact_ids - interval not null - allowed_fully_qualified_host_names + _text + anchor_tenant_add_grace_period_length - _text - allowed_registrant_contact_ids + interval not null + auto_renew_grace_period_length - _text - anchor_tenant_add_grace_period_length + interval not null + automatic_transfer_length interval not null - auto_renew_grace_period_length + claims_period_end - interval not null - automatic_transfer_length + timestamptz not null + creation_time - interval not null - claims_period_end + timestamptz not null + currency - timestamptz not null - creation_time + text not null + dns_paused - timestamptz not null - currency + bool not null + dns_writers - text not null - dns_paused + _text not null + drive_folder_id - bool not null - dns_writers + text + eap_fee_schedule - _text not null - drive_folder_id + hstore not null + escrow_enabled - text - eap_fee_schedule + bool not null + invoicing_enabled - hstore not null - escrow_enabled + bool not null + lordn_username - bool not null - invoicing_enabled + text + num_dns_publish_locks - bool not null - lordn_username + int4 not null + pending_delete_length - text - num_dns_publish_locks + interval not null + premium_list_name - int4 not null - pending_delete_length + text + pricing_engine_class_name - interval not null - premium_list_name + text + redemption_grace_period_length - text - pricing_engine_class_name + interval not null + registry_lock_or_unlock_cost_amount - text - redemption_grace_period_length + numeric(19, 2) + registry_lock_or_unlock_cost_currency - interval not null - registry_lock_or_unlock_cost_amount + text + renew_billing_cost_transitions - numeric(19, 2) - registry_lock_or_unlock_cost_currency + hstore not null + renew_grace_period_length - text - renew_billing_cost_transitions + interval not null + reserved_list_names - hstore not null - renew_grace_period_length + _text + restore_billing_cost_amount - interval not null - reserved_list_names + numeric(19, 2) + restore_billing_cost_currency - _text - restore_billing_cost_amount + text + roid_suffix - numeric(19, 2) - restore_billing_cost_currency + text + server_status_change_billing_cost_amount - text - roid_suffix + numeric(19, 2) + server_status_change_billing_cost_currency text - server_status_change_billing_cost_amount + tld_state_transitions - numeric(19, 2) - server_status_change_billing_cost_currency + hstore not null + tld_type - text - tld_state_transitions + text not null + tld_unicode - hstore not null - tld_type + text not null + transfer_grace_period_length - text not null - tld_unicode + interval not null + default_promo_tokens - text not null - transfer_grace_period_length + _text + dns_a_plus_aaaa_ttl - interval not null - default_promo_tokens + interval + dns_ds_ttl - _text - dns_a_plus_aaaa_ttl + interval + dns_ns_ttl interval - dns_ds_ttl + idn_tables - interval - dns_ns_ttl + _text + breakglass_mode - interval - idn_tables + bool not null + bsa_enroll_start_time - _text - breakglass_mode + timestamptz + create_billing_cost_transitions - bool not null - bsa_enroll_start_time - - timestamptz - create_billing_cost_transitions - - hstore not null - + hstore not null + domain_6c51cffa:w->tld_f1fa57e2:e - - - - - - - - fk_domain_tld + + + + + + + + fk_domain_tld domainhistory_a54cc226 - - public."DomainHistory" - - [table] - history_revision_id + + public."DomainHistory" + + [table] + history_revision_id + + int8 not null + history_by_superuser + + bool not null + history_registrar_id - int8 not null - history_by_superuser + text + history_modification_time - bool not null - history_registrar_id + timestamptz not null + history_reason text - history_modification_time + history_requested_by_registrar - timestamptz not null - history_reason + bool + history_client_transaction_id text - history_requested_by_registrar + history_server_transaction_id - bool - history_client_transaction_id + text + history_type - text - history_server_transaction_id + text not null + history_xml_bytes - text - history_type + bytea + admin_contact - text not null - history_xml_bytes + text + auth_info_repo_id - bytea - admin_contact + text + auth_info_value text - auth_info_repo_id + billing_recurrence_id - text - auth_info_value + int8 + autorenew_poll_message_id - text - billing_recurrence_id + int8 + billing_contact - int8 - autorenew_poll_message_id + text + deletion_poll_message_id int8 - billing_contact + domain_name text - deletion_poll_message_id + idn_table_name - int8 - domain_name + text + last_transfer_time - text - idn_table_name + timestamptz + launch_notice_accepted_time - text - last_transfer_time + timestamptz + launch_notice_expiration_time timestamptz - launch_notice_accepted_time + launch_notice_tcn_id - timestamptz - launch_notice_expiration_time + text + launch_notice_validator_id - timestamptz - launch_notice_tcn_id + text + registrant_contact text - launch_notice_validator_id + registration_expiration_time - text - registrant_contact + timestamptz + smd_id text - registration_expiration_time + subordinate_hosts - timestamptz - smd_id + _text + tech_contact text - subordinate_hosts + tld - _text - tech_contact + text + transfer_billing_cancellation_id - text - tld + int8 + transfer_billing_recurrence_id - text - transfer_billing_cancellation_id + int8 + transfer_autorenew_poll_message_id int8 - transfer_billing_recurrence_id + transfer_billing_event_id int8 - transfer_autorenew_poll_message_id + transfer_renew_period_unit - int8 - transfer_billing_event_id + text + transfer_renew_period_value - int8 - transfer_renew_period_unit + int4 + transfer_registration_expiration_time - text - transfer_renew_period_value + timestamptz + transfer_poll_message_id_1 - int4 - transfer_registration_expiration_time + int8 + transfer_poll_message_id_2 - timestamptz - transfer_poll_message_id_1 + int8 + transfer_client_txn_id - int8 - transfer_poll_message_id_2 + text + transfer_server_txn_id - int8 - transfer_client_txn_id + text + transfer_gaining_registrar_id text - transfer_server_txn_id + transfer_losing_registrar_id text - transfer_gaining_registrar_id + transfer_pending_expiration_time - text - transfer_losing_registrar_id + timestamptz + transfer_request_time - text - transfer_pending_expiration_time + timestamptz + transfer_status - timestamptz - transfer_request_time + text + creation_registrar_id - timestamptz - transfer_status + text + creation_time - text - creation_registrar_id + timestamptz + current_sponsor_registrar_id text - creation_time + deletion_time timestamptz - current_sponsor_registrar_id + last_epp_update_registrar_id text - deletion_time + last_epp_update_time timestamptz - last_epp_update_registrar_id + statuses - text - last_epp_update_time + _text + update_timestamp timestamptz - statuses + domain_repo_id - _text - update_timestamp + text not null + autorenew_end_time timestamptz - domain_repo_id + history_other_registrar_id - text not null - autorenew_end_time + text + history_period_unit - timestamptz - history_other_registrar_id + text + history_period_value - text - history_period_unit + int4 + autorenew_poll_message_history_id - text - history_period_value + int8 + transfer_autorenew_poll_message_history_id - int4 - autorenew_poll_message_history_id + int8 + transfer_history_entry_id int8 - transfer_autorenew_poll_message_history_id + transfer_repo_id - int8 - transfer_history_entry_id + text + transfer_poll_message_id_3 int8 - transfer_repo_id + current_package_token text - transfer_poll_message_id_3 + lordn_phase - int8 - current_package_token + text not null + last_update_time_via_epp - text - lordn_phase - - text not null - last_update_time_via_epp - - timestamptz - + timestamptz + domainhistory_a54cc226:w->allocationtoken_a08ccbef:e - - - - - - - - fk_domain_history_current_package_token + + + + + + + + fk_domain_history_current_package_token domainhistory_a54cc226:w->domain_6c51cffa:e - - - - - - - - fk_domain_history_domain_repo_id + + + + + + + + fk_domain_history_domain_repo_id domainhistory_a54cc226:w->registrar_6e1503e3:e - - - - - - - - fk_domain_history_registrar_id + + + + + + + + fk_domain_history_registrar_id billingcancellation_6eedf614:w->billingevent_a57d1815:e - - - - - - - - fk_billing_cancellation_billing_event_id + + + + + + + + fk_billing_cancellation_billing_event_id billingcancellation_6eedf614:w->billingrecurrence_5fa2cb01:e - - - - - - - - fk_billing_cancellation_billing_recurrence_id + + + + + + + + fk_billing_cancellation_billing_recurrence_id billingcancellation_6eedf614:w->registrar_6e1503e3:e - - - - - - - - fk_billing_cancellation_registrar_id + + + + + + + + fk_billing_cancellation_registrar_id graceperiod_cd3b2e8f - - public."GracePeriod" - - [table] - grace_period_id + + public."GracePeriod" + + [table] + grace_period_id + + int8 not null + billing_event_id + + int8 + billing_recurrence_id - int8 not null - billing_event_id + int8 + registrar_id - int8 - billing_recurrence_id + text not null + domain_repo_id - int8 - registrar_id + text not null + expiration_time - text not null - domain_repo_id + timestamptz not null + type text not null - expiration_time - - timestamptz not null - type - - text not null - + graceperiod_cd3b2e8f:w->billingevent_a57d1815:e - - - - - - - - fk_grace_period_billing_event_id + + + + + + + + fk_grace_period_billing_event_id graceperiod_cd3b2e8f:w->domain_6c51cffa:e - - - - - - - - fk_grace_period_domain_repo_id + + + + + + + + fk_grace_period_domain_repo_id graceperiod_cd3b2e8f:w->billingrecurrence_5fa2cb01:e - - - - - - - - fk_grace_period_billing_recurrence_id + + + + + + + + fk_grace_period_billing_recurrence_id graceperiod_cd3b2e8f:w->registrar_6e1503e3:e - - - - - - - - fk_grace_period_registrar_id + + + + + + + + fk_grace_period_registrar_id billingrecurrence_5fa2cb01:w->registrar_6e1503e3:e - - - - - - - - fk_billing_recurrence_registrar_id + + + + + + + + fk_billing_recurrence_registrar_id bsadomainrefresh_c8f4c45d - - public."BsaDomainRefresh" - - [table] - job_id + + public."BsaDomainRefresh" + + [table] + job_id + + bigserial not null + + auto-incremented + creation_time - bigserial not null + timestamptz not null + stage - auto-incremented - creation_time + text not null + update_timestamp - timestamptz not null - stage - - text not null - update_timestamp - - timestamptz - + timestamptz + bsadownload_98d031ce - - public."BsaDownload" - - [table] - job_id + + public."BsaDownload" + + [table] + job_id + + bigserial not null + + auto-incremented + block_list_checksums - bigserial not null + text not null + creation_time - auto-incremented - block_list_checksums + timestamptz not null + stage text not null - creation_time + update_timestamp - timestamptz not null - stage - - text not null - update_timestamp - - timestamptz - + timestamptz + bsalabel_2755e1da - - public."BsaLabel" - - [table] - label - - text not null - creation_time - - timestamptz not null - + + public."BsaLabel" + + [table] + label + + text not null + creation_time + + timestamptz not null + bsaunblockabledomain_b739a38 - - public."BsaUnblockableDomain" - - [table] - label + + public."BsaUnblockableDomain" + + [table] + label + + text not null + tld + + text not null + creation_time - text not null - tld + timestamptz not null + reason text not null - creation_time - - timestamptz not null - reason - - text not null - + bsaunblockabledomain_b739a38:w->bsalabel_2755e1da:e - - - - - - - - fkbsaunblockabledomainlabel + + + + + + + + fkbsaunblockabledomainlabel claimsentry_105da9f1 - - public."ClaimsEntry" - - [table] - revision_id + + public."ClaimsEntry" + + [table] + revision_id + + int8 not null + claim_key + + text not null + domain_label - int8 not null - claim_key - - text not null - domain_label - - text not null - + text not null + claimslist_3d49bc2b - - public."ClaimsList" - - [table] - revision_id + + public."ClaimsList" + + [table] + revision_id + + bigserial not null + + auto-incremented + creation_timestamp - bigserial not null + timestamptz not null + tmdb_generation_time - auto-incremented - creation_timestamp - - timestamptz not null - tmdb_generation_time - - timestamptz not null - + timestamptz not null + claimsentry_105da9f1:w->claimslist_3d49bc2b:e - - - - - - - - fk6sc6at5hedffc0nhdcab6ivuq + + + + + + + + fk6sc6at5hedffc0nhdcab6ivuq consoleeppactionhistory_bcc2a2c6 - - public."ConsoleEppActionHistory" - - [table] - history_revision_id + + public."ConsoleEppActionHistory" + + [table] + history_revision_id + + int8 not null + history_modification_time + + timestamptz not null + history_method - int8 not null - history_modification_time + text not null + history_request_body - timestamptz not null - history_method + text + history_type text not null - history_request_body + history_url - text - history_type + text not null + history_entry_class text not null - history_url + repo_id text not null - history_entry_class + revision_id - text not null - repo_id + int8 not null + history_acting_user text not null - revision_id - - int8 not null - history_acting_user - - text not null - + consoleupdatehistory_5237b2aa - - public."ConsoleUpdateHistory" - - [table] - revision_id + + public."ConsoleUpdateHistory" + + [table] + revision_id + + int8 not null + modification_time + + timestamptz not null + "method" - int8 not null - modification_time + text not null + type - timestamptz not null - "method" + text not null + url text not null - type + description - text not null - url + text + acting_user text not null - description - - text - acting_user - - text not null - + user_f2216f01 - - public."User" - - [table] - email_address + + public."User" + + [table] + email_address + + text not null + registry_lock_password_hash + + text + registry_lock_password_salt - text not null - registry_lock_password_hash + text + global_role - text - registry_lock_password_salt + text not null + is_admin - text - global_role + bool not null + registrar_roles - text not null - is_admin + hstore not null + update_timestamp - bool not null - registrar_roles + timestamptz + registry_lock_email_address - hstore not null - update_timestamp - - timestamptz - registry_lock_email_address - - text - + text + consoleupdatehistory_5237b2aa:w->user_f2216f01:e - - - - - - - - fk_console_update_history_acting_user + + + + + + + + fk_console_update_history_acting_user contact_8de8cb16:w->registrar_6e1503e3:e - - - - - - - - fk1sfyj7o7954prbn1exk7lpnoe + + + + + + + + fk1sfyj7o7954prbn1exk7lpnoe contact_8de8cb16:w->registrar_6e1503e3:e - - - - - - - - fk93c185fx7chn68uv7nl6uv2s0 + + + + + + + + fk93c185fx7chn68uv7nl6uv2s0 contact_8de8cb16:w->registrar_6e1503e3:e - - - - - - - - fkmb7tdiv85863134w1wogtxrb2 + + + + + + + + fkmb7tdiv85863134w1wogtxrb2 contact_8de8cb16:w->registrar_6e1503e3:e - - - - - - - - fk_contact_transfer_gaining_registrar_id + + + + + + + + fk_contact_transfer_gaining_registrar_id contact_8de8cb16:w->registrar_6e1503e3:e - - - - - - - - fk_contact_transfer_losing_registrar_id + + + + + + + + fk_contact_transfer_losing_registrar_id contacthistory_d2964f8a - - public."ContactHistory" - - [table] - history_revision_id + + public."ContactHistory" + + [table] + history_revision_id + + int8 not null + history_by_superuser + + bool not null + history_registrar_id - int8 not null - history_by_superuser + text + history_modification_time - bool not null - history_registrar_id + timestamptz not null + history_reason text - history_modification_time + history_requested_by_registrar - timestamptz not null - history_reason + bool + history_client_transaction_id text - history_requested_by_registrar + history_server_transaction_id - bool - history_client_transaction_id + text + history_type - text - history_server_transaction_id + text not null + history_xml_bytes - text - history_type + bytea + auth_info_repo_id - text not null - history_xml_bytes + text + auth_info_value - bytea - auth_info_repo_id + text + contact_id text - auth_info_value + disclose_types_addr - text - contact_id + _text + disclose_show_email - text - disclose_types_addr + bool + disclose_show_fax - _text - disclose_show_email + bool + disclose_mode_flag bool - disclose_show_fax + disclose_types_name - bool - disclose_mode_flag + _text + disclose_types_org - bool - disclose_types_name + _text + disclose_show_voice - _text - disclose_types_org + bool + email - _text - disclose_show_voice + text + fax_phone_extension - bool - email + text + fax_phone_number text - fax_phone_extension + addr_i18n_city text - fax_phone_number + addr_i18n_country_code text - addr_i18n_city + addr_i18n_state text - addr_i18n_country_code + addr_i18n_street_line1 text - addr_i18n_state + addr_i18n_street_line2 text - addr_i18n_street_line1 + addr_i18n_street_line3 text - addr_i18n_street_line2 + addr_i18n_zip text - addr_i18n_street_line3 + addr_i18n_name text - addr_i18n_zip + addr_i18n_org text - addr_i18n_name + addr_i18n_type text - addr_i18n_org + last_transfer_time - text - addr_i18n_type + timestamptz + addr_local_city text - last_transfer_time + addr_local_country_code - timestamptz - addr_local_city + text + addr_local_state text - addr_local_country_code + addr_local_street_line1 text - addr_local_state + addr_local_street_line2 text - addr_local_street_line1 + addr_local_street_line3 text - addr_local_street_line2 + addr_local_zip text - addr_local_street_line3 + addr_local_name text - addr_local_zip + addr_local_org text - addr_local_name + addr_local_type text - addr_local_org + search_name text - addr_local_type + transfer_poll_message_id_1 - text - search_name + int8 + transfer_poll_message_id_2 - text - transfer_poll_message_id_1 + int8 + transfer_client_txn_id - int8 - transfer_poll_message_id_2 + text + transfer_server_txn_id - int8 - transfer_client_txn_id + text + transfer_gaining_registrar_id text - transfer_server_txn_id + transfer_losing_registrar_id text - transfer_gaining_registrar_id + transfer_pending_expiration_time - text - transfer_losing_registrar_id + timestamptz + transfer_request_time - text - transfer_pending_expiration_time + timestamptz + transfer_status - timestamptz - transfer_request_time + text + voice_phone_extension - timestamptz - transfer_status + text + voice_phone_number text - voice_phone_extension + creation_registrar_id text - voice_phone_number + creation_time - text - creation_registrar_id + timestamptz + current_sponsor_registrar_id text - creation_time + deletion_time timestamptz - current_sponsor_registrar_id + last_epp_update_registrar_id text - deletion_time + last_epp_update_time timestamptz - last_epp_update_registrar_id + statuses - text - last_epp_update_time + _text + contact_repo_id - timestamptz - statuses + text not null + update_timestamp - _text - contact_repo_id + timestamptz + transfer_history_entry_id - text not null - update_timestamp + int8 + transfer_repo_id - timestamptz - transfer_history_entry_id + text + transfer_poll_message_id_3 int8 - transfer_repo_id + last_update_time_via_epp - text - transfer_poll_message_id_3 - - int8 - last_update_time_via_epp - - timestamptz - + timestamptz + contacthistory_d2964f8a:w->contact_8de8cb16:e - - - - - - - - fk_contact_history_contact_repo_id + + + + + + + + fk_contact_history_contact_repo_id contacthistory_d2964f8a:w->registrar_6e1503e3:e - - - - - - - - fk_contact_history_registrar_id + + + + + + + + fk_contact_history_registrar_id pollmessage_614a523e - - public."PollMessage" - - [table] - type + + public."PollMessage" + + [table] + type + + text not null + poll_message_id + + int8 not null + registrar_id text not null - poll_message_id + contact_repo_id - int8 not null - registrar_id + text + contact_history_revision_id - text not null - contact_repo_id + int8 + domain_repo_id text - contact_history_revision_id + domain_history_revision_id int8 - domain_repo_id + event_time - text - domain_history_revision_id + timestamptz not null + host_repo_id - int8 - event_time + text + host_history_revision_id - timestamptz not null - host_repo_id + int8 + message text - host_history_revision_id + transfer_response_contact_id - int8 - message + text + transfer_response_domain_expiration_time - text - transfer_response_contact_id + timestamptz + transfer_response_domain_name text - transfer_response_domain_expiration_time + pending_action_response_action_result - timestamptz - transfer_response_domain_name + bool + pending_action_response_name_or_id text - pending_action_response_action_result + pending_action_response_processed_date - bool - pending_action_response_name_or_id + timestamptz + pending_action_response_client_txn_id text - pending_action_response_processed_date + pending_action_response_server_txn_id - timestamptz - pending_action_response_client_txn_id + text + transfer_response_gaining_registrar_id text - pending_action_response_server_txn_id + transfer_response_losing_registrar_id text - transfer_response_gaining_registrar_id + transfer_response_pending_transfer_expiration_time - text - transfer_response_losing_registrar_id + timestamptz + transfer_response_transfer_request_time - text - transfer_response_pending_transfer_expiration_time + timestamptz + transfer_response_transfer_status - timestamptz - transfer_response_transfer_request_time + text + autorenew_end_time timestamptz - transfer_response_transfer_status + autorenew_domain_name text - autorenew_end_time + transfer_response_host_id - timestamptz - autorenew_domain_name - - text - transfer_response_host_id - - text - + text + pollmessage_614a523e:w->domain_6c51cffa:e - - - - - - - - fk_poll_message_domain_repo_id + + + + + + + + fk_poll_message_domain_repo_id pollmessage_614a523e:w->contact_8de8cb16:e - - - - - - - - fk_poll_message_contact_repo_id + + + + + + + + fk_poll_message_contact_repo_id pollmessage_614a523e:w->contacthistory_d2964f8a:e - - - - - - - - fk_poll_message_contact_history + + + + + + + + fk_poll_message_contact_history pollmessage_614a523e:w->contacthistory_d2964f8a:e - - - - - - - - fk_poll_message_contact_history + + + + + + + + fk_poll_message_contact_history host_f21b78de - - public."Host" - - [table] - repo_id + + public."Host" + + [table] + repo_id + + text not null + creation_registrar_id + + text + creation_time - text not null - creation_registrar_id + timestamptz + current_sponsor_registrar_id text - creation_time + deletion_time timestamptz - current_sponsor_registrar_id + last_epp_update_registrar_id text - deletion_time + last_epp_update_time timestamptz - last_epp_update_registrar_id + statuses - text - last_epp_update_time + _text + host_name - timestamptz - statuses + text + last_superordinate_change - _text - host_name + timestamptz + last_transfer_time - text - last_superordinate_change + timestamptz + superordinate_domain - timestamptz - last_transfer_time + text + inet_addresses - timestamptz - superordinate_domain + _text + update_timestamp - text - inet_addresses + timestamptz + transfer_poll_message_id_3 - _text - update_timestamp + int8 + last_update_time_via_epp timestamptz - transfer_poll_message_id_3 - - int8 - last_update_time_via_epp - - timestamptz - + pollmessage_614a523e:w->host_f21b78de:e - - - - - - - - fk_poll_message_host_repo_id + + + + + + + + fk_poll_message_host_repo_id hosthistory_56210c2 - - public."HostHistory" - - [table] - history_revision_id + + public."HostHistory" + + [table] + history_revision_id + + int8 not null + history_by_superuser + + bool not null + history_registrar_id - int8 not null - history_by_superuser + text not null + history_modification_time - bool not null - history_registrar_id + timestamptz not null + history_reason - text not null - history_modification_time + text + history_requested_by_registrar - timestamptz not null - history_reason + bool + history_client_transaction_id text - history_requested_by_registrar + history_server_transaction_id - bool - history_client_transaction_id + text + history_type - text - history_server_transaction_id + text not null + history_xml_bytes - text - history_type + bytea + host_name - text not null - history_xml_bytes + text + inet_addresses - bytea - host_name + _text + last_superordinate_change - text - inet_addresses + timestamptz + last_transfer_time - _text - last_superordinate_change + timestamptz + superordinate_domain - timestamptz - last_transfer_time + text + creation_registrar_id - timestamptz - superordinate_domain + text + creation_time - text - creation_registrar_id + timestamptz + current_sponsor_registrar_id text - creation_time + deletion_time timestamptz - current_sponsor_registrar_id + last_epp_update_registrar_id text - deletion_time + last_epp_update_time timestamptz - last_epp_update_registrar_id + statuses - text - last_epp_update_time + _text + host_repo_id - timestamptz - statuses + text not null + update_timestamp - _text - host_repo_id + timestamptz + transfer_poll_message_id_3 - text not null - update_timestamp + int8 + last_update_time_via_epp timestamptz - transfer_poll_message_id_3 - - int8 - last_update_time_via_epp - - timestamptz - + pollmessage_614a523e:w->hosthistory_56210c2:e - - - - - - - - fk_poll_message_host_history + + + + + + + + fk_poll_message_host_history pollmessage_614a523e:w->hosthistory_56210c2:e - - - - - - - - fk_poll_message_host_history + + + + + + + + fk_poll_message_host_history pollmessage_614a523e:w->registrar_6e1503e3:e - - - - - - - - fk_poll_message_registrar_id + + + + + + + + fk_poll_message_registrar_id pollmessage_614a523e:w->registrar_6e1503e3:e - - - - - - - - fk_poll_message_transfer_response_gaining_registrar_id + + + + + + + + fk_poll_message_transfer_response_gaining_registrar_id pollmessage_614a523e:w->registrar_6e1503e3:e - - - - - - - - fk_poll_message_transfer_response_losing_registrar_id + + + + + + + + fk_poll_message_transfer_response_losing_registrar_id cursor_6af40e8c - - public."Cursor" - - [table] - "scope" + + public."Cursor" + + [table] + "scope" + + text not null + type + + text not null + cursor_time - text not null - type + timestamptz not null + last_update_time - text not null - cursor_time - - timestamptz not null - last_update_time - - timestamptz not null - + timestamptz not null + delegationsignerdata_e542a872 - - public."DelegationSignerData" - - [table] - domain_repo_id + + public."DelegationSignerData" + + [table] + domain_repo_id + + text not null + key_tag + + int4 not null + algorithm - text not null - key_tag + int4 not null + digest - int4 not null - algorithm + bytea not null + digest_type int4 not null - digest - - bytea not null - digest_type - - int4 not null - + delegationsignerdata_e542a872:w->domain_6c51cffa:e - - - - - - - - fktr24j9v14ph2mfuw2gsmt12kq + + + + + + + + fktr24j9v14ph2mfuw2gsmt12kq dnsrefreshrequest_4e6affb3 - - public."DnsRefreshRequest" - - [table] - id + + public."DnsRefreshRequest" + + [table] + id + + bigserial not null + + auto-incremented + name - bigserial not null + text not null + request_time - auto-incremented - name + timestamptz not null + tld text not null - request_time + type - timestamptz not null - tld + text not null + last_process_time - text not null - type - - text not null - last_process_time - - timestamptz not null - + timestamptz not null + domainhost_1ea127c2 - - public."DomainHost" - - [table] - domain_repo_id - - text not null - host_repo_id - - text - + + public."DomainHost" + + [table] + domain_repo_id + + text not null + host_repo_id + + text + domainhost_1ea127c2:w->domain_6c51cffa:e - - - - - - - - fkfmi7bdink53swivs390m2btxg + + + + + + + + fkfmi7bdink53swivs390m2btxg domainhost_1ea127c2:w->host_f21b78de:e - - - - - - - - fk_domainhost_host_valid + + + + + + + + fk_domainhost_host_valid host_f21b78de:w->domain_6c51cffa:e - - - - - - - - fk_host_superordinate_domain + + + + + + + + fk_host_superordinate_domain host_f21b78de:w->registrar_6e1503e3:e - - - - - - - - fk_host_creation_registrar_id + + + + + + + + fk_host_creation_registrar_id host_f21b78de:w->registrar_6e1503e3:e - - - - - - - - fk_host_current_sponsor_registrar_id + + + + + + + + fk_host_current_sponsor_registrar_id host_f21b78de:w->registrar_6e1503e3:e - - - - - - - - fk_host_last_epp_update_registrar_id + + + + + + + + fk_host_last_epp_update_registrar_id domaindsdatahistory_995b060d - - public."DomainDsDataHistory" - - [table] - ds_data_history_revision_id + + public."DomainDsDataHistory" + + [table] + ds_data_history_revision_id + + int8 not null + algorithm + + int4 not null + digest - int8 not null - algorithm + bytea not null + digest_type int4 not null - digest + domain_history_revision_id - bytea not null - digest_type + int8 not null + key_tag int4 not null - domain_history_revision_id + domain_repo_id - int8 not null - key_tag - - int4 not null - domain_repo_id - - text - + text + domainhistoryhost_9f3f23ee - - public."DomainHistoryHost" - - [table] - domain_history_history_revision_id + + public."DomainHistoryHost" + + [table] + domain_history_history_revision_id + + int8 not null + host_repo_id + + text + domain_history_domain_repo_id - int8 not null - host_repo_id - - text - domain_history_domain_repo_id - - text not null - + text not null + domainhistoryhost_9f3f23ee:w->domainhistory_a54cc226:e - - - - - - - - fka9woh3hu8gx5x0vly6bai327n + + + + + + + + fka9woh3hu8gx5x0vly6bai327n domainhistoryhost_9f3f23ee:w->domainhistory_a54cc226:e - - - - - - - - fka9woh3hu8gx5x0vly6bai327n + + + + + + + + fka9woh3hu8gx5x0vly6bai327n domaintransactionrecord_6e77ff61 - - public."DomainTransactionRecord" - - [table] - id + + public."DomainTransactionRecord" + + [table] + id + + bigserial not null + + auto-incremented + report_amount - bigserial not null + int4 not null + report_field - auto-incremented - report_amount + text not null + reporting_time - int4 not null - report_field + timestamptz not null + tld text not null - reporting_time + domain_repo_id - timestamptz not null - tld + text + history_revision_id - text not null - domain_repo_id - - text - history_revision_id - - int8 - + int8 + domaintransactionrecord_6e77ff61:w->tld_f1fa57e2:e - - - - - - - - fk_domain_transaction_record_tld + + + + + + + + fk_domain_transaction_record_tld featureflag_3ee43a78 - - public."FeatureFlag" - - [table] - feature_name - - text not null - status - - hstore not null - + + public."FeatureFlag" + + [table] + feature_name + + text not null + status + + hstore not null + graceperiodhistory_40ccc1f1 - - public."GracePeriodHistory" - - [table] - grace_period_history_revision_id + + public."GracePeriodHistory" + + [table] + grace_period_history_revision_id + + int8 not null + billing_event_id + + int8 + billing_recurrence_id - int8 not null - billing_event_id + int8 + registrar_id - int8 - billing_recurrence_id + text not null + domain_repo_id - int8 - registrar_id + text not null + expiration_time - text not null - domain_repo_id + timestamptz not null + type text not null - expiration_time + domain_history_revision_id - timestamptz not null - type + int8 + grace_period_id - text not null - domain_history_revision_id - - int8 - grace_period_id - - int8 not null - + int8 not null + hosthistory_56210c2:w->host_f21b78de:e - - - - - - - - fk_hosthistory_host + + + + + + + + fk_hosthistory_host hosthistory_56210c2:w->registrar_6e1503e3:e - - - - - - - - fk_history_registrar_id + + + + + + + + fk_history_registrar_id lock_f21d4861 - - public."Lock" - - [table] - resource_name + + public."Lock" + + [table] + resource_name + + text not null + "scope" + + text not null + acquired_time - text not null - "scope" + timestamptz not null + expiration_time - text not null - acquired_time - - timestamptz not null - expiration_time - - timestamptz not null - + timestamptz not null + packagepromotion_56aa33 - - public."PackagePromotion" - - [table] - package_promotion_id + + public."PackagePromotion" + + [table] + package_promotion_id + + bigserial not null + + auto-incremented + last_notification_sent - bigserial not null + timestamptz + max_creates - auto-incremented - last_notification_sent + int4 not null + max_domains - timestamptz - max_creates + int4 not null + next_billing_date - int4 not null - max_domains + timestamptz not null + package_price_amount - int4 not null - next_billing_date + numeric(19, 2) not null + package_price_currency - timestamptz not null - package_price_amount + text not null + token - numeric(19, 2) not null - package_price_currency - - text not null - token - - text not null - + text not null + passwordresetrequest_8484e7b1 - - public."PasswordResetRequest" - - [table] - type + + public."PasswordResetRequest" + + [table] + type + + text not null + request_time + + timestamptz not null + requester text not null - request_time + fulfillment_time - timestamptz not null - requester + timestamptz + destination_email text not null - fulfillment_time + verification_code - timestamptz - destination_email + text not null + registrar_id text not null - verification_code - - text not null - registrar_id - - text not null - + premiumentry_b0060b91 - - public."PremiumEntry" - - [table] - revision_id + + public."PremiumEntry" + + [table] + revision_id + + int8 not null + price + + numeric(19, 2) not null + domain_label - int8 not null - price - - numeric(19, 2) not null - domain_label - - text not null - + text not null + premiumlist_7c3ea68b - - public."PremiumList" - - [table] - revision_id + + public."PremiumList" + + [table] + revision_id + + bigserial not null + + auto-incremented + creation_timestamp - bigserial not null + timestamptz + name - auto-incremented - creation_timestamp + text not null + bloom_filter - timestamptz - name + bytea not null + currency text not null - bloom_filter - - bytea not null - currency - - text not null - + premiumentry_b0060b91:w->premiumlist_7c3ea68b:e - - - - - - - - fko0gw90lpo1tuee56l0nb6y6g5 + + + + + + + + fko0gw90lpo1tuee56l0nb6y6g5 rderevision_83396864 - - public."RdeRevision" - - [table] - tld + + public."RdeRevision" + + [table] + tld + + text not null + mode + + text not null + "date" - text not null - mode + date not null + update_timestamp - text not null - "date" + timestamptz + revision - date not null - update_timestamp - - timestamptz - revision - - int4 not null - + int4 not null + registrarpoc_ab47054d - - public."RegistrarPoc" - - [table] - email_address - - text not null - allowed_to_set_registry_lock_password - - bool not null - fax_number - - text - name - - text - phone_number - - text - registry_lock_password_hash - - text - registry_lock_password_salt - - text - types - - _text - visible_in_domain_whois_as_abuse - - bool not null - visible_in_whois_as_admin - - bool not null - visible_in_whois_as_tech - - bool not null - registry_lock_email_address - - text - registrar_id - - text not null - + + public."RegistrarPoc" + + [table] + email_address + + text not null + allowed_to_set_registry_lock_password + + bool not null + fax_number + + text + name + + text + phone_number + + text + registry_lock_password_hash + + text + registry_lock_password_salt + + text + types + + _text + visible_in_domain_whois_as_abuse + + bool not null + visible_in_whois_as_admin + + bool not null + visible_in_whois_as_tech + + bool not null + registry_lock_email_address + + text + registrar_id + + text not null + id + + bigserial not null + + auto-incremented + registrarpoc_ab47054d:w->registrar_6e1503e3:e - - - - - - - - fk_registrar_poc_registrar_id + + + + + + + + fk_registrar_poc_registrar_id @@ -3514,412 +3519,412 @@ <h2>System Information</h2> <title> registrarupdatehistory_8a38bed4:w->registrar_6e1503e3:e - + - - - - - - fkregistrarupdatehistoryregistrarid + + + + + + fkregistrarupdatehistoryregistrarid registrarpocupdatehistory_31e5d9aa - - public."RegistrarPocUpdateHistory" - - [table] - history_revision_id + + public."RegistrarPocUpdateHistory" + + [table] + history_revision_id + + int8 not null + history_modification_time + + timestamptz not null + history_method - int8 not null - history_modification_time + text not null + history_request_body - timestamptz not null - history_method + text + history_type text not null - history_request_body + history_url - text - history_type + text not null + email_address text not null - history_url + registrar_id text not null - email_address + allowed_to_set_registry_lock_password - text not null - registrar_id + bool not null + fax_number - text not null - allowed_to_set_registry_lock_password + text + login_email_address - bool not null - fax_number + text + name text - login_email_address + phone_number text - name + registry_lock_email_address text - phone_number + registry_lock_password_hash text - registry_lock_email_address + registry_lock_password_salt text - registry_lock_password_hash + types - text - registry_lock_password_salt + _text + visible_in_domain_whois_as_abuse - text - types + bool not null + visible_in_whois_as_admin - _text - visible_in_domain_whois_as_abuse + bool not null + visible_in_whois_as_tech bool not null - visible_in_whois_as_admin + history_acting_user - bool not null - visible_in_whois_as_tech - - bool not null - history_acting_user - - text not null - + text not null + registrarpocupdatehistory_31e5d9aa:w->registrarpoc_ab47054d:e - - - - - - - - fkregistrarpocupdatehistoryemailaddress + + + + + + + + fkregistrarpocupdatehistoryemailaddress registrarpocupdatehistory_31e5d9aa:w->registrarpoc_ab47054d:e - - - - - - - - fkregistrarpocupdatehistoryemailaddress + + + + + + + + fkregistrarpocupdatehistoryemailaddress registrylock_ac88663e - - public."RegistryLock" - - [table] - revision_id + + public."RegistryLock" + + [table] + revision_id + + bigserial not null + + auto-incremented + lock_completion_time - bigserial not null + timestamptz + lock_request_time - auto-incremented - lock_completion_time + timestamptz not null + domain_name - timestamptz - lock_request_time + text not null + is_superuser - timestamptz not null - domain_name + bool not null + registrar_id text not null - is_superuser + registrar_poc_id - bool not null - registrar_id + text + repo_id text not null - registrar_poc_id + verification_code - text - repo_id + text not null + unlock_request_time - text not null - verification_code + timestamptz + unlock_completion_time - text not null - unlock_request_time + timestamptz + last_update_time - timestamptz - unlock_completion_time + timestamptz not null + relock_revision_id - timestamptz - last_update_time + int8 + relock_duration - timestamptz not null - relock_revision_id - - int8 - relock_duration - - interval - + interval + registrylock_ac88663e:w->registrylock_ac88663e:e - - - - - - - - fk2lhcwpxlnqijr96irylrh1707 + + + + + + + + fk2lhcwpxlnqijr96irylrh1707 reservedentry_1a7b8520 - - public."ReservedEntry" - - [table] - revision_id + + public."ReservedEntry" + + [table] + revision_id + + int8 not null + comment + + text + reservation_type - int8 not null - comment + int4 not null + domain_label - text - reservation_type - - int4 not null - domain_label - - text not null - + text not null + reservedlist_b97c3f1c - - public."ReservedList" - - [table] - revision_id + + public."ReservedList" + + [table] + revision_id + + bigserial not null + + auto-incremented + creation_timestamp - bigserial not null + timestamptz not null + name - auto-incremented - creation_timestamp - - timestamptz not null - name - - text not null - + text not null + reservedentry_1a7b8520:w->reservedlist_b97c3f1c:e - - - - - - - - fkgq03rk0bt1hb915dnyvd3vnfc + + + + + + + + fkgq03rk0bt1hb915dnyvd3vnfc serversecret_6cc90f09 - - public."ServerSecret" - - [table] - secret - - uuid not null - id - - int8 not null - + + public."ServerSecret" + + [table] + secret + + uuid not null + id + + int8 not null + signedmarkrevocationentry_99c39721 - - public."SignedMarkRevocationEntry" - - [table] - revision_id + + public."SignedMarkRevocationEntry" + + [table] + revision_id + + int8 not null + revocation_time + + timestamptz not null + smd_id - int8 not null - revocation_time - - timestamptz not null - smd_id - - text not null - + text not null + signedmarkrevocationlist_c5d968fb - - public."SignedMarkRevocationList" - - [table] - revision_id + + public."SignedMarkRevocationList" + + [table] + revision_id + + bigserial not null + + auto-incremented + creation_time - bigserial not null - - auto-incremented - creation_time - - timestamptz - + timestamptz + signedmarkrevocationentry_99c39721:w->signedmarkrevocationlist_c5d968fb:e - - - - - - - - fk5ivlhvs3121yx2li5tqh54u4 + + + + + + + + fk5ivlhvs3121yx2li5tqh54u4 spec11threatmatch_a61228a6 - - public."Spec11ThreatMatch" - - [table] - id + + public."Spec11ThreatMatch" + + [table] + id + + bigserial not null + + auto-incremented + check_date - bigserial not null + date not null + domain_name - auto-incremented - check_date + text not null + domain_repo_id - date not null - domain_name + text not null + registrar_id text not null - domain_repo_id + threat_types - text not null - registrar_id + _text not null + tld text not null - threat_types - - _text not null - tld - - text not null - + tmchcrl_d282355 - - public."TmchCrl" - - [table] - certificate_revocations + + public."TmchCrl" + + [table] + certificate_revocations + + text not null + update_timestamp + + timestamptz not null + url text not null - update_timestamp + id - timestamptz not null - url - - text not null - id - - int8 not null - + int8 not null + userupdatehistory_24efd476 - - public."UserUpdateHistory" - - [table] - history_revision_id + + public."UserUpdateHistory" + + [table] + history_revision_id + + int8 not null + history_modification_time + + timestamptz not null + history_method - int8 not null - history_modification_time + text not null + history_request_body - timestamptz not null - history_method + text + history_type text not null - history_request_body + history_url - text - history_type + text not null + email_address text not null - history_url + registry_lock_password_hash - text not null - email_address + text + registry_lock_password_salt - text not null - registry_lock_password_hash + text + global_role - text - registry_lock_password_salt + text not null + is_admin - text - global_role + bool not null + registrar_roles - text not null - is_admin + hstore + update_timestamp - bool not null - registrar_roles + timestamptz + history_acting_user - hstore - update_timestamp + text not null + registry_lock_email_address - timestamptz - history_acting_user - - text not null - registry_lock_email_address - - text - + text + @@ -4801,7 +4806,7 @@

Tables

- default '2021-05-31 20:00:00-04'::timestamp with time zone + default '2021-06-01 00:00:00+00'::timestamp with time zone @@ -11288,6 +11293,21 @@

Tables

registrar_id text not null + + + id + bigserial not null + + + + + default nextval('"RegistrarPoc_id_seq"'::regclass) + + + + + auto-incremented + diff --git a/db/src/main/resources/sql/flyway.txt b/db/src/main/resources/sql/flyway.txt index 6904f7abb3d..619ee22c187 100644 --- a/db/src/main/resources/sql/flyway.txt +++ b/db/src/main/resources/sql/flyway.txt @@ -192,3 +192,4 @@ V191__remove_fk_registrarpocupdatehistory.sql V192__add_last_poc_verification_date.sql V193__password_reset_request.sql V194__password_reset_request_registrar.sql +V195__registrar_poc_id.sql diff --git a/db/src/main/resources/sql/flyway/V195__registrar_poc_id.sql b/db/src/main/resources/sql/flyway/V195__registrar_poc_id.sql new file mode 100644 index 00000000000..4b9cbf9e4f2 --- /dev/null +++ b/db/src/main/resources/sql/flyway/V195__registrar_poc_id.sql @@ -0,0 +1,15 @@ +-- 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. + +ALTER TABLE "RegistrarPoc" ADD COLUMN IF NOT EXISTS id BIGSERIAL; diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql index 809413e1519..4209e47dcb2 100644 --- a/db/src/main/resources/sql/schema/nomulus.golden.sql +++ b/db/src/main/resources/sql/schema/nomulus.golden.sql @@ -1020,7 +1020,8 @@ CREATE TABLE public."RegistrarPoc" ( visible_in_whois_as_admin boolean NOT NULL, visible_in_whois_as_tech boolean NOT NULL, registry_lock_email_address text, - registrar_id text NOT NULL + registrar_id text NOT NULL, + id bigint NOT NULL ); @@ -1053,6 +1054,25 @@ CREATE TABLE public."RegistrarPocUpdateHistory" ( ); +-- +-- Name: RegistrarPoc_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public."RegistrarPoc_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: RegistrarPoc_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public."RegistrarPoc_id_seq" OWNED BY public."RegistrarPoc".id; + + -- -- Name: RegistrarUpdateHistory; Type: TABLE; Schema: public; Owner: - -- @@ -1445,6 +1465,13 @@ ALTER TABLE ONLY public."PackagePromotion" ALTER COLUMN package_promotion_id SET ALTER TABLE ONLY public."PremiumList" ALTER COLUMN revision_id SET DEFAULT nextval('public."PremiumList_revision_id_seq"'::regclass); +-- +-- Name: RegistrarPoc id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."RegistrarPoc" ALTER COLUMN id SET DEFAULT nextval('public."RegistrarPoc_id_seq"'::regclass); + + -- -- Name: RegistryLock revision_id; Type: DEFAULT; Schema: public; Owner: - -- From 7156d30b0589f216b843fbd7976dbaa229962c63 Mon Sep 17 00:00:00 2001 From: Weimin Yu Date: Tue, 3 Jun 2025 11:17:43 -0400 Subject: [PATCH 38/49] Fix create_cdns_tld command (#2760) The Cloud DNS rest api is now case-sensitive about enum names (must be lower case, counterintuitively). --- core/src/main/java/google/registry/tools/CreateCdnsTld.java | 6 +++--- .../test/java/google/registry/tools/CreateCdnsTldTest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/google/registry/tools/CreateCdnsTld.java b/core/src/main/java/google/registry/tools/CreateCdnsTld.java index a3aff7db304..cfa8203ff62 100644 --- a/core/src/main/java/google/registry/tools/CreateCdnsTld.java +++ b/core/src/main/java/google/registry/tools/CreateCdnsTld.java @@ -72,11 +72,11 @@ protected void init() { .setDescription(description) .setNameServerSet( RegistryToolEnvironment.get() == RegistryToolEnvironment.PRODUCTION - ? "cloud-dns-registry" - : "cloud-dns-registry-test") + ? "cloud-dns-registry" + : "cloud-dns-registry-test") .setDnsName(dnsName) .setName((name != null) ? name : dnsName) - .setDnssecConfig(new ManagedZoneDnsSecConfig().setNonExistence("NSEC").setState("ON")); + .setDnssecConfig(new ManagedZoneDnsSecConfig().setNonExistence("nsec").setState("on")); } @Override diff --git a/core/src/test/java/google/registry/tools/CreateCdnsTldTest.java b/core/src/test/java/google/registry/tools/CreateCdnsTldTest.java index bb6f0867e46..c4e797dbb90 100644 --- a/core/src/test/java/google/registry/tools/CreateCdnsTldTest.java +++ b/core/src/test/java/google/registry/tools/CreateCdnsTldTest.java @@ -55,7 +55,7 @@ private ManagedZone createZone( .setDnsName(dnsName) .setDescription(description) .setName(name) - .setDnssecConfig(new ManagedZoneDnsSecConfig().setState("ON").setNonExistence("NSEC")); + .setDnssecConfig(new ManagedZoneDnsSecConfig().setState("on").setNonExistence("nsec")); } @Test From 9b129e8b215ee723c165fb6bd9ae1d527660e0ec Mon Sep 17 00:00:00 2001 From: gbrodman Date: Wed, 4 Jun 2025 11:36:22 -0400 Subject: [PATCH 39/49] Add console action test base case (#2762) We can probably improve on this in the future if we want, but there's a lot of boilerplate that we don't need to repeat over and over --- .../console/ConsoleActionBaseTestCase.java | 54 ++++++ .../console/ConsoleDomainGetActionTest.java | 30 +--- .../console/ConsoleDomainListActionTest.java | 92 +++------- .../console/ConsoleDumDownloadActionTest.java | 28 +-- .../console/ConsoleEppPasswordActionTest.java | 51 ++---- .../server/console/ConsoleOteActionTest.java | 76 +++----- .../ConsoleRegistryLockActionTest.java | 81 ++++----- .../ConsoleRegistryLockVerifyActionTest.java | 28 +-- .../ConsoleUpdateRegistrarActionTest.java | 38 ++-- .../console/ConsoleUserDataActionTest.java | 36 +--- .../console/ConsoleUsersActionTest.java | 26 +-- .../server/console/RegistrarsActionTest.java | 51 ++---- .../domains/ConsoleBulkDomainActionTest.java | 78 ++------- .../console/settings/ContactActionTest.java | 162 +++++++----------- .../RdapRegistrarFieldsActionTest.java | 50 ++---- .../console/settings/SecurityActionTest.java | 23 +-- 16 files changed, 298 insertions(+), 606 deletions(-) create mode 100644 core/src/test/java/google/registry/ui/server/console/ConsoleActionBaseTestCase.java diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleActionBaseTestCase.java b/core/src/test/java/google/registry/ui/server/console/ConsoleActionBaseTestCase.java new file mode 100644 index 00000000000..9f95aac98b7 --- /dev/null +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleActionBaseTestCase.java @@ -0,0 +1,54 @@ +// 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.ui.server.console; + +import static google.registry.testing.DatabaseHelper.createAdminUser; +import static google.registry.testing.DatabaseHelper.createTld; + +import com.google.gson.Gson; +import google.registry.model.console.User; +import google.registry.persistence.transaction.JpaTestExtensions; +import google.registry.request.auth.AuthResult; +import google.registry.testing.ConsoleApiParamsUtils; +import google.registry.testing.FakeClock; +import google.registry.testing.FakeResponse; +import google.registry.tools.GsonUtils; +import org.joda.time.DateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class ConsoleActionBaseTestCase { + + protected static final Gson GSON = GsonUtils.provideGson(); + + protected final FakeClock clock = new FakeClock(DateTime.parse("2024-04-15T00:00:00.000Z")); + + @RegisterExtension + final JpaTestExtensions.JpaIntegrationTestExtension jpa = + new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension(); + + protected ConsoleApiParams consoleApiParams; + protected FakeResponse response; + protected User fteUser; + + @BeforeEach + void beforeEachBaseTestCase() { + createTld("tld"); + fteUser = createAdminUser("fte@email.tld"); + AuthResult authResult = AuthResult.createUser(fteUser); + consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); + response = (FakeResponse) consoleApiParams.response(); + } +} diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java index f5142f8e227..2c99450418a 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java @@ -15,7 +15,6 @@ package google.registry.ui.server.console; import static com.google.common.truth.Truth.assertThat; -import static google.registry.testing.DatabaseHelper.createTld; import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static jakarta.servlet.http.HttpServletResponse.SC_OK; import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; @@ -25,7 +24,6 @@ import google.registry.model.console.RegistrarRole; import google.registry.model.console.User; import google.registry.model.console.UserRoles; -import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; import google.registry.request.auth.AuthResult; import google.registry.testing.ConsoleApiParamsUtils; @@ -33,20 +31,12 @@ import google.registry.testing.FakeResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; /** Tests for {@link google.registry.ui.server.console.ConsoleDomainGetAction}. */ -public class ConsoleDomainGetActionTest { - - private ConsoleApiParams consoleApiParams; - - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().buildIntegrationTestExtension(); +public class ConsoleDomainGetActionTest extends ConsoleActionBaseTestCase { @BeforeEach void beforeEach() { - createTld("tld"); DatabaseHelper.persistActiveDomain("exists.tld"); } @@ -62,8 +52,8 @@ void testSuccess_fullJsonRepresentation() { .build())), "exists.tld"); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()) .isEqualTo( "{\"domainName\":\"exists.tld\",\"adminContact\":{\"key\":\"3-ROID\",\"kind\":" + "\"google.registry.model.contact.Contact\"},\"techContact\":{\"key\":\"3-ROID\"," @@ -81,7 +71,7 @@ void testSuccess_fullJsonRepresentation() { void testFailure_emptyAuth() { ConsoleDomainGetAction action = createAction(AuthResult.NOT_AUTHENTICATED, "exists.tld"); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_UNAUTHORIZED); + assertThat(response.getStatus()).isEqualTo(SC_UNAUTHORIZED); } @Test @@ -89,7 +79,7 @@ void testFailure_appAuth() { ConsoleDomainGetAction action = createAction(AuthResult.createApp("service@registry.example"), "exists.tld"); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_UNAUTHORIZED); + assertThat(response.getStatus()).isEqualTo(SC_UNAUTHORIZED); } @Test @@ -98,17 +88,14 @@ void testFailure_noAccessToRegistrar() { createAction( AuthResult.createUser(createUser(new UserRoles.Builder().build())), "exists.tld"); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_NOT_FOUND); + assertThat(response.getStatus()).isEqualTo(SC_NOT_FOUND); } @Test void testFailure_nonexistentDomain() { - ConsoleDomainGetAction action = - createAction( - AuthResult.createUser(createUser(new UserRoles.Builder().setIsAdmin(true).build())), - "nonexistent.tld"); + ConsoleDomainGetAction action = createAction(AuthResult.createUser(fteUser), "nonexistent.tld"); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_NOT_FOUND); + assertThat(response.getStatus()).isEqualTo(SC_NOT_FOUND); } private User createUser(UserRoles userRoles) { @@ -120,6 +107,7 @@ private User createUser(UserRoles userRoles) { private ConsoleDomainGetAction createAction(AuthResult authResult, String domain) { consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); + response = (FakeResponse) consoleApiParams.response(); when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.GET.toString()); return new ConsoleDomainGetAction(consoleApiParams, domain); } diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java index 8ce60433be6..3928ef904e8 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java @@ -16,49 +16,31 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; -import static google.registry.testing.DatabaseHelper.createAdminUser; -import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.persistActiveDomain; import static google.registry.testing.DatabaseHelper.persistDomainAsDeleted; import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static org.mockito.Mockito.when; import com.google.common.collect.Iterables; -import com.google.gson.Gson; import google.registry.model.EppResourceUtils; import google.registry.model.domain.Domain; -import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; import google.registry.request.auth.AuthResult; import google.registry.testing.ConsoleApiParamsUtils; import google.registry.testing.DatabaseHelper; -import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; -import google.registry.tools.GsonUtils; import google.registry.ui.server.console.ConsoleDomainListAction.DomainListResult; import java.util.Optional; import javax.annotation.Nullable; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; /** Tests for {@link ConsoleDomainListAction}. */ -public class ConsoleDomainListActionTest { - - private static final Gson GSON = GsonUtils.provideGson(); - - private final FakeClock clock = new FakeClock(DateTime.parse("2023-10-20T00:00:00.000Z")); - - private ConsoleApiParams consoleApiParams; - - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension(); +public class ConsoleDomainListActionTest extends ConsoleActionBaseTestCase { @BeforeEach void beforeEach() { - createTld("tld"); for (int i = 0; i < 10; i++) { DatabaseHelper.persistActiveDomain(i + "exists.tld", clock.nowUtc()); clock.advanceOneMilli(); @@ -70,9 +52,7 @@ void beforeEach() { void testSuccess_allDomains() { ConsoleDomainListAction action = createAction("TheRegistrar"); action.run(); - DomainListResult result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains).hasSize(10); assertThat(result.totalResults).isEqualTo(10); assertThat(result.checkpointTime).isEqualTo(clock.nowUtc()); @@ -84,9 +64,7 @@ void testSuccess_allDomains() { void testSuccess_noDomains() { ConsoleDomainListAction action = createAction("NewRegistrar"); action.run(); - DomainListResult result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains).hasSize(0); assertThat(result.totalResults).isEqualTo(0); assertThat(result.checkpointTime).isEqualTo(clock.nowUtc()); @@ -97,9 +75,7 @@ void testSuccess_pages() { // Two pages of results should go in reverse chronological order ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, null); action.run(); - DomainListResult result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains.stream().map(Domain::getDomainName).collect(toImmutableList())) .containsExactly("9exists.tld", "8exists.tld", "7exists.tld", "6exists.tld", "5exists.tld"); assertThat(result.totalResults).isEqualTo(10); @@ -107,9 +83,7 @@ void testSuccess_pages() { // Now do the second page action = createAction("TheRegistrar", result.checkpointTime, 1, 5, 10L, null); action.run(); - result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains.stream().map(Domain::getDomainName).collect(toImmutableList())) .containsExactly("4exists.tld", "3exists.tld", "2exists.tld", "1exists.tld", "0exists.tld"); } @@ -118,9 +92,7 @@ void testSuccess_pages() { void testSuccess_partialPage() { ConsoleDomainListAction action = createAction("TheRegistrar", null, 1, 8, null, null); action.run(); - DomainListResult result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains.stream().map(Domain::getDomainName).collect(toImmutableList())) .containsExactly("1exists.tld", "0exists.tld"); } @@ -130,9 +102,7 @@ void testSuccess_checkpointTime_createdBefore() { ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 10, null, null); action.run(); - DomainListResult result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains).hasSize(10); assertThat(result.totalResults).isEqualTo(10); @@ -142,9 +112,7 @@ void testSuccess_checkpointTime_createdBefore() { // Even though we persisted a new domain, the old checkpoint should return no more results action = createAction("TheRegistrar", result.checkpointTime, 1, 10, null, null); action.run(); - result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains).isEmpty(); assertThat(result.totalResults).isEqualTo(10); } @@ -153,9 +121,7 @@ void testSuccess_checkpointTime_createdBefore() { void testSuccess_checkpointTime_deletion() { ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, null); action.run(); - DomainListResult result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); clock.advanceOneMilli(); Domain toDelete = @@ -165,9 +131,7 @@ void testSuccess_checkpointTime_deletion() { // Second page should include the domain that is now deleted due to the checkpoint time action = createAction("TheRegistrar", result.checkpointTime, 1, 5, null, null); action.run(); - result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains.stream().map(Domain::getDomainName).collect(toImmutableList())) .containsExactly("4exists.tld", "3exists.tld", "2exists.tld", "1exists.tld", "0exists.tld"); } @@ -176,9 +140,7 @@ void testSuccess_checkpointTime_deletion() { void testSuccess_searchTerm_oneMatch() { ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, "0"); action.run(); - DomainListResult result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(Iterables.getOnlyElement(result.domains).getDomainName()).isEqualTo("0exists.tld"); } @@ -186,9 +148,7 @@ void testSuccess_searchTerm_oneMatch() { void testSuccess_searchTerm_returnsNone() { ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, "deleted"); action.run(); - DomainListResult result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains).isEmpty(); } @@ -196,9 +156,7 @@ void testSuccess_searchTerm_returnsNone() { void testSuccess_searchTerm_caseInsensitive() { ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, "eXiStS"); action.run(); - DomainListResult result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains).hasSize(5); assertThat(result.totalResults).isEqualTo(10); } @@ -207,9 +165,7 @@ void testSuccess_searchTerm_caseInsensitive() { void testSuccess_searchTerm_tld() { ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, "tld"); action.run(); - DomainListResult result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains).hasSize(5); assertThat(result.totalResults).isEqualTo(10); } @@ -218,9 +174,7 @@ void testSuccess_searchTerm_tld() { void testPartialSuccess_pastEnd() { ConsoleDomainListAction action = createAction("TheRegistrar", null, 5, 5, null, null); action.run(); - DomainListResult result = - GSON.fromJson( - ((FakeResponse) consoleApiParams.response()).getPayload(), DomainListResult.class); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains).isEmpty(); } @@ -228,14 +182,14 @@ void testPartialSuccess_pastEnd() { void testFailure_invalidResultsPerPage() { ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 0, null, null); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo("Results per page must be between 1 and 500 inclusive"); action = createAction("TheRegistrar", null, 0, 501, null, null); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo("Results per page must be between 1 and 500 inclusive"); } @@ -243,9 +197,8 @@ void testFailure_invalidResultsPerPage() { void testFailure_invalidPageNumber() { ConsoleDomainListAction action = createAction("TheRegistrar", null, -1, 10, null, null); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) - .isEqualTo("Page number must be non-negative"); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()).isEqualTo("Page number must be non-negative"); } private ConsoleDomainListAction createAction(String registrarId) { @@ -259,9 +212,10 @@ private ConsoleDomainListAction createAction( @Nullable Integer resultsPerPage, @Nullable Long totalResults, @Nullable String searchTerm) { - AuthResult authResult = AuthResult.createUser(createAdminUser("email@email.example")); + AuthResult authResult = AuthResult.createUser(fteUser); consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.GET.toString()); + response = (FakeResponse) consoleApiParams.response(); return new ConsoleDomainListAction( consoleApiParams, registrarId, diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleDumDownloadActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleDumDownloadActionTest.java index 102eddeffd5..53fc78b5f77 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleDumDownloadActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleDumDownloadActionTest.java @@ -15,7 +15,6 @@ package google.registry.ui.server.console; import static com.google.common.truth.Truth.assertThat; -import static google.registry.testing.DatabaseHelper.createTld; import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static jakarta.servlet.http.HttpServletResponse.SC_OK; import static org.mockito.Mockito.when; @@ -24,32 +23,19 @@ import google.registry.model.console.GlobalRole; import google.registry.model.console.User; import google.registry.model.console.UserRoles; -import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; import google.registry.request.auth.AuthResult; import google.registry.testing.ConsoleApiParamsUtils; import google.registry.testing.DatabaseHelper; -import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import java.io.IOException; -import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -class ConsoleDumDownloadActionTest { - - private final FakeClock clock = new FakeClock(DateTime.parse("2024-04-15T00:00:00.000Z")); - - private ConsoleApiParams consoleApiParams; - - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension(); +class ConsoleDumDownloadActionTest extends ConsoleActionBaseTestCase { @BeforeEach void beforeEach() { - createTld("tld"); for (int i = 0; i < 3; i++) { DatabaseHelper.persistActiveDomain( i + "exists.tld", clock.nowUtc(), clock.nowUtc().plusDays(300)); @@ -60,13 +46,7 @@ void beforeEach() { @Test void testSuccess_returnsCorrectDomains() throws IOException { - User user = - new User.Builder() - .setEmailAddress("email@email.com") - .setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()) - .build(); - - AuthResult authResult = AuthResult.createUser(user); + AuthResult authResult = AuthResult.createUser(fteUser); ConsoleDumDownloadAction action = createAction(authResult); action.run(); ImmutableList expected = @@ -75,7 +55,6 @@ void testSuccess_returnsCorrectDomains() throws IOException { "2exists.tld,2024-04-15 00:00:00.002+00,2025-02-09 00:00:00.002+00,{INACTIVE}", "1exists.tld,2024-04-15 00:00:00.001+00,2025-02-09 00:00:00.001+00,{INACTIVE}", "0exists.tld,2024-04-15 00:00:00+00,2025-02-09 00:00:00+00,{INACTIVE}"); - FakeResponse response = (FakeResponse) consoleApiParams.response(); assertThat(response.getStatus()).isEqualTo(SC_OK); ImmutableList actual = ImmutableList.copyOf(response.getStringWriter().toString().split("\r\n")); @@ -93,11 +72,12 @@ void testFailure_forbidden() { AuthResult authResult = AuthResult.createUser(user); ConsoleDumDownloadAction action = createAction(authResult); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_FORBIDDEN); + assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); } private ConsoleDumDownloadAction createAction(AuthResult authResult) { consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); + response = (FakeResponse) consoleApiParams.response(); when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.GET.toString()); return new ConsoleDumDownloadAction(clock, consoleApiParams, "TheRegistrar", "test_name"); } diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleEppPasswordActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleEppPasswordActionTest.java index bf41066b807..73ef97016d0 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleEppPasswordActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleEppPasswordActionTest.java @@ -30,22 +30,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSetMultimap; -import com.google.gson.Gson; import google.registry.flows.PasswordOnlyTransportCredentials; import google.registry.model.console.ConsoleUpdateHistory; -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.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; import google.registry.request.RequestModule; -import google.registry.request.auth.AuthResult; import google.registry.request.auth.AuthenticatedRegistrarAccessor; -import google.registry.testing.ConsoleApiParamsUtils; -import google.registry.testing.DatabaseHelper; -import google.registry.testing.FakeResponse; -import google.registry.tools.GsonUtils; import google.registry.ui.server.console.ConsoleEppPasswordAction.EppPasswordData; import google.registry.util.EmailMessage; import jakarta.mail.internet.AddressException; @@ -56,20 +46,12 @@ import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -class ConsoleEppPasswordActionTest { - private static final Gson GSON = GsonUtils.provideGson(); +class ConsoleEppPasswordActionTest extends ConsoleActionBaseTestCase { private static String eppPostData = "{\"registrarId\":\"%s\",\"oldPassword\":\"%s\",\"newPassword\":\"%s\",\"newPasswordRepeat\":\"%s\"}"; - private ConsoleApiParams consoleApiParams; protected PasswordOnlyTransportCredentials credentials = new PasswordOnlyTransportCredentials(); - private FakeResponse response; - - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().buildIntegrationTestExtension(); @BeforeEach void beforeEach() { @@ -86,9 +68,8 @@ void beforeEach() { void testFailure_emptyParams() throws IOException { ConsoleEppPasswordAction action = createAction("", "", "", ""); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) - .isEqualTo("Missing param(s): registrarId"); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()).isEqualTo("Missing param(s): registrarId"); } @Test @@ -96,9 +77,8 @@ void testFailure_passwordsDontMatch() throws IOException { ConsoleEppPasswordAction action = createAction("TheRegistrar", "oldPassword", "newPassword", "newPasswordRepeat"); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) - .contains("New password fields don't match"); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()).contains("New password fields don't match"); } @Test @@ -106,9 +86,8 @@ void testFailure_existingPasswordIncorrect() throws IOException { ConsoleEppPasswordAction action = createAction("TheRegistrar", "oldPassword", "randomPasword", "randomPasword"); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_FORBIDDEN); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) - .contains("Registrar password is incorrect"); + assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); + assertThat(response.getPayload()).contains("Registrar password is incorrect"); } @Test @@ -124,12 +103,12 @@ void testSuccess_sendsConfirmationEmail() throws IOException, AddressException { + " environment") .setBody( "The following changes were made in registry unittest environment to the" - + " registrar TheRegistrar by user email@email.com:\n" + + " registrar TheRegistrar by admin fte@email.tld:\n" + "\n" + "password: ******** -> ••••••••\n") .setRecipients(ImmutableList.of(new InternetAddress("notification@test.example"))) .build()); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); } @Test @@ -137,7 +116,7 @@ void testSuccess_passwordUpdated() throws IOException { ConsoleEppPasswordAction action = createAction("TheRegistrar", "foobar", "randomPassword", "randomPassword"); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); assertDoesNotThrow(() -> credentials.validate(loadRegistrar("TheRegistrar"), "randomPassword")); ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get(); assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.EPP_PASSWORD_UPDATE); @@ -147,16 +126,6 @@ void testSuccess_passwordUpdated() throws IOException { private ConsoleEppPasswordAction createAction( String registrarId, String oldPassword, String newPassword, String newPasswordRepeat) throws IOException { - response = new FakeResponse(); - User user = - new User.Builder() - .setEmailAddress("email@email.com") - .setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()) - .build(); - DatabaseHelper.putInDb(user); - - AuthResult authResult = AuthResult.createUser(user); - consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); AuthenticatedRegistrarAccessor authenticatedRegistrarAccessor = AuthenticatedRegistrarAccessor.createForTesting( ImmutableSetMultimap.of("TheRegistrar", OWNER)); diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleOteActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleOteActionTest.java index f32b8ede04e..21bb589e8b0 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleOteActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleOteActionTest.java @@ -27,19 +27,16 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.gson.Gson; import google.registry.model.OteStatsTestHelper; import google.registry.model.console.GlobalRole; import google.registry.model.console.User; import google.registry.model.console.UserRoles; -import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; import google.registry.request.auth.AuthResult; import google.registry.testing.CloudTasksHelper; import google.registry.testing.ConsoleApiParamsUtils; import google.registry.testing.DeterministicStringGenerator; import google.registry.testing.FakeResponse; -import google.registry.tools.GsonUtils; import google.registry.tools.IamClient; import google.registry.ui.server.console.ConsoleOteAction.OteCreateData; import google.registry.util.StringGenerator; @@ -50,19 +47,11 @@ import org.json.simple.JSONArray; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -class ConsoleOteActionTest { +class ConsoleOteActionTest extends ConsoleActionBaseTestCase { - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().buildIntegrationTestExtension(); - - private static final Gson GSON = GsonUtils.provideGson(); private final IamClient iamClient = mock(IamClient.class); private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper(); - private FakeResponse response; - private ConsoleApiParams consoleApiParams; private StringGenerator passwordGenerator = new DeterministicStringGenerator("abcdefghijklmnopqrstuvwxyz"); @@ -90,16 +79,12 @@ void testFailure_missingGlobalPermission() { Optional.of("someRandomString"), Optional.of(new OteCreateData("testRegistrarId", "tescontact@registry.example"))); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_FORBIDDEN); + assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); } @Test void testFailure_invalidParamsNoRegistrarId() { - user = - user.asBuilder() - .setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()) - .build(); - AuthResult authResult = AuthResult.createUser(user); + AuthResult authResult = AuthResult.createUser(fteUser); consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); ConsoleOteAction action = createAction( @@ -109,9 +94,8 @@ void testFailure_invalidParamsNoRegistrarId() { Optional.of("someRandomString"), Optional.of(new OteCreateData("", "test@email.com"))); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) - .isEqualTo("OT&E create body is invalid"); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()).isEqualTo("OT&E create body is invalid"); } @Test @@ -130,18 +114,13 @@ void testFailure_invalidParamsNoEmail() { Optional.of("someRandomString"), Optional.of(new OteCreateData("testRegistrarId", ""))); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) - .isEqualTo("OT&E create body is invalid"); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()).isEqualTo("OT&E create body is invalid"); } @Test void testSuccess_oteCreated() { - user = - user.asBuilder() - .setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()) - .build(); - AuthResult authResult = AuthResult.createUser(user); + AuthResult authResult = AuthResult.createUser(fteUser); consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); ConsoleOteAction action = createAction( @@ -152,8 +131,7 @@ void testSuccess_oteCreated() { Optional.of(new OteCreateData("theregistrar", "contact@registry.example"))); action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); action.run(); - String response = ((FakeResponse) consoleApiParams.response()).getPayload(); - var obsResponse = GSON.fromJson(response, Map.class); + var obsResponse = GSON.fromJson(response.getPayload(), Map.class); assertThat( ImmutableMap.of( "theregistrar-1", "theregistrar-sunrise", @@ -162,7 +140,7 @@ void testSuccess_oteCreated() { "theregistrar-5", "theregistrar-eap", "password", "abcdefghijklmnop")) .containsExactlyEntriesIn(obsResponse); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); verifyIapPermission( "contact@registry.example", Optional.of("someRandomString@email.test"), @@ -183,50 +161,40 @@ void testFail_statusMissingParam() { Optional.of(new OteCreateData("theregistrar", "contact@registry.example"))); action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) - .isEqualTo("Missing registrarId parameter"); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()).isEqualTo("Missing registrarId parameter"); } @Test void testSuccess_finishedOte() throws Exception { OteStatsTestHelper.setupCompleteOte("theregistrar"); - user = - user.asBuilder() - .setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()) - .build(); - AuthResult authResult = AuthResult.createUser(user); + AuthResult authResult = AuthResult.createUser(fteUser); consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); ConsoleOteAction action = createAction( Action.Method.GET, authResult, "theregistrar-1", Optional.empty(), Optional.empty()); action.run(); - List> response = - GSON.fromJson(((FakeResponse) consoleApiParams.response()).getPayload(), JSONArray.class); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); - assertTrue(response.stream().allMatch(status -> Boolean.TRUE.equals(status.get("completed")))); + List> responseMaps = GSON.fromJson(response.getPayload(), JSONArray.class); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertTrue( + responseMaps.stream().allMatch(status -> Boolean.TRUE.equals(status.get("completed")))); } @Test void testSuccess_unfinishedOte() throws Exception { OteStatsTestHelper.setupIncompleteOte("theregistrar"); - user = - user.asBuilder() - .setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()) - .build(); - AuthResult authResult = AuthResult.createUser(user); + AuthResult authResult = AuthResult.createUser(fteUser); consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); ConsoleOteAction action = createAction( Action.Method.GET, authResult, "theregistrar-1", Optional.empty(), Optional.empty()); action.run(); - List> response = - GSON.fromJson(((FakeResponse) consoleApiParams.response()).getPayload(), JSONArray.class); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + List> responseMaps = GSON.fromJson(response.getPayload(), JSONArray.class); + assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat( - response.stream() + responseMaps.stream() .filter(status -> Boolean.FALSE.equals(status.get("completed"))) .map(status -> status.get("description")) .collect(Collectors.toList())) @@ -240,9 +208,9 @@ private ConsoleOteAction createAction( String registrarId, Optional maybeGroupEmailAddress, Optional oteCreateData) { - response = new FakeResponse(); consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); when(consoleApiParams.request().getMethod()).thenReturn(method.toString()); + response = (FakeResponse) consoleApiParams.response(); return new ConsoleOteAction( consoleApiParams, iamClient, diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockActionTest.java index fccf4d5f952..4ff52cc2899 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockActionTest.java @@ -42,12 +42,10 @@ import google.registry.model.domain.Domain; import google.registry.model.domain.RegistryLock; import google.registry.model.eppcommon.StatusValue; -import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.auth.AuthResult; import google.registry.testing.CloudTasksHelper; import google.registry.testing.ConsoleApiParamsUtils; import google.registry.testing.DeterministicStringGenerator; -import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.tools.DomainLockUtils; import google.registry.util.EmailMessage; @@ -55,13 +53,11 @@ import jakarta.mail.internet.InternetAddress; import java.io.IOException; import java.util.Optional; -import org.joda.time.DateTime; import org.joda.time.Duration; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -71,7 +67,7 @@ /** Tests for {@link ConsoleRegistryLockAction}. */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class ConsoleRegistryLockActionTest { +public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase { private static final String EXPECTED_EMAIL_MESSAGE = """ @@ -81,16 +77,9 @@ public class ConsoleRegistryLockActionTest { https://registrarconsole.tld/console/#/registry-lock-verify?lockVerificationCode=\ 123456789ABCDEFGHJKLMNPQRSTUVWXY"""; - private final FakeClock fakeClock = new FakeClock(DateTime.parse("2024-04-18T12:00:00.000Z")); - - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationTestExtension(); - @Mock GmailClient gmailClient; private ConsoleRegistryLockAction action; private Domain defaultDomain; - private FakeResponse response; private User user; @BeforeEach @@ -118,15 +107,15 @@ void afterEach() { @Test void testGet_simpleLock() { - saveRegistryLock(createDefaultLockBuilder().setLockCompletionTime(fakeClock.nowUtc()).build()); + saveRegistryLock(createDefaultLockBuilder().setLockCompletionTime(clock.nowUtc()).build()); action.run(); assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat(response.getPayload()) .isEqualTo( """ [{"domainName":"example.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\ -{"creationTime":"2024-04-18T12:00:00.000Z"},"unlockRequestTime":"null","lockCompletionTime":\ -"2024-04-18T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false}]\ +{"creationTime":"2024-04-15T00:00:00.000Z"},"unlockRequestTime":"null","lockCompletionTime":\ +"2024-04-15T00:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false}]\ """); } @@ -148,11 +137,11 @@ void testGet_allCurrentlyValidLocks() { .setRegistrarId("TheRegistrar") .setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY") .setRegistrarPocId("johndoe@theregistrar.com") - .setLockCompletionTime(fakeClock.nowUtc()) - .setUnlockRequestTime(fakeClock.nowUtc()) + .setLockCompletionTime(clock.nowUtc()) + .setUnlockRequestTime(clock.nowUtc()) .build(); saveRegistryLock(expiredUnlock); - fakeClock.advanceBy(Duration.standardDays(1)); + clock.advanceBy(Duration.standardDays(1)); RegistryLock regularLock = new RegistryLock.Builder() @@ -161,9 +150,9 @@ void testGet_allCurrentlyValidLocks() { .setRegistrarId("TheRegistrar") .setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY") .setRegistrarPocId("johndoe@theregistrar.com") - .setLockCompletionTime(fakeClock.nowUtc()) + .setLockCompletionTime(clock.nowUtc()) .build(); - fakeClock.advanceOneMilli(); + clock.advanceOneMilli(); RegistryLock adminLock = new RegistryLock.Builder() .setRepoId("repoId") @@ -171,7 +160,7 @@ void testGet_allCurrentlyValidLocks() { .setRegistrarId("TheRegistrar") .setVerificationCode("122222222ABCDEFGHJKLMNPQRSTUVWXY") .isSuperuser(true) - .setLockCompletionTime(fakeClock.nowUtc()) + .setLockCompletionTime(clock.nowUtc()) .build(); RegistryLock incompleteLock = new RegistryLock.Builder() @@ -189,8 +178,8 @@ void testGet_allCurrentlyValidLocks() { .setRegistrarId("TheRegistrar") .setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY") .setRegistrarPocId("johndoe@theregistrar.com") - .setLockCompletionTime(fakeClock.nowUtc()) - .setUnlockRequestTime(fakeClock.nowUtc()) + .setLockCompletionTime(clock.nowUtc()) + .setUnlockRequestTime(clock.nowUtc()) .build(); RegistryLock unlockedLock = @@ -200,9 +189,9 @@ void testGet_allCurrentlyValidLocks() { .setRegistrarId("TheRegistrar") .setRegistrarPocId("johndoe@theregistrar.com") .setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUUUUU") - .setLockCompletionTime(fakeClock.nowUtc()) - .setUnlockRequestTime(fakeClock.nowUtc()) - .setUnlockCompletionTime(fakeClock.nowUtc()) + .setLockCompletionTime(clock.nowUtc()) + .setUnlockRequestTime(clock.nowUtc()) + .setUnlockCompletionTime(clock.nowUtc()) .build(); saveRegistryLock(regularLock); @@ -218,24 +207,24 @@ void testGet_allCurrentlyValidLocks() { assertThat(response.getPayload()) .isEqualTo( """ -[{"domainName":"adminexample.test","lockRequestTime":{"creationTime":"2024-04-19T12:00:00.001Z"},\ -"unlockRequestTime":"null","lockCompletionTime":"2024-04-19T12:00:00.001Z","unlockCompletionTime":\ +[{"domainName":"adminexample.test","lockRequestTime":{"creationTime":"2024-04-16T00:00:00.001Z"},\ +"unlockRequestTime":"null","lockCompletionTime":"2024-04-16T00:00:00.001Z","unlockCompletionTime":\ "null","isSuperuser":true},\ \ {"domainName":"example.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\ -{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":\ -"2024-04-19T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\ +{"creationTime":"2024-04-16T00:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":\ +"2024-04-16T00:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\ \ {"domainName":"expiredunlock.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\ -{"creationTime":"2024-04-18T12:00:00.000Z"},"unlockRequestTime":"2024-04-18T12:00:00.000Z",\ -"lockCompletionTime":"2024-04-18T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\ +{"creationTime":"2024-04-15T00:00:00.000Z"},"unlockRequestTime":"2024-04-15T00:00:00.000Z",\ +"lockCompletionTime":"2024-04-15T00:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\ \ {"domainName":"incompleteunlock.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\ -{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"2024-04-19T12:00:00.001Z",\ -"lockCompletionTime":"2024-04-19T12:00:00.001Z","unlockCompletionTime":"null","isSuperuser":false},\ +{"creationTime":"2024-04-16T00:00:00.001Z"},"unlockRequestTime":"2024-04-16T00:00:00.001Z",\ +"lockCompletionTime":"2024-04-16T00:00:00.001Z","unlockCompletionTime":"null","isSuperuser":false},\ \ {"domainName":"pending.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\ -{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":"null",\ +{"creationTime":"2024-04-16T00:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":"null",\ "unlockCompletionTime":"null","isSuperuser":false}]"""); } @@ -288,7 +277,7 @@ void testPost_lock() throws Exception { @Test void testPost_unlock() throws Exception { - saveRegistryLock(createDefaultLockBuilder().setLockCompletionTime(fakeClock.nowUtc()).build()); + saveRegistryLock(createDefaultLockBuilder().setLockCompletionTime(clock.nowUtc()).build()); persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); action = createDefaultPostAction(false); action.run(); @@ -301,7 +290,7 @@ void testPost_unlock() throws Exception { @Test void testPost_unlock_relockDuration() throws Exception { - saveRegistryLock(createDefaultLockBuilder().setLockCompletionTime(fakeClock.nowUtc()).build()); + saveRegistryLock(createDefaultLockBuilder().setLockCompletionTime(clock.nowUtc()).build()); persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); action = createPostAction( @@ -318,10 +307,7 @@ void testPost_unlock_relockDuration() throws Exception { @Test void testPost_adminUnlockingAdmin() throws Exception { saveRegistryLock( - createDefaultLockBuilder() - .setLockCompletionTime(fakeClock.nowUtc()) - .isSuperuser(true) - .build()); + createDefaultLockBuilder().setLockCompletionTime(clock.nowUtc()).isSuperuser(true).build()); persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); user = user.asBuilder() @@ -387,10 +373,7 @@ void testPost_failure_unlock_noLock() throws Exception { @Test void testPost_failure_nonAdminUnlockingAdmin() throws Exception { saveRegistryLock( - createDefaultLockBuilder() - .setLockCompletionTime(fakeClock.nowUtc()) - .isSuperuser(true) - .build()); + createDefaultLockBuilder().setLockCompletionTime(clock.nowUtc()).isSuperuser(true).build()); persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); action = createDefaultPostAction(false); action.run(); @@ -464,9 +447,9 @@ void testPost_failure_alreadyLocked() throws Exception { void testPost_failure_alreadyUnlocked() throws Exception { saveRegistryLock( createDefaultLockBuilder() - .setLockCompletionTime(fakeClock.nowUtc()) - .setUnlockRequestTime(fakeClock.nowUtc()) - .setUnlockCompletionTime(fakeClock.nowUtc()) + .setLockCompletionTime(clock.nowUtc()) + .setUnlockRequestTime(clock.nowUtc()) + .setUnlockCompletionTime(clock.nowUtc()) .build()); action = createDefaultPostAction(false); action.run(); @@ -501,7 +484,7 @@ private ConsoleRegistryLockAction createGenericAction( new DomainLockUtils( new DeterministicStringGenerator(StringGenerator.Alphabets.BASE_58), "adminreg", - new CloudTasksHelper(fakeClock).getTestCloudTasksUtils()); + new CloudTasksHelper(clock).getTestCloudTasksUtils()); response = (FakeResponse) params.response(); return new ConsoleRegistryLockAction( params, domainLockUtils, gmailClient, optionalPostInput, "TheRegistrar"); diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyActionTest.java index edf0511327c..5618a87f3f6 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyActionTest.java @@ -30,12 +30,10 @@ import google.registry.model.domain.Domain; import google.registry.model.domain.RegistryLock; import google.registry.model.eppcommon.StatusValue; -import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.auth.AuthResult; import google.registry.testing.CloudTasksHelper; import google.registry.testing.ConsoleApiParamsUtils; import google.registry.testing.DeterministicStringGenerator; -import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.tools.DomainLockUtils; import google.registry.util.StringGenerator; @@ -43,19 +41,11 @@ import org.joda.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; /** Tests for {@link ConsoleRegistryLockVerifyAction}. */ -public class ConsoleRegistryLockVerifyActionTest { +public class ConsoleRegistryLockVerifyActionTest extends ConsoleActionBaseTestCase { private static final String DEFAULT_CODE = "123456789ABCDEFGHJKLMNPQRSTUUUUU"; - private final FakeClock fakeClock = new FakeClock(); - - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationTestExtension(); - - private FakeResponse response; private Domain defaultDomain; private User user; @@ -96,8 +86,8 @@ void testSuccess_unlock() { persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); saveRegistryLock( createDefaultLockBuilder() - .setLockCompletionTime(fakeClock.nowUtc()) - .setUnlockRequestTime(fakeClock.nowUtc()) + .setLockCompletionTime(clock.nowUtc()) + .setUnlockRequestTime(clock.nowUtc()) .build()); action.run(); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); @@ -130,8 +120,8 @@ void testSuccess_admin_unlock() { saveRegistryLock( createDefaultLockBuilder() .isSuperuser(true) - .setLockCompletionTime(fakeClock.nowUtc()) - .setUnlockRequestTime(fakeClock.nowUtc()) + .setLockCompletionTime(clock.nowUtc()) + .setUnlockRequestTime(clock.nowUtc()) .build()); user = user.asBuilder() @@ -159,7 +149,7 @@ void testFailure_invalidCode() { @Test void testFailure_expiredLock() { saveRegistryLock(createDefaultLockBuilder().build()); - fakeClock.advanceBy(Duration.standardDays(1)); + clock.advanceBy(Duration.standardDays(1)); action.run(); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST); assertThat(response.getPayload()).isEqualTo("The pending lock has expired; please try again"); @@ -181,8 +171,8 @@ void testFailure_nonAdmin_unlock() { saveRegistryLock( createDefaultLockBuilder() .isSuperuser(true) - .setLockCompletionTime(fakeClock.nowUtc()) - .setUnlockRequestTime(fakeClock.nowUtc()) + .setLockCompletionTime(clock.nowUtc()) + .setUnlockRequestTime(clock.nowUtc()) .build()); action.run(); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST); @@ -209,7 +199,7 @@ private ConsoleRegistryLockVerifyAction createAction(String verificationCode) { new DomainLockUtils( new DeterministicStringGenerator(StringGenerator.Alphabets.BASE_58), "adminreg", - new CloudTasksHelper(fakeClock).getTestCloudTasksUtils()); + new CloudTasksHelper(clock).getTestCloudTasksUtils()); response = (FakeResponse) params.response(); return new ConsoleRegistryLockVerifyAction(params, domainLockUtils, verificationCode); } diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java index 3ff07068446..4f4872c0da9 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java @@ -28,22 +28,17 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import com.google.gson.Gson; import google.registry.model.console.ConsoleUpdateHistory; 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.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; import google.registry.request.RequestModule; import google.registry.request.auth.AuthResult; import google.registry.testing.ConsoleApiParamsUtils; -import google.registry.testing.FakeClock; -import google.registry.testing.FakeResponse; import google.registry.testing.SystemPropertyExtension; -import google.registry.tools.GsonUtils; import google.registry.util.EmailMessage; import google.registry.util.RegistryEnvironment; import jakarta.mail.internet.AddressException; @@ -59,11 +54,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; /** Tests for {@link google.registry.ui.server.console.ConsoleUpdateRegistrarAction}. */ -class ConsoleUpdateRegistrarActionTest { - private static final Gson GSON = GsonUtils.provideGson(); - private final FakeClock clock = new FakeClock(DateTime.parse("2025-01-01T00:00:00.000Z")); - private ConsoleApiParams consoleApiParams; - private FakeResponse response; +class ConsoleUpdateRegistrarActionTest extends ConsoleActionBaseTestCase { private Registrar registrar; @@ -77,10 +68,6 @@ class ConsoleUpdateRegistrarActionTest { @Order(Integer.MAX_VALUE) final SystemPropertyExtension systemPropertyExtension = new SystemPropertyExtension(); - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension(); - @BeforeEach void beforeEach() throws Exception { createTlds("app", "dev"); @@ -98,7 +85,6 @@ void beforeEach() throws Exception { .setEmailAddress("user@registrarId.com") .setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()) .build()); - consoleApiParams = createParams(); } @Test @@ -110,12 +96,12 @@ void testSuccess_updatesRegistrar() throws IOException { "TheRegistrar", "app, dev", false, - "\"2024-12-12T00:00:00.000Z\"")); + "\"2023-12-12T00:00:00.000Z\"")); action.run(); Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get(); assertThat(newRegistrar.getAllowedTlds()).containsExactly("app", "dev"); assertThat(newRegistrar.isRegistryLockAllowed()).isFalse(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get(); assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE); assertThat(history.getDescription()).hasValue("TheRegistrar"); @@ -143,8 +129,8 @@ void testFailure_pocVerificationInTheFuture() throws IOException { false, "\"2025-02-01T00:00:00.000Z\"")); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat((String) ((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat((String) response.getPayload()) .contains("Invalid value of LastPocVerificationDate - value is in the future"); } @@ -160,8 +146,8 @@ void testFails_missingWhoisContact() throws IOException { false, "\"2024-12-12T00:00:00.000Z\"")); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat((String) ((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat((String) response.getPayload()) .contains("Cannot modify allowed TLDs if there is no WHOIS abuse contact set"); } @@ -188,12 +174,12 @@ void testSuccess_presentWhoisContact() throws IOException { "TheRegistrar", "app, dev", false, - "\"2024-12-12T00:00:00.000Z\"")); + "\"2023-12-12T00:00:00.000Z\"")); action.run(); Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get(); assertThat(newRegistrar.getAllowedTlds()).containsExactly("app", "dev"); assertThat(newRegistrar.isRegistryLockAllowed()).isFalse(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); } @Test @@ -205,7 +191,7 @@ void testSuccess_sendsEmail() throws AddressException, IOException { "TheRegistrar", "app, dev", false, - "\"2024-12-12T00:00:00.000Z\"")); + "\"2023-12-12T00:00:00.000Z\"")); action.run(); verify(consoleApiParams.sendEmailUtils().gmailClient, times(1)) .sendEmail( @@ -215,11 +201,11 @@ void testSuccess_sendsEmail() throws AddressException, IOException { + " environment") .setBody( "The following changes were made in registry unittest environment to the" - + " registrar TheRegistrar by user user@registrarId.com:\n" + + " registrar TheRegistrar by admin fte@email.tld:\n" + "\n" + "allowedTlds: null -> [app, dev]\n" + "lastPocVerificationDate: 1970-01-01T00:00:00.000Z ->" - + " 2024-12-12T00:00:00.000Z\n") + + " 2023-12-12T00:00:00.000Z\n") .setRecipients(ImmutableList.of(new InternetAddress("notification@test.example"))) .build()); } diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleUserDataActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleUserDataActionTest.java index d3054aaad89..c999c509bea 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleUserDataActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleUserDataActionTest.java @@ -21,13 +21,8 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; -import com.google.gson.Gson; -import google.registry.model.console.User; -import google.registry.persistence.transaction.JpaTestExtensions; -import google.registry.request.RequestModule; import google.registry.request.auth.AuthResult; import google.registry.testing.ConsoleApiParamsUtils; -import google.registry.testing.DatabaseHelper; import google.registry.testing.FakeResponse; import jakarta.servlet.http.Cookie; import java.io.IOException; @@ -35,41 +30,25 @@ import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; /** Tests for {@link google.registry.ui.server.console.ConsoleUserDataAction}. */ -class ConsoleUserDataActionTest { - - private static final Gson GSON = RequestModule.provideGson(); - - private ConsoleApiParams consoleApiParams; - - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().buildIntegrationTestExtension(); +class ConsoleUserDataActionTest extends ConsoleActionBaseTestCase { @Test void testSuccess_hasXSRFCookie() throws IOException { - User user = DatabaseHelper.createAdminUser("email@email.com"); - AuthResult authResult = AuthResult.createUser(user); - ConsoleUserDataAction action = - createAction(Optional.of(ConsoleApiParamsUtils.createFake(authResult))); + ConsoleUserDataAction action = createAction(Optional.of(consoleApiParams)); action.run(); - List cookies = ((FakeResponse) consoleApiParams.response()).getCookies(); + List cookies = response.getCookies(); assertThat(cookies.stream().map(cookie -> cookie.getName()).collect(toImmutableList())) .containsExactly("X-CSRF-Token"); } @Test void testSuccess_getContactInfo() throws IOException { - User user = DatabaseHelper.createAdminUser("email@email.com"); - AuthResult authResult = AuthResult.createUser(user); - ConsoleUserDataAction action = - createAction(Optional.of(ConsoleApiParamsUtils.createFake(authResult))); + ConsoleUserDataAction action = createAction(Optional.of(consoleApiParams)); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); - Map jsonObject = - GSON.fromJson(((FakeResponse) consoleApiParams.response()).getPayload(), Map.class); + assertThat(response.getStatus()).isEqualTo(SC_OK); + Map jsonObject = GSON.fromJson(response.getPayload(), Map.class); assertThat(jsonObject) .containsExactly( "userRoles", @@ -92,7 +71,7 @@ void testSuccess_getContactInfo() throws IOException { void testFailure_notAuthenticated() throws IOException { ConsoleUserDataAction action = createAction(Optional.empty()); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_UNAUTHORIZED); + assertThat(response.getStatus()).isEqualTo(SC_UNAUTHORIZED); } private ConsoleUserDataAction createAction(Optional maybeConsoleApiParams) @@ -101,6 +80,7 @@ private ConsoleUserDataAction createAction(Optional maybeConso maybeConsoleApiParams.orElseGet( () -> ConsoleApiParamsUtils.createFake(AuthResult.NOT_AUTHENTICATED)); when(consoleApiParams.request().getMethod()).thenReturn("GET"); + response = (FakeResponse) consoleApiParams.response(); return new ConsoleUserDataAction( consoleApiParams, "Nomulus", "support@example.com", "+1 (212) 867 5309", "test"); } diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java index 5654aa5c069..517d8bf59f1 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java @@ -29,14 +29,11 @@ import com.google.api.services.directory.Directory.Users.Insert; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.gson.Gson; import google.registry.model.console.GlobalRole; import google.registry.model.console.RegistrarRole; import google.registry.model.console.User; import google.registry.model.console.UserRoles; import google.registry.persistence.VKey; -import google.registry.persistence.transaction.JpaTestExtensions; -import google.registry.request.RequestModule; import google.registry.request.auth.AuthResult; import google.registry.testing.CloudTasksHelper; import google.registry.testing.ConsoleApiParamsUtils; @@ -53,11 +50,8 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -class ConsoleUsersActionTest { - - private static final Gson GSON = RequestModule.provideGson(); +class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { private final Directory directory = mock(Directory.class); private final Users users = mock(Users.class); @@ -70,12 +64,6 @@ class ConsoleUsersActionTest { private StringGenerator passwordGenerator = new DeterministicStringGenerator("abcdefghijklmnopqrstuvwxyz"); - private ConsoleApiParams consoleApiParams; - - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().buildIntegrationTestExtension(); - @BeforeEach void beforeEach() { User dbUser1 = @@ -130,7 +118,6 @@ void testSuccess_registrarAccess() throws IOException { Optional.of("GET"), Optional.empty()); action.run(); - var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getPayload()) .isEqualTo( "[{\"emailAddress\":\"test1@test.com\",\"role\":\"PRIMARY_CONTACT\"},{\"emailAddress\":\"test2@test.com\",\"role\":\"PRIMARY_CONTACT\"}]"); @@ -155,7 +142,6 @@ void testFailure_noPermission() throws IOException { Optional.of("GET"), Optional.empty()); action.run(); - var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); } @@ -172,7 +158,6 @@ void testFailure_invalidPrefix() throws IOException { when(directory.users()).thenReturn(users); when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert); action.run(); - var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); assertThat(response.getPayload()).contains("Email prefix is invalid"); } @@ -190,7 +175,6 @@ void testSuccess_createsUser() throws IOException { when(directory.users()).thenReturn(users); when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert); action.run(); - var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(SC_CREATED); assertThat(response.getPayload()) .contains( @@ -215,7 +199,6 @@ void testFailure_noPermissionToDeleteUser() throws IOException { when(directory.users()).thenReturn(users); when(users.delete(any(String.class))).thenReturn(delete); action.run(); - var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); assertThat(response.getPayload()) .contains("Can't update user not associated with registrarId TheRegistrar"); @@ -234,7 +217,6 @@ void testFailure_userDoesntExist() throws IOException { when(directory.users()).thenReturn(users); when(users.delete(any(String.class))).thenReturn(delete); action.run(); - var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); assertThat(response.getPayload()).contains("User email-1@email.com doesn't exist"); } @@ -258,7 +240,6 @@ void testSuccess_deletesUser() throws IOException { when(directory.users()).thenReturn(users); when(users.delete(any(String.class))).thenReturn(delete); action.run(); - var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat(DatabaseHelper.loadByKeyIfPresent(VKey.create(User.class, "test2@test.com"))) .isEmpty(); @@ -299,7 +280,6 @@ void testSuccess_removesRole() throws IOException { when(directory.users()).thenReturn(users); when(users.delete(any(String.class))).thenReturn(delete); action.run(); - var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(SC_OK); Optional actualUser = DatabaseHelper.loadByKeyIfPresent(VKey.create(User.class, "test4@test.com")); @@ -343,7 +323,6 @@ void testFailure_limitedTo4UsersPerRegistrar() throws IOException { when(directory.users()).thenReturn(users); when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert); action.run(); - var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); assertThat(response.getPayload()).contains("Total users amount per registrar is limited to 4"); } @@ -372,7 +351,6 @@ void testSuccess_updatesUserRole() throws IOException { new UserData("test2@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null))); action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); action.run(); - var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat( DatabaseHelper.loadByKey(VKey.create(User.class, "test2@test.com")) @@ -398,7 +376,6 @@ void testFailure_noPermissionToUpdateUser() throws IOException { Optional.of( new UserData("test3@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null))); action.run(); - var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); assertThat(response.getPayload()) .contains("Can't update user not associated with registrarId TheRegistrar"); @@ -413,6 +390,7 @@ private ConsoleUsersAction createAction( maybeConsoleApiParams.orElseGet( () -> ConsoleApiParamsUtils.createFake(AuthResult.NOT_AUTHENTICATED)); when(consoleApiParams.request().getMethod()).thenReturn(method.orElse("GET")); + response = (FakeResponse) consoleApiParams.response(); return new ConsoleUsersAction( consoleApiParams, directory, diff --git a/core/src/test/java/google/registry/ui/server/console/RegistrarsActionTest.java b/core/src/test/java/google/registry/ui/server/console/RegistrarsActionTest.java index 8ce0b3b66e7..a388c9601e3 100644 --- a/core/src/test/java/google/registry/ui/server/console/RegistrarsActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/RegistrarsActionTest.java @@ -28,7 +28,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.gson.Gson; import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.console.GlobalRole; import google.registry.model.console.RegistrarRole; @@ -36,7 +35,6 @@ import google.registry.model.console.UserRoles; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarPoc; -import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; import google.registry.request.RequestModule; import google.registry.request.auth.AuthResult; @@ -51,13 +49,9 @@ import java.util.Optional; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; /** Tests for {@link google.registry.ui.server.console.RegistrarsAction}. */ -class RegistrarsActionTest { - - private static final Gson GSON = RequestModule.provideGson(); - private ConsoleApiParams consoleApiParams; +class RegistrarsActionTest extends ConsoleActionBaseTestCase { private StringGenerator passwordGenerator = new DeterministicStringGenerator("abcdefghijklmnopqrstuvwxyz"); @@ -94,10 +88,6 @@ class RegistrarsActionTest { "{ \"street\": [\"test street\"], \"city\": \"test city\", \"state\": \"test state\"," + " \"zip\": \"00700\", \"countryCode\": \"US\" }"); - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().buildIntegrationTestExtension(); - @Test void testSuccess_onlyRealAndOteRegistrars() { Registrar registrar = persistNewRegistrar("registrarId"); @@ -115,8 +105,8 @@ void testSuccess_onlyRealAndOteRegistrars() { createUser( new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_LEAD).build()))); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); - String payload = ((FakeResponse) consoleApiParams.response()).getPayload(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + String payload = response.getPayload(); var actualRegistrarIds = ImmutableList.copyOf(GSON.fromJson(payload, Registrar[].class)).stream() @@ -135,8 +125,8 @@ void testSuccess_getRegistrars() { AuthResult.createUser( createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); - String payload = ((FakeResponse) consoleApiParams.response()).getPayload(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + String payload = response.getPayload(); assertThat( ImmutableList.of( "\"registrarId\":\"NewRegistrar\"", @@ -162,8 +152,8 @@ void testSuccess_getOnlyAllowedRegistrars() { .build()))); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); - String payload = ((FakeResponse) consoleApiParams.response()).getPayload(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + String payload = response.getPayload(); Registrar[] registrars = GSON.fromJson(payload, Registrar[].class); assertThat(registrars).hasLength(1); assertThat(registrars[0].getRegistrarId()).isEqualTo("registrarId"); @@ -171,12 +161,9 @@ void testSuccess_getOnlyAllowedRegistrars() { @Test void testSuccess_createRegistrar() { - RegistrarsAction action = - createAction( - Action.Method.POST, - AuthResult.createUser(createUser(new UserRoles.Builder().setIsAdmin(true).build()))); + RegistrarsAction action = createAction(Action.Method.POST, AuthResult.createUser(fteUser)); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); Registrar r = loadRegistrar("regIdTest"); assertThat(r).isNotNull(); assertThat( @@ -202,14 +189,10 @@ void testFailure_createRegistrar_missingValue() { .filter(entry -> !entry.getKey().equals(key)) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); RegistrarsAction action = - createAction( - Action.Method.POST, - AuthResult.createUser( - createUser(new UserRoles.Builder().setIsAdmin(true).build()))); + createAction(Action.Method.POST, AuthResult.createUser(fteUser)); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()) - .isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo( String.format( "Missing value for %s", userFriendlyKeysToRegistrarKeys.get(key))); @@ -219,13 +202,10 @@ void testFailure_createRegistrar_missingValue() { @Test void testFailure_createRegistrar_existingRegistrar() { saveRegistrar("regIdTest"); - RegistrarsAction action = - createAction( - Action.Method.POST, - AuthResult.createUser(createUser(new UserRoles.Builder().setIsAdmin(true).build()))); + RegistrarsAction action = createAction(Action.Method.POST, AuthResult.createUser(fteUser)); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo("Registrar with registrarId regIdTest already exists"); } @@ -237,6 +217,7 @@ private User createUser(UserRoles userRoles) { private RegistrarsAction createAction(Action.Method method, AuthResult authResult) { consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); when(consoleApiParams.request().getMethod()).thenReturn(method.toString()); + response = (FakeResponse) consoleApiParams.response(); if (method.equals(Action.Method.GET)) { return new RegistrarsAction( consoleApiParams, Optional.ofNullable(null), passwordGenerator, passcodeGenerator); diff --git a/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java b/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java index 1012fb997bd..d2b5bbac84e 100644 --- a/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java @@ -17,7 +17,6 @@ import static com.google.common.truth.Truth.assertThat; import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL; import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE; -import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.loadByEntity; import static google.registry.testing.DatabaseHelper.loadSingleton; import static google.registry.testing.DatabaseHelper.persistActiveContact; @@ -33,36 +32,28 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; -import com.google.gson.Gson; import com.google.gson.JsonElement; import google.registry.flows.DaggerEppTestComponent; import google.registry.flows.EppController; import google.registry.flows.EppTestComponent; import google.registry.model.common.FeatureFlag; import google.registry.model.console.ConsoleUpdateHistory; -import google.registry.model.console.GlobalRole; import google.registry.model.console.RegistrarRole; import google.registry.model.console.User; import google.registry.model.console.UserRoles; import google.registry.model.domain.Domain; import google.registry.model.eppcommon.StatusValue; -import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.auth.AuthResult; import google.registry.testing.ConsoleApiParamsUtils; -import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; -import google.registry.tools.GsonUtils; +import google.registry.ui.server.console.ConsoleActionBaseTestCase; import google.registry.ui.server.console.ConsoleApiParams; import java.util.Optional; -import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; /** Tests for {@link ConsoleBulkDomainAction}. */ -public class ConsoleBulkDomainActionTest { - - private static final Gson GSON = GsonUtils.provideGson(); +public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase { private static ImmutableSet serverSuspensionStatuses = ImmutableSet.of( @@ -72,14 +63,7 @@ public class ConsoleBulkDomainActionTest { StatusValue.SERVER_DELETE_PROHIBITED, StatusValue.SERVER_HOLD); - private final FakeClock clock = new FakeClock(DateTime.parse("2024-05-13T00:00:00.000Z")); - - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension(); - private EppController eppController; - private FakeResponse fakeResponse; private Domain domain; @BeforeEach @@ -96,7 +80,6 @@ void beforeEach() { .build() .startRequest() .eppController(); - createTld("tld"); domain = persistDomainWithDependentResources( "example", @@ -115,8 +98,8 @@ void testSuccess_delete() { GSON.toJsonTree( ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test"))); action.run(); - assertThat(fakeResponse.getStatus()).isEqualTo(SC_OK); - assertThat(fakeResponse.getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()) .isEqualTo( """ {"example.tld":{"message":"Command completed successfully; action pending",\ @@ -129,22 +112,14 @@ void testSuccess_delete() { @Test void testSuccess_suspend() throws Exception { - User adminUser = - persistResource( - new User.Builder() - .setEmailAddress("email@email.com") - .setUserRoles( - new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build()) - .build()); ConsoleBulkDomainAction action = createAction( "SUSPEND", GSON.toJsonTree( - ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test")), - adminUser); + ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test"))); action.run(); - assertThat(fakeResponse.getStatus()).isEqualTo(SC_OK); - assertThat(fakeResponse.getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()) .isEqualTo( """ {"example.tld":{"message":"Command completed successfully","responseCode":1000}}"""); @@ -157,25 +132,17 @@ void testSuccess_suspend() throws Exception { @Test void testSuccess_unsuspend() throws Exception { - User adminUser = - persistResource( - new User.Builder() - .setEmailAddress("email@email.com") - .setUserRoles( - new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build()) - .build()); persistResource(domain.asBuilder().addStatusValues(serverSuspensionStatuses).build()); ConsoleBulkDomainAction action = createAction( "UNSUSPEND", GSON.toJsonTree( - ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test")), - adminUser); + ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test"))); assertThat(loadByEntity(domain).getStatusValues()) .containsAtLeastElementsIn(serverSuspensionStatuses); action.run(); - assertThat(fakeResponse.getStatus()).isEqualTo(SC_OK); - assertThat(fakeResponse.getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()) .isEqualTo( """ {"example.tld":{"message":"Command completed successfully","responseCode":1000}}"""); @@ -197,8 +164,8 @@ void testHalfSuccess_halfNonexistent() throws Exception { "reason", "test"))); action.run(); - assertThat(fakeResponse.getStatus()).isEqualTo(SC_OK); - assertThat(fakeResponse.getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()) .isEqualTo( """ {"example.tld":{"message":"Command completed successfully; action pending","responseCode":1001},\ @@ -214,8 +181,8 @@ void testHalfSuccess_halfNonexistent() throws Exception { void testFailure_badActionString() { ConsoleBulkDomainAction action = createAction("bad", GSON.toJsonTree(ImmutableMap.of())); action.run(); - assertThat(fakeResponse.getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(fakeResponse.getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo( "No enum constant" + " google.registry.ui.server.console.domains.ConsoleDomainActionType.BulkAction.bad"); @@ -225,8 +192,8 @@ void testFailure_badActionString() { void testFailure_emptyBody() { ConsoleBulkDomainAction action = createAction("DELETE", null); action.run(); - assertThat(fakeResponse.getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(fakeResponse.getPayload()).isEqualTo("Bulk action payload must be present"); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()).isEqualTo("Bulk action payload must be present"); } @Test @@ -247,7 +214,7 @@ void testFailure_noPermission() { .build()) .build()); action.run(); - assertThat(fakeResponse.getStatus()).isEqualTo(SC_FORBIDDEN); + assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); } // @ptkach - reenable with suspend change @@ -271,21 +238,14 @@ void testFailure_noPermission() { // } private ConsoleBulkDomainAction createAction(String action, JsonElement payload) { - User user = - persistResource( - new User.Builder() - .setEmailAddress("email@email.com") - .setUserRoles( - new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build()) - .build()); - return createAction(action, payload, user); + return createAction(action, payload, fteUser); } private ConsoleBulkDomainAction createAction(String action, JsonElement payload, User user) { AuthResult authResult = AuthResult.createUser(user); ConsoleApiParams params = ConsoleApiParamsUtils.createFake(authResult); when(params.request().getMethod()).thenReturn("POST"); - fakeResponse = (FakeResponse) params.response(); + response = (FakeResponse) params.response(); return new ConsoleBulkDomainAction( params, eppController, "TheRegistrar", action, Optional.ofNullable(payload)); } diff --git a/core/src/test/java/google/registry/ui/server/console/settings/ContactActionTest.java b/core/src/test/java/google/registry/ui/server/console/settings/ContactActionTest.java index e5137c91845..00338b69ae5 100644 --- a/core/src/test/java/google/registry/ui/server/console/settings/ContactActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/settings/ContactActionTest.java @@ -20,7 +20,6 @@ import static google.registry.model.registrar.RegistrarPoc.Type.ADMIN; import static google.registry.model.registrar.RegistrarPoc.Type.MARKETING; import static google.registry.model.registrar.RegistrarPoc.Type.TECH; -import static google.registry.testing.DatabaseHelper.createAdminUser; import static google.registry.testing.DatabaseHelper.insertInDb; import static google.registry.testing.DatabaseHelper.loadAllOf; import static google.registry.testing.SqlHelper.saveRegistrar; @@ -36,19 +35,16 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.gson.Gson; 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.RegistrarPoc; -import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; -import google.registry.request.RequestModule; import google.registry.request.auth.AuthResult; import google.registry.testing.ConsoleApiParamsUtils; import google.registry.testing.FakeResponse; -import google.registry.ui.server.console.ConsoleApiParams; +import google.registry.ui.server.console.ConsoleActionBaseTestCase; import google.registry.util.EmailMessage; import jakarta.mail.internet.AddressException; import jakarta.mail.internet.InternetAddress; @@ -57,10 +53,9 @@ import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; /** Tests for {@link google.registry.ui.server.console.settings.ContactAction}. */ -class ContactActionTest { +class ContactActionTest extends ConsoleActionBaseTestCase { private static String jsonRegistrar1 = "{\"name\":\"Test Registrar 1\"," + "\"emailAddress\":\"test.registrar1@example.com\"," @@ -70,17 +65,10 @@ class ContactActionTest { + "\"visibleInWhoisAsTech\":false,\"visibleInDomainWhoisAsAbuse\":false}"; private Registrar testRegistrar; - private ConsoleApiParams consoleApiParams; private RegistrarPoc adminPoc; private RegistrarPoc techPoc; private RegistrarPoc marketingPoc; - private static final Gson GSON = RequestModule.provideGson(); - - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().buildIntegrationTestExtension(); - @BeforeEach void beforeEach() { testRegistrar = saveRegistrar("registrarId"); @@ -122,28 +110,19 @@ void beforeEach() { @Test void testSuccess_getContactInfo() throws IOException { insertInDb(adminPoc); - ContactAction action = - createAction( - Action.Method.GET, - AuthResult.createUser(createAdminUser("email@email.com")), - testRegistrar.getRegistrarId()); + ContactAction action = createAction(Action.Method.GET, fteUser, testRegistrar.getRegistrarId()); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) - .isEqualTo("[" + jsonRegistrar1 + "]"); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()).isEqualTo("[" + jsonRegistrar1 + "]"); } @Test void testSuccess_noOp() throws IOException { insertInDb(adminPoc); ContactAction action = - createAction( - Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), - testRegistrar.getRegistrarId(), - adminPoc); + createAction(Action.Method.POST, fteUser, testRegistrar.getRegistrarId(), adminPoc); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); verify(consoleApiParams.sendEmailUtils().gmailClient, never()).sendEmail(any()); } @@ -151,14 +130,10 @@ void testSuccess_noOp() throws IOException { void testSuccess_onlyContactsWithNonEmptyType() throws IOException { adminPoc = adminPoc.asBuilder().setTypes(ImmutableSet.of()).build(); insertInDb(adminPoc); - ContactAction action = - createAction( - Action.Method.GET, - AuthResult.createUser(createAdminUser("email@email.com")), - testRegistrar.getRegistrarId()); + ContactAction action = createAction(Action.Method.GET, fteUser, testRegistrar.getRegistrarId()); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()).isEqualTo("[]"); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()).isEqualTo("[]"); } @Test @@ -166,13 +141,9 @@ void testSuccess_postCreateContactInfo() throws IOException { insertInDb(adminPoc); ContactAction action = createAction( - Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), - testRegistrar.getRegistrarId(), - adminPoc, - techPoc); + Action.Method.POST, fteUser, testRegistrar.getRegistrarId(), adminPoc, techPoc); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat( loadAllOf(RegistrarPoc.class).stream() .filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId())) @@ -186,13 +157,9 @@ void testSuccess_postUpdateContactInfo() throws IOException { insertInDb(techPoc.asBuilder().setEmailAddress("incorrect@email.com").build()); ContactAction action = createAction( - Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), - testRegistrar.getRegistrarId(), - adminPoc, - techPoc); + Action.Method.POST, fteUser, testRegistrar.getRegistrarId(), adminPoc, techPoc); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); HashMap testResult = new HashMap<>(); loadAllOf(RegistrarPoc.class).stream() .filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId())) @@ -210,13 +177,13 @@ void testFailure_postUpdateContactInfo_duplicateEmails() throws IOException { ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), + fteUser, testRegistrar.getRegistrarId(), adminPoc, techPoc.asBuilder().setEmailAddress("test.registrar1@example.com").build()); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo( "One email address (test.registrar1@example.com) cannot be used for multiple contacts"); assertThat( @@ -232,13 +199,12 @@ void testFailure_postUpdateContactInfo_requiredContactRemoved() throws IOExcepti ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), + fteUser, testRegistrar.getRegistrarId(), adminPoc.asBuilder().setTypes(ImmutableSet.of(ABUSE)).build()); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) - .isEqualTo("Must have at least one primary contact"); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()).isEqualTo("Must have at least one primary contact"); assertThat( loadAllOf(RegistrarPoc.class).stream() .filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId())) @@ -253,7 +219,7 @@ void testFailure_postUpdateContactInfo_phoneNumberRemoved() throws IOException { ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), + fteUser, testRegistrar.getRegistrarId(), adminPoc .asBuilder() @@ -261,8 +227,8 @@ void testFailure_postUpdateContactInfo_phoneNumberRemoved() throws IOException { .setTypes(ImmutableSet.of(ADMIN, TECH)) .build()); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo("Please provide a phone number for at least one technical contact"); assertThat( loadAllOf(RegistrarPoc.class).stream() @@ -276,12 +242,12 @@ void testFailure_postUpdateContactInfo_whoisContactMissingPhoneNumber() throws I ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), + fteUser, testRegistrar.getRegistrarId(), adminPoc.asBuilder().setPhoneNumber(null).setVisibleInDomainWhoisAsAbuse(true).build()); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo("The abuse contact visible in domain WHOIS query must have a phone number"); assertThat( loadAllOf(RegistrarPoc.class).stream() @@ -297,12 +263,12 @@ void testFailure_postUpdateContactInfo_whoisContactPhoneNumberRemoved() throws I ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), + fteUser, testRegistrar.getRegistrarId(), adminPoc.asBuilder().setVisibleInDomainWhoisAsAbuse(false).build()); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo("An abuse contact visible in domain WHOIS query must be designated"); assertThat( loadAllOf(RegistrarPoc.class).stream() @@ -317,7 +283,7 @@ void testFailure_postUpdateContactInfo_newContactCannotSetRegistryLockPassword() ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), + fteUser, testRegistrar.getRegistrarId(), adminPoc .asBuilder() @@ -325,8 +291,8 @@ void testFailure_postUpdateContactInfo_newContactCannotSetRegistryLockPassword() .setRegistryLockEmailAddress("lock@example.com") .build()); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo("Cannot set registry lock password directly on new contact"); assertThat( loadAllOf(RegistrarPoc.class).stream() @@ -347,12 +313,12 @@ void testFailure_postUpdateContactInfo_cannotModifyRegistryLockEmail() throws IO ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), + fteUser, testRegistrar.getRegistrarId(), adminPoc.asBuilder().setRegistryLockEmailAddress("unlock@example.com").build()); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo("Cannot modify registryLockEmailAddress through the UI"); assertThat( loadAllOf(RegistrarPoc.class).stream() @@ -374,12 +340,12 @@ void testFailure_postUpdateContactInfo_cannotSetIsAllowedToSetRegistryLockPasswo ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), + fteUser, testRegistrar.getRegistrarId(), adminPoc.asBuilder().setAllowedToSetRegistryLockPassword(true).build()); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(((FakeResponse) consoleApiParams.response()).getPayload()) + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) .isEqualTo("Cannot modify isAllowedToSetRegistryLockPassword through the UI"); assertThat( loadAllOf(RegistrarPoc.class).stream() @@ -392,13 +358,9 @@ void testFailure_postUpdateContactInfo_cannotSetIsAllowedToSetRegistryLockPasswo void testSuccess_sendsEmail() throws IOException, AddressException { insertInDb(techPoc.asBuilder().setEmailAddress("incorrect@email.com").build()); ContactAction action = - createAction( - Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), - testRegistrar.getRegistrarId(), - techPoc); + createAction(Action.Method.POST, fteUser, testRegistrar.getRegistrarId(), techPoc); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); verify(consoleApiParams.sendEmailUtils().gmailClient, times(1)) .sendEmail( EmailMessage.newBuilder() @@ -407,7 +369,7 @@ void testSuccess_sendsEmail() throws IOException, AddressException { + " environment") .setBody( "The following changes were made in registry unittest environment to the" - + " registrar registrarId by admin email@email.com:\n" + + " registrar registrarId by admin fte@email.tld:\n" + "\n" + "contacts:\n" + " ADDED:\n" @@ -442,13 +404,9 @@ void testSuccess_postDeleteContactInfo() throws IOException { insertInDb(adminPoc, techPoc, marketingPoc); ContactAction action = createAction( - Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), - testRegistrar.getRegistrarId(), - adminPoc, - techPoc); + Action.Method.POST, fteUser, testRegistrar.getRegistrarId(), adminPoc, techPoc); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat( loadAllOf(RegistrarPoc.class).stream() .filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId())) @@ -463,43 +421,39 @@ void testFailure_postDeleteContactInfo_missingPermission() throws IOException { ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser( - new User.Builder() - .setEmailAddress("email@email.com") - .setUserRoles( - new UserRoles.Builder() - .setRegistrarRoles( - ImmutableMap.of( - testRegistrar.getRegistrarId(), RegistrarRole.ACCOUNT_MANAGER)) - .build()) - .build()), + new User.Builder() + .setEmailAddress("email@email.com") + .setUserRoles( + new UserRoles.Builder() + .setRegistrarRoles( + ImmutableMap.of( + testRegistrar.getRegistrarId(), RegistrarRole.ACCOUNT_MANAGER)) + .build()) + .build(), testRegistrar.getRegistrarId(), techPoc); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_FORBIDDEN); + assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); } @Test void testFailure_changesAdminEmail() throws Exception { insertInDb(adminPoc.asBuilder().setEmailAddress("oldemail@example.com").build()); ContactAction action = - createAction( - Action.Method.POST, - AuthResult.createUser(createAdminUser("email@email.com")), - testRegistrar.getRegistrarId(), - adminPoc); + createAction(Action.Method.POST, fteUser, testRegistrar.getRegistrarId(), adminPoc); action.run(); - FakeResponse fakeResponse = (FakeResponse) consoleApiParams.response(); + FakeResponse fakeResponse = response; assertThat(fakeResponse.getStatus()).isEqualTo(400); assertThat(fakeResponse.getPayload()) .isEqualTo("Cannot remove or change the email address of primary contacts"); } private ContactAction createAction( - Action.Method method, AuthResult authResult, String registrarId, RegistrarPoc... contacts) + Action.Method method, User user, String registrarId, RegistrarPoc... contacts) throws IOException { - consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); + consoleApiParams = ConsoleApiParamsUtils.createFake(AuthResult.createUser(user)); when(consoleApiParams.request().getMethod()).thenReturn(method.toString()); + response = (FakeResponse) consoleApiParams.response(); if (method.equals(Action.Method.GET)) { return new ContactAction(consoleApiParams, registrarId, Optional.empty()); } else { diff --git a/core/src/test/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsActionTest.java b/core/src/test/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsActionTest.java index eab21fc292c..f95113548d4 100644 --- a/core/src/test/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsActionTest.java @@ -26,13 +26,11 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Maps; -import com.google.gson.Gson; import google.registry.model.console.ConsoleUpdateHistory; 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.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; import google.registry.request.RequestModule; import google.registry.request.auth.AuthResult; @@ -40,24 +38,18 @@ import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role; import google.registry.testing.ConsoleApiParamsUtils; import google.registry.testing.DatabaseHelper; -import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; -import google.registry.ui.server.console.ConsoleApiParams; +import google.registry.ui.server.console.ConsoleActionBaseTestCase; import google.registry.ui.server.console.ConsoleModule; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.util.HashMap; -import org.joda.time.DateTime; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; /** Tests for {@link RdapRegistrarFieldsAction}. */ -public class RdapRegistrarFieldsActionTest { +public class RdapRegistrarFieldsActionTest extends ConsoleActionBaseTestCase { - private ConsoleApiParams consoleApiParams; - private static final Gson GSON = RequestModule.provideGson(); - private final FakeClock clock = new FakeClock(DateTime.parse("2023-08-01T00:00:00.000Z")); private final AuthenticatedRegistrarAccessor registrarAccessor = AuthenticatedRegistrarAccessor.createForTesting( ImmutableSetMultimap.of("TheRegistrar", Role.OWNER, "NewRegistrar", Role.OWNER)); @@ -81,10 +73,6 @@ public class RdapRegistrarFieldsActionTest { "{\"street\": [\"123 Example Boulevard\"], \"city\": \"Williamsburg\", \"state\":" + " \"NY\", \"zip\": \"11201\", \"countryCode\": \"US\"}")); - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension(); - @Test void testSuccess_setsAllFields() throws Exception { Registrar oldRegistrar = Registrar.loadRequiredRegistrarCached("TheRegistrar"); @@ -113,7 +101,7 @@ void testSuccess_setsAllFields() throws Exception { + " \"NL\", \"zip\": \"10011\", \"countryCode\": \"CA\"}")); RdapRegistrarFieldsAction action = createAction(); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get(); // skip cache assertThat(newRegistrar.getLocalizedAddress().toJsonMap()).isEqualTo(addressMap); assertThat(newRegistrar.getPhoneNumber()).isEqualTo("+1.4155552671"); @@ -130,34 +118,30 @@ void testSuccess_setsAllFields() throws Exception { @Test void testFailure_noAccessToRegistrar() throws Exception { Registrar newRegistrar = Registrar.loadByRegistrarIdCached("NewRegistrar").get(); - AuthResult onlyTheRegistrar = - AuthResult.createUser( - new User.Builder() - .setEmailAddress("email@email.example") - .setUserRoles( - new UserRoles.Builder() - .setRegistrarRoles( - ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT)) - .build()) - .build()); + User onlyTheRegistrar = + new User.Builder() + .setEmailAddress("email@email.example") + .setUserRoles( + new UserRoles.Builder() + .setRegistrarRoles( + ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT)) + .build()) + .build(); uiRegistrarMap.put("registrarId", "NewRegistrar"); RdapRegistrarFieldsAction action = createAction(onlyTheRegistrar); action.run(); - assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_FORBIDDEN); + assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); // should be no change assertThat(DatabaseHelper.loadByEntity(newRegistrar)).isEqualTo(newRegistrar); } - private AuthResult defaultUserAuth() { - return AuthResult.createUser(DatabaseHelper.createAdminUser("email@email.example")); - } - private RdapRegistrarFieldsAction createAction() throws IOException { - return createAction(defaultUserAuth()); + return createAction(fteUser); } - private RdapRegistrarFieldsAction createAction(AuthResult authResult) throws IOException { - consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); + private RdapRegistrarFieldsAction createAction(User user) throws IOException { + consoleApiParams = ConsoleApiParamsUtils.createFake(AuthResult.createUser(user)); + response = (FakeResponse) consoleApiParams.response(); when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.POST.toString()); doReturn(new BufferedReader(new StringReader(uiRegistrarMap.toString()))) .when(consoleApiParams.request()) diff --git a/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java b/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java index 6b47bf3a1d8..99d9f194cb1 100644 --- a/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java @@ -27,20 +27,14 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.ImmutableSortedMap; -import com.google.gson.Gson; import google.registry.flows.certs.CertificateChecker; import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.registrar.Registrar; -import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; import google.registry.request.RequestModule; -import google.registry.request.auth.AuthResult; import google.registry.request.auth.AuthenticatedRegistrarAccessor; -import google.registry.testing.ConsoleApiParamsUtils; -import google.registry.testing.DatabaseHelper; -import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; -import google.registry.ui.server.console.ConsoleApiParams; +import google.registry.ui.server.console.ConsoleActionBaseTestCase; import google.registry.ui.server.console.ConsoleModule; import java.io.BufferedReader; import java.io.IOException; @@ -49,19 +43,15 @@ import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; /** Tests for {@link google.registry.ui.server.console.settings.SecurityAction}. */ -class SecurityActionTest { +class SecurityActionTest extends ConsoleActionBaseTestCase { private static String jsonRegistrar1 = String.format( "{\"registrarId\": \"registrarId\", \"clientCertificate\": \"%s\"," + " \"ipAddressAllowList\": [\"192.168.1.1/32\"]}", SAMPLE_CERT2); - private static final Gson GSON = RequestModule.provideGson(); - private ConsoleApiParams consoleApiParams; - private final FakeClock clock = new FakeClock(); private Registrar testRegistrar; private AuthenticatedRegistrarAccessor registrarAccessor = @@ -77,10 +67,6 @@ class SecurityActionTest { ImmutableSet.of("secp256r1", "secp384r1"), clock); - @RegisterExtension - final JpaTestExtensions.JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension(); - @BeforeEach void beforeEach() { testRegistrar = saveRegistrar("registrarId"); @@ -91,7 +77,6 @@ void testSuccess_postRegistrarInfo() throws IOException { clock.setTo(DateTime.parse("2020-11-01T00:00:00Z")); SecurityAction action = createAction( - AuthResult.createUser(DatabaseHelper.createAdminUser("email@email.com")), testRegistrar.getRegistrarId()); action.run(); assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); @@ -105,9 +90,7 @@ void testSuccess_postRegistrarInfo() throws IOException { assertThat(history.getDescription()).hasValue("registrarId"); } - private SecurityAction createAction(AuthResult authResult, String registrarId) - throws IOException { - consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); + private SecurityAction createAction(String registrarId) throws IOException { when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.POST.toString()); doReturn(new BufferedReader(new StringReader(jsonRegistrar1))) .when(consoleApiParams.request()) From e6504c689b6b2ce922076945edfec3681d78d0e1 Mon Sep 17 00:00:00 2001 From: gbrodman Date: Fri, 6 Jun 2025 14:11:54 -0400 Subject: [PATCH 40/49] Update version of google-java-format (#2766) This picks up a few changes including aligning the placement of quotes in text blocks with the Google style guide. --- ...=> google-java-format-1.27.0-all-deps.jar} | Bin 3613813 -> 3613620 bytes java-format/google-java-format-git-diff.sh | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename java-format/{google-java-format-1.23.0-all-deps.jar => google-java-format-1.27.0-all-deps.jar} (87%) diff --git a/java-format/google-java-format-1.23.0-all-deps.jar b/java-format/google-java-format-1.27.0-all-deps.jar similarity index 87% rename from java-format/google-java-format-1.23.0-all-deps.jar rename to java-format/google-java-format-1.27.0-all-deps.jar index 05c37510a3bacf4bb137d3440e2f8dc24e4bbbfa..8083460e87d09f7b169f49b8b317c3b8c8533e4b 100644 GIT binary patch delta 300848 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¨$yu5BoHUPV^@Q&NF51oK^wC`Tqd08G)3V$H?x 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

b3BSA46l~@*l<1Kuy_L&)>c2 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pbFLn|g&mlt#L5@4U>ZE0p+xztuJQM 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

e6g2Q#mFaAj!jmY8_DOV+ z?GxdQ+*s^9s)VPy>aESfn?SwwO)w#krL<gKZFbNP(mf+W}!j8t=za{71{||IY+!s1F(w3=E4f&9+6L@ z!nnWOcpy&<#yY*o&_yctrxgu7_SIrb)gU%t;d^V*$Ff->P&*dfN@-WnsAi%Oa>En4 zIOCyk#buSBhPKJzxr80iubYY5z&=P=bBI`-BdB8^rM<5btsLNgIEN6;(a_h4pueMI zkYiw^V-kV3Fo~`wI}WBh95b9KE^y+w*h#|`PCAx48CdRQ;!Y@YuNuq8R^WtMif#p=o@Dlzw7&8^eRLi%AO=>K@EVN_vac?u@NSZrMF5i{)$ zYN0qnq;WwwmtNqwU14lo1*^i?^k{|gSm`mgjNp(R;=XBr(HRx&?+Sf>!cZx@g{z(M zV7i!B!O;6~<@9X2K|sl<1R1u)$pB%w*i9nTo6cQ$&$%0aUlE)CA7YmOaaI$K+|LYW4YQrK zEX{d_Bdd*P`7gKxMRU|0< zBkA8jB4Y>3jU%4J$ZU8HBjQg(#b{0=&cMVrQRVVobU;L<@Gm&f62~I zx1TGB)h)mDXn7UHQ4T;MJ)Z#fFp&pVYP_*)KHYn>+p}wy(eD@IvOefR_m7{}@Au|( z{n+&VpAQo#o1bN-$*KM-(zOhq(q)E!ynI`y%h%{KOI|)_(B%%g%$Ap1O}gBaFXT{h z`G7^2_tDdO^71yDE^nmEguGmmF5bSJt{ckh`RU^IS^2_jDXyoci}xqe^G5P|WV%Dw zgXy}7yzZOM>AECegpr8rqVyV~qGX<&oouJ`7JJTlC+SGaUSLgyBdNrQ9AqBB^%ioOEB~aa zKe0m)ntA&K`^0KFQ#1Ei&eZv+M$U9a3TLVry^TIfl~AfU)3t#vO=|wkmjBIioWIR% zEuCD2bstW{xk(v8dNVnn z!*ISp%=r>o&R1yQe2r$#H$+VKqNDRK^me{Qsq-C%Io}hB-AByMPdMNCnYNW*NTB+S zh{f+%=KO)1oIi1k^KaZoziXWTgjnyj!4R$uhH$ME<~4MrE$T(5r4q$|q?g`!OMA63 zG_!4x!1os+3en?F^W{M8G&>FqE7d*Qk`Pf9eDTZwh_b{wE{>cqz}QB?%)xX)(^P)?ggcDR>0^ zGI1!Iv!4k10n~90BH*=*#X6pbM|dW-@;Z2i*TZgJAA5KLuki+Wlb*fH8{!k56NJ$h zNp^Rslome?X7E#Iw@gvU@wWZDb0^JUJ7Q^Hk|8-Av0mvP*WpBe=VhV24>lF=t=rT* z=RmKd3ZyMXd(|>KN^=U3Pg7nOJn}@uKdt2-CG8H)-(_}Ni9Y!|&2EANtcYxGup0Rd zhyyfBgcw*6*;**PFTKoH@_sLMzYI~4qE6fo6_X<_wK%*5EZ!1v-U@YjYvl4aIG!Je zZag1-c_I4o<7t$C+XWJIoHp7$&OnLZLnDvL19+(vc%%%Kc~mr65_wc2A%ShE%$edp z8xpv0>W9(IGP@mwrCDmHeFeIHf_|dnmakXq;78ir`ijiKifPs#3kgNt#KV=CB77M{ z7H`2x`x+dU?O%#@6g-j`nmLIu(zmz9yraQrLC{VAJ+q&G1fdXD?xMFHfbimWOPd{KKV5*LGd6rY2QqLzGfZ!8vTEO09GC-s zR0{P$L?j13TAJR^R`qVW}>U7KnyX#;+cQ$lJpx(1o?bd zC;i5M-T|{dvOZS4#keZLY#!&UoN%l93-WVe@bgfI&!{no?cHz?8`bo%wL$T^H+%kq zl7%R(n)*B?SSECGqO`kwPG1ZWjK+v{!qQ=m4Kpa_xKzGjLTa`Kx-&7bfwcOc%^VL@ zF>WMt{m^nR^`WH-^<#aZ-aIRLaxMw?+s!F|B(|Gpi?fhr3UKdyWD;?{7d{#(3CNX2 zJ7H`y&y_T*w5;;}Dy9-Iqj@Od)-sZ%z~`VTzW{CcT(sr$(1FjVE#N}*;R`T;UyNb= z5{%)OVk%#V8T>L_!WR)~xg0n0D{wEr3J>wcc$8mFq-6=7;@98{z7*f^Yw;al#u&eU zj#>PA#`(1@&Tn9K_;QxRSFqOnMwZ8KV(t0OtP{V5b?3JQrK2qDvXmL)icSLT>#32khg5%_>Zcr&I6pP&)B=YXb zdzN`tknN_rXP(~YhXZIL0%bIFfq9XCr($IDJryHv7AqmZs6v%LZ8zpsV^c0jqKBv2 ztCBvcou595XogImR5kaz$|xXJey`P(6Sj)iD!+$T;k}6Q`;f&~qdvc%_Wm`<<7){4 z9zYR)5S{rt;;zatp0CGb{t(XP8*l;NNL)MyXQA(TD^q{AL;Q%w*|f8PFk0pQtn8ZgxHmH19F>)~7^~bl*mM1U)^9)W$pe zPUIO?ROvI~=><2<>PFy**O|4>DpN#ijB-_|Ps{zhEc1L?hO>lQvv~oLDRX|+Q?mGz z#1K`4NL2lx1N8~#zLZU?ACRbjHP+f{>bcmf=TceEqU!6}LG?USV?AG0spnqnzG~{Z zJj=Y2YFI4mxwiUxo~L?VsIi`}tJJf~x+};p?Vl)=WnLdbYGnzIEcPwIbyDre;tUb> zj=4Paj=4Pi&QpTi-dJP>zbQp?&WaxcoIVhmn?#)6BI5LaHt{|mkOc4{ z&f#GJHuWT-Z)Eq*4S7k>;JLZE)YIFoJ5*0+tDfdkBX3LH5bmhaM_Agt_~+EZFQ|oI zq5=PkTKRSLS{-W1UfGgdUzCnq=btgijuX^|p3M)!ux0X`CI$I^ibxq(?OpxDFrcjR zEAdPzn#42V_-?MM-tj$ua;tIyR*82+9%gTcmX7aLJTv%ru=)2$<3FG={pRz1DCR$* z3;&rmf?qI!|4I_q@0i8^z#RT3=J9_AGQt?tm%%h*)u*HzQU@2FPtvuBzwUzKDqidI z8Ohz6Z=gIZbe1cd2dRad60dgvY0$Knw3n57@*Mh<1R0DKVZdE~J)OdCH1`V0q%(wp z#g>mRt@zTy0=bM;l$LqdI@kpj=Dh>Au;$)9 z|2X?1K;jC2hj)xcEBe>Iz^4oY%h0r7r#_{^yq}8MjQ*=r=;_w6nl7w@mn!cN31JJL zG}p+3o(pzkwqNVobtojkcepyZsaZ(pBpMUW-lbG?K0}jy?BM`$B;ww>a`WLrW#bG^ zLVe17Wb~8f=3=w3!hE#Cd^}-}t}wTc+HO88*xl!UBSn^0WQ(NOj>SsImP`7r&Tx_vy`)*j-PLaLXA)$JgOtzh!ZQqOZgqdwu=<~fy!NgXX zuZT9?me4(y51FU;8viPwtTM}zqZRRl<7?MDi}^%R>Aev7nXj993H|0LoGkNIsT!^q zDI&&yDT8hp^E_R#Uc|Gxr7!b~_yCP`-SE!QvcTvf7-a`XLy;3W9adLc?q>!m%k=!xLbPRNLMMx$sknn%0f z)Mz&hjdrKC(gWk8Juxxb8)ruQU`q5fS}Xm3F(*0@^P{D>JUSRxMTg^>=qTJ5Jsl54 zM`LSr40c4v;pOOfyc3;(kE3Vcv*;xJD|#k=iJpc3L?`2~=-D_LJ&)Cip3mw{)*of#wY&1PPBYGp79=(ZO7`>IPh~B2@(L1&D=t?br zD|(;SAi7#>6TM&S5M85nj;_@P(C^UbgW8zrdTm1VA#F-@gHLnWNoX#0RGc+JPpQuk zC;bIUU$v>uf20OW(ssug3H-=9#4v^*k0!^$Cq>U6)i{o=##`5Jcay!=+o#F-9cQyGqm$OOFPd_%!)pT3!~5D;^>RGIl3En zMqk3(=*xI4x(DUaS7-(8!K=~NXa&8FPor<(i|Cu!8+{A=qVEJI=sVHTIV{pT;W0FD zj?m7;NwT}z{!SjcPLmMqU_VLhWwP{_bQep2ZxUUaYm+gC3MuGGCH^Op;VABzf}(mHfR>=wyCkAStoAxc_sYPRbIOzof)(r#i_N zir<8x@E@GdrekL&8JVBf5e37KBE0UV|w%#qWQn#qUdi#^MA*R=pVQv`X}yxjs6=CM*oA2(Z2$@ zKP7%&;-QS+m)IxvyG>jpERB#;B<;ouM>=R%m{!sxr#9I8iQ@^m>{3Ab5y$2yxp-7Sevb7-<-F3RD3{pf;AKpgG?23xT1grh zWvvk77L{Uun2h&=qdiIjg<*+L4XNZ&Ls9x$obn(_+@?Bov}6q>g)}f0-*Sie027n& zCFfLI*paNnql+F49;JKoeI8S8II9tBE(m0rI0T@7SFD46$_CLKE@m`gG^slbx{8RO9KeHH*gPU8-t%}mk z7g@gP{^knfvjKThqYy+mtSs<%Ul+N_bxsrm)@K-3Iw05nYgZPTtEXgDaQ^lTf;>a&msT@mrNcBx3D}Xmmv5*a>JK zI}t^Hu_BC%bs~iAj5)EBadoTxBnmy-^nHi-%*U1SXXBn9p6g zMK;E@l8F?;_h!i)itGs+%q`B-DLK?OOIA-k<7sN!Yz8hJvVM|VWpkNe`Y;Muc6Ju+ zIo3faziXaOyUv&9S86UME7$&q^J!=YNZ0;<@X+GM(AYMB^wz;5p(85ID-0!2p5bpz z$-QS4I))iC^+=FD?Tj#kRUqh++FRt-=AV-o=$^bAT!jlH=|^8=&@}Xi5gUMlSSd}z zAaschM)%lI^ob3_@Yryi5gUOSu~AqUI~|K-W3W6n7B|Jl({xP0n%G1<6gw-B(A&d* zIIu%M>>RegPO)o=JKw0?Bpqf!<`MKJw4FlIxvxLwnY0R4 zYrZeHIPgN+q4IW_2ZfjIPV?Y4^DsmIvdZ7`s<=7Fro)Pzi#o9xp_FfRr7QtI%?5E> zT)#k{8zj~8a+~MmNj_nKr|wG&^n0{_qC%Qbsl<)VrdsBN64>O(Q4oP0**q)AKNJ)G zB!a~ej16JaUh`k7&izVTGm5Zjoj41oDo%MKs)$Js@)6bXH9Hc8grm1)KMKpWs{<=T zM0g#W4_9$6h+POXwgA!CMKt}F;Dp$vD2^>e-`Hgs7+ZwFv8ymLwm1~hRabU@N1*$t zU{Xf~lR8S0c+u@qij^=igx3+CzrjSyJGA=rq+?A1(Yjn3g^j*PBX+>cG}@V(4L|3=x#yReo^Hmg)7I#LL*V;IZonA#Om!*m5+FtswNj z5uIW;p-1dy+D2~)O=G{HxBX;)f5MSn{bYZF#8O;&Bl2uQ*7fE?s!MB?3AR9~k_LE4 z+gKp&!2E)3789#}jnFMA6#hCtNiSuZqSz|fvAd8KyPH~l51PjAMeEpo$cwEGl-YHV z#p~LCsr31}OrLL~MU^(oJ3*V}90)aTGu7~V^xvdFf0(N@a#wit{40d zI>Z|C^SxJXem+l?SVegldl2#1I=Hd*$c{aPMzM#{B(?!vV;gC%9-+C~gdwpKf$Uqpk$AIfK_%qSp+iJZz#&VqxW>c%8cRj z$LHqz4lTsa8gvny+vJ^!QO&{A!46{hHj*m3+!Xml__}CimCFO#F_&6r(M~?`7|n5B ze(DKS$3PCfsk$JO*Oyy)M!*1HQUc#W*%fv?P!MU+lh?RI1^JA~k-t`8t5^v(h*ju-XR|NU2KlM zhsWrfCu1Mrnb?Qe6Z;5n#XiO-u}`os_9^~|eTIXv&zTUYQV%PUq=LW>m=7}VHGDC%@U}usQ@}}(`lF=QXeESe090Sld`(xDdNPH$6dtY z>7lJ6bZoNM=uiV2e})mfZ}AC2Q}(JbCBmVN_U+^tGZ`Ak3#>fb)Yo zk7LRP#p8pVszHr@rD;$6`%-VM{@-Em302NuVBVoAI=R>b?@ruZpX74L_& z@zby|-XG7z2jIo{K)e(mgpcBb@ppU(YuviSbeFr1)soBR+-= zkB?;|sXQ)z2Adk6$S#ggVhiJEu|@HzY(;z;dmw%`TSw)M@pIXZ_<8Kv`1!%fp`CE1 z!(<%pMod$sXNVZ*3MB+Ie_II!Y!Lb5)!H|xwXci@dd^$q9yH$ChsN8e<;Cxdt`_jCsJMH9Pr`=m@G!$fmmgD_GTk>VXXy_R{MOxhEh5qGE{WY24SI*%!t4P`+ z8zdhS845kAypw2_$d+bxO{uquU$50oe@53^MAUQc7w1owRel~x&a?D0i7$g5zYfjg z*P}3g1KP!JqWQfUc-^%@%$}TuRv|)X1gmr4mA<4 zu=l{;Sib=z7NoEkgulhPP+1DF(qjz5D(sQ9qlgCLEWv7T8X`zFZ zvehe-i_!^oMZ@BGqRqlrA)4Y8OfT0F?C0a5Qp_(R#1YtYWX1`I6Y*aIa0dy(+^CUu0S2d>sSzf1B>Es z;!3){I{r4Ui@%TM@po`b`~%z({}7wvAK|_D$M`n>2@b_SV@CW7mKOhtHH?4F+Q+|P zJ>z>>|Mb(0_)qNG_^<57`0wntfA}Blq4?kI>G(hF zh4_B9+g z=f<^}Zko2hP1lyVnc59*w)TKqSKI0~(6+ld+Ag=Tw%cu@z2kZ(!+e3dtUp@tA|gTA zxJ7F%BZ4#W9{Y};e?+oI7QSWg^G;N%BZx%clJLIo{6!_@{6(d8o(Ht{zI5u+|1gjw>ba1yg+Nw|gyw}d^hnSlm0vVhj7^AW%~mC@;RfUVMO* ziHV^b6r%N^!w|cbp{$W2+45B0P+FzAN zkhf_>e;=!U9&&qO ztJ@ppZeKj>o`P51Q}Mdn4{y2s@t!*XU%93D-W`lz-62eOhq5epIBVdJU^(tc*2F!X z4RA-Zf$mr~#2v@RyA#-CcOpCAoy2CjXR-_2f5~jFJB3~7P7Aa}51}>hDNj}-%n0PU z1@-}%LqL=cN>^r$&6n9mlKGdsFQMue}z*_ny%Uvf`jpF7RYW=zM?G#gsIVIKuB?xEFX^8x_$_%uT)$6M#uBmJ9t)H_TDM9c zl_ok?P1tz0o%H9@i|n$>9)YnS97>bKf6z_HCk&kq?w*GR?hNF+GjY5-3+?Im1a~&N zyK~Uby#T}9xtQe6!+Gv}%yTcqCGG-T<6eYi?!{Q|UV=y5OYyjS8MeEN@Pd0e-f^$M zr|y;b*1ZZpxr_0)do>QbOPJwa!??SYWw_U}`tCB8>t4qS-0Rs1?hWi@cLnR|f8NMW zac^R!?k$Abx3b~xZEUQ2J3G_8gPr5v891K5fE##k`ybj=3=AN?ppx#b$3spBr={49 zS#XLIn=Z4CFMudWx}io@>zGqdkkasq$9h5Nz9QPf{A(fOL7Pnmf>JJjAoYh>JcoFcUNZ$4X#te z2Z2{ZAL#6SHd8H`4%B3kbZ5PIyE!6h~IVbi=&m|rB_MM@t1Nlnt1Nk8Sj}k7C9I0bSdv^5 zRuEeqY2;_e^Pv)#6a(A&j_jz`b`vU9Y zzDSGhB{qo4Biuc-jl9Yxy06hz@;aO5zQN9Q-(+*#x7bDQ+w5}pe;u~ieV1M5zQ=BJ z-)F1c4_KM|agY!FUEIX`%Hui@;T(R7SS;9#{k*YsH`zi<=~U^1v;+I3x=?H;>w`tQ zp7&FWbUiL*Y=3e# zq)xaCPC3RhES~uZ!V6*Y@!{U{wqBF&L$}&-ze7RL{k3MR&8g&cuRL;#;edEOQ1xfdUR)X%jM_{{0QO6}= zE)8gr#&CR^e};XpN^qv z85o|Hg;8nQ0nYS9CLhL!OPm=U;LK=AEW`i|;UlC%vk|(mrhKGQP^Ket6a~z(W)dc} zJ}8yN3#^OO$%~3xs7rALILK!7b3r6WQ*zp*|CJ&>R`M>n#Gj9|zfQ?0 zDc+~m<~`%%>6zzx9El!57s+4LE%Ez50o+&=e`%`FXpdT0!-Q!>r#@Nw1s@uP+yDs4b_L9+0EIaO%t?Ek0q@Kauubug_hQikf%M`rgcDvv=eY*S`qrC zorFPYod}su3@i)HkP)Pg9wwPLAyy9IXUJ*vBGrr|a0T7SC2UFbBuea~;-{H4D7+428~8;jQ-;dQXyW^wW}B2bpaxt(4GaC|vr@ zNE;SL?PL;CMT`afr>lflr~B{=s)tVde|XwN#OOCIEqv@&dH_8=Aob~dqI3dEXHNz7 zj92w!h5Aw8of%)Xswq^>RH|kQGSjAos>%wg$_lE=;*(@mI^M#$s;bkJLt`3IfQ7!y zV=YRmRHWBI+H_cH=hAqEJsH!yDtylZmxijlAGnND6lIa3z9OXM*5y^SKWXy`f98du z@lOkyou*=g)AfeBZ(R$H)_uyla`cnd;;KaI;_%VB3Q|Voj>w(r`!1rL5-e;q06tJFfDiMt zYXZ2wRsc8gb7}&(u~q=L@aZ)He6%(IwK@FUngBjts}^qK=hX!8$yx#2!DrM2@Tpn> z+{MqY3E;D}0cf`6Giw6)e63peGM`lwz}>Y1_!^&G6Tnw$1@J9CrzU`Jf7A-#d;Ef$ z0KQWzfFJR>H39sfRscWa^J)V4Nv!~W#pl-q@QYdj{1?BlCV=163g8cXK}`U^s};bX z`9(DW+*d1rzw?W00{CmK0RD$xQWL;GYXxvWzqBTRf7c4&VZN{?fCp;>&<4M(CV*8n zI{!O>I$u-+KwGO7K$~A)e-l8nRsduCikblOS^-SwSJnj3trfsJ{HmG&X4VQ|eZIIR zfOTsHFo$1V6Tk+w0@#!}?S^;dquc--O^I8FH!uLhnp;iDp@#|{>SX3*3UHA<(0W7W+z!JW^CV<^)e*@5H!dKJ;uxG7W zcnZIqCfalc;;2eHe zO#o-r0-(n4=JzNae^m)Q52u)atW&vhWfhR|S3(-ISfDYB@q5JqBk7#q$5$(8y#h5{ zez`q2$R_nzD!}OKunJBlF%o~$`2Bp11h|$zpa70g0PE)G$%L}y_JzUO+L)JA?n=0P zSqN@jnX^^=smmW^T&bH4Xc;A<5mxgdlv8FC8X_aaTg+`!c_ADEzQ4B4U= zi-l{p)wB#Ie?hOXmp)o8Tu(im?aX$(NzSGk9+7orI}z;{_?a*@x2H~ZZ3<$L^eNte8*OCS%ZF9iH|Bfk+w5C_ZT9WqGf$)e z$D9GnJRgoZ3mN8Y6qs{RY|ai1?#X@)(`^M5$a0UL@u91krjf3kISin3^=B!S6JMpRg4`_i>RzdxWX zYU&O%Q5m&L5-eBDO1jO?LP^vv$zf;HuS36Av87R7QlBlS-zaRYknYDya08>in+ z^y|{Ect4GP=h1IE{cfk<4Ep^>-xX6 zF8XU6HEyH+)He;QWwfQ~m?O@6aWYa2mk=3 z{#chm3@96u(DO)t{tO^MEJ#H{5e!AyonV5iB$m>&&jk|_0}sGMAxJ zck+FFzP$rDLF8ivmJ2(;DjbIXgwJ_W@up1f@`)%q2IoRHvb$uk2E)vQN8~U*I*t!q z_zb6os*_TwvJ%Un9UfI%^DZ%F*M{CuIx;j9?Nq9U!M;*|4}zhW%0}GI>Rhz1g1wmMfYdQ?3mc@tq5zxdQS{I!zG&VtFMt)g&lK3N)`&DT zcH3W2O9u!VWTfBl4*>uGz5xILP)h>@6aWYa2mk=3{#cd?8XK4Jh52cp4*V8SAcg)hmMWXLRGmH;Y>yLDeH;DWd`6|DlcA;43q zt@dfPZmsXN+SjVpYTwgmtF})?={a}4$;JfoFh9Pz=lt)v_dEC8bIv{YW|7|vHWpRYuD9xu>a)E5dy2zqVI=dZWIggLo+6(=#p zg!%CaN^}e)3|3OHsLx+CFB~>ESNI~)bPPaBItC$2$6y257($qTaT0G@VumB4=pbuT z3->T<1|nrXzs1X#oLkXlt@5Z9Hf#M>Nkx}7qDV>J@(c{aa2*~4BQTOMu%`gQ<&&|| z5Wi+9+oP+y+{Q~XFdAcYj5UyhafGa6McAlKW@NFoDXR8qm5eyWzywSrXh8*MC+6<9 zyKKP8JvO~28ukT$8(qjFkT)HZP+*`CUP5{#YKEhc6~1T_VMzCt$A^^Utx87`PStUm zfzvUSQ1Tr%mS8l9kvrL`JspL~Kq$zK-_NTkDh~u=QL(-lw9`4#aVDneILp9v%per~ zKgY8n&H8%6!rWucc5=#;7?_D!98o;)JiniCc5brWKAFjXCt;3(xtPa(;0xAUo2nXk z-*e0J%B8E+zyd5Jq(x%25w%<5Y31d4$L^wtpDp6&$S(IKvBcsF%?Rra^W2tzcuc$%Q8AN-*oy zWM(MR;(}i$i<3do&~%|i>C|e4BkZa!gk_>mE-FH;F0`sSnk+vL+$3|v$qS{MUvV)< zuh-k)+vN2HT0$I9-cZoL*@a7ZN4(xhv#-S)VVDhnoe*$gD`AY?7q$YSR_TldV-c&q zOXhOI=uVjie`uo!`9u5@-*6VWlHjpLWFzWBb>4U|MpeXLEgJGdu2rk74{!E{W6WO1 zJIAA}sMp%$i+X(x-nu5K=Q;T8k8;x^@= zOJrAn33GE-eYf;4)81j=PW*rqhVqEc*qwCTgi$R^sIj6Yazt!1upK*iHyf?!a?>AU z%_@>4xMZQ4Bn>8um_fM5z`eMSFrY;al*H@#Ov>$#mPuZ-TxPxBz%D$%YgeZ>1iC0yqz!daR1CNRG zXcCeFJZ|6#;7aEQfir+NbX@zbJV+XSm~UJkz#)24E>9N z5AhLuNPI0STO6au>SWbPQse%&825h&`F$ogg)D6fhc>EGHg9)-0(gU%GT5pYP+6+j zW75gNr6r|ssGK;56DFK6)w98W+1BJVDAl%gMb+X?TeBU>Wl$QWOV$z-Ee*vl5cwxE zRM%~U?;>OvG(f61E#kY-5+KtcHx1$%u2J;tRrc9F#qmOe4az3INVyWnO&@_;F2ch& zvS_#}G?ShxG=fIzG|HgSG$yH6^e_$YTAe?{sko}e;(DyA$BIJ%R;b&5<*PS(;{D`k zqa1_A$?Zz_MHU8HqMKcG3h(kYDi&?vjxAFAxt~ z*z0^bP70(sbhbg~P&qpjdwQi6iJ0P9Juk+#bQTy$>Yza5?L$V8l8=7&PjNHlD= zR9exdP<_Nj_p;KlATNSVW!2|M>MYJ77wzPOF3h%n@r4vwfgEs zI22a0y9gdC&Z!H<{PjvgYOfrz0C#7m(u3*r5IwBZ52aveV;AjO7T@zi&6d5o`1QEV ztmBy2OxV`lZnj`t?{j#KT;)xu2!)zsEyoYg9s?ysCJ{fuCDERu)b#snxDMc5H_C%S zD_rV-HzN^=A>GdDIz7s_;eLLe*6GKDiTzrkPEQcV9AA_}IxdV;`7-AKkb)6%D`jeg z+?Z55TeU8JoJi$MT0K)me2XPP^7n)GGT|VLe^V7WjvDyahzn37HOX-D(R!BR!Kwd% zhIBytvIBa>Aq-f#ADNZ;g&lAg9>7pe5#9rT7)98wy~)IQ!eLA*);wBPZaeaKAiJO) zQ#@Kb&hWOQctxAG$D_GtwPW@)eTaTHMtZcAA$mLJ7dt)L5M5i>j3j8n^E=KZ@Irn4j!u`>=!g_Z+)F{e<~)$78SUVtz~d)eqNj`?Jg^Ug4=}XI9?R zzvccLD@R^?HS>F_HXPw)(Cbw(P3E0{Ue)^k9nAZy7wntL?M>Am*Z!T`H7l+|2KNtb zy!DFd+&WwL@3@}Z`?jQiSiw6uTeZS=IJZumkCdwI)(o$ z^D7kpr&Qs8&-_Zozp4xWBJ-;if7m7bLFO+|{2sR&@CXBI6|mE-Mr>oiRKQJtZZ+f@ z2I>^B#hoTVD+87S{O)uCECw1Cu-cs=z)}W$3RvVe1Sn;oSphTL0|Ypofq(*X-2(*} z$3RE{!`ztyxEa`>fK<1e0UTu@A^^T{4-()L24V{MvpY+GKQORS0dKem3-A&Hn-%b^ zJ6nK53|ypuz3w3b>}Ft#0`7Nz4;5fL1D7h`X7?}wu4Ujd1zh4DF2F_xu24Y0?Gd1X zfm{WwagPvS83R`-;9U1e0TwWDjRK0@qXd}B!1oo9=N>JuV-7YXSe+vw4WEdJ#Tb--tP8rEP(wU|Ab0^8`_J8i-6>2JB{u&&l$mfJu0Qqm>L7Y5>V+zJ` zhArf*JPoH|0Zzx+OfA7#Scd6Xix~)@1RF3DQOwcKzyhrp3$|FIi>L&Jo~RDbTi{sw%FTU>)OGhJPOnMW}YTK8$?guFzZ zlb5o2cHx2eS%{syV2#^}1#$qgBfq_8u{cfEpPf&^u`;E9pk}33Skr+Iy!-LjXH;yY z@dWSM{*$E!f1eRa@7MEtZcuw59+kmo_g1=0EQE>5W>Az^o)jgPcPKj=HX=5IvKb%c zi*&qeD}6b?VLNu%O5e(J%B+(MpNF$>Pk}nFcVLhxo_`P@$DO?(ad>yR`%vC;8Lr_r ze!(5c#GR~v@?A>hocNSVJt#_4CPnpDbh1*El%g6OsY7iwuV6Lr#eKG#ce5jid8X{c zCmeQvKY;%uR2IgI3ydgC=(jlE+kvkS;HU#_N8j7qscqc2yI8Xa*q{$`Xg<`bX%5oq zO4dqrof_IF4WdJHuCh7;7smJNFH~jVnxAW>$JdlRrWo~zmFjok8|>Q0%h2X<=BTtJc(BPR9WN!{B<0_ z4(`o=q3fASVVO+2fhn=$rF5g(Sr<0aO&rQyR*EAR+54G}lq1OHs2N>k8|h}ovN6Oq z(hWRYszgqH&;7i$Yd^j1G2b0j6>z26hB3v?Lo`oGlrl_Z2Wb)D zaqg$eXWEYX_wvHlVGFj>9NU<)+1-JsdH0^-{C*e{@GNKgpK~_<1xoQ8s_{!6@dD@d z7kS*v*ot4_a=gTD{0h7DtIE)`dFdNq&@C!^Ue0%@t#m6>>rg_s(d}%S5x7hlS{!qK zCz~~_9CIh#(P{Fb%6Fvi!p^>g@r$4zfv$4oC{BIZ!;iApJ5?e}BuBavq|=-)G2*Mv zoOT!2_uD!z2Md&wa-6seJ7~$1_{3c-EwQDRcF?MJx}crRCuycc8g+}bQ`i$Gz>P^< zWlZGMym$2|M5SKY(0T6#cfB@kP zkn9G-86iOg!W9SsIVDqP2ot3FIKj$E!Nr=Bcv@1gaWZe1V?en78B`&VX~^M zEfxrtt@1^8to60JVZxDtOehms3fagZcn2b`DXiwN#3}Cjpf9wuCJ^#R2{|>X6PS9X z8^e%iVz|Nx3)Z zDMl%bMgd`Xt1l7@_<}1m(|QSWS2O^rBpDN9d4EcbC6S2lqMAT7=EfLHRpS*VU?M@a zw8eb&LBB0~ZWq~Qy=G{#!W0w{Tme}{plTVNrZ8QP27S@k8W|~3@W@C`$iF)%^w#@B zje$_JiBiHSO_ar3B+rwXa)r||pJ#;HMn9|47Yj4Jyk7J)p$dgcEF@&eY;{N*#gv+XY)CKCdlxC3 ziN%C$xeb3P7G32JwhHg10}`m}wwkSMDXL5?Q&^4_gh_**&6P*_)g0h?oGNu7|`s&F>8F_+Ea6w~@z*L%psIfU{7 z0irc(TSC#&g!6xH^)c&>Zq(ru3g_W`R#A&T(#$3sDIV}H`X^Rda;h6ecAr8$8d!FH zU-SEreMs7H^X2*#n$S#0kH#1lHidT*HViD3wvr-V=?@JeMRFxG9l!-9b}9tL3a1Wf z7c<3oTODfj?-5Qz3Sr?ijlGd)-MA1D6H$d2+6WT{fiHi-&0sj{ZzQbisxNjIabkw5 z%DzVj!dOk2h1-oiCN5IA7@zDOfBFGZzBykbb5c}1DDjdlBIu_SF2$$WxdPGUEv>PO zv~$*pZ(pu(g{V}qs>DjF!fm0LOzc&-5?3+w@I(^i#UlqhQO>(Y;aXhB&e_(=AhJBv z*cuLmnC*Yj#U*`fNr2&cg&TyGbY8keKtL#cUf~OxQZT$bMJa=Q>5B?q!c7b-wTvY5 z|1a+FWrbVB9YhD~BFp?uzP4aYOlF_Le%!`72t;jI?13mYREsL>@KuEaxSb7UwE_bR>qh^D zZT?V$zp{UxB((iV;m4A$q(!uc2=V6?ekzJ_Mgtf7Gw^e~VB!}FFXAQ6xCYJstfVR? zfiQFV+Vz{Zoxfp8?aJjGc!vNZNAZdTwO4<|NUQt7>(>-s#~U1h+CpqF1pnX=GC0Nz ziML-Xyot9Md7=Vf(~GNz6l2oyTh8nzy77CwZQ>6Kf5e{%<9i=i?~D5F8;Z(>_**&4 zm2-OAKy@E>A=dC`v4+18X7uIa{n~*W@8UfZe^dB7{?R9VYu3c7*7{-%JLGQNyLW#C zVt$<+O778@0()-K=X{{>Py9>LV?7vM5)4YFG`4^9trHOax57sf z@v*`Q)-R^_e}mO6$y_|D%ZG65MRMqp;x6D;Wi4it8qgZL(Gq(kLy?1=l0R7ShwrX* zAd~D)L899+N>gMKmtbanB0ZloU}=BTjYd)G9y7_UD3cV!U5jt0f6bS5g?k6uV4X_QH$ z6&28!ZdtSDC!NjFE=9UWt!jIm35=sclg2BWATcGc_Y%H<9X<{ddt$P7lA?dfG=*_6 z!~;FSH~~mBm8O|AT~RTW5Q_U3cv&FIWoyj8EZoAmy_F+KHK5I9AWe1OD9H(V6_wHq zzT^g9C>#njNYq?A(0%lvF_aW2lgbp$B(4ER`}ef^B;#DlW%o|8x()tj_7moO*?++Z zbQpR;#G0#Uo)koB?EAi8lyiSLI$hCxT43eDI^h;=RSz-g=JcG?ZmLwYP~ur8(^wyl z2E@mO_98`RO29RRHeWHr6D?7+l&W~lh9`;lRRTeov|P~&T4^n}%Jm*GDv84n;!_f+ zE|;ADAve{~T9axOt)umc=&#o^Ebic7(jwO2Dz2_BspiavHYnOin;3r}wI%9=MbeIu zeMW(5;dzUqt%BQWA%C+k=9i&uit6Yb$(E&ds16MXuuQ~DT-QBD=Mm=hyRNzqVUx&1 z+sS8Ay}}6MG_-WkEMbGcHOQgEN=TQn@O{OrlcB;a_^HXHX3mU=)mnCv;7M5CX90cE z7|d*3plBztOS&|g6TE*z$8rsaAw^+oWus5L2p|;nQ?bdLfZY^P6eU(T zT+rXXAt1mQ+NI#4-Ko=i15I%qF4n}2 zt*BiHE1U3=rkK17J%;Dy>y>o+78l%QWsK7FZAJG<1?kprexh3gu^s7jNTxj!jvi3- zpu|5XXSpFa9j3TRM-+8XXa70!S{A~>4tCn?L?xGaZ@GVRh!E406l<+3AucJVxam=P z%%sN!?!HTyb<#u#tNJc1PKBN6>6n{NPjVifkxoyu2G5O&f}a(${Ju^T`UxsSFIp=_ z;^#zMu6DQe!TNtowVrfjr_)au&!TOuVmbz%;5(j@Xe=_ghOSZ84RD~P;^5YK+SliZ8 z?~iN}r2(atS7(Do=er94>GLpYD5f*Y`eyZ=JnO~0G$l4h5 zHSFYTwYeJ6;*0ED5ss|y`oj@n^U&WD5B-Jm7O{Uk0VIahA@AZmQon;Tf?@vJ5g?Qg za=ZP!Yn3w3w>$OPjPpnsfm&{GUMHC`G3^ZNWs$DCTrp0aYvtl}mE?+8p>1UM%e|;I zalz$-G0~q@uQ>&Q`lcmPrb%WTvft;4%A101QO+CFHL~6 zZ0Dx%YQ~fLWHoBPGRO#|-cI;Vmhi%pt2TecAnQ&p0u0?gh4@WFv4?)ld#JLXg3PX9 zSSNd~9Rd9e4a(-p^Ee<14AVtMci-s~>U#X#-bX^g-g|NikRR%;ABa_?8v|*(BLVG4 zi+e@hlh$z4;KH0V)joyK(m^KiLZs(Q%YkE5HA6;B+d<5#0*K**xc0m26z$>v*@}P0 z1br>_jlQB@##F@UR5h?7dg-^XpZHlD@{)XGxGloC7?8l1lS)`llMRI2rQvWa8jJW^ z*ZN~S!i~{fY|BYAO5MmbW+149U9Th!uWxgGHO_hF$GsoOKWFl(@|>%m!@ydg7n#CK+SFnF{zzRNd9!pt{k*tGBJUWfF;AK6`#2MUL#4A=}4OU@2 zOSc)T@d>PS z$>wr;gD*qaT#f^{iIwpyZaZPj%Vq|DO>f$4Ud#Jj{PcP|(AWvi+7^{|phdUJoy^9_ zLS`f1$zL64?ZhqvDcxGkoDDa&As=0AbIZx|kI~yqnc07Gyv==+kJ-Xnxnw`?bu4057Q}Ix=4n#C^B^|Mh;-ukj9ygK zZQ$yoSQE!*dH?6E)}5HreaAkyoQpi$xbNJ~mezja9??2~rn0aP;Koke?2wD8a24)D zrpFt{t@|;$z!k?=SSNSH@%5wlS{&cphb*B|EL4hFjo;pf$xMHxcz@UNE+*SfEsjM@ z?SXQqquiD6VqxeUZ$8sKQl6Hd_ASxaZhcumhWrU8f86{zpFdgrS*EX>Et3zKojWm( zN4B=p2;IrBI%dAhxm`{;h~ZvoGwEFS9Z1{jqOQJ^}#$tTOScyZ%W;~!huZb-s1MTz&`Xl>R9#i_0_N^-!>jVZ|c#l@oJM?Fy z;Y~W9{=%~^yi7ajuX@KDR8Q~f9qfAv|K-+u7Ups5Z+MA)^zS;N*dz208(wB%^l{`7 zRphB-m|~)C#D~ZjmGv>)M9g{xDK%!)xZHw?BZ_}M!i*2$8es>O_Zf{opnuw!w2~PS zSj`dZ$@>lC;P2viXKM;~+43EOsD(}BOT zi@d)d* zq~m{4zL>{Qi0^XXJjy}j2@X5oV;Fsk4e)7%@JyG$6Sjp)M>Bn>FJU@X(Z5+dgLlrQ zkN7$q$?MP~d>syb9l3BEL!pTWkE4)GTsV$Z@gKeiOUq@VAbz@-*)t9$b(^LKf2dhd z)&iRaqT}?jh0m@qGdGO4+}uH~wVn>jsO5j~UE%aPER``*FR>#AWf`b&6}UQS82c&Z z6DraQ(mH9ZK+NgVnn>ch6{e@a?4Y7JdE#_hMS4N{12j8MbM_&v{lrfToCWDR__UvR zy1>~NEhD`fE#{%XOk#hW$~nAMq$h@LC~!9FiI$2C8=flMd<<1^=*!H{Jb>w16gqz+ zKa*E7IxDuBd=;m~``!Wl=)R4BhNd2YGNR|hc4ldt5cWpP@ykGCn_ z_NRO`qadUAMw_#Pn31no5!DAVUPxpWNMM>LZon)X~4v5p{0zTtxP4N!ordGFO zlqN8$z+I5z$j^<_xm!i^TJF;P%zUQI&fG!g$Ek5^`|Mj6Cdl4~r*T*F{|$uZVpy5W#9+{Y-=WH_11v-C|2!raQh^YobE zVxV{96?(`><8_(rbS*lNC=Oxo7$&!}=qdcbNat2I2e^M~UL>8nIT=!JHZr8hfYZ3q za2ryhWEj_TXv<-k&Ni+#m+f`|F@N+4PNq#0nI?9d7NrqS~=`MHqc*WizbKa&{74I?AxIA$_Nb7#2g z1coE6H(|zJ9Vd-pym=3_WL?}y0< zdnjz3Ra9hKldXZm-JQbS-Mw&kcXxM5G;ybJcX!u93U_yRDBLOFa{lfydUT(=U-BWJ zGWQoN)><=WMDvlzH)@{pV(*obc1d+iian#T>7l4gD~R=e>FAQhGzA;Yh31tJscDF& z8X3OzB=|X>COy>DQhM>6Sc}5?O>71+VVdrEHQ&afD{6uvCyGe{b1M_@ZMz=kxj9Jg z^qyn_;&P&HiL&aUH>N+JJSU0r9U^0B>8O z%0-5CUz0U-=-HIo+VTvJFS|(ry|2%hxYdDj@N{QQSqI8Q=Fc-u83C^-0~3u}2^eLg!T)~b-*gre2# zk$|~D5Zih$1USk!b4sns8w$-k0EMQBJ$c3^q{b<+`DEgWBGLzgAz(ei3jGIj=nBff zC|&)`k;3ioRZIYfMixzbdU<~wILm>7$cpYJ&F5=O?xF9(a%iPbRt((m))0BlWn9cW zr*~a*Dp${E2c?AxS0mW(co{ovg-)hy_eMw)s=}zGHN4ydd80_6YWw9afWfyTRDrz1 zMOgQlBXgYRGD6m*Yv44~sUy`!!QL-S=gp93eb=?4eWjVhP23aEzOHe_2(<#1m=8h0Pe zAd^`SPR}-3(?uL7&I`WQDMN_?IN@_1Js$%x1-}+oEH0)EnOZFqGu8xuyPsB%3~^6I zz%eBhSU;jUQQxxz0R2bN@c9_=ti%QZ+2a0}qOt1?{`EiLqa@W;L8R6xf|7v$r^(Xlj7nZ_NjW&!CxEK{bb2ID%{s_&BpAE}f z)of!2wB>S2=HPiB^j(KIi04#fQVJQZV;PVvq4XP1P`Bt04&3i8UQ;00hVj5YI*T#- zPCkK0mDv^xI}kOvsXMtu(oobmYYGQwGcueVl-n@&%!!Z7|7E8`byg$x-d#>Dl11-Ta0kw9rH*<9EK>hry}duOzC*o0UGB+y z{U&4Jl({<~`1cK^7pGL=g>X(9O__2v`j+#Cs$bd@o>l~NhD49JGUQq2_ZjCu^ytaE zrs9KvfZT%ro1PC)=6|U94*C16pB8Yu94zuu(xMya%94?GL9arm!BTSz zfhh}V$wfIvuhLXuC<){es5rL`x%mb~+0>#|<~)9zNIy?6wMMKxoz2&<9M3zu-`_p1 z>>r*g1wX*AahC2RiL*mXOrUWBA;!Uil2F+}lI z;$gOxp>t7iSKD1m(xOXFC+ldTgVP+-eA(OtkqFxH$^Dgvb+9o;>^{zVq$hC6a<5Ny-((IBsPB5r zkBz&o|0U^Qrw3U<1tyqF=eFp|ed{{Vh~DC^z53DUOaVI>bnNCE2@$y40{>>+`I)q) zQal)Uei3D04`b;D7)z|!uG3rH_+u;mS>HbDVV`WqO+VlI;=xuPy3WykFg@K1Px&Z= z>Tc(^jPNJcb~SL)$-wonThsU#($d}fko?Tgli&o+K=n4w6t{u(`evz}bx4n^ zNmvssBrJ;-r)5TEkw)oJO2zVi^Vdrf|F#A39a`u9Ox2%bbeLtcS!5ySzy< zFSQ?y#EVS6xOE(xxusvYK%Ohq$+F(9>}*=K>Tl4&TGNtii1+ErQ`mNm8*(iZe$ItD zXsW0II1D;bfDQgcv7r?%B`p;CHTiwax9E#P&GL^}8Gw;5Ve#070L5!E^BTg0=l}xx z>8{>yFbYY0G)kX(<2}&@JpzMeVa^RpF%FUF`>}D;R;PAE5oolr`$s;BGL4ND1{&mdu? zjS;pjh@C&b92Zc~a`GZFZ{I*b9%25qU@&&T{eSX`BLHz{*g831_iJPo1Pz2dS+y;j zoz`jeC8I_Yx7}DnV+PvK^^@d;**})KGEVTUei?%Tb3n@46Di6Kdc{*&yq9ZxT$!pl zZP}a}X)*R7Fq`8hS+1*<^q_`b$nGW4TAd0-uU;Nw z!y@uu-`6Q5EY>=)<`u=C#WYZOWeLJ+2cdQ4|URXdW@;-{(2itqUCbLb#;4Ou_7L z9rqA9##y&S&uc9^C09rxTsQhvMOdzkt_Zmt3B$l4d5bkc8L=FVyY7g!dEvOc;PQzt z@w)sbcZD>l6n_aVU*#P3g{|ye`47=LYy5itR`TuXMw{EdQwk{`XY-_=IO`ykFF-i! zQ2SCG&@xP)>%S1cHNyU5)BIM4Njm0k0jF?@s(hcW1gW`7Y!i zX>GKq6aLh>kf(Nar880i!W}sOuNzwyPk`MAy*Hx4-uXVUZ}8RiHK8 z0og(LS1c*|lKp>T2@$|xVr%5$Qmp3Wf+mLkX@{F}Bw1XfnTHOCYMmj%DF(sbj64TE zfd9K*6N(^gRiDG?{E%ZyugMonFl1_EApvK&_eC+*fm0$gwj^&-&V0Ma@6D(8Y3p)5 z74ZH66`<3WD+X(VQ{=)G=mcEPxx9^CA~2vBhU-1kaa>;P;izYca{gJ_yRJa>r zK!>Q>xM$0*NL~_18n}Ve2k*ky;eDpGIq{jAt1ZKkA!H&l1ElCqzP^!r6(RDgJ=3^doz@t9qJ&vF#t*SbRS1w2KurNDr((ybnoc4#v>^ zg^(!pogUK49z@-nkPsP)6z7VX7U$FCIsSld1^(1NapXkqFBS1VYBV$d9oSvmUFy?y z%^R2oVQR|b_15$o8NQ86(vZ&5%Y>oA-bVhFiLXc-1|HqO*#h{IW2Rb>nM{tBF{N~G z5J5n!&LKHm8XopeENIc>;%9gI-|c2~_Nw*zSYWe)(sFlLiClRCRIg9)uERf>2L)e7 z#NjLR(0!RA|F`!qHE;tGFIDjr6f=pa6eTq-5ex>Pq<|)j{Lw<+-SS3lGexYY4s*r8+UH;UL>Gq?;cYk}6j6?Q1NSi&V)FfM>Hg_J^F$KYk7%JCn$#gIf6r}e zgbSPFrCkDsui(yq#w%s&t+-`Di&SBAqCHI_AzzVKpeDQLBIo-e&eDfcC4uPZZ+1E+ zE{Y48Nes~rWjn66sk+34(zfr<^z-T91Af#1)!y$wil$^Utnn1*nkDPz&SrZ*#(X!& z;QZ&iWp@X%pfutqGDYR_XRCh4{`ggPzdj}aRkNks5I>5$mqJ3Rt0h>CyeAcX4vz&d zGm`$iP)Xa2l=fOGCf$d?AXG}O^Rio>EZIXI^Sx?dycY^@Yy=!DjcL6OI8G zD|+g)AQ&M)W(hk9Wxs}&jPCl^I+IT7o@}Q(Pe^WY#LyrVAfR`6kaK@oOGJnXoFd)d zVBMu>+-g1-(DMm$4Nl8{B2*guCAxHAe-ER1%#da$^*4PC4&LdWb28*R_r~K>hw_yv zfpJjXOPQmfrenP)1Xt<QW= z_4Nj;!!@i9o5P=9F9ndOC6`@@L%sUylx@S3HavCkemegldt$4+Rv!`fvd`RIG?TKB zX)$u{v33LoX2ko_$9S}F!Ml^%>!=zb+d*lsyv!O;T||WJTK%yWNx|V!p~tStuE_=i zs^%o#6&+T`s2b_=~p<1$DMm+wt&L>yh{X9Hy0;ko-H#8hY|HO$Iz7w#*Tq*dT- z!}2{Im=5SklV$NYjIl?9pbyoS;yj?$G|Ca%=|hbEX7!}d#gG-DCrDEb^V&bE4)hTK zB{WdR#6X|0Z>xnUlU2}J+mlqkpBj7Rr{V%oywah=j=Bc;Q%%{9^%u9vRLWj^B6Ub$ zZF6;8F{G+5*>lo3X^6EpBa?OCl;X*#o396*RpYIe$l()h^>+&?HX9iodkeW3{b zmGl2MiXdN4>Q4e%tp60CUf-~wri$;gAOeb0bu`d4(1$<6bco=~47+u1q)}(6!bO6K zl+i88z#^rq-H4Ky+*w2^(UP~Wnho-D3-x4@t!=LKD*sp$`dSjMg+1o`{r5-R=J5eG zX}S@_sP^#%_gmI`X7^K;PEUu+K4w6$=Xe}y@~dJ=9zF&u4>i%}&)AWAkZK-;zsGcI2)ulZv}Ms+b-!c8Sg zj<>_CE_hHMkhNNt>)yb&zl0aJaYGHx}rr17sM0;E!+JPE7B3nj`%)r}kE zFWuWl@8Kh$Hx5PK(r~WDxy-yk`Xub0|9*agV(TeNMUSEXc=rI<=X&|_aSbW5S5V3= zj;=Iezozhv^4yuLy1V`8Ab}?k^9}Hw2c4fyWHW33Gkf?Pj|AH`c(LoCRf|!2t#Mt> zmn3$$YCIpP<9Rk&iq`bS~_})IsxU{UwhzWt; zLbdd32;D*@fx&@L{fWhZg|U^SJw+2u9WGNOPO~z6z}%dC@9$dYu>dP;_gdjn;@;%v zA{)!R!s-zFpRQtPp}W-9KY!&}bPOG=QCmBVo^zght^55!^`}Ld7N*WDhL>3-G0RN; zqby!@k=P(1&C~1R8#NZcfCQ(d5L(mAW}J~@pOljj9aa+@dbFD`c;Xzb#b7^iSs>ORB&0`3&Tgl^M-9PI2m+@X3R0> za%s7oJ?_CXuE9IF!3mAHOJNoe@ZJ`R=qrih|HI;b_+*%pD^`IyXxb4ocb~5(K#q(Y z@7tW?F96P5a`8{)ENcB=St6vcut59 zcCR8pbZ8rPA>*Phi%{Ukj{AefT#rw?ccySX=R3dXL!+m>_W~_{1;xlwi48Wl-p-;& zS^txlzS?nD#n7FnaMzxghG8)8H5Ay#38=3WSnwsc&6j=Y$L{+@LSx>9RwFN+D#%-? zA)wd$R|YxV1>(9M$XkqIug|Z3)TKSdbsdnmU_*fSFKx1_NrEM9khds9fX}ZrvZ_r2 z_dJkCq`r@bfMyxZL4qYEkWSIMPd-Bwm~-baH-?aU<>W>dB+b;2hqi$ZIK!XdmA3&8 zk}E{|DRrc%M@a0G`|#Ucn77u7qtL6{+bWR<%OS&d6qwibLYdK-Y zmv4aa$JZ32Jl?os@PX1>@oVpFg3Yp3a z$%>lV5($O^5XHMeHEQf^P_L6j{l*N&U>}YO9i&a*K5b7iF;&Id7^vB-Zr8nCJFDN- zlo#4k&n`2ww05TO9{0-E^vG+$kvW+Zs@IRidib0{vf9e6wtFtMG?q}Y zI|GK?ojb5JsvgTez7L=ebXc7Y%l08poNs{-bQBfT!Sm+iKRolZturE79$F6I0>uZ$ zu%Lw{oKY6jjsd1gY*(E*sH=%uU4< zBL*r>{sF4QBc82l?E1g8zUIVZvX@8^<7m7Mg)H~N>eR~(RO1T{xdYE_9E>cNHbY(3 z2gtFgJ%Ko>@WpMsT(2|Jzx>*VE`pLsXpvMwwd#FV1V=a(F?!bn*0~|gxSDuLOx7p? zi7h`LG&YgljU)#M@CVwGjAZ2K-`)^;y#*NY$0#YwhM6$sbrYi0zSS5hyld6W$rTTJ zIFS_%+7Ki(mJSkE?nJs$n~JvKNglm#8PMF71db=gd9dxHFa_nXF@P%7-X({`+hdQJQ;2%S~?)Ya8!ydc>0 zv{pce)vt2n9WK^jPfMh;_=roLpx;B+$ zC_GbnjKVh>ad0XN^lBTf0DoaG-m?`!MTovUbil-jc87*z`1A0`!+WY*gUUUBPZ4)sLv7 zt1&3f5r=2qD{_Sb!1c%xWy7h~+;sll%7pH;E=eZdL@JOOh*PDV6f1UI?_wILCwn8s z0I#Krh|zCTCc7FFGg8(fEcIPD{KMy@H&y+SSQ_I7Igt&wuX`nqRCUF?oKtx5f?AA_ zECbXRC>U=lx)Q=l<`%G|VGJyp1O%QxBw)QgG&YJsO$^|-R6QF9KQb_G~$ z@5S}73)114e(jG_dFhTb*Cvz4}^5Or~Dzz(Uup_cjzH znyPcZ6thY@bxdPe4dlQU`M+8ZX<(w$B82ch;YVc4jdz}PMJFa=rsj2{g!qj8Il>xM zM&IR|!jl#N6Z;W&o)iHqwshKS%yGw4`^6x*ke%b}`fu1zFTtFhQJ%+mZGL?5~Nz)zf;~ozgXrJ<&gP!n;Bpoe8w52cwUeL7;^U+$2_J}*u7ecMu}n^h76<} z3})yUrzd8k7n~`BunyKe&mmmv;KHpPKSgT~3ShfjY=9@%FNsitR*kMXLy{=HIrI|d zNf{UFBW((bBK3xyclS>?{-M3)k5|tfO-sRmvcJfA`m8-L9Q(Nk!`qi%CQ8 zC@%~s5D*`%Z2Cq`-mT?erz|QIMZ|p zt&Zj3uS}&Dj~7>er@49Wj<4c!K}xRI#PF06=eV0lMNd{S4MMPBo4-!tt;G(|>70SE z6FMT+w!VwPDNLT&O)Jm?@KL4UJ?qw7dXooI9PZ)+hxnCmn&2m5EFlzJEUOXy6D@$1Xed|8loRXznv zV8+Q^8OB!VB;Wc55x=7e=nwB|p(d!7NRRydnK)}pwWoOtfJzf|I`ASJo=Oj<3`t1Q z-rK=3dhS>3x!_9MVI$}E6s9s*g(m1Ck=2Kkpvrw*v0azx z%)9zqiooeLNtQ{__S?o@5Tw;9%nsATsrKa-b>O1$W6r_#vQKMWY`B)c%?5tlPsUkG zlT#;3sxWHV;V+7%;J{@gfvhq0@I5gfS8h)0Js#z$pDZJj+YYuL^nk=EHJ>bvZ-}w7 zROIhA>^>@=uMMHuAht%j1@BZ#NNL)LtK$z){k21io7{4vw`$=HT$}9Aov3B z|LhS_2x0$AVi3G%K}faSWx)UxD~-$d3!(F4MTTmFD&2$vL*agbr!ZWjh#K397zPqM zX&DYC&N0s${{;rqv}n(djB7qJda?EJmiBihxV{iMY5V4DMo1WufDo?&I0#<->gb{YQ=V@@#JJ(={fi!J@-22 z$(`VZeNfTyR^FgCU$>YPH5)n8s$5r{{>vCSJkw#~uIHg4jYqxy`SNu~{qt&Y zdlk26iS(G_seIA07pN5g04>>yJTlYl^(K?)?a=9OygaIHYKl}GQF(f?hAeNk2(n&q zb@C&!PW8-`wDPV%nlP2LuNh>twQC(EcEEeS{yfT}6C5k=zK$#`3l3W%B+C+ONmjKY zV@|g*%og^`JfhjejG_adx}N`(?j%n(XIg!AvUB`;LI1oQSoHsC`Ik2est-`p`MSkM z|0wA+TQ}3mBT0?KVz7pj7VCo}+C^2B%%@nS2z{kq$w_zVxyx>BY}^L;@_{=dp}~s4$H~Z(tGH)1n1R>jJDl3Y z>dd~we`cnaT1{1BGd(j&P0| z+zP($Z=fJ3O%Z5MI5jE-1F?hWG}6Wdj!%zL&r=k{&@wOv*_w#@kQuaM?UkrGX9p&z%H2m7`vA5yCq=eAcUe zhd35LaTk)=?M&*mwU{?7UL(b58K;mlY6^pXEK#4T3wy}rFiO&_d;i|?DOF}=W5oXG zXna-m7#-%+W!N&{gaH(IHQJv0FBU6O|75tfr(aZ|)yc0PL>_@Ol~4!miCsgGnqr%C691`JS#TI*bz-oRt7hkDK#3c;gF!&i+>r6}yRh zokQ2@#H&1F{%dB($zaReZ!$UeTODwsXlGmbTa9dY^oF+?8GnGr=o#^Wpc~d1)L`7q zeGVPahK)14TlCgp*exXFlZn_s4Xx@Er@n|} zkZri)Ug9fM$K!1K9TEiurzb`bHEw}3(U2SpOE|RD1UUI5>TSH|EkZ$Oe9LSd+*dy^ z>yh52_+h80CJ;cYSV25bb@(B#>L2DqS4Fp~+~+NkgjQmiD|sgWN~359XKoYh{2{5O zs#_bf9T`zfJqH*7B(3z148}1s7C^@fO0=Tiu#t+e<`ZS4v~KdFlNC}T6T^7FBBG-i z(%FRmd}NZm!w!O4p7tkCsz39?N;6Oq|1HP|L%y|371n*tqmzci~lE942`o=is@`U zM%|-oKM(+cwO|p*F+ht#iVx!bY@^}itXL=Se<=+?Z0x2b9Q}O_MYt>8Z74}$AC6c?r~@R1B4#)wMDI{bsevH z4iZD?GwA#nVXbsVmV>)$m1*dgy4dh|Bs#1gp5kG#?vQRp$>V}37^yie4Mjl2LDi=7 zlOI{cs`dEmh9}Y`x5^RWi>~=yR;qk^?)f@mak2XM%s&8VM}Nd)_%0nQjnJ2HN)qK( zT`<2KlmNMJV$Kq%xUAD6$cPd>a+BPIP9yA*?PRr=z?;XOK z%kaz|1w5j*qt_f+%@P~ru@-Ipy{$&5KO)T+j{y1u#j51#G!Z$~Eh`MP^UY&3`uCn@ zHW3bQu>-4-hk`p60J~sYT7nHqB<)$NHjMp1YS^djL|%p;qw@_)Pv3dm6n^UgM?anV z@)%cIcE{)$Tcn@LTg)je$K7XXrPEKb7|{ZlNSIQPhqKMssJ{0x-7u)I{h;z+``*+V z{vD9Nk01uGL8da{N8!&{LqhbD6k@Cz)i^7f#Sez|_ZQ(`vdsq*Ol53x{6g9v-xwi& z$`hkga|o9qR}`9jgF4xM4@-=+F~(4)M$J1E(qA?kGi@Rx?_uh0D})Q^g#4Ldo|J6z^%#vL&F!%VbiCh0X2d=%oHT;5oE&+hdZI%|l3 z)u4XC%G4av|BH4gXse}KmANnaGyg^ZKeEApKdE@dpyH`ja-c8(1C@Cpw4g+Jy)+QEN1>|kBXJcsa&L{FV`5{@}tjfLka z9I~{ou3x-AdUR&Ky_}zWfW%$10YQRrMR>H4%}3NXy(^A7*fVpx^)nqN`85{g1j!dw z4^IMWGu(0-<)#|}k8}*tW3w65eEN@axvU|_^uxEg%&z2-)?s!$&8n+dDJtXWElHV5 zKadxZ=baFE26S18OL3&dU$PH1o@9;!g^|TgYMpTU<120m!!a6;W#brWIf1__Md0l$ zmyws92ngV#CYd_nE8C5ionj+2lxyg!XvYI$dFN0oY^x91;h9}?m#zBl+P)@9kHYOnVnaG< zv;gx@9IV@xO|?HtXezPB+B+BM{)mn5R)#$gFwh*u%B92>=&=8hQ_*zxGlze9OBt?n zJ*2~#$AR1g+>R|ob8P2{n;EXd-=ID{ZR$A}bqipj(;Zwt-9LyHl;>fs6D4(d`$d$^ z+rl!`8(ET1oZ-E?O`+n`c7_#Th~JRO+V3NQGJ8 z#hpQ<60@xxz)i37k)Q)irP=eZwt=K$Oe-(C{a34VBLZ8hn1V5zdwg@B`qf zrUgku0N7ovgSwJzTBhzAMA^>d#yA{)n_k6us(`vqpbwHaQ8ci*O$x3W9?C14O zuVsu_ixu@PCtJ!$l;{_zGoWmTv^=*@W~{ivY~A3PzljVNMvqxyNvq{@o7Q3M?$!EO zUaL(Te3ZqMm_jiQ+W`IiEbDJv0vX7bK4e-Lhvh8_(c6a*(Y~M*PXz#nTcf;xF z;A*GPk7{`t$4CTGETg5ZnYcLy$0pk=UiNy^=gbcPN{O$jwZ>LOdW+0&o5d*NE)w0V z@;}}*m&_CHLb0z(b#%LuF?szqln!y1Ne7Md_XFTNabM_4BrI5^3desZ;uBzI^96s@ z(%vA$0VCBKqEGaj3&B@Q1skDyNWSh6nlZu7h49=!=&9st zb0p0nvRqTib@T7b>GIhc#~;n?az@=L z{y)(2dW?7GeL;KuFF`kn=|Szks_mIj!I)EbKtZ9CdSAc#T&!IxW5QuT{1sXVvJ;LZ z3JM5kBQ}QRn_0llO>wuxYT<3>B~N_b>%EqE&Wp&~?IYZYW7%4*2`EP1H9EGr;(prj zIi9NB4if~GB}EekvBM356oF=Swae@@U*pJD_V#nh#3B>*+;XOC|22~$d9ZP~MifrCUav(E`~#JP#i%nu{0eT<;Ur?HliG*|l^Psg z^cMDO`tLvbn(oXem3=;$sKzlP2z&EDP^)EbT1OHPlxi`a8;Y?>bNbR)EKW4{Yjh8# zLo1B{K}NTzfpfpLQAIi$%+)p8>7oiMO(Gf0LnaX$EV1WNCU(k!T54p8n1b@rhZm@E zxw+z-DU(6y&HO+GO4=hlj%R}m`Pn0TKH5R~?lqEG3()DZ@%6i_B83EIJD-8XED|Pl z{IF$~meY=`F*MaK+#FW;_f%s9Mr_D0O=t^Xi*1tSO)TWWCxjs8fKY8*ac70-wf&8W z%<@5;-;hm0WxG$A^le7D@a^R9nQ+o~I>|!OLLuIYwjdA5*-^u9K$K{(E$McnEopA$ zsZ*uv%_1xHPmywmnI8Ovux`;)6pml{q$1Y)m^)ME*Ka9?-W|TzAxKLY&mfz*nb(9S z@;h#X)7RG{FZf!OfX(u6_aDFHGA{A=1ZLb?dMc2wbcCZ zEYNwZ7~gJ983nZX&{8H)gmGQ@pei51j!4crk%Z{X`%g#bnfES(=T4xihDN?6^o>eEdl%%6#Sft*+T=p5 z5sB_z1`As5Cz&^lP@vIxvsm;nb^2#@j`-q0&@Kp4#!{c$MFpJ;04<&-fD~ z4M|LL$k!h^%Z;;odpGUraHQbXCo*ZiD`yxS9bgk{f2HvXIp9L&ze%kgd}Mj-eZiAIBY_djNu|`SBKT{VOXlMnRTZ-DM%38{7<(*df`ezbF+kXC$ z!Y*fmErd~5Zv)gQZLo2>uWQ>pozmMIWb^JNV#^mB73ku{lE-hx)?-`z_6&vDD{S+m zi5aPb+L+YW$mZQXZzc|okKcD)ONgMOZNQ))31iAD{gY0E?F=E(_d7>%pSY`W0Voyn zQJiBK-y3IMGcOSbx5kV^sm&~AL{ow?G=%5zllEk)92(Ge{uf`8IL=^WCRMFkCzH_9 z(#rWwB;@|S_y*O4=&*9B4zSYBktNHN#odsv8_KsDGx?`-ITFsXq@Ql$Nb;`351}D7 zWFTQ`JO_ z+Gx#a*bb1+i*5Wh2NjbLDu#fZDkZN{kqhGIMUoom<37GeKRv!x+WQ& z-DDt>C@6zEms)_7+s7-ziE^l!c835AE}gu7ejREvc=4Cmfb_H3vzvcTW>D^i$ml{@ zGn7Z9V0d|>A2QQf6>0;KQ(b2^Zw4C4sNX2ZTmTSSsm-_bdQHuDLB`ZVB%5sWTf2X7 zKwa}VlZ>s;BGe!sr<73Yc45J!l9R3Tr3@W?l9r)TFWw+Yu(azsylUw;u(}0OI6{wO zFo<-dikZ$^VddNrh;fd=VMt1R3|&1IHl#MqUQxD8#G{NQmA_i#h&e?;<`X1#j;BZ8 z7Z2#vC!8!+#`G_7WSH6yIF_tf)*;QB;m1ZC!0%Ak&vVQt3qtq9cfB=BQtiYI+Iujd z(j{Y1I1)i|%3e9sfbC7`NG>x`de$v)r1lpb(cC}I(CY;vbup^$?Yf-bLbd}Xgre|p zFl~>xi8=X8G*6V>p2g%$^}qUkjmSPcs{=Z}Vblav67N{sqaT{R(1cDl@NI0G^_isz!|Hz{@g4!#%3MBaVMdBzPlMkSHmvvXpiD#J#a5(QslZ1 zQpADR_dmd{JAT75bs}Cbog;7!qiJsgKl?rZZ60ZMeWS>+5qyYAK0w&RFuT*TTLNG) z+OAzPK_;=?~PP;)b z^Z5HNsxqpA<`yo>*3zPXwm>VaUms8&^2ji0yE0rUCF0ZD|4qb(-e_*@FRIcBacG!X zZn?|&IF6=Hwjg7J8u4oFby+<8S3fx9B}=u%m!FIM`%a;hA;UX+EOsaaXv|WIr*|SY2 z%Z880{>nY=PD*UhDDPU@u6#W!`%ana)@v-H%aQ5|-gbWaKW!^YegdZvxgiv}117!~MRRCLs}G?C};31X6O!!?;re&|}E?uugmW*=Q+eRfyzfiR}K=s3igH!wd4)%)QCw zFC4{&Ei-@9WhRqi#LT@d$>3KMI~fmC@u**QLPdVH9Vu|~-qss;f|XTFu6f!-8$Q@o zO)mFBPfo^S;4?$XedS_rz>r(u^WID2i`8J0@`R7jic>LdC_oDx{l!I=G_EQ(?+QQ3 zIF>t5URsJtNt;n)LgfzFESgkd6Wv~#Xy!U?M4!am1KmWHJp&U96|rM1i!P>CX49~I zMOPJPI;N$;oKPx06l>o7sH*5_Xe(%`>uPE$tKv!V%`ZSDCiI2Y-PI^5(|wLX#<#*j z9Q}hX_OqY9T0?eMt=dv)1#+>Nyt`hr>O?g=-x#H;DVbndjr$G|tV>q>iUk4o5s;m7 zeHf;W3>$mqXG{mctgdxA_wSb<4_&oN;I)-4=HTV#T`b^AKP%gkwim06s5N`<@Zj@J zn&np>A&jThx%kt*5^}sYDr!^Jq@IwP0X_WwBg!=Kda?=7xZa(jMjh)st#B3dqTr^B zv8i=Hk(8TJ?b-n-2v{hO9w<4;*gSzvX z_gd3AUb(R@SNlIn#CjJ*L_e5{K>*b!#vMN{H!c+;yd(gvMOfz^c&{-9<3$Nh*DW$S zUBn8FoO5IlcCHW}w_cNUyEL&|KVv|j&Tz)nr*wC-Mm@Ybv}YYc7kq6KII>!()+$)l zy~Ny{yl|dCI+Uo(=*A2z$vF}V#~WV)18jmrQnG7cB(^zv7H1In^+icqlZrVyBH;QO zW~FGe#-{+f%sMqQ8@u!FVy!cX^;UjWQ0byBCVS+@SXRCh&ks$J`iQbwz< zU6CYb;GXPWYZJ$IrCOsc9U9~8C^?#e4E&?wn)?HwmzWjOo+HFWHUovuPc>s|kNSST zq4Ycu`5mQAu1+iqgi!514djoJDd;$XS4q_(<$RK}VeVcc9^l^WhL4wDW?JjiONl zXKjoDvs*d!MIv4KCvx6VUKvkt{a=QmV>_`cOSiKFyYdx_!l#uj+PfiB3kBDyv8kgZ z$@{Ct zUsh|6nM2h=djIz+tSO8}QtJ}OJnU8z5+a`nan@})@8?@2@?f2X1aa%t;0>=hS;`~Q60T$F1dEaz%2Jr%x*YNDum-g7Nq}$=Y&SX0#nOOYcrVp6eY&K_2 z{+<9{>hH)$|ufs#VbvvyVU6j+>isLS) znP)O4y3WM(mq5~9j|CQ``}4!9m2RCd7@&H$37>ti*9?9thi-+ z*3C}=F?stG>^LMi&0LaIFLqEHA$QQ4Ea^#6EDZaZ;r`rZoGDj?fmo!S7h<2Hu0eu=)SUI>+G1-o0HfGs5FP9<&wiX*ok#a3mUMSOm{0)715nfm z{fmF5S^51z7OtE?F)mvALHTi;bZkhM@dLYbhB(GizjiEKsF(-loKZUmE#nFw{ixj3 zos`r2IyQj*?l(5hG5aF6So#6GQWEbMAj{0KQQ zdf|%}2ciIXjJ+$q0Re!RW)lE5k>DdQmE|>S2dB6Z-=Z|BAGJ5*E51APBCQO~g^E6U z&xT^^r!wLY#hP-0YDLXc6#!i+L~=2O^N6 zuoEK~pAO$xTpXw8zfjLD>YCGD48biI#9+$Grgh(nS%Qhike|P_8t5_YL$1*PX7V?$ z%Sv;JrTQ#`*1^w*Cds~+C0066oxDtnlf538A7%x(%pbseyo0YU{`8vk2MYR`O( zvB4t}ieW6x{P31FmQ4m>Bx6W@uNleRCg4ww1AJh$y4i3>zXK3QI>pSe2R9N~%`VH` zm;UxwNJehTt5v09RVnc6=+oC$AJvL=&v{vU`IkfB6Toq2dKJR{#dgST=rD12dtNS z>u>aGJ16#Ny24o72B)KKSbBT7H5pgNgSa&*f9Kr+mxb1wV(o3Y#|J<)LvZsJ?m-!K zxq|LV(}PBjSlqH|2N0ISO3qw4C9vE=fA>5*5$R?dpOL!#(avn%(|Jdw9!Ydde!2(G zoK}kJb(t3Jx)>2}?+Sg9n&q{!hrSP|MYl$CxR0`f++arD=F~&pU{&7vBMgO(U_6ht z{lLQlpzR?UF@0KE_m_^qdYiuE=3;r>2G_#@ZX{1fKE;6RR!2EKydR@psE z-x0v6&2triFliTkp(v4$hOp%=T?YomLv$ztbjP&lCj)S5UK1yW>QRh*6%FrsHK+TV z@B4KW@(0rxZedFI?AjMUz()7EYL<7LJhcCv#|4E0Fw`DV>^ zh|5!g+aaODgP^aT?&9rj~wSp!$KeHw z&FM=Q5RmUas7VjX3mPd5XIDhd7!t_<`ZOI0hj7kjD4EboXQp zNJl48;?wqM`4m#$a1fd1)G4MIjgAtkay~T^(@~x(R=YwsxW3STuB{5B+MZc|QAE7C zI_u)NqO4&%dkiULpSY4)URhsXw})@~22!qqCZkKMjOAHM7h+Q94XcZm4hTs(kkSmA zWHHzky_y>>z^PewN=>vA8M+7F*_Sb*W;f=PZ z6dp{|j=Kxo9&E#y>||715A032Y_&TfZ12wg{q&l^HXg7X1lW2WVw!E6c?pAX60V2Os9k7jK5uyF~<69wxg_^nQ)thaaoL(wbbIhz|(PCgip zy|l1)ql7%jp-p_K7Esik1?nacN;GQ-WNLTTg_}svpFA_fMzaSgS7`|*wi5D)hkLr>{<>8aZ5Cf0ALT{JFd=a;T={(iJa;>n=vx9aV{R>r zkxDietVUR%B_#KNQl6=l6nCU5iTEBWgY9HaQbiE22=&d*uyTafA`;zL17H9* zjRO~`dA>#X)uSYp^{W(dV&C=Q4QH91M5FZ{>F8nHt01z8eiml{|2i8{j zoip0K1sUo*t+FB(>YOd}!q%$z)#{|^hNzbV)mG%jDbOXL1gxFPH;ii|v^~-1m)*Q? zO;fC6@zzZ8ABOV@EL?uM_&_&~@SJ>Z&lJ>or>2CQlc+nw_A?SqF}eknM@XAOwesZc zJXb^7M^vS*adf+di$_|3SMGKp+^N;;*r`zNX<>85!<-F-_2Z!c^4Q8vT;J2@j}ZDYB&RmG zS+P_LV-*dUI7{QlD4=@1&17vC)jtTq?n+b;R-+~P4-QBsW z=Xq4X_E0J8s~6aZ5nBjso!e}$+rkCxtLV=bu2VkG`@F!ORER03Ss)&Yt2{h6rQ|_+ zPUs2Su2Qn)t>+?IJ%pf|TfZzqzC~f(jA=Dx2&#Y}%3)Oj%pKpG( zAfitM@+;`zGpZ*x&LQ?KQ9~ILhjlu|<(2keA2tzCaR>;DD4}IEuQ_v-cA536Iq{zN zU`%ARu{A4l%?Xxu1kMn!Y3Ne?mj#RDC=k6hN=+ zAU*sDA6Y&Gf&BWwbl(r~JqQLrbaH%t1vGg#HkN+!@c4s*9@PI40TT$B)*Zh2@b2(! zt^6G&^Fy*8z1@HAem{N zhFTjS;k8tPoXEtSo^%^+|i%mr@oc1_>W1lXsfkhSIpyV18J<{mQMV(SvBikDqa`gI|UM~5Y&jF$-mhK^tq z0U=>W@cPf!r;V?ykMP}|y!|g1aO%HHVW|JgUExxpv4Q9S5sIcMc_f;aba4i-(dYq4 zpo7dx)e{jHeYP}_xo;BTOlOOW&+T|i&ez{Jm8Ebnmvu8aPBR|UPBT6)cMlV8Kj7lx zcMa85rc4`bGoC_@`(hd@tJ#6Ux-3gFw0TA%iZ?P?J{8|)mo-~dE3Ff^JZsA| ze?a46=str3poRrgzBQP*J`oRZCcoGLbqxs{V=2q2DUF*x5+RdYiUeA}Pap@}G(^RN zcqiykbh~&Q^x_c&=z?QXQ}1gVNJ5%dwv;i1`9O&u$J-(VKx^0OVWRWta>k%aLJSuZ zho^94nkiT#3?PUY4ckN`zJ3`;yCyPUQ%1L_ZW^WmVspoJZON-u&yxy`wK^2!vTOPv zetnfiVWAt{7P|^&kB1?YW z<)TqQ?cwtYPbVGkAt*&i?DSP!!)_K=l>RJNesVRxtLTCClsqvzD~0mv&zHxESX47K z3EOnW)Az=+HQ2ld7eXAdsihAXNV|n7Jb}OS3Hs}chMt!aC^9z~f?z_Frq3{`AP`&l zNq`IC9hKC_j4E=0&SSwn1^xMt!$>DQX>RU2dX@jL==Faj*#G|FPq6|a0^F6CP(JNi z(uvW)e?n9Su!}SLLx)lMiz5O|7upYs&;ABR9G}@URMw{ zu}H+HT&`JPwpHC&AHHtRtghA}U;q5u^P8BsKlau~#&L)1CdX@<=Z%MruJ>#5;m0(J zlg=1?nsd8D@Sfc2UKpQg3m|>#6Lhd;N}U-_W6YI6YyhOSZ;haGVaHU%27&2G(K)kP zz0}~11?tJXS@2V3x^wxG)d)Bpp;1d;!l7ChiJ`#eQZ-y(%g-R0T77<%o9wt3p=t2VE}97D3|#td_EbVHA`R$w)r`F5rF6QM*Q=MUjfk zR8$Lvo~t;WH7|u#o>5z^xlzmzm=sd_nwfJ_bei(2z*OHU5G6LWys|HAzfas1ll4e2 zzK^321zHaX2d9oQ&#Nvf}U3}&NX zS!9?ba1plXc;bNp3Q*d)o17NS^ox5i6})Xd+I{wtw0Ckp;VGg(z2FxwB*M}HQY4s_ z_KqXR&j8FB6ml+u=*%iuri@6&XPq><`X6SHNx4m(-B7Xo8?IPmUBKK_8mqm6=t-%lTkaos zkszo!zu+PahBXHjhzhz9|^JW7-gnxT~vkOFkrV2`)?g zLU~}wXouW;r3Elm(8D02QB1Bfmv9;m%LH2{nJbpvSs)X&Hs;z#4`~LwJnC9tQUiUo zxgcJk)ab+04yzg#4v}h0wm58rwO}^AB%21Js^*zIzDB3;|8`3c%0GNCf5D7On?q&l zSbrOalW2l%E1)zC=4>H8g$u5YExu~hD3GB^giTFblLO$Tw0ZZf3l~WcB4+j}&$wp&pE0N~VIqgWOKCG?AGK0=! zS?QuCp+Sqc>lnmZHOg+A+7gw-#_7E`Z6*fqxk08Fz>JT%;2B zMAgd6EQzXU&BYni!fadxDZ8&Z_%n%GvjRe8+V5B-OM>R+v4v33x~A}fg~YW`p)NPTQnxgB^lt<3+iuPi6;rMtt_C6oK6?GD8z`d>WU z3T<8u#KzR;1Wp|>d>Mu3?v@@FFxDq1IRBAwfwt}XO>5;KRV65LNT)pVrKrDTjs%!9 zU=6spN~-Ejd5%t4T99w=a2!4#4@sVSf?*LLX~pU_f~#+<9EjdoI7_A7ZfkZn+C1s? zi-k;v10+2oeM;BL=hf7mhfRn=2OXGZU;zy7O8MBHS?}oVN@8cyhfR7A!5%AhwiP-bK9R&(m5y=wUD+ISO7>0iTaGH`R<)n%@_r>IL^@NaAur%`J?-toMSmb z+fyY&^$Qw@qt1|G{!}#!iW)~FKvmu>8pl*)57cqqO&}MO?lmC&eQ~H&GET;caV4=P z*#xgRY%QZ5;OKmerpO6{|4vX5MieE}VaO^5_vcXmKuR)&Tp5=IUUhCykpLXel94uJ zJ=bMxRp}oa#Ja13s%`AI%Vf^A$HodR!8Z~W>022#QCTj9qe`^79#uUNvBhF$7(PPX zE4-#6=;g&Y-(Tg(Qo58~Vi}aIM+B>BYl)jT2BEhUvMz+^OpUK~u_KCT>ph}X2~inS zF9sCYoo&($2lAjRT4t5QhXLJC749ct8`eP=(w$qD(VfE-hvl#!{gjNfVv}P?Ov4tm z43VQT>^gm1`ib_Bkn8v+m?y-Vwn6T{w@B@8c7x<-oLkf-9z-VFT9+F*5p{@?<_-{W zOV4f5_qZ{p3)aJgF)?hK(x8Qt=?FnPg){hJWi2>GYz`2Ifg9z-fdHwyQy+_Py5qzS zs1Jm+9`itJX;HlgI;aPLTdJR5L&{6eh&dJObRVXr#XDe+T&cZZ1HBRsL3%Usq>(og zJ-o)tB=Ja(SJ_#T_q?*a2gAgrijDcbub{*me$G zg920#8Ol+6vxGVgn65Rdx@mL@8IKsIejinIT%)e3kouKQ9;^uio;bEW!5<$#$B;ik( zb%|}_KeZCo%K`pWiEaEp@g(=LyC+z9+Grv0f6Ldt@_P z&~n+cyuUo3wj$+lOH8uBJ)`4+vu0M+f6oy6U&6;ZL?}FjC@jK4U5~9LOk-Gp+tP*(;Q-MB`E#oPMJ>Wrn}qY0 zz`&q0Y!CArQ!uR3rfWS4vv0adZaXN$`RcPcN3|}Dt0Xtv2Rq{36H|jzo;HKh3Muiz z=v*X);sEemQtwgmeq;FHL2i>j9jGd7+XZhc6+k^MS6Ic#H!;m0M&bfCN`x6bJh zD{Xj{E(wA(X|>~XecbEm?qDT9I5~akY^s~rC=3QUV~ZB=wfgnyC0cZ{#VpdBQmImb zb~P&0gUh`hIO+e;nM%OZ>y@`)P62bP4r3MBiiZaF|u=p)#*Bu$*Wa3@1dp{z*dt6co| z(TUcIryiYeon2V39`@mlwL;lA<_9l@%qE}+N5BbN@T}v*eej`C_x=5cD|>G5{ktmQ z`o1f9E0hnrPbjDL?RMqoYO7qTc5|$l)2av?cbJlg<5d6P7-FJ}wH$(y%iS)CXxaXy zsdCmhgN}PMQsd$tv8hv!G=#$L@lm;QR=smW8j)n$wbCcpzuNTVol^>1P(OZ@W5EB{ z-ok%idH=MMQ@73-;jyxnb(|JNk$EDs$X$&uOC+F~oA~j^93%XgixN>j83A{`XWQQk z{lC|rA2+!GAoYG(pwhgvF%0xUcA=N@heV?{+1X~i=3GZL`ly{hz;acL=BCM$WN|AB z^z;ycdkt8Bu58g=eQXyR@IYNR!rQws9N3^ab8ZLlP3bpr-EwV$42kZ1LO>c{Xr;S7 zAH_${TET51$DleqlJ0V1C_sPS7-bk(Bu;(mgF`%U(qgp*fF!bfF);2k&{q%cwv*}3 zDipaD&#?S#nKYFqp^Nk#?31BpKvtnxDWD}mly^Iv)LBCzO@Y}=NKE^Z1B7zSJ(P*1 zW_}eWo)gETK_em-;1`?>0?XZY12xl=Nknc8IOH~G34p438nu#G_0gN*mg}kdp3{uG z*{?#G$u)6ZZy3PQOCv2!YtF9iGoWcXGZn@dv)bSmGP~Fh9SoI5kxHC)=OhkM#tyiq z3DW{j%Zaze?PWsxT+Sbr0FMP@d|Oguc+EV~M(4etAzn?E3BgSiS^z(dX0=(p!e_+d zounJi=+gW-cLa_O0+PuC-YZijJnlXrl=dh(h`_mXs;u9HUvVNe>b-@^Tz!*JQ5J8g z)zdvgFPpqR^T>V3!Jw$8K%J60D#OrK2yx@^MFAneFPa_=gfctufG^}Lvlr`m?MjKZ zpW4+JHcR8fSJ=FJ%zy zhEYr+_WJEy>skc(hHe)|QpCA5-0&LX`Vrbmd3)saoIUoBE{ja>~x z*}$Fdn{~KzrG%cVYAJ6;{AjUntF%%8$-GP-IsC)yoa{msiisPP(?s6`(B7)RZs4Dc z{6R!V=|aGRT@mx-QfqYd12JsO3OP>l`bjIP*$qX5z+$TrXF2>H%V~a@D>cT+#W-jH zc7pDd^tgC(oLY10eK@kN{B#VWvA1d{cVp?qsCY0_rJ*3devwy0CjaP>BtJRKrJS+X zFfx!5lf;Ll*cJ}Vnwl6J z^DtDX^v(Iz5d>qfO@QVuc8a;kR=ReHyS9MS%&|1!rYnMytXfDjSSVOtp>_aJTp%G* z83Bv3p>nA>S6rYak{bbpGDs<^996+yFi^c81Q`n{6f8H~6p(!2?6^E8ZJI94>wTpNSC1n7^A~7d<@wCfJP5C6 z*Sq7;ox%F{ukJd|(TzhaFnmsG$jB#D9n?(f7R}4Lwyk~INFCHpnm-x<>^gR9hoSwl zVItHmR5|rWb!O^UYFRC4&8%kh#KrZa6<`&05tr`33+0>6B3GoN*oC3ZzhQg>=^I7J zU1BqqfBwk718<9gjN;*stCmWgb;g?E3B=!_nB3vbDkT-%67UlGQjZpgSTQEiGElZR z`V2`Hbl3Ji;G*8Vi^;hHya_9JbFfjAVV`f5WZUyOj)t2gUqM)D&-|C~en@uB3!5<8 zG8x22$i=g z$dxxUjFY!B#1mk0@T;2|A_$lmQVok|Ls$3f>Em^Am)M+mKs>D@IZPHZzsM4K1 zSBGins9I`VA3cu%l+<(for~CDRSH$rSkkJBX>=E{)NziSn^eayu~(`!k*eyd)>=+g zk!t8HXsW+xbeA+YK-PKcSQ()hK7}k#Q##_=Z4Yl|_p+7w$$GjLqIR2$@%gMvtr-8{ z1U;jKo!f9ipKxBLGvdphN7x(^@D@vbXZ>({hY!7D(s|U=bU^MkfbJcvUbWYDFwkCu z(7^~&a~Q165|-zXv2%F=rk*#b(;)jI|{erC2{?Ut@pMcnFsE3inohLGJPG4esr7fKPlal{xW%ax%9LH__2IDAOw>F)fvu- z9r8lNdu%Y7DDd3uMqH59|3Z$G4lu0S?%uJ}2Zce?MdHnXwu;9L`B@BFPY%ec;gdg$ zJINQYSWr`FqiWD54Wwa_bc|VObQQ5BCBr(Opn`4z-*FG!vs zUPc9An*Qb#wKj;cc1rN`pyub}J^LFuvzN0?<;&gNrsJv`YV!G#ji+q$)#2y0vBM<9 zN7k#{|4d?9KXi$lmXfpgt6wL>#Jus)Kh^nBP5G-s&>Ce(j5L)dt=$Mu^lxe24!mbN zza5?^U;hH|NBgQay+G$5ywzn3iEQgSrF;++O&k`0Qsm6I0^e*;)4p>Rg_m}ILLIOZ zBzhst#bhzozZS0B*{-D1-+@luf75#ZqZjxm)DaT`!T?ywEy$z$POr~ZBcmg=OLy5T zsL6+n4+jO;*@rVZF=0f(^To|4E6g`btlTNQNc=PJKl4d`;03D!iq3bsTqQTdG4s|n z^Y$mp_v`Kj%g>`r8Qx$lU6)7cp)12ExXun6y)#FTEdUyT+*)B~s;jqKVh5JOmtzI0 zyIrjbu!RQ;tRjej4Vm5?@wDL5)7GG=VWLL+)bjSP9AzGA_jIdo+Dh=FbFrMA9nOnV za}17NVjLXrWHnZq3k(XNh3OV&6qXrvq6z6~(uD_(*;{5T&i0u&+~nOVrQ(q z2a0slv?qehFlR;VZE!24$+|{&46oP{n}~PS2k_LsF5iX=Ic~bvnIR2LxK6PIl*r`+ z6g6vSO(!yBR}Z7DY6&+$|3(^%(Rj)1G%Z_Sb9n@F4ugyd&sqMe?Gnw?PcgXOnP)Wj zSZVgIUwE!mr1PPcS@?5Wc;nZ^JQPdIkA9LSeupGNrGOc06y*+Q!7MojIA>UtJLMId z5yNEQF7N`=Gr9@q^BWBV|AZ!pl~1$=>_fLs_p|dk2IrQTT?K&PAO$6I)5%~vQeL@> z3tnR63ztbt!{!>Ep3+G|=G;;SDRK)Q@xSLr#FYn)!wIV=z-lBdVtqiikl+YW+Wj7t zA7`VABnT|0r$!5j_RcRm)Hy@`D~liA@1p^o#Kg_+@A;@zlG$m7cPiu(hWz_CbAGNx zMbv>)7d(3)#~ifi3iNR*DBz!r2JWhpG4VU2ss69Y(A0Pws{fq6;8MRc8i-WvGe&Sg zcan~rwg~#@*PvobDo{%oGGtF)70olS4aF)XVd&P+&bm&sPA^ExbH`Pl;9wKDYg*=Q zmU_1DCthB-OjosPG(!o)996CKY_%p z690ivq98mx^2w#!IsU!ZC+4bQEpt6iVon4^^ISHxgYwOUdPCvru)f3alraF;7Rg*R zu792y1@eTf1U@|@Cft5hpI-zO$I;YXQtH->9S!o9Vtbi9O%rq0?Ov6HcmiWMU#;B_L zK1WMO5=!e-5#4>)5cozm!vY{oao_XCBOGlN3df_zK`#Ma>7XP!sy>ZjoOvH=;_*^NWh#ZD}RhRyZk$oocOdfe?Fn?Rb9g5*Bz-U60YCAc{Y*Zhwig zpaydYq>cRiQ59{&rUyVW&-tAiab#$t>O)2a9F0)Rzhh=rpzf;l)`?|1?8+)htEWhJ zlcbs@8UIpN&!gpZ&-Dn2?O?UMQG2)*Oel17KsSnhQd&Rd*mI~7c{Ej|wQ&zA&_!kc zG<1VNLwKZW#ZW7G5r5~nRxKjQM6?2z7_+gMZLIx3t848}s0*e%K z?wM;;bHO{Vz49e;j5>8NS)D=0&}xxmoRU#X-)fa(dOgcMv01*R4{H&_n7iCHwpr{E z-mc}8&@Seb(GE~^N_mMoMmgr4vZMcNGLLDy=QF?276>PCvyaP5*c}VzF8+$q)8AUOvj_Gr z{)*%~LRZwg&-O0+8tMn0r^Xx+cS?HoP1Z_drlt*pm!7uXkfgVX^7s3Vzr#QKUw*EZOso&3A}y@uH~9OKy!!Z+FX2Cle+tFF z>OnCZs>xJA!%2H7t13g;G0uuKgRY+kw)?)GpVBS9qy6hDI~Ym^tM_gEQutpIAJA{D zLh+}1>OWBYe=f0}myFPWYIQGfr6ruN99A1i&$m1gNMrM!K0kJeepucGLvf6`4CLbj;9h`>FNCm!SnB1w z8cRH`JXxxs4(Xys3SY~C+nFlVI2G{a<28^M6X)Dd_MMqg6%uLLvAY)G$Pb3DS-M%|On&xQ&^6YJ-q3mvli72^ro>!OC zA!APLbSg1@Y_|0yOSdlyLVxv*S5)FsJMI(vBZbKH%53C~ ztOb)%l?@64{0nK&*euQso0%|AA|w`(bJ$BEyL;p+u+~rfJTFmau0hqO7~U9u;02ke%Q}WZ5r_fuSYQ}b z>u&fV1I%%a$=aqN040l-#@J?_x$?5_z_KQ@G(;!}uszX)qck^~@{nLD9v{#u*GDmm z+DKY-UP9U_a6^!$U&kmthYF24*RIaXMq8_B6(~IV-6Bth#4FXm=6Jx&)0+mpvrAU! z-XPSEnM2J=CgbH{Ssg$>7EF5verE08E*94z>dnq0RWB<*c7cHs9hyiPno+C(a-pG# zs~}Sju+xc5DS+N0rx-$Zet7>S;m<)QTR||cA*Z<##b^v?4vDCpldM2tIh&BAr5h1V z(sI{^&RVOrGHmxd7tl^^5Q6p8E&5V|;c&_hxM;VG5cHIXBY8S@5tUfn2L;&y z^hGy?4lTNuZj;P;zyGEM>ExJ_Bv%nybQt2wJn(Jj*pw#WPVenj>9?7m%C6{PM3$PM z?$POQMX#eDSIL#w;6m}n@*;=tXO*+k5_1Z4b`jHhaJ0iGpQU^L$$frtEqiepm^V(L z6nR752IZz?FYd6B^6MopWx&h0m@}FWK-}bIUyHP(bnCMzHac)C9{0QJBTkEaNX&#w zaviEJck}RA(3@WXPR$^{+B=J#-)A!Z*efuLF|!zt+&vB_yLHa~1MaR4DVE$FIrQ}@ z+6N^rR~gAt+-#tys`(Zj*KHpHL~fsA{Z({V#-Fj5Zgp88TR=(7DUxk=?l?aYV7whh zIOy$Luxml8w7hTeL~Zlb#H?Az{ZSQg;@(j-ccHOw(|oM`jFIEE4+6qBO81Kthqnw_ zDnj3)iLPbay+dont(~Wgx|+)=FVC|LL+a!Qo*@BGHZhtD^h0AX(fxXP(Ud~%_fRB+ znic+5cgl?Dz~QO+p1idQImVG2hSF)CQi`|zJUE-)yqZN$$(mLc_ZGX97*;A29S|1a zHl##;ph(vkH;JN~qn|RO`tNI6isGBIW^|ag@u{`4&>}EKf@0>-d!2NIruoU0bO@r( zNC(7mkEG09y9MFc{BaLm6lg?-tdWhvP9=v;my`ab^2?UCK6sIVYmTBmZTYIPwkRm> zpA-;Jw@E|}n1~l0-R;;yFZrXI%l=0n#8;Hp z>L?s^tRFkj4gd_@b%KK1gEqBSJmHK-q#+AGaG3YX=4u zj-JJ^bektJ@=Y+_njC8Bv-GzbK~EQvUQ=gA7*V72)_d2@JPHJIwSoZw2B>dDs9;b= z==;%CFGW9fAuoQi8?goVqM2Qj)Xt%q%|38kr+0(5<2;T?JCY=F6d^f-j0vIfh$zIc z4|DljEW%6%$v$L~A7@g~%nTA{$$i{_u+p)qOJhmX^->@)xoIMl&T654*d~4=9~ZL;~g;0z%xV zd0|NLQc*(vIRIP~U4huXr7)W&{+-lVJ5vD^(Pt-OKb`1`hjfBG{=%pAHqvuRPcjmf zAx zCBhX!fL%*M){+E7&^6NMh z)wDC5BW0KM_89M^dS|Hh%2IT`M<{EjILId!@sH2?5^o*g48!VQ$BWalHV+w8Pkbfb z{EgVqm(loAFYNaVoNc-j=*NlDp--lPk5PEvvuV%I+>J z$OGi#fCvR4o;3eC^A_*o>vUs@B>`)mAc@6sm?27$aCDmo+47@o#WhFBgpfCG8eSsh zh(>bKNf5byiyTcA;|pA6OtPcoO%3_L`bg0InJc56h`FXu%f`~pNQaR3eI7=Qi#Jah z3g?^eaY3}0O{G(xQCsPvF<+m3TBWv-Ag-Gv!1+b=i)2P+T=t?&L#L(;6m~rBC3BeS z12W&AeN+y*G7;y6lbuU}&59(S@0M=6G2eCGXm|JXwJqc?oZno;=8YZx^iNFMx8ar;zKv7kx^vFYSERN#NR_RI<(0kvxN}?bmKjy5DM()@5C0w zRy5B%TGpmiaauCB*xC$I-ZD;uq?`?+0AYp+>apd9fi#hoRn&h>tApOaZeR&zbCw%a z1JJajqi`D$HAC|*c30-XX(e0Ju+? zu@(b8bgWP@n^SsK*eP%7!GGK6T9YuQ<||qg3979k%dw)w0(}i?*ie#|0tBEeh8Zu8 z+ZGqCwS#OcE{BCa>?lRCTME^+ZIt4^iG;g$iUL5q^#z^{+y|%g9e8}>Ug)va(_684 zU}{`{4$^l8-XDE8`=1G&YQnO~rO`UeGF<03N)`X^oMjn|b2!7L3!lK~`8%DJ!+&Wr z#~24cqeaKgZ5=*{9DM#GVcJx!)cFnl<3|$ie{C%Pzhpe`ixKhPuElCq88s{wbYEJC zbb9FpKbm4nUq$GAn+U%p(D;M^P_kbV5g&Eu@OJa%zoE@c=asb0m(J&Mcb9{=EYXrX zZrdK8h29ZpD`s%^Eq<<{GhrWRzipp<_}+Luj1KDgg4m;KalXRO!L!W9rt#m0xGan> z(v+r9*lxuElKJsj?YLa)@Zlw7Y+>C_s4q|2*# zm}%8$z@%4d_1LM|oJFU)Ez``T7WY9G@dL`eeG94Uj6d~sG;koWm*beffm{B2@?{vS zPtp!tbvmOJ3t86IU|hgNnQZhWT}-TvJX|=`RIhUw z?J+R`K!bmzCWUl#jp;(4Itz?2>8#Qi=p#A&^TjwOxcsXmK=@pJXN) zmpX={Kv!(^^lLJa7lw`buRF*p|IjFLHt%)-`h>@=`B1Zokl|Us9)N$sHaDmOeTZMD z9>qWYCH1e7{;)a}H%lhEB2dgjIezBtegK>Kb|`UqaPH*SL3)K{tGu-7+q6nUCtYS3 zfyl(}41aK(n&0%QFG$%gRF0u&rE|L0p_V46_3dUn1_mgr{AIDXopy0@E@82D<#5IY z@VOi~VbVSc&Qyg^bQVVCzE_MeGS5@rk&o_RdmMQU44 zn|WUUVW00B`YhCb=Lll|YnS8Sn;iWOME`dW_FInw@$Wx+w-&^ASN`cMJ5yrY_#P2| zj4{Fwlh{jG6-i!58lPSx;}?D2{=}pddirEkBYa)6SM$21)>Vm4l_E&8sz~;)gAmu+ z@0$FZESi%0^`RL_4SKAa?+28esKl9Yb-c&aY0n21e`yCZ50Vdu4hZVm@?X zWe)L7bo?3i^MS;y6e(rw_lHBDRM$uxAAF4a77b8tEC+PZ|6%N%!Yd2AuFXnPv2EM7 zZL?x0JGPTuv2ClOig%n;Y}>ZYimLN|U;kHkU++mDt%G&Co;Bxu#vJ1&aFQEghmB?z zZ~wC!amp=@c8EqO?~LV0k8?Pgt9L4Q)jDVu(yv%EN~WU52aSoBcFKc2-f){&2h*`k z9ETIo0+h9GXiT=RRn(jhR*;#DqZ4b}y@!!tFdISm>g|_objtR@InbumFg~_Kf_>vrfpFQVRCX_dQX})G{te}DEssr?hSEHn zoRB<71BM{9^O8sh68)TT9#5rsIlOzxRvqz%xL9ja2Dzq8*eiXo7SchbjP*;KE8Fw4 z)0NFVo2$#s&CQLLOvK4ld(8XwC@#coH#;f*bFx9@7k8eW<8ZdEu&3Q!YY92SH5$?H zKqy$w(^-mjILgdwKK&Dtl$+0_7{_vjeN}H$HDyd#L-m-V+*}P)OH#neUS}mOmZ`7y zc|c1Af@(iLOoJMg80l%?=1J=7DLI1z=btXY!X6p$Lk}_gPp}%(P{MSV6nN+&QttI9 zrHeVI%(!u9NYNIIiKxw`el#YRa0T){;FO04R70&x2{iBn(DdqjY|#54(88?ZuemJ~nf zrCa6%nUNX$PY=bEKryMnNcx+om@|jn6J2Nd1^dU;USGcDTp&sx9+?ZWs+a{8@KyA* zr7+Cl(VE@7M_A+}=Qe5Nyj*y5ZG!!MR>7#t+)HaLCkEU>+w`oRb_^4p~;0~|2pR%@nQa3rYWnxjAmMk z#`4BICyy08b+PiWH4${|>Tw|oz_>?7|M&8hIW_>U@4};3OBlY}M#M9URt_6fQ2(7v zrus@+!}f zH$8=T?$*RFL_Gr);dORWmcQ`o(7)2(H8!5$f=eGWIukfVZL-)NVs!1!iqGE9w>cQ zYiAN}4l`Ea`|ka^=eMM6*<#rBTll!Pe7lIqCVp~v#KUo= zlFpW&X!-Vg9z}{SEtimZE4O-JX8z3mgq!+$s`^O6sG`&1yIWTfP^VrC)^u=9mWM+? zT!$0=40QOO6E`itIl0f^_)1Q9=H~S81o8FH-1qqX=(+FR1Ba>}=nAMN9UP$jm#Dbf zJ>HtdeurR2V}G`%A7G1czHBEYSU&3zhRV7UDiBD2e%1nB+T$E$9ITYJn59YcPl=;0 z5|!?Y;v9F^lWxGef!V~VQHA|4HF4LYN}w5ZwmH^RwG@#?t~Zmjt@mtU!hzd1FS&8s z`^fsa#}<%U6vorUYPdETi$q&?uR&}f%f}>HNYlF)@gWwxz}BiAB8ooAJNcJnjuPR9 z2fFoCIm6ycywBhJjxYS!9lSF#+{Y6*u^ITQ>(8Kq+DV7RWH0+^O2z2ujj^bH*{6RM z`$jL^j@A?hsg$301VTwMz0@~ZasTLS2_7W-?OLc;!y??!G+1T_DfEE2A`pd2;!?*J z^UtWXR*+~Wz(OU-U(S$bA-=^B4)Cgj3$CFRmC+R$1bC?@6Dk1RBe}vX z{CHkm+UYp}*JS-~%o^|hjhYTkYr`LH^z?rdc+1bCQzR9UEG=u4$`S?kR;^x&EU{aP z?pLb~)L%?@ARAT4%aqFIadUb=MMpy^<6BPy-XnJ$z^l2nkVewjse_foq$`GnEq_bw z>@2vV(VCPrmIhg!^yTjKb^S`-5=pl-C9Cnit=^(DmVT2w*BR5_#zut}`qx&%46>J| z^47eXHX0@BoQnehB|&FUJ9wqPzFOuM1Z81Wb8W_1xnVI>?o=^m2s@|;MB4K7;PzNI z(omEFRH#`1q;7yptNQ!vcOi9)=XG({E`4KehQ21<{-A3u=?3u-qW$uxRnJn6-9H}ECGrS%tjZ` z7V+!w^1a9b`w9KKTT1CC3v%`w2YO6`tUw1KQhttgt2KL*ZDVLZl)Z(lqH=m{$f2M; z@epYUU%7fQQ*QeyB5n(d>hI?p8_U_yger_eg_Leuz1ge%9lEucAgUhdB>#_Mzpi)kb4(7 zFH`?MzhG~}qk;9=4bVhrpQvM*r}cwU?iQtD3)%XdlIy_n<2Xd*T_Me}>Qn>JvX)jn z!!GIM)lUhM;_v}9E4n<3sCLJ##JByDpR-5@cG`({tN4Aog6wMxCtu@3F37_tU+KCG zd4wDvQlj;7fx`#xV{n49c($5XH}i$;D=8qVJyht|e7J>~)}AcUCXZ8s5S^sodLy*- zdWn_4LRtJg^#LzunNI&?GF=5Q^bOoUu(NdgRmQSdJrc>rz7_x(#&0h2nj+WbT{R+n zV(qp(5E`*p&5OZ?G=dAgIciV;HnYjMDIUJU{wv%K5h-jkA}NqAkiM+e0HYmN=4r_H zR4Ddp`4q;V>iLv}Ik7A_Y z@iWDEgKqjt#iyO`6afdM{UOE{vbl=Q6?KY9v1WXNRP^e4E55Nm#Ny}C@dPX8)gL%^ zFE*l5(v4;V50*=F)~2V6D5$4T9vJ-NqAke-de4OXO24m9XIp?ey-AtV2f z(^MU_RP^YkR&HKB2#>M=5ZR$*rDF%`-PrX@0V8O5+)~4r4ie}3r}k9CN!j)&*Jzsm zp1#K2{(E@uTm}U&zTZ!FzdO)c~=~v;B)TQf&2=BFm&Qt z#P95aRQ6UFQ!{)8!(`XEi3sw2N8+plB(==*$FFRL(y^Y=H~Qv2<5vSq_s{1Pg0tXT z{67;d`nWYPZCacbP|6*Syo~ClmIeqq;lI#N?s+1Pe(od!p~I~#3`mgGc%Mr6`vZ4E z#Pw6;E6y7XVCA4MM(PS5=Zy`Fk_od1wAb)iH+=bH3iedcU18;2PqonRsmMI>h%ZQB zjS%H$k37pM{nc`*b)}OI)#+i7c#<`;d8RYkZIxa&e)%#&8}SgLkY^~_EUGa@mx(i% zQHG`|^a@@Ae^JldTZ|;LEryYpMIF;s6aJ~uJZ+%3Tc6hN6nek#z zWNGBeB~R+8pz^URRptKxPfUtESf`uLkP0j1)^5#PfRq`&DS7gHCzM^d{=HB!QdUEz zp8&0MW-fz;d`E_M^uSNV^ue3Z zRHC3C6egZ1O8H$jJ6Y;mf2>Wd-m^&L7e_zL!i+=!k@?OTzdMu_FQ_;^kcSuGX2E`R zAk>K;l{hMT?Fkp<5vAPMK*nI!7039NgY%qvKKP=11a^P#3<(9pS{Y3&nX2Ot{7zb6TDsH(8zxo@ z#Sud9?{7HXj9fh&&Y^5F!*N_dF60vul8zHjC155Vz76<{*!a6i)O6*8rV6KEiU099S;yg_vd`P+N=w9^bIq`16 zG1r)#oI8KJ9?Eti--^S3Ik)^9EaC@4+%xP1FBGWfObmZa6Cbln?uFH|51HqUN<55w zEBg-Rxr6+IVK^v?)|xHngv{7OsvFYpMs!u8wIQwNEbP7;0m>quFo4zrP8yv$+Tm}D zgMH+F_Hw-(58&&;ey6zY3V~Sd}%pX+jBb8I>+wjyS zVA+-rWs*~+8{6}!v5x!!R2bLg#YbWdKW;8Op@`O;XDQkZfszt3GF!FPft{QA;ew`J zWIk6FcJ6J6euyWAw^bTB|8v=?IjkSvP&_ki-&Cl$`L-FEEm(PHv^99hE0`<5!93Zw z^W7`PE>wBP94PvCcPn6B74$q*9wR;W3UA{jg13c}dcX5t=KS6ZoO0IPFv2b_aW8h8 zQm6Yuq0nim2eRc4PO#mez+cf|?V036OM88iR`nI@#Ac`&aH<={s=Zd~6!i5<)vNHN zo;zxgrSNd;+Nkx>f=hCg8mGf9!@+ood^(E1U(^Um9izpmU2f7a@bnoSqwAZBE5>MT zPv2X~T^jk_H~b>Ky+$}p1mk}u$YomT?Xe?7<4?N*{ z)VqD`7ZgS^umPKBpbZIt zJ?FizNsO*(Bfw?kgK9rORn@=-j2hg6R-IpdmJ!n`+7$=*;YZ2X`+AiUQqHJkO)sy% zr>kP1-qUVA4a_4pJHRW>!?E{e5e3Na+DPia28Rihou*Y8% zf3cBXPpkr8DbPE-6lxeMDrzcXXK!!}7-6v}o~2U63MfPiD3nCHa_Q691AnO<%2?2i zwlY=X{P-D!1z78I7R~DNB_RLcEXKHOJgED}g|WOCH=E7(1IN*}F?L^~dtQrMY57(> zC}yrh`cyDB)O||qFRv80>x3f%-f(Y#m1>L-j~p@3`*$3-EB|q=81LOEbY`Uc1!=XL z{D#&!??=#>ih?U^y3+nZ_s^0Rl=Bk=yI+F{Z7|s<3JFgH^FlGocZq|8EIG>thEvMP+L9@@k zRlmzW_lCor18d1etISbQaggHkiK4NI_&8cq ze*x2(&VWSgK6~hNjcLir#A?{V4%7CpqR%3PT(FAcxemL&!fryV2nR?iZC7eZkq|Bg z*sR6+Cv^YtdtCZH&r4CV@$u&*lsUjwuy{S8oF<*# z^_wLuR#e^hdzKGfDPB2P-VPNSCXI1sG;F=%qD*1#Aw`pH>v{sUM)nVRiOLm*c5id| z>>+%K+7&I03E72L&yp1;rp}efbII-%;la{77$JJ5ee+i|L)7#J$AwuJUgjEO8|}M! z6bF;Ozrg?O?8%ynb30&t=~jIIrCZgB;Q#M!_RI?#W;#UP7w7$dS$LbEOHim_ zU%9<19b8db!ctUWsfxHQ`VD|q{>OhJ z2*F9IccJ=c+BK5kz3RY=xd}rL12_>4={2fYP3xV(k3N?4P;B`<+P8)^8@V1G4fo3N zVD_`{oZDo}W@&Ue&d?Sw6fx{nN8Y2klpC)iO$0dWu27j}Q{4j@5}?sf`9=7e$Vxk% zVtp8#vOL;5{Ps@QM1d!guwVDZQ@LA$K}HO-W7zU=9T{Tg<+ZIYH@g^x=%K$F15#ckT1!dp~pVYH-7V8FzLf2E=%SS70{*E&0T{)gf zB)L8h)V|*~@jFiJ8Z5js1!2rTN|4YR z(txYpFNK+m0YXRlc){qx0ij>qDhDkIB?eo`SeM9*YoyA)NgW}z?2~IAxm&w3@K$+Cr8zdkwHxfQDE%}f=fiJF8TzS%61;s)5aqsQYz9U6 z*P}B`UjwA?TZgp$F;DCpxYdmcNBXQMRQ#N{@c(3f$#TJS{3rSd?!Szn|A&M3pC`w+ zbRjfw^7JpTEqZ!h8JH(nMGICr7$Y`b;4lBqLhNtfT6q8K5U~_v{LdjuW-L!y&_lpZ9^AYfd-YdMZ_{v|A>$TZAZB+apvKu_?4rcdFPl0vkaw6HOW#QR z{H9~g`(0sJHmoo20I`sPHHIx1#0J>I%qi{35@XU+3f@&>(-f#9M}tfDiZxRcG*pLl z(i+TOQx-zn7cg8-WN`+=#+|yh7jvf;x&%69^9Twti00Fb{EEbwL#S80Qpdt0qnYcY zDP^fP8`D4vnv;nAwuN_*p ze`Qm3JKr!o%quy#DW>9P+*7}}y#05r3r9}1S+?oH!21gpJiOIhM~DXwHpRcR4+tzZ z#TojYwA>>1hj7kB1HuKHk#geWEx~F57sK+$i_7R^Nt_ec7J;-EJlC_o=wsD$&F|YlvW5CSmbdgQ zPY7Zlx!bG?WF>r>mg*~EALi>FR5cbr`x5^&zh^F!xDY5k2eb9-7**mVGenBr83Qnp zQ4qXn1hBcXVW!j3=UCinV9zK#7)HgrK3!m_~N#m|ukA3L0KJB{NT zlpZLWK<`gtCWz7wE0(~#xQ%-#d&E6JOqL4#i+QW|imIe2*jH&pUQ-9~K4P7U-63_m z#J5h&9YM;J2r{FB$f^;ewodlrQ~Xq%^~Jk}lFp|TL_fYk8YLGUQHC8Q3%5NOdhBT$ z$sak1m~IdTM?Q6c70f~G=SZQCcaAi%A{ol|XT|HpIh8^}wG{h)>1z)4_s2=YqL`i< zA~$$-b_Vj6_Z+w;bTOW>7qVp1Tnbu~`rHEXPk}KB2?9*N@y@{6-n{SI$B~LS@o{-S*LbB)^q*V5XH1d7ncl3F6Z~5NmfYc$)Kl zc5lLvfKZDP7=j4(v3K)*J|>r#{g(9|_AVghi4dkYoWE)ne^R&UQsn3SM?)eFp%^tM zv5jgqaSXdoJ$xZPB5Y4MS$}>PZ@lu^u*6ZYiZbC`-kY>*Yqna397NiS3L{1D$1yp1 zBdi-*XWVOZY_sq9(_fb%l-zlyE1JN+Ukc?vTO5<0~7b_$LcUNo4=!#TIpIK@gM~6+~a7YToWpx z1D-0RPVKvFUdD7kEz9d(HK$MyOqP4ucRSSbs|}1m{&fYh+|sQEdL2QE^^<)?1+tj1 zz~`(7c+p&n^oxgyEmO=6wyjjlRnyGn=h@{@>f({5)mmc%)bVcG5w7M&jxZ@2+kA^ zln;dFY67teyUOqxy5rQrgh733#%)>re-b|;ZO2$^h(%#z@olWFsaw6j@9$`5N(zk{ z=8M(}MN_1z^ZOmiW)Aqa67PW%p-@>jNt{dD#N4Idcv&3q|L41|EpB%NeU(V#|0(7@=+ClDj18gGm{}e$Ep32UE(k?6ex7<2_R5V= zt~TDBFh;6%0Bco#6>dgi-vtpNT+CxpC$-RPi zO~a{i*=(XsYFL2ttZA{_tTwwT!K^u!UN5!ZiA2}ktQuAO&I)&QSp=>iZBvO}^KRUR zdiAIXi(|>BGEzD(cL;NedB_z5UlFWm(P3SxN{+caH*MH3?lq3kk*HV)6Bhf-9rqGm zwFnjvLwxx$5PwkbMTut&dBbB9=t~6dKG55-A9y*{^|=H6!%aX=ekP7fhcb1ih%?4gd#YBhEt-gHpA6V6z zPQN|8C&7kfs>yG{QiP{P@?EXg?$sR$sO>#>HYrWcgEDR4uQmXojZ4c!a2R#bX_Q+0 zUZ<&C>=ik#9k!ACOv>tLh~mip^whh0`+#6AT~h^F8Nqil{W~uVSh_#(xk9E=@noxg zo(RBW{-R-Ue}>~Y?GkDUbzXka>o|mo?Qb9R@;GkVGZQ6=nYpWF#_@opUb`ZR?BMK- z;p0}58BYl4itdG@-&gWkH7hcH^CLj_lhh8AmxC@>f>#nA!1P6Df8_(oPFQ{P5 z6d6H&yw0;!+3B*Mx1_77*dS%Y9e+75?OWgv`iu&#h$m*JB%`gJdW^iH)#KKsxqNDW zbiIHl*rEKL3-&+o?@`ZeFQ=4LMHuLRsn7>xSZA|_u^Brd@wyeBu$((fy2WmpcIo|s z-Epp7X;7nE(FI6R%e;z)L*WAPyQz z0M4_fpl;v`WWO5uUF?#xq?$;f&m2Ns;lSj#@!1Zl<4FDbWkk=4C1l4%sbbJ6<(ArZ za$7YzaIMh8ssXF-Tog5q&8ac+M+Rd?G6_;T;Vw#dkSLr{Qy7n3U>PG$+}dZU5cB zmyQmjW}h`I!jr<#=S7o}oSI!OQbO|8PU`mQU83O9GG|6m9TkiXMDj#_Vnr-@y76tU z>2c9qP-pW%mcdyz*_Rru6Yv+=W81^p>RtiAN6V`vSW-}1Hya=+u{-lkp_oI<^tto_ zHE#M?LwUFt3NdRBZib8_Dxjhfa{#8WX>doeNLy(2_LCyPi=!lf=#?~aATxflC7FsM zlTH1ENN(hzocrJ4JKudWoa}1r;?7Ogt>(UgRi1EXC4R)0iJzTIRKJ;-6ya)E2I!-c zQPqj*RV|EDjZaaF)H3iCIOvqeT}<_Wu61hz+I3-is^NGSCs4b2XYxe5P*@_KkRq^+@N4b}Y_csHF*LadV&| z)tabSGBj)JDE;Q$TX;%H;6;4kWnY$U_&WrT;EBkH$ch9t6BdDR`&8qj+a9-A z+B{}`5JE6(uFtn3Db&a;rw21Zq5iKhU(C=LJvS-AzcS@?};L|>IL}`L`BkZdS ztp};^HSkq(Xs!m)<;L0Xtq$T@>@5)%4(Ml#eFHjB#eGD zoq@3Vo{2zTm2_axrtQU4!D8v5<{Z>khVhT@gA%IV2`$h!$y2n^94-KPFwKt~Q<5 zmRfHAdeBbtSUodXWAbY~F<5&t9*7VtJQullTG21WsP3D)j^1UvQ-mhaz9(4`#K?nK zZmNpunq{qBJ_Z{etctj#HJHd?BmG;y#p$v&>KiBT3*qNx&$Z$^^k+|)Zy(ZdS4(^A z-=uHJAP~d%zy!>RD<=;as0{iGd?T{rJ>H4owJ=MG%%mhz3_Jci}g^QI)oa%b^DEW(vCo&|CY_EOqlzG;tvV zq%%eFYW#x6uSR_583sXGl%J{bihji=kU=4@$SYr&D6O~H6w}u@k-`^S5&fY{kW8lM zI0h%lm2GY|>^Z_)>SaNhOTNb?MrYo+75M;h!8Bkc7km^_-bvcsDX5)g-b8A|Y7xs3 z&r5_5qw(DVKAeCaSWr4$qGU^h4yWl=jtFyExZGmzA+q#H%V|4Nt2iREIvDr-nM5O?>NsYu5#iOL{^ZaMl4iVbM)c_`nW#CcePb zbS;PNU@Ux>`;HP>(gTe-I7&V9^v5^w-|IxQd6^evvdMiqn>uRuwO$H$<5yE)+jc~z z6@$`M@01kFJVkO?@nwO7>*MrM|*cEdiNwP# z{95Z;LDu{@z`jD@nhalYEf@6KV+mySV>NlV+{c_$hVhcD=Yj^C@L;Bd1+CH{ftgw! zOUiYr7ni23?RZH1`;-~@gPdMwwp$(!*;|Its0Mz0v4Lo5R5F!-klJ#*oryAwucGz{tlLU;pfyR_tIZb~xX$dfsLW^2=~t(XeB-oh6fso&`F2LVa!l49EJ$mNI?Q z*r%gWZ~$!vXf3{ljGcU5k)hFdsE0KkR@KeM63lez<6-FYpg|k4RmADzEm5n#H0%Z3 zF*~;bGi(;KjUj2_bP4f4v~Fj%X|VEXbaLk$Wz@R6Oq8zyNS=~w7nop+Sc>>b+E_Fq z9&WK73xGP!e&yvcJuD=CRccdru9c~yI^nGFNp;YHGxE_GdZ_>j^3iC^im}oT9zeWV zK+@6C&gPFw#2Ie9pI#p40k68xf%gk<`Li3uio8)4)>U*`Ip z!Jqmb)AeGX1%!-Gq5XOteA0C*)<5aO1KQG=M(a)F!@H{+f_jgBnk}nPGJ75H-xZN| zQVAucuV^9jU%Ffy8dl?|^!o$7IS1nMxu0sO!lDLhw(#Lr6Wa?gV}FNm^0tNGg7RsB zF8a`>{Ux=5?0Or9eG#d4;)c=f9pTPo zM`Va+)ZxlhAdu?^8!tK6z?AiZaXB zme=9+c`z<>Z^mhlWcfz^8LQ!DuPPfrbZNfK;XpVSl}IuT3X*(A`~v}zsbjw29D{<9 z0*uorg*ECB_P-vms`uML1_#Y$Re!MI@i%hMhJ+Y&Q&d;u8@_i?bc2u|quHX_e0Mkf zUs}%Am!nIg{H>v0>$wm+5}S^1px>N(U(=m-ix}MZifoD_q5~RBZYA735OC#yXbG2O zIF<-R>e+#37lPr)52QLNi_s$v8>o5?dMORW{1N}=B=a85Hu^S-asu?JyfDt;U5P2M zlvTA%9E58W`fYVqqitWE1fN~dp1YQ!*O!J!+27-lpgWR{mk-AB`r|PyFz^<9O`0p; z$&{I|P6w|9!-W{>$4>f#$B=M=Ye?L?UIB*S?xdD`Y6tsl1fk=s(Ap~J+NNMNeWxpt zhpl)*`r_!BxXwGx=o&j zObV5%KW#a`L0=tc`EhXcvk|?IMEH8dvIdkIFC)@S!+mU1`Tf-3uNo;KTG2BxdhTHK*3&y_2Z@6|7-rS%P7Yy%VZ9pNu2Y1N=Gjf+dkj z(Z^*ZdSsI{ux=UEUed>Bpkx39|F70pS978vH*-7|gjG~%RTDJiZ*3aklNK1CQolE2 zTRKKWpt_IepDXd{Oo@Cru;bx2cO!?`d>AW@nDY!fj&GRr;`IcKf(0Wpz2K;K02BS} zy7$9sahW5jM!33q_rZBn}lSTv>&XngyL(>%`U!nnwW8>dgbtiFb+!@%vtY*1;}uGGxmR7v7- zCS@Z_Tl)>yi00aBsVDkK^ou}7q`s$k$C+ztfp=i3c;eqSs7bp|qeyNr67t-Fi)<$M z!-x4ZuJ390+IQ^r2Kj1E`;*Jn9U1du`t62YrPK)N_YTb=aM3FlW2@lBk!^3gNchj> z+D#4CyoE2R*(F`TG-+ka&OHUQ%C$pDV23-UY_^wO<@vHk;duTlElt9HN`NGQ$H3N$a$y_mBcbL=s8sFCVes?*mRvuC zxIZ5GR7beP>;ob91qLY~^6~|f{iZh4UD%}b1A}`9_&kvbBss8x#o{g4L>zcU3LtM8 zF1o={5R7n#5|USb`3p{sLzA!(RAn;ch4WQD8}dq7T0JGbYjb)e$nG`COf)qqE334} z43qAp9JEzL`<8&JQmRr6>kAdBQEa@Lnu7tnintBds#=tlc?>>?$vzg5x=TZ80w8@t z+LP2|m`;lCe3THX$t;~4c#yIQEJCPXiM0&>p8@qh^XdPQHY^B*rPU9+ zsY`SPFW+L|brU1$u@j_%NfS19f=X-3ENFsVtwwU&DD2*qnIay=#NcGL&uL5J@s|js+HAz2bjxuiqN`1=M068E_=`-}qj^mF8^uMBi$=jX2 zd?v^g{ib+mZGBEw(S8(d(r4j$b<-HmdJ*9`cFbEnPe%;L=PF_Ua5jEdsiCxXoK z7{-%X7#0N2X&=f0YEs0w(Lb0t8~0w9dtws+YMU*VIv>GzQc# z)Xm7Kpd={tlNI$9J^Ht#-gUbGqN3Yxg#(+mGLjJWC0o6d7Z(?fR#wMH!_&;$-|TiA zpkOHsc!)|9$S|@&*lxcr+*VcoAPa}tSNIenRpDJ^D62j|`_Rw16FD=cc2kBd6dtK* z02f?W_w1)INFRT~*ASy6wfSvIX4PS#w-=s#ua)39c`li&(mZ9yWDa<*7=6_mB?8`7AjS>1YqVyXBL{#URr3;1E6U+3wY$ddl%D`Z5?vkn@BSw+LH68Q4n zYr=bW46&&vP1fllW47!U&dy$YNqF(|stwi725!_J!1tv^JA>uRl&5%v>HXZVHCQP;n~ zA1{c>geEm+x7HaYtQo)Rb!#m)A_Pm~HObR=ar_azsqNz~i+5JK5_%@3?^{FNH)Iw6 z{ZAyD!qSiq#n;7n(tk&??cx&rul7q)hLWx(1x8AxJB~vDAu7tn#6=aBRF}5&l(bmW z&;h>D@<1uO-Uyg0wa-fLDoM|j0%HL>>qnCZm4{`-QE|ZAwVUg6ftq(GER_|F*rEeP z?tdQY^cIM4`) zFA=#M#hjw(+>(#jSH6ANYW6NY{Lrl{^z4x_H&xf|NT2N~z$bA5#k{>st zv^g3At+G~Ox!NYq@^xq9Ch>@7>LCfiA#9=2@H{(v7(&a7)Q5*4q2Qm3DXH7mew*PZ zf$Is*KgyLp5A?N)2_3y^^#H)*t0+j+%N^B(SOm@1xLom?(P!7d6d5M6t*VWc#G;Df zjb>)5_en2)ol>r?Fm@mjH!A|mMvlf0wFwW^;{rILV!OHevKio#L}bWT}bL zYBJDx@(lg?uYZ_WvIPoyNr>x8LkWW8r`OZYn9Ak^p2G@cOfi`78{tn=&V);IUMwaK zVncG(VxZ0(AplcqD*Fy{a&9RJ-|CD=j>>l#PfOmgps+nJ3wpxZg(88xfeo!;G6k#SgkdemHy0O8-kn+tI=jC;(U2MEc>}* zLlFJ&XGGHP-iX8BiFB}z(DVtUZ+~qRpo5Q=)bJ9_DI*>T#FaUziHi=yNJTa!;`EoV zxbTpOd%8FuH>XhciN|E^B{kfTbXU_M zr_RY9c!<*s6}*NITe+DKNN+{ygepl$xu7!Y97yOXD#@IQxv~jVY1l7kq+M*9?wNCj zorY~xf5Q#`F=kkovpO@Cr+Gop!FQe6wSi#48VI6D+iE($FVP!a{u6iHTNj0#DxuT`h&zwgBm&;OI_fUh^)FNZcb5eq~W) zsK=c|pS=;09(uzAy^@o5`8kvgsNB2ytnBGq^9Q%D-@05X}a|b;c7*V<EcxRIJT^x0%No~l%C?%@cSo9{_aG69b$>$hk{((M~WIgN?R-w zqOXIvwHKnZH)H{ZHr3FAfH4;v3NHrXMTKdL2#^7{vPgS}F=mu#JS*O*pqqA8V{m`s zY9&9tTOLdX*l?EJM>BPMDXBYW?f#_T%DVoOFm?Uvd5_;9|~CGw%~ zy6uO8^0joU&S2xW54%KWc5;8s30Msic>VA=4t)ai)a2lBa6hJIu_R_QByAl>MjAOvSYD`c#~Z!$8wD^T#ckEqYt3t9FXd2; z6iN?%gQs}EbxE(wm?dGt)uF`iTgT6=0DwCDdA+Qk6UATQ|7b8Q)H4BxU#cwEe=`|a zilP2@jgvcY2^X8PWfUGb(-*|IZsBD>t*8iH8X*x2%JcyPK%1n-;(o;O6K; z!OCQ2XX5HwqW*tJ;xf76LsAv#e;1|EP7UJLFlLcX++E7FH-^DeZ|8$6p|pn{dI7#O zP=(<1Bi~5lZmT=Mf{{7{rd{*6uOC;}w}Amf;2Aw);G`jt5)u-s!!xz~dLV6JgT-`3 zTRZ7njmgMY+T561;>%$yKpW@GgQL>Y3xQm`zp~91QgH}XTwg>?LQoLPcx$`TcmD6oNmH< z^4Re!QnlqihjVcR6$S2IiAy`M>HxE8g2ii+wbgWz%%Lb$)M+CMV2dU0Wz9ps!zC^~ z(_XTw`NLg~)8BuKqbh2zJ#L)7CDQshm3*)yC#i?f4en%bZ`mK4}Y^U`9ZAN=bkwY#K>%GM%<;Z8e^xKBppEN-AWiV@`s{zW65HfN}^$7!EN z$)01J=lakncw(a*;lT{noyu%3TxwoIXsN3HVGfw$ZMS5!tr`7WUtf4&`$`q*Jb_+f zoa3FG90#9nm%=RyiJ=E?j5MI$9EW-6PUjF zZjQtEuPF+{7Zd}2guBM{29zPaO1UU3@lwe5rCA-~h9IdrhLH;`^OE8brHEnK{8cb@@ z$Adrdgo!AItZohEdNBFvgpe$QI%SONErv<&rThYRzCzJh7tI`EB2aeq_Zb#zkVI%7 zGCyl8{o$$jHdn;ZJAfg7Yl<*Jg_VdS6yTIr%SW~RARXQAE)Gh&q%c%tg*i4;^)*Q1@pLlHkJKa$P6dr zC*QD9Rp&X@j`BSrXZIentNK@@cagiCL8$lZ9!_Pm}QWG^w&lJ8kd8mHX@Ko2RW#1>owIR9Z`^In3T6 zV2!G#c1D~whW$I0v%oQWin%0+rIA}X4)`Lko=YNrR3oq*6M`05m&UST3H{g#9k}t! zXw}q5_pi%x<3NT6Q$5Bc?DGlDfmCZ(xk&@z_3Xx27L1K2l)x_zXA$iJu>zIxxEZm| zO@9T^)^=fH^)jyS0oKKv6Qc7?cSBtqG3?B}GT2~}aG*;Th+u4k1}KS z*X{*6shkdlj&ntG5pd!DHbJ7r8(Ay!4B@`LyV;?3QsW5&IKe+q;W)gY93A6 z=Lo;tweo}nZ8F@$osm!L)TCbzF+p@vZifm6E2x6q-zhkxt^jNnw+2Q`fkRu%ZQahc za-NeOVzxS>Y{lbgUVPh*yMXZSRjE}L&Vro0jbdrEJ1$Rq$Mdt_7Y-LB9P-K|G`!nj z=-tqIX#U@o1+nJ!#cjTp?_nasCqE*1?9P4*My{-kl8dc@vosbaoxH?{ zC#JyAq|nbJSfk#P`jLFT096pCo@u8>*0n=@}eEP(v$)hVy}J znru%uJDmbsArNx(J;`?#zUABh6E17_yfY5pGhhrylW#VtU0) zOuS?=JN9!B3YsbDi9{%gEdD}Sp5gn{+}I`I@rF6ANc~aY_gO;tdf?%DmTX(Bv>yhs zs?4x?gWdT&MY0UL36hW^iR4z~tqeqN!c$X1CmC)(;fc>cP5dcl0!UMl?;P8OOqo8<#tNtc3?u8-ng=ZaRE=aLNdaNv>&QrgGyl9jt-{m65GpTvIU zlMeiK*jHX(U{)W-p_B1qN!62kI6e!Sf>U$=kfiEheWEflQI}n#ZB=5Go6P!m>>|=gfW8s&YHePM}sc4QeMlJIB&# z!Z#U)ZhZ)Kj!X_JZ`$s{pa~E8KOCCLB3yBP#L+W37nA@sQi{~zGA0W`bV6pP>lBp>R^~p z{rpXKf2CsC`M$~STIE*&{Vo4nPNRj_X2PXIaeqd{5DRd+s!lohljc^yU>7u`!mE>Z zH`1c?JAx4XaUAo;XD!rb!>?zrj;v^Sygyf~F+!lL$YY<`!!)B`HW|tw#Y^HS^37mL z(G5mN(4aQqEp2G$7P%&;`Q1JD0P(DX$-kz-nF;38B%RLnhPa18P4Uh!dCejZM(AJpg{Uspi3mGR|VmQ$=?E7LJyT5_5bDE6tN9XnDiqhG}} zLIq}&XsC|N8RgqphltWAf2T%kkqSilGE+E`ml zJL}03fVEe7jda1rHtSmvIHJy{Jx^QVk-dNo0Gz#{2`}V*VSlU`LLtB$rT;i zO%Uey7CACfmGt%87r3S;$4MHeh~YB>QadYmz-KPuFD~mvgqcemWcaO9L(=M9V}O*j zzh~Rn3w?&$S#ux0$BLH9`Sb>EH58>M*=E!)tROY6Tp}LL8LV5*rYFe%HV?4AE0U$q zD9P!c?0nk}V$`USJ5Hub>i` zqKIZ`X(mhc^b#jQZ*@hnvr@bU8GIcXtteRX+uL!%p>6pbFPrbjb=*}V$3PQ@;MjUW&6xx_!6qX~Hb^ zZrY=o#Cyyax>RV5OakALaG-^ltTL!QXKnFt7SYdm^<6R|r(qo5=~7eWcs|-KQIxxq z;+1irTve8~qJPGCr*+W2;?K@% zCal92zc)Ew36+gX0>dRC2J5LJ<>g+v24jYs=j^@LXe9q|1tl-9KCpR{^336d=>*~Y zhh95WJh}J%P?kkRvQ$F1KQrTJ&blP=(VSpHPpwZv*H7b-5C``=%h~QZ2LTU@62P;# z%;Q{a0cerhCwSx#6qWsGeivW^El4t_h+JZ4Pu=s+MAXG#0#UwH;TW5S4vJ19JD#Dx z*~sB&0#PY-n?_ws3Q+#9MU^p{({$L45~ZMwaAb})WzIL3@6?tF7h`PfAmP+|4ng!S zBYlFB&E5J1kc`a9ypE6JJo9O4maX4UAu62(R_|~^{xTEr-jW@hdtI5{!X5OA?O`3o z!*>JMAGM1)%_z&l(A|CE{%CaG_tYtNR$gO-c7Gc8 zUKCO{e754aG5GhRee9@Cs`TJrs|LH%<_`2ry=DQIX?@WrSh2`1L`@Ly_ww4clmA#B zAhS0yt5d%i@U5^9`E{l$?JS}Q-Q*DJDalmc$*TxC*7GxsPAlb(ez|>KynY1rp|6{9 z{;$PrZ7TW>}`6Mr3j77n*{1fwmpnw@8fY-&M921KMSLb^rK}OCGMPH_Ov702EtN=S15hTm6Z`yordStc7pzhZ%Q~C<mG7eZQ)195;y3HLiL;^7Fy!?JTi-tYOANej?+WM8x zb)UI!Kfvaw4s)K~hB(9m)3<8Gg(pLYyexHd&p?L*T~T1@OL=OB!6^F5 ztniaVK4S+;=Q z{3WpkG=XTAkO*0v82t%bHY|bY7}wSrk0ysVmV$q@Lj9^nv0&?1XL|F1K8@`xMQOKNko_Qd57UUX^LolC1&t*f1KeZ?sf(6SxHp;RVf|&u z3sgDdl)FHUMvaEYac>N43$N6iU_?&EAWvgQNnLNEQIavUTx)DGKf0W8LEvcRT%@|wuuLZ()ul~5b5V*%a~czarJh2N36-4OZ;XkQ zhaZ=5lTDXdA^ET%Mq?ztwFKhbDzyGkPksdEeHipA?!IMwGhD%oOiN>-hsIv7gEvoY z`5be*Zg9pKHdZ;^*u}Ld#$h0C3x+ErZQ3n0?3;J^deh{=>h$cwv_*?(Ox1US%63)_ z*YspeI#<)GoDxer4W+fJU&?yp1T`82JzZ~--7bm#R(XoZDL016@2wG^%d~Sf#6MN_ zdszdAfBP(4p&~K%JU_r%Y(>urVh!^Z%la*iBAZ7cob>bOOj|Kl0MJ*Fa7xjT;jS1SX`pc-VV8NXv9Aqu8G>8MoRA;2(1S1S3Z}5?R2)pqi7#=orqP{)bNJ|vd2?zG_@uGQM zI8F}$?^F%E>jUl`|Asjh6!c8M9lhMDwelhu5~F+TWRvqfDh#YwB~+1L4Dtk{FX?0L z(HzxGiB#5!y`H7|W`*!7!5!%bO<#xYnHal8 z9(Fwk)fUQR!LJ|a+sy1>anCp`#+vC&kxA>g)OKy;?SSg~jn(%VY7Eh5fBe%WIO9 zyjkn5Gk5Z|@}8?0a`xkUXB>^tv$toQ*Na%?AEUg)g8km7)Mdj;&ufXRVY&6W0wG&o zcuWG5YrDLlGs7WcZ5og{sfI_s20uoq`5KC&k*5%l{+&nAn0yW5SF9~MpGA3%4Nj$( zar{o{Zvq6An!!LkkAuQPJ~6#LiL&0X>4><;I7AzFBm^-U>5Gq(Z>JEUEV;cOx$DQs z74^sxr>2_JeGvm;;9UC>zv=x=AWv>nm9z~px-&4WFoyH1@DKtUeC@w(%Xt%g-6m$i1`Y|tY(mFuDLkR; z;5>Ic^ZgKJ59}HWcmyAKLO3`!bVfBWc6-Nn7+{Uszixy0G;(~W&M{7UL;RSkG&rLE zJc?hrA?}K4y}{zW;X2wU3<2jz4LeTEGw*UFj3VKu34KsuLR2AwqREBIRfMsI)-|I7 z`e$kS{j`{C1`rzNuW?MQ462Des?Z8bn%}$f<>VAY-IFmEXj;|BL|h^ zbxh&uHYUxCI^E-pPMS1_y8WsVIGJxW2_M;j?odyn=ddXMq)Uk~ADgGCG#buu4~(up zDZEjrvh1?kt!*At!IAL=@J)zUl_6#W;Rd;UrnfU6ZgtZHk@O6u9x}^Vz75Y8JLZ{t ze7IpHa7?&H)Nk{?}5mI3iTea#$e_%D~vjqpt@p%0Q)vbu~yJknQU! z67%aXPN+cd-~Wt0BvtE~m6Zd??=LRzE7$Hr5uUFntm=JS83eTtiNnZ+m8#Gx*GjkN z{&vwgkWl$2lr_pYeqo>ZW9R>bo(-?vPlUA7!U4Hsoa(g(>+#5c4Z{=cn`O5uq1s|P z-8NNz6LeFhYB9~wqF#=Keg)Rd_3*3PVG%xPmGUTD>^+}2C$r!e7mUW!E1pyw1E4Rn zgQS$rlC?|d$C2D&HnU7 z>&3>@das-GSubK8oS>MD&+R<`etEi|9L54n1c`kfOgG<)yq>tQcn2aA5Crz+D?8ti zZO7*6;caL!R4g%nY{kRC=tBXyN2TEm;|%KP+TH7mui$9t zxHD8AV2MBUuFCID7FKTv<&F*>C3SO<=*Vy&=FiP1zEU>L&+2;s_P3 z7j#4*$fu|EGl+Xa6kpH-8-CR#^L4?$gi;-2_~(umOHBxBqe>;Et4Xc5RDC~wI1ZV7 z46+ILt-VZ`IQr;f*if8%2-cC3odQzBHf@^N1SdO<&8qH6aV?GJ+HfJ6?Iu6Rd=CW} zg0eNMnjYz#{FRR+l+h2BN8HT|H^a=X3N$zW6nXxZN!9gO7@JTBR{pFtAvKgFugUWD zfZJI7G7}^$@u{w@|I#%%T|eOj##Yu!!m^)qY;Jg5=tNf{VRAwVcEAy4F4nA9A=pQ( zO9Ot-^{b5*JLhO%$w)SsJ`h~6E0`3CwIRyyqefc{{u5g-l28u9{)tWIWYU7v9di{6 z+)HKJMMvx!p)t(`2D>mNEY4U9hnM0+dQpo(w!dJmoVPvG3be>{N{lQblOO!0sp-GQ z0lLP7GpALV>Qo*Gbt7l2io%194fM(VM&N}vN!4m7(g$oA6y&NL1Y1&Q7CgHqoH-E{ z2A=lQO-Z4YYJ=Nm4i?E7>C36;u97H$0{)XH4v&!^+JyPFO4|8vlWZS3)Bn`#^*NY7 z(6b&z3U-f%jIU#Ms?Lb#(q+-`gxFZOq7xcru zWYF$n_j$X*m{$Uf^i{X@K}j#^)Ro|Z3Y zJ3YRm@c>WFxD}fQZqP-b{?tYMy9KI^uzO@WK+q;Y!NFY!?eaVUpIP(CW0t%+NQY~Q z%Ac(jE*U)fuHxpdtk}u8O|X~{N%jY-Fp9~}nI#I9 zlzKnGu|ID_Xh3dSt*{u$>3{P2HuHK#mcGh$^uITs|9>j(zpR2(_Y*cWglhF2*Ntx| zAJYkiiOBmrz{l?vUO$Yl3yN$KNFko~?$53~dbhf}w_g9&5(B|*xP;W>Ovl zhWcsiL)hK!J=4;5L1UodBsr)Z#d?^7z~J4@)c(-t=IhZo<p zZ8r~2jq)Ybs@)|R?8`dfof8;LA5fl3Hj(Rx90bG)UwHc*zB`K`lc!av z(`t$W8beVpTf@plOg=WLWh1R*3%ip!4tO`p!2qV z$CNR{u}h3s=T@4?*C>}{GE-}x?*lgxv z-S0X~8MLR)-*N7^#RicO{%!j3v_?D+PjQsJ&_JX@C4aSm!ePE`FpiUK;l*#Mi}+7z zwo}lkoJvz=GemFaHJEs*lhcM+FSgdhwmGj;Je7Zh{=<;7QjT5h%df1Xk>-|wGO`ok z%kD3M^f8=)w}wF_;|rW*+-Zp#jtiEKv(Wg!*fWC<>;tz``W#D`g0mZWD;Yuf4U?VkT9_XS-GXJ2s0GWB4oEEk6{iy zSnma=aJGxDf2_VnAI|Ix76rd?iuGgwagB=!yP6P2j#Qb)X67|F5(fyjsk2JsOG232 zX_~)F2VT&9CG{qi%m@xY6#|V^Zop#2SjuVPVN8-jWJ3ZAh%iE%zKyB7JlEzsi2H>7 zw5QorpcVa)3KR&1QiH<|$yc)wMV*w>ilN2cFKeXSf5nP;Lut6*A{`M%k6K#=>QARh z{#HxBLKYgDN}I`h7$2sR?d17{#Ln4WQkDmb9#lG-JP_&$9kQ9iL7;1iUbg?dWy7Do zQN+ckx%z9FrT7VGA&p-d0m+vd5lmf?2o*qVs#Xr!qy~^x$J2i$zW4)VRWQi6o-q`bnaG#t6)fJ1hR?*TP&CQQc+q6HBAEmi(0Tj@s_zZ0gIc=G+9maQ}OJ zLi!|}UHei!e|#O8{x5U(zuNbIPfw|}vfwg6$s`F3R5E9{cuIWo9*}!OIuEjq2WTMn zBiC=1IoK-s$nrBBC?+V#J|;$$KT)xD#ou}@_;`QGz5x7qKfnIRVvde1sOp=dI1$uO z1f?i_@m9pv>RLx(R^6W>#@jB{MxrFTp}XFQ|w!pNp&t?+z+ z&Wc9TjF-baF^}EW=ZMVrx)uxf{3$1BZYy=-jpQsU0DRX!$_!O%fTEv+5v+1!ROhyB0e?VWB-dK&^s#zg%c@{)Wi{>W;snrO$}6 z*g;jIr`A}po8(!_W7V61mD%ZhF=sQT%hRgn+qS|?BPA9Z-1Qk)E`P^egR7icPv3LL zQHH0u<;FdC@hxh+nK)6QWpd?qklbbr@9* z55>t_+JE}p?`c#B(C*=Zln?C5Zoq0&2rRd02!sz4r?s7?cf6o)5uyP?`&waH;#!Ag z!(dN1a#>J9{qhL{@f4Ooxw{ke@F-+t~T zO0RUr$->QF(j2S41*>n$jJ*wK|8>gwKqN;V%{6vos;z=E#~hh>cFE7;7dbl1Dx!R3 zZI1Pv2=VVq^_2|&l9bYP^$}nwX3SwDpKzYDs$Z@#!5o#66lG0VtetY zWE~(5rpN~L#Ngc^_8j@e6!#qLtOi;czZ(AA0(z5~AuqmKfY^Vvfd4f=q?V3@(S4a7 z;8RK9*vV2O>%j4VL)gI)Mi5rw;&7N9GEBzCjrg99X?MlFg~El|%Fbpb0g34!Ngu&q z$bDDO)D|4;7G13TYRkBLr0ClG6ke{O>1p^0dOA7U8M~EKXrLmZei(~@S1s_-W?))ob}3O1 z@l{r(%nTd1;~eKxOI%Io%B}4(?zl>yZ5eRY20zV~wWFC)nSM;ZQ8%{wo*TK@h%Lq( zM5hF;S8oq=!4y1AR0B?+L=)GgVa6CIFN9}tcpHcdL*wWO63WURyJ?Th{%s0G09Ybt z!h3yZ4Le3*Vr@e15)Tu2k{2g{ldUkQcBve4I^s!<^@Gn_AT6Rfg9!CT*n<~+@uT^o zBJIGWu__oWPgTuA0p$uJn4J{*-L)kRRmhMwOxu8dv$f9y(9990Z?aNlhmJOCe+|(| zG5r&j)-{x|BSBiF$@Fe=ZJ~X?q0=>3qlBQWL7j-pMRtiBcvrhjK^ix5Gl>r!G_@vx zN3$t;7Wtt+&!7m>cvGwNCb{6Pcvc1>%}R{6#yo7ZYza0GWp39F(=q>>dWkChp^0Cw zQvpD&lj3d~w9+UO;@~6PfN*X}$w9^7a+S!;MjuX+%4U*S9La;yAu9P|leQYovZ){tC(5&bLuRf%vk)CjwzZJs|4oZv0W?AJ+0ri)EB7t3?ZjNdCl&c;<5)C!0pFI0whE^CduKfS**pwtsnKP2=_!wLhpWG_UNLn^c+G$X-kEZ-dLqzo zzbk#4FhjT75Fdxi+CAHXs+YpEFW_;gCtvxmh{aqW?`CQ8F@iBXvQQXK z=h`FMgllkOFQJ&XyymVak!&n8Zr+$WwuJypf%|Po?Cue)V?B}qN8kDA;6IMTh2UW& z4=CFbGb)FhNJ8G(@`6&of*c<5$T5I)b@B(XL%Z-D^*(*y?yJMgM#%7KZ;T2FLoQ{-#6Nppxbv52QqoncmjQKfH}aJkk)P&j|<2e0Y74_ zOr!qW9Y(mH67FYd(}(aX&V14zZ{{6Elomh>Vf7pkMV$o)=`&i9?00->>S&TKZ!D>t zYuLTZy)FsfPXvmkU}`P<_)ou)uz6RrKXC8|-G`*q8{MD(!7cuBA3<~c$_cOjFGkwG zT`9H6lmYwy=HBT6zQFGixv+tQ|KaS6 zFW>OkTJ&FC_GnVru(omlrKVHg)9BRdIP{!pM3?NXPv3L_4cIxQToE^XY_#x|VWsKB zV9`@jC!l_Ys8EMB{vuFDH9wvetN)&1IfIKy=UXuOocv~D+*~)X-ALDgf*luH`8?k! zCb2s=IbHTWuFo6S8FQ;mM9ikd{tSGp+->}Tg|jk8TW4q4mTCB*KuNMUf`TcbOc)HR za*4#9KVxkkn92oJqAJyJB454si^y3`Z?x|BS|Mgy-vP0jGh8av0Pi5%W+=E|d1Cso z8)smgSP*X1*Bh9aJg0Lo3bs5Pw}Qt|Jws9mzkef3KE6Q2`>r`&ckfbJU&CLy-XW8N zT?(tab;w!+a^!3&N3hh8>idG>iLyaW7JVr#1b$ej{ilHb`eII+bTM54p`JLR~A zKo+rNUY#$bJ>yL2^|@Yf+`Z&yXpwaJDr0F0GY&)dbyAgc2|1^E_nA;jW=+&9xay~> zp$MZ+%9R&&EO+&JTHYEQA4~Urb)3cPvjar1GO_xISArvfMDSSd0PJw-I3C$y?!4p$ z`7|LJ@b)LePh}s_v;GC*Ff}Xa2ez=p7_O{X6j9y@O5gzmaCoPv`Gp<6l|?aauj9~b zD&LI|<$jC`HYoV+bhS?i+T*jI;(*I@k5vNVhA*5QqEvnavxhD~4p-Vhu6vIe#kQX- zFqyL)vAqGyW}zhef;o^s?LJZS?eKOcDYFetcXM<}5K_=o`VO$hUK1i9&f9lGWYXEo zi=IX78>w|cm{9+SLmDui${S*hd_9ky?F^0Fg$wsGL&DF0^Pj1ZBTd^$+t)4o=D%;* zqEi2>!vD&PLj@+Q8Gi9~Q9kX<^=L)aGw#g|#qMIy4GD)M2mB+P^PQ*q&&r7_BMu-TwfHwYqQ&u@L_Q>Is_?@uXl0|Mr&zm`nc#-soIHgDGB zC-!Bf>!dcT%o3J2#bd{^u-6+V5I4RT!)3_Lgb0vb7h~F)A!wWT#FIogOT{%XY{{9t zrv;A1RRPPeEYr&2#vgsNsa9Yd*hwb!6ucdrPHj;Ab8!<(7{x$euAh?wXr0 zXXbB@DY6!A>~5MEa@iG4`ufWAPBMtPdUr_iB-R?bXfm zYCQZV(vG=4UADmq`60f5Kdj;36`O@_P%83$)`17gq)C2tnJSPa9;nWFVkQCo3cAe< zhmz@UKSm_M&fP9v6hX1=lfUda?7A%_VUX!6ANkR9qZfscr(7{retM|O*3kdN2W71v zvs59ar$fC&r+y+AoJp~mo#sf-CHGrc7G?%EL3Ssm!6?Kx=|_>u%rn{fLD5%X_YS@g z6n@Ru`4nk(1+lEBP!gQlV(>+(7>#)!HnjQ`B8=KXHYmuq8DApUis23WdW?TB2xk$S zOb-iL^ra8b9p#hD`6L|b>ohta`U`~F+m03D=q}fZy2La=z9_*V*FW2x{U^JSc6bL> z_p25u|Em_i`tCqC67GNc?ib(|G106Ck0!;9CWK6`AKV%Ov9b&bYC;2GPZ=^FxX+Q! zB~G2r!d|YX>^$hN*_2q)!$G&iqYl8{1Q#IdqA}6JTu9s_(!ZWd+N?9YO6+ter`0tU zLgc1g=rC@^Nrdbrrxn~wsd<-;GOblst#C&V;@9eo{KJUO3Izt8YeP=!3{A^cfZYt> zTv|x{-b(<)Duic;(jzy)vS42{%*F=nkEFEt&n-)<>TZ+}0{*}ciYyf6zZeT-mRpp;F2|f)kNWA+=SdEMGnl2lGXDS=Br1jb*8Nk(N{X+1BqAv@oCFf1 zS12O)w3Zcz3oOI@PE8g(j4^&oCZOhLb7ofTl4c7WQQ?#det;_`G6s{AU>LkJPEsZV z!Vw=aq6Y>$x)nO7@a77ybkEH2ih zInXo}oRg`f{SI}Pt3x`6vO}2*iv}!6J*B7QkI6X1K|`l*Pl>l#3Ji<&O3^A;aG6H_ zs3rqu)J5m}eVbP1#GL@h2HMa}`Bd;0S_2S*G@HJ+Q7WfPezTKeAD&T>kJ!1}eVSAm ztL9IP(5Bmp)esM_WL?|mH3-!}Z8Zrcoc_3Mz?7S1L{zNN*cf-T&J4?bOL<`sBvgLE zn2N=;!mh_BY)X%t`c_D!d2wfVXN-J%n5PFk=OZr zbdbj>o=5!2kX%0@mo}5+b>F)f1xfZrw})iQf6^*bGHx6VF$yv4OEa;@V1l;#EA9IZ zx`}%iv^yiE`K)u$Nb#o9%~DO8c(+8qY~tD*l=yUpCtQMWk$rV9bZ3V>G>D6U7J3b_ z*w%zjnGhe?pUEn{F@GIW7kKdtY+8VnYC)4)?`j@@6`3on*k z86(WtlUFseQ?-qPYIN03T-HfVpz9nXuxoEP5W{eWSiy_#@l)1nbi!FveV@qOp`Gkx zQ4Jms5W8r1+rnCJxCY-)WjL2gAL;hOvMwSf&1A1qOp^@nN-4D4URqFuN)Lf78?p+D zU(4CBnn#QIL~6IBFL0X9GcYt}eB9l3G?!~41FP5(?nul|wiu&OIZ;71e-Vp!4aF(Y zxc^HI(FX%?rNXAvlfqe2^A!4BOeFnwE5aV$3x45DX9xrS2KDXSaGDYN3WPfrCZ5J z@Hu^nKLih!(*zIv^d6$voj2I0r-DkY46eYCU|3#aa}bMu!GWv~S++YGX#^8b_Kkq{ z8(4%tyX`urz#r29~WV_yeWc038sC#8oR2Py z?X;13@WAmdyZ4U339eD6@+lv{<33=P8XJAhjD8rom)=#Yzo**mQQmt=v4j+fifrI+G}x1P!muXk38)Ah zQTzV3@q^)NdVd>Cn$!i9J^ZeB0XY=sDov*?;6^oLo3EeNtBm@o*!qdwF$6T>Vp=Ht z>-fPigmo-N|7W#}o=}W;TgXRHF!Cgf>9G%)n->1Nt-sq&POIq?xIxn>U02ed7tRJB z4CVv%F4I=nRDaqRrm3C5SDx?+;MXV-29~HjVAA(%$t8yPU!%qU$8(tIkxLBu&%YDL z|Hn#|IGsxar0NTc>-cAwkd?w}sF^d2EDQva5N%ql5XUhh`BlV@9#L&yy@lP`x_s5S z6?t;MTQ7XSYUr!@nU}M0*^^2-5*H`3NAZWv_1g2=bLa8O_s^?v?;E@kvxThk4>33< zTnQSBRbo~uh(Zu6o_h9V8(VP{&T)|m%+q&2&Pzl$V1%4>?C_<=B+)Fx6qh_`-$G&K z0t+PR=tWwVljcrSBN3|3j$?yE6NFERCTpG~YEgESqoIeaCAk?@2DYKLB$8=T{?1|K z6kG8S-)4fuzUYfTkRFAsG%CN;z#)FZB+5O3TV(A1EnMnSMJ-PyP-;Y9E1vm)P$BR( zro=l67-?rJwuGzUGdRFkaa#COXk+nyj>NJBzLHXHyi5`?h+DOxJHo!QWEu{XOv;&I zq%F&kO%2FwxN!qO%1l>kjNnz7J87eaY-V)hb_aXQwm^Wm2<)VLl|g@3NsJ4l6#P{1 z*c2o3bz2HHtlMjgHcyDQaw6F5B)fwd*}4dTPPSBi9-IU3zr2R1VljLGn2;HFt1NP1 zbxI)OXpVMqI8Np8KTCw3J5=O9E-A{jI2_!+69Ial-o;<)DmL5$3Do^yJY-iXO}QW9 zaQ-03H<}K$nY1uu*^psak45|9q@_x2P?LvQD6Xg_hv(pk=ch*6vFn%-^ofFGD^yy6 zHfz7@R0SgY58SD^9c<@BvhSMCytYc_3TKViFIbhnQTBC^WRp8v5eM7j_Jp{!MY=)3zd8X@_LN~WgOA9zU zSVdC*c5#(@*=yX)~Cm(d>cH;9n&ou?_Gn@FcA}JkZ?sRkiVlg114tOd}U4`r0(_zdo%{bT^qB2dV2y% zl!KC4`I5mCZ7(=6<__fAD>hf#1LDFArg$gi+F`S23UQ=ayf!5c>4tq$=6Zdm6jrEC z2InTH69lZLiIdES=xj{;6$hIBpgt-JNQvv2QvYrCQ ziS6ySdlhe!}TcQz9}wz_}(nF)tRmE zqBfTUMy}EPwJP(xlr^J_$tfPgZ*&&W6J`9+pJWu`)#7`sWt~IE%gdmwS3Ys0_{ms| zyr|h)q^YKhkD=ROL^g)=#^kYYsc5c{Buc!2(oJ2P?{+_CKy~`5gGrS@xr=qqH6;?j z2G+_X4$`q_o-$XM znZP`$g4ZsO$>5(4O{5X>o%qvNw0HRYxDT-LliF@Dk8BW>S459o$g>4o)#w6;PA*Gg zY!MD{;+^tSK^nj=vud)pj~-WvMnxOPyie&f{bX_QkNe#tUq^?3j(MZjw8cCc0A>Fn z335w@gFC;Qp`hIvw>FOR0KYkCIkPm8Mw-Q6&1rC?~!VLMLCHl+#|+}ebKSR_wlKn}p6 z(|Z94_5DrHcs%jLXXx!B)OuMBk`HB8-pdZ25t?-rZ!U z3#Ey{5YKQu6D+dB{&PJAOrAmBNg<_RSUvIZ8DWIKhkq$hk$J(XzXyWh<%(fKt5WD> z%a`L_@A(Iw?CI0oV9cd$t7ZQ{pKbzbJ_Cb1Fem*!bZ0GC`z`zeJRnBob-G+ByhCJp zf@J}vu=#YjweZBd;>v+^rI4DC(3M4`gf-cHFa#wkfkrQM%hF5-+9{jnevle-vJ65c zmZ?L*6k16yZ_%wN=^&fx0Du)Faoxl3w6&{)5b=JkcAfQX&|7KrGCXBXX9p$ zyE%KQOX3dKP}+|+_ewf;p-2Z?ap=9OE)D^W1RTrxetH?V6#IK@3!U)J@H+1Jf82hc z|G+h)gv7zJ(I8iCt?I~st}Az_JWtvBz6JP9_x{$})xP9*LeN^k-vP8;D@5=SXD8fZ z%TCa5+GN4`1|{mo0=qQKHmH(M*rCltpn`Hd@a5VK&4$PLhEp^*p|?j{`*+vdH)1@D zjEb}%58oMNG*u@boll)k=Y<(q17yFU_%JKmXD2&u!7(kvLEkp}Z3 z(c+B2633nN8;?<|+T)?7KU308>)LW;TZwWvn>5x}tSK$32We<1I;Y!cK6~|7luJr9 zyBqwfrOB`nKbTQmG!J96y#h5qXf=l)*l>AXIh;c7H5hQKw*swIE8^tUtw!BTCv*#~ zLP%9SmewQ9-6w0&GOBSuBn9|e^cyd&uZfw8^Ix+Nn zT!V|Bi@gM6q{evXCo zbF4FNC;uE}t`&&Es&||AwuZg`xJL(01dNEQop4SO>Pr`AnMQsaR+`u6H6ab**u&8m z)$tlHcM~J=GD!>u&rc%4-yH3m7EYqjJJ9!%-TS8*;=1FiIBG#K2(GvIwP#q`2YY~C zdfzr|0AJ`UPUo6&9o6tFPUl-5`=|+_`+&p2ng-(usTJ6aC-Qp;x)T=)~8Qf$YCzQ2!Tl>mP@&AEu6}Qz76Wpn8y1CYB5yYy#J+ zO>=4{rCt|vx-WN>a6j=bHt?t6IiPtMIfj@|)xD(W9UK_$^25Bwk zCWttUWl5+~3s`b;OX9xN>HwK_Esy;SkKAT?$%ErF{Kbi9V%el4M6+W0DKx}M;YRRt z+DT>zbvl|Mi!Wl)0vBp`x z1xVtgAceojh}E)SA|S!tD?3%Q7IF_I7aPl?=exmyW^On?PX zi6*yR?+9TK|7J0yIOzL3#)GYN|1gu@{G|Og1H(}T1)L*-1A-Jr; zNxbQEAd3hhUkA4K4P>pHV3YfT_1zu(pmfKqK_zB>TX)3)r@Trf;nmV`#kyArB-4HZ zh}*3zyj5qTr86c{E!K*fVU+rI67h;PX~AC0d&fTQXZyvCo~PsS52o_p-(2AxZPS)- zk+$udPj_qP>F^Z#om#SGyuG7MTwVp!H|{vQfd%BTm4Kjg539)Tl!arZ)DLe>x600) zVq=i8(Tx7zOP#>&J8d`vqqOJR?+txBKa#%EeqLs$n@mb3P$n0SDeGlhMo%)Y*jN*{ z%y8(QQ_j?;`cnR+uv5*Tf|>{So35Rxp%Ps6s$QC*)-9N@1xVh~N~KFbqDKINC5s90 zz)Q{6UNZUf1>VN|=0Ksf^S`- z@()|MV!Ss|gtjmSo{&Y_n{WmDDbzUqb(-;*wNKS~I{&P)8&Fn zfHc*Sx>61e&Q_$v%MssH&4PgBfsw~PP)O?mtm)#({~lPaB6m-a_s=ft{?pG3Vpgd} z)R$-#mRPnt1~}Ay@U`2pfdpbILuMb@EMZt(tO_&kJ}xIWh#5t@eaJy712YoKPZl;q zzi{^Xw>MI%#Wr*?Aof+lHL1ZTJZnq19){dnxGS>Lv#(xfDHdgWjA-Ery3Y0t&`su3 zjO;Cf{wdl@fRwD4{_0?Z!=wHGkadpTnT6f5j-3vl*tTtSY}>YtC$??dwv7%t>DYG1 z*6EG0Kb-eJtZ~m(RdZgYaU*~cS<*t&Aza-d^#u8|jVkqvM2s!Qs)J&j8gYppw(TU= z-iKXRRzHnWOJ+$5ERhM8L<*sQ@LydA{`1PA(s`j+ z5z@txz$k%nSg_J$nU(=`iJXVZ48^Oht-a)TY9C;I^O$c?sB#}5{!|B@S1>V&l1z=<2OF`0Do}mJ9lJ?t&=N&GFJNk-+ebQQXC#+BPa^=EcB!qmph( zr1H3RMfAtbz#$*nrR4gEC2j@wYVyzrqmk0Bk-_K?)i@L*OhUjALZIwdCtX~_;Sl;Sn?c2Tk{R^uX3BuAPQNXZ(cmaOJQgEE7nUS%gSS96&T)PzZ zpjk~nl8-pnnhYTg$=VjyFynjwaZYLUiaQp;7*K^rX;q&J7q^<@`{L(Y73LQVCh`Qv z81WA9_=Vu{j35|VRb zz^jKVy?XZ4{n)(iaEH33kSy3oB+~ndKv$MToV&{|I|hRGtP=sv{By00NzNqBRx3+z zx#@pwjFCaBul|37%Jn}6Rr>!$GrRDt@agg&EKup^5MU0#CEGX{TsR1HX(|!s;iLir zccim7MM8K`d1K}}`Uzh|81=Zk%?E~_wDP3bF`T2@|Jh{q7w51&r6 z3UwXWRxBWZlak$QfyybcROw9600bFIt7KiSy8;5l=`gAB-c+=UKN9HlW4oJw`nq<@%@Tamm*mEC{F4It z(me}9#5w{}*j(#o-Yiuq4A7UCp1mOcFnQiJs?~-Yi~f!Y;FErsVRfrV1t`U8!(tjS zi#kRB`$?l6p86#)dpRHw-`*!9gfD8Ne^-M9m8vOQdfEaLr5OapAWb@~_9~fxnBsz` z(bm}~6J5T+0z17&k^eltG#`L!)cO$HdwIJ~fs4Q7F zljNszgmQqRki|ClODajK!zpxDPDmQ$FRPQ_I^f)QjMvAdkZjmvOI1Cbf}j;6r^0ZY zoOVqByc@mjuj)_U_)Z*7PlRG{qzD!ufhyKn)D|_Z-&XRzLwRKQO@oLI3DcXa_p%(o znb&Xpvi82J9!2kz1DW}Eumn1RZn+mM(3Q)`!q;N9kzcFQkj%~?r0fR7jCh$*Ohugn zTdo_H7tpHWo&{gT68iFv!hS=ImOo~JzRsCjJHOj~(05{*gdi4>o1a5Sz6mPyfb)_w z`S}KUUj6fUGgzIZ(9uKV_t0YVNo8H%-*NI^@mZ&$f{oB|F-WtrF^E5A5fJ5njl|*M z$HdZJ(rKTprQZyKVt@vuLl1%C11FP&NaZuI#-u{m>0GLAEE@xjcAje1QnoZoL$RWV z*KahpVJ&_O^a|naII#w&?>_}?VqvMR_Hk%P=ccwm^UodWbBD50^pRmBsnC(Sb4+^} z#Te?g&4-|;#7@%VWM({XMrV0Gm!up?p{cyEL~j-)yM2JtPD>J8M5U@!1AEYO)i3T2 zID@OgDfK9gc4^_B9m4)GyOQAfqqUmdX>4|-mN(fG0#j0F!kB}#(45HbR2*pb9I@x3 zAqz0m6}N~+xkL4A>Ls>rb$tcCoBmxbjrG7v{AdzK5myW*&%pf2L_JfIle`425MwIa zv8(i378BYzZ@ic8qfC_IDS!o;v|Q?yrqf72JbM{^Z5#CsD%c9ae1r7lwRv(H2O zbpm*+mT9rit}v1^-vXTPPDBa~{(}<@3=E0Gesp8)!iJtc?JA zM6OP-FGv;lEFs(8+#Yx9GhYb$6xheW$)dI(($NTfSo5 z_K-D9t4=eg=(bFku*%m)Wx`nda!pwsC2==5)a06%_$SdU0wg+Iv*pJKW=btG?JC#c zYMD9hODX8*5>E^pfXn$FSOv)*bsqa?`{>H!Q7+Yo%0Ix89O@)X&HS80OTx};#FzM% zT(SVTiB3CF23U<4U(4`X$C=pF&r9RCZ3>c(ly%c9qONA%LLD znxQze+;!}3b*k|?K^M4iuGpEU#=vgId2KF)_<5^(^P8guXa$Mny4`$gk>2*UIx_{>Pq)X{ zJ~}{y6WmpS5jgsms(zcDT5KviOQnfUj8c)Js&Wm_Wo@ ztX^%3aXjP!YNs{wFEq#G;{DAye&3p2bx|wsue?2f&EI{)zVk%gFgg(UWyF}|8ST(^ z;&f~B0Wt3}6y7au?QQtURL>q%kp|cChzG8WW8(2;2TB@}zi;v82O~Jn_)>o;OZ$Sl zaKGUrAfBroo!s~6^P^)LDQM8Dv`}l^el1X4c05aXlj0_0~`n7VIdW_#bf!4ks#@tR3P~BswSU;V-WfK@le9(+EKs59U%q3_9qA{L|FHK4$Ge^#=`TK>HphK{JE;F`T<)6iN!}50-4W zsJK9ga!fONB@uU zY?uj;n~a=8lbq*^52}{jj#HM_X%J0%*+X{LM2+cIPMwBuz~X@0T#f33)8SXYKZn~Ko#AhsLxv56h!sU!g~QNgpN!jGbN||S$2{3l_Ex2w>EFtO!W4rYNI9KaQOMpR zLq&Ro7J-V~E)4nic{zeLH{h5p(tKdmEd zP8eg_gV7_#@uaSBMK6?WWEKI2D&iBI@d{o46--gp#jH+{{tZG8eb+^-B91~FjSZA? z2_Yd*^9?;ENLhio76x#nQrtSo?K5wZF%HmcQ5DQo7&^(}`5Umr7F;SzoPw`Li4={d z5=CMtaL@*A9G-B|pOioLm)DFy9)Vy!7f$RdD?*(UGhB;qe3GLjBfj#9D4f$Kkzqch zbH%|%dYqDyIifzB0Y{Z76LbH*|Mw2$#aoitpMTP$_&=qGQ69p7DP8jPzIRYq(B$-q zcTj0y=Jo09X6D6(gP;Hd2v?vCq-%kSL|h^K7S0p2aa~H=zB0wnlr;NykIw?OYf5Sb z>Aa5NgFlMS+%efWUDt0)(d9TlkbZ1cFqoWW@_8q$C>d&kmKkZ*<)Ro<>_?NN@1RcR zJL-6T~5SXNXI$q4BC<( zDlJN*CQclFBH)_c`TA8IYn55V%6PC2W1O8oQgMEP$n6{p zi-TGOQ^gG1s-{*I5?e%3tTc4tjBQ#0XXNfj?#oG?y{k!Xio^0lk}zjd?T?{7PGcFJ z2LZKYyQzvIJA`u1l4a$!wOkQDamHAa`LY3W_y>O)r?0qpx`r2RMM_-Z&BvJ)eoZK- zm88|-LX2bj~suAF0BqgjGUw%2qtBQ!pRsT1&O$Kj*|{Wvct*i zE$^KsWp8b|3<>;J1Kdu>L7o!+jxxn1#}&jlDZ{&Uln~gH5ZM_MP+Nt)dkVcF$KQrL zQ0MJ&pTYMG=`47>#4wiRa!DMio~w(!$0_0Po^mV9QN7ilqcgw8559f};4xHeO<@I- zC5y{&Q_z(3vRw>_tzvR4(3Ti9o>2amr}L$IhxqyLOE>tBowIZ!Kj{DQbpGipwExvt zGup7e|2V4O`IDyGbc}YASwkrpT_GH_qST0XsBm^!2`yUE3!x#UewkiUWx2m(EzZCg zh~PT>NJ!vF@=3JQ!CD8{i0RbO(W`^N;3DXX=tw4%F$?~pW3oXH!T;Rh4^n_hJ01;S#QIIl62A&h&G88^1z#PHj9ncGOv)= zIL#5l%UWu7J@n|)E(^IW?=KpC6%EXDq<3)bUxaY>hQc=Jc%qg$aP6aK9{Bq7Sbs?6 z(T&4`NC2!(bVhyHg4(OsTBLOl5_HX5gfU|+1?i*qIU%dZBIc;IP9%i(Mu%D8W3&aEP$jlcZqbxC#W ze~M90!Hm0(e}Wox6a28$?=?VzEAMJFt*MqBR1t6F$DWm@%mDtG-8`k2dZJZT-8VHi zfDZ@nW?eI2C3x35O^eej+eIIWH1^a`zFG3G<;02_``G3&@r&?VC1o;(-GjK#ocXep zO`okZtc;?`nYBcTWA&tywPfbZ{Dj&IR8K2GAK$J8b*sY*F)Ub8+fGAqt|4hyMGguHGQ z!yW=A@YQr+JJw7!7!_Kq30&JjN$58+V!0R$n}0iO<~G8MRxBbQXw`FfY$#gL;06ldpOv4!vv= z9285#v8Ow9=_(=Rw1RgxwM&d%XQSWQS6JI?=Aw5$6&E*ipq=ng<>c&`YnTpPh%UsJQ&j;I^uBAnrj3?!=|qh$3S zZxkJr2Xr_1pm&r!m_hj8Y6BAh#ut_w^ZMh=tiUrw%%s>Mc&ycPkgcT(b9#S7h6xEcI8Z)|zUTMXGi!tX+d zc;JQZ&L3AdH#tcIutMRmN`ufj+79heOh-hEv`Ko>UkSm*&YVV8ld4go&~>Bf=)PK` zZXJ0IFW@RfT(JRYLUTVj8;$roghw@;NR71X32>!4@CqTxORXNmLu$bo&ic#tFFGP` z4VDpg88e6U+cB2-FG#xP<~&R9#W@eM;edUm#z=i~noOYb6oWC?4UG`Yg}cY7tS!@h zHkE5EgHs-AzDPc>QG(h#gmI1N<5~7f>qjHC9dd&LfSqywp`;qFRigUL0j7th;D?_g zp9^Lz=snB6bH~(|gUVZhyZ|zYE&|E7y3U}`l@d4(k+l#hT0aH{CweHg%}`=@W}ti8 z-Vf_-t~h~xPWjDXAHZh$K9Rpr2J4>f?>rbY#}PDhb1owl=bo|9_ghj(LG@b@0#glq z-q6^MaxoojLsNq_a`g|@QPCG{OnqT)zTy&B+scaFlN{xypSczJz{>w>3jGAHE*jn; z{Bc^ehHR8ie?Bv1Dtx;_LojM00{o~*lz`(0s;Upt`415ykwPkV|D z=BxEY^*02NvSwNmcANFLMPpN2CAk=k8vuF=_vLy<3wThVTF!aFzY2sVg0?#dVma>R z!e^x>Zr(!=G!!!lJg|T&Hy(iR0YTPTvl!Tzeo+2ZT5Nh01cI?{n(bi}rN*&tl^{6$ z29Sfw!O-FwMv_#zl;J&Fpg^pj0EV<(Hg+26rgEJ$hjIp^{wV*C!U|K{54Z_|pFdJ_ zlxa|MnCVevI#ZLmCsi!7q!+34vv=4)j;%{HJj0qq;YS|Z2tWm}gUBuZa8(At0kR5} zQD$U=Mj>LEi%U6?_Z{uwFD_Z^uo;F6tH~EL&n%bS`^MzQQFe>8fUZ3v&7H|5+-1Z} zU0NqaePpyw_v-6nlvQl$$Bki{{U;5~AN>9d?|$A)6m^=BSINSIm-EZJE8u;p`iwr@ z)qbD=TGu3Lcq^2`JMdZ#N1oha*ygkV=j{g77yGrJurjjfF=`d6Qk1yef3Ei#1aVS{ z+^0&S1`+{1XVk`vz|oK>q^4PUnGP}x7kP~3eO);jwW^{;L_@uDuXd+zOVf%_E56eF z##qJVC#T=egHmqmnfouzSE@1vEQL(~np`Q1Mw*_Y0i?0>Jr5cYY}b}{Oeqc)FQKsfgJ#eP$oGVX6GL6?QsKQp;(USg@-dxPq4PQl5p=Smgn@LK2Dy2P}4rb(eS zR1xUwQ;^@5Vd{}8y1AvdDnbogf;9aI9uHvFqBNmayu4KyW>j_;i7jj6@sm^{rtK+2F13a!#X-~NT~ux*z+`Q8T}F&fOHMz@Ql#jp9|~ zA1ZGp()md#`+GMqV=0cFn_CGKB;@@BDD!cyL|Y7c5--kt4VfnX}u-hSEhlh8`nRgW<17#3sPM!z2x zurHrjplO(0&&kdXS+9nnG_NQ;kx55c+t<`-X6qkhgrp{XQiW#5Op^wXt61WC)>qt0 zM>d#Ci)8iY88><9270wO?oa^E@WKEXS;P2Zz!?h4H1&SuU!ytF4M=v^3fruT?dU*I>H1RmZ!Bi5^83?!0|#bxhA} zr!c1BOPIhyp2a|qa20u9bB@t}U+ebG#u*-d~h-lane z;3e&B*Mhb~dfv96GMb5qszrj^Td4$GKW1HWEz^-bN75~iZIMRM@HlA*Waf(iTOs8&tBA~gCJhn^!gwTC!&wTy_m-|d-cLlnkskO;fhv@N8OgNTS{9us#o+nrtyG2&l2D%%{ap7G zu~8=A;>dbE!p^0jLymn@bmK8G%`(rW)`K+_#L|30|A4VyqMoij$T2R&(*i3}3)ER?5m=r9_^bRMZVfow%= zF*Fa*LuKEpda6AkiKu!TjMxXeiH4gIMu-Pej4;BF%uMeTi2A|zRG1A zr)HGBB0%+1wbU<>dM7E?R}U0jiFg`?FrBZ)P|$HFf!;CPzP%F8Gy2eN^)sf=SonGr z6Kwpta;=N&C#zqj%gHn?x}Rqr!0?4n%FYSoDkbkW-T8!Soq0eur;Nuo?Tit=1!4omctA+onjzQ!8}UaF9_C7YV16l zJr1ko7j`^OyfNCR9;I?S>$g30G+F#grUvN;V%g~jCHKkS`c;~EA;}bGK%LQj=`VN7 zkII;mG1Fhc@E8$-ywU0KxDkE7JP0D{kSFMQLEZ!mG*5ZHS|$fuUHO4kV-Rm}TLU{k z-e|V>wTy55DNK*9k3T`ft$czTd+V3FW@lHc4I)aS1s;eu4jXTiLR#)_Z zQ{H_Cfzsg!y$a~(kaFt60HJ{bgPZ)3R-SN>o;a*G2*>+cbVDv&fH@QDykvw_?Cgcu zhkoU}(FgW79#(F|BUd!P;G-OP&Mny;)AgNwY=(K1TOVPu!Pb^==o-FPb*@ndDnzMM zue6^`j`zrXbd&pOO+hNOs9do}TK~{2gPE{3De2YlI4{mHvAJf;z)wvVkc+BX`@VB5 zAq%kVXG3qdzsL{r9)Cd}wmCy;b0GskA)ZOq{XsC*PFNC=x?Rv-Rw??rde)EIQgv#V zAq4blJ5nJ8RB4i`W0QA?@<63={V`bk;i)y7WWPHlXTjmfm$S zB?}sus`ZSZ_Qq&=0wF2))bbv2=6sSZ`r>$c0x{Zjp|{7(d%y*^#bB<9<48Lq=sH52 z9l5ofAdmKOn)aZY4k(hw%KZ>-e>nmJ0Hm5-R-S};N;sj>Z zd%)u#`OK~LK8;JuOI#VwP6e%|`e%5pQ3+Q247_D_+b8a_qmXEZ&!IKgiz2pazgRXe$h z$Pp8ipDBV4v*4d-4)Mbr_J&wQ&nn&hBCSw#jF|C)z+D>(k2h{(`YST?uX{>?dCKSBF(yXON+O|+3uIVbXFhxq-YA(>O@@k?5v?ocy zI7ljb1@R4m!=0&GRp?%9H$mh+ftW|t3u}*2w3OyWK;FYwQV3hz_|0xRx4R{%YZE!p zjL3=Jjdgjf#;8pk)G7qVVl;2$M;%zf9?vWP0Ak-B%$Ze`A8}<{|CYBfJCus~3VZ5rsWRcc~RMytSz@HMoVP_K);=4wS+YSp;Sv7Pfv90Q2 zSe2Yj@CR|j(Hoj{lvi6XKnPm+W=#8=Wf)STVUl407S=_qCp*Kn(OT;0P6#wnTfT_s zfvYQgKTkZGo3En`!Z9NFQ$5JdYE?lG~$l64!k zcmeuX=>0cxIJYpYd;1p9&=V^xKfh+1z*}nB%cryjBy$R&sYwLfQ5J?i&s94X?Ts#l zyEJt%nfUG>B_I&+>e?{1uxrj1f=eh)foM8acw?Wbl|B|`iNzCVZIq{r*&`<>b>qLH zNWvpb%2CFHAH3i_0mCC3Rx*nfZ@xNJH9m3nwX*Z+;?Qs0zU`5Zjto9hl+fOo>Wf~Q z5+^3`dI3y&5z$tx48`<0fB#(q=p!ULU`=SSB77tfm;<%K1-wJ-?8h;}A`F}+fV{}! zq@P*!IhteCdrIE2VIRU_4y1=jQw7I3FdmBs54dfT1oq`S3uw0WzL&+6UbQH9Br>=@ z{G&eDfBY>JlZo?-Ze17Z*C9IQ!sSM|NI~>!6Y{~o{dcK~9AgR8E!#yP`J~Cr(Q$W~ z_)M^H{O2zDNbB=TPfSyEwCv^A z3Qw0Fr~KYK#{=qEKqnsS!sg8ukZ?MTx=8uGYrN{<<-7LOYY&w_Nj5HydWjB3iD!`B z+eHJ$jXF6PfWN^1cAfQJ*tS{GKtO0&{{JxL-~A^GD%elph6|b-`d3@0El!&L0z7(x zjFn7NFC9^Y_P{a~8#%qCO(57ghhCCh@>WwfWY5ihG*aKoK6nq;EY}+d&996Jt~g)- zFy(-|-!>gN%@~_7YiG@1?m725hyUbq=lkOpGXSk4`zJgJOA(xLgv_btZ-+B&t@fz( z*HS?7u2cjtpN)Qzm!3Fjmac^)P#s$ea-%>Ji*^-IYq>Gnv_}}zZ^R4OT7+0)`#h{^ zb#pM>)=x!dtu;G{gP*|7$5NcbVAwstKC>9A*qs!4S=OQyz`HEY%+RW%_6SuS+lHSKyb)un?f zsMW2^Os8EsAThKyCVOFjkohrPd3wc$8cPXmppUf5Nlw&Y3f6*^XpgNFF2Yb2an9j9%bi;;W5SlB63D6y7#~Fylq$=di^%4k( zi6=v_U%o}8-FN0`w-V#9&ez#o=rG5qm}!1uK~(n1JGOG~Z8GJ$e^XxDjWxl1xC;XQ z&G@)95bsTr+VbM+ZzboHf26|Euj@NCWx3F)&8@kgyX>@cbs03!V>^FH0q{nrMV_&o zH0i;ZJMbcE8M%oY+%Nq$hxsZmW!0&Iw2R>O*`mg>kS()@e~4#}dzizi<238eLpTRM z_Ma$J^I2$xC70Ij-hD+WDq^>jAvgeq^ATt^1Jl7Opg=>CJlh1#eD_>z*PXd!FKQrB z>qNz)1g#&mWN$U3a`e8|E_R+@DgER|qzsUFHqIEs;vTAaY61uM_@ksZE?q z0ydU@)9@_)Gw8Og6>6gJtrdEr^sMuBN4LihcffM}!{?>@Wg?1%mRZ&f6QY2)=$#RM zR7S`YVT~dL1Ebz}ce+p^qi^I!PEhni`*DoRBO#lgQ8V`fXTm$U8vh&^(#!-c%A^a;Vpr-1y}Y3 zv7`$aC_Ng%P+vI-mma!)K^`h9d_wGp$|n~Kj3vh%?Jwqe?;){BuemD?io>bph&zlF zAD~O;#51>Ze`xyWpqH|Bh-=7sBEq}I;rK-YOR6{7Tdu?kX?(iS+-@ zIbi=czv$(IH&i)VI>{;Nwepq`-Dp9ag9Attq9en#L&!Bd5>g-T}qp*Y&} z-8bRMR|&_+=PJE#tfVT8Kx*SY$;ob#%l&(He0*$jo);wa?ee!*YLSCpYRli&x&M9Z znz#Pi_vCTwWoptQ4>}-|A{hW93zq>@lMFHqS=9YXTaferrl{ZO(B0iUw|#1FRWyy61l^RpF_)uHl)dDnS#TRR2rxfp{am*Rl4t7+kZ~AcZJ$4onGH$=&zqddwB5! zg;;7`yINO1L0WG0>4iaqvxbnXTshlN)$YFqYp2y@t^}ro4I|s~Zy^qH#5(|0;8qPg zz_4jdMKDD64Dl>o(r?lSqqtAEXVYV{`h;j}5)irJ4y$$pP)a z%qm4QQ2UQ2R??Q>rgwpOkZINPAhh1_?z^v#SHfFSU4BJ)Fw;I(8n(sN2N$(0;PIxy zwLwd8In^ShHNsr{by_uz*_(qm#|wuV^a0D2js!V#Nk`?h#t}@$OpAuWaPnH%qi7Y@ zR)~KS&KLk$ArYqa8|~B`TBgf2Llh_(j3u!R}IDflsn! ze$&#Kokw)(G($z4xI>KLhrgxTt_WZ~pnZHb&`oAhkvUf%IItC>NNATw^GRPj%c0aA zwb~nORST6#Z53V%XRBhwdnv?a0+St9T2DO*8LUNsuR}M--D0=?!$4`zxC4S%YU2T> zG}6v%Mp3V9=m_Wt`F=(qGgVc+mXs6{s8>0p=Ex3axAz&jwD70~s0+Q&^lGcQu_@3R zvVq>hY;0p_)`4HCb_CyyNTA4?$+Hg3wrDU%A02Nq*A*LeP~XE)a)KYOd}8lxhriKL z>p--(U|$S;X7zMRnEd%SC>ziLZscst8XE8e;12ImH3;1LFy}~8@4)@5w5xv$`Rud{ z$T#3kZH*(qRjN>31xKQ~kz@WuB0;#g`Z6U%LWa8vTDn`R9BThrMJ?O^H@fx)hOcVC zq^YrQONYV<*o;cEBGX z;z-n&Y6Wy`#+uy0<0Qx{2&IB1m-K-e#3yBn#*`u0?Wgg z@0E*Hc_`69#wS3bCe2aF#AUN z=dH%8c>?@V^Et%jEQWV&t+N={=88bEWE1kxJ%Nb&8oLp2g2DN?zaA%8n z&HQcS@KA|A&zb3-qRnDO9ftv$XaRA8PebXtkbyXHDvmu-ub$pLaJz7r=*kZb@;2{K zcL%}NW+@V~^M$P0Xlir=Qh3YE*!<4(e0Hvy9`fDYTY8C6VwyFEkx|Kw=)}EqF3W{N z3G68<{fWKi7rjF$1~e=nj#W{MHwd+kl#Rmr^?Nf{0_hpXl^h&ix;3{kUO=ccYA%&g zEiPPQ3y{^zA31&at+NF-&27%XLpd#DK>(NmVVXSZYJSYU&7gBn1aG&;tPtHA2GCXVk} zX-=3Wdo%!zgU%i~uq#~ftee&;VYiqmNND*E@zGXP_EPp5O?|!gjxuktrMp*luy;Pd zT>70v)DN@NMcQ4?iqnx;W39EunRIDq;l~nbT8=`UL304--f=^N8*pcIIH#^)#IK67 z!Mi#9v3-fUY)V&gQdXElzhEEZ;e4$^)u7r}4rd7ss%_2hf{|gibAFB(ds*v7q%9UL zbU7eO*jA#!`Y3I{TEWqUa4)Q|$=GEf=zy*x7%h518Qx#4E)=6Dq`MaEk_EtsX^4T* z3N;nP`>K`I;GY_$0fgUiR}i{ale0INU%Yf`XNNJyzEuO%r=mnrHq19qqM=6^sN8z@ z1a4`~Gb%6qQ66EjvMc55lY(U2ri0KJ+ttSU>WXTgI?o2ri{_eW+Pk9h2lr?!+0e=M z96p~cZu?;NtivGls9GgnPrvMI*g8X`^3>qVHcDJR7OP$V3v{*-E`Yq=b9649JGkTQ zyx#;(Z(hQmxPJ}ylCNLspV>+|xjnLruJO<7r>`&F!hJ~T7h*KB8eF>^AS*QHqAB4qmHue+$V9z{-gAgd)U+l=2 z8>Ns+yXv(GM>*`i35e1YKU1#1+Gq8Y9JDZF*ZN|Vl;;FU2=vJ{r0n_>9qi(-VUr_Kv-awYF&1XjL?*4F%mWpga))#9iPl%8U~ zFwO5N+K+D~U;hP8qcu)HQOSs*dBfiKt8o{lPZq;cP~#R8Y^H1@h@yWwz-*AyOEz@k zIX&rU%Q95kSK=Wjxi`$;%DV_}O!~h0mXqd7y=J+g!9#06A1E^r)GvY6Tj#2`xBg0Q zlS7Uv2c*vIWoYo%oAeU1sDnqZqt(@*(ft-46`Zmy6$69de8&|0k{T5}c(<+H(D52> zs4S-LqXsV&5F=|gCT)Tzc9Rr%s5SKpfR|8l!&yPtP? z7^hkZFV&(-#xl%c@7BGU_i~tqc1NnRBxW8J;%TG(AW@UhJ z0Hu{Cr7P)ztJckEaaI`f4cawj%H_T02e?kOtgz^msQl*PRBH~{Xc5a1LRr+NNO;S` zJ}qo0pIg5;NZE}$5#Zk*-2^wT>;SawT=KLlnCRF4U=#q1469OIXDYTJdWP6Ta!D*0 zk%L{eR7O$4SaC_XmUd~sAj=wQFI04G;LsiN{`i!3oCbA1S2k<=JD+ip3Di)IONUIl zYTd5PSCTX{DnY$UOCV`D(Lq*3fBuG6!eT0@TO~4fkmQfK_;eESF|VzYS%Z0!vG8~TOu2q z=jLkt&5}A>cmzXAg3|c(i|c|l&K}dB`?(-qvJ}vwT&QO3pg!IL>>abQ$smL__CgH9 zQPUQ+c_bA+NO2Au$&1WaS@3f06S+`Z7m`1q5-c-kth}TiJ{_m@_OiDH3ST>G%u)5$ zq=_(C{{wI-v)Y}jWL~kR(Hkz^9XB7~`FfpJq+( z&P8*^9N!u98q}iHg{yMPwn$$8&2FM;v~#2|Bc^&2r2H;^tDc*G!CX8Nlwf zE|RGALAm+mq;Vz2uu(|>*N%71<1-QXs4o+Y31OL&=td z%JGaB^W@gO7fgEqWR9P;JqKQ{J?k=zO;)VB)#sSSVSh|SOoM*D1-UP-^{zS0L zB0Boe(vg#6D*9X^nfcOYF7H2%@;`dsSYi&ZjdSrkCUdnVPMG6KwLL*_U#3Fl_(AP^0GRuet@4wxW{_Y#~+qfaDwx_;L;V%?79eKXFzkni6b^(G7Z#kO+B#QH>UVESjbvkea6 zi>y^TcWE-HPejR}cq<|%dhJlV%&tDKJ?pIx+U!VMIH5y?>^YT-uN8# z9X;4A!Q;IRbee;av@2J@ucCX(#tp{2qNq*c4E3(GBI>dn%(I_GjyokGCysM}6H14m zqTn-U#Tu+mr{5n|#6;W9DLhMz@NviUgkBiBFqW~I_%lL!6lh5;pmtW~kz`x#2F>}N z-7?JMp;+B!=9x>6o1OETXdrf^kQlW)0IKTBU2cpDgdV|?dQ#xe!%zF7NEyMg6(H3S z_R)wL?%?qt(!5B!$+Bvuzj+VFL+90T>jLMfkx?kyk;K1fr}@nBH!3!89Pd_t1OHSi zd{%u!`^*GiB{1^x)yaHbz=@R#+qZR>zG)6tYW=y>i-RACmF!yLZHYBefBW9v(mqBd zio~V_y!$HT|D*Inz(e`FJXOBFeGtvHIc3Q@e{QR+H@%{_8ISuzZb4PJ5Xt8|&4HM%#;Y>(tZ`Z^dX^vmQU%^cY! zf~hB8SoL%jB<7)NxOqNCx<`a^wP-Z13~(+E!Nh-3_Y(5xoPv}%b!%^E9!6U`Y?L* zAyPDe?1l}22ZW0Kt3VHH`AlGt^TCWk7_F9((( znn_2-xY-%zmFP|UA2!iS- zU1CA5ydqm#PO7H>;m*kRA|1F%yxl8qhtE&)rTmU;4sxB9twiBCfl!+1N z@gTn^4qwP?1Cl~GDHo`~aO4dn#()&vPe3#iezX%p+OBR+jD-m`!)WDd#G)e*KQHjY ziRTpy$f{|C_={AqRfMI(kV0?5yHp6Au8adt2-O#glzKt|kb`SKh|;m`In^a{)sjl= zB(o@I`gZc0V_C%bJlcx)3It7W1iYzJ!bjX7lCL`>UvFq`S=5>{2UZGh63kwe{8Ll4DfQ?w@3cWpWyneUhP%a7}j zYd#dMNn1G_RxfgkL984^mBt*en)#BPamz6w3YlvbduE=r!6O`D)?Ha((kFIW{Fkl8 z;acj4>>(?{DMSg)4a~h80$j|eFF@vV|+D~u3bS+YpPI#O1-N?eo z38qhnZa@y_1|gAuIT#X{F3gb!PU__KQx}1(Mw4k>kq&;{TqzF2@-_0WO2MI)f4L8I!8$p1<3QJ-CGD zY73G84+yZHcJ>9c{tftJkeR@}XHz%FX~^jK2f+@#`4Ms!E-N#z6Q2IvQF9OU7o5@T zxhPlD??hJ+iF8U#3`{p8f{l@_QO|?-$O-J*2>J4 zYt{DHX%+aVH>ZX>nR^2|I7|pRuEW%i&IXxTsme|b1%mHp)I&v+j#1@ls?aC#v;d!rR{6#boV(OD9#&vayPAvdK{x?0Jf6Lr(Q5|Z#fMKdB z&E)TsbytGGxj2BG@AV@~_@INQ+g|K@a+OO%(+48Lhj3Te*TI+4krZ-GgQbU1<(5kd zIPcm|?s-Ff5GwL3KAqF30#Cck; zi0#3fdC1MbXuYF%H!t$zuSwQqe`nc144KvU;^paIE3I+=4!LpvU34Swap=H^PbcWh z%ODuY%OV)%l>4}qU$A{m#j;Jk_e$NT>PVGu2E8!C)*r3G(}P}mWOq1f6nvb0as$nJ z9j^zZRJ0+pIMINeQaG6JyBe{+YqdTKWD;B#WhC|p6+7A%J2JL9l&l@Vh zEhBgwb@t}26hg(14r0yR;eZ&JOX0=f#)E`9qb2C{zE!M#bY2^EC;EHUt~m|`$jGg~=%j-G~_tn!pMJGcKNVP(2s zQba9Oc+BzjS?iSi??vjWgA~&gGL{yA<+-hTbHD+i#U-4|+TqkJ1LGkt;mMpRgEZlm zO78r)oQGhE8-rBC_h_c{~_ESHl{JuEL`NLdS4X#5sz3&}Igyj|rf*acMk zh7$9BgA`vy_-=2o+9!L~ubn=vPyDLWPGx5Epq3IL+pW^!bb$3Q^4lZ#(`zz;4mGMf``Gr8WtJU<&6yy4}VmZ+? zHEwVIu46edTg{?DmtYRm`77^`nsgvf!HYFBJI}pu9_#S-XJD$Jn$#}i?9OaK7T9|c z&nBK}%;)6!A)Me(P1Mc8LQ$oSu|3Y7BdMgo_ZraG7N}J4C0VykO^0iGz|pVRj%45H z^g)+`gfw8U;q`!_EZDcJq9YcVdVBkW84c}@F8}zBJX0?9KU7Kpz(Vv(z%dn33slg8 z^OZ9_B2NfA0LJx>RVx&5BKMqEkMIxl;?DJU4-=^m)rp?GKPNdx+sqs@B@T5QXfPop zc(iMt9POD9qW>Y~mnzW!nEW$H>W0%Je{jo>0KHwda0i+xB!((o@H45^Ui@ICVIs93 zjQ&h+fHXl~(OK3w8{db}v?nLG^s9QadVMcBS;d&*g_ukgCaifYUBah*e26c{sH`JT zGE$Z1P``TcG^%eEJIFb~`1GK&P0~Y?eYKPOOcn)q;11y)6Hx#G@C$v_J%+U%HDR3o znjeIAXm{1lnlxT!pI7{P{&F+dbf&C`X_*OcrhbnrW``Ya)P_R6*;1^ag}U+e-i6{X zL+Fi1pvf`O(xNF1aC|!-rGD1TNl98QfL8<+!3vBLLU>yU`h0}muhAYGM0kiR#~*D-7?$lw5F6{b#peIEFK5vSM1yTN?4xM z=QQHqIS|uWX#R&O++sA~KQn_Un~d&%^~`)21HXs|I+Bpcne#rz?!h>hkV^1@I^AaE zDX{d2oY#X!K98QraFz^{#L+b0o?T(mZWwgM-b#`W#^lpM0CzQT1|6`pIg_^FKfoL{ z_|{pJwvf~TeKlB5ReZcs9r>8B4nLB$k7&1uCy+gu@?y(&aVJ>0vXfKqBD!K-3+yKm zvqjiMdw&QQG=@l!!}%Ve z7DT8+aSIAY6+)fi;8f-!n9i|O={244WEU1|wLK7=!5XBTWl&w$Qo47wR2PVw2t5u& z(z7Hgy&@8mEb%6IJX-AVF*CQl1vZ9X6zN{HZp!^OfOhz~!$e4H9^o0Cv46|TKNs9O zIArpbQwZOK7?iw6`5n-XzLYn|s2q5}qtZ^pA)?wx_P^2DRhhHmbC5f)>>I9feG+MX z((lTSKA(&Lv&A6;OL%i>5pB#IZC$qn45G!9P}vxA36(2GPE1UmDSb8c!V=<2K`D5! zoUI!)z>D1+vt38f0}|_%#fK+mOj&0^dYvlwNBW-(Nw168qopKx%B*@i5pC>l5kZTI zDW+J1k~}POwl$ykSIn5Zx>-fiA_LZ7lA>oR+L(3I9sQRMdjZF6QedS*0++B=#Im=Xg4U^af;CXJO`zxl+Ap21=sCf?e(IWniU}R)N z&e59fGim#{DvY}}Oy8>1^HKWI7vh=ro&76r`y^L)P4G8s3IGH$zG==-`$lkg4IrZ0PCyPcoaDf575~@zM+crf9vga%%c+mJ2$aITY0NNAc{&PY@;OF9+KI6u<>z40* zCH(~D2~fV!;%PPwSNz&#ylv&O@%Ka(fl|sBW&PFEd2{{I)oRHI`1^7S9Vp<38wkmc z)5YU=%Q{thsYhqJkwM4EE<4iLV8qKUI%LMlG4A@z((8b(mrF2y1hAKE+nbnEo2aL| z4h_FtY##Ks9^1u?F2xm?WXj~D!o4FxAkHT)FT&s%w1&RKs7|-V%tnhx56y^8`AlaT z+6OXzm6}I^05hfBBnFLvaqjBI8`_%QiBZt=(eUKOK3-JJ{2fdj#;b~M|z{8u=B`V zg}-)>9r$M}WJ3wI5TWkE3)E4?=e}jK9!>fhl89i-qdm%ojoAzbd(tB;1IE#f(ryK< z)PC4@4tRj0aLN_QH28)IZUUt|Q5w1z0gsICOC~gzv+N83Kn9L%fRoxXq;IynP~JGL zs8_|^;9Xi3CNFpZhtrys2h1~5z=Xx-sG+xqomgFre9I>6{MdL%>4)92hjrl$(hlK5 z5^ZuT#LC&oQrfGH%}rL0)4XIb0-~jkKkV`OPtBo@;YY{hccxuQ)XUp6&{?fJf@OBX zA8;5>+akCC_SMr1aN%k=LW7+=5Y8C|#uq3M_H$07Z}!a-G52SDg%SYkP!|hL1ixC#F{=EywsI9ucf6ryq;vu29 zxJsC6Gg+V|TaA@k)e2FqvpY@Fukqo%fw`D_Bq0JMUovL!D@U~uNpOT{6=%Q=4IeMo zHFm$duE829Jm5OadBR2tXAiIrOX2FQwiF)FePyrje=*0~CmPQi7$$OWF2hgwWy>so zf3b+0zbtBB-+ch0^&x8YVtv>T=l^B&_yyARXwOhPAej!THdA)&C?%%j!6*jG!*oV# z3|Uc=D0I0()0T&(r|aATchW!lDmLxwgqk;>HZIx|F;aZN0yWnF?6|9aMqjMUcZLVh z(^}~dGXrnlyYzbx7FF2D`88~VDURwXm3i^p|nYxcAPF|jipg{FH?my}Mv zASK!K-f^!ACX;i9RiJ&Pib}Nx{;^*01b?FLl1=6OpnOJ@ z>S3fV(H*PgfRz2s;h^dp7l?NbJJz`@9AxIMHG&T|D{`f8vvEt_^9*^)e$-FGdHeZa z^AzC9Ob_fg5zOv?MDJ5`F#i)YF@Gfku>efOef0=YnLoZ|IpmP!iTbUGlIx+Qt`fYZ zB`jPqh?<9OYL=F3bZQDTbaioS+LWO|)H>Fe*E?3O*E`%A*5}v7qwl|N{8=-ku|xzu z|1+%axzl};>w4XO@;TG)c^vujA^>4jYZ?lPfQIFi=~SS^R7a1KyM`R=5nBCt05JJS z0q|ubc&4(AEOhV1V-1i6z4ky zFrmmXAfc&g)X<1vPr%2pCoE?ttg|(lWUHKNYgTu*QY>9STYnroL%b- zdsRc%?dzBgA9%E$<2#!Cq=4D%hjMTE+|$F5Pt>BZ_WU($_6jG*?A%p2qb*~rNn-7s zONyl|rYfHu@vDu<#ywqtdx}P0&6?-@pnzgQeoE+fANF#zj&_D-Y}e(z2cUMcNmQNy zCfC{oL!pGoRyF~Lhc>HH1Di`;DR(MUlOUVC4bFa!kxoMh+~zrlCBl)9=xx`w=MzUeuv=G=k1N|2oR_YhArA->aO6% zYS3wpgZ7#5@muW^hrtvSvE+sz!0}zCvFA*;Qk|ri7A>yH&}lF))UR2cr9}`=UR6L9 zCSO>y4l|k1Atwk$`c_gy7MF7xQBD~jV14N^#5WbC!GfV;tE$2Ss^2qb=r}gbK1XIr zS&4tmiB&^U+p*NI0fsQ#pl#w~{cRg*%@R%ZWoh*-vC6ovx=xK9UC-)35EYKeVxN16)U=_8Z4>bg_`ASUKVT)X~HmU z1>5rNUt=O?_Hkf9&OIo5Y<6uat_N1YNgE?thw?7Tya`Vj)59KPv0kve)Q2A*-GRB@QC^O4xj+^f7zgVDu z0tFaTk$ESm>hd-J8TNt&u-|eX4Z!8!iO%A@cIF>rbs3+#&?dq#EA5O3!hcQ<_1@*d zaoH$y#9_RG1_|$_B5YOuhS;7KDkiXsa^gIw(3*!i)Oy3GjR{qwtw#9qeyI&7<)K2-(Vdd#^rS(=-fQMSry zGf!f?kmE~a$Jkkin`}_eSsO#oJuHU93=l(YIz0I`PCk$1pjrP%j4f9Uy25+6XZiR< z=XcaK5nq%b9qQt@l8hGs=XN(xj>Ww3bxg37)k^TlD1wh@G|MGePQC1F>MzVOLK)b3 zczD3*ZlPk*FZc?!5}8tXLQq%ldLC%7FZ@I+5^OuwH{U*--_~#$&dH)Wf*ayr)c~kn zmndph^VE*jyKof^*^m5<47t_A&U5EIb@S>4Id1(NUznozz?zQZbQVo>-H=G4d1G)b zrSXi?#qyp+IVgDD;PKMuu(kfT((DpSFw#kp0Ck4Tv?h6O$@5DmnX*FZ6w$+U_a`8o znHk*XLTWm?wRHHY>b#Lm(XGB!sp^P5nCf$IMY8dgFY8dTDhkEDO0T3s#E-gUnpkZ&5Vy z4PAN?6`}&6(I*nTQs3^3JgF;R{3=X&H-b|ZkcGNtJI4$!Yx}8q-&LSE-vtu@| zp7lBsl))T=CPSP$-cO~L=}@&)k2>$T;vTEA-JE89se-Q*C-}iN7ErVoFnEiw;SD1$ zT6=s(7LNZ*C0tMq^JW)9GHK0>QPa2`lHMhjMozOz&L5wZUE+R~gfsn~8+QT)eow1$ zlAcfh528e+Da@*03yd^Lxnq;yUbJpnaW$AN;ts?+fb4u>RDQ>T*5S?M?zGgX4`M7! zzFpjz9)Uuq#;wwA5>OMoU>$BYOE_VUC-+*IsFcQ-7&V+Ovy?D%BkM$EW(^W`;Gx=6 zj?gC2+^w%)Q`y6q;nI0s#OQfDwueDrh^baa?Z=i@$HRYUi}=Qi(&I#v_-H?Nehw?) zr&d&>dWQS}d2r}VX-e{MV)LF|9rB$t(Tb(}hoLVY&NKP^3!ukrG+`MYVIJ@+hL zYfg=1v1H)%qV$n!5)B-6a$h6(<1p11>}hGq!$_mGjMTMWyZrK4fi}suEy!fch4CtD zIqoRCt|PPD0AEquPFC&3k!DH3L#HP+sW33^qq+DxI9emSv#Y>CnYLN+^zK@Kk#JcvqPbC_M1}XYsQXXMfdVN?wl>{S52WY`kqk{@w;_3OJ_BTN#1=ZC|Ge z3+>$RD--}-H$*(m^#F#q%3thJw5f?ER?kA$_&X=dF#y5&d|mnbyRh#_V4b&)3nmMV zp?e>WL^s{iS$|rzruQF$VuE`DikC%U9mN(FzRa@+wZPxp8>6VGun<%Kh_FN6#Z-(x zbRw5&enT9)95?PQ!Tk)UakGS%_g|$~t>JkH6``Nn0$$~svA8&46pkXsWYua>c#!;) z4I&)Vf(4Y1&kIlZ3ju+jI!x|d6dZ@G86Bp9O)MDhkx4a{c*>csHV<uLHFIiR@|X&bB4Oh1*D<&ngs~K;<86X1Loyx`pmM|j6uIUa zd*8*^`5oC0FS&q_5`&8oBf8Q+K9pFss)%LnB7g*Z@3?V=@l!esxF5`HO0-4zze`ZTw(gFsQZ@If~;u58fr8aDSP!7k$6GA)3wh4Ap+W-H~t ze`%bb!n%jEuHh_^eRfe|)vp6c7wGO|?2S8oCB0O%&0FJ6URqX&wnxB0Jg4!L_+&qO zlk6y#^z%b$$4%lF;lqF)?n96sK`O+jUcji7X3(5nT7{wL=?R$JVH1?Y(-9@L=e<3Q zSe{w*JCU#P55#-%!>mubMbV}=?Wkevdn7!kN*}9r2Qn zc2~AVkT*G%WhFg&2_u1TZw$VvZR=;hHS%tqTdJDU(^Qqk0$rJ4v{qcj|6MqMWRTEvBz*-TtUZdt3yk>==adF1Z zz%WMT@b%y&`-jte<{2-uCFk+Y$Qt_}xt$ohJlo8bGk5iuf`Cz?waN0&aHYs2o#&rf zRqy1H2A6hE(TUC4RjVqQ)61FYPJnGPdl*Vf-)3q(ol)_Yxcz*01+JyxGNzMOyPwfO9RunjC31s0>xYlA?;GB6$Y0(>!K>2W+P@&y{h^Sx7HtP%!@G`KiR6Ky&q4Z1l^EF{v-ZnZ%p|nXghCs;=*y zvxk2prsnhhd5N9D2K-Dj0K54dp7-(Kf%jdW8It@n#)Z&CZ=PI@O~IASItooe0#m*r zW%7&4^V0OO*PiXFL@R94Wi!CPccX4)lGI`M=NjYAe$tG1n4fOXF9R2JJxJ!|zISAL zh!*@tz;`iw%ySJKvJijo84GEn_lG+S+!Zd%#4>i{Vmrh@n6ssFS)jkQzPEHtSMB-Az|WI+ zFn8n$<7yc32Tt3`Y}0mOu%z(d-};dz#E%s(e*f2sxL+r>QQfuvUe42Qb)&j`rknwD z(S~(B1G~^pe)0{|fKlDe=$)Hk+Dz^K?15a-w41?;%}T-LHrkkthG&?Agq17zr+r1= z+ftR|K7!A|pe6org9_?K@5lY#jB!QtkL?@uQ3GI%N%hW>P#v002SJ^0omZd$_8etV zn-cbD5xr)H!G~>S$x1-}$idU&gjj7@JaxQjL%{Py6tui%}bir1p8DrRA z<%)i9)PS4azUJij>s8dK-Jhd_7;bWO@-3>2ZQ6&9f<@D+7F<{wnU;`dWXFb;Vt!w9 z;r=3(9m;5pm;4?JF^vHza}jP%+W{RHtMd`6VNbfVxT2JjcF0nPtnT@FSF4y6ZW)0^ z${PPh3tkBtfWwhX;&ee_J+6c>V)h?=w<;oseA(_8$y+{yA>Q{l2!sjK`)0n;fU>xl z7iSK^Ix|1NOd$Y!;$;)ulPxp)iZOBRD2j`A_zW_G1IcI>Hf5{RBkbiDiLZmXBIosp z$7b|~lc2+>!=|vbVSm7;v^Ou+Th%Lg4{*f^^KV2QKsAJ*E3#AEDL#1@mj&fThWi?m zkRYMyE}w0mSUV~wsgLu>hL3K=C{1rBwjfnyK;DY76hg9(Z zQg6xg2;5I`yYJ{+uAK{a$rGC-x+LGMB^>=nbL0s^Ue`QF1#JgfdFKS@Qvv zdK4$)Es=ZFqu?T!d~)c$KZ7>PT#t=Fwh$QzG#nw(j{UM&3wesx9$fv? zl5Yl-E%o*PLM#kcyE$M3g_Ih5?&KPNBA{?8jM3`1My1|?R}{E*v_&6t%l^R2ja{4p zXxTZrd9|&R*!&9~p!50HCUCb={<|5n1Wf8o=?8WH7hI`Q`;ZTLZNltXwy9~RnyuI` zdiJnwkyyCs$0kB-xVl6MAgVfTESv*=t3sXc54tWbrqQJko?#;nN(>q;C(Slj$Fk0M}!pUwJU|CKZVEO##ZQA7rye}Y(`IUX}p*1}$%?YOhW7pH| z&kpPNMO}QR5f~ThM5#N#f!L6LMFoAB;W@o7=oE-(t%n5+x88a31p#bUb|&1kB(x`< zV8^zKwpOGKj@C=I4p}r6PRfN1nwmd4Qr zY{kO-Dc+xbnr)HR)BVDGN{D^VM<`?zBqil1dB*Gjbwwo0<&Z4x1aHRT|sJ z3@x!OSc(^T%8kJ|?sfbE{TlzR@PxCKY|9%az33|zAUWNIMctdG*&KIMO8ZX3_p6jQ zh3bpuZn7Z9gfD7_^RUtaBNmc5Y1;{Dbo_PhfHHLCmTW0`37w~toK!IoU*9u1{UYNoY>S)3s*Jw1T*W8{*2flcm zi6nH_|B;uLP+1F$L;vw(kMVz6VL?k6|ECr9Z2^LshWPRA*jD`nf(1&(e-fnMsk6ZR$G8F&tpB|%pOS; zG>Aj6ls5JP5(qMzCjAPc4zkyp_X;xn6UPurysY&<<>j>eaS-;@4;I98bUMc^pw#ne}~h72x<@6Tr95X=4*J&OSeX~8YYFq@wVsj zw(CsyO%LM^qQKAeK@l_%Fm);%5h&K7$F zcQr9~ypR#68Mn;0%!5UoE>p3;uA}xifB*v({``1t7ml_oc{<){iq@~uR71u)dcb&X zEl=)|Zk+rf{ivx?2NhGku~L0_dkoTS0+1_?l}dn|WeB#{4a0w>(KZ@6+em6_%_h;x zKwjR5dQ#!dJ=cXgqHNis40=+GIanE;l0AlaoN2BkZ?n|&gcdmZP{qKB-%0ZukXRki z^k+JjytRe;ku|0??-^$HNsc>M6>aOxm6)CVOkQ023mcv~+g!hVhjB zF5eD!;V)Pv!>^#6Ae6tlM+1uhn12^g64+uP)Z94qf7L~~E6w{zSVC!gq&UtQMfVsx z@UiLOgCOs`ai?4HUeuD`YH4<369&F@)-`}4LH*)?{@3FPIV})@6*6tA z1@t@TG8R)>tcNn@oB+iTu+mX0jIy2Lr} z;zIj9;RJlwo@_Ye6SZsy!drL12y(Y91X@6pT3ZcoF_G|G7sM8nsptWHOB<-^M%~nP z5C+w_4_n+K*_mn@v!sU?3W`u)ux$`eV~8=Tt5JAGx=)vphe2g1XAq~8of`FeS`PsN z-noZyk602v)z$H=PsI(fzA7t0;pjC%N+y0o?p>A(V=Xd=8dutD;N{pU12AjH^VbnL zRl+6RhnPlYn{Ds1N%^eJsWZ-*v2a>zvIouk*|7yQd>uLThq*GY?@P)L`IO~H9sKO= z!f|p@I>~Cu<%@1x5@yn`c}qfwlQ00<4&NPTm79`5opwiM=#i>|4lJ+vQhoFUwvv^w&GU&X>o#VHDl>+Y`?baKGyB5>xvMJS9c`fo( zKY+{R#Kn~Q0gofeA0{idKkRFf7Ie<!+C9)Tt-AF~Tj*aOnZpq3XFQ z1N_FXdIs|>ng3lKWmCKN!0I2mlbR=qrjg3?_&=N|Kc5fMhyUn>WZ);M$^IkZlFt{B>K-D^Mae848+9`5_v7Hq(6l| zxH|$>AE|=FcbE&IZ2W>P8i`VnACct&*wT+lWHRSvk^9tdJZt;oO_P2WW6^uwk=wUN zKiuyevEE!Vq`LuJr>JPc=Q3d837BMuB{$^EvMoU-7^j@!=)DMVRa&H@6diy6Yq^b> z7%YMU`r`)<)c-tGj#K{?j7LthMPh|cqiX=g0no@|S~1zzLsPdcZ5(po^T?)Wpa~21EWNz~U5$VuP^#A+#;`MAT|54T)lq8&?wMiise1~A^rY3C9xg-f|Hm)<>Zb<Hc8(x6x2B@;0 zG-6^ND@^BR*cdeUY#^avsous!9dPv{=^Fx6T<=?)hKtRermM}=11Q-~t(w&no`43; zWoj{|!Awa_YZa`juGqlDKgd`d)7c&M2{L~a=%*+tn-<{c#%DQ=GvX${bRYx!cHnSX zb>q47`SGwrD>5^1hM>iywJo z^{wVJ>X})4R=zb)EOkjP6_+rA`38A4U{5=R;X*YhO$RNzw2uudx`b$KfGr@CzNOA7 z`F*NgTIOEN&U&0e|Hr5(*oZLa6l2-uHEgk{I_cpRb-kfCra!-SfoNS7``(8GV^5w3IY`^X zzA)RGcOU|0tFD-mteVuUcWB@KbJ?E{oS)Wfhzc&qkOJ+K%0iZoIv%vMa2B{}? zsMAnh;_iKbYgOdVfN1oz8hg{1S_LQl8LREja_I%M=g>d+lCxZp<@{FVT@2*szloIV ztRsBziH;h(*jPMmR{)BV{_^(o%B5(VCRjE#GMLLzE>4_>8EASx;1`XP4OgTu1^%p$e&v5wid3mR{A6nD7k?ZCF@6P!h0A-mK&w4KkIyQ}y&_gqZ zHY4NX)s1M>*SMc(U^JhMl=ZPm3RopxM%+62nM?}Mfi9Q zZ?{$t1ifzsQaT*m;>2x@NN1QFgCpNBGMJ*;4FHBgkJUIbJ_!zql|q5&SoKnq#BU5> zRQ@0|(%5zS?~W@@TL52B$34<{9@(^KK4ZTa`%vV5%0(~nLe9vQhya-`;YZee`ocU^ zK*lL1CFFP(kTEhK%<+~U;{WLW1TDlhDSl&=g8#+i156_XNKv3uRH2<6d81je`dHtsX{i#|4@|B{iEvrO zquDWKl6oDz9&;(ioh*)gAFjgkykz8i`ZK1L}S-$fE#v~z zZh9Z!+t-$fIr0}ACC@9l;w?_u{G@ z@>^Mq_Dp8}t2q*TYCcH6HJIF_6xmIA6mRfM;g#0_i#;oV==E4e6-GjCJsH9YxDUF0 z>lrDI!M)agf+b<$Bc7?BvT4ZVjB!GhHu?exb@Ri*#%E0YgU{c|qyEUrJ?OkPEVH<} z+rP04oHW<;+i=mW=}YwJs)+1BWyxmzH0`2xkr3t2kztbHy-e#}$ERiXzm^TRs4 zL(u@%tXozi_;~MRyz4f?`ATho$;ppFQ! zJY`ChrYKPJfKFT;ohkVX+h-s0bX^``HJ)chS#kTZWL?25NfEbyk%gK)!Gztf5{en+8oJlqnwvl2$c*}Mi z&l{19ZdqHia|qH(ay!6`T)&*zlSh~HUr(m|SPe(N_c@1CirWX?!E12nJ4)3n%*89t z#w!F!kGRxH-X1T{SC70Oo^4wMdCA*vhUp{1eZ_2Z*9^gAEX#G6>|cda7B30Af1i7Ub82^c+K6Hq5*~fzr8D}>yK0YV zy+lj$A;m8^jX(eEF9pDZ7j6B~a& z*J;2)$wOhB#pD!~p~l3?@x-L`;8*QXk)fkk_pKh1534Q)^$z`Sujl0&%aqhvf1{I| zng?QF=pX>!%*_N2vsH(CG;X#c&oka>(@Ic8_d1XWY{`0Z#tkX|y#PsnTr$bNfb#D6 zM(h2Rf5}JE=vi8-Z z!Koh>IPP5g&Twz}cRsxA+z9@lXh;^pH^JHGL6KsKLHkNF{^+^7k6LRK&?WrJH-jF0 z^q4SdrzAm3r-08}U7MhEnzakh9Vhr&${ z7@`Lqtif(>?STyXl@g7^t=!v@!c1Dio`}NN;Db!86lD-2r&cg(t`rub+rvG5_`%_4 z*rBVlWhxecYO=YVpAM)4BVkb&G_tdn#?WM1iOGBLIMFUvo@vsY3|j=s|TW)%FWAHVyUBvy)#k64<;*bV*Rk6F^x zKIP+z0qZ+55E3%KQ4mEZH8^*{phaz#7$L<4PR1@0R8&UIu|yA05Qj<9G^rMu3@57y z=~Y1I5LVEp^0RVBL$Ft1L>clv4&u!V8z-3??*e>c&WU;@nBHg$Ib?H%QxoM61A@Qu z74TgaDVB^dB`TJRe#Go?AX#E*N_(nZg1vb<18(|y75{?r@!e961421cUa%CCr%j`d z(?6}%#14^F@oL%)XV){OWPA{>d{6%d$jz?M2-iRcpJr%y2ztn+I^^zp z?xi}_HgOG7JanYW0KLIaW;**=!qrx9ilVVPx1_!6(OINzn?C@F-Y**k_%8vdlhS2F zt0IG8OfjKykD|~_NKO`Bu$G7C2Y@H*0T`y(Vrf zn9h+Tqns|2kHOmp`y9KqA@hNKaM=ipl});BMM4WufG!%MKuWL4JuEQH+EXgo-G6&M zV`Bqb&`1m9AeVzlgiaaD6H|0aSp$EAVm)?Lp)y$VHadl2O^MW2+mJfp)_lx=s9G;x zjVNGhRkN>Mh!qRS;z~bPPrSD!9_K3$@Kg4X%)@mxb!=`BK)_b`hmR&os}Y4sYSZr| zS~IVH$Oi$VR7)}K1W7QV{T}B&3K1M7KbpGW*`V2Qg~fBt>mic_NPEWd;VNGpMP?S# z?u<9ByMzDFxvh&XM&kV5;-~-F=Cr-PAarSj8!SYC615E#RAD6kUvzdT8lVwIw=zOf zLQq~HY#{8+P>nyuzv=(<<4Bm#BK95)pS1LZQqn1eyOP?^bjT#dFcBY2{0kdt3znrH zD>sCKAX7%A&qmY%xOo^05gFOXhu0b)}!O z!Hs)jd0DPudajw>J}`2ta6=E$b%0{38h>K*uyz~Hh`ZtKKdSYw3Y#=Xjout?z+ zYLtcR%F-E`rYdXR%D`iagO!7b>l+eR>iA zhC5TNQrw+W*o7pcQI-y_mZEyx%9ZQn)2oYk`q>vU<6p&^6_^4mRnVA^Q6=rUqW?xR zkEhw%yWZFe*xOZ!87b#z(8o3Kv3X?5=*N`We*~Xg`(7(8@2zhGHQw^N3a)&RfOoV< zWGiyx<%s!G{mom?y0L z<;u{cDVmGIjdrY1`%rZ@{EvM3O4PYI`dIfEl*3Jf*;sq7=yLK7z^?|SNL3L5Jo0B@ z@h)KPTsoq!NON_TCTX}PwCRI<<46X%KR%?Ghd8}MuM5>g72&_a+y3D@7eH}Pp@=9C zrURrRanke3(wIPPPenkS(IVsMXK^!Sik^uM?A;0+?It>MnD>_!JnwMD~i%W2W$a~ zmK#D8#9f5zjzxlrc%isNl;1;9fIri;Jf86qyqM1S%;&tA-+v{?sI+=w_7g-W6yjxE z6~&^v*p}lbr*8*EPFu!dsUc^1nJ^2WY$Rhz@$Knvr!vPrb^KbEJ}WNj+@yR1 zfdzZBwZ1c8Tmu$n&vz)l;lLLZgw39X&1Qs2icAT91pQ*UK^Ots7vl~GNEkHbU=1TV zz9Q@OX>Yfkyj_!&PTX@8V!qrFkoq#kz2Ga4BO^{=vlkj;6Ews`tX|!H%Ip7p&DY~U zii670HxY?$Q_>e)S^od%dI#>zqHbHaDz#jTyu;yM(36#*{O$EZUu$owv9 zeD6qU-DX*#oPUV+kKza+eZ7Ak2>jRv$A%GL= zlJ>ZwXg+kBIGWgF{hIh@#IWIXYhgj>?8N;mV0uE+zzzdt@d z^}%CQ`yl@XQDgrE!={%=JJA?rRR*@5BJd<{jOJSWMdxYSl%YLl2~`Os*5@%&HskVd zi62>skyrx@QlKNIH3BgX5hXal3ZPmjqB^m>FR7%9g0dvf`W82yVjZdwiE^*93D++% zKI3M-wn_uW(gI518dJ)S`P6tT3t5G0ADJsCt%Ns4VVf{un;hm(m`|Ak4S=3vQzm<2 zN^Iu{EZIfG?Q-N`$p(Vp5jtY}6G=?4_1LEHapA(L*pgT*IMv-%b~1JnMVBzWTz;U?nP=%-6oOSa1~jxLTx0EDW{jfAqyCk_-8a z)FWgsb-XMnNYviu3CIVMC&1#0@8re?sTKt>I80T_uns#)MM`Ne+Gv)=w|Rq0cPVv2 zDfK9oilrt>>iuW(7L8sd?6I0GTH4Uf%F8^7GR=_dTvL+^7v5<&WgPinn+T_R^-+!B zg`|l#%ffSflFR|2_9*FRp|Gg^PXdHJrHW+x^eC5O{WMMpEDme70iZ7Bh1$0opJkY_ zkx6g3e?w4(UB4hmmYJPYPsf;5HWLEAtqO3ytcFrBUp1Lf+Gd%lGG7iOsZeE#CXGG4 z(3B6u05050*n`f@JtOM9yWxY&ZnL~PhD1l&qBjPhT~J%fv=C(p+Tmkv+Lo%L%EKzk zBS*{5E1%&B!Q}Cp0aENxV}C3k?&x1MqGx8IktOM5@b>TWvNVsigow)l97fq(GE*Ad zf}mfyv6uEBq@wvUIt%)e!*3-#l*r`#GL0#*a{JaDL0JFQGFLJtD3HUAO87Hd>v7re zab;QqS=jP#(zyDS%;SOu$50 z&dy6msX}vor7_x&u%ZVhNMkS(lo2K^w5BgLa_4{t@A!EZIx1kJ1MQ0(_Skwk51Y`It;b?UJ|B$}>lSw27H5}l z(VWo;Rmu8fKzQ+VlM3V08r?h8cc125a$V4AWhI!$AyuQ;jPSz|7*uQyH_b`<3T>G`G1QY!H^fA>>WMd( z|DZZFV;`mV0pbAB9Csm(&f{nK2tsmB1gOPSoI-%6lg1}ghy4SNM`J#y`O+VCcN>Xq zrWV|O_jtqbb{yIFK=9g$`P3?7L_ha~DVsfKe+MMNzvET2Ws6*sa-C7{IUA)n1{{$H z7zB9}2g@w+R{lIWwX;TGV{7pN<`i@2v#0MvUz0z{&ynvvxWdA;ckW|2xbgU6QjRb+rbLt^Ye-P z@g_&aIRk&V0C@wJgI03y=}@c0daLUMgf6>LW@X9h-hB)SEXg?*@Bpj3^8oMj@@VF^ z>=K86%lkE^%PzJFhT`oTP4b=r?jJ7$H`qLv?HLFVhw(1=YHL>?`ZRAgAc}JTw z;D`GG&wAS!0+Plqr#}H*@dcF{o9o59KLs8KFB<-U=i^22rVINPP9vWtVRbeHT$Yk+ zc`ZWL;UTy%7PJ(-g%GpR0uS%T8veM=FgF7o&hDh*tK9Y7CKb z>fXx`MY{+5oH3Z!pays7*cDX0vB0sy3+i`tYg*suAnz;t;m}JJv~LG78RDKSlbC(b zAbC*FkzKq$aYuA>z=C<+jV{hsK{Ddv;#?nB0HTEtCLdL7{^x&{U0%!>D9-N|3+h{- z^S`%QDPBKm6SrVs{ugwAKnL1eWuf&8Xa;biNW>Ka0fi$*>cs$rBlnj;lNcoGODM4S zL=$884jY?}%NWu^8z130O4cbB)#yv-gDiD=-e-BI5} zN^lhxa`A^oSUt39pq*Q|bahe0QPPcUmEe+OU(_)C63syDPJ$-GSSb)uZy)zMozhP- zjsFy;wfpzxb!rUgm~T1Hz(OAToLxf5Tv^QmCjBv`?S|10~5Gq~eC&ce9W zkFD{Gaz^SzPBgz!7D3aaUzCSn^y=V;kt-QDNM|VY`Nwhz`*9&c3pqrLI*TADfWSX@%;oe$-SY=Gdb$Jfs!ZTSC9yzg!~&m?wj+ zk39r`J+r)kzDgJHJRLj%GPD42jX>e0a`*x)J>d13c*llY8Fwf^9yB-teNpXdm z4h*sN)=hVfo;gXU7ts;^_Kq>P=qRq-PZ`Wb`T!oK?}wZ7#W_=whP^_8;qp>x4iuzb zz0CH-g&pnvz>KMN^N7Mw2-2%rAUuTMyw+opK82 zNy%U&s3Dm2-HnHLPqz=CD~tho5>K+j2M8kwcq9Au7fvCLWaLW%Ejw8 z9|kN9i>+l$=e)LrQ(Tj&J{#tP;@Mon4+9`r9zb42(60j0u=ZIK2)$xJsX#9lE`?RKk`$eih70oaQx6dd#8 z#NlT^j++$9G1RUy9yD30B;Z=V1N6;hWC{oSoBMW9xGz-_UwmQMHuwyDggytQjf@fdH^!*~{_GqIhNDf^TF=5bIq*akSC|QbXdI2gOiO<@UFpp;m`bgme&?Qc5IObr z7_W?@q^B_0`7-QCK<-~t8&5`r^XNIH7nR6W`i;C-wJ22LlWj*No-BIl$m^f+M$Nyfc*=xtC0yPp{{|0DS}2MM6b zC%!;)OS_j;tm8N5t4 z33PV|#N-Jc6YkwL=pCh9W9wu&U4q)+PmBq3@R*}?+Sb552@6)*NU%LZl!Sm4XL%Qe z$14@Kp|U+$6nkU@e^T1$MuYFL(4aW%Zq4P|#E7tkayj8(c+90C|7p12&51;q=4t5_ zWMj+6F%9cdYTOf1-9r7H}uxJdbLI}tjuuLHeP7pOD*rBtw{_! zBhYPR-=c`a2fo7peJyoG8q49Ph+nlguky3dT=2O^=z zV1=eNG?oOfW$38WRgA8P^uyk+a&e5USn(1=FjF)uOAd~w+A9s1+v%RGcBK0{ki0Og z)o%Ch9MjD{UxY~nV_R&IPY5r5ms{EgE{ZJ4kt10O*k6<3YgA;=uA~>gK^ceF~L@-MV8_B|y$z^HRc&>P9PRqryqESQ^i$6x$zo?PX}DjN4L&h@Bk zinD(%8gVk;3;QK;yKYwG1fU)?ZP*}xBj@mcF$hRb#H;p%ki6=;MHb?pb=UrS`f;{9 z+jzUtG(J1uNk?tQwrMjtV<5P`>7*N$_%{1Zu!%TS;}{@g^?UlHne32D2Fm`dHb0x@ zuOeR3A|9(nf=tO~PxqcAv_O5(p5Mmh%CLYvz@_QojZAR75RP{}vy|>nX@~BG1kzL-kBk@3gj-SjL{e zwI20XGOdyTQYTAywmkm91(Mj5l{*1rlTi5z=qfX$H%}C$H5L=#qZbP z{uus;SM+DFx7^>W>VggFlo$xdJQ)@ z_~a2=eO)GvT`-U)5<7=l5&5QARoxMQ%P0AJ!CXN8YlJgV#{l~`)#l0Bf1-g&dEv#6 zsR*3z=+}<_xzolzwfS}R7!kYzs?%Ynpuyn5hPzMLhH~Y$b>1#zx{PdmB2P5AGbhmC z^_(^svn#8B1b)u(ymV&+;4YV^w4>TtrM&;N0c)0fbkrl{X}|Fp${v=XNg5qCMbTA%055fy5L52YSt388>74g?}(&eSx#zuDqrQ$@iq4;a5H$uB!DL<7rSGb220Rr7!!cKQm zNM0;wbDCTPA7HxNv+w0?V_ERs#ZY5tgDVUfFy;<%T58-JzVV)*y!uBUPRPhXEkY2A z@jgSfVHM3A=wLYGi@DVpFBzPtjKKz;GH#*+E4blc)CdP%5G|E@Z>;g12@JXfVo25} ze{A+-ZH{0(nvC}sq(u%b^Zh%Q=+WltSUUBSc$E?&qRH@;m|Cq^xJC6yp}_d?tIuoP zmXw`F;hX{tql4OUdEW#-!E3&&EDp$TOL{p_;0BenDPg2;hDV6o?-l8fcx}WI*N%{< zLe7E`08b=zB?VBXre7WW53d)SYZ^7QIaU{SDTVHU?d6^~gu=l<`as(};pVuUf}6`so;4r->o$jO^T=&Zw`cm` zfMkS?gCRis<*N1T{(iUqk`C6!nH^DV2*mVd#IJM`LWXghKWqxW|HH?<1W~*JNLXv{ z3Va{|P4sL+--een0UED-1~I>MF?e_Keq?1wKAI;`qu9N@>?{EQeN`<*7jh4jQ``FBPfL z{>E7Ge{jwW5l8wMMB2(th*}`9YQ%rmn^EbqQRXrZJz|;V#w1U*)l>nr{lopcU=qms z*+4da_1N9#>C>&aYb@!>8o!biL;|BvB71nP7V&~MZ4FjQhbJ+a`}?%;STyc2IK`(f*dW|NBw$Yc4BQcDnla!Wa_oC~B4QYq8RUbi= zo|8fJl z)O&aRfux%;0|#LY7%I@z!B#OO0RCpkZPoDwt4%1gXHuwxHDIgp9uTdIfd5<)c^{u2{;}WZLvrNo^=>MY}=zd-0Xdgwf+ap7g&2-8+{K;#sn|N19DsqTtSX& zP}p1Q|Ckj1`4y8lfIX?W52e%}Qy$ppJ!VEzm(*8?rnqJ!x_G1t?sX&H_3p&**@Rf% zDnp3jh_I_6+ieqyGhg@cX+%iv;!QBr8JApZO>M=|T3!cn{9^5qd$(eu?r5_fwBr18 z?V4Im5EaK9A9L@_f!L!OV_%B0g>vZB)b{9uh51v525LShAFwaSnzHcFM=Ck{O}jB znbgu01E>|5G1R!MJxgXD;`%6%*zNPDj-`! zW!vDBPo$x$OFAfTA`I;d&EF8UI%C#WJ}k+0rvp$ShOH3%B(&l`(#1<~l4v;jd9D{SqhzZ~~Ncei zM#}H3tjxn&I(AZ*}CU(g|SI|p60)E_eYb}2KYCnx^1-L9NZ%m6jEJUe`S zK$vaoF6~|}UncAY&f~v1%~T89Q7rnAW2v_$mGbHQOndy$Se`kU@*W_3a*Bfohtv1& zY?&77QAw9Do35-2pU&kfbo ztFpV+Qy3kWA{CKGK^cSGX|by~ofSwXBM_sQX({Z9txS2omguWW&@@D~7kFF3gBS2u zE^&$~no0@UwZ{7yps2WcCoJw9%iD1RsWUj@a6Ws0ef(7wP28G|P(gpjia4@Wo~*Y3 zV3p?4VZ&5*(=OHD#Z>-F>R&73-22NVC{$QyxQYclVWC_TE3k)O=J5y!^GVEeub;nl zWbGZ7@Hj|t)MED|HBi{T)_(bjk#)qNPGo;;McV7}Z!7IBIOc`0YI3UD3Mdg3Jb=<9 zK=?0E715JU*UY??NI_q^UbV{FV%(2QV(e#EbL>i5fRFMKYU-aAP%_|v>QJ`>*Hp+y zD>aZ{idX1LxneQ1Faz^7!!%k)lS1(!WUCVq{O2At^J75^Kwl!5kn#1{li=5SBw_*AR+8dRmzY6ms1VFNlPcIM02l+kiY z!mcKZJ-*+)V;b$$Vga<%*0<9>pO$9POk4@KC)gYW{@!aBu%GtL?z_Z=vS<4(3EX|Y zcgq@3T_dTQ#%YaM8GjhU;O_Y@;Z+B}8r7-R^WoOZI$C58_E1Md*HnKJCZn>4oUwZLHU84w;nucsU@M8U zN?$#VC}Ur;UAS!13)a0-#;?kpcU|&DcfeOtTuLmPd!R#i@)0N3nJO#SVJM+rqUpG7 zE?d*BsB)E8<=Lp8r*~^y-K$bvlCUL0eA0{Eu}vOoH2_A93JZB)-H>=cBw1H~S^7QekPH*53F8U<%93?gyr|i zLyb^R-y3z0OT)KP@w$|3uTI(vjV5TJ@b&I zdax_lLx@EHR(2R!rMAeb_Tar2MO)n<-r8(z`lQD+BdQ9TAm(%mtZcA4}z%#dxi-LOU z{WRxkvamwe>EBRDgazk<3R@xnSEBb*VPIO3?V!$sK~|ovVD>B2q#}n&jm95#U$zyh z>7!GMxbGjSP_h|Mg8V|28dilKW>x}EaC+6Uf%N>*AgBTqWo6aYvh8+0N+Cb!sq6jv z4UYwxPd+I^Wfu4QJI*pwI)v#)=gy3Yr)rXt!(kc@U%hZi2%dvkxz=a)=vz+A2;N^KO9H^xe>r;}_0lMc@qYXO5&s|Sy8i~G zBB!3Aupp*3U4c3P?FIPNi2I7WtG^w#D3?;JD>)4|#VjH2SqoDqiA{+M8##NfNO%H@beAK?YHeoyE;2L|>fme@eV)*k^vc6Sl}QG`nbj&)I*;;}dkA>k+Hl@R zr^}L@l$yJ5Q|50gek@vSWu|DP+;|M#7{r-LE^RnC;= z^pW@oI}73S=7ki^Tf>NKeo=2WhQX9L(>@nyG;;3+p-AV-X0aasCX_Uz$_+s31-Ws#=u9cMGNpTRHqou-2HCOb zW6i?${{_&FAqgk}(-h=E^PedLE(|Zwma8rK^xW9dwdZlQdX#f3IkJjl#+_-+QKgl) zN&4k?Q!;^P!y&Wluw+hBO0*P}scu=PS)Z!KiNyYDs9kY3wb5P6eU6aHaT;$?|22vR z6D-w4OB&n|dpg@&~`6g3}Wa;Vl&l%w5Xy-{94h2&I`V1#RGBUt?pIA`NOl)n21 ze%LH>NI067@W-h5G`$fPCRr3E)_|yg=@&#^h*fzx2<#h|2jp$=T>NPi5wc)HX*5f9 zc;5_7v7p(sBsiB2M%eph`7bxXn;VG#c2=X5(~KsojE_ZqZT{NWs#G>@c)sewsmTQU z^BM<4Hd|noRfT)L1L-po5MH!Nrf&xxQ!$|b|a@C*z$BfG47v1ZQznW zl~z2v)(z2aM_J*3$Cgg|HMtGONZi|ffN4@gEZRT{zYC<0Y$H)A@C1W<{u5)@l6hHZ z$?Udi2j-7p+9vsEZ4_k=aR_tUbykhuHnm5UhXh$QXw{%hP&rd7ySfqkPjw@H!P`*# zKC`9wDo|Vr1I^55QQSS=??k&P4AaZtVLKqSFHHw;)3Q?{}IDl!&v)!B3Yx*g1uU8O*?aX&3VHD{4F9r?=i-M>A z8YcZ(6U=@oI;V$EeRGI*1k3_?_UJeu^u&%QcJudt^m}gD?eRO0-}V)W|LOA4Plv@# zM9M<`mJ`SU32_QDDB0PGsgs(XnVcl)4aR(pU9XyMbNNe@e>>mTeu8%GMl~s#Wxbvf z&IlCll^oHpDV#?HrScu+bRK1Gy>tNIAI9u{Oz!lNc_PgrEzLyoht#_ajdI`@TSUa^ z9C18H>}ly7TG3S>S)un~sJGCG2IkoTS8L$8`jE!tXaO^^K_P`PKab&t z2$`fR4wgk^l;=<Tns#$BIg@a>!_0iW~K`4O-$VDTh29=f+e2+An1HikPIPz(5zq zEnWx35T6^hG&I)?O2=$47Va#spT8T1NpId`xyFhe7-Rh)UPot*V%m2dPh`;U!eeL)sHVF%MJd2qy2Ti^Ojwf#1mKR3yP84 z#|mlkM#mpQTL(mOw4A`Ip2VNe7MSN%S=cc*0>|quhzj9nQLy5>{U}As5r=&Nnj(=M72A1@acE?oGWc*4bkwF;RcB z=1ZNN;@bs*A-yd+Y?P=A*&EIrwqS;w)X*v&7f6fRhz3a{mu`fw)+(3nYz0yaZY=BZ zOslwX6z$A{^=u3tK9!@|2F|U+IoUC3#OR{c9Hod^@I^V`DokM+9PYuGIX|% zDaOaI1yT*fGx0SVQ3l#ys0BGB`IyJ^f$GSlQo?fYR{fpgDjT?29kA@_R>E)4fdX+v zAtXM^JrA+SLlo0DR8r13D=OXJ!30n(#N!Ep-!|BATPb>1os@7dSTmu}??E!qT2A&e ztcSXenRwjt2mK?(D;pk`&#y1`o(beEoXw(Y5HqD*DVoGL{L-uG z%(Z9qQY#pXO9Ca4RZcI=xvvn-TXa_MXt1t4jw@|E;6Se}!n)_S=8EXyCy4oBbLi;16&HCE*whA$1wKG0qRlok1R5ZhObHy z9Ch3P33@j2pMQp~l=~Z6SFqxE)B3W)+|KZ%&c@py{^63&@+BPX*uFotrE9oo8SD6~ zQdQOJ+Z4H(WuLB%>5^r6BQcykJXq^V01&~DE@@r;Pki#tieXF?%v1$flciea$e zc=(@_H5Ai^U?d=@r&Fe>RH_%bD@H5gdtiWMI1{mx2xl~-ju2>Wz%Ge@)aM#8q^GtP zn4{FV&ugduVua`!JJtlN8(Xz{egPW2|GXM$-Ru&I1*v)}oU>G^c%U{BLzNLwlWZTh zVe;huMLQ}E)TE}47@fOFpPK4UG%Ah*1rB;YB?(}DS8{Xetg>qepL$$E;zxAnjO+-^ z_FHgX;Dw{RowaOuqaSAid2zc9$&$n*Z)Y^}x+3hiHy?4Q3GiS(1mvQt#_ z&1pE8B2vbFdCbQyN6Cz=+>bLtan?k*KZs7l@mbO;UI(=~eHG;3N}S-*o=jS`op9-q@vafh493FJ>; zHtT+}ObKu-}e4LMGOY@U0?9jjiA%70D^nbv)Y{~Q?;1lliTG>6~ zZpMXNBjq!O-lc+OFBcjB>tpDarCNBLj>)B$UZQIx>pU5bG7>~iMaat_56ro%mTKYG zMtqplCs(N9iCmJ4P5u~9;l<$E7la_5Jwx%YGGx-8h9RMMBwb&Kd5IgJbqAsOFnl~R z;d2;4=B2szbImzH%-PL2(AX9fqD1nh^YcvCMY)mKUwwi72*maQW>AFuOjvVS?_sS8 z_F2L{@0LG#X5}ZvQS#te}0`9}40l?r1mQ zQFMfq_+;IM}v!JSPSd-pH;`uBg0W^a)Vh+Y!4b94iyw*PoSBJ3H!i?oX(hMR~v-4ocB|3Zk3 zqw=orcPl^phZMW;_7!4f_7ez|Fz5`mhEse>48`7{nwo^BT1wj7!sK-Pa^ksF#uw;w z>gSg?Qr>uP`#T0vW7plU^}B`3SR`syzkLa_#B|KJY7g_7+@W{{7Yf3AB1;)}(sc6& zzOV(EUVk+!@Lg+2+Pzf(BoZn?x9Dg=&&6d9W(pD#6B$Nwb|P_-piCb3Ug3R#GgZeP{*mPMS>|pmgwKcae`=yqq4r5%B5iCx|F4EDKWVR@x%5{k{?>DNF9kY7SWdk6mISrdMh#D+iOv#=0$ zc#fdl?#Y%F0^~8OxQw_wJFWpkp)NNhKNzu5RMKS6LK}ZzcA>x{Y-TaN7~$s;HX@nI zFUlcyJL_v#%HN`@W!8yOO(kpVpH&;H$S!uO%vFnI?4JH%Gg358XbjT6N?ddEQHQ>4 z65V`RzzESd%O_h!z537zQc*|uw1Y6tes`t%6G}VwTSy4keBqrQkGxr~yRaUBcTlE& z=!(E_HPuQWo9#KEp2L3F4%6}pfeS16cMIL}iJfA0SRWJ^kM>#$MBoJo3∾Ue~|7 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

&!Wz@o+e!M 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

GzHaaf11Ko^JHtD8fM_vU`R{LVX_Nx!}{cYB&D) zQo0F!`8P~_isKSwLSh3tRzx*?dv9U@XI7(m7vk6iMofQ-=Ui_Y2Pb!aK=8cpDUg^( zBtp1rZlHEG#S*Zh2Asjo4d9_Q@UfM(0a8$-?SL~FmCrDKAuBu(ti=M~r?v)^q*{c> zm#~ZQ8G7{nTG+(tvJmN8vBS;R2q`OFl}RfEO4Np9dHqUHml`wBZFx=-!6IH7`&bl|fu^ z3e?!Mt+D!GW$H_qpVyA-@>)Z8?j@4ig>c04mG*PtN+=f=zrqg61T`1lze0spYq&7x zHDc$u4qUkU8rfCg91B*FAJ?N)u^)3`Lp^97U8t%a!EzZ6L4TtiBPs9y zR*5!m;Zr{xOvm6pZ`p(CHy~9yPQ_rHlrJ@QtinGM`h6`U&fK3-??H^|&Eehe z(K2rKk%ZM&VkaT7AOAa63=@=ibC6m4qK`&F5%pT->Sv;xaC-npo&Tr7vVkq0V@q9W zfH8D^acl-lvCdf33Xg_x!Do;LD{O)oWRf4Hq39?`5lm{}k`HD3iB_~#5WP&kik1l8 zlo+XTq){67xW`m9C36$e&E$TJUgU2lbw2P_LcPXw;U<;x#znBNuH_@;$&D4h^2HXDl19!BY(n zc)QSC^b%f9q3*7t2c0w*)k4pyT)1f=y3t-g)U0R$PF5rAMK!J>o@_Dr4UVj2Nx|+X z{5g|U&BbnlXCgM>h~YxC^xKJwn~Uv*Y`yTuG*oJ!C2C~BEJj0nn~ru8xfJis=`2Ns zFd~VzTcQzcO`9;$%%+%Q6Z7L)T$C>SxBKmP6%J|wIn<{^`!gVj4%i*V1 zVi%#yd@hWdjb_6(L73i{T`+*ui?GRj`2w0_Ew<#yGq!SieE}6J#FkQdeHbw;-`Uf_ z*vRM+etI@Jl)cSDP8~8=!*@_{GlL!A!GT3wm*F4=a5&l)aMu(L4_>12lo~77<+Xqg zZwE;^@3RptC`QxXp6e1^pzF3&uX7Q-C9h1jC%0f;RpO^}6R#XnIW;Q-JwC}%w3hf+ zE28|Lfj1fa4o&!JX^j7g#8SrfJ2VK448h2#eG}H5bZ+&EQuOp@)$&vQ+KDqvjEvSd zGcr;&p>Cw{c48~K(ot1XFd~fVVmGhIO$>Of;nZ&HaZyWp zX?Y347Z2&`2JU72sGIN?ujTxKsptS$8P#9lYB(U6tmnGm7Gj{}_r2Up+dIQVFpAjJ zOa*m!gx`;M!3x_^BzYMYv~|a1FKk68iTb{0V0YF@uKvfgSGH`LSA)K< zv6i}WAJ?HDw&QSyGhoyG9KPu+`kR)r5oFK~EPLIca96Q4t+hsR7|J}tWs&|ht;*nE z*Z>(Ju_}@`pZ(Pg9ZhLu^qHQ`f31^PVYJLbbdU=1JmcJLHtzJnPec>ysUyr_7)82? z9gXKxp}UCJa@@oq1PCuPKzx_QCw@26zMlooo49}9M83=mZjuKhhP%#Bxyn!*nJB=RVHH9_cJ z+*D^q%{)YVYUU#jpI3J`>l#oyOY*xh(2YjyH zSDa#8O2>UMzpV5{I@b~^@D=}uHDspqr9P#I1;hNHdD@8hGP6O#kjtENqEv&%!S-l| e_`~eRYZ9!xrU|8u{-U+zyRS@*Vy?6C@BaWKJz?4a diff --git a/java-format/google-java-format-git-diff.sh b/java-format/google-java-format-git-diff.sh index 536bbd8266a..f9346657890 100755 --- a/java-format/google-java-format-git-diff.sh +++ b/java-format/google-java-format-git-diff.sh @@ -40,7 +40,7 @@ where: show show the effect of the formatting as unified diff" SCRIPT_DIR="$(realpath $(dirname $0))" -JAR_NAME="google-java-format-1.23.0-all-deps.jar" +JAR_NAME="google-java-format-1.27.0-all-deps.jar" # Make sure we have a valid python interpreter. if [ -z "$PYTHON" ]; then From a5e98aebce77c897bfc9a25e42e525836eb08349 Mon Sep 17 00:00:00 2001 From: Weimin Yu Date: Mon, 9 Jun 2025 14:46:54 +0000 Subject: [PATCH 41/49] Add an aggregate module for DNS writers (#2769) Add a new DnsWritersModule for use by the component classes. To override the set of writers installed, we can easily overwrite this file with a private version. --- .../registry/dns/writer/DnsWritersModule.java | 29 +++++++++++++++++++ .../registry/module/RequestComponent.java | 12 ++------ .../module/backend/BackendComponent.java | 4 +-- .../backend/BackendRequestComponent.java | 12 ++------ .../registry/tools/RegistryToolComponent.java | 12 ++------ 5 files changed, 39 insertions(+), 30 deletions(-) create mode 100644 core/src/main/java/google/registry/dns/writer/DnsWritersModule.java diff --git a/core/src/main/java/google/registry/dns/writer/DnsWritersModule.java b/core/src/main/java/google/registry/dns/writer/DnsWritersModule.java new file mode 100644 index 00000000000..e25f5837d51 --- /dev/null +++ b/core/src/main/java/google/registry/dns/writer/DnsWritersModule.java @@ -0,0 +1,29 @@ +// 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.dns.writer; + +import dagger.Module; +import google.registry.dns.writer.clouddns.CloudDnsWriterModule; +import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; + +/** + * Groups all {@link DnsWriter} implementations to be installed. + * + *

To cherry-pick the DNS writers to install, overwrite this file with your private version in + * the release process. + */ +@Module( + includes = {CloudDnsWriterModule.class, DnsUpdateWriterModule.class, VoidDnsWriterModule.class}) +public class DnsWritersModule {} diff --git a/core/src/main/java/google/registry/module/RequestComponent.java b/core/src/main/java/google/registry/module/RequestComponent.java index 5ea879796a6..19dd00f7e2f 100644 --- a/core/src/main/java/google/registry/module/RequestComponent.java +++ b/core/src/main/java/google/registry/module/RequestComponent.java @@ -38,11 +38,8 @@ import google.registry.dns.ReadDnsRefreshRequestsAction; import google.registry.dns.RefreshDnsAction; import google.registry.dns.RefreshDnsOnHostRenameAction; -import google.registry.dns.writer.VoidDnsWriterModule; -import google.registry.dns.writer.clouddns.CloudDnsWriterModule; +import google.registry.dns.writer.DnsWritersModule; import google.registry.dns.writer.dnsupdate.DnsUpdateConfigModule; -import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; -import google.registry.dns.writer.powerdns.PowerDnsWriterModule; import google.registry.export.ExportDomainListsAction; import google.registry.export.ExportPremiumTermsAction; import google.registry.export.ExportReservedTermsAction; @@ -141,15 +138,13 @@ BatchModule.class, BillingModule.class, CheckApiModule.class, - CloudDnsWriterModule.class, ConsoleModule.class, CronModule.class, CustomLogicModule.class, DnsCountQueryCoordinatorModule.class, DnsModule.class, DnsUpdateConfigModule.class, - DnsUpdateWriterModule.class, - PowerDnsWriterModule.class, + DnsWritersModule.class, EppTlsModule.class, EppToolModule.class, IcannReportingModule.class, @@ -162,7 +157,6 @@ Spec11Module.class, TmchModule.class, ToolsServerModule.class, - VoidDnsWriterModule.class, WhiteboxModule.class, WhoisModule.class, }) @@ -354,4 +348,4 @@ abstract class Builder implements RequestComponentBuilder { @Module(subcomponents = RequestComponent.class) static class RequestComponentModule {} -} +} \ No newline at end of file diff --git a/core/src/main/java/google/registry/module/backend/BackendComponent.java b/core/src/main/java/google/registry/module/backend/BackendComponent.java index da1bf752ff4..49a23ca5016 100644 --- a/core/src/main/java/google/registry/module/backend/BackendComponent.java +++ b/core/src/main/java/google/registry/module/backend/BackendComponent.java @@ -22,7 +22,6 @@ import google.registry.config.CloudTasksUtilsModule; import google.registry.config.CredentialModule; import google.registry.config.RegistryConfig.ConfigModule; -import google.registry.dns.writer.VoidDnsWriterModule; import google.registry.dns.writer.powerdns.PowerDnsConfig.PowerDnsConfigModule; import google.registry.export.DriveModule; import google.registry.export.sheet.SheetsServiceModule; @@ -75,11 +74,10 @@ SheetsServiceModule.class, StackdriverModule.class, UrlConnectionServiceModule.class, - VoidDnsWriterModule.class, UtilsModule.class }) interface BackendComponent { BackendRequestHandler requestHandler(); Lazy metricReporter(); -} +} \ No newline at end of file diff --git a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java index af041fef322..02d2b3db9ac 100644 --- a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java +++ b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java @@ -34,11 +34,8 @@ import google.registry.dns.ReadDnsRefreshRequestsAction; import google.registry.dns.RefreshDnsAction; import google.registry.dns.RefreshDnsOnHostRenameAction; -import google.registry.dns.writer.VoidDnsWriterModule; -import google.registry.dns.writer.clouddns.CloudDnsWriterModule; +import google.registry.dns.writer.DnsWritersModule; import google.registry.dns.writer.dnsupdate.DnsUpdateConfigModule; -import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; -import google.registry.dns.writer.powerdns.PowerDnsWriterModule; import google.registry.export.ExportDomainListsAction; import google.registry.export.ExportPremiumTermsAction; import google.registry.export.ExportReservedTermsAction; @@ -83,14 +80,12 @@ modules = { BatchModule.class, BillingModule.class, - CloudDnsWriterModule.class, CronModule.class, CustomLogicModule.class, DnsCountQueryCoordinatorModule.class, DnsModule.class, DnsUpdateConfigModule.class, - DnsUpdateWriterModule.class, - PowerDnsWriterModule.class, + DnsWritersModule.class, IcannReportingModule.class, RdeModule.class, ReportingModule.class, @@ -98,7 +93,6 @@ SheetModule.class, Spec11Module.class, TmchModule.class, - VoidDnsWriterModule.class, WhiteboxModule.class, }) public interface BackendRequestComponent { @@ -193,4 +187,4 @@ abstract class Builder implements RequestComponentBuilder Date: Wed, 11 Jun 2025 16:30:15 -0400 Subject: [PATCH 42/49] Add some changes related to RDAP Feb 2024 profile (#2759) This implements two type of changes: 1. changing the link type for things like the terms of service 2. adding the request URL to each and every link with the "value" field. This is a bit tricky to implement because the links are generated in various places, but we can implement it by adding it to the results after generation. See b/418782147 for more information --- .../registry/rdap/AbstractJsonableObject.java | 2 +- .../google/registry/rdap/RdapActionBase.java | 40 +- .../rdap/RdapIcannStandardInformation.java | 4 +- .../registry/rdap/RdapJsonFormatter.java | 2 +- .../registry/rdap/RdapSearchActionBase.java | 2 - .../registry/rdap/RdapActionBaseTest.java | 6 - .../registry/rdap/RdapActionBaseTestCase.java | 181 ++++++- .../registry/rdap/RdapDataStructuresTest.java | 5 +- .../registry/rdap/RdapDomainActionTest.java | 41 +- .../rdap/RdapDomainSearchActionTest.java | 3 +- .../registry/rdap/RdapEntityActionTest.java | 286 ++++++----- .../rdap/RdapEntitySearchActionTest.java | 486 +++++++++++------- .../registry/rdap/RdapHelpActionTest.java | 6 +- .../rdap/RdapNameserverActionTest.java | 167 ++---- .../rdap/RdapNameserverSearchActionTest.java | 105 ++-- .../google/registry/rdap/RdapTestHelper.java | 103 +--- .../rdap/rdap_associated_contact.json | 11 +- ...p_associated_contact_no_personal_data.json | 3 +- .../google/registry/rdap/rdap_contact.json | 13 +- .../registry/rdap/rdap_contact_deleted.json | 8 +- ..._contact_no_personal_data_with_remark.json | 3 +- .../google/registry/rdap/rdap_domain.json | 27 +- .../rdap/rdap_domain_add_grace_period.json | 12 +- .../rdap_domain_auto_renew_grace_period.json | 12 +- .../registry/rdap/rdap_domain_cat2.json | 27 +- .../registry/rdap/rdap_domain_deleted.json | 27 +- ...ap_domain_explicit_renew_grace_period.json | 12 +- ..._domain_no_contacts_exist_with_remark.json | 18 +- ...rdap_domain_no_registrant_with_remark.json | 24 +- ...ending_delete_redemption_grace_period.json | 12 +- ..._domain_redacted_contacts_with_remark.json | 27 +- .../rdap_domain_transfer_grace_period.json | 12 +- .../registry/rdap/rdap_domain_unicode.json | 27 +- ...omain_unicode_no_contacts_with_remark.json | 27 +- .../rdap/rdap_domains_four_truncated.json | 35 +- .../rdap_domains_four_with_one_unicode.json | 32 +- ...mains_four_with_one_unicode_truncated.json | 35 +- .../registry/rdap/rdap_domains_two.json | 26 +- .../google/registry/rdap/rdap_error.json | 10 +- .../google/registry/rdap/rdap_help_index.json | 16 +- .../google/registry/rdap/rdap_help_tos.json | 10 +- .../google/registry/rdap/rdap_host.json | 12 +- .../registry/rdap/rdap_host_external.json | 12 +- .../registry/rdap/rdap_host_linked.json | 12 +- .../registry/rdap/rdap_host_unicode.json | 14 +- .../rdap_incomplete_domain_result_set.json | 29 +- .../rdap/rdap_incomplete_domains.json | 26 +- .../registry/rdap/rdap_multiple_contacts.json | 2 +- .../rdap/rdap_multiple_contacts2.json | 16 +- .../registry/rdap/rdap_multiple_hosts.json | 16 +- .../rdap/rdap_nontruncated_contacts.json | 22 +- .../rdap/rdap_nontruncated_domains.json | 32 +- .../rdap/rdap_nontruncated_hosts.json | 22 +- .../rdap/rdap_nontruncated_registrars.json | 22 +- .../google/registry/rdap/rdap_registrar.json | 13 +- .../registry/rdap/rdap_registrar_test.json | 6 +- .../rdap/rdap_truncated_contacts.json | 25 +- .../registry/rdap/rdap_truncated_hosts.json | 25 +- .../rdap/rdap_truncated_mixed_entities.json | 25 +- .../rdap/rdap_truncated_registrars.json | 25 +- .../rdap/rdap_unformatted_output.json | 2 +- .../registry/rdap/rdapjson_toplevel.json | 4 +- .../rdap/rdapjson_toplevel_domain.json | 8 +- 63 files changed, 1326 insertions(+), 949 deletions(-) diff --git a/core/src/main/java/google/registry/rdap/AbstractJsonableObject.java b/core/src/main/java/google/registry/rdap/AbstractJsonableObject.java index 983d9bc98d7..95994b2683e 100644 --- a/core/src/main/java/google/registry/rdap/AbstractJsonableObject.java +++ b/core/src/main/java/google/registry/rdap/AbstractJsonableObject.java @@ -336,7 +336,7 @@ private static JsonElement toJsonElement(String name, Member member, Object obje // According to RFC 9083 section 3, the syntax of dates and times is defined in RFC3339. // // According to RFC3339, we should use ISO8601, which is what DateTime.toString does! - return new JsonPrimitive(((DateTime) object).toString()); + return new JsonPrimitive(object.toString()); } if (object == null) { return JsonNull.INSTANCE; diff --git a/core/src/main/java/google/registry/rdap/RdapActionBase.java b/core/src/main/java/google/registry/rdap/RdapActionBase.java index 66389eb18a7..4ccc734e461 100644 --- a/core/src/main/java/google/registry/rdap/RdapActionBase.java +++ b/core/src/main/java/google/registry/rdap/RdapActionBase.java @@ -24,10 +24,14 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.collect.Streams; import com.google.common.flogger.FluentLogger; import com.google.common.net.MediaType; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import google.registry.config.RegistryConfig.Config; import google.registry.model.EppResource; import google.registry.model.registrar.Registrar; @@ -41,6 +45,7 @@ import google.registry.request.Parameter; import google.registry.request.RequestMethod; import google.registry.request.RequestPath; +import google.registry.request.RequestUrl; import google.registry.request.Response; import google.registry.util.Clock; import jakarta.inject.Inject; @@ -75,6 +80,7 @@ protected enum DeletedItemHandling { @Inject Response response; @Inject @RequestMethod Action.Method requestMethod; @Inject @RequestPath String requestPath; + @Inject @RequestUrl String requestUrl; @Inject RdapAuthorization rdapAuthorization; @Inject RdapJsonFormatter rdapJsonFormatter; @Inject @Parameter("includeDeleted") Optional includeDeletedParam; @@ -198,7 +204,9 @@ void setPayload(ReplyPayloadBase replyObject) { TopLevelReplyObject topLevelObject = TopLevelReplyObject.create(replyObject, rdapJsonFormatter.createTosNotice()); Gson gson = formatOutputParam.orElse(false) ? FORMATTED_OUTPUT_GSON : GSON; - response.setPayload(gson.toJson(topLevelObject.toJson())); + JsonObject jsonResult = topLevelObject.toJson(); + addLinkValuesRecursively(jsonResult); + response.setPayload(gson.toJson(jsonResult)); } /** @@ -264,4 +272,34 @@ DateTime getRequestTime() { return rdapJsonFormatter.getRequestTime(); } + /** + * Adds a request-referencing "value" to each link object. + * + *

This is the "context URI" as described in RFC 8288. Basically, this contains a reference to + * the request URL that generated this RDAP response. + * + *

This is required per the RDAP February 2024 response profile sections 2.6.3 and 2.10, and + * the technical implementation guide sections 3.2 and 3.3.2. + * + *

We must do this here (instead of where the links are generated) because many of the links + * (e.g. terms of service) are static constants, and thus cannot by default know what the request + * URL was. + */ + private void addLinkValuesRecursively(JsonElement jsonElement) { + if (jsonElement instanceof JsonArray jsonArray) { + jsonArray.forEach(this::addLinkValuesRecursively); + } else if (jsonElement instanceof JsonObject jsonObject) { + if (jsonObject.get("links") instanceof JsonArray linksArray) { + addLinkValues(linksArray); + } + jsonObject.entrySet().forEach(entry -> addLinkValuesRecursively(entry.getValue())); + } + } + + private void addLinkValues(JsonArray linksArray) { + Streams.stream(linksArray) + .map(JsonElement::getAsJsonObject) + .filter(o -> !o.has("value")) + .forEach(o -> o.addProperty("value", requestUrl)); + } } diff --git a/core/src/main/java/google/registry/rdap/RdapIcannStandardInformation.java b/core/src/main/java/google/registry/rdap/RdapIcannStandardInformation.java index 3b8e4a70065..c1b3479b348 100644 --- a/core/src/main/java/google/registry/rdap/RdapIcannStandardInformation.java +++ b/core/src/main/java/google/registry/rdap/RdapIcannStandardInformation.java @@ -44,7 +44,7 @@ public class RdapIcannStandardInformation { + " https://icann.org/epp") .addLink( Link.builder() - .setRel("alternate") + .setRel("glossary") .setHref("https://icann.org/epp") .setType("text/html") .build()) @@ -57,7 +57,7 @@ public class RdapIcannStandardInformation { .setDescription("URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf") .addLink( Link.builder() - .setRel("alternate") + .setRel("help") .setHref("https://icann.org/wicf") .setType("text/html") .build()) diff --git a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java index d79056b4137..61208f40a42 100644 --- a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java +++ b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java @@ -271,7 +271,7 @@ Notice createTosNotice() { URI htmlUri = htmlBaseURI.resolve(rdapTosStaticUrl); noticeBuilder.addLink( Link.builder() - .setRel("alternate") + .setRel("terms-of-service") .setHref(htmlUri.toString()) .setType("text/html") .build()); diff --git a/core/src/main/java/google/registry/rdap/RdapSearchActionBase.java b/core/src/main/java/google/registry/rdap/RdapSearchActionBase.java index 8b4d65b0b60..2daa836c869 100644 --- a/core/src/main/java/google/registry/rdap/RdapSearchActionBase.java +++ b/core/src/main/java/google/registry/rdap/RdapSearchActionBase.java @@ -31,7 +31,6 @@ import google.registry.request.HttpException.UnprocessableEntityException; import google.registry.request.Parameter; import google.registry.request.ParameterMap; -import google.registry.request.RequestUrl; import jakarta.inject.Inject; import jakarta.persistence.criteria.CriteriaBuilder; import java.io.UnsupportedEncodingException; @@ -54,7 +53,6 @@ public abstract class RdapSearchActionBase extends RdapActionBase { private static final int RESULT_SET_SIZE_SCALING_FACTOR = 30; - @Inject @RequestUrl String requestUrl; @Inject @ParameterMap ImmutableListMultimap parameterMap; @Inject @Parameter("cursor") Optional cursorTokenParam; @Inject @Parameter("registrar") Optional registrarParam; diff --git a/core/src/test/java/google/registry/rdap/RdapActionBaseTest.java b/core/src/test/java/google/registry/rdap/RdapActionBaseTest.java index 588dafdd688..d5dd9c90b74 100644 --- a/core/src/test/java/google/registry/rdap/RdapActionBaseTest.java +++ b/core/src/test/java/google/registry/rdap/RdapActionBaseTest.java @@ -90,12 +90,6 @@ void testRuntimeException_returns500Error() { assertThat(response.getStatus()).isEqualTo(500); } - @Test - void testValidName_works() { - assertThat(generateActualJson("no.thing")).isEqualTo(loadJsonFile("rdapjson_toplevel.json")); - assertThat(response.getStatus()).isEqualTo(200); - } - @Test void testContentType_rdapjson_utf8() { generateActualJson("no.thing"); diff --git a/core/src/test/java/google/registry/rdap/RdapActionBaseTestCase.java b/core/src/test/java/google/registry/rdap/RdapActionBaseTestCase.java index b278046be11..a7232101bf1 100644 --- a/core/src/test/java/google/registry/rdap/RdapActionBaseTestCase.java +++ b/core/src/test/java/google/registry/rdap/RdapActionBaseTestCase.java @@ -22,7 +22,12 @@ import static google.registry.request.Action.Method.HEAD; import static org.mockito.Mockito.mock; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import google.registry.model.console.User; import google.registry.model.console.UserRoles; import google.registry.persistence.transaction.JpaTestExtensions; @@ -35,6 +40,7 @@ import google.registry.util.TypeUtils; import java.util.HashMap; import java.util.Optional; +import javax.annotation.Nullable; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; @@ -43,6 +49,7 @@ abstract class RdapActionBaseTestCase { protected final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); + static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); @RegisterExtension final JpaIntegrationTestExtension jpa = @@ -107,18 +114,13 @@ void loginAsAdmin() { metricRole = ADMINISTRATOR; } - JsonObject generateActualJson(String domainName) { - action.requestPath = actionPath + domainName; - action.requestMethod = GET; - action.run(); - return RdapTestHelper.parseJsonObject(response.getPayload()); + JsonObject generateActualJson(String name) { + return RdapTestHelper.parseJsonObject(runAction(name)); } - String generateHeadPayload(String domainName) { - action.requestPath = actionPath + domainName; + String generateHeadPayload(String name) { action.requestMethod = HEAD; - action.run(); - return response.getPayload(); + return runAction(name); } JsonObject generateExpectedJsonError(String description, int code) { @@ -138,16 +140,135 @@ JsonObject generateExpectedJsonError(String description, int code) { "TITLE", title, "CODE", - String.valueOf(code)); + String.valueOf(code), + "REQUEST_URL", + action.requestUrl); + } + + JsonFileBuilder jsonFileBuilder() { + return new JsonFileBuilder(action.requestUrl); + } + + private String runAction(String name) { + action.requestPath = actionPath + name; + action.requestUrl = "https://example.tld" + actionPath + name; + action.run(); + return response.getPayload(); } - static JsonFileBuilder jsonFileBuilder() { - return new JsonFileBuilder(); + JsonElement createTosNotice() { + return JsonParser.parseString( +""" +{ + "title": "RDAP Terms of Service", + "description": [ + "By querying our Domain Database, you are agreeing to comply with these terms so please read \ +them carefully.", + "Any information provided is 'as is' without any guarantee of accuracy.", + "Please do not misuse the Domain Database. It is intended solely for query-based access.", + "Don't use the Domain Database to allow, enable, or otherwise support the transmission of mass \ +unsolicited, commercial advertising or solicitations.", + "Don't access our Domain Database through the use of high volume, automated electronic \ +processes that send queries or data to the systems of any ICANN-accredited registrar.", + "You may only use the information contained in the Domain Database for lawful purposes.", + "Do not compile, repackage, disseminate, or otherwise use the information contained in the \ +Domain Database in its entirety, or in any substantial portion, without our prior written \ +permission.", + "We may retain certain details about queries to our Domain Database for the purposes of \ +detecting and preventing misuse.", + "We reserve the right to restrict or deny your access to the database if we suspect that you \ +have failed to comply with these terms.", + "We reserve the right to modify this agreement at any time." + ], + "links": [ + { + "rel": "self", + "href": "https://example.tld/rdap/help/tos", + "type": "application/rdap+json", + "value": "%REQUEST_URL%" + }, + { + "rel": "terms-of-service", + "href": "https://www.example.tld/about/rdap/tos.html", + "type": "text/html", + "value": "%REQUEST_URL%" + } + ] +} +""" + .replaceAll("%REQUEST_URL%", action.requestUrl)); + } + + JsonObject addPermanentBoilerplateNotices(JsonObject jsonObject) { + if (!jsonObject.has("notices")) { + jsonObject.add("notices", new JsonArray()); + } + JsonArray notices = jsonObject.getAsJsonArray("notices"); + notices.add(createTosNotice()); + notices.add( + JsonParser.parseString( +""" +{ + "description": [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars \ +version 1.0" + ] +} +""")); + return jsonObject; + } + + JsonObject addDomainBoilerplateNotices(JsonObject jsonObject) { + addPermanentBoilerplateNotices(jsonObject); + JsonArray notices = jsonObject.getAsJsonArray("notices"); + notices.add( + JsonParser.parseString( +""" +{ + "title": "Status Codes", + "description": [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links": [ + { + "rel": "glossary", + "href": "https://icann.org/epp", + "type": "text/html", + "value": "%REQUEST_URL%" + } + ] +} +""" + .replaceAll("%REQUEST_URL%", action.requestUrl))); + notices.add( + JsonParser.parseString( +""" +{ + "title": "RDDS Inaccuracy Complaint Form", + "description": [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links": [ + { + "rel": "help", + "href": "https://icann.org/wicf", + "type": "text/html", + "value": "%REQUEST_URL%" + } + ] +} +""" + .replaceAll("%REQUEST_URL%", action.requestUrl))); + return jsonObject; } protected static final class JsonFileBuilder { private final HashMap substitutions = new HashMap<>(); + private JsonFileBuilder(String requestUrl) { + substitutions.put("REQUEST_URL", requestUrl); + } + public JsonObject load(String filename) { return RdapTestHelper.loadJsonFile(filename, substitutions); } @@ -158,6 +279,14 @@ public JsonFileBuilder put(String key, String value) { return this; } + public JsonFileBuilder putAll(String... keysAndValues) { + checkArgument(keysAndValues.length % 2 == 0); + for (int i = 0; i < keysAndValues.length; i += 2) { + put(keysAndValues[i], keysAndValues[i + 1]); + } + return this; + } + public JsonFileBuilder put(String key, int index, String value) { return put(String.format("%s%d", key, index), value); } @@ -189,10 +318,38 @@ JsonFileBuilder addRegistrar(String fullName) { return putNext("REGISTRAR_FULL_NAME_", fullName); } + JsonFileBuilder addFullRegistrar( + String handle, @Nullable String fullName, String status, @Nullable String address) { + if (fullName != null) { + putNext("REGISTRAR_FULLNAME_", fullName); + } + if (address != null) { + putNext("REGISTRAR_ADDRESS_", address); + } + return putNext("REGISTRAR_HANDLE_", handle, "STATUS_", status); + } + JsonFileBuilder addContact(String handle) { return putNext("CONTACT_HANDLE_", handle); } + JsonFileBuilder addFullContact( + String handle, + @Nullable String status, + @Nullable String fullName, + @Nullable String address) { + if (fullName != null) { + putNext("CONTACT_FULLNAME_", fullName); + } + if (address != null) { + putNext("CONTACT_ADDRESS_", address); + } + if (status != null) { + putNext("STATUS_", status); + } + return putNext("CONTACT_HANDLE_", handle); + } + JsonFileBuilder setNextQuery(String nextQuery) { return put("NEXT_QUERY", nextQuery); } diff --git a/core/src/test/java/google/registry/rdap/RdapDataStructuresTest.java b/core/src/test/java/google/registry/rdap/RdapDataStructuresTest.java index 33b069f0e65..6d742dcb2e3 100644 --- a/core/src/test/java/google/registry/rdap/RdapDataStructuresTest.java +++ b/core/src/test/java/google/registry/rdap/RdapDataStructuresTest.java @@ -59,9 +59,12 @@ void testLink() { .setRel("myRel") .setTitle("myTitle") .setType("myType") + .setValue("myValue") .build(); assertThat(link.toJson()) - .isEqualTo(createJson("{'href':'myHref','rel':'myRel','title':'myTitle','type':'myType'}")); + .isEqualTo( + createJson( + "{'href':'myHref','rel':'myRel','title':'myTitle','type':'myType','value':'myValue'}")); assertRestrictedNames(link, "links[]"); } diff --git a/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java b/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java index 4ea3d648ab2..81f86946b96 100644 --- a/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java +++ b/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java @@ -230,16 +230,11 @@ void beforeEach() { clock.nowUtc().minusMonths(6))); } - private JsonObject addBoilerplate(JsonObject obj) { - RdapTestHelper.addDomainBoilerplateNotices(obj, "https://example.tld/rdap/"); - return obj; - } - private void assertProperResponseForCatLol(String queryString, String expectedOutputFile) { assertAboutJson() .that(generateActualJson(queryString)) .isEqualTo( - addBoilerplate( + addDomainBoilerplateNotices( jsonFileBuilder() .addDomain("cat.lol", "C-LOL") .addContact("4-ROID") @@ -357,7 +352,7 @@ void testIdnDomain_works() { assertAboutJson() .that(generateActualJson("cat.みんな")) .isEqualTo( - addBoilerplate( + addDomainBoilerplateNotices( jsonFileBuilder() .addDomain("cat.みんな", "1D-Q9JYB4C") .addContact("19-ROID") @@ -376,7 +371,7 @@ void testIdnDomainWithPercentEncoding_works() { assertAboutJson() .that(generateActualJson("cat.%E3%81%BF%E3%82%93%E3%81%AA")) .isEqualTo( - addBoilerplate( + addDomainBoilerplateNotices( jsonFileBuilder() .addDomain("cat.みんな", "1D-Q9JYB4C") .addContact("19-ROID") @@ -395,7 +390,7 @@ void testPunycodeDomain_works() { assertAboutJson() .that(generateActualJson("cat.xn--q9jyb4c")) .isEqualTo( - addBoilerplate( + addDomainBoilerplateNotices( jsonFileBuilder() .addDomain("cat.みんな", "1D-Q9JYB4C") .addContact("19-ROID") @@ -414,7 +409,7 @@ void testMultilevelDomain_works() { assertAboutJson() .that(generateActualJson("cat.1.tld")) .isEqualTo( - addBoilerplate( + addDomainBoilerplateNotices( jsonFileBuilder() .addDomain("cat.1.tld", "25-1_TLD") .addContact("21-ROID") @@ -473,7 +468,7 @@ void testDeletedDomain_works_loggedInAsCorrectRegistrar() { assertAboutJson() .that(generateActualJson("dodo.lol")) .isEqualTo( - addBoilerplate( + addDomainBoilerplateNotices( jsonFileBuilder() .addDomain("dodo.lol", "15-LOL") .addContact("11-ROID") @@ -493,7 +488,7 @@ void testDeletedDomain_works_loggedInAsAdmin() { assertAboutJson() .that(generateActualJson("dodo.lol")) .isEqualTo( - addBoilerplate( + addDomainBoilerplateNotices( jsonFileBuilder() .addDomain("dodo.lol", "15-LOL") .addContact("11-ROID") @@ -512,7 +507,9 @@ void testAddGracePeriod() { "addgraceperiod", "lol", clock.nowUtc(), clock.nowUtc().plusYears(1)); assertAboutJson() .that(generateActualJson("addgraceperiod.lol")) - .isEqualTo(addBoilerplate(jsonFileBuilder().load("rdap_domain_add_grace_period.json"))); + .isEqualTo( + addDomainBoilerplateNotices( + jsonFileBuilder().load("rdap_domain_add_grace_period.json"))); } @Test @@ -522,7 +519,8 @@ void testAutoRenewGracePeriod() { assertAboutJson() .that(generateActualJson("autorenew.lol")) .isEqualTo( - addBoilerplate(jsonFileBuilder().load("rdap_domain_auto_renew_grace_period.json"))); + addDomainBoilerplateNotices( + jsonFileBuilder().load("rdap_domain_auto_renew_grace_period.json"))); } @Test @@ -545,7 +543,7 @@ void testRedemptionGracePeriod() { assertAboutJson() .that(generateActualJson("redemption.lol")) .isEqualTo( - addBoilerplate( + addDomainBoilerplateNotices( jsonFileBuilder().load("rdap_domain_pending_delete_redemption_grace_period.json"))); } @@ -568,7 +566,8 @@ void testRenewGracePeriod() { assertAboutJson() .that(generateActualJson("renew.lol")) .isEqualTo( - addBoilerplate(jsonFileBuilder().load("rdap_domain_explicit_renew_grace_period.json"))); + addDomainBoilerplateNotices( + jsonFileBuilder().load("rdap_domain_explicit_renew_grace_period.json"))); } @Test @@ -590,7 +589,8 @@ void testTransferGracePeriod() { assertAboutJson() .that(generateActualJson("transfer.lol")) .isEqualTo( - addBoilerplate(jsonFileBuilder().load("rdap_domain_transfer_grace_period.json"))); + addDomainBoilerplateNotices( + jsonFileBuilder().load("rdap_domain_transfer_grace_period.json"))); } @Test @@ -631,12 +631,15 @@ void testBlockedByBsa() { "rel", "alternate", "type", - "text/html"))); + "text/html", + "value", + "https://example.tld/rdap/domain/example.lol"))); + JsonObject actuaResponse = generateActualJson("example.lol"); JsonObject expectedErrorResponse = generateExpectedJsonError("example.lol blocked by BSA", 404); expectedErrorResponse .getAsJsonArray("notices") .add(RdapTestHelper.GSON.toJsonTree(expectedBsaNotice)); - assertAboutJson().that(generateActualJson("example.lol")).isEqualTo(expectedErrorResponse); + assertAboutJson().that(actuaResponse).isEqualTo(expectedErrorResponse); assertThat(response.getStatus()).isEqualTo(404); } diff --git a/core/src/test/java/google/registry/rdap/RdapDomainSearchActionTest.java b/core/src/test/java/google/registry/rdap/RdapDomainSearchActionTest.java index 9ae1e4b78fe..44a059c3e9e 100644 --- a/core/src/test/java/google/registry/rdap/RdapDomainSearchActionTest.java +++ b/core/src/test/java/google/registry/rdap/RdapDomainSearchActionTest.java @@ -473,8 +473,7 @@ private void runSuccessfulTestWithCat2Lol( private JsonObject wrapInSearchReply(JsonObject obj) { obj = RdapTestHelper.wrapInSearchReply("domainSearchResults", obj); - RdapTestHelper.addDomainBoilerplateNotices(obj, "https://example.tld/rdap/"); - return obj; + return addDomainBoilerplateNotices(obj); } private void runSuccessfulTest(RequestType requestType, String queryString, JsonObject expected) { diff --git a/core/src/test/java/google/registry/rdap/RdapEntityActionTest.java b/core/src/test/java/google/registry/rdap/RdapEntityActionTest.java index e28fe9298c0..ec66e681810 100644 --- a/core/src/test/java/google/registry/rdap/RdapEntityActionTest.java +++ b/core/src/test/java/google/registry/rdap/RdapEntityActionTest.java @@ -15,7 +15,6 @@ package google.registry.rdap; import static com.google.common.truth.Truth.assertThat; -import static google.registry.rdap.RdapTestHelper.loadJsonFile; import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistSimpleResources; @@ -28,7 +27,6 @@ import static org.mockito.Mockito.verify; import com.google.common.collect.ImmutableList; -import com.google.gson.JsonObject; import google.registry.model.contact.Contact; import google.registry.model.host.Host; import google.registry.model.registrar.Registrar; @@ -39,13 +37,15 @@ import google.registry.request.Action; import google.registry.testing.FullFieldsTestEntityHelper; import java.util.Optional; -import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** Unit tests for {@link RdapEntityAction}. */ class RdapEntityActionTest extends RdapActionBaseTestCase { + private static final String CONTACT_NAME = "(◕‿◕)"; + private static final String CONTACT_ADDRESS = "\"1 Smiley Row\", \"Suite みんな\""; + RdapEntityActionTest() { super(RdapEntityAction.class); } @@ -67,7 +67,7 @@ void beforeEach() { registrant = FullFieldsTestEntityHelper.makeAndPersistContact( "8372808-REG", - "(◕‿◕)", + CONTACT_NAME, "lol@cat.みんな", ImmutableList.of("1 Smiley Row", "Suite みんな"), clock.nowUtc(), @@ -75,7 +75,7 @@ void beforeEach() { adminContact = FullFieldsTestEntityHelper.makeAndPersistContact( "8372808-ADM", - "(◕‿◕)", + CONTACT_NAME, "lol@cat.みんな", ImmutableList.of("1 Smiley Row", "Suite みんな"), clock.nowUtc(), @@ -83,7 +83,7 @@ void beforeEach() { techContact = FullFieldsTestEntityHelper.makeAndPersistContact( "8372808-TEC", - "(◕‿◕)", + CONTACT_NAME, "lol@cat.みんな", ImmutableList.of("1 Smiley Row", "Suite みんな"), clock.nowUtc(), @@ -110,7 +110,7 @@ void beforeEach() { disconnectedContact = FullFieldsTestEntityHelper.makeAndPersistContact( "8372808-DIS", - "(◕‿◕)", + CONTACT_NAME, "lol@cat.みんな", ImmutableList.of("1 Smiley Row", "Suite みんな"), clock.nowUtc(), @@ -123,186 +123,191 @@ void beforeEach() { clock.nowUtc().minusMonths(6)); } - private JsonObject generateExpectedJson( - String handle, - String fullName, - String status, - @Nullable String address, - String expectedOutputFile) { - return loadJsonFile( - expectedOutputFile, - "NAME", handle, - "FULLNAME", fullName, - "ADDRESS", (address == null) ? "\"1 Smiley Row\", \"Suite みんな\"" : address, - "TYPE", "entity", - "STATUS", status); - } - - private JsonObject generateExpectedJsonWithTopLevelEntries( - String handle, - String expectedOutputFile) { - return generateExpectedJsonWithTopLevelEntries( - handle, "(◕‿◕)", "active", null, expectedOutputFile); - } - - private JsonObject generateExpectedJsonWithTopLevelEntries( - String handle, - String fullName, - String status, - String address, - String expectedOutputFile) { - JsonObject obj = generateExpectedJson(handle, fullName, status, address, expectedOutputFile); - RdapTestHelper.addNonDomainBoilerplateNotices(obj, "https://example.tld/rdap/"); - return obj; - } - - private void runSuccessfulHandleTest(String handleQuery, String fileName) { - runSuccessfulHandleTest(handleQuery, "(◕‿◕)", "active", null, fileName); - } - - private void runSuccessfulHandleTest(String handleQuery, String fullName, String fileName) { - runSuccessfulHandleTest(handleQuery, fullName, "active", null, fileName); - } - - private void runSuccessfulHandleTest( - String handleQuery, - String fullName, - String rdapStatus, - String address, - String fileName) { - assertAboutJson() - .that(generateActualJson(handleQuery)) - .isEqualTo( - generateExpectedJsonWithTopLevelEntries( - handleQuery, fullName, rdapStatus, address, fileName)); - assertThat(response.getStatus()).isEqualTo(200); - } - - private void runNotFoundTest(String handleQuery) { - assertAboutJson() - .that(generateActualJson(handleQuery)) - .isEqualTo(generateExpectedJsonError(handleQuery + " not found", 404)); - assertThat(response.getStatus()).isEqualTo(404); - } - @Test void testUnknownEntity_RoidPattern_notFound() { - runNotFoundTest("_MISSING-ENTITY_"); + assertAboutJson() + .that(generateActualJson("_MISSING-ENTITY_")) + .isEqualTo(generateExpectedJsonError("_MISSING-ENTITY_ not found", 404)); + assertThat(response.getStatus()).isEqualTo(404); } @Test void testUnknownEntity_IanaPattern_notFound() { - runNotFoundTest("123"); + assertAboutJson() + .that(generateActualJson("123")) + .isEqualTo(generateExpectedJsonError("123 not found", 404)); + assertThat(response.getStatus()).isEqualTo(404); } @Test void testUnknownEntity_notRoidNotIana_notFound() { // Since we allow search by registrar name, every string is a possible name - runNotFoundTest("some,random,string"); + assertAboutJson() + .that(generateActualJson("some,random,string")) + .isEqualTo(generateExpectedJsonError("some,random,string not found", 404)); + assertThat(response.getStatus()).isEqualTo(404); } @Test void testValidRegistrantContact_works() { login("evilregistrar"); - runSuccessfulHandleTest(registrant.getRepoId(), "rdap_associated_contact.json"); + assertAboutJson() + .that(generateActualJson(registrant.getRepoId())) + .isEqualTo( + addPermanentBoilerplateNotices( + jsonFileBuilder() + .addFullContact(registrant.getRepoId(), null, CONTACT_NAME, CONTACT_ADDRESS) + .load("rdap_associated_contact.json"))); } @Test void testValidRegistrantContact_found_asAdministrator() { loginAsAdmin(); - runSuccessfulHandleTest(registrant.getRepoId(), "rdap_associated_contact.json"); + assertAboutJson() + .that(generateActualJson(registrant.getRepoId())) + .isEqualTo( + addPermanentBoilerplateNotices( + jsonFileBuilder() + .addFullContact(registrant.getRepoId(), null, CONTACT_NAME, CONTACT_ADDRESS) + .load("rdap_associated_contact.json"))); } @Test void testValidRegistrantContact_found_notLoggedIn() { - runSuccessfulHandleTest( - registrant.getRepoId(), - "(◕‿◕)", - "active", - null, - "rdap_associated_contact_no_personal_data.json"); + assertAboutJson() + .that(generateActualJson(registrant.getRepoId())) + .isEqualTo( + addPermanentBoilerplateNotices( + jsonFileBuilder() + .addFullContact(registrant.getRepoId(), "active", CONTACT_NAME, CONTACT_ADDRESS) + .load("rdap_associated_contact_no_personal_data.json"))); } @Test void testValidRegistrantContact_found_loggedInAsOtherRegistrar() { login("otherregistrar"); - runSuccessfulHandleTest( - registrant.getRepoId(), - "(◕‿◕)", - "active", - null, - "rdap_associated_contact_no_personal_data.json"); + assertAboutJson() + .that(generateActualJson(registrant.getRepoId())) + .isEqualTo( + addPermanentBoilerplateNotices( + jsonFileBuilder() + .addFullContact(registrant.getRepoId(), "active", CONTACT_NAME, CONTACT_ADDRESS) + .load("rdap_associated_contact_no_personal_data.json"))); } @Test void testValidAdminContact_works() { login("evilregistrar"); - runSuccessfulHandleTest(adminContact.getRepoId(), "rdap_associated_contact.json"); + assertAboutJson() + .that(generateActualJson(adminContact.getRepoId())) + .isEqualTo( + addPermanentBoilerplateNotices( + jsonFileBuilder() + .addFullContact(adminContact.getRepoId(), null, CONTACT_NAME, CONTACT_ADDRESS) + .load("rdap_associated_contact.json"))); } @Test void testValidTechContact_works() { login("evilregistrar"); - runSuccessfulHandleTest(techContact.getRepoId(), "rdap_associated_contact.json"); + assertAboutJson() + .that(generateActualJson(techContact.getRepoId())) + .isEqualTo( + addPermanentBoilerplateNotices( + jsonFileBuilder() + .addFullContact(techContact.getRepoId(), null, CONTACT_NAME, CONTACT_ADDRESS) + .load("rdap_associated_contact.json"))); } @Test void testValidDisconnectedContact_works() { login("evilregistrar"); - runSuccessfulHandleTest(disconnectedContact.getRepoId(), "rdap_contact.json"); + assertAboutJson() + .that(generateActualJson(disconnectedContact.getRepoId())) + .isEqualTo( + addPermanentBoilerplateNotices( + jsonFileBuilder() + .addFullContact( + disconnectedContact.getRepoId(), "active", CONTACT_NAME, CONTACT_ADDRESS) + .load("rdap_contact.json"))); } @Test void testDeletedContact_notFound() { - runNotFoundTest(deletedContact.getRepoId()); + String repoId = deletedContact.getRepoId(); + assertAboutJson() + .that(generateActualJson(repoId)) + .isEqualTo(generateExpectedJsonError(repoId + " not found", 404)); + assertThat(response.getStatus()).isEqualTo(404); } @Test void testDeletedContact_notFound_includeDeletedSetFalse() { action.includeDeletedParam = Optional.of(false); - runNotFoundTest(deletedContact.getRepoId()); + String repoId = deletedContact.getRepoId(); + assertAboutJson() + .that(generateActualJson(repoId)) + .isEqualTo(generateExpectedJsonError(repoId + " not found", 404)); + assertThat(response.getStatus()).isEqualTo(404); } @Test void testDeletedContact_notFound_notLoggedIn() { action.includeDeletedParam = Optional.of(true); - runNotFoundTest(deletedContact.getRepoId()); + String repoId = deletedContact.getRepoId(); + assertAboutJson() + .that(generateActualJson(repoId)) + .isEqualTo(generateExpectedJsonError(repoId + " not found", 404)); + assertThat(response.getStatus()).isEqualTo(404); } @Test void testDeletedContact_notFound_loggedInAsDifferentRegistrar() { login("idnregistrar"); action.includeDeletedParam = Optional.of(true); - runNotFoundTest(deletedContact.getRepoId()); + String repoId = deletedContact.getRepoId(); + assertAboutJson() + .that(generateActualJson(repoId)) + .isEqualTo(generateExpectedJsonError(repoId + " not found", 404)); + assertThat(response.getStatus()).isEqualTo(404); } @Test void testDeletedContact_found_loggedInAsCorrectRegistrar() { login("evilregistrar"); action.includeDeletedParam = Optional.of(true); - runSuccessfulHandleTest( - deletedContact.getRepoId(), - "", - "inactive", - "", - "rdap_contact_deleted.json"); + assertAboutJson() + .that(generateActualJson(deletedContact.getRepoId())) + .isEqualTo( + addPermanentBoilerplateNotices( + jsonFileBuilder() + .addContact(deletedContact.getRepoId()) + .load("rdap_contact_deleted.json"))); } @Test void testDeletedContact_found_loggedInAsAdmin() { loginAsAdmin(); action.includeDeletedParam = Optional.of(true); - runSuccessfulHandleTest( - deletedContact.getRepoId(), - "", - "inactive", - "", - "rdap_contact_deleted.json"); + assertAboutJson() + .that(generateActualJson(deletedContact.getRepoId())) + .isEqualTo( + addPermanentBoilerplateNotices( + jsonFileBuilder() + .addContact(deletedContact.getRepoId()) + .load("rdap_contact_deleted.json"))); } @Test void testRegistrar_found() { - runSuccessfulHandleTest("101", "Yes Virginia