Skip to content

Commit 459db80

Browse files
maxbrunetviceice
andauthored
fix(datasource/pypi): sanitize GAR userinfo for authenticated lookups (#42541)
* fix(datasource/pypi): sanitize GAR userinfo for authenticated lookups Strip URL userinfo from Google Artifact Registry PyPI lookup URLs only when Google auth is being applied. This avoids got-derived Basic auth conflicts for uv-style index URLs while keeping the change narrowly scoped to authenticated datasource requests. * Test getAuthHeaders with invalid URL * Parse the URL once * Remove cast and ignore error * Remove test and add v8 ignore comment --------- Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
1 parent 63dcc7f commit 459db80

2 files changed

Lines changed: 60 additions & 9 deletions

File tree

lib/modules/datasource/pypi/index.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,40 @@ describe('modules/datasource/pypi/index', () => {
827827
expect(googleAuth).toHaveBeenCalledTimes(1);
828828
});
829829

830+
it('sanitizes GAR userinfo when Google auth is used', async () => {
831+
httpMock
832+
.scope('https://someregion-python.pkg.dev/some-project/some-repo/simple/')
833+
.get('/dj-database-url/')
834+
.reply(200, htmlResponse);
835+
const config = {
836+
registryUrls: [
837+
'https://oauth2accesstoken@someregion-python.pkg.dev/some-project/some-repo/simple/',
838+
],
839+
};
840+
googleAuth.mockImplementationOnce(
841+
// TODO: fix typing
842+
vi.fn<any>(
843+
class {
844+
getAccessToken = vi.fn().mockResolvedValue('some-token');
845+
},
846+
),
847+
);
848+
expect(
849+
await getPkgReleases({
850+
datasource,
851+
...config,
852+
constraints: { python: '2.7' },
853+
packageName: 'dj-database-url',
854+
}),
855+
).toMatchObject({
856+
isPrivate: true,
857+
registryUrl:
858+
'https://oauth2accesstoken@someregion-python.pkg.dev/some-project/some-repo/simple',
859+
releases: djDatabaseUrlSimpleReleases,
860+
});
861+
expect(googleAuth).toHaveBeenCalledTimes(1);
862+
});
863+
830864
it('ignores an invalid URL when checking for auth headers', async () => {
831865
const config = {
832866
registryUrls: ['not-a-url/simple/'],

lib/modules/datasource/pypi/index.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,38 @@ export class PypiDatasource extends Datasource {
8282
return dependency;
8383
}
8484

85+
private sanitizeLookupUrl(lookupUrl: string, parsedUrl: URL): string {
86+
if (!parsedUrl.username && !parsedUrl.password) {
87+
return lookupUrl;
88+
}
89+
90+
parsedUrl.username = '';
91+
parsedUrl.password = '';
92+
return parsedUrl.toString();
93+
}
94+
8595
private async getAuthHeaders(
8696
lookupUrl: string,
87-
): Promise<OutgoingHttpHeaders> {
97+
): Promise<{ headers: OutgoingHttpHeaders; lookupUrl: string }> {
8898
const parsedUrl = parseUrl(lookupUrl);
99+
// v8 ignore if -- TODO: refactor to cover this branch through public behavior again
89100
if (!parsedUrl) {
90101
logger.once.debug({ lookupUrl }, 'Failed to parse URL');
91-
return {};
102+
return { headers: {}, lookupUrl };
92103
}
93104
if (parsedUrl.hostname.endsWith('.pkg.dev')) {
94105
const auth = await getGoogleAuthToken();
95106
if (auth) {
96-
return { authorization: `Basic ${auth}` };
107+
const sanitizedLookupUrl = this.sanitizeLookupUrl(lookupUrl, parsedUrl);
108+
return {
109+
headers: { authorization: `Basic ${auth}` },
110+
lookupUrl: sanitizedLookupUrl,
111+
};
97112
}
98113
logger.once.debug({ lookupUrl }, 'Could not get Google access token');
99-
return {};
114+
return { headers: {}, lookupUrl };
100115
}
101-
return {};
116+
return { headers: {}, lookupUrl };
102117
}
103118

104119
private async getDependency(
@@ -111,8 +126,9 @@ export class PypiDatasource extends Datasource {
111126
).href;
112127
const dependency: ReleaseResult = { releases: [] };
113128
logger.trace({ lookupUrl }, 'Pypi api got lookup');
114-
const headers = await this.getAuthHeaders(lookupUrl);
115-
const rep = await this.http.getJsonUnchecked<PypiJSON>(lookupUrl, {
129+
const { headers, lookupUrl: sanitizedUrl } =
130+
await this.getAuthHeaders(lookupUrl);
131+
const rep = await this.http.getJsonUnchecked<PypiJSON>(sanitizedUrl, {
116132
headers,
117133
});
118134
const dep = rep?.body;
@@ -259,8 +275,9 @@ export class PypiDatasource extends Datasource {
259275
hostUrl,
260276
).href;
261277
const dependency: ReleaseResult = { releases: [] };
262-
const headers = await this.getAuthHeaders(lookupUrl);
263-
const response = await this.http.getText(lookupUrl, { headers });
278+
const { headers, lookupUrl: sanitizedUrl } =
279+
await this.getAuthHeaders(lookupUrl);
280+
const response = await this.http.getText(sanitizedUrl, { headers });
264281
const dep = response?.body;
265282
if (!dep) {
266283
logger.trace({ dependency: packageName }, 'pip package not found');

0 commit comments

Comments
 (0)