Skip to content

Commit e44c8ed

Browse files
committed
Key Buildkite map on immutable IDs instead of renameable slugs
Switch _extract_repo_buildkite to read organization_id and pipeline_id claims (UUIDs, immutable) rather than organization_slug and pipeline_slug (renameable, can be reclaimed by another org). This prevents identity hijacking via slug rename: a released slug claimed by a different organization would no longer silently match an existing mapping entry. ci_providers.yml format changes from: vllm/ci: vllm-project/vllm to: org-uuid/pipeline-uuid: vllm-project/vllm # vllm/ci
1 parent de21cbc commit e44c8ed

3 files changed

Lines changed: 30 additions & 23 deletions

File tree

aws/lambda/cross_repo_ci_relay/config/ci_providers.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66
# Format: one top-level key per CI provider. Each provider section maps
77
# the provider's pipeline identifier to the GitHub repo it represents.
88
#
9+
# IMPORTANT: Buildkite mappings use immutable organization_id/pipeline_id
10+
# (UUIDs) rather than slugs. Slugs are renameable and a released slug
11+
# can be claimed by a different organization; IDs are permanent.
12+
# Find IDs via: buildkite-agent oidc request-token --audience <aud>
13+
#
914
# The Lambda fetches this file at runtime (via CI_PROVIDERS_URL) and
1015
# caches it in Redis, so changes here take effect without redeployment.
1116

1217
buildkite:
13-
# org_slug/pipeline_slug: owner/repo
14-
# vllm/ci: vllm-project/vllm
15-
# vllm/release: vllm-project/vllm
18+
# organization_id/pipeline_id: owner/repo # slug (for readability)
19+
# 018e4f2a-1b2c-3d4e/018e5a6b-7c8d-9e0f: vllm-project/vllm # vllm/ci
1620

1721
# gitlab:
1822
# group/project: owner/repo

aws/lambda/cross_repo_ci_relay/tests/test_jwt_helper.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ def _fake_github_claims(**overrides):
2525
def _fake_buildkite_claims(**overrides):
2626
base = {
2727
"iss": BUILDKITE_ISSUER,
28+
"organization_id": "org-uuid-123",
29+
"pipeline_id": "pipe-uuid-456",
2830
"organization_slug": "myorg",
2931
"pipeline_slug": "mypipeline",
3032
"build_commit": "abc123",
@@ -135,7 +137,7 @@ def setUp(self):
135137
self.mock_decode = self.patcher_decode.start()
136138

137139
self._orig_map = BUILDKITE_REPO_MAP.copy()
138-
load_ci_provider_mappings({"buildkite": {"myorg/mypipeline": "myorg/myrepo"}})
140+
load_ci_provider_mappings({"buildkite": {"org-uuid-123/pipe-uuid-456": "myorg/myrepo"}})
139141

140142
def tearDown(self):
141143
self.patcher_detect.stop()
@@ -155,21 +157,21 @@ def test_valid_buildkite_token_returns_mapped_repo(self):
155157

156158
def test_unregistered_pipeline_raises_403(self):
157159
self.mock_decode.return_value = _fake_buildkite_claims(
158-
organization_slug="unknown", pipeline_slug="unknown"
160+
organization_id="unknown-org", pipeline_id="unknown-pipe"
159161
)
160162
with self.assertRaises(HTTPException) as ctx:
161163
verify_oidc_token("bk.oidc.token")
162164
self.assertEqual(ctx.exception.status_code, 403)
163165
self.assertIn("not registered", ctx.exception.detail)
164166

165-
def test_missing_org_slug_raises_401(self):
166-
self.mock_decode.return_value = _fake_buildkite_claims(organization_slug="")
167+
def test_missing_org_id_raises_401(self):
168+
self.mock_decode.return_value = _fake_buildkite_claims(organization_id="")
167169
with self.assertRaises(HTTPException) as ctx:
168170
verify_oidc_token("bk.oidc.token")
169171
self.assertEqual(ctx.exception.status_code, 401)
170172

171-
def test_missing_pipeline_slug_raises_401(self):
172-
self.mock_decode.return_value = _fake_buildkite_claims(pipeline_slug="")
173+
def test_missing_pipeline_id_raises_401(self):
174+
self.mock_decode.return_value = _fake_buildkite_claims(pipeline_id="")
173175
with self.assertRaises(HTTPException) as ctx:
174176
verify_oidc_token("bk.oidc.token")
175177
self.assertEqual(ctx.exception.status_code, 401)
@@ -193,10 +195,10 @@ def tearDown(self):
193195
BUILDKITE_REPO_MAP.update(self._orig_map)
194196

195197
def test_loads_valid_buildkite_entries(self):
196-
raw = {"buildkite": {"vllm/ci": "vllm-project/vllm", "acme/build": "acme/repo"}}
198+
raw = {"buildkite": {"org-id-1/pipe-id-1": "vllm-project/vllm", "org-id-2/pipe-id-2": "acme/repo"}}
197199
load_ci_provider_mappings(raw)
198-
self.assertEqual(BUILDKITE_REPO_MAP[("vllm", "ci")], "vllm-project/vllm")
199-
self.assertEqual(BUILDKITE_REPO_MAP[("acme", "build")], "acme/repo")
200+
self.assertEqual(BUILDKITE_REPO_MAP[("org-id-1", "pipe-id-1")], "vllm-project/vllm")
201+
self.assertEqual(BUILDKITE_REPO_MAP[("org-id-2", "pipe-id-2")], "acme/repo")
200202

201203
def test_empty_config_clears_map(self):
202204
BUILDKITE_REPO_MAP[("old", "entry")] = "old/repo"
@@ -209,10 +211,10 @@ def test_missing_buildkite_section_clears_map(self):
209211
self.assertEqual(len(BUILDKITE_REPO_MAP), 0)
210212

211213
def test_skips_invalid_entries(self):
212-
raw = {"buildkite": {"noslash": "vllm-project/vllm", "ok/pipeline": "ok/repo"}}
214+
raw = {"buildkite": {"noslash": "vllm-project/vllm", "ok-id/pipe-id": "ok/repo"}}
213215
load_ci_provider_mappings(raw)
214216
self.assertNotIn(("noslash", ""), BUILDKITE_REPO_MAP)
215-
self.assertEqual(BUILDKITE_REPO_MAP[("ok", "pipeline")], "ok/repo")
217+
self.assertEqual(BUILDKITE_REPO_MAP[("ok-id", "pipe-id")], "ok/repo")
216218

217219

218220
class TestUnsupportedIssuer(unittest.TestCase):

aws/lambda/cross_repo_ci_relay/utils/jwt_helper.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@
3434
issuer: jwt.PyJWKClient(cfg["jwks_uri"]) for issuer, cfg in _ISSUER_CONFIG.items()
3535
}
3636

37-
# Runtime-populated mapping from Buildkite (org_slug, pipeline_slug) to the
38-
# GitHub-style "owner/repo" identity. Loaded from the ci_providers.yml
39-
# config file so that adding a new downstream repo only requires a config
37+
# Runtime-populated mapping from Buildkite (organization_id, pipeline_id) to
38+
# the GitHub-style "owner/repo" identity. Uses immutable IDs rather than
39+
# slugs to prevent identity hijacking via slug rename. Loaded from
40+
# ci_providers.yml so adding a new downstream repo only requires a config
4041
# change — no Lambda redeployment.
4142
BUILDKITE_REPO_MAP: Dict[Tuple[str, str], str] = {}
4243

@@ -129,18 +130,18 @@ def _extract_repo_github(claims: dict) -> str:
129130

130131

131132
def _extract_repo_buildkite(claims: dict) -> str:
132-
org = claims.get("organization_slug", "")
133-
pipeline = claims.get("pipeline_slug", "")
134-
if not org or not pipeline:
133+
org_id = claims.get("organization_id", "")
134+
pipeline_id = claims.get("pipeline_id", "")
135+
if not org_id or not pipeline_id:
135136
raise HTTPException(
136137
401,
137-
"Buildkite OIDC token missing 'organization_slug' or 'pipeline_slug'",
138+
"Buildkite OIDC token missing 'organization_id' or 'pipeline_id'",
138139
)
139-
repo = BUILDKITE_REPO_MAP.get((org, pipeline))
140+
repo = BUILDKITE_REPO_MAP.get((org_id, pipeline_id))
140141
if not repo:
141142
raise HTTPException(
142143
403,
143-
f"Buildkite pipeline {org}/{pipeline} is not registered with CRCR",
144+
f"Buildkite pipeline {org_id}/{pipeline_id} is not registered with CRCR",
144145
)
145146
return repo
146147

0 commit comments

Comments
 (0)