Fix: Only add auth token when registry url matches pathname url (fixes #3907)#3987
Fix: Only add auth token when registry url matches pathname url (fixes #3907)#3987BYK merged 3 commits intoyarnpkg:masterfrom
Conversation
a38a003 to
ab1d919
Compare
| } | ||
|
|
||
| getRequestUrl(registry: string, pathname: string): string { | ||
| const isPathnameUrl = pathname.match(/^https?:/); |
There was a problem hiding this comment.
You probably want /^https?:/.test(pathname) here which is faster and returns a boolean instead of a full match object.
Also, what is a "path name URL"? Can't we just say isHTTP or something for clarity?
There was a problem hiding this comment.
The .match string method is used throughtout this class and I'm guessing those should be repalced as well. I can fix this in another PR.
I will rename the variable to isUrl. I think pathname can also be a path, /foo/bar/, as well as a URL.
There was a problem hiding this comment.
.test is not part of the string prototype only the Regex prototype.
| const requestUrl = this.getRequestUrl(registry, pathname); | ||
|
|
||
| const alwaysAuth = this.getRegistryOrGlobalOption(registry, 'always-auth'); | ||
| const customHostSuffix = this.getRegistryOrGlobalOption(registry, 'custom-host-suffix'); |
There was a problem hiding this comment.
May be this should be part of the isRequestToRegistry function itself? This looks like a leaky abstraction to me.
There was a problem hiding this comment.
I will have to move the isRequestToRegistry inside the NPMRegistry class since it will needs access to the config to read the 'custom-host-suffix`.
| const {mockRequestManager, mockRegistries, mockReporter} = createMocks(); | ||
| const npmRegistry = new NpmRegistry(testCwd, mockRegistries, mockRequestManager, mockReporter); | ||
|
|
||
| const pathname = 'http://github.com/yarnpkg/yarn.tgz'; |
There was a problem hiding this comment.
This is a URL not a path name. Variable names are important ;)
|
|
||
| const requestParams = mockRequestManager.request.mock.calls[0][0]; | ||
|
|
||
| expect(requestParams.url).toBe(pathname); |
There was a problem hiding this comment.
You should probably use toHaveBeenLastCalledWith instead.
There was a problem hiding this comment.
Is there a way to use .toHaveBeenLastCalledWith and only match one property of a JSON object? Sinon has matchers that can be used but I don't see anything similar in the documentation of Jest.
|
|
||
| const requestParams = mockRequestManager.request.mock.calls[0][0]; | ||
|
|
||
| expect(requestParams.headers.authorization).toBe(undefined); |
There was a problem hiding this comment.
Same as above: use toHaveBeenCalledWith or toHaveBeenLastCalledWith
| }; | ||
| } | ||
|
|
||
| test('should call requestManager.request with pathname url', () => { |
| const {mockRequestManager, mockRegistries, mockReporter} = createMocks(); | ||
| const npmRegistry = new NpmRegistry(testCwd, mockRegistries, mockRequestManager, mockReporter); | ||
|
|
||
| const pathname = 'http://github.com/yarnpkg/yarn.tgz'; |
There was a problem hiding this comment.
Use HTTPS URLs, please. Even for tests ;)
|
|
||
| const requestParams = mockRequestManager.request.mock.calls[0][0]; | ||
|
|
||
| expect(requestParams.headers.authorization).toBe(undefined); |
| const pathname = 'https://registry.npmjs.org/yarnpkg/yarn.tgz'; | ||
|
|
||
| npmRegistry.config = { | ||
| 'always-auth': false, |
There was a problem hiding this comment.
I think this is the default value anyways, right?
There was a problem hiding this comment.
I think the default might be undefined do you think I should have a test for that as well.
There was a problem hiding this comment.
If you feel like it I wouldn't say "no" to more tests. That said if the default is false or "falsy" I think just omitting it from the config and adding a comment saying // The default is always-auth: false would be enough.
|
@BYK Thanks for the review! I have made the following changes: I did not use |
5234a99 to
e5c0a49
Compare
- Add test for non-registry and always-auth true - Rename variables - Use Regex.text instead of String.match - Move isRequesteRegistry into NPMRegistry class - Move customHostSuffix code inside isRequestToRegistry - Use .toBeUndefined
e5c0a49 to
429bbea
Compare
BYK
left a comment
There was a problem hiding this comment.
Thanks for the additional test case! I think we have some code cleaning up and then we're good to merge!
| const pathname = 'https://registry.npmjs.org/yarnpkg/yarn.tgz'; | ||
|
|
||
| npmRegistry.config = { | ||
| 'always-auth': false, |
There was a problem hiding this comment.
If you feel like it I wouldn't say "no" to more tests. That said if the default is false or "falsy" I think just omitting it from the config and adding a comment saying // The default is always-auth: false would be enough.
| return port; | ||
| } | ||
|
|
||
| isRequestToRegistry(requestUrl: string, registry: string): boolean { |
There was a problem hiding this comment.
I don't see any reason for this or getPortOrDefaultPort methods to be "instance methods". Can you make them static?
| } | ||
| } | ||
|
|
||
| getPortOrDefaultPort(port: ?string, protocol: ?string): ?string { |
There was a problem hiding this comment.
Just say getPort? Also you can simplify your function as follows:
const DEFAULT_PORTS = Object.freeze({
'http:': '80',
'https:': '443',
});
/**
* Remove the port if it is the default for the protocol, fallback to emptry string if no port provided.
*/
static getPort(port: ?string, protocol: ?string): string {
return !port || port === DEFAULT_PORTS[protocol] ? '' : port;
}
| requestPort === registryPort && | ||
| (requestPath.startsWith(registryPath) || | ||
| // For some registries, the package path does not prefix with the registry path | ||
| (typeof customHostSuffix === 'string' && customHostSuffix.length > 0 && requestHost.endsWith(customHostSuffix))) |
There was a problem hiding this comment.
Why can't we just do (customHostSuffix && requestHost.endsWith(customHostSuffix)?
There was a problem hiding this comment.
I originally did have it like that but flow check was failing. I think the return value of getRegistryOrGlobalOption() is mixed and therefore would need to assert it is a string before performing string operations.
I can remove the .length check since that should not be necessary.
There was a problem hiding this comment.
Oh, makes sense. Well, may be you can do
const customHostSuffix = String(this.getRegistryOrGlobalOption(registry, 'custom-host-suffix') || '');
So it is always a string? The only downside is if someone someone sets this to null or something, it will be converted into the string 'null'. Not too concerned about that since AFAIK typeof checks are expensive.
| ), | ||
| ).toBe(true); | ||
|
|
||
| expect(npmRegistry.isRequestToRegistry('https://foo.bar:443/foo/bar/baz', 'https://foo.bar/foo/')).toBe(true); |
There was a problem hiding this comment.
How about this:
const validRegistryUrls = Object.feeze({
'https://foo.bar:443/foo/bar/baz': 'https://foo.bar/foo/',
'https://foo.bar/foo/bar/baz': 'https://foo.bar:443/foo/',
....
});
const invalidRegistryUrls = Object.freeze({
'https://wrong.thing/foo/bar/baz': 'https://foo.bar/foo/',
...
});
Object.keys(validRegistryUrls).forEach(url => expect(npmRegistry.isRequestToRegistry(url, validRegistryUrls[url]).toBeTruthy());
Object.keys(invalidRegistryUrls).forEach(url => expect(npmRegistry.isRequestToRegistry(url, validRegistryUrls[url]).toBeFalsy());
There was a problem hiding this comment.
You may also wanna look at this library which does all this logic for you: https://yarnpkg.com/en/package/normalize-url
There was a problem hiding this comment.
I used the normalize-url library cleans up the getPort() logic
There was a problem hiding this comment.
I used the normalize-url library cleans up the getPort() logic
Did you push these changes? I don't see them yet.
There was a problem hiding this comment.
I had to use arrays instead of Objects since some tests had duplicate keys.
|
I was able to remove the getPort() function and use the library normalize-url to handle that logic. Also normalizes unicode urls which might not have been checked before. I cleaned up the tests by creating an array of the tests cases. Let me know if anything else can be improved. |
- Commented out default value for always-auth - Added library to normalize urls - Removed defaultPort code and use library instead - Removed length check on customHostSuffix
3c71e7a to
d4307df
Compare
| } | ||
|
|
||
| getRequestUrl(registry: string, pathname: string): string { | ||
| const isUrl = /^https?:/.test(pathname); |
There was a problem hiding this comment.
Nitpick: technically git://xxx.com/yyy is also a URL so you'd want to name this as isHTTPUrl or something.
There was a problem hiding this comment.
This will break protocol relative URLs, which are generated by default in .npmrc
| isRequestToRegistry(requestUrl: string, registryUrl: string): boolean { | ||
| const normalizedRequestUrl = normalizeUrl(requestUrl); | ||
| const normalizedRegistryUrl = normalizeUrl(registryUrl); | ||
| const requestParsed = url.parse(normalizedRequestUrl); |
There was a problem hiding this comment.
Simplification:
const {requestHost, requestPath} = url.parse(normalizeUrl(requestUrl));
const {registryHost, registryPath} = url.parse(normalizeUrl(registryUrl));
Also, registryUrl should not change frequently so we may consider memoizing parsing that in a future patch.
|
|
||
| npmRegistry.request(url); | ||
|
|
||
| const requestParams = mockRequestManager.request.mock.calls[0][0]; |
| expect(requestParams.url).toBe(url); | ||
| }); | ||
|
|
||
| test('should not add authorization header if pathname not to registry', () => { |
**Summary** Fixes matching protocol-relative registry URLs from config. Reported here: #3987 (comment) **Test plan** Added one new test.
**Summary** Fixes matching protocol-relative registry URLs from config. Reported here: #3987 (comment) **Test plan** Added one new test.
**Summary** Fixes matching protocol-relative registry URLs from config. Reported here: yarnpkg#3987 (comment) **Test plan** Added one new test.
Summary
Fixes issue: #3907
In yarn version 0.27.4 a patch (5ff6922) introduced a change that caused an authorization header to be added to requests that were not being sent to the registry.
This commit brings back some older logic to ensure we only add the authorization header when we are sending requests to the registry.
Test plan
To test this change you need a private repository using a repository server such, as Verdaccio configured, to only allow authenticated calls to be made for both download and publish commands. This configuration requires that yarn has the
always-authoption enabled.If we install additional packages from github and save them to the
yarn.lockfile it should not send the authorization header for these requests.I started to add some unit tests. This is my first time writing mocks with Jest and any suggestions are welcome. I can add more tests for additional cases as well.