Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(#3565 #2421) Avoid credential bleed from saved sources with the same hostname #3568

Open
wants to merge 5 commits into
base: hotfix/2.4.1
Choose a base branch
from

Conversation

vexx32
Copy link
Member

@vexx32 vexx32 commented Nov 19, 2024

Description Of Changes

  • Rework handling in ChocolateyNugetCredentialProvider to ensure we only match credentials from the full URL
  • Add a config property so we can actually tell what the literal arguments passed to --source on the command line are, and start making use of it here to aid with looking up source credentials.
    • In discussions with Gary, we have agreed the behaviour of looking up a source based purely on the URL here is bad In General, because choco should probably just use the literal command line arguments and not assume you want it to look things up in the config when you did not ask, so this will later be expanded upon and some of the searching functionality here can be removed at a later date.
  • Add unit tests for this credential provider

Motivation and Context

See #3565 - the credentials are being reused in places that they shouldn't.

Testing

Unit tests! :3 Including a regression test specifically for #3565

Open VS and run the tests in the chocolatey.tests project and make sure they pass.

Cory added: Ran CredentialProvider tag in Test Kitchen.

Operating Systems Testing

Win10

Change Types Made

  • Bug fix (non-breaking change).
  • Feature / Enhancement (non-breaking change).
  • Breaking change (fix or feature that could cause existing functionality to change).
  • Documentation changes.
  • PowerShell code changes.

Change Checklist

  • Requires a change to the documentation.
  • Documentation has been updated.
  • Tests to cover my changes, have been added.
  • All new and existing tests passed?
  • PowerShell code changes: PowerShell v3 compatibility checked?

Related Issue

Fixes #3565
Fixes #2421 (different symptom, same issue, likely somewhat duplicate issue)

@gep13 gep13 changed the title Avoid credential bleed from saved sources with the same hostname (#3565 #2421) Avoid credential bleed from saved sources with the same hostname Nov 20, 2024
@gep13 gep13 changed the base branch from develop to hotfix/2.4.1 November 20, 2024 09:01
Previously we looked up any available sources in the config by the
hostname, before falling back to trying an exact match if we had
collisions.

This still allowed credentials to be reused in situations where we don't
actually know if they're applicable; many repository servers will
support different credentials for individual repositories, so we cannot
and should not assume that credentials for one repository will actually
match another repository, nor that users want the credentials to be
shared for both.

It also led to the possibility of users storing one repository first,
and then later specifying a different repository on the same server, and
choco would try to use the stored credentials for the first repository
for the explicitly-entered URL which is nowhere in config.

Instead, we should only match the whole URL (which can be done with
Uri. Equals to ensure that we match hostnames case-insensitively, but
routes case-sensitively), and expect users to provide credentials if
they provide a URL that is not explicitly in the sources.

Additionally, we try to ensure that if a user has named a specific
source, rather than themselves providing a URL at the command line, we
prioritise finding that in the already-configured sources and use that
source if the URL matches the current URL that NuGet requires a
credential for.
These tests ensure that the use cases we expect to handle in the
credential provider are appropriately handled according to our
expectations, based on the user-provided input and the transformed input
that is left in configuration.Sources once the credential provider
typically gets queried.
@corbob
Copy link
Member

corbob commented Nov 20, 2024

Converting this PR to draft while I add and validate Pester tests.

@corbob corbob marked this pull request as ready for review November 21, 2024 01:19
@corbob
Copy link
Member

corbob commented Nov 21, 2024

I've updated the Pester tests once I got them sorted in Test Kitchen. This is once again ready for review.

Copy link
Member

@AdmiringWorm AdmiringWorm left a comment

Choose a reason for hiding this comment

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

In general, this looks good. But there are a few things to fix up, or questions needed to be answered.

Comment on lines 73 to 77
[OneTimeSetUp]
public async Task OneTimeSetup()
{
await With();
}
Copy link
Member

Choose a reason for hiding this comment

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

This should be a Because clause, not a separate OneTimeSetup.

There are no guarantee in what order a OneTimeSetup will be ran when multiple ones are specified (both Context and Because is in a single OneTimeSetup).

Suggested change
[OneTimeSetUp]
public async Task OneTimeSetup()
{
await With();
}
public override void Because()
{
With().Wait();
}

At some point, if we want to test async code more often, then we should think about introducing an async TinySpec instead of doing this (You can look at the AyncTinySpec class I created for Agent).

Copy link
Member

Choose a reason for hiding this comment

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

This has been updated.

Comment on lines 88 to 93
public override void Because()
{
Configuration.Sources = Configuration.ExplicitSources = TargetSourceUrl;
Configuration.SourceCommand.Username = "user";
Configuration.SourceCommand.Password = "totally_secure_password!!!";
}
Copy link
Member

Choose a reason for hiding this comment

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

This is part of the setup, not the actual run of a code. As such this should be in a Context method.

Suggested change
public override void Because()
{
Configuration.Sources = Configuration.ExplicitSources = TargetSourceUrl;
Configuration.SourceCommand.Username = "user";
Configuration.SourceCommand.Password = "totally_secure_password!!!";
}
public override void Context()
{
base.Context();
Configuration.Sources = Configuration.ExplicitSources = TargetSourceUrl;
Configuration.SourceCommand.Username = "user";
Configuration.SourceCommand.Password = "totally_secure_password!!!";
}

Copy link
Member

Choose a reason for hiding this comment

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

This has been updated.

Comment on lines 95 to 99
[Fact]
public void Creates_a_valid_credential()
{
Result.Should().NotBeNull();
}
Copy link
Member

Choose a reason for hiding this comment

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

This does not test if the credentials are valid, just that they were created.

We also commonly have prefixed assertions with Should_ (this part is also true for all other methods created in this file, I am just not commenting on it right now). Additionally, each word should have the first letter in uppercase (it wasn't done when converting to FluentAssertion due to the amount of time it would take).

Suggested change
[Fact]
public void Creates_a_valid_credential()
{
Result.Should().NotBeNull();
}
[Fact]
public void Should_Create_Credentials()
{
Result.Should().NotBeNull();
}

Copy link
Member

Choose a reason for hiding this comment

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

This has been updated.

A note on the naming and casing: should it not be PascalCase as well?

Copy link
Member

Choose a reason for hiding this comment

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

A note on the naming and casing: should it not be PascalCase as well?

Not for tests, as the names of tests can become large, it will be easier to read these names when the words are separated by an underscore.

Comment on lines 116 to 120
public override void Because()
{
Configuration.Sources = TargetSourceUrl;
Configuration.ExplicitSources = TargetSourceName;
}
Copy link
Member

Choose a reason for hiding this comment

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

This should be part of the context (ie the setup), not the because method.

Suggested change
public override void Because()
{
Configuration.Sources = TargetSourceUrl;
Configuration.ExplicitSources = TargetSourceName;
}
public override void Context()
{
base.Context();
Configuration.Sources = TargetSourceUrl;
Configuration.ExplicitSources = TargetSourceName;
}

Copy link
Member

Choose a reason for hiding this comment

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

This has been updated.

Comment on lines 143 to 146
public override void Because()
{
Configuration.Sources = Configuration.ExplicitSources = TargetSourceUrl;
}
Copy link
Member

Choose a reason for hiding this comment

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

Again, should be Context.

Suggested change
public override void Because()
{
Configuration.Sources = Configuration.ExplicitSources = TargetSourceUrl;
}
public override void Context()
{
base.Context();
Configuration.Sources = Configuration.ExplicitSources = TargetSourceUrl;
}

Copy link
Member

Choose a reason for hiding this comment

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

This has been updated.

Comment on lines +42 to +43
configuration.Sources = configuration.ExplicitSources = option.UnquoteSafe();
configuration.ListCommand.ExplicitSource = true;
Copy link
Member

Choose a reason for hiding this comment

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

Why are we storing a Source and an ExplicitSource, at the same time that we are setting a boolean property called ExplicitSource?

Also, what happens if the source that is passed is the named source of a configuration? Or one of the alternative sources?
Should that also be considered an explicit source?

Copy link
Member

Choose a reason for hiding this comment

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

In the credential provider, the first thing it does it try to look up any explicitly set named sources, and then the remainder as URIs. I if it's an alternative source it'll be handled before getting to the credential provider and so will not matter if it's set as an explicit source.

Copy link
Member

Choose a reason for hiding this comment

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

I am ok with the introduction of the ExplicitSources property on the top-level configuration object, but that does bring up the question of whether the ExplicitSource boolean is needed anymore on the ListCommand object. Should we not replace the usage of ExplicitSource in the ChocolateyListCommand class, and replace if with having anything in the ExplicitSources property? Perhaps mark ExplicitSource as obsolete, and then we can remove it in future breaking change?

Comment on lines 97 to 98
.ToList();
if (namedExplicitSources?.Count > 0)
Copy link
Member

Choose a reason for hiding this comment

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

Should be a newline here.

Suggested change
.ToList();
if (namedExplicitSources?.Count > 0)
.ToList();
if (namedExplicitSources?.Count > 0)

Copy link
Member

Choose a reason for hiding this comment

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

This has been updated.

Comment on lines +102 to +104
source = _config.MachineSources
.Where(s => namedExplicitSources.Contains(s.Name) && new Uri(s.Key.TrimEnd('/')) == trimmedTargetUri)
.FirstOrDefault();
Copy link
Member

Choose a reason for hiding this comment

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

Why are we doing case sensitive checking here?

The Contains method is case sensitive, and as the comment says it does a case sensitive match on the url (just not the hostname).
That do not make sense, and I would think it is a case insensitive match we would want.

Copy link
Member

Choose a reason for hiding this comment

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

The reason for doing a case sensitive match on everything but the hostname is that the hostname is handled by DNS and is case insensitive, but the rest of the URL is up to the responding server which may or may not be case insensitive. For example: https://docs.chocolatey.org/en-us/central-management/ is valid while https://docs.chocolatey.org/en-us/central-managemenT/ is not. But if you were to case-insensitive compare them they are identical.

Copy link
Member

Choose a reason for hiding this comment

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

While that is true for the documentation site we have, that is actually quite uncommon.

If we take the api we have, the URL https://community.chocolatey.org/api/v2/ returns the exact same information as https://community.chocolatey.org/API/v2/.

But even if they were not returning the same information, it would not be the point.

The point is, we are looking up information about a URL in the machine sources configuration, and it is not guaranteed that a user will remember the exact case of the URI that was configured but still intend to use it. By not allowing the case to be different, we are preventing this configured URL to be picked up. We should also use the URL as specified in the machine configuration (and if I am reading everything correctly, we are doing that), so even if the server would accept one case but not the other, it wouldn't be a problem since it already uses the case that is configured on the machine object.

There is also the case that with this change, a user can not use a named source that differs in casing either. Ie, they can not pass the source Chocolatey when the name is configured to be chocolatey. This is something that is changing the behavior of what is currently available, as currently using a named source with a different case will resolve to a configured URL.

Comment on lines +112 to +113
// Note: This behaviour remains as removing it would be a breaking change, but we may want
// to remove this in a future version, as specifying an explicit URL should potentially
Copy link
Member

Choose a reason for hiding this comment

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

If there has been a decision to remove this, or potentially removing it, we would need an issue to be created to keep track of this.

Copy link
Member

Choose a reason for hiding this comment

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

I think we agree that we would add an issue to track this discussion, and point to it in this comment in the code, right?

.Where(s => !string.IsNullOrWhiteSpace(s.Username)
&& !string.IsNullOrWhiteSpace(s.EncryptedPassword)
&& Uri.TryCreate(s.Key.TrimEnd('/'), UriKind.Absolute, out var trimmedSourceUri)
&& trimmedSourceUri == trimmedTargetUri)
Copy link
Member

Choose a reason for hiding this comment

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

Coming back to the comment further up, if this is doing a case-sensitive match on the URL (except the hostname), then this type of comparison is probably not what we would want.

Copy link
Member

Choose a reason for hiding this comment

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

Add Pester tests to ensure we don't inadvertently bleed configured
credentials into scenarios where they should not be used.
The query string used by some commands seems to be ever so slightly
different depending on where they are run. This removes the query string
from the tests as these tests are not concerned with the entire message,
just the initial part of the message.
[string]$Name = "*",

[Parameter()]
[switch]$All
Copy link
Member

Choose a reason for hiding this comment

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

Am I missing where this is being used?

# Skip the download command if chocolatey.extension is not installed.
Context 'Command (<Command>)' -Skip:($Command -eq 'download' -and -not $HasLicensedExtension) {
BeforeAll {
# Picked a package that is on `hermes-setup` but not on `hermes`.
Copy link
Member

Choose a reason for hiding this comment

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

How are we going to enforce this going forward?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants