Files
truf-server/tests/test_gcp_keycheck_security.py
2026-09-30 20:30:56 +03:00

144 lines
6.8 KiB
Python

import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
APP_DIR = ROOT / 'app'
sys.path.insert(0, str(APP_DIR))
from keycheckers.gcp import gcpKeycheck
import keycheck_runner
from runtime_security import ensure_private_directory, harden_private_file
class GCPTokenEndpointTests(unittest.TestCase):
def test_adc_rejects_local_and_untrusted_token_uri_without_request(self):
base = {
'client_id': 'client-id',
'client_secret': 'client-secret',
'refresh_token': 'refresh-token',
}
for token_uri in (
'http://127.0.0.1:8080/token',
'https://evil.example/token',
'https://oauth2.googleapis.com/token?next=https://evil.example',
):
with self.subTest(token_uri=token_uri), mock.patch.object(gcpKeycheck.requests, 'post') as post:
result = gcpKeycheck.check_adc({**base, 'token_uri': token_uri}, None, 5)
self.assertEqual(result['status'], 'UNKNOWN')
self.assertIn('untrusted', result['message'])
post.assert_not_called()
def test_service_account_rejects_untrusted_uri_before_signing_or_request(self):
credentials = {
'client_email': 'fixture@example.invalid',
'private_key_id': 'fixture',
'private_key': 'not-read-for-an-untrusted-uri',
'token_uri': 'https://evil.example/token',
}
with mock.patch.object(gcpKeycheck, 'rsa_pkcs1v15_sha256_sign') as sign, \
mock.patch.object(gcpKeycheck.requests, 'post') as post:
result = gcpKeycheck.check_service_account(credentials, None, 5)
self.assertEqual(result['status'], 'UNKNOWN')
sign.assert_not_called()
post.assert_not_called()
def test_trusted_endpoint_is_posted_without_redirect_following(self):
response = mock.Mock(status_code=400)
response.text = 'invalid_grant'
response.json.return_value = {'error': 'invalid_grant'}
credentials = {
'client_id': 'client-id',
'client_secret': 'client-secret',
'refresh_token': 'refresh-token',
'token_uri': gcpKeycheck.GOOGLE_TOKEN_URL,
}
with mock.patch.object(gcpKeycheck.requests, 'post', return_value=response) as post:
result = gcpKeycheck.check_adc(credentials, None, 5)
self.assertEqual(result['status'], 'DEAD')
self.assertEqual(post.call_args.args[0], gcpKeycheck.GOOGLE_TOKEN_URL)
self.assertFalse(post.call_args.kwargs['allow_redirects'])
def test_redirect_is_not_followed_or_forwarded(self):
response = mock.Mock(status_code=302, headers={'Location': 'https://evil.example/token'})
credentials = {
'client_id': 'client-id',
'client_secret': 'client-secret',
'refresh_token': 'refresh-token',
}
with mock.patch.object(gcpKeycheck.requests, 'post', return_value=response) as post:
result = gcpKeycheck.check_adc(credentials, None, 5)
self.assertEqual(result['status'], 'UNKNOWN')
self.assertIn('redirect refused', result['message'])
self.assertEqual(post.call_count, 1)
self.assertFalse(post.call_args.kwargs['allow_redirects'])
class GCPVertexEndpointTests(unittest.TestCase):
def test_vertex_api_host_supports_global_multi_region_and_regional_locations(self):
self.assertEqual(gcpKeycheck.vertex_api_host('global'), 'aiplatform.googleapis.com')
self.assertEqual(gcpKeycheck.vertex_api_host('us'), 'aiplatform.us.rep.googleapis.com')
self.assertEqual(gcpKeycheck.vertex_api_host('eu'), 'aiplatform.eu.rep.googleapis.com')
self.assertEqual(gcpKeycheck.vertex_api_host('us-east5'), 'us-east5-aiplatform.googleapis.com')
def test_gemini_probe_uses_global_and_multi_region_hosts(self):
response = mock.Mock(status_code=200)
response.json.return_value = {'totalTokens': 1}
with mock.patch.object(gcpKeycheck.requests, 'post', return_value=response) as post:
result = gcpKeycheck.probe_vertex_llm(
'token', 'project', None, 6,
locations=['global', 'us', 'eu'], models=['gemini-3.6-flash'], max_attempts=3,
)
self.assertTrue(result['enabled'])
self.assertEqual([call.args[0] for call in post.call_args_list], [
'https://aiplatform.googleapis.com/v1/projects/project/locations/global/publishers/google/models/gemini-3.6-flash:countTokens',
'https://aiplatform.us.rep.googleapis.com/v1/projects/project/locations/us/publishers/google/models/gemini-3.6-flash:countTokens',
'https://aiplatform.eu.rep.googleapis.com/v1/projects/project/locations/eu/publishers/google/models/gemini-3.6-flash:countTokens',
])
def test_anthropic_probe_uses_global_host_and_one_token_generation(self):
response = mock.Mock(status_code=200)
with mock.patch.object(gcpKeycheck.requests, 'post', return_value=response) as post:
result = gcpKeycheck.probe_vertex_anthropic(
'token', 'project', None, 6,
locations=['global'], models=['claude-opus-5'], max_attempts=1,
)
self.assertTrue(result['enabled'])
self.assertEqual(
post.call_args.args[0],
'https://aiplatform.googleapis.com/v1/projects/project/locations/global/publishers/anthropic/models/claude-opus-5:rawPredict',
)
self.assertEqual(post.call_args.kwargs['json']['max_tokens'], 1)
class GCPLegacyVertexImportTests(unittest.TestCase):
def test_collect_deduplicates_valid_credentials_across_vertex_files(self):
with tempfile.TemporaryDirectory() as temp_dir:
keycheck_dir = Path(temp_dir) / 'keychecks'
gcp_dir = keycheck_dir / 'gcp'
ensure_private_directory(str(keycheck_dir))
ensure_private_directory(str(gcp_dir))
credential = json.dumps({
'client_email': 'fixture@example.invalid',
'private_key_id': 'fixture-key-id',
'private_key': '-----BEGIN PRIVATE KEY-----\nfixture\n-----END PRIVATE KEY-----',
}, sort_keys=True, separators=(',', ':'))
for filename in keycheck_runner.LEGACY_GCP_VERTEX_STATUS_FILES:
path = gcp_dir / filename
path.write_text(f'{credential}\tVERTEX\tlegacy\n', encoding='utf-8')
harden_private_file(str(path))
entries, skipped = keycheck_runner.collect_legacy_gcp_vertex_credentials({
'keycheck_dir': str(keycheck_dir),
})
self.assertEqual(skipped, 0)
self.assertEqual(len(entries), 1)
self.assertEqual(next(iter(entries.values())), set(keycheck_runner.LEGACY_GCP_VERTEX_STATUS_FILES))
if __name__ == '__main__':
unittest.main()