Skip to content

Commit a767fe7

Browse files
tjgqcopybara-github
authored andcommitted
Test that a credential helper can supply credentials for bzlmod.
Closes #18428. PiperOrigin-RevId: 532793826 Change-Id: I0f63aa7ee341f5181b905c7ba78af60321b90836
1 parent 70e5632 commit a767fe7

File tree

3 files changed

+246
-0
lines changed

3 files changed

+246
-0
lines changed

src/test/py/bazel/BUILD

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,17 @@ py_test(
346346
":test_base",
347347
],
348348
)
349+
350+
py_test(
351+
name = "bzlmod_credentials_test",
352+
size = "large",
353+
srcs = ["bzlmod/bzlmod_credentials_test.py"],
354+
tags = [
355+
"no_windows", # test uses a Python script as a credential helper
356+
"requires-network",
357+
],
358+
deps = [
359+
":bzlmod_test_utils",
360+
":test_base",
361+
],
362+
)
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# pylint: disable=g-backslash-continuation
2+
# Copyright 2023 The Bazel Authors. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Tests using credentials to connect to the bzlmod registry."""
16+
17+
import base64
18+
import os
19+
import tempfile
20+
import unittest
21+
22+
from src.test.py.bazel import test_base
23+
from src.test.py.bazel.bzlmod.test_utils import BazelRegistry
24+
from src.test.py.bazel.bzlmod.test_utils import StaticHTTPServer
25+
26+
27+
class BzlmodCredentialsTest(test_base.TestBase):
28+
"""Test class for using credentials to connect to the bzlmod registry."""
29+
30+
def setUp(self):
31+
test_base.TestBase.setUp(self)
32+
self.registries_work_dir = tempfile.mkdtemp(dir=self._test_cwd)
33+
self.registry_root = os.path.join(self.registries_work_dir, 'main')
34+
self.main_registry = BazelRegistry(self.registry_root)
35+
self.main_registry.createCcModule('aaa', '1.0')
36+
37+
self.ScratchFile(
38+
'.bazelrc',
39+
[
40+
# In ipv6 only network, this has to be enabled.
41+
# 'startup --host_jvm_args=-Djava.net.preferIPv6Addresses=true',
42+
'common --enable_bzlmod',
43+
# Disable yanked version check so we are not affected BCR changes.
44+
'common --allow_yanked_versions=all',
45+
],
46+
)
47+
self.ScratchFile('WORKSPACE')
48+
# The existence of WORKSPACE.bzlmod prevents WORKSPACE prefixes or suffixes
49+
# from being used; this allows us to test built-in modules actually work
50+
self.ScratchFile('WORKSPACE.bzlmod')
51+
self.ScratchFile(
52+
'MODULE.bazel',
53+
[
54+
'bazel_dep(name = "aaa", version = "1.0")',
55+
],
56+
)
57+
self.ScratchFile(
58+
'BUILD',
59+
[
60+
'cc_binary(',
61+
' name = "main",',
62+
' srcs = ["main.cc"],',
63+
' deps = ["@aaa//:lib_aaa"],',
64+
')',
65+
],
66+
)
67+
self.ScratchFile(
68+
'main.cc',
69+
[
70+
'#include "aaa.h"',
71+
'int main() {',
72+
' hello_aaa("main function");',
73+
'}',
74+
],
75+
)
76+
self.ScratchFile(
77+
'credhelper',
78+
[
79+
'#!/usr/bin/env python3',
80+
'import sys',
81+
'if "127.0.0.1" in sys.stdin.read():',
82+
' print("""{"headers":{"Authorization":["Bearer TOKEN"]}}""")',
83+
'else:',
84+
' print("""{}""")',
85+
],
86+
executable=True,
87+
)
88+
self.ScratchFile(
89+
'.netrc',
90+
[
91+
'machine 127.0.0.1',
92+
'login foo',
93+
'password bar',
94+
],
95+
)
96+
97+
def testUnauthenticated(self):
98+
with StaticHTTPServer(self.registry_root) as static_server:
99+
_, stdout, _ = self.RunBazel([
100+
'run',
101+
'--registry=' + static_server.getURL(),
102+
'--registry=https://bcr.bazel.build',
103+
'//:main',
104+
])
105+
self.assertIn('main function => [email protected]', stdout)
106+
107+
def testMissingCredentials(self):
108+
with StaticHTTPServer(
109+
self.registry_root, expected_auth='Bearer TOKEN'
110+
) as static_server:
111+
_, _, stderr = self.RunBazel(
112+
[
113+
'run',
114+
'--registry=' + static_server.getURL(),
115+
'--registry=https://bcr.bazel.build',
116+
'//:main',
117+
],
118+
allow_failure=True,
119+
)
120+
self.assertIn('GET returned 401 Unauthorized', '\n'.join(stderr))
121+
122+
def testCredentialsFromHelper(self):
123+
with StaticHTTPServer(
124+
self.registry_root, expected_auth='Bearer TOKEN'
125+
) as static_server:
126+
_, stdout, _ = self.RunBazel([
127+
'run',
128+
'--experimental_credential_helper=%workspace%/credhelper',
129+
'--registry=' + static_server.getURL(),
130+
'--registry=https://bcr.bazel.build',
131+
'//:main',
132+
])
133+
self.assertIn('main function => [email protected]', stdout)
134+
135+
def testCredentialsFromNetrc(self):
136+
expected_auth = 'Basic ' + base64.b64encode(b'foo:bar').decode('ascii')
137+
138+
with StaticHTTPServer(
139+
self.registry_root, expected_auth=expected_auth
140+
) as static_server:
141+
_, stdout, _ = self.RunBazel(
142+
[
143+
'run',
144+
'--registry=' + static_server.getURL(),
145+
'--registry=https://bcr.bazel.build',
146+
'//:main',
147+
],
148+
env_add={'NETRC': self.Path('.netrc')},
149+
)
150+
self.assertIn('main function => [email protected]', stdout)
151+
152+
def testCredentialsFromHelperOverrideNetrc(self):
153+
with StaticHTTPServer(
154+
self.registry_root, expected_auth='Bearer TOKEN'
155+
) as static_server:
156+
_, stdout, _ = self.RunBazel(
157+
[
158+
'run',
159+
'--experimental_credential_helper=%workspace%/credhelper',
160+
'--registry=' + static_server.getURL(),
161+
'--registry=https://bcr.bazel.build',
162+
'//:main',
163+
],
164+
env_add={'NETRC': self.Path('.netrc')},
165+
)
166+
self.assertIn('main function => [email protected]', stdout)
167+
168+
169+
if __name__ == '__main__':
170+
unittest.main()

src/test/py/bazel/bzlmod/test_utils.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
"""Test utils for Bzlmod."""
1717

1818
import base64
19+
import functools
1920
import hashlib
21+
import http.server
2022
import json
2123
import os
2224
import pathlib
2325
import shutil
26+
import threading
2427
import urllib.request
2528
import zipfile
2629

@@ -319,3 +322,62 @@ def createLocalPathModule(self, name, version, path, deps=None):
319322

320323
with module_dir.joinpath('source.json').open('w') as f:
321324
json.dump(source, f, indent=4, sort_keys=True)
325+
326+
327+
class StaticHTTPServer:
328+
"""An HTTP server serving static files, optionally with authentication."""
329+
330+
def __init__(self, root_directory, expected_auth=None):
331+
self.root_directory = root_directory
332+
self.expected_auth = expected_auth
333+
334+
def __enter__(self):
335+
address = ('localhost', 0) # assign random port
336+
handler = functools.partial(
337+
_Handler, self.root_directory, self.expected_auth
338+
)
339+
self.httpd = http.server.HTTPServer(address, handler)
340+
self.thread = threading.Thread(target=self.httpd.serve_forever, daemon=True)
341+
self.thread.start()
342+
return self
343+
344+
def __exit__(self, exc_type, exc_value, traceback):
345+
self.httpd.shutdown()
346+
self.thread.join()
347+
348+
def getURL(self):
349+
return 'http://{}:{}'.format(*self.httpd.server_address)
350+
351+
352+
class _Handler(http.server.SimpleHTTPRequestHandler):
353+
"""A SimpleHTTPRequestHandler with authentication."""
354+
355+
# Note: until Python 3.6, SimpleHTTPRequestHandler was only able to serve
356+
# files from the working directory. A 'directory' parameter was added in
357+
# Python 3.7, but sadly our CI builds are stuck with Python 3.6. Instead,
358+
# we monkey-patch translate_path() to rewrite the path.
359+
360+
def __init__(self, root_directory, expected_auth, *args, **kwargs):
361+
self.root_directory = root_directory
362+
self.expected_auth = expected_auth
363+
super().__init__(*args, **kwargs)
364+
365+
def translate_path(self, path):
366+
abs_path = super().translate_path(path)
367+
rel_path = os.path.relpath(abs_path, os.getcwd())
368+
return os.path.join(self.root_directory, rel_path)
369+
370+
def check_auth(self):
371+
auth_header = self.headers.get('Authorization', None)
372+
if auth_header != self.expected_auth:
373+
self.send_error(http.HTTPStatus.UNAUTHORIZED)
374+
return False
375+
return True
376+
377+
def do_HEAD(self):
378+
if self.check_auth():
379+
return super().do_HEAD()
380+
381+
def do_GET(self):
382+
if self.check_auth():
383+
return super().do_GET()

0 commit comments

Comments
 (0)