Compare commits

...
Author SHA1 Message Date
Pavlo TkachandGitHub d30cc202d2 Replace CSRF source from cookie to header in the console requests (#3047)
* Update XSRF cookie to be a header

* Replace CSRF cookie with header
2026-05-15 17:45:33 +00:00
Juan CelhayandGitHub 0998d5485c add additional label to nomulus deployments (#3046) 2026-05-14 20:01:18 +00:00
Juan CelhayandGitHub 1bff89085b Build Cloud Deploy artifacts via Cloud Build job (#3044)
* add README

* updating comments/files
2026-05-14 15:35:32 +00:00
Weimin YuandGitHub d9d83205c7 Verify user can send email (#3045)
Change the CannedScriptExecutionAction to send a email
message as a user-specified G workspace user.

This change is part of b/510340944, to verify that a newly
added dedicated sender is properly set up for sending emails.

Once the new sender is tested, the changes in this PR can be
dropped.
2026-05-14 15:13:57 +00:00
16 changed files with 324 additions and 59 deletions
+123
View File
@@ -628,6 +628,18 @@
}
}
},
"node_modules/@angular/build/node_modules/@types/node": {
"version": "25.7.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@types/node/-/node-25.7.0.tgz",
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~7.21.0"
}
},
"node_modules/@angular/build/node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz",
@@ -641,6 +653,24 @@
"vite": "^6.0.0 || ^7.0.0"
}
},
"node_modules/@angular/build/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/build/node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -664,6 +694,22 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@angular/build/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/build/node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@@ -697,6 +743,15 @@
"node": ">= 12"
}
},
"node_modules/@angular/build/node_modules/undici-types": {
"version": "7.21.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/undici-types/-/undici-types-7.21.0.tgz",
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@angular/build/node_modules/vite": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
@@ -916,6 +971,24 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@angular/cli/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/cli/node_modules/cli-spinners": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
@@ -1019,6 +1092,22 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@angular/cli/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/cli/node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@@ -4606,6 +4695,24 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@schematics/angular/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@schematics/angular/node_modules/cli-spinners": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
@@ -4709,6 +4816,22 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@schematics/angular/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@schematics/angular/node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+10 -2
View File
@@ -23,7 +23,11 @@ import { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import {
HttpClientXsrfModule,
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import {
@@ -118,6 +122,10 @@ export class SelectedRegistrarModule {}
MaterialModule,
SelectedRegistrarModule,
SnackBarModule,
HttpClientXsrfModule.withOptions({
cookieName: 'X-CSRF-Token',
headerName: 'X-CSRF-Token',
}),
],
providers: [
BackendService,
@@ -130,7 +138,7 @@ export class SelectedRegistrarModule {}
subscriptSizing: 'dynamic',
},
},
provideHttpClient(),
provideHttpClient(withInterceptorsFromDi()),
],
})
export class AppModule {}
@@ -54,9 +54,15 @@ public class BatchModule {
static final int DEFAULT_MAX_QPS = 10;
@Provides
@Parameter("url")
static String provideUrl(HttpServletRequest req) {
return extractRequiredParameter(req, "url");
@Parameter("sender")
static String provideSender(HttpServletRequest req) {
return extractRequiredParameter(req, "sender");
}
@Provides
@Parameter("receiver")
static String provideReceiver(HttpServletRequest req) {
return extractRequiredParameter(req, "receiver");
}
@Provides
@@ -16,30 +16,32 @@ package google.registry.batch;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.api.services.gmail.Gmail;
import com.google.common.flogger.FluentLogger;
import dagger.Lazy;
import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GmailClient;
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 google.registry.util.Retrier;
import jakarta.inject.Inject;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
/**
* Action that executes a canned script specified by the caller.
*
* <p>This class provides a hook for invoking hard-coded methods. The main use case is to verify in
* Sandbox and Production environments new features that depend on environment-specific
* configurations. For example, the {@code DelegatedCredential}, which requires correct GWorkspace
* configuration, has been tested this way. Since it is a hassle to add or remove endpoints, we keep
* this class all the time.
* configurations.
*
* <p>This action can be invoked using the Nomulus CLI command: {@code nomulus -e ${env} curl
* --service BACKEND -X POST -u '/_dr/task/executeCannedScript}'}
* --service BACKEND -X POST -d 'sender=sender@example.com' -d 'receiver=receiver@example.com' -u
* '/_dr/task/executeCannedScript'}
*/
@Action(
service = Action.Service.BACKEND,
@@ -50,39 +52,50 @@ import javax.net.ssl.HttpsURLConnection;
public class CannedScriptExecutionAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject UrlConnectionService urlConnectionService;
@Inject Lazy<Gmail> gmail;
@Inject Retrier retrier;
@Inject
@Config("isEmailSendingEnabled")
boolean isEmailSendingEnabled;
@Inject Response response;
@Inject
@Parameter("url")
String url;
@Parameter("sender")
String sender;
@Inject
@Parameter("receiver")
String receiver;
@Inject
CannedScriptExecutionAction() {}
@Override
public void run() {
Integer responseCode = null;
String responseContent = null;
// For b/510340944, validating a new G Workspace user can send email. Code below can be
// removed or changed afterward.
try {
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);
logger.atInfo().log("Sending email from %s to %s", sender, receiver);
GmailClient gmailClient =
new GmailClient(
gmail, retrier, isEmailSendingEnabled, sender, sender, new InternetAddress(sender));
gmailClient.sendEmail(
EmailMessage.newBuilder()
.addRecipient(new InternetAddress(receiver))
.setSubject(String.format("Email send test from %s", sender))
.setBody(String.format("This is a test email sent from %s to %s.", sender, receiver))
.build());
response.setPayload("Email sent successfully.");
} catch (AddressException e) {
logger.atWarning().withCause(e).log(
"Invalid email address: sender=%s, receiver=%s", sender, receiver);
response.setStatus(400);
response.setPayload("Invalid email address provided.");
} catch (Exception e) {
logger.atWarning().withCause(e).log("Connection to %s failed", url);
logger.atSevere().withCause(e).log("Failed to send email");
throw new RuntimeException(e);
} finally {
if (responseCode != null) {
response.setStatus(responseCode);
}
if (responseContent != null) {
response.setPayload(responseContent);
}
}
}
}
@@ -56,8 +56,9 @@ public final class GmailClient {
private final InternetAddress outgoingEmailAddressWithUsername;
private final InternetAddress replyToEmailAddress;
// TODO(b/510340944): make package private after feature is rolled out
@Inject
GmailClient(
public GmailClient(
Lazy<Gmail> gmail,
Retrier retrier,
@Config("isEmailSendingEnabled") boolean isEmailSendingEnabled,
@@ -46,11 +46,8 @@ import google.registry.security.XsrfTokenManager;
import google.registry.util.DiffUtils;
import google.registry.util.RegistryEnvironment;
import jakarta.inject.Inject;
import jakarta.servlet.http.Cookie;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
@@ -143,14 +140,10 @@ public abstract class ConsoleApiAction implements Runnable {
}
private boolean verifyXSRF(User user) {
Optional<Cookie> maybeCookie =
Arrays.stream(consoleApiParams.request().getCookies())
.filter(c -> XsrfTokenManager.X_CSRF_TOKEN.equals(c.getName()))
.findFirst();
if (maybeCookie.isEmpty()
|| !consoleApiParams
.xsrfTokenManager()
.validateToken(user.getEmailAddress(), maybeCookie.get().getValue())) {
String xsrfToken = consoleApiParams.request().getHeader(XsrfTokenManager.X_CSRF_TOKEN);
if (xsrfToken == null
|| xsrfToken.isEmpty()
|| !consoleApiParams.xsrfTokenManager().validateToken(user.getEmailAddress(), xsrfToken)) {
consoleApiParams.response().setStatus(SC_UNAUTHORIZED);
return false;
}
@@ -65,6 +65,7 @@ public class ConsoleUserDataAction extends ConsoleApiAction {
XsrfTokenManager.X_CSRF_TOKEN,
consoleApiParams.xsrfTokenManager().generateToken(user.getEmailAddress()));
xsrfCookie.setSecure(true);
xsrfCookie.setPath("/");
consoleApiParams.response().addCookie(xsrfCookie);
JSONObject json =
@@ -38,14 +38,11 @@ public final class ConsoleApiParamsUtils {
new SendEmailUtils(ImmutableList.of("notification@test.example"), gmailClient);
XsrfTokenManager xsrfTokenManager =
new XsrfTokenManager(new FakeClock(Instant.parse("2020-02-02T01:23:45Z")));
String token =
xsrfTokenManager.generateToken(authResult.user().map(User::getEmailAddress).orElse(""));
when(request.getCookies())
.thenReturn(
new Cookie[] {
new Cookie(
XsrfTokenManager.X_CSRF_TOKEN,
xsrfTokenManager.generateToken(
authResult.user().map(User::getEmailAddress).orElse("")))
});
.thenReturn(new Cookie[] {new Cookie(XsrfTokenManager.X_CSRF_TOKEN, token)});
when(request.getHeader(XsrfTokenManager.X_CSRF_TOKEN)).thenReturn(token);
when(request.getRequestURI()).thenReturn("/console/fake-url");
return ConsoleApiParams.create(
request,
+1
View File
@@ -12,6 +12,7 @@ spec:
metadata:
labels:
service: backend
traffic: backend-all
spec:
serviceAccountName: nomulus
nodeSelector:
+1
View File
@@ -12,6 +12,7 @@ spec:
metadata:
labels:
service: console
traffic: console-all
spec:
serviceAccountName: nomulus
containers:
+1
View File
@@ -12,6 +12,7 @@ spec:
metadata:
labels:
service: frontend
traffic: frontend-all
spec:
serviceAccountName: nomulus
nodeSelector:
+1
View File
@@ -12,6 +12,7 @@ spec:
metadata:
labels:
service: pubapi
traffic: pubapi-all
spec:
serviceAccountName: nomulus
containers:
+93
View File
@@ -0,0 +1,93 @@
# This Cloud Build job prepares and applies Google Cloud Deploy configurations.
# It merges the internal repository and populates stableDeploymentAlertPolicyChecks
# in delivery-pipeline.yaml based on environment-specific configuration files.
#
# To manually trigger a build on GCB, run:
# gcloud builds submit --config release/cloudbuild-clouddeploy.yaml --substitutions \
# _INTERNAL_REPO_URL=[URL],PROJECT_ID=[PROJECT_ID] ..
steps:
# Check the out internal repo.
- name: 'gcr.io/cloud-builders/git'
entrypoint: /bin/bash
args:
- -c
- |
set -e
git clone https://gerrit.googlesource.com/gcompute-tools
sed -i s@/usr/bin/python@/usr/bin/python3@g ./gcompute-tools/git-cookie-authdaemon
./gcompute-tools/git-cookie-authdaemon
git clone ${_INTERNAL_REPO_URL} nomulus-internal
# Merge the repos.
- name: 'gcr.io/cloud-builders/git'
entrypoint: /bin/bash
args:
- -c
- |
set -e
shopt -s dotglob
rm -rf .git && rm -rf nomulus-internal/.git
cp -rf nomulus-internal/* .
rm -rf nomulus-internal
# Populate stableDeploymentAlertPolicyChecks in delivery-pipeline.yaml and variables in targets
- name: 'gcr.io/cloud-builders/gcloud'
entrypoint: /bin/bash
args:
- -c
- |
set -e
for env in crash; do
config_file="release/clouddeploy/${env}-config.yaml"
if [ -f "$config_file" ]; then
echo "Extracting checks from $config_file..."
# Extract only the indented block under stableDeploymentAlertPolicyChecks.
awk '
/^stableDeploymentAlertPolicyChecks:/ { capture = 1; next }
capture {
if ($0 ~ /^[^[:space:]]/ && $0 != "") { capture = 0; exit }
print " " $0
}
' "$config_file" > checks.tmp
# Insert the checks where the placeholder is located and remove the placeholder
sed -i '/stableDeploymentAlertPolicyChecks/r checks.tmp' release/clouddeploy/delivery-pipeline.yaml
sed -i '/stableDeploymentAlertPolicyChecks/d' release/clouddeploy/delivery-pipeline.yaml
rm -f checks.tmp
# Populate variables in target file
target_file="release/clouddeploy/${env}-target.yaml"
if [ -f "$target_file" ]; then
echo "Populating variables in $target_file..."
artifact_storage=$(sed -n 's/^artifactStorage: //p' "$config_file")
service_account=$(sed -n 's/^serviceAccount: //p' "$config_file")
cluster_val=$(sed -n 's/^cluster: //p' "$config_file")
sed -i "s|artifactStorage: artifactStorage|artifactStorage: $artifact_storage|" "$target_file"
sed -i "s|serviceAccount: serviceAccount|serviceAccount: $service_account|" "$target_file"
sed -i "s|cluster: cluster|cluster: $cluster_val|" "$target_file"
fi
fi
done
# Apply Cloud Deploy configuration
- name: 'gcr.io/cloud-builders/gcloud'
entrypoint: /bin/bash
args:
- -c
- |
set -e
for env in crash; do
target_file="release/clouddeploy/${env}-target.yaml"
if [ -f "$target_file" ]; then
echo "Applying target $target_file..."
gcloud deploy apply --file="$target_file" --region=us-central1 --project=${PROJECT_ID}
fi
done
echo 'Applying delivery-pipeline.yaml...'
gcloud deploy apply --file=release/clouddeploy/delivery-pipeline.yaml --region=us-central1 --project=${PROJECT_ID}
timeout: 3600s
options:
machineType: 'E2_HIGHCPU_32'
+20 -2
View File
@@ -10,12 +10,30 @@ Defines the `DeliveryPipeline` resource named `deploy-nomulus`. It sets up the s
### Target Configurations (e.g., `crash-target.yaml`)
Files matching this format define the `Target` resources for Cloud Deploy. They specify the GKE cluster and other environment-specific settings for deployment.
### Environment Configurations (e.g., `crash-config.yaml`)
Configuration files containing environment-specific parameters and SLA-based alert policy checks (such as EPP and RDAP success metrics) used for automated analysis and target population.
### `skaffold.yaml`
Defines the Skaffold configuration used by Cloud Deploy to render and deploy the application manifests.
## Usage
## Automated Configuration and Deployment Process
You can apply or modify these configurations in Google Cloud by using the `gcloud` CLI. For example:
The preparation and application of Cloud Deploy configurations is automated via Cloud Build using `release/cloudbuild-clouddeploy.yaml`.
When executed, the Cloud Build job performs the following workflow:
1. **Repository Merge**: Clones the internal repository (`nomulus-internal`) and merges internal configurations into the workspace.
2. **Dynamic Configuration Population**: Reads variables and alert policy checks specified in the configuration file for the environment from the internal repository, populating them into `delivery-pipeline.yaml` and the corresponding target files.
3. **Apply Configurations**: Runs `gcloud deploy apply` to register the updated targets and delivery pipeline in Google Cloud Deploy.
### Manual Execution on Cloud Build
To manually trigger this configuration pipeline on Google Cloud Build, run:
```bash
gcloud builds submit --config release/cloudbuild-clouddeploy.yaml --substitutions _INTERNAL_REPO_URL=[URL],PROJECT_ID=[PROJECT_ID]
```
## Manual Local Usage
You can also apply or modify rendered configurations directly using the `gcloud` CLI:
```bash
gcloud deploy apply --file=<config-file>.yaml --project=<project-id> --region=<region>
+3 -3
View File
@@ -13,9 +13,9 @@ executionConfigs:
executionTimeout: 3600s
defaultPool:
# Placeholder: Replace with artifact bucket name.
artifactStorage: gs://_artifact_bucket_
artifactStorage: artifactStorage
# Placeholder: Replace with project number.
serviceAccount: _project_number_-compute@developer.gserviceaccount.com
serviceAccount: serviceAccount
gke:
# Placeholder: Replace with project ID, location, and cluster name.
cluster: projects/_project_id_/locations/_location_/clusters/_cluster_name_
cluster: cluster
@@ -9,3 +9,11 @@ serialPipeline:
- targetId: crash
profiles:
- crash
strategy:
standard:
analysis:
# 10 minutes.
duration: 600s
googleCloud:
alertPolicyChecks:
stableDeploymentAlertPolicyChecks