1
0
mirror of https://github.com/google/nomulus synced 2026-02-11 15:21:28 +00:00

Compare commits

..

91 Commits

Author SHA1 Message Date
Pavlo Tkach
9006312253 Create reusable dialog / bottom sheet component (#2237) 2023-12-08 17:52:57 -05:00
gbrodman
e5e2370923 Debouncedly use a search term in console domain list (#2242) 2023-12-08 15:37:30 -05:00
sarahcaseybot
b3b0efd47e Add a dryrun tag to UpdatePremiumListCommand and early exit command if no new changes to the list (#2246)
* Add a dryrun tag to UpdatePremiumListCommand and early exit command if no new changes to the list

* Change prompt string when no change to list to reflect that there is no actual prompted user input

* Add camelCase and correct flag name
2023-12-08 14:35:05 -05:00
Lai Jiang
e82cbe60a9 Do not log nested transactions in production (#2251)
This might be the cause of the SQL performance degradation that we are
observing during the recent launch. The change went in a month ago but
there hasn't been enough increase in mutating traffic to make it
problematic until the launch.

Note that presubmits should run faster too with this chance, which
serves as an evidence that excessive logging is the culprit.
2023-12-07 19:02:16 -05:00
Weimin Yu
923bc13e3a Start using Tld's bsaEnrollStartTime field (#2239)
* Start using Tld's bsaEnrollStartTime field

    Longer-term change is tracked in b/309175410
2023-12-06 17:11:36 -05:00
Lai Jiang
4893ea307b Check for null error stream (#2249) 2023-12-06 13:30:37 -05:00
Pavlo Tkach
01f868cefc Increase number of service to 5 in cloudbuild-deploy (#2248) 2023-12-06 13:21:17 -05:00
Weimin Yu
1b0919eaff Add the BsaDomainRefresh table (#2247)
Add the BsaDomainRefresh table which tracks the refresh actions.

The refresh actions checks for changes in the set of registered and
reserved domains, which are called unblockables to BSA.
2023-12-06 11:55:42 -05:00
Lai Jiang
92b23bac16 Use the error stream when HTTP response code is non-200 (#2245) 2023-12-06 10:42:19 -05:00
gbrodman
cc9b3f5965 Filter in SQL when updating/deleting alloc tokens (#2244)
This doesn't fix any issues with dead/livelocks when deleting or
updating allocation tokens, but it at least will significantly reduce
the time to load the tokens that we'll want to update/delete.
2023-12-04 19:24:17 -05:00
gbrodman
dd86c56ddc Return the correct renewal fee for anchor tenants in domain checks (#2238)
The code as previously written assumed that creation fees would be the
same as renewal fees -- this is not the case for anchor tenants, where
the renewal fee is always the standard cost for the TLD (instead of any
premium cost). This was already handled properly in the actual billing
implementation, but we didn't tell the user the right renewal cost in
domain checks.

This also removes some warning logs related to nested transactions
2023-12-01 15:37:05 -05:00
Pavlo Tkach
08551f7bc7 Enable static ip for bsa service production (#2240) 2023-12-01 14:25:38 -05:00
Lai Jiang
e7171a326b Use reTransact when loading caches (#2234)
Similar to #2179, but adds a few calls missed in that PR.
2023-11-30 15:13:36 -05:00
gbrodman
c3eae7b76f Add an optional search term for ConsoleDomainListAction (#2225)
It's a case-insensitive query and it can appear anywhere (including
TLDs)
2023-11-30 11:42:50 -05:00
Pavlo Tkach
2687181045 Update console file naming to be camelCase like (#2235) 2023-11-30 11:42:36 -05:00
gbrodman
68750569db Pretty-print reserved list updates in the CLI (#2226)
We shouldn't have to parse through every single entry to see what
changed

Note: we don't do this for premium lists because those can be HUGE and
we don't want/need to load and display every entry. This was an explicit
choice made in https://github.com/google/nomulus/pull/1482
2023-11-30 11:32:12 -05:00
Lai Jiang
028e5cc958 Make read-only transactions more performant (#2233)
Since the replica SQL instance is read-only, any transaction performed
on it should be explicitly read-only, which would allow PostgreSQL to
optimize away (some) use of predicate locks.

Also changed the EPP cache to read from the replica. The foreign key
cache already behaves this way.

See: https://www.postgresql.org/docs/current/transaction-iso.html
2023-11-29 15:55:50 -05:00
Weimin Yu
853e571d01 Add more BSA configs (#2230)
* Add more BSA configs

Added urls for reporting order and domains to BSA.

Also added operational configs.
2023-11-28 16:40:36 -05:00
Lai Jiang
9b79f5af2c Add a dedicated IP header to accommodate Java 17 on GAE (#2224)
For reasons unclear at this point, Java 17's servlet implementation on
GAE injects IP addresses (including unroutable private IPs) into the
standard X-Forwarded-For header, which we currently use to embed
registrar IP addresses to check against the allow list. This results in
the server not properly parsing the header and rejecting legitimate
connections.

This PR sets a custom header that should not be interfered with by any
JVM implementation to store the IP address, while maintaining the old
header as a fallback. The proxy will set both headers to allow the
server to gracefully migrate from Java 8 and Java 17 (and potentially
rollback).

Also removed some headers and logic that are not used.
2023-11-28 13:20:01 -05:00
Weimin Yu
4195871541 Fix misconfiguration in new BSA service (#2227)
Also add dependency locking to services:bsa
2023-11-27 20:18:34 -05:00
Weimin Yu
504d7ccaac Preparing renaming BsaDomainInUse table (#2228)
Add the replacement table: BsaUnblockableDomain
2023-11-27 19:55:47 -05:00
gbrodman
36a8908712 Add a basic domain-list page to the new console (#2219)
This does not include any styling for now, just wanted to make sure
we're all good with regards to the basic approach. I'm open to suggestion on
which columns to include.

Note: filter searching is not implemented yet because the backend does
not allow for it (yet)
2023-11-27 14:58:48 -05:00
Weimin Yu
e42c11051e Download scheduler for BSA (#2209)
* Add BSA download scheduler
2023-11-17 16:15:14 -05:00
Weimin Yu
85b588b51f Add a disposition header to email attachments (#2223)
This may help with the billing-team with attached invoices.

This is a standard header that should do no harm.
2023-11-16 13:31:12 -05:00
Pavlo Tkach
572b7101cb Create separate BSA service (#2221) 2023-11-15 18:38:26 -05:00
Weimin Yu
445825957d Bsa Persistence entity classes (#2205)
* Add persistence model object
2023-11-15 16:43:22 -05:00
Weimin Yu
7ab76f3573 Pin Flyway tool jar to 9.22.3 (#2222)
Flyway 10+ is not compatible with Java 8.

Rollback this change after we upgrade to Java 11.
2023-11-15 14:48:55 -05:00
Weimin Yu
9e3c58989a Add an IDN helper (#2217)
* Add an IDN helper

Add a helper that checks the validity of labels in IDNs.
All organizes TLDs according to the IDNs they support.
2023-11-10 19:55:04 -05:00
Lai Jiang
cf9c1ec7c3 Use Java 8 runtime on sandbox and production (#2218)
Java 17 injects unexpected headers to X-Forwarded-For, which causes
issues with validating incoming IP addresses.

This is a partial reversion of #2201. We are still keeping Java 17 in other environment but sandbox and production needs to be able to parse the header to accept incoming EPP connections from registrars. Once we fix it we will re-enable Java 17 in these environment.
2023-11-10 14:39:16 -05:00
Pavlo Tkach
69ea87be31 Add handler for Console API requests and XSRF token creation and verification (#2211) 2023-11-09 17:51:53 -05:00
Lai Jiang
779d0c9d37 Add a fallback token verifier (#2216)
This allows us to switch the proxy to a different client ID without
disrupting the service. This is a temporary measure and will be removed
once the switch is complete.
2023-11-09 16:05:14 -05:00
Weimin Yu
2855944214 Add TLD BSA enroll start date to schema (#2215)
Also adds a placeholder getter in the Tld class, so that it can be
mocked/spied in tests. This way more BSA related code can be submitted
before the schema is deployed to prod.
2023-11-09 13:52:45 -05:00
Ben McIlwain
992d1c1349 Reduce the QPS of the refresh DNS for all domains action (#2212)
This also adds a targeted QPS as a parameter in case we need to manually bump it
up (or down) for some reason without having to make code changes and re-deploy.
2023-11-08 13:47:37 -05:00
Pavlo Tkach
f50290ce1d Add static IP connector to crash and alpha configs (#2213) 2023-11-08 13:26:32 -05:00
Pavlo Tkach
e647d4e215 Add retry to cloud build node installation (#2210) 2023-11-06 09:15:36 -05:00
Lai Jiang
08471242df Refactor transact() related methods. (#2195)
This PR makes a few changes to make it possible to turn on
per-transaction isolation level with minimal disruption:

1) Changed the signatures of transact() and reTransact() methods to allow
passing in lambdas that throw checked exceptions. Previously one has
always to wrap such lambdas in try-and-retrow blocks, which wasn't a
big issue when one can liberally open nested transactions around small
lambdas and keeps the "throwing" part outside the lambda. This becomes a
much bigger hassle when the goal is to eliminate nested transactions and
put as much code as possible within the top-level lambda. As a result,
the transactNoRetry() method now handles checked exceptions by re-throwing
them as runtime exceptions.

2) Changed the name and meaning of the config file field that used to
indicate if per-transaction isolation level is enabled or not. Now it
decides if transact() is called within a transaction, whether to
throw or to log, regardless whether the transaction could have
succeeded based on the isolation override level (if provided). The
flag will initially be set to false and would help us identify all
instances of nested calls and either refactor them or use reTransact()
instead. Once we are fairly certain that no nested calls to transact()
exists, we flip the flag to true and start enforcing this logic.
Eventually the flag will go away and nested calls to transact() will
always throw.

3) Per-transaction isolation level will now always be applied, if an
override is provided. Because currently there should be no actual
use of such feature (except for places where we explicitly use an
override and have ensured no nested transactions exist, like in
RefreshDnsForAllDomainsAction), we do not expect any issues with
conflicting isolation levels, which would resulted in failure.

3) transactNoRetry() is made package private and removed from the
exposed API of JpaTransactionManager. This saves a lot of redundant
methods that do not have a practical use. The only instances where this
method was called outside the package was in the reader of
RegistryJpaIO, which should have no problem with retrying.
2023-11-03 17:43:27 -04:00
Lai Jiang
cd23fea698 Switch to a stronger algorithm for password hashing (#2191)
We have been using SHA256 to hash passwords (for both EPP and registry lock),
which is now considered too weak.

This PR switches to using Scrypt, a memory-hard slow hash function, with
recommended parameters per go/crypto-password-hash.

To ease the transition, when a password is being verified, both Scrypt
and SHA256 are tried. If SHA256 verification is successful, we re-hash
the verified password with Scrypt and replace the stored SHA256 hash
with the new one. This way, as long as a user uses the password once
before the transition period ends (when Scrypt becomes the only valid
algorithm), there would be no need for manual intervention from them.

We will send out notifications to users to remind them of the transition
and urge them to use the password (which should not be a problem with
EPP, but less so with the registry lock). After the transition,
out-of-band reset for EPP password, or remove-and-add on the console for
registry lock password, would be required for the hashes that have not
been re-saved.

Note that the re-save logic is not present for console user's registry
lock password, as there is no production data for console users yet.
Only legacy GAE user's password requires re-save.
2023-11-03 17:29:01 -04:00
Ben McIlwain
ba54208dad Also load domains for domain checks of type renew/transfer (#2207)
The domains (and their associated billing recurrences) were already being loaded
to check restores, but they also now need to be loaded for renews and transfers
as well, as the billing renewal behavior on the recurrence could be modifying
the relevant renew price that should be shown. (The renew price is used for
transfers as well.)

See https://buganizer.corp.google.com/issues/306212810
2023-11-03 14:33:34 -04:00
Weimin Yu
b5e131ecba Add BSA schema (#2204)
* Add BSA schema

Also lock down flyway due to java8 compatiblity
2023-11-02 15:38:23 -04:00
Pavlo Tkach
87e99f59bc Replace node.js installation method in build.sh (#2206) 2023-11-02 14:17:18 -04:00
Weimin Yu
30accea383 Add keyring support for BSA API key (#2208)
* Add keyring support for BSA API key

Also removing JSON_CREDENTIAL. It is an exported service account key,
which we no longer use.
2023-11-02 14:08:50 -04:00
Lai Jiang
72e0101746 Delete unused actions (#2197)
Both actions have not been used for a while (the wipe out action
actually caused problems when it ran unintentionally and wiped out QA).
Keeping them around is a burden when refactoring efforts have to take
them into consideration.

It is always possible to resurrect them form git history should the need
arises.
2023-11-02 11:41:03 -04:00
Lai Jiang
3090df9a78 Upgrade to Java 17 runtime (#2201)
We finally fixed Spinnaker (I hope) to deploy bundled services with Java
17 runtime. Note that the bytecodes are still targeting Java 8. The only
change this PR introduces is to switch the runtime environment to Java
17.

TESTED=deployed to crash.
2023-11-02 10:08:14 -04:00
gbrodman
7332b1fa38 Add TypeAdapters for VKey objects (#2194)
GSON doesn't allow for clean (de)serialization of Class or Serializable
objects which we'll need for converting VKeys to/from JSON.
2023-10-31 15:14:41 -04:00
Lai Jiang
9330e3a50d Move truely public endpoints to a separate Auth (#2200)
This allows us to more easily refactor public endpoints that still use
the legacy auth mechanism to identify logged-in users (for the legacy
console).
2023-10-31 13:58:45 -04:00
gbrodman
1d6b119340 Add a console action to retrieve a paged list of domains (#2193)
In the future we'll want to add searching capability but for now we can
go with straightforward pagination.
2023-10-30 17:01:31 -04:00
Weimin Yu
8158f761c8 Add BSA configurations (#2202) 2023-10-30 16:44:28 -04:00
Pavlo Tkach
08838e091f Enable BACKEND service to route external traffic through VPC on Sandbox (#2199) 2023-10-30 13:36:04 -04:00
sarahcaseybot
59720a207d Change the default config for perTransactionIsolation to true (#2196)
This was already set to true in all environments except prod last week. Now that the release has gone out and we have not seen any issues, we should feel safe turning this on in production as well.
2023-10-26 17:16:02 -04:00
Pavlo Tkach
26bae65e1e Add registrar details view (#2186) 2023-10-26 09:14:09 -04:00
Pavlo Tkach
23a2861b37 Remove node.js download instruction (#2192) 2023-10-25 14:48:35 -04:00
Pavlo Tkach
341238305d Update console versions (#2190) 2023-10-24 09:34:02 -04:00
Lai Jiang
d210bed744 Add connection.disconnect() in finally blocks (#2189) 2023-10-23 16:38:16 -04:00
dependabot[bot]
fe710e5510 Bump postcss from 8.4.21 to 8.4.31 in /console-webapp (#2187)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.21 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.21...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-23 10:29:49 -04:00
sarahcaseybot
8f8ffe7020 Add a dryrun flag to configure_tld command (#2188)
This will be used for presubmit testing.
2023-10-20 16:16:05 -04:00
Lai Jiang
16e5018489 Update postcss version (#2185)
Per https://github.com/google/nomulus/security/dependabot/17
2023-10-20 13:30:40 -04:00
Lai Jiang
af303bd26f Remove URLFetch (#2181)
We previously needed to use URLFetch in some instances where TLS 1.3 is
required (mostly when connecting to ICANN servers),and the JDK-bundled SSL
engine that came with App Engine runtime did not support TLS 1.3.

It appears now that the Java 8 runtime on App Engine supports TLS 1.3
out of the box, which allows us to get rid of URLFetch, which depends on
App Engine APIs.

Also removed some redundant retry and logging logic, now that we know
the HTTP client behaves correctly.

TESTED=modified the CannedScriptExecutionAction and deployed to alpha, used the
new HTTP client to connect to the three URL endpoints that were
problematic before and confirmed that TLS connections can be established. HTTP
sessions were rejected in some cases when authentication failed, but
that was expected.
2023-10-19 14:51:51 -04:00
sarahcaseybot
bf3bb5d804 Add a Cloud Build job for syncing Tld configuration files from the internal repo with the database (#2174)
* Add a cloudbuild-tld-sync job

This job checks the Tld config files in the internal repo and syncs them with the actual Tld objects in the database using the configure_tld numulus command.

* Add the dockerfile and shell script

* Force the command

* Add comments

* add newline

* Create a separate copy of the job for each environment

* fix file name

* Fix indentation
2023-10-19 14:01:40 -04:00
dependabot[bot]
dcb16e05bd Bump @babel/traverse from 7.22.10 to 7.23.2 in /console-webapp (#2184)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.10 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 11:46:02 -04:00
sarahcaseybot
2facedd60f Lower the isolation level for RefreshDnsForAllDomainsAction (#2182)
* Lower the isolation level for RefreshDnsForAllDomainsAction

This lowers the isolation level to TRANSACTION_REPEATABLE_READ which will hopefully allow the action to run the entire action without timing out on our larger TLDs.

* Unchange default config
2023-10-17 16:58:37 -04:00
Lai Jiang
b1ec81f054 Remove the wipeout job on QA (#2183) 2023-10-17 13:05:31 -04:00
gbrodman
779da518df Pass name/email/phone info to the new console front end (#2180) 2023-10-16 16:51:35 -04:00
sarahcaseybot
4f53ae0e89 Use reTransact when loading the cache for database objects (#2179)
Cache loads will likely always be inner transactions, if they have a transaction at all. Cache loads do not always call a transaction since they are only necessary if the cache is not fresh at the time it is called. Since the cache itself needs to decide whether or not a DB transaction is necessary, it should use the reTransact method to safely indicate that the isolation level of the outer transaction is what should be used.
2023-10-16 15:22:22 -04:00
gbrodman
da04caeea2 Don't check cert validation if we're not changing the certs in the console (#2178)
If the cert(s) are invalid or expired that's a problem, but that
shouldn't necessarily prevent us from changing other things. If we're
not changing the certs, leave them alone.
2023-10-16 13:37:57 -04:00
gbrodman
a63916b08e Refine error handling in RequestHandler and the console slightly (#2177)
If we don't explicitly handle random unexpected exceptions, the error
that the front end receives includes a big ole stacktrace, which is
unhelpful for regular users and possibly bad to expose. Instead, we
should provide a vague "something went wrong" message.

Separately, we can create a default SnackBar options and use that (we
want it longer than 1.5 seconds because that's pretty short).
2023-10-12 14:03:12 -04:00
Lai Jiang
36bd508bf9 Remove OAuthAuthenticationMechanism (#2171)
Also made some refactoring to various Auth related classes to clean up things a bit and make the logic less convoluted:

1. In Auth, remove AUTH_API_PUBLIC as it is only used by the WHOIS and EPP endpoints accessed by the proxy. Previously, the proxy relies on OAuth and its service account is not given admin role (in OAuth parlance), so we made them accessible by a public user, deferring authorization to the actions themselves. In practice, OAuth checks for allowlisted client IDs and only the proxy client ID was allowlisted, which effectively limited access to only the proxy anyway.

2. In AuthResult, expose the service account email if it is at APP level. RequestAuthenticator will print out the auth result and therefore log the email, making it easy to identify which account was used. This field is mutually exclusive to the user auth info field. As a result, the factory methods are refactored to explicitly create either APP or USER level auth result.

3. Completely re-wrote RequestAuthenticatorTest. Previously, the test mingled testing functionalities of the target class with testing how various authentication mechanisms work. Now they are cleanly decoupled, and each method in RequestAuthenticator is tested individually.

4. Removed nomulus-config-production-sample.yaml as it is vastly out of date.
2023-10-11 19:12:26 -04:00
Lai Jiang
bbdbfe85ed Remove the GAIA ID column from the User table (#2172)
The field has already been removed from the Java code base in #2170.
2023-10-11 12:47:48 -04:00
gbrodman
2a7e9a266a Fix minor alignment issue on console WHOIS page (#2166) 2023-10-11 09:25:05 -04:00
Weimin Yu
bd0d8af7b3 Make sure unsafe names can be sent in emails (#2169)
Surround the dot in unsafe domain names with a square bracket. This
is suggested by Gmail abuse-detection and allows outgoing messages
to pass Gmail's check. This should also help with recipients' checks.
2023-10-05 11:19:31 -04:00
Lai Jiang
2da8ea0185 Replace JacksonFactory with GsonFactory (#2173)
JacksonFactory is deprecated and GsonFactory is the recommended
replacement.
2023-10-04 17:02:13 -04:00
Lai Jiang
7a84844000 Remove the GAIA ID field from User (#2170)
It is not used and it is not possible to derive the GAIA ID when
creating a new User from the email address alone.
2023-10-04 15:32:03 -04:00
Weimin Yu
1580555d30 Throttle outgoing emails (#2168)
Adds a delay between emails sent in a tight loop. This helps avoid
triggering Gmail abuse detections.

Also updated the recipient address for billing alerts.
2023-10-04 11:16:56 -04:00
Pavlo Tkach
4fb8a1b50b Add dark theme support to the console (#2167) 2023-10-03 15:54:25 -04:00
Pavlo Tkach
e07f25000d Add console registrars paging, fix empty registrars mobile (#2162) 2023-10-03 15:51:48 -04:00
sarahcaseybot
cc1777af0c Add custom YAML serializer for Duration (#2161)
* Add custom YAML serializer for Duration

This addresses b/301119144. This changes the YAML representation of a TLD to show Duration fields as a String reperesntation using the Java Duration object's toString() format. This eliminates the previous ambiguity over the time unit that is being used for each duration.

* change standardSeconds to standardMinutes in test

* Add custom serializer to the entire mapper
2023-10-03 13:46:19 -04:00
Lai Jiang
87e54c001f Remove unused fields to make the linter happy (#2165) 2023-10-03 13:25:07 -04:00
Pavlo Tkach
2dc87d42b4 Fix console nextUrl stacking routes (#2164) 2023-10-02 17:38:03 -04:00
Lai Jiang
1eed9c82dc Deprecate the OAuth header in Nomulus tool (#2160)
Unless an --oauth flag is used, the nomulus tool will only send the OIDC
header. The server still accepts both headers and the user should use
`create_user` command to create an admin User (with the --oauth flag on), which
will then allow one to use the nomulus tool without the --oauth flag.

The --oauth flag and the server's ability to support OAuth-based
authentication will be removed soon. Users are urged to create the User
object in time to avoid service interruption.

TESTED=verified on alpha.
2023-10-02 15:50:30 -04:00
gbrodman
cf43de7755 Open resources link in new tab (#2163)
We want to do this because it takes the user to an external site, which
could potentially lead to confusion if they tried to use the back button
without a new tab.
2023-10-02 15:06:33 -04:00
Weimin Yu
f54bec7553 Add docs for Cloud Build status notification (#2157)
Add documentation that describes the current Cloud Build status notification
to Google Chat, as well as how to update the configuration and the
notifier service.
2023-09-29 10:49:15 -04:00
gbrodman
cf698c2586 Add page for WHOIS-editable fields in the console (#2155)
This isn't the prettiest thing, but it replicates the type of view /
edit functionality that we had in the original console.

Of note: this doesn't include input field validation, which would
probably be a good idea to add at some point.
2023-09-28 22:46:18 -04:00
Lai Jiang
cb240a8f03 Use equals() method to compare equality (#2158)
It will call equalsImmutableObject(), which seems the right thing to do.
We only care if the two Tld objects have the same fields, not if they
are the same object. ErrorProne complained about comparison by identity.
2023-09-28 13:27:36 -04:00
gbrodman
0801679173 Close sidenav on click (#2156)
It shouldn't stick around after we've clicked on one of the links
2023-09-25 14:43:07 -04:00
sarahcaseybot
a87c4a31a3 Add breakglass handling to configureTldCommand (#2154)
* Add a breakglass flag to configureTldCommand

* Add tests

* small fixes
2023-09-22 11:51:02 -04:00
sarahcaseybot
58c7e3a52c Change __REMOVEDOMAIN__ token to __REMOVE_BULK_PRICING__ (#2152) 2023-09-21 16:03:39 -04:00
Pavlo Tkach
dded258864 Add resources widget front-end (#2151) 2023-09-21 13:59:40 -04:00
Lai Jiang
759143535f Update proxy k8s manifest (#2153)
The beta API is deprecated.

TESTED=deployed the new manifest to alpha. Without the change, deploying
resulted in an error.

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/2153)
<!-- Reviewable:end -->
2023-09-21 10:53:39 -04:00
Weimin Yu
46fdf2c996 Defend against deserialization-based attacks (#2150)
* Defend against deserialization-based attacks

Added the `SafeObjectInputStream` class that defends attacks using
malformed serialized data, including remote code execution and
denial-of-service attacks.

Started using the new class to handle EPP resource VKeys and
PendingDeposits, which are passed across credential-boundaries: between
TaskQueue and AppEngine server, and between AppEngine server and the RDE
pipeline on GCE. Note that the wireformat of VKeys do not change,
therefore existing tasks sitting in the TaskQueue are not affected.

Also removed an unused class: JaxbFragment.
2023-09-20 16:56:56 -04:00
sarahcaseybot
fc1857717d Use PrintStream in ConfirmingCommand (#2140)
* Use PrintStream in ConfirmingCommand

* Add errorPrintStream

* remove unneccesary line
2023-09-19 12:11:18 -04:00
sarahcaseybot
e182692a5f Check for diffs in ConfigureTldCommand (#2146)
* Check for diffs in ConfigureTldCommand

* undo override

* Add handling for ordering sets

* Fix comments

* fix formatting

* fix test
2023-09-19 12:10:26 -04:00
gbrodman
a65e85f9e1 Don't include a nextUrl when accessing the console homepage (#2149)
In this case we should just display the standard page, no need to
redirect anywhere since there's nothing to redirect to.
2023-09-15 12:28:04 -04:00
392 changed files with 14554 additions and 8173 deletions

View File

@@ -60,9 +60,8 @@ dependencyLocking {
}
node {
download = true
version = "16.14.0"
npmVersion = "6.14.11"
download = false
version = "16.19.0"
}
wrapper {
@@ -348,6 +347,7 @@ subprojects {
def services = [':services:default',
':services:backend',
':services:bsa',
':services:tools',
':services:pubapi']

View File

@@ -41,8 +41,8 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,11 @@ import SettingsSecurityComponent from './settings/security/security.component';
import { RegistrarGuard } from './registrar/registrar.guard';
import { RegistrarComponent } from './registrar/registrarsTable.component';
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
import ContactComponent from './settings/contact/contact.component';
import WhoisComponent from './settings/whois/whois.component';
import SecurityComponent from './settings/security/security.component';
import UsersComponent from './settings/users/users.component';
import { DomainListComponent } from './domains/domainList.component';
const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
@@ -32,7 +37,12 @@ const routes: Routes = [
{ path: 'home', component: HomeComponent, canActivate: [RegistrarGuard] },
{ path: 'tlds', component: TldsComponent, canActivate: [RegistrarGuard] },
{
path: 'settings',
path: DomainListComponent.PATH,
component: DomainListComponent,
canActivate: [RegistrarGuard],
},
{
path: SettingsComponent.PATH,
component: SettingsComponent,
children: [
{
@@ -41,32 +51,27 @@ const routes: Routes = [
pathMatch: 'full',
},
{
path: 'contact',
path: ContactComponent.PATH,
component: SettingsContactComponent,
canActivate: [RegistrarGuard],
},
{
path: 'whois',
path: WhoisComponent.PATH,
component: SettingsWhoisComponent,
canActivate: [RegistrarGuard],
},
{
path: 'security',
path: SecurityComponent.PATH,
component: SettingsSecurityComponent,
canActivate: [RegistrarGuard],
},
{
path: 'epp-password',
component: SettingsSecurityComponent,
canActivate: [RegistrarGuard],
},
{
path: 'users',
path: UsersComponent.PATH,
component: SettingsUsersComponent,
canActivate: [RegistrarGuard],
},
{
path: 'registrars',
path: RegistrarComponent.PATH,
component: RegistrarComponent,
},
],

View File

@@ -37,7 +37,7 @@
background-color: transparent;
}
.active {
background: #eae1e1;
background-color: var(--secondary);
}
}
&__content-wrapper {

View File

@@ -12,20 +12,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { RegistrarService } from './registrar/registrar.service';
import { UserDataService } from './shared/services/userData.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { NavigationEnd, Router } from '@angular/router';
import { MatSidenav } from '@angular/material/sidenav';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
export class AppComponent implements AfterViewInit {
renderRouter: boolean = true;
@ViewChild('sidenav')
sidenav!: MatSidenav;
constructor(
protected registrarService: RegistrarService,
protected globalLoader: GlobalLoaderService
protected userDataService: UserDataService,
protected globalLoader: GlobalLoaderService,
protected router: Router
) {
registrarService.activeRegistrarIdChange.subscribe(() => {
this.renderRouter = false;
@@ -34,4 +43,12 @@ export class AppComponent {
}, 400);
});
}
ngAfterViewInit() {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.sidenav.close();
}
});
}
}

View File

@@ -36,23 +36,31 @@ import { RegistrarGuard } from './registrar/registrar.guard';
import SecurityComponent from './settings/security/security.component';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
import { RegistrarSelectorComponent } from './registrar/registrar-selector.component';
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { ContactWidgetComponent } from './home/widgets/contact-widget.component';
import { PromotionsWidgetComponent } from './home/widgets/promotions-widget.component';
import { TldsWidgetComponent } from './home/widgets/tlds-widget.component';
import { ResourcesWidgetComponent } from './home/widgets/resources-widget.component';
import { EppWidgetComponent } from './home/widgets/epp-widget.component';
import { BillingWidgetComponent } from './home/widgets/billing-widget.component';
import { DomainsWidgetComponent } from './home/widgets/domains-widget.component';
import { SettingsWidgetComponent } from './home/widgets/settings-widget.component';
import { ContactWidgetComponent } from './home/widgets/contactWidget.component';
import { PromotionsWidgetComponent } from './home/widgets/promotionsWidget.component';
import { TldsWidgetComponent } from './home/widgets/tldsWidget.component';
import { ResourcesWidgetComponent } from './home/widgets/resourcesWidget.component';
import { EppWidgetComponent } from './home/widgets/eppWidget.component';
import { BillingWidgetComponent } from './home/widgets/billingWidget.component';
import { DomainsWidgetComponent } from './home/widgets/domainsWidget.component';
import { SettingsWidgetComponent } from './home/widgets/settingsWidget.component';
import { UserDataService } from './shared/services/userData.service';
import WhoisComponent from './settings/whois/whois.component';
import { SnackBarModule } from './snackbar.module';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { DomainListComponent } from './domains/domainList.component';
import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.component';
@NgModule({
declarations: [
AppComponent,
DialogBottomSheetWrapper,
BillingWidgetComponent,
ContactDetailsDialogComponent,
ContactWidgetComponent,
DomainListComponent,
DomainsWidgetComponent,
EmptyRegistrar,
EppWidgetComponent,
@@ -60,6 +68,7 @@ import { SettingsWidgetComponent } from './home/widgets/settings-widget.componen
HomeComponent,
PromotionsWidgetComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistrarSelectorComponent,
ResourcesWidgetComponent,
SecurityComponent,
@@ -68,6 +77,7 @@ import { SettingsWidgetComponent } from './home/widgets/settings-widget.componen
SettingsWidgetComponent,
TldsComponent,
TldsWidgetComponent,
WhoisComponent,
],
imports: [
AppRoutingModule,
@@ -76,11 +86,13 @@ import { SettingsWidgetComponent } from './home/widgets/settings-widget.componen
FormsModule,
HttpClientModule,
MaterialModule,
SnackBarModule,
],
providers: [
BackendService,
GlobalLoaderService,
RegistrarGuard,
UserDataService,
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {

View File

@@ -0,0 +1,60 @@
<div class="console-domains">
<mat-form-field>
<mat-label>Filter</mat-label>
<input
type="search"
matInput
[(ngModel)]="searchTerm"
(ngModelChange)="sendInput()"
#input
/>
</mat-form-field>
<div *ngIf="isLoading; else domains_content" class="console-domains__loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<ng-template #domains_content>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="domainName">
<th mat-header-cell *matHeaderCellDef>Domain Name</th>
<td mat-cell *matCellDef="let element">{{ element.domainName }}</td>
</ng-container>
<ng-container matColumnDef="creationTime">
<th mat-header-cell *matHeaderCellDef>Creation Time</th>
<td mat-cell *matCellDef="let element">
{{ element.creationTime.creationTime }}
</td>
</ng-container>
<ng-container matColumnDef="registrationExpirationTime">
<th mat-header-cell *matHeaderCellDef>Expiration Time</th>
<td mat-cell *matCellDef="let element">
{{ element.registrationExpirationTime }}
</td>
</ng-container>
<ng-container matColumnDef="statuses">
<th mat-header-cell *matHeaderCellDef>Statuses</th>
<td mat-cell *matCellDef="let element">{{ element.statuses }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="4">No domains found</td>
</tr>
</table>
<mat-paginator
[length]="totalResults"
[pageIndex]="pageNumber"
[pageSize]="resultsPerPage"
[pageSizeOptions]="[10, 25, 50, 100, 500]"
(page)="onPageChange($event)"
aria-label="Select page of domain results"
showFirstLastButtons
></mat-paginator>
</ng-template>
</div>

View File

@@ -0,0 +1,36 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DomainListComponent } from './domainList.component';
describe('DomainListComponent', () => {
let component: DomainListComponent;
let fixture: ComponentFixture<DomainListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DomainListComponent],
}).compileComponents();
fixture = TestBed.createComponent(DomainListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,98 @@
// Copyright 2023 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, ViewChild } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { BackendService } from '../shared/services/backend.service';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { RegistrarService } from '../registrar/registrar.service';
import { Domain, DomainListService } from './domainList.service';
import { Subject, debounceTime } from 'rxjs';
@Component({
selector: 'app-domain-list',
templateUrl: './domainList.component.html',
styleUrls: ['./domainList.component.scss'],
providers: [DomainListService],
})
export class DomainListComponent {
public static PATH = 'domain-list';
private readonly DEBOUNCE_MS = 500;
displayedColumns: string[] = [
'domainName',
'creationTime',
'registrationExpirationTime',
'statuses',
];
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
isLoading = true;
searchTermSubject = new Subject<string>();
searchTerm?: string;
pageNumber?: number;
resultsPerPage = 50;
totalResults?: number;
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
constructor(
private backendService: BackendService,
private domainListService: DomainListService,
private registrarService: RegistrarService
) {}
ngOnInit() {
this.dataSource.paginator = this.paginator;
// Don't spam the server unnecessarily while the user is typing
this.searchTermSubject
.pipe(debounceTime(this.DEBOUNCE_MS))
.subscribe((searchTermValue) => {
this.reloadData();
});
this.reloadData();
}
ngOnDestroy() {
this.searchTermSubject.complete();
}
reloadData() {
this.isLoading = true;
this.domainListService
.retrieveDomains(
this.pageNumber,
this.resultsPerPage,
this.totalResults,
this.searchTerm
)
.subscribe((domainListResult) => {
this.dataSource.data = domainListResult.domains;
this.totalResults = domainListResult.totalResults;
this.isLoading = false;
});
}
sendInput() {
this.searchTermSubject.next(this.searchTerm!);
}
onPageChange(event: PageEvent) {
this.pageNumber = event.pageIndex;
this.resultsPerPage = event.pageSize;
this.reloadData();
}
}

View File

@@ -0,0 +1,68 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
import { RegistrarService } from '../registrar/registrar.service';
import { tap } from 'rxjs';
export interface CreateAutoTimestamp {
creationTime: string;
}
export interface Domain {
creationTime: CreateAutoTimestamp;
currentSponsorRegistrarId: string;
domainName: string;
registrationExpirationTime: string;
statuses: string[];
}
export interface DomainListResult {
checkpointTime: string;
domains: Domain[];
totalResults: number;
}
@Injectable()
export class DomainListService {
checkpointTime?: string;
constructor(
private backendService: BackendService,
private registrarService: RegistrarService
) {}
retrieveDomains(
pageNumber?: number,
resultsPerPage?: number,
totalResults?: number,
searchTerm?: string
) {
return this.backendService
.getDomains(
this.registrarService.activeRegistrarId,
this.checkpointTime,
pageNumber,
resultsPerPage,
totalResults,
searchTerm
)
.pipe(
tap((domainListResult: DomainListResult) => {
this.checkpointTime = domainListResult.checkpointTime;
})
);
}
}

View File

@@ -1,24 +1,30 @@
<p>
<p class="console-app__header">
<mat-toolbar color="primary">
<button mat-icon-button aria-label="Open menu" (click)="toggleNavPane()">
<mat-icon>menu</mat-icon>
</button>
<span>
<a
[routerLink]="'/home'"
routerLinkActive="active"
class="console-app__logo"
>
Google Registry
</a>
</span>
<a
[routerLink]="'/home'"
routerLinkActive="active"
class="console-app__logo"
>
Google Registry
</a>
<span class="spacer"></span>
<app-registrar-selector />
<button mat-icon-button aria-label="Open FAQ">
<mat-icon>question_mark</mat-icon>
</button>
<button mat-icon-button aria-label="Open user info">
<button
mat-icon-button
[matMenuTriggerFor]="menu"
#menuTrigger
aria-label="Open user info"
>
<mat-icon>person</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="logOut()">Log out</button>
</mat-menu>
</mat-toolbar>
</p>

View File

@@ -17,6 +17,21 @@
color: inherit;
text-decoration: none;
}
&__header {
@media (max-width: 599px) {
.mat-toolbar {
padding: 0;
}
.console-app__logo {
font-size: 16px;
}
button {
padding-left: 0;
padding-right: 0;
width: 30px;
}
}
}
}
.spacer {
flex: 1;

View File

@@ -28,4 +28,8 @@ export class HeaderComponent {
this.isNavOpen = !this.isNavOpen;
this.toggleNavOpen.emit(this.isNavOpen);
}
logOut() {
window.open('/console?gcp-iap-mode=CLEAR_LOGIN_COOKIE', '_self');
}
}

View File

@@ -29,4 +29,9 @@
}
}
}
@media (max-width: 510px) {
.console-app__widget-wrapper__wide {
grid-column: initial;
}
}
}

View File

@@ -17,7 +17,7 @@ import { RegistrarService } from 'src/app/registrar/registrar.service';
@Component({
selector: '[app-billing-widget]',
templateUrl: './billing-widget.component.html',
templateUrl: './billingWidget.component.html',
})
export class BillingWidgetComponent {
constructor(public registrarService: RegistrarService) {}

View File

@@ -1,27 +0,0 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<mat-icon class="console-app__widget-icon">call</mat-icon>
<h1 class="console-app__widget-title">Contact Support</h1>
<h4 class="secondary-text text-center">
View Google Registry support email and phone information
</h4>
</div>
<div class="console-app__widget_right">
<button mat-button color="primary" class="console-app__widget-link">
Give us a Call
</button>
<p class="secondary-text">
Call Google Registry support at +1 (404) 978 8419
</p>
<button mat-button color="primary" class="console-app__widget-link">
Send us an Email
</button>
<p class="secondary-text">
Email Google Registry at support@google.com
</p>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@@ -0,0 +1,29 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<mat-icon class="console-app__widget-icon">call</mat-icon>
<h1 class="console-app__widget-title">Contact Support</h1>
<h4 class="secondary-text text-center">
Let us know if you have any questions
</h4>
</div>
<div class="console-app__widget_right">
<div class="console-app__widget-section-header">Give us a Call</div>
<p class="secondary-text">
Call {{ userDataService.userData?.productName }} support at
<a href="tel:{{ userDataService.userData?.supportPhoneNumber }}">{{
userDataService.userData?.supportPhoneNumber
}}</a>
</p>
<div class="console-app__widget-section-header">Send us an Email</div>
<p class="secondary-text">
Email {{ userDataService.userData?.productName }} at
<a href="mailto:{{ userDataService.userData?.supportEmail }}">{{
userDataService.userData?.supportEmail
}}</a>
</p>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@@ -13,11 +13,12 @@
// limitations under the License.
import { Component } from '@angular/core';
import { UserDataService } from 'src/app/shared/services/userData.service';
@Component({
selector: '[app-contact-widget]',
templateUrl: './contact-widget.component.html',
templateUrl: './contactWidget.component.html',
})
export class ContactWidgetComponent {
constructor() {}
constructor(public userDataService: UserDataService) {}
}

View File

@@ -13,7 +13,12 @@
Create a Domain
</button>
<p class="secondary-text">Register a new domain name</p>
<button mat-button color="primary" class="console-app__widget-link">
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openDomainsPage()"
>
View DUMs
</button>
<p class="secondary-text">

View File

@@ -0,0 +1,29 @@
// Copyright 2023 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 { Router } from '@angular/router';
import { DomainListComponent } from 'src/app/domains/domainList.component';
@Component({
selector: '[app-domains-widget]',
templateUrl: './domainsWidget.component.html',
})
export class DomainsWidgetComponent {
constructor(private router: Router) {}
openDomainsPage() {
this.router.navigate([DomainListComponent.PATH]);
}
}

View File

@@ -16,7 +16,7 @@ import { Component } from '@angular/core';
@Component({
selector: '[app-epp-widget]',
templateUrl: './epp-widget.component.html',
templateUrl: './eppWidget.component.html',
})
export class EppWidgetComponent {
constructor() {}

View File

@@ -16,7 +16,7 @@ import { Component } from '@angular/core';
@Component({
selector: '[app-promotions-widget]',
templateUrl: './promotions-widget.component.html',
templateUrl: './promotionsWidget.component.html',
})
export class PromotionsWidgetComponent {
constructor() {}

View File

@@ -1,13 +1,17 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<a
class="console-app__widget_left"
href="{{ userDataService.userData?.technicalDocsUrl }}"
target="_blank"
>
<mat-icon class="console-app__widget-icon">menu_book</mat-icon>
<h1 class="console-app__widget-title">Resources</h1>
<h4 class="secondary-text text-center">
Use Google Drive to view onboarding FAQs, and technical documentation.
</h4>
</div>
</a>
</div>
</mat-card-content>
</mat-card>

View File

@@ -13,11 +13,12 @@
// limitations under the License.
import { Component } from '@angular/core';
import { UserDataService } from 'src/app/shared/services/userData.service';
@Component({
selector: '[app-resources-widget]',
templateUrl: './resources-widget.component.html',
templateUrl: './resourcesWidget.component.html',
})
export class ResourcesWidgetComponent {
constructor() {}
constructor(public userDataService: UserDataService) {}
}

View File

@@ -10,11 +10,21 @@
</h4>
</div>
<div class="console-app__widget_right">
<button mat-button color="primary" class="console-app__widget-link">
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openContactsPage()"
>
Contact Information
</button>
<p class="secondary-text">Manage Primary, Technical, etc contacts.</p>
<button mat-button color="primary" class="console-app__widget-link">
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openSecurityPage()"
>
Security
</button>
<p class="secondary-text">
@@ -28,7 +38,12 @@
User Management
</button>
<p class="secondary-text">Create and manage console user accounts</p>
<button mat-button color="primary" class="console-app__widget-link">
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openRegistrarsPage()"
>
Registrar Management
</button>
<p class="secondary-text">Create and manage registrar accounts</p>

View File

@@ -13,11 +13,32 @@
// limitations under the License.
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { RegistrarComponent } from 'src/app/registrar/registrarsTable.component';
import ContactComponent from 'src/app/settings/contact/contact.component';
import SecurityComponent from 'src/app/settings/security/security.component';
import { SettingsComponent } from 'src/app/settings/settings.component';
@Component({
selector: '[app-settings-widget]',
templateUrl: './settings-widget.component.html',
templateUrl: './settingsWidget.component.html',
})
export class SettingsWidgetComponent {
constructor() {}
constructor(private router: Router) {}
openRegistrarsPage() {
this.navigate(RegistrarComponent.PATH);
}
openSecurityPage() {
this.navigate(SecurityComponent.PATH);
}
openContactsPage() {
this.navigate(ContactComponent.PATH);
}
private navigate(route: string) {
this.router.navigate([SettingsComponent.PATH, route]);
}
}

View File

@@ -16,7 +16,7 @@ import { Component } from '@angular/core';
@Component({
selector: '[app-tlds-widget]',
templateUrl: './tlds-widget.component.html',
templateUrl: './tldsWidget.component.html',
})
export class TldsWidgetComponent {
constructor() {}

View File

@@ -45,6 +45,7 @@ import { DialogModule } from '@angular/cdk/dialog';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatChipsModule } from '@angular/material/chips';
@NgModule({
exports: [
@@ -81,6 +82,7 @@ import { MatPaginatorModule } from '@angular/material/paginator';
DialogModule,
MatSnackBarModule,
MatPaginatorModule,
MatChipsModule,
],
})
export class MaterialModule {}

View File

@@ -8,6 +8,7 @@
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
&-icon {
transform: scale(3);

View File

@@ -14,9 +14,10 @@
import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { RegistrarService } from './registrar.service';
import { Subscription } from 'rxjs';
import { RegistrarService } from './registrar.service';
@Component({
selector: 'app-empty-registrar',
templateUrl: './emptyRegistrar.component.html',

View File

@@ -1,25 +0,0 @@
// Copyright 2023 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 { RegistrarService } from './registrar.service';
@Component({
selector: 'app-registrar-selector',
templateUrl: './registrar-selector.component.html',
styleUrls: ['./registrar-selector.component.scss'],
})
export class RegistrarSelectorComponent {
constructor(protected registrarService: RegistrarService) {}
}

View File

@@ -13,7 +13,11 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { RegistrarService } from './registrar.service';
@@ -26,12 +30,16 @@ export class RegistrarGuard {
private registrarService: RegistrarService
) {}
canActivate(): Promise<boolean> | boolean {
canActivate(
_: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean> | boolean {
if (this.registrarService.activeRegistrarId) {
return true;
}
// Get the full URL including any nested children (skip the initial '#/')
const nextUrl = location.hash.split('#/')[1];
return this.router.navigate([`/empty-registrar`, { nextUrl }]);
return this.router.navigate([
`/empty-registrar`,
{ nextUrl: state.url || '' },
]);
}
}

View File

@@ -13,14 +13,16 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
import { Observable, Subject, tap } from 'rxjs';
import { BackendService } from '../shared/services/backend.service';
import {
GlobalLoader,
GlobalLoaderService,
} from '../shared/services/globalLoader.service';
import { MatSnackBar } from '@angular/material/snack-bar';
interface Address {
export interface Address {
street?: string[];
city?: string;
countryCode?: string;
@@ -30,16 +32,20 @@ interface Address {
export interface Registrar {
allowedTlds?: string[];
ipAddressAllowList?: string[];
emailAddress?: string;
billingAccountMap?: object;
driveFolderId?: string;
emailAddress?: string;
faxNumber?: string;
ianaIdentifier?: number;
icannReferralEmail?: string;
ipAddressAllowList?: string[];
localizedAddress?: Address;
phoneNumber?: string;
registrarId: string;
registrarName: string;
registryLockAllowed?: boolean;
url?: string;
whoisServer?: string;
}
@Injectable({
@@ -52,7 +58,8 @@ export class RegistrarService implements GlobalLoader {
constructor(
private backend: BackendService,
private globalLoader: GlobalLoaderService
private globalLoader: GlobalLoaderService,
private _snackBar: MatSnackBar
) {
this.loadRegistrars().subscribe((r) => {
this.globalLoader.stopGlobalLoader(this);
@@ -66,7 +73,7 @@ export class RegistrarService implements GlobalLoader {
)[0];
}
public updateRegistrar(registrarId: string) {
public updateSelectedRegistrar(registrarId: string) {
this.activeRegistrarId = registrarId;
this.activeRegistrarIdChange.next(registrarId);
}
@@ -82,6 +89,6 @@ export class RegistrarService implements GlobalLoader {
}
loadingTimeout() {
// TODO: Decide what to do when timeout happens
this._snackBar.open('Timeout loading registrars');
}
}

View File

@@ -0,0 +1,40 @@
<div class="registrarDetails" *ngIf="registrarInEdit">
<h3 mat-dialog-title>Edit Registrar: {{ registrarInEdit.registrarId }}</h3>
<div mat-dialog-content>
<form (ngSubmit)="saveAndClose()">
<mat-form-field class="registrarDetails__input">
<mat-label>Registry Lock:</mat-label>
<mat-select
[(ngModel)]="registrarInEdit.registryLockAllowed"
name="registryLockAllowed"
>
<mat-option [value]="true">True</mat-option>
<mat-option [value]="false">False</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="registrarDetails__input">
<mat-label>Onboarded TLDs: </mat-label>
<mat-chip-grid #chipGrid aria-label="Enter TLD">
<mat-chip-row
*ngFor="let tld of registrarInEdit.allowedTlds"
(removed)="removeTLD(tld)"
>
{{ tld }}
<button matChipRemove aria-label="'remove ' + tld">
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
</mat-chip-grid>
<input
placeholder="New tld..."
[matChipInputFor]="chipGrid"
(matChipInputTokenEnd)="addTLD($event)"
/>
</mat-form-field>
<mat-dialog-actions>
<button mat-button (click)="this.params?.close()">Cancel</button>
<button type="submit" mat-button color="primary">Save</button>
</mat-dialog-actions>
</form>
</div>
</div>

View File

@@ -0,0 +1,8 @@
.registrarDetails {
min-width: 30vw;
&__input {
display: block;
margin-top: 0.5rem;
}
}

View File

@@ -0,0 +1,61 @@
// Copyright 2023 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 { Registrar, RegistrarService } from './registrar.service';
import { MatChipInputEvent } from '@angular/material/chips';
import { DialogBottomSheetContent } from '../shared/components/dialogBottomSheet.component';
type RegistrarDetailsParams = {
close: Function;
data: {
registrar: Registrar;
};
};
@Component({
selector: 'app-registrar-details',
templateUrl: './registrarDetails.component.html',
styleUrls: ['./registrarDetails.component.scss'],
})
export class RegistrarDetailsComponent implements DialogBottomSheetContent {
registrarInEdit!: Registrar;
params?: RegistrarDetailsParams;
constructor(protected registrarService: RegistrarService) {}
init(params: RegistrarDetailsParams) {
this.params = params;
this.registrarInEdit = JSON.parse(
JSON.stringify(this.params.data.registrar)
);
}
saveAndClose() {
this.params?.close();
}
addTLD(e: MatChipInputEvent) {
this.removeTLD(e.value); // Prevent dups
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.concat(
[e.value.toLowerCase()]
);
}
removeTLD(tld: string) {
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.filter(
(v) => v != tld
);
}
}

View File

@@ -1,10 +1,21 @@
<div class="console-app__registrar">
<div>
<button
mat-button
[routerLink]="'/settings/registrars'"
routerLinkActive="active"
*ngIf="isMobile; else desktop"
>
{{ registrarService.activeRegistrarId || "Select registrar" }}
<mat-icon>open_in_new</mat-icon>
</button>
<ng-template #desktop>
<mat-form-field class="mat-form-field-density-5" appearance="fill">
<mat-label>Registrar</mat-label>
<mat-select
[ngModel]="registrarService.activeRegistrarId"
(selectionChange)="registrarService.updateRegistrar($event.value)"
(selectionChange)="
registrarService.updateSelectedRegistrar($event.value)
"
>
<mat-option
*ngFor="let registrar of registrarService.registrars"
@@ -14,5 +25,5 @@
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-template>
</div>

View File

@@ -14,7 +14,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistrarSelectorComponent } from './registrar-selector.component';
import { RegistrarSelectorComponent } from './registrarSelector.component';
describe('RegistrarSelectorComponent', () => {
let component: RegistrarSelectorComponent;

View File

@@ -0,0 +1,46 @@
// Copyright 2023 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, OnInit } from '@angular/core';
import { RegistrarService } from './registrar.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { distinctUntilChanged } from 'rxjs';
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
@Component({
selector: 'app-registrar-selector',
templateUrl: './registrarSelector.component.html',
styleUrls: ['./registrarSelector.component.scss'],
})
export class RegistrarSelectorComponent implements OnInit {
protected isMobile: boolean = false;
readonly breakpoint$ = this.breakpointObserver
.observe([MOBILE_LAYOUT_BREAKPOINT])
.pipe(distinctUntilChanged());
constructor(
protected registrarService: RegistrarService,
protected breakpointObserver: BreakpointObserver
) {}
ngOnInit(): void {
this.breakpoint$.subscribe(() => this.breakpointChanged());
}
private breakpointChanged() {
this.isMobile = this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT);
}
}

View File

@@ -1,25 +1,54 @@
<div class="console-app__registrars">
<table
mat-table
[dataSource]="registrarService.registrars"
<mat-form-field class="console-app__registrars-filter">
<mat-label>Search</mat-label>
<input
matInput
(keyup)="applyFilter($event)"
placeholder="..."
type="search"
/>
<mat-icon matPrefix>search</mat-icon>
</mat-form-field>
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z8"
class="console-app__registrars-table"
matSort
>
<ng-container matColumnDef="edit">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let row">
<button
mat-icon-button
color="primary"
aria-label="Edit registrar"
(click)="openDetails($event, row)"
>
<mat-icon>edit</mat-icon>
</button>
</mat-cell>
</ng-container>
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
>
<th mat-header-cell *matHeaderCellDef>
{{ column.header }}
</th>
<td mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></td>
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
(click)="registrarService.updateSelectedRegistrar(row.registrarId)"
></mat-row>
</mat-table>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<mat-paginator
class="mat-elevation-z8"
[pageSizeOptions]="[5, 10, 20]"
showFirstLastButtons
></mat-paginator>
<app-dialog-bottom-sheet-wrapper
#registrarDetailsView
></app-dialog-bottom-sheet-wrapper>
</div>

View File

@@ -1,5 +1,36 @@
.console-app {
$min-width: 756px;
&__registrars {
margin-top: 1.5rem;
width: 100%;
overflow: auto;
}
&__registrars-filter {
min-width: $min-width !important;
width: 100%;
}
&__registrars-table {
min-width: $min-width !important;
}
.mat-mdc-paginator {
min-width: $min-width !important;
}
.mat-column {
&-edit {
max-width: 55px;
padding-left: 5px;
}
&-driveId {
min-width: 200px;
word-break: break-all;
}
&-registryLockAllowed {
max-width: 80px;
}
}
}

View File

@@ -12,15 +12,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Component, ViewChild, ViewEncapsulation } from '@angular/core';
import { Registrar, RegistrarService } from './registrar.service';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { RegistrarDetailsComponent } from './registrarDetails.component';
import { DialogBottomSheetWrapper } from '../shared/components/dialogBottomSheet.component';
@Component({
selector: 'app-registrar',
templateUrl: './registrarsTable.component.html',
styleUrls: ['./registrarsTable.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class RegistrarComponent {
public static PATH = 'registrars';
dataSource: MatTableDataSource<Registrar>;
columns = [
{
columnDef: 'registrarId',
@@ -70,6 +78,34 @@ export class RegistrarComponent {
cell: (record: Registrar) => `${record.driveFolderId || ''}`,
},
];
displayedColumns = this.columns.map((c) => c.columnDef);
constructor(protected registrarService: RegistrarService) {}
displayedColumns = ['edit'].concat(this.columns.map((c) => c.columnDef));
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild('registrarDetailsView')
detailsComponentWrapper!: DialogBottomSheetWrapper;
constructor(protected registrarService: RegistrarService) {
this.dataSource = new MatTableDataSource<Registrar>(
registrarService.registrars
);
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
openDetails(event: MouseEvent, registrar: Registrar) {
event.stopPropagation();
this.detailsComponentWrapper.open<RegistrarDetailsComponent>(
RegistrarDetailsComponent,
{ registrar }
);
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.dataSource.filter = filterValue.trim().toLowerCase();
}
}

View File

@@ -41,4 +41,7 @@
<mat-icon>add</mat-icon>Create a Contact
</button>
</div>
<app-dialog-bottom-sheet-wrapper
#contactDetailsWrapper
></app-dialog-bottom-sheet-wrapper>
</div>

View File

@@ -12,21 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Inject } from '@angular/core';
import {
MatDialog,
MAT_DIALOG_DATA,
MatDialogRef,
} from '@angular/material/dialog';
import {
MatBottomSheet,
MAT_BOTTOM_SHEET_DATA,
MatBottomSheetRef,
} from '@angular/material/bottom-sheet';
import { Component, ViewChild } from '@angular/core';
import { Contact, ContactService } from './contact.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { HttpErrorResponse } from '@angular/common/http';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
DialogBottomSheetContent,
DialogBottomSheetWrapper,
} from 'src/app/shared/components/dialogBottomSheet.component';
enum Operations {
DELETE,
@@ -40,7 +33,13 @@ interface GroupedContacts {
contacts: Array<Contact>;
}
let isMobile: boolean;
type ContactDetailsParams = {
close: Function;
data: {
contact: Contact;
operation: Operations;
};
};
const contactTypes: Array<GroupedContacts> = [
{ value: 'ADMIN', label: 'Primary contact', contacts: [] },
@@ -52,72 +51,46 @@ const contactTypes: Array<GroupedContacts> = [
{ value: 'WHOIS', label: 'WHOIS-Inquiry contact', contacts: [] },
];
class ContactDetailsEventsResponder {
private ref?: MatDialogRef<any> | MatBottomSheetRef;
constructor() {
this.onClose = this.onClose.bind(this);
}
setRef(ref: MatDialogRef<any> | MatBottomSheetRef) {
this.ref = ref;
}
onClose() {
if (this.ref == undefined) {
throw "Reference to ContactDetailsDialogComponent hasn't been set. ";
}
if (this.ref instanceof MatBottomSheetRef) {
this.ref.dismiss();
} else if (this.ref instanceof MatDialogRef) {
this.ref.close();
}
}
}
@Component({
selector: 'app-contact-details-dialog',
templateUrl: 'contact-details.component.html',
templateUrl: 'contactDetails.component.html',
styleUrls: ['./contact.component.scss'],
})
export class ContactDetailsDialogComponent {
contact: Contact;
export class ContactDetailsDialogComponent implements DialogBottomSheetContent {
contact?: Contact;
contactTypes = contactTypes;
operation: Operations;
contactIndex: number;
onCloseCallback: Function;
contactIndex?: number;
params?: ContactDetailsParams;
constructor(
public contactService: ContactService,
private _snackBar: MatSnackBar,
@Inject(isMobile ? MAT_BOTTOM_SHEET_DATA : MAT_DIALOG_DATA)
public data: {
onClose: Function;
contact: Contact;
operation: Operations;
}
) {
this.onCloseCallback = data.onClose;
this.contactIndex = contactService.contacts.findIndex(
(c) => c === data.contact
private _snackBar: MatSnackBar
) {}
init(params: ContactDetailsParams) {
this.params = params;
this.contactIndex = this.contactService.contacts.findIndex(
(c) => c === params.data.contact
);
this.contact = JSON.parse(JSON.stringify(data.contact));
this.operation = data.operation;
this.contact = JSON.parse(JSON.stringify(params.data.contact));
}
onClose(e: MouseEvent) {
e.preventDefault();
this.onCloseCallback.call(this);
close() {
this.params?.close();
}
saveAndClose(e: SubmitEvent) {
e.preventDefault();
if (!this.contact || this.contactIndex === undefined) return;
if (!(e.target as HTMLFormElement).checkValidity()) {
return;
}
const operation = this.params?.data.operation;
let operationObservable;
if (this.operation === Operations.ADD) {
if (operation === Operations.ADD) {
operationObservable = this.contactService.addContact(this.contact);
} else if (this.operation === Operations.UPDATE) {
} else if (operation === Operations.UPDATE) {
operationObservable = this.contactService.updateContact(
this.contactIndex,
this.contact
@@ -127,11 +100,9 @@ export class ContactDetailsDialogComponent {
}
operationObservable.subscribe({
complete: this.onCloseCallback.bind(this),
complete: () => this.close(),
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error, undefined, {
duration: 1500,
});
this._snackBar.open(err.error);
},
});
}
@@ -143,11 +114,13 @@ export class ContactDetailsDialogComponent {
styleUrls: ['./contact.component.scss'],
})
export default class ContactComponent {
public static PATH = 'contact';
@ViewChild('contactDetailsWrapper')
detailsComponentWrapper!: DialogBottomSheetWrapper;
loading: boolean = false;
constructor(
private dialog: MatDialog,
private bottomSheet: MatBottomSheet,
private breakpointObserver: BreakpointObserver,
public contactService: ContactService,
private _snackBar: MatSnackBar
) {
@@ -173,9 +146,7 @@ export default class ContactComponent {
if (confirm(`Please confirm contact ${contact.name} delete`)) {
this.contactService.deleteContact(contact).subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error, undefined, {
duration: 1500,
});
this._snackBar.open(err.error);
},
});
}
@@ -197,20 +168,9 @@ export default class ContactComponent {
operation: Operations = Operations.UPDATE
) {
e.preventDefault();
// TODO: handle orientation change
isMobile = this.breakpointObserver.isMatched('(max-width: 599px)');
const responder = new ContactDetailsEventsResponder();
const config = { data: { onClose: responder.onClose, contact, operation } };
if (isMobile) {
const bottomSheetRef = this.bottomSheet.open(
ContactDetailsDialogComponent,
config
);
responder.setRef(bottomSheetRef);
} else {
const dialogRef = this.dialog.open(ContactDetailsDialogComponent, config);
responder.setRef(dialogRef);
}
this.detailsComponentWrapper.open<ContactDetailsDialogComponent>(
ContactDetailsDialogComponent,
{ contact, operation }
);
}
}

View File

@@ -45,7 +45,7 @@ export class ContactService {
return this.backend
.getContacts(this.registrarService.activeRegistrarId)
.pipe(
tap((contacts) => {
tap((contacts = []) => {
this.contacts = contacts;
})
);

View File

@@ -1,7 +1,7 @@
<h3 mat-dialog-title>Contact details</h3>
<div mat-dialog-content>
<div mat-dialog-content *ngIf="contact">
<form (ngSubmit)="saveAndClose($event)">
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Name: </mat-label>
<input
@@ -11,9 +11,9 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Primary account email: </mat-label>
<input
@@ -25,9 +25,9 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Phone: </mat-label>
<input
@@ -36,9 +36,9 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Fax: </mat-label>
<input
@@ -47,7 +47,7 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div class="contact-details__group">
<label>Contact type:</label>
@@ -97,7 +97,7 @@
>
</section>
<mat-dialog-actions>
<button mat-button (click)="onClose($event)">Cancel</button>
<button mat-button (click)="close()">Cancel</button>
<button type="submit" mat-button>Save</button>
</mat-dialog-actions>
</form>

View File

@@ -29,6 +29,8 @@ import { RegistrarService } from 'src/app/registrar/registrar.service';
providers: [SecurityService],
})
export default class SecurityComponent {
public static PATH = 'security';
loading: boolean = false;
inEdit: boolean = false;
dataSource: SecuritySettings = {};
@@ -62,9 +64,7 @@ export default class SecurityComponent {
this.resetDataSource();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error, undefined, {
duration: 1500,
});
this._snackBar.open(err.error);
},
});
this.cancel();

View File

@@ -15,9 +15,9 @@
.console-settings {
.mdc-tab {
&.active-link {
border-bottom: 2px solid #673ab7;
border-bottom: 2px solid var(--primary);
.mdc-tab__text-label {
color: #673ab7;
color: var(--primary);
}
}
}

View File

@@ -20,4 +20,6 @@ import { Component, ViewEncapsulation } from '@angular/core';
styleUrls: ['./settings.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class SettingsComponent {}
export class SettingsComponent {
public static PATH = 'settings';
}

View File

@@ -19,4 +19,6 @@ import { Component } from '@angular/core';
templateUrl: './users.component.html',
styleUrls: ['./users.component.scss'],
})
export default class UsersComponent {}
export default class UsersComponent {
public static PATH = 'users';
}

View File

@@ -1 +1,250 @@
<p>whois works!</p>
<div class="settings-whois">
<h2>WHOIS settings</h2>
<h3>
General registrar information for your WHOIS record. This information is
always visible in WHOIS.
</h3>
<div *ngIf="loading" class="settings-whois__loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Name:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.registrarName"
disabled
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>IANA Identifier:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.ianaIdentifier"
disabled
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>ICANN Referral Email:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="email"
[(ngModel)]="registrar.icannReferralEmail"
disabled
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>WHOIS server:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.whoisServer"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Referral URL:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.url"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Email:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="email"
[(ngModel)]="registrar.emailAddress"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Phone::</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.phoneNumber"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Fax:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.faxNumber"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Address Line 1:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress?.street"
matInput
type="text"
[(ngModel)]="(registrar.localizedAddress?.street)![0]"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>City:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress"
matInput
type="text"
[(ngModel)]="registrar.localizedAddress.city"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Address Line 2:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress?.street"
matInput
type="text"
[(ngModel)]="(registrar.localizedAddress?.street)![1]"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>State/Region:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress"
matInput
type="text"
[(ngModel)]="registrar.localizedAddress.state"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Address Line 3:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress?.street"
matInput
type="text"
[(ngModel)]="(registrar.localizedAddress?.street)![2]"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Country Code:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress"
matInput
type="text"
[(ngModel)]="registrar.localizedAddress.countryCode"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
</div>
<div class="settings-whois__actions">
<ng-template [ngIf]="inEdit" [ngIfElse]="inView">
<button
class="actions-save"
mat-raised-button
color="primary"
(click)="save()"
>
Save
</button>
<button class="actions-cancel" mat-stroked-button (click)="cancel()">
Cancel
</button>
</ng-template>
<ng-template #inView>
<button #elseBlock mat-raised-button (click)="enableEdit()">Edit</button>
</ng-template>
</div>
</div>

View File

@@ -11,3 +11,49 @@
// 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.
.settings-whois {
margin-top: 1.5rem;
&__section {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
min-width: 400px;
}
&__section-address {
display: flex;
flex-wrap: wrap;
margin-bottom: 5px;
min-width: 400px;
width: 50%;
max-width: 50%;
}
&__section-description {
display: inline-block;
margin-block-start: 1em;
width: 160px;
}
&__section-form {
display: inline-block;
width: 70%;
mat-form-field {
width: 90%;
min-width: 300px;
}
input:disabled {
border: 0;
}
}
&__loading {
margin: 2rem 0;
}
&__actions {
margin-top: 50px;
display: flex;
justify-content: flex-end;
margin-right: 50px;
button {
margin-left: 20px;
}
}
}

View File

@@ -12,11 +12,65 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { HttpErrorResponse } from '@angular/common/http';
import { Component } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
Registrar,
RegistrarService,
} from 'src/app/registrar/registrar.service';
import { WhoisService } from './whois.service';
@Component({
selector: 'app-whois',
templateUrl: './whois.component.html',
styleUrls: ['./whois.component.scss'],
providers: [WhoisService],
})
export default class WhoisComponent {}
export default class WhoisComponent {
public static PATH = 'whois';
loading = false;
inEdit = false;
registrar: Registrar;
constructor(
public whoisService: WhoisService,
public registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {
this.registrar = JSON.parse(
JSON.stringify(this.registrarService.registrar)
);
}
enableEdit() {
this.inEdit = true;
}
cancel() {
this.inEdit = false;
this.resetDataSource();
}
save() {
this.loading = true;
this.whoisService.saveChanges(this.registrar).subscribe({
complete: () => {
this.loading = false;
this.resetDataSource();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
this.loading = false;
},
});
this.cancel();
}
resetDataSource() {
this.registrar = JSON.parse(
JSON.stringify(this.registrarService.registrar)
);
}
}

View File

@@ -0,0 +1,45 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { switchMap } from 'rxjs';
import { Address, RegistrarService } from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service';
export interface WhoisRegistrarFields {
ianaIdentifier?: number;
icannReferralEmail?: string;
localizedAddress?: Address;
registrarId?: string;
url?: string;
whoisServer?: string;
}
@Injectable()
export class WhoisService {
whoisRegistrarFields: WhoisRegistrarFields = {};
constructor(
private backend: BackendService,
private registrarService: RegistrarService
) {}
saveChanges(newWhoisRegistrarFields: WhoisRegistrarFields) {
return this.backend.postWhoisRegistrarFields(newWhoisRegistrarFields).pipe(
switchMap(() => {
return this.registrarService.loadRegistrars();
})
);
}
}

View File

@@ -0,0 +1,69 @@
// Copyright 2023 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 { BreakpointObserver } from '@angular/cdk/layout';
import { ComponentType } from '@angular/cdk/portal';
import { Component } from '@angular/core';
import {
MatBottomSheet,
MatBottomSheetRef,
} from '@angular/material/bottom-sheet';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
export interface DialogBottomSheetContent {
init(data: Object): void;
}
/**
* Wraps up a child component in an Angular Material Dalog for desktop or a Bottom Sheet
* component for mobile depending on a screen resolution, with Breaking Point being 599px.
* Child component is required to implement @see DialogBottomSheetContent interface
*/
@Component({
selector: 'app-dialog-bottom-sheet-wrapper',
template: '',
})
export class DialogBottomSheetWrapper {
private elementRef?: MatBottomSheetRef | MatDialogRef<any>;
constructor(
private dialog: MatDialog,
private bottomSheet: MatBottomSheet,
protected breakpointObserver: BreakpointObserver
) {}
open<T extends DialogBottomSheetContent>(
component: ComponentType<T>,
data: any
) {
const config = { data, close: () => this.close() };
if (this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)) {
this.elementRef = this.bottomSheet.open(component);
this.elementRef.instance.init(config);
} else {
this.elementRef = this.dialog.open(component);
this.elementRef.componentInstance.init(config);
}
}
close() {
if (this.elementRef instanceof MatBottomSheetRef) {
this.elementRef.dismiss();
} else if (this.elementRef instanceof MatDialogRef) {
this.elementRef.close();
}
}
}

View File

@@ -19,6 +19,9 @@ import { SecuritySettingsBackendModel } from 'src/app/settings/security/security
import { Contact } from '../../settings/contact/contact.service';
import { Registrar } from '../../registrar/registrar.service';
import { UserData } from './userData.service';
import { WhoisRegistrarFields } from 'src/app/settings/whois/whois.service';
import { DomainListResult } from 'src/app/domains/domainList.service';
@Injectable()
export class BackendService {
@@ -61,6 +64,35 @@ export class BackendService {
);
}
getDomains(
registrarId: string,
checkpointTime?: string,
pageNumber?: number,
resultsPerPage?: number,
totalResults?: number,
searchTerm?: string
): Observable<DomainListResult> {
var url = `/console-api/domain-list?registrarId=${registrarId}`;
if (checkpointTime) {
url += `&checkpointTime=${checkpointTime}`;
}
if (pageNumber) {
url += `&pageNumber=${pageNumber}`;
}
if (resultsPerPage) {
url += `&resultsPerPage=${resultsPerPage}`;
}
if (totalResults) {
url += `&totalResults=${totalResults}`;
}
if (searchTerm) {
url += `&searchTerm=${searchTerm}`;
}
return this.http
.get<DomainListResult>(url)
.pipe(catchError((err) => this.errorCatcher<DomainListResult>(err)));
}
getRegistrars(): Observable<Registrar[]> {
return this.http
.get<Registrar[]>('/console-api/registrars')
@@ -90,4 +122,19 @@ export class BackendService {
securitySettings
);
}
getUserData(): Observable<UserData> {
return this.http
.get<UserData>('/console-api/userdata')
.pipe(catchError((err) => this.errorCatcher<UserData>(err)));
}
postWhoisRegistrarFields(
whoisRegistrarFields: WhoisRegistrarFields
): Observable<WhoisRegistrarFields> {
return this.http.post<WhoisRegistrarFields>(
'/console-api/settings/whois-fields',
whoisRegistrarFields
);
}
}

View File

@@ -0,0 +1,57 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Observable, tap } from 'rxjs';
import { BackendService } from './backend.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { GlobalLoader, GlobalLoaderService } from './globalLoader.service';
export interface UserData {
globalRole: string;
isAdmin: boolean;
productName: string;
supportEmail: string;
supportPhoneNumber: string;
technicalDocsUrl: string;
}
@Injectable({
providedIn: 'root',
})
export class UserDataService implements GlobalLoader {
public userData?: UserData;
constructor(
private backend: BackendService,
protected globalLoader: GlobalLoaderService,
private _snackBar: MatSnackBar
) {
this.getUserData().subscribe(() => {
this.globalLoader.stopGlobalLoader(this);
});
this.globalLoader.startGlobalLoader(this);
}
getUserData(): Observable<UserData> {
return this.backend.getUserData().pipe(
tap((userData: UserData) => {
this.userData = userData;
})
);
}
loadingTimeout() {
this._snackBar.open('Timeout loading user data');
}
}

View File

@@ -0,0 +1,24 @@
// Copyright 2023 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 { NgModule } from '@angular/core';
import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar';
/** Provides a default set of options for the snack bar. */
@NgModule({
providers: [
{ provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { duration: 5000 } },
],
})
export class SnackBarModule {}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
@use "@angular/material" as mat;
@import "app/registrar/registrar-selector.component.scss";
@import "app/registrar/registrarSelector.component.scss";
html,
body {
@@ -44,16 +44,23 @@ body {
&-link {
padding: 0 !important;
text-align: left;
height: 20px !important;
min-width: auto !important;
height: min-content !important;
}
&-section-header {
font-weight: 500;
color: var(--primary) !important;
}
&-title {
color: var(--primary) !important;
text-align: center;
}
&-icon {
font-size: 4rem;
line-height: 4rem;
height: 4rem !important;
width: 4rem !important;
color: var(--text);
font-size: 5rem;
line-height: 5rem;
height: 5rem !important;
width: 5rem !important;
}
&_left {
flex: 1;

View File

@@ -17,20 +17,9 @@ $theme-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
// The warn palette is optional (defaults to red).
$theme-warn: mat.define-palette(mat.$red-palette);
// Create the theme object. A theme consists of configurations for individual
// theming systems such as "color" or "typography".
$theme: mat.define-light-theme(
(
color: (
primary: $theme-primary,
accent: $theme-accent,
warn: $theme-warn,
),
density: 0,
)
);
/** Application specific section **/
/**
** Application specific section - Global styles and mixins
**/
@mixin form-field-density($density) {
$field-typography: mat.define-typography-config(
@@ -46,19 +35,6 @@ $theme: mat.define-light-theme(
@include form-field-density(-5);
}
$foreground: map.merge($theme, mat.$light-theme-foreground-palette);
// Access and define a class with secondary color exposed
.secondary-text {
color: map.get($foreground, "secondary-text");
}
:root {
--primary: #{mat.get-color-from-palette($theme-primary, 500)};
--secondary: #{map.get($foreground, "secondary-text")};
}
@include mat.all-component-themes($theme);
@import "@angular/material/theming";
// Define application specific typography settings, font-family, etc
@@ -67,3 +43,61 @@ $typography-configuration: mat-typography-config(
);
@include angular-material-typography($typography-configuration);
/**
** Light theme
**/
$light-theme: mat.define-light-theme(
(
color: (
primary: $theme-primary,
accent: $theme-accent,
warn: $theme-warn,
),
density: 0,
)
);
// Access and define a class with secondary color exposed
.secondary-text {
color: map.get(mat.$light-theme-foreground-palette, "secondary-text");
}
:root {
--text: #{map.get(mat.$light-theme-foreground-palette, "base")};
--primary: #{mat.get-color-from-palette($theme-primary, 500)};
--secondary: #{map.get(mat.$light-theme-foreground-palette, "secondary-text")};
}
@include mat.all-component-themes($light-theme);
/**
** Dark theme
**/
$dark-theme: mat.define-dark-theme(
(
color: (
primary: mat.define-palette(mat.$pink-palette),
accent: mat.define-palette(mat.$blue-grey-palette),
),
density: 0,
)
);
@mixin _apply-dark-mode-colors() {
@include mat.all-component-colors($dark-theme);
.secondary-text {
color: map.get(mat.$dark-theme-foreground-palette, "secondary-text");
}
:root {
--text: #{map.get(mat.$dark-theme-foreground-palette, "base")};
--primary: #{mat.get-color-from-palette(mat.$pink-palette, 500)};
--secondary: #{map.get(mat.$dark-theme-background-palette, "secondary-text")};
}
}
@media (prefers-color-scheme: dark) {
@include _apply-dark-mode-colors();
}

View File

@@ -43,6 +43,12 @@ public class BatchModule {
public static final String PARAM_DRY_RUN = "dryRun";
public static final String PARAM_FAST = "fast";
@Provides
@Parameter("url")
static String provideUrl(HttpServletRequest req) {
return extractRequiredParameter(req, "url");
}
@Provides
@Parameter("jobName")
static Optional<String> provideJobName(HttpServletRequest req) {

View File

@@ -14,26 +14,20 @@
package google.registry.batch;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.RegistrarUtils.normalizeRegistrarId;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GmailClient;
import google.registry.groups.GroupsConnection;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.UrlConnectionService;
import google.registry.request.UrlConnectionUtils;
import google.registry.request.auth.Auth;
import google.registry.util.EmailMessage;
import java.io.IOException;
import java.util.Set;
import java.net.URL;
import javax.inject.Inject;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.net.ssl.HttpsURLConnection;
/**
* Action that executes a canned script specified by the caller.
@@ -50,88 +44,45 @@ import javax.mail.internet.InternetAddress;
@Action(
service = Action.Service.BACKEND,
path = "/_dr/task/executeCannedScript",
method = POST,
method = {POST, GET},
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
public class CannedScriptExecutionAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final GroupsConnection groupsConnection;
private final GmailClient gmailClient;
private final InternetAddress senderAddress;
private final InternetAddress recipientAddress;
private final String gSuiteDomainName;
@Inject UrlConnectionService urlConnectionService;
@Inject Response response;
@Inject
CannedScriptExecutionAction(
GroupsConnection groupsConnection,
GmailClient gmailClient,
@Config("projectId") String projectId,
@Config("gSuiteDomainName") String gSuiteDomainName,
@Config("newAlertRecipientEmailAddress") InternetAddress recipientAddress) {
this.groupsConnection = groupsConnection;
this.gmailClient = gmailClient;
this.gSuiteDomainName = gSuiteDomainName;
try {
this.senderAddress = new InternetAddress(String.format("%s@%s", projectId, gSuiteDomainName));
} catch (AddressException e) {
throw new RuntimeException(e);
}
this.recipientAddress = recipientAddress;
logger.atInfo().log("Sender:%s; Recipient: %s.", this.senderAddress, this.recipientAddress);
}
@Parameter("url")
String url;
@Inject
CannedScriptExecutionAction() {}
@Override
public void run() {
Integer responseCode = null;
String responseContent = null;
try {
// Invoke canned scripts here.
checkGroupApi();
EmailMessage message = createEmail();
this.gmailClient.sendEmail(message);
logger.atInfo().log("Finished running scripts.");
} catch (Throwable t) {
logger.atWarning().withCause(t).log("Error executing scripts.");
throw new RuntimeException("Execution failed.");
}
}
// Checks if Directory and GroupSettings still work after GWorkspace changes.
void checkGroupApi() {
ImmutableList<Registrar> registrars =
Streams.stream(Registrar.loadAllCached())
.filter(registrar -> registrar.isLive() && registrar.getType() == Registrar.Type.REAL)
.collect(toImmutableList());
logger.atInfo().log("Found %s registrars.", registrars.size());
for (Registrar registrar : registrars) {
for (final RegistrarPoc.Type type : RegistrarPoc.Type.values()) {
String groupKey =
String.format(
"%s-%s-contacts@%s",
normalizeRegistrarId(registrar.getRegistrarId()),
type.getDisplayName(),
gSuiteDomainName);
try {
Set<String> currentMembers = groupsConnection.getMembersOfGroup(groupKey);
logger.atInfo().log("%s has %s members.", groupKey, currentMembers.size());
// One success is enough for validation.
return;
} catch (IOException e) {
logger.atWarning().withCause(e).log("Failed to check %s", groupKey);
}
logger.atInfo().log("Connecting to: %s", url);
HttpsURLConnection connection =
(HttpsURLConnection) urlConnectionService.createConnection(new URL(url));
responseCode = connection.getResponseCode();
logger.atInfo().log("Code: %d", responseCode);
logger.atInfo().log("Headers: %s", connection.getHeaderFields());
responseContent = new String(UrlConnectionUtils.getResponseBytes(connection), UTF_8);
logger.atInfo().log("Response: %s", responseContent);
} catch (Exception e) {
logger.atWarning().withCause(e).log("Connection to %s failed", url);
throw new RuntimeException(e);
} finally {
if (responseCode != null) {
response.setStatus(responseCode);
}
if (responseContent != null) {
response.setPayload(responseContent);
}
}
logger.atInfo().log("Finished checking GroupApis.");
}
EmailMessage createEmail() {
return EmailMessage.newBuilder()
.setFrom(senderAddress)
.setSubject("Test: Please ignore<eom>.")
.setRecipients(ImmutableList.of(recipientAddress))
.setBody("Sent from Nomulus through Google Workspace.")
.build();
}
}

View File

@@ -1,161 +0,0 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.batch;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryEnvironment;
import google.registry.persistence.PersistenceModule.SchemaManagerConnection;
import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Retrier;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.function.Supplier;
import javax.inject.Inject;
/**
* Wipes out all Cloud SQL data in a Nomulus GCP environment.
*
* <p>This class is created for the QA environment, where migration testing with production data
* will happen. A regularly scheduled wipeout is a prerequisite to using production data there.
*/
@Action(
service = Action.Service.BACKEND,
path = "/_dr/task/wipeOutCloudSql",
auth = Auth.AUTH_API_ADMIN)
public class WipeOutCloudSqlAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final ImmutableSet<RegistryEnvironment> FORBIDDEN_ENVIRONMENTS =
ImmutableSet.of(RegistryEnvironment.PRODUCTION, RegistryEnvironment.SANDBOX);
private final Supplier<Connection> connectionSupplier;
private final Response response;
private final Retrier retrier;
@Inject
WipeOutCloudSqlAction(
@SchemaManagerConnection Supplier<Connection> connectionSupplier,
Response response,
Retrier retrier) {
this.connectionSupplier = connectionSupplier;
this.response = response;
this.retrier = retrier;
}
@Override
public void run() {
response.setContentType(PLAIN_TEXT_UTF_8);
if (FORBIDDEN_ENVIRONMENTS.contains(RegistryEnvironment.get())) {
response.setStatus(SC_FORBIDDEN);
response.setPayload("Wipeout is not allowed in " + RegistryEnvironment.get());
return;
}
try {
retrier.callWithRetry(
() -> {
try (Connection conn = connectionSupplier.get()) {
dropAllTables(conn, listTables(conn));
dropAllSequences(conn, listSequences(conn));
}
return null;
},
e -> !(e instanceof SQLException));
response.setStatus(SC_OK);
response.setPayload("Wiped out Cloud SQL in " + RegistryEnvironment.get());
} catch (RuntimeException e) {
logger.atSevere().withCause(e).log("Failed to wipe out Cloud SQL data.");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload("Failed to wipe out Cloud SQL in " + RegistryEnvironment.get());
}
}
/** Returns a list of all tables in the public schema of a Postgresql database. */
static ImmutableList<String> listTables(Connection connection) throws SQLException {
try (ResultSet resultSet =
connection.getMetaData().getTables(null, null, null, new String[] {"TABLE"})) {
ImmutableList.Builder<String> tables = new ImmutableList.Builder<>();
while (resultSet.next()) {
String schema = resultSet.getString("TABLE_SCHEM");
if (schema == null || !schema.equalsIgnoreCase("public")) {
continue;
}
String tableName = resultSet.getString("TABLE_NAME");
tables.add("public.\"" + tableName + "\"");
}
return tables.build();
}
}
static void dropAllTables(Connection conn, ImmutableList<String> tables) throws SQLException {
if (tables.isEmpty()) {
return;
}
try (Statement statement = conn.createStatement()) {
for (String table : tables) {
statement.addBatch(String.format("DROP TABLE IF EXISTS %s CASCADE;", table));
}
for (int code : statement.executeBatch()) {
if (code == Statement.EXECUTE_FAILED) {
throw new RuntimeException("Failed to drop some tables. Please check.");
}
}
}
}
/** Returns a list of all sequences in a Postgresql database. */
static ImmutableList<String> listSequences(Connection conn) throws SQLException {
try (Statement statement = conn.createStatement();
ResultSet resultSet =
statement.executeQuery("SELECT c.relname FROM pg_class c WHERE c.relkind = 'S';")) {
ImmutableList.Builder<String> sequences = new ImmutableList.Builder<>();
while (resultSet.next()) {
sequences.add('\"' + resultSet.getString(1) + '\"');
}
return sequences.build();
}
}
static void dropAllSequences(Connection conn, ImmutableList<String> sequences)
throws SQLException {
if (sequences.isEmpty()) {
return;
}
try (Statement statement = conn.createStatement()) {
for (String sequence : sequences) {
statement.addBatch(String.format("DROP SEQUENCE IF EXISTS %s CASCADE;", sequence));
}
for (int code : statement.executeBatch()) {
if (code == Statement.EXECUTE_FAILED) {
throw new RuntimeException("Failed to drop some sequences. Please check.");
}
}
}
}
}

View File

@@ -209,7 +209,7 @@ public final class RegistryJpaIO {
@ProcessElement
public void processElement(OutputReceiver<T> outputReceiver) {
tm().transactNoRetry(
tm().transact(
() -> {
query.stream().map(resultMapper::apply).forEach(outputReceiver::output);
});

View File

@@ -27,6 +27,10 @@ import static google.registry.beam.rde.RdePipeline.TupleTags.REVISION_ID;
import static google.registry.beam.rde.RdePipeline.TupleTags.SUPERORDINATE_DOMAINS;
import static google.registry.model.reporting.HistoryEntryDao.RESOURCE_TYPES_TO_HISTORY_TYPES;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.SafeSerializationUtils.safeDeserializeCollection;
import static google.registry.util.SafeSerializationUtils.serializeCollection;
import static google.registry.util.SerializeUtils.decodeBase64;
import static google.registry.util.SerializeUtils.encodeBase64;
import static org.apache.beam.sdk.values.TypeDescriptors.kvs;
import com.google.common.collect.ImmutableList;
@@ -65,11 +69,7 @@ import google.registry.rde.PendingDeposit.PendingDepositCoder;
import google.registry.rde.RdeMarshaller;
import google.registry.util.UtilsModule;
import google.registry.xml.ValidationMode;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashSet;
import javax.inject.Inject;
@@ -658,14 +658,8 @@ public class RdePipeline implements Serializable {
*/
@SuppressWarnings("unchecked")
static ImmutableSet<PendingDeposit> decodePendingDeposits(String encodedPendingDeposits) {
try (ObjectInputStream ois =
new ObjectInputStream(
new ByteArrayInputStream(
BaseEncoding.base64Url().omitPadding().decode(encodedPendingDeposits)))) {
return (ImmutableSet<PendingDeposit>) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new IllegalArgumentException("Unable to parse encoded pending deposit map.", e);
}
return ImmutableSet.copyOf(
safeDeserializeCollection(PendingDeposit.class, decodeBase64(encodedPendingDeposits)));
}
/**
@@ -674,12 +668,7 @@ public class RdePipeline implements Serializable {
*/
public static String encodePendingDeposits(ImmutableSet<PendingDeposit> pendingDeposits)
throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(pendingDeposits);
oos.flush();
return BaseEncoding.base64Url().omitPadding().encode(baos.toByteArray());
}
return encodeBase64(serializeCollection(pendingDeposits));
}
public static void main(String[] args) throws IOException, ClassNotFoundException {

View File

@@ -12,12 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
package google.registry.bsa;
@Component({
selector: '[app-domains-widget]',
templateUrl: './domains-widget.component.html',
})
export class DomainsWidgetComponent {
constructor() {}
/** Identifiers of the BSA lists with blocking labels. */
public enum BlockList {
BLOCK,
BLOCK_PLUS;
}

View File

@@ -0,0 +1,46 @@
// Copyright 2023 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.bsa;
/** The processing stages of a download. */
public enum DownloadStage {
/** Downloads BSA block list files. */
DOWNLOAD,
/** Generates block list diffs with the previous download. */
MAKE_DIFF,
/** Applies the label diffs to the database tables. */
APPLY_DIFF,
/**
* Makes a REST API call to BSA endpoint, declaring that processing starts for new orders in the
* diffs.
*/
START_UPLOADING,
/** Makes a REST API call to BSA endpoint, sending the domains that cannot be blocked. */
UPLOAD_DOMAINS_IN_USE,
/** Makes a REST API call to BSA endpoint, declaring the completion of order processing. */
FINISH_UPLOADING,
/** The terminal stage after processing succeeds. */
DONE,
/**
* The terminal stage indicating that the downloads are discarded because their checksums are the
* same as that of the previous download.
*/
NOP,
/**
* The terminal stage indicating that the downloads are not processed because their BSA-generated
* checksums do not match those calculated by us.
*/
CHECKSUMS_NOT_MATCH;
}

View File

@@ -0,0 +1,95 @@
// Copyright 2023 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.bsa;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Maps.transformValues;
import static google.registry.model.tld.Tld.isEnrolledWithBsa;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldType;
import google.registry.model.tld.Tlds;
import google.registry.tldconfig.idn.IdnLabelValidator;
import google.registry.tldconfig.idn.IdnTableEnum;
import google.registry.util.Clock;
import javax.inject.Inject;
import org.joda.time.DateTime;
/**
* Checks labels' validity wrt Idns in TLDs enrolled with BSA.
*
* <p>Each instance takes a snapshot of the TLDs at instantiation time, and should be limited to the
* Request scope.
*/
public class IdnChecker {
private static final IdnLabelValidator IDN_LABEL_VALIDATOR = new IdnLabelValidator();
private final ImmutableMap<IdnTableEnum, ImmutableSet<Tld>> idnToTlds;
private final ImmutableSet<Tld> allTlds;
@Inject
IdnChecker(Clock clock) {
this.idnToTlds = getIdnToTldMap(clock.nowUtc());
allTlds = idnToTlds.values().stream().flatMap(ImmutableSet::stream).collect(toImmutableSet());
}
/** Returns all IDNs in which the {@code label} is valid. */
ImmutableSet<IdnTableEnum> getAllValidIdns(String label) {
return idnToTlds.keySet().stream()
.filter(idnTable -> idnTable.getTable().isValidLabel(label))
.collect(toImmutableSet());
}
/**
* Returns the TLDs that support at least one IDN in the {@code idnTables}.
*
* @param idnTables String names of {@link IdnTableEnum} values
*/
public ImmutableSet<Tld> getSupportingTlds(ImmutableSet<String> idnTables) {
return idnTables.stream()
.map(IdnTableEnum::valueOf)
.filter(idnToTlds::containsKey)
.map(idnToTlds::get)
.flatMap(ImmutableSet::stream)
.collect(toImmutableSet());
}
/**
* Returns the TLDs that do not support any IDN in the {@code idnTables}.
*
* @param idnTables String names of {@link IdnTableEnum} values
*/
public SetView<Tld> getForbiddingTlds(ImmutableSet<String> idnTables) {
return Sets.difference(allTlds, getSupportingTlds(idnTables));
}
private static ImmutableMap<IdnTableEnum, ImmutableSet<Tld>> getIdnToTldMap(DateTime now) {
ImmutableMultimap.Builder<IdnTableEnum, Tld> idnToTldMap = new ImmutableMultimap.Builder();
Tlds.getTldEntitiesOfType(TldType.REAL).stream()
.filter(tld -> isEnrolledWithBsa(tld, now))
.forEach(
tld -> {
for (IdnTableEnum idn : IDN_LABEL_VALIDATOR.getIdnTablesForTld(tld)) {
idnToTldMap.put(idn, tld);
}
});
return ImmutableMap.copyOf(transformValues(idnToTldMap.build().asMap(), ImmutableSet::copyOf));
}
}

View File

@@ -0,0 +1,45 @@
// Copyright 2023 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.bsa;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import google.registry.request.Action;
import google.registry.request.Action.Service;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import javax.inject.Inject;
@Action(
service = Service.BSA,
path = PlaceholderAction.PATH,
method = Action.Method.GET,
auth = Auth.AUTH_API_ADMIN)
public class PlaceholderAction implements Runnable {
private final Response response;
static final String PATH = "/_dr/task/bsaDownload";
@Inject
public PlaceholderAction(Response response) {
this.response = response;
}
@Override
public void run() {
response.setStatus(SC_OK);
response.setPayload("Hello World");
}
}

View File

@@ -0,0 +1,102 @@
// Copyright 2023 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.bsa.persistence;
import com.google.common.base.Objects;
import google.registry.bsa.persistence.BsaDomainInUse.BsaDomainInUseId;
import google.registry.model.CreateAutoTimestamp;
import google.registry.persistence.VKey;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.IdClass;
/** A domain matching a BSA label but is in use (registered or reserved), so cannot be blocked. */
@Entity
@IdClass(BsaDomainInUseId.class)
public class BsaDomainInUse {
@Id String label;
@Id String tld;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
Reason reason;
/**
* Creation time of this record, which is the most recent time when the domain was detected to be
* in use wrt BSA. It may be during the processing of a download, or during some other job that
* refreshes the state.
*
* <p>This field is for information only.
*/
@SuppressWarnings("unused")
@Column(nullable = false)
CreateAutoTimestamp createTime = CreateAutoTimestamp.create(null);
// For Hibernate
BsaDomainInUse() {}
public BsaDomainInUse(String label, String tld, Reason reason) {
this.label = label;
this.tld = tld;
this.reason = reason;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof BsaDomainInUse)) {
return false;
}
BsaDomainInUse that = (BsaDomainInUse) o;
return Objects.equal(label, that.label)
&& Objects.equal(tld, that.tld)
&& reason == that.reason
&& Objects.equal(createTime, that.createTime);
}
@Override
public int hashCode() {
return Objects.hashCode(label, tld, reason, createTime);
}
enum Reason {
REGISTERED,
RESERVED;
}
static class BsaDomainInUseId implements Serializable {
private String label;
private String tld;
// For Hibernate
BsaDomainInUseId() {}
BsaDomainInUseId(String label, String tld) {
this.label = label;
this.tld = tld;
}
}
static VKey<BsaDomainInUse> vKey(String label, String tld) {
return VKey.create(BsaDomainInUse.class, new BsaDomainInUseId(label, tld));
}
}

View File

@@ -0,0 +1,131 @@
// Copyright 2023 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.bsa.persistence;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.bsa.DownloadStage.DOWNLOAD;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.bsa.BlockList;
import google.registry.bsa.DownloadStage;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.persistence.VKey;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
import org.joda.time.DateTime;
/** Records of ongoing and completed download jobs. */
@Entity
@Table(indexes = {@Index(columnList = "creationTime")})
public class BsaDownload {
private static final Joiner CSV_JOINER = Joiner.on(',');
private static final Splitter CSV_SPLITTER = Splitter.on(',');
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long jobId;
@Column(nullable = false)
CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
@Column(nullable = false)
UpdateAutoTimestamp updateTime = UpdateAutoTimestamp.create(null);
@Column(nullable = false)
String blockListChecksums = "";
@Column(nullable = false)
@Enumerated(EnumType.STRING)
DownloadStage stage = DOWNLOAD;
BsaDownload() {}
long getJobId() {
return jobId;
}
DateTime getCreationTime() {
return creationTime.getTimestamp();
}
/**
* Returns the starting time of this job as a string, which can be used as folder name on GCS when
* storing download data.
*/
public String getJobName() {
return getCreationTime().toString();
}
public DownloadStage getStage() {
return this.stage;
}
BsaDownload setStage(DownloadStage stage) {
this.stage = stage;
return this;
}
BsaDownload setChecksums(ImmutableMap<BlockList, String> checksums) {
blockListChecksums =
CSV_JOINER.withKeyValueSeparator("=").join(ImmutableSortedMap.copyOf(checksums));
return this;
}
ImmutableMap<BlockList, String> getChecksums() {
if (blockListChecksums.isEmpty()) {
return ImmutableMap.of();
}
return CSV_SPLITTER.withKeyValueSeparator('=').split(blockListChecksums).entrySet().stream()
.collect(
toImmutableMap(entry -> BlockList.valueOf(entry.getKey()), entry -> entry.getValue()));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof BsaDownload)) {
return false;
}
BsaDownload that = (BsaDownload) o;
return Objects.equal(creationTime, that.creationTime)
&& Objects.equal(updateTime, that.updateTime)
&& Objects.equal(blockListChecksums, that.blockListChecksums)
&& stage == that.stage;
}
@Override
public int hashCode() {
return Objects.hashCode(creationTime, updateTime, blockListChecksums, stage);
}
static VKey<BsaDownload> vKey(long jobId) {
return VKey.create(BsaDownload.class, Long.valueOf(jobId));
}
}

View File

@@ -0,0 +1,79 @@
// Copyright 2023 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.bsa.persistence;
import com.google.common.base.Objects;
import google.registry.persistence.VKey;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.joda.time.DateTime;
/**
* Specifies a second-level TLD name that should be blocked from registration in all TLDs except by
* the label's owner.
*
* <p>The label is valid (wrt IDN) in at least one TLD.
*/
@Entity
public final class BsaLabel {
@Id String label;
/**
* Creation time of this label. This field is for human use, and should give the name of the GCS
* folder that contains the downloaded BSA data.
*
* <p>See {@link BsaDownload#getCreationTime} and {@link BsaDownload#getJobName} for more
* information.
*/
@SuppressWarnings("unused")
@Column(nullable = false)
DateTime creationTime;
// For Hibernate.
BsaLabel() {}
BsaLabel(String label, DateTime creationTime) {
this.label = label;
this.creationTime = creationTime;
}
/** Returns the label to be blocked. */
public String getLabel() {
return label;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof BsaLabel)) {
return false;
}
BsaLabel label1 = (BsaLabel) o;
return Objects.equal(label, label1.label) && Objects.equal(creationTime, label1.creationTime);
}
@Override
public int hashCode() {
return Objects.hashCode(label, creationTime);
}
static VKey<BsaLabel> vKey(String label) {
return VKey.create(BsaLabel.class, label);
}
}

View File

@@ -0,0 +1,73 @@
// Copyright 2023 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.bsa.persistence;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableMap;
import google.registry.bsa.BlockList;
import google.registry.bsa.DownloadStage;
import java.util.Optional;
/** Information needed when handling a download from BSA. */
@AutoValue
public abstract class DownloadSchedule {
abstract long jobId();
public abstract String jobName();
public abstract DownloadStage stage();
/** The most recent job that ended in the {@code DONE} stage. */
public abstract Optional<CompletedJob> latestCompleted();
/**
* Returns true if download should be processed even if the checksums show that it has not changed
* from the previous one.
*/
abstract boolean alwaysDownload();
static DownloadSchedule of(BsaDownload currentJob) {
return new AutoValue_DownloadSchedule(
currentJob.getJobId(),
currentJob.getJobName(),
currentJob.getStage(),
Optional.empty(),
/* alwaysDownload= */ true);
}
static DownloadSchedule of(
BsaDownload currentJob, CompletedJob latestCompleted, boolean alwaysDownload) {
return new AutoValue_DownloadSchedule(
currentJob.getJobId(),
currentJob.getJobName(),
currentJob.getStage(),
Optional.of(latestCompleted),
/* alwaysDownload= */ alwaysDownload);
}
/** Information about a completed BSA download job. */
@AutoValue
public abstract static class CompletedJob {
public abstract String jobName();
public abstract ImmutableMap<BlockList, String> checksums();
static CompletedJob of(BsaDownload completedJob) {
return new AutoValue_DownloadSchedule_CompletedJob(
completedJob.getJobName(), completedJob.getChecksums());
}
}
}

View File

@@ -0,0 +1,131 @@
// Copyright 2023 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.bsa.persistence;
import static com.google.common.base.Verify.verify;
import static google.registry.bsa.DownloadStage.CHECKSUMS_NOT_MATCH;
import static google.registry.bsa.DownloadStage.DONE;
import static google.registry.bsa.DownloadStage.NOP;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.joda.time.Duration.standardSeconds;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import google.registry.bsa.persistence.DownloadSchedule.CompletedJob;
import google.registry.util.Clock;
import java.util.Optional;
import javax.inject.Inject;
import org.joda.time.Duration;
/**
* Assigns work for each cron invocation of the BSA Download job.
*
* <p>The download job is invoked at a divisible fraction of the desired data freshness to
* accommodate potential retries. E.g., for 30-minute data freshness with up to two retries on
* error, the cron schedule for the job should be set to 10 minutes.
*
* <p>The processing of each BSA download progresses through multiple stages as described in {@code
* DownloadStage} until it reaches one of the terminal stages. Each stage is check-pointed on
* completion, therefore if an invocation fails mid-process, the next invocation will skip the
* completed stages. No new downloads will start as long as the most recent one is still being
* processed.
*
* <p>When a new download is scheduled, the block list checksums from the most recent completed job
* is included. If the new checksums match the previous ones, the download may be skipped and the
* job should terminate in the {@code NOP} stage. However, if the checksums have stayed unchanged
* for longer than the user-provided {@code maxNopInterval}, the download will be processed.
*
* <p>The BSA downloads contains server-provided checksums. If they do not match the checksums
* generated on Nomulus' side, the download is skipped and the job should terminate in the {@code
* CHECKSUMS_NOT_MATCH} stage.
*/
public final class DownloadScheduler {
/** Allows a new download to proceed if the cron job fires a little early due to NTP drift. */
private static final Duration CRON_JITTER = standardSeconds(5);
private final Duration downloadInterval;
private final Duration maxNopInterval;
private final Clock clock;
@Inject
DownloadScheduler(Duration downloadInterval, Duration maxNopInterval, Clock clock) {
this.downloadInterval = downloadInterval;
this.maxNopInterval = maxNopInterval;
this.clock = clock;
}
/**
* Returns a {@link DownloadSchedule} instance that describes the work to be performed by an
* invocation of the download action, if applicable; or {@link Optional#empty} when there is
* nothing to do.
*/
public Optional<DownloadSchedule> schedule() {
return tm().transact(
() -> {
ImmutableList<BsaDownload> recentJobs = loadRecentProcessedJobs();
if (recentJobs.isEmpty()) {
// No jobs initiated ever.
return Optional.of(scheduleNewJob(Optional.empty()));
}
BsaDownload mostRecent = recentJobs.get(0);
if (mostRecent.getStage().equals(DONE)) {
return isTimeAgain(mostRecent, downloadInterval)
? Optional.of(scheduleNewJob(Optional.of(mostRecent)))
: Optional.empty();
} else if (recentJobs.size() == 1) {
// First job ever, still in progress
return Optional.of(DownloadSchedule.of(recentJobs.get(0)));
} else {
// Job in progress, with completed previous jobs.
BsaDownload prev = recentJobs.get(1);
verify(prev.getStage().equals(DONE), "Unexpectedly found two ongoing jobs.");
return Optional.of(
DownloadSchedule.of(
mostRecent,
CompletedJob.of(prev),
isTimeAgain(mostRecent, maxNopInterval)));
}
});
}
private boolean isTimeAgain(BsaDownload mostRecent, Duration interval) {
return mostRecent.getCreationTime().plus(interval).minus(CRON_JITTER).isBefore(clock.nowUtc());
}
/**
* Adds a new {@link BsaDownload} to the database and returns a {@link DownloadSchedule} for it.
*/
private DownloadSchedule scheduleNewJob(Optional<BsaDownload> prevJob) {
BsaDownload job = new BsaDownload();
tm().insert(job);
return prevJob
.map(
prev ->
DownloadSchedule.of(job, CompletedJob.of(prev), isTimeAgain(prev, maxNopInterval)))
.orElseGet(() -> DownloadSchedule.of(job));
}
@VisibleForTesting
ImmutableList<BsaDownload> loadRecentProcessedJobs() {
return ImmutableList.copyOf(
tm().getEntityManager()
.createQuery(
"FROM BsaDownload WHERE stage NOT IN :nop_stages ORDER BY creationTime DESC")
.setParameter("nop_stages", ImmutableList.of(CHECKSUMS_NOT_MATCH, NOP))
.setMaxResults(2)
.getResultList());
}
}

View File

@@ -28,6 +28,7 @@ import com.google.common.base.Ascii;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import dagger.Module;
@@ -879,6 +880,17 @@ public final class RegistryConfig {
return Optional.ofNullable(config.misc.sheetExportId);
}
/**
* Returns the desired delay between outgoing emails when sending in bulk.
*
* <p>Gmail apparently has unpublished limits on peak throughput over short period.
*/
@Provides
@Config("emailThrottleDuration")
public static Duration provideEmailThrottleSeconds(RegistryConfigSettings config) {
return Duration.standardSeconds(config.misc.emailThrottleSeconds);
}
/**
* Returns the email address we send various alert e-mails to.
*
@@ -1163,44 +1175,6 @@ public final class RegistryConfig {
return CONFIG_SETTINGS.get();
}
/**
* Provides the OAuth scopes that authentication logic should detect on access tokens.
*
* <p>This list should be a superset of the required OAuth scope set provided below. Note that
* ideally, this setting would not be required and all scopes on an access token would be
* detected automatically, but that is not the case due to the way {@code OAuthService} works.
*
* <p>This is an independent setting from the required OAuth scopes (below) to support use cases
* where certain actions require some additional scope (e.g. access to a user's Google Drive)
* but that scope shouldn't be required for authentication alone; in that case the Drive scope
* would be specified only for this setting, allowing that action to check for its presence.
*/
@Provides
@Config("availableOauthScopes")
public static ImmutableSet<String> provideAvailableOauthScopes(RegistryConfigSettings config) {
return ImmutableSet.copyOf(config.auth.availableOauthScopes);
}
/**
* Provides the OAuth scopes that are required for authenticating successfully.
*
* <p>This set contains the scopes which must be present to authenticate a user. It should be a
* subset of the scopes we request from the OAuth interface, provided above.
*
* <p>If we feel the need, we could define additional fixed scopes, similar to the Java remote
* API, which requires at least one of:
*
* <ul>
* <li>{@code https://www.googleapis.com/auth/appengine.apis}
* <li>{@code https://www.googleapis.com/auth/cloud-platform}
* </ul>
*/
@Provides
@Config("requiredOauthScopes")
public static ImmutableSet<String> provideRequiredOauthScopes(RegistryConfigSettings config) {
return ImmutableSet.copyOf(config.auth.requiredOauthScopes);
}
/**
* Provides service account email addresses allowed to authenticate with the app at {@link
* google.registry.request.auth.AuthSettings.AuthLevel#APP} level.
@@ -1212,19 +1186,18 @@ public final class RegistryConfig {
return ImmutableSet.copyOf(config.auth.allowedServiceAccountEmails);
}
/** Provides the allowed OAuth client IDs (could be multibinding). */
@Provides
@Config("allowedOauthClientIds")
public static ImmutableSet<String> provideAllowedOauthClientIds(RegistryConfigSettings config) {
return ImmutableSet.copyOf(config.auth.allowedOauthClientIds);
}
@Provides
@Config("oauthClientId")
public static String provideOauthClientId(RegistryConfigSettings config) {
return config.auth.oauthClientId;
}
@Provides
@Config("fallbackOauthClientId")
public static String provideFallbackOauthClientId(RegistryConfigSettings config) {
return config.auth.fallbackOauthClientId;
}
/**
* Provides the OAuth scopes required for accessing Google APIs using the default credential.
*/
@@ -1424,6 +1397,86 @@ public final class RegistryConfig {
return config.bulkPricingPackageMonitoring.bulkPricingPackageDomainLimitUpgradeEmailBody;
}
@Provides
@Config("bsaGcsBucket")
public static String provideBsaGcsBucket(@Config("projectId") String projectId) {
return projectId + "-bsa";
}
@Provides
@Config("bsaChecksumAlgorithm")
public static String provideBsaChecksumAlgorithm(RegistryConfigSettings config) {
return config.bsa.bsaChecksumAlgorithm;
}
@Provides
@Config("bsaLockLeaseExpiry")
public static Duration provideBsaLockLeaseExpiry(RegistryConfigSettings config) {
return Duration.standardMinutes(config.bsa.bsaLockLeaseExpiryMinutes);
}
/** Returns the desired interval between successive BSA downloads. */
@Provides
@Config("bsaDownloadInterval")
public static Duration provideBsaDownloadInterval(RegistryConfigSettings config) {
return Duration.standardMinutes(config.bsa.bsaDownloadIntervalMinutes);
}
/**
* Returns the maximum period when a BSA download can be skipped due to the checksum-based
* equality check with the previous download.
*/
@Provides
@Config("bsaMaxNopInterval")
public static Duration provideBsaMaxNopInterval(RegistryConfigSettings config) {
return Duration.standardHours(config.bsa.bsaMaxNopIntervalHours);
}
@Provides
@Config("bsaLabelTxnBatchSize")
public static int provideBsaLabelTxnBatchSize(RegistryConfigSettings config) {
return config.bsa.bsaLabelTxnBatchSize;
}
@Provides
@Config("bsaAuthUrl")
public static String provideBsaAuthUrl(RegistryConfigSettings config) {
return config.bsa.authUrl;
}
@Provides
@Config("bsaAuthTokenExpiry")
public static Duration provideBsaAuthTokenExpiry(RegistryConfigSettings config) {
return Duration.standardSeconds(config.bsa.authTokenExpirySeconds);
}
@Provides
@Config("bsaDataUrls")
public static ImmutableMap<String, String> provideBsaDataUrls(RegistryConfigSettings config) {
return ImmutableMap.copyOf(config.bsa.dataUrls);
}
/** Provides the BSA Http endpoint for reporting order processing status. */
@Provides
@Config("bsaOrderStatusUrl")
public static String provideBsaOrderStatusUrls(RegistryConfigSettings config) {
return config.bsa.orderStatusUrl;
}
/** Provides the BSA Http endpoint for reporting new unblockable domains. */
@Provides
@Config("bsaAddUnblockableDomainsUrl")
public static String provideBsaAddUnblockableDomainsUrls(RegistryConfigSettings config) {
return String.format("%s?%s", config.bsa.unblockableDomainsUrl, "action=add");
}
/** Provides the BSA Http endpoint for reporting domains that have become blockable. */
@Provides
@Config("bsaRemoveUnblockableDomainsUrl")
public static String provideBsaRemoveUnblockableDomainsUrls(RegistryConfigSettings config) {
return String.format("%s?%s", config.bsa.unblockableDomainsUrl, "action=remove");
}
private static String formatComments(String text) {
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
.map(s -> "# " + s)
@@ -1467,6 +1520,15 @@ public final class RegistryConfig {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.backendServiceUrl);
}
/**
* Returns the address of the Nomulus app bsa HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getBsaServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.bsaServiceUrl);
}
/**
* Returns the address of the Nomulus app tools HTTP server.
*
@@ -1563,9 +1625,9 @@ public final class RegistryConfig {
return CONFIG_SETTINGS.get().hibernate.connectionIsolation;
}
/** Returns true if per-transaction isolation level is enabled. */
public static boolean getHibernatePerTransactionIsolationEnabled() {
return CONFIG_SETTINGS.get().hibernate.perTransactionIsolation;
/** Returns true if nested calls to {@code tm().transact()} are allowed. */
public static boolean getHibernateAllowNestedTransactions() {
return CONFIG_SETTINGS.get().hibernate.allowNestedTransactions;
}
/** Returns true if hibernate.show_sql is enabled. */

View File

@@ -43,6 +43,7 @@ public class RegistryConfigSettings {
public ContactHistory contactHistory;
public DnsUpdate dnsUpdate;
public BulkPricingPackageMonitoring bulkPricingPackageMonitoring;
public Bsa bsa;
/** Configuration options that apply to the entire GCP project. */
public static class GcpProject {
@@ -52,17 +53,16 @@ public class RegistryConfigSettings {
public boolean isLocal;
public String defaultServiceUrl;
public String backendServiceUrl;
public String bsaServiceUrl;
public String toolsServiceUrl;
public String pubapiServiceUrl;
}
/** Configuration options for authenticating users. */
public static class Auth {
public List<String> availableOauthScopes;
public List<String> requiredOauthScopes;
public List<String> allowedOauthClientIds;
public List<String> allowedServiceAccountEmails;
public String oauthClientId;
public String fallbackOauthClientId;
}
/** Configuration options for accessing Google APIs. */
@@ -115,7 +115,7 @@ public class RegistryConfigSettings {
/** Configuration for Hibernate. */
public static class Hibernate {
public boolean perTransactionIsolation;
public boolean allowNestedTransactions;
public String connectionIsolation;
public String logSqlQueries;
public String hikariConnectionTimeout;
@@ -208,6 +208,7 @@ public class RegistryConfigSettings {
public static class Misc {
public String sheetExportId;
public boolean isEmailSendingEnabled;
public int emailThrottleSeconds;
public String alertRecipientEmailAddress;
// TODO(b/279671974): remove below field after migration
public String newAlertRecipientEmailAddress;
@@ -263,4 +264,18 @@ public class RegistryConfigSettings {
public String bulkPricingPackageDomainLimitUpgradeEmailSubject;
public String bulkPricingPackageDomainLimitUpgradeEmailBody;
}
/** Configurations for integration with Brand Safety Alliance (BSA) API. */
public static class Bsa {
public String bsaChecksumAlgorithm;
public int bsaLockLeaseExpiryMinutes;
public int bsaDownloadIntervalMinutes;
public int bsaMaxNopIntervalHours;
public int bsaLabelTxnBatchSize;
public String authUrl;
public int authTokenExpirySeconds;
public Map<String, String> dataUrls;
public String orderStatusUrl;
public String unblockableDomainsUrl;
}
}

View File

@@ -20,9 +20,11 @@ gcpProject:
# URLs of the services for the project.
defaultServiceUrl: https://default.example.com
backendServiceUrl: https://backend.example.com
bsaServiceUrl: https://bsa.example.com
toolsServiceUrl: https://tools.example.com
pubapiServiceUrl: https://pubapi.example.com
gSuite:
# Publicly accessible domain name of the running G Suite instance.
domainName: domain-registry.example
@@ -189,11 +191,13 @@ registryPolicy:
sunriseDomainCreateDiscount: 0.15
hibernate:
# Make it possible to specify the isolation level for each transaction. If set
# to true, nested transactions will throw an exception. If set to false, a
# transaction with the isolation override specified will still execute at the
# default level (specified below).
perTransactionIsolation: false
# If set to false, calls to tm().transact() cannot be nested. If set to true,
# nested calls to tm().transact() are allowed, as long as they do not specify
# a transaction isolation level override. These nested transactions should
# either be refactored to non-nested transactions, or changed to
# tm().reTransact(), which explicitly allows nested transactions, but does not
# allow setting an isolation level override.
allowNestedTransactions: true
# Make 'SERIALIZABLE' the default isolation level to ensure correctness.
#
@@ -304,24 +308,6 @@ caching:
# Note: Only allowedServiceAccountEmails and oauthClientId should be configured.
# Other fields are related to OAuth-based authentication and will be removed.
auth:
# Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth.
# OAuth scopes to detect on access tokens. Superset of requiredOauthScopes.
availableOauthScopes:
- https://www.googleapis.com/auth/userinfo.email
# Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth.
# OAuth scopes required for authenticating. Subset of availableOauthScopes.
requiredOauthScopes:
- https://www.googleapis.com/auth/userinfo.email
# Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth.
# OAuth client IDs that are allowed to authenticate and communicate with
# backend services, e.g. nomulus tool, EPP proxy, etc. The value in
# registryTool.clientId field should be included in this list. Client IDs are
# typically of the format
# numbers-alphanumerics.apps.googleusercontent.com
allowedOauthClientIds: []
# Service accounts (e.g. default service account, account used by Cloud
# Scheduler) allowed to send authenticated requests.
allowedServiceAccountEmails:
@@ -337,6 +323,10 @@ auth:
# the same as this one.
oauthClientId: iap-oauth-clientid
# Same as above, but serve as a fallback, so we can switch the client ID of
# the proxy without downtime.
fallbackOauthClientId: fallback-oauth-clientid
credentialOAuth:
# OAuth scopes required for accessing Google APIs using the default
# credential.
@@ -443,6 +433,9 @@ misc:
# Whether emails may be sent. For Prod and Sandbox this should be true.
isEmailSendingEnabled: false
# Delay between bulk messages to avoid triggering Gmail fraud checks
emailThrottleSeconds: 30
# Address we send alert summary emails to.
alertRecipientEmailAddress: email@example.com
@@ -613,3 +606,18 @@ bulkPricingPackageMonitoring:
Registrar: %3$s
Active Domain Limit: %4$s
Current Active Domains: %5$s
# Configurations for integration with Brand Safety Alliance (BSA) API
bsa:
# Http endpoint for acquiring Auth tokens.
authUrl: "https://"
# Auth token expiry.
authTokenExpirySeconds: 1800
# Http endpoints for downloading data
dataUrls:
"BLOCK": "https://"
"BLOCK_PLUS": "https://"
# Http endpoint for reporting order processing status
orderStatusUrl: "https://"
# Http endpoint for reporting changes in the set of unblockable domains.
unblockableDomainsUrl: "https://"

View File

@@ -1,76 +0,0 @@
# This is a sample production config (to be deployed in the WEB-INF directory).
# This is the same as what Google Registry runs in production, except with
# placeholders for Google-specific settings.
gcpProject:
projectId: placeholder
# Set to true if running against local servers (localhost)
isLocal: false
# The "<service>-dot-" prefix is used on the project ID in this URL in order
# to get around an issue with double-wildcard SSL certs.
defaultServiceUrl: https://domain-registry-placeholder.appspot.com
backendServiceUrl: https://backend-dot-domain-registry-placeholder.appspot.com
toolsServiceUrl: https://tools-dot-domain-registry-placeholder.appspot.com
pubapiServiceUrl: https://pubapi-dot-domain-registry-placeholder.appspot.com
gSuite:
domainName: placeholder
outgoingEmailDisplayName: placeholder
outgoingEmailAddress: placeholder
adminAccountEmailAddress: placeholder
supportGroupEmailAddress: placeholder
registryPolicy:
contactAndHostRoidSuffix: placeholder
productName: placeholder
greetingServerId: placeholder
registrarChangesNotificationEmailAddresses:
- placeholder
- placeholder
defaultRegistrarWhoisServer: placeholder
tmchCaMode: PRODUCTION
tmchCrlUrl: http://crl.icann.org/tmch.crl
tmchMarksDbUrl: https://ry.marksdb.org
checkApiServletClientId: placeholder
registryAdminClientId: placeholder
whoisDisclaimer: |
multi-line
placeholder
icannReporting:
icannTransactionsReportingUploadUrl: https://ry-api.icann.org/report/registrar-transactions
icannActivityReportingUploadUrl: https://ry-api.icann.org/report/registry-functions-activity
oAuth:
allowedOauthClientIds:
- placeholder.apps.googleusercontent.com
- placeholder-for-proxy
rde:
reportUrlPrefix: https://ry-api.icann.org/report/registry-escrow-report
uploadUrl: sftp://placeholder@sftpipm2.ironmountain.com/Outbox
sshIdentityEmailAddress: placeholder
registrarConsole:
logoFilename: placeholder
supportPhoneNumber: placeholder
supportEmailAddress: placeholder
announcementsEmailAddress: placeholder
integrationEmailAddress: placeholder
technicalDocsUrl: https://drive.google.com/drive/folders/placeholder
misc:
sheetExportId: placeholder
cloudDns:
rootUrl: null
servicePath: null
keyring:
activeKeyring: KMS
kms:
projectId: placeholder
registryTool:
clientId: placeholder.apps.googleusercontent.com
clientSecret: placeholder

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>backend</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<service>bsa</service>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
<max-instances>100</max-instances>
<idle-timeout>10m</idle-timeout>
</basic-scaling>
<system-properties>
<property name="java.util.logging.config.file"
value="WEB-INF/logging.properties"/>
<property name="google.registry.environment"
value="alpha"/>
</system-properties>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-alpha/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<static-files>
<include path="/*.html" expiration="1m"/>
</static-files>
</appengine-web-app>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>default</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>pubapi</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>tools</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>

View File

@@ -31,6 +31,12 @@ encoding="UTF-8"?>
<context-root>backend</context-root>
</web>
</module>
<module>
<web>
<web-uri>bsa</web-uri>
<context-root>bsa</context-root>
</web>
</module>
<module>
<web>
<web-uri>tools</web-uri>

View File

@@ -0,0 +1,17 @@
# A default java.util.logging configuration.
# (All App Engine logging is through java.util.logging by default).
#
# To use this configuration, copy it into your application's WEB-INF
# folder and add the following to your appengine-web.xml:
#
# <system-properties>
# <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
# </system-properties>
#
# Set the default logging level for all loggers to INFO.
.level = INFO
# Turn off logging in Hibernate classes for misleading ERROR-level logs
org.hibernate.engine.jdbc.batch.internal.BatchingBatch.level=OFF
org.hibernate.engine.jdbc.spi.SqlExceptionHelper.level=OFF

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- Servlets -->
<!-- Servlet for injected backends actions -->
<servlet>
<display-name>BsaServlet</display-name>
<servlet-name>bsa-servlet</servlet-name>
<servlet-class>google.registry.module.bsa.BsaServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- Test action -->
<servlet-mapping>
<servlet-name>bsa-servlet</servlet-name>
<url-pattern>/_dr/task/bsaDownload</url-pattern>
</servlet-mapping>
<!-- Security config -->
<security-constraint>
<web-resource-collection>
<web-resource-name>Internal</web-resource-name>
<description>
Admin-only internal section. Requests for paths covered by the URL patterns below will be
checked for a logged-in user account that's allowed to access the AppEngine admin console
(NOTE: this includes Editor/Viewer permissions in addition to Owner and the new IAM
App Engine Admin role. See https://cloud.google.com/appengine/docs/java/access-control
specifically the "Access handlers that have a login:admin restriction" line.)
TODO(b/28219927): lift some of these restrictions so that we can allow OAuth authentication
for endpoints that need to be accessed by open-source automated processes.
</description>
<!-- Internal AppEngine endpoints. The '_ah' is short for app hosting. -->
<url-pattern>/_ah/*</url-pattern>
<!-- Registrar console (should not be available on non-default module). -->
<url-pattern>/registrar*</url-pattern>
<!-- Verbatim JavaScript sources (only visible to admins for debugging). -->
<url-pattern>/assets/sources/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
<!-- Repeated here since catch-all rule below is not inherited. -->
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- Require TLS on all requests. -->
<security-constraint>
<web-resource-collection>
<web-resource-name>Secure</web-resource-name>
<description>
Require encryption for all paths. http URLs will be redirected to https.
</description>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
</web-app>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>backend</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<service>bsa</service>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
<max-instances>10</max-instances>
<idle-timeout>10m</idle-timeout>
</basic-scaling>
<system-properties>
<property name="java.util.logging.config.file"
value="WEB-INF/logging.properties"/>
<property name="google.registry.environment"
value="crash"/>
</system-properties>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-crash/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<static-files>
<include path="/*.html" expiration="1m"/>
</static-files>
</appengine-web-app>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>default</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<basic-scaling>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>pubapi</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>tools</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>backend</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<service>bsa</service>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
<max-instances>10</max-instances>
<idle-timeout>10m</idle-timeout>
</basic-scaling>
<system-properties>
<property name="java.util.logging.config.file"
value="WEB-INF/logging.properties"/>
<property name="google.registry.environment"
value="local"/>
<property name="appengine.generated.dir"
value="/tmp/domain-registry-appengine-generated/local/"/>
</system-properties>
<static-files>
<include path="/*.html">
<http-header name="Cache-Control" value="max-age=0,must-revalidate" />
</include>
</static-files>
</appengine-web-app>

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