Skip to content

Fix: Only add auth token when registry url matches pathname url (fixes #3907)#3987

Merged
BYK merged 3 commits intoyarnpkg:masterfrom
soda0289:fix-registry-auth-token
Jul 26, 2017
Merged

Fix: Only add auth token when registry url matches pathname url (fixes #3907)#3987
BYK merged 3 commits intoyarnpkg:masterfrom
soda0289:fix-registry-auth-token

Conversation

@soda0289
Copy link
Copy Markdown
Contributor

@soda0289 soda0289 commented Jul 21, 2017

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-auth option enabled.

If we install additional packages from github and save them to the yarn.lock file 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.

@soda0289 soda0289 force-pushed the fix-registry-auth-token branch from a38a003 to ab1d919 Compare July 21, 2017 20:48
@soda0289 soda0289 changed the title Fix: Only add auth token when registry url matches pathname url (fixes 3907) Fix: Only add auth token when registry url matches pathname url (fixes #3907) Jul 21, 2017
@BYK BYK requested a review from arcanis July 23, 2017 13:53
Copy link
Copy Markdown
Member

@BYK BYK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thank you!

I've made some comments and it'd be great if you can address them. But more importantly, I think we are missing a crucial test case: "to non-registry with always-auth: true", right?

Comment thread src/registries/npm-registry.js Outdated
}

getRequestUrl(registry: string, pathname: string): string {
const isPathnameUrl = pathname.match(/^https?:/);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.test is not part of the string prototype only the Regex prototype.

Comment thread src/registries/npm-registry.js Outdated
const requestUrl = this.getRequestUrl(registry, pathname);

const alwaysAuth = this.getRegistryOrGlobalOption(registry, 'always-auth');
const customHostSuffix = this.getRegistryOrGlobalOption(registry, 'custom-host-suffix');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be this should be part of the isRequestToRegistry function itself? This looks like a leaky abstraction to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`.

Comment thread __tests__/registries/npm-registry.js Outdated
const {mockRequestManager, mockRegistries, mockReporter} = createMocks();
const npmRegistry = new NpmRegistry(testCwd, mockRegistries, mockRequestManager, mockReporter);

const pathname = 'http://github.com/yarnpkg/yarn.tgz';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a URL not a path name. Variable names are important ;)

Comment thread __tests__/registries/npm-registry.js Outdated

const requestParams = mockRequestManager.request.mock.calls[0][0];

expect(requestParams.url).toBe(pathname);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably use toHaveBeenLastCalledWith instead.

Copy link
Copy Markdown
Contributor Author

@soda0289 soda0289 Jul 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread __tests__/registries/npm-registry.js Outdated

const requestParams = mockRequestManager.request.mock.calls[0][0];

expect(requestParams.headers.authorization).toBe(undefined);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: use toHaveBeenCalledWith or toHaveBeenLastCalledWith

Comment thread __tests__/registries/npm-registry.js Outdated
};
}

test('should call requestManager.request with pathname url', () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just url nor pathname url

Comment thread __tests__/registries/npm-registry.js Outdated
const {mockRequestManager, mockRegistries, mockReporter} = createMocks();
const npmRegistry = new NpmRegistry(testCwd, mockRegistries, mockRequestManager, mockReporter);

const pathname = 'http://github.com/yarnpkg/yarn.tgz';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use HTTPS URLs, please. Even for tests ;)

Comment thread __tests__/registries/npm-registry.js Outdated

const requestParams = mockRequestManager.request.mock.calls[0][0];

expect(requestParams.headers.authorization).toBe(undefined);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use toBeUndefined instead.

Comment thread __tests__/registries/npm-registry.js Outdated
const pathname = 'https://registry.npmjs.org/yarnpkg/yarn.tgz';

npmRegistry.config = {
'always-auth': false,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the default value anyways, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the default might be undefined do you think I should have a test for that as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@soda0289
Copy link
Copy Markdown
Contributor Author

@BYK Thanks for the review!

I have made the following changes:
- 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

I did not use .toHaveBeenLastCalledWith() since I was unsure how to only check one JSON property in the argument. I made all of the other changes.

@soda0289 soda0289 force-pushed the fix-registry-auth-token branch from 5234a99 to e5c0a49 Compare July 24, 2017 15:55
 - 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
@soda0289 soda0289 force-pushed the fix-registry-auth-token branch from e5c0a49 to 429bbea Compare July 24, 2017 16:40
Copy link
Copy Markdown
Member

@BYK BYK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the additional test case! I think we have some code cleaning up and then we're good to merge!

Comment thread __tests__/registries/npm-registry.js Outdated
const pathname = 'https://registry.npmjs.org/yarnpkg/yarn.tgz';

npmRegistry.config = {
'always-auth': false,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/registries/npm-registry.js Outdated
return port;
}

isRequestToRegistry(requestUrl: string, registry: string): boolean {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any reason for this or getPortOrDefaultPort methods to be "instance methods". Can you make them static?

Comment thread src/registries/npm-registry.js Outdated
}
}

getPortOrDefaultPort(port: ?string, protocol: ?string): ?string {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}

Comment thread src/registries/npm-registry.js Outdated
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)))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we just do (customHostSuffix && requestHost.endsWith(customHostSuffix)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread __tests__/registries/npm-registry.js Outdated
),
).toBe(true);

expect(npmRegistry.isRequestToRegistry('https://foo.bar:443/foo/bar/baz', 'https://foo.bar/foo/')).toBe(true);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may also wanna look at this library which does all this logic for you: https://yarnpkg.com/en/package/normalize-url

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the normalize-url library cleans up the getPort() logic

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the normalize-url library cleans up the getPort() logic

Did you push these changes? I don't see them yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to use arrays instead of Objects since some tests had duplicate keys.

@soda0289
Copy link
Copy Markdown
Contributor Author

@BYK

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
@soda0289 soda0289 force-pushed the fix-registry-auth-token branch from 3c71e7a to d4307df Compare July 25, 2017 16:24
Copy link
Copy Markdown
Member

@BYK BYK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks a lot!

}

getRequestUrl(registry: string, pathname: string): string {
const isUrl = /^https?:/.test(pathname);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: technically git://xxx.com/yyy is also a URL so you'd want to name this as isHTTPUrl or something.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will break protocol relative URLs, which are generated by default in .npmrc

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. Issue filed somewhere?

Copy link
Copy Markdown
Member

@BYK BYK Sep 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isRequestToRegistry(requestUrl: string, registryUrl: string): boolean {
const normalizedRequestUrl = normalizeUrl(requestUrl);
const normalizedRegistryUrl = normalizeUrl(registryUrl);
const requestParsed = url.parse(normalizedRequestUrl);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expect(requestParams.url).toBe(url);
});

test('should not add authorization header if pathname not to registry', () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/pathname/request

@BYK BYK merged commit 050815d into yarnpkg:master Jul 26, 2017
BYK added a commit that referenced this pull request Sep 8, 2017
**Summary**

Fixes matching protocol-relative registry URLs from config.
Reported here: #3987 (comment)

**Test plan**

Added one new test.
BYK added a commit that referenced this pull request Sep 8, 2017
**Summary**

Fixes matching protocol-relative registry URLs from config.
Reported here: #3987 (comment)

**Test plan**

Added one new test.
joaolucasl pushed a commit to joaolucasl/yarn that referenced this pull request Oct 27, 2017
**Summary**

Fixes matching protocol-relative registry URLs from config.
Reported here: yarnpkg#3987 (comment)

**Test plan**

Added one new test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants