Skip to content

Commit 2f0f3e1

Browse files
authored
[5.4.0] Add integration tests for --experimental_credential_helper. (bazelbuild#16880)
Closes bazelbuild#15996. PiperOrigin-RevId: 463851754 Change-Id: I13b851454f0b8e6550b2335caa2d802312931929
1 parent b55f322 commit 2f0f3e1

File tree

3 files changed

+128
-6
lines changed

3 files changed

+128
-6
lines changed

src/test/shell/bazel/remote/remote_execution_test.sh

+64
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,70 @@ else
7171
declare -r EXE_EXT=""
7272
fi
7373

74+
function setup_credential_helper() {
75+
cat > "${TEST_TMPDIR}/credhelper" <<'EOF'
76+
#!/usr/bin/env python3
77+
print("""{"headers":{"Authorization":["Bearer secret_token"]}}""")
78+
EOF
79+
chmod +x "${TEST_TMPDIR}/credhelper"
80+
}
81+
82+
function test_credential_helper_remote_cache() {
83+
setup_credential_helper
84+
85+
mkdir -p a
86+
87+
cat > a/BUILD <<'EOF'
88+
genrule(
89+
name = "a",
90+
outs = ["a.txt"],
91+
cmd = "touch $(OUTS)",
92+
)
93+
EOF
94+
95+
stop_worker
96+
start_worker --expected_authorization_token=secret_token
97+
98+
bazel build \
99+
--remote_cache=grpc://localhost:${worker_port} \
100+
//a:a >& $TEST_log && fail "Build without credentials should have failed"
101+
expect_log "Failed to query remote execution capabilities"
102+
103+
bazel build \
104+
--remote_cache=grpc://localhost:${worker_port} \
105+
--experimental_credential_helper="${TEST_TMPDIR}/credhelper" \
106+
//a:a >& $TEST_log || fail "Build with credentials should have succeeded"
107+
}
108+
109+
function test_credential_helper_remote_execution() {
110+
setup_credential_helper
111+
112+
mkdir -p a
113+
114+
cat > a/BUILD <<'EOF'
115+
genrule(
116+
name = "a",
117+
outs = ["a.txt"],
118+
cmd = "touch $(OUTS)",
119+
)
120+
EOF
121+
122+
stop_worker
123+
start_worker --expected_authorization_token=secret_token
124+
125+
bazel build \
126+
--spawn_strategy=remote \
127+
--remote_executor=grpc://localhost:${worker_port} \
128+
//a:a >& $TEST_log && fail "Build without credentials should have failed"
129+
expect_log "Failed to query remote execution capabilities"
130+
131+
bazel build \
132+
--spawn_strategy=remote \
133+
--remote_executor=grpc://localhost:${worker_port} \
134+
--experimental_credential_helper="${TEST_TMPDIR}/credhelper" \
135+
//a:a >& $TEST_log || fail "Build with credentials should have succeeded"
136+
}
137+
74138
function test_remote_grpc_cache_with_protocol() {
75139
# Test that if 'grpc' is provided as a scheme for --remote_cache flag, remote cache works.
76140
mkdir -p a

src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorker.java

+54-6
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,16 @@
4949
import com.google.devtools.build.remote.worker.http.HttpCacheServerInitializer;
5050
import com.google.devtools.common.options.OptionsParser;
5151
import com.google.devtools.common.options.OptionsParsingException;
52+
import io.grpc.Context;
53+
import io.grpc.Contexts;
54+
import io.grpc.Metadata;
5255
import io.grpc.Server;
56+
import io.grpc.ServerCall;
57+
import io.grpc.ServerCall.Listener;
58+
import io.grpc.ServerCallHandler;
5359
import io.grpc.ServerInterceptor;
5460
import io.grpc.ServerInterceptors;
61+
import io.grpc.Status;
5562
import io.grpc.netty.GrpcSslContexts;
5663
import io.grpc.netty.NettyServerBuilder;
5764
import io.netty.bootstrap.ServerBootstrap;
@@ -71,6 +78,9 @@
7178
import java.io.OutputStreamWriter;
7279
import java.io.Writer;
7380
import java.nio.charset.StandardCharsets;
81+
import java.util.ArrayList;
82+
import java.util.List;
83+
import java.util.Optional;
7484
import java.util.concurrent.ConcurrentHashMap;
7585
import java.util.concurrent.Executors;
7686
import java.util.logging.Level;
@@ -107,6 +117,39 @@ static FileSystem getFileSystem() {
107117
return new JavaIoFileSystem(hashFunction);
108118
}
109119

120+
/** A {@link ServerInterceptor} that rejects requests unless an authorization token is present. */
121+
private static class AuthorizationTokenInterceptor implements ServerInterceptor {
122+
private static final Metadata.Key<String> AUTHORIZATION_HEADER_KEY =
123+
Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
124+
125+
private static final String BEARER_PREFIX = "Bearer ";
126+
127+
private final String expectedToken;
128+
129+
AuthorizationTokenInterceptor(String expectedToken) {
130+
this.expectedToken = expectedToken;
131+
}
132+
133+
private Optional<String> getTokenFromMetadata(Metadata headers) {
134+
String val = headers.get(AUTHORIZATION_HEADER_KEY);
135+
if (val != null && val.startsWith(BEARER_PREFIX)) {
136+
return Optional.of(val.substring(BEARER_PREFIX.length()));
137+
}
138+
return Optional.empty();
139+
}
140+
141+
@Override
142+
public <ReqT, RespT> Listener<ReqT> interceptCall(
143+
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
144+
Optional<String> actualToken = getTokenFromMetadata(headers);
145+
if (!expectedToken.equals(actualToken.get())) {
146+
call.close(Status.PERMISSION_DENIED, new Metadata());
147+
return new ServerCall.Listener<ReqT>() {};
148+
}
149+
return Contexts.interceptCall(Context.current(), call, headers, next);
150+
}
151+
}
152+
110153
public RemoteWorker(
111154
FileSystem fs,
112155
RemoteWorkerOptions workerOptions,
@@ -149,20 +192,25 @@ public RemoteWorker(
149192
}
150193

151194
public Server startServer() throws IOException {
152-
ServerInterceptor headersInterceptor = new TracingMetadataUtils.ServerHeadersInterceptor();
195+
List<ServerInterceptor> interceptors = new ArrayList<>();
196+
interceptors.add(new TracingMetadataUtils.ServerHeadersInterceptor());
197+
if (workerOptions.expectedAuthorizationToken != null) {
198+
interceptors.add(new AuthorizationTokenInterceptor(workerOptions.expectedAuthorizationToken));
199+
}
200+
153201
NettyServerBuilder b =
154202
NettyServerBuilder.forPort(workerOptions.listenPort)
155-
.addService(ServerInterceptors.intercept(actionCacheServer, headersInterceptor))
156-
.addService(ServerInterceptors.intercept(bsServer, headersInterceptor))
157-
.addService(ServerInterceptors.intercept(casServer, headersInterceptor))
158-
.addService(ServerInterceptors.intercept(capabilitiesServer, headersInterceptor));
203+
.addService(ServerInterceptors.intercept(actionCacheServer, interceptors))
204+
.addService(ServerInterceptors.intercept(bsServer, interceptors))
205+
.addService(ServerInterceptors.intercept(casServer, interceptors))
206+
.addService(ServerInterceptors.intercept(capabilitiesServer, interceptors));
159207

160208
if (workerOptions.tlsCertificate != null) {
161209
b.sslContext(getSslContextBuilder(workerOptions).build());
162210
}
163211

164212
if (execServer != null) {
165-
b.addService(ServerInterceptors.intercept(execServer, headersInterceptor));
213+
b.addService(ServerInterceptors.intercept(execServer, interceptors));
166214
} else {
167215
logger.atInfo().log("Execution disabled, only serving cache requests");
168216
}

src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorkerOptions.java

+10
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,16 @@ public class RemoteWorkerOptions extends OptionsBase {
176176
+ "requires client authentication (aka mTLS).")
177177
public String tlsCaCertificate;
178178

179+
@Option(
180+
name = "expected_authorization_token",
181+
defaultValue = "null",
182+
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
183+
effectTags = {OptionEffectTag.UNKNOWN},
184+
help =
185+
"The authorization token expected to be present in every request. This is useful for"
186+
+ " testing only.")
187+
public String expectedAuthorizationToken;
188+
179189
private static final int MAX_JOBS = 16384;
180190

181191
/**

0 commit comments

Comments
 (0)