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

Add apt credentials parser #60

Merged
merged 23 commits into from
Aug 11, 2023
Merged

Conversation

woky
Copy link
Contributor

@woky woky commented May 15, 2023

Add apt.auth.conf(5) machine credentials parser. See the parser code for
documentation.

This commit doesn't change the current behavior. It's only a preparation
for future commits that will add support for different types of archives.

This PR depends on #55.

  • Have you signed the CLA?

@woky woky mentioned this pull request May 15, 2023
1 task
@woky woky force-pushed the pro-new/netrc-parser branch 2 times, most recently from e38fe87 to f5faec1 Compare May 15, 2023 21:22
@woky woky mentioned this pull request May 15, 2023
@woky woky force-pushed the pro-new/netrc-parser branch 3 times, most recently from caabf58 to 41e2547 Compare May 17, 2023 08:47
@woky woky force-pushed the pro-new/netrc-parser branch 3 times, most recently from 3d20fa5 to e708109 Compare May 18, 2023 21:23
@cjdcordeiro cjdcordeiro added the Priority Look at me first label May 22, 2023
@woky woky force-pushed the pro-new/netrc-parser branch 3 times, most recently from 7a08f21 to f9e2c5c Compare May 24, 2023 20:12
Copy link
Member

@rebornplusplus rebornplusplus left a comment

Choose a reason for hiding this comment

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

Looks nice! I only have a few queries.

internal/archive/credentials.go Outdated Show resolved Hide resolved
internal/archive/credentials.go Outdated Show resolved Hide resolved
internal/archive/credentials.go Outdated Show resolved Hide resolved
internal/archive/credentials.go Outdated Show resolved Hide resolved
internal/archive/credentials.go Show resolved Hide resolved
@woky woky force-pushed the pro-new/netrc-parser branch 2 times, most recently from 64391b9 to 1babeb0 Compare June 21, 2023 18:05
Add apt.auth.conf(5) machine credentials parser. See the parser code for
documentation.

This commit doesn't change the current behavior. It's only a preparation
for future commits that will add support for different types of
archives.
@woky woky force-pushed the pro-new/netrc-parser branch from 1babeb0 to f573944 Compare June 22, 2023 04:28
Copy link
Contributor

@niemeyer niemeyer left a comment

Choose a reason for hiding this comment

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

Thanks Thomas, this is looking good. Here is a fisrt pass:

internal/archive/export_test.go Outdated Show resolved Hide resolved
internal/archive/credentials_test.go Show resolved Hide resolved
internal/archive/credentials_test.go Outdated Show resolved Hide resolved
internal/archive/credentials_test.go Show resolved Hide resolved
internal/archive/credentials.go Outdated Show resolved Hide resolved
internal/archive/credentials.go Show resolved Hide resolved
token := p.scanner.Text()
if i := strings.Index(token, "://"); i != -1 {
if token[0:i] != p.query.scheme {
return netrcInvalid, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

So this gets back to the start state? Again, the idea of the parser being in an invalid state and just going on is a bit confusing, and it seems to relate to the bug mentioned earlier as it just ignores the current context while looking for special keywords. We'll probably need to sync on this with more bandwidth.

Copy link
Contributor Author

@woky woky Jun 28, 2023

Choose a reason for hiding this comment

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

This is how the format is defined and parsed by Apt. It deliberately ignores current context. We're not at liberty to interpret the file differently.

internal/archive/credentials.go Show resolved Hide resolved
if !strings.HasPrefix(token, p.query.port) {
return netrcInvalid, nil
}
token = token[len(p.query.port):]
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like it'll match the port ":90" when looking at ":9000".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The check below mitigates that

               if !strings.HasPrefix(p.query.path, token) {
                       return netrcInvalid, nil
               }

internal/archive/credentials.go Show resolved Hide resolved
@niemeyer niemeyer added Reviewed Supposedly ready for tuning or merging and removed Priority Look at me first labels Jun 26, 2023
woky added 2 commits June 28, 2023 13:53
Remove global comment.
Test findCredsInDir() instead of findCredentials(). The latter calls the
former with the directory set to a CHISEL_AUTH_DIR environment variable
value if it's non-empty or "/etc/apt/auth.conf.d".
woky added 2 commits June 28, 2023 14:18
Adorn error return value.
Report filename on parser error.
@woky woky added Priority Look at me first and removed Reviewed Supposedly ready for tuning or merging labels Jun 28, 2023
@woky woky requested review from niemeyer and cjdcordeiro June 28, 2023 12:36
woky added 3 commits June 28, 2023 14:43
Report file path on os.Open() error too.
Skip files with invalid extension before stat()-ing them.
internal/archive/credentials.go Outdated Show resolved Hide resolved
}
var err error
for state := netrcInvalid; state != nil; {
state, err = state(&p)
Copy link
Collaborator

Choose a reason for hiding this comment

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

that logic is true @woky.

Some questions though:

  • if err != nil and state == nil, then do we need to continue to line 221? If not, better check the err here and just return
  • as you say, the above condition is by design and It would be a bug for a state to return both state and err non-nil.. So isn't that the check you should be doing? I.e. if state != nil && err != nil?

@cjdcordeiro cjdcordeiro requested a review from flotter July 17, 2023 16:05
Copy link

@flotter flotter left a comment

Choose a reason for hiding this comment

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

Looks really great. I did a review just to help out and improve my Go skills :)

func findCredentialsInDir(repoURL string, credsDir string) (creds credentials, err error) {
contents, err := os.ReadDir(credsDir)
if err != nil {
if os.IsNotExist(err) {
Copy link

Choose a reason for hiding this comment

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

Just an observation: here we are concluding that no credentials exist, which could be reached if CHISEL_AUTH_DIR is set, but pointing to a non-existing directory. This may be by design, but just pointing out that this does not mean that /etc/apt/auth.conf.d does not contain credentials.

Copy link
Contributor Author

@woky woky Jul 24, 2023

Choose a reason for hiding this comment

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

It is indeed by my design. :-) We did not specify this, so I did what felt intuitive to me. Warning when CHISEL_AUTH_DIR doesn't exist and ignoring non-existent /etc/apt/auth.conf.d feels inconsistent to me. Would you like a different behavior?

Example from elsewhere: mpv looks up its configs in $MPV_HOME or in ~/.config/mpv/ (among other locations). It doesn't complain if either of those doesn't exist. Similarly, git doesn't complain when $GIT_CONFIG_SYSTEM or /etc/gitconfig doesn't exist but it reads it if it does. In both cases, if the variable is set (or non-empty?), the default location is skipped.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see both of your points. I think it would help to simply have a debug log message to say that CHISEL_AUTH_DIR's path does not exist, regardless of its path.

Copy link

Choose a reason for hiding this comment

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

It does make sense to me, the naming of the environment variable suggests an overriding behaviour, which if abused, must yield consequences :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@cjdcordeiro @flotter I've added a debug message when the credentials directory does not exist, regardless of whether it's the default location or overridden by the variable. See commit 482ff15. Please resolve if you think it addresses your comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

As a good rule of thumb, any function that is requested to perform a given task must either succeed at its task, or return an error informing that it could not. That's even more true when the function actually returns an error, as the caller will imply that unless there's an error, its intention was successful. The task of this function is to find credentials in a directory, so it must either succeed at that, or return an error informing that it cannot perform its requested task.

The call site, though, may choose to ignore a given scenario. For handling some of those cases we have error results that can be more easily verified, either directly or via a function.

Copy link
Contributor Author

@woky woky Aug 8, 2023

Choose a reason for hiding this comment

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

Introduced ErrCredentialsNotFound error. The function now returns this error when no credentials are found. @niemeyer Please resolve the thread if appropriate.

internal/archive/credentials.go Outdated Show resolved Hide resolved
internal/archive/credentials.go Show resolved Hide resolved
internal/archive/credentials.go Outdated Show resolved Hide resolved
internal/archive/credentials.go Outdated Show resolved Hide resolved
internal/archive/credentials.go Outdated Show resolved Hide resolved
@woky woky requested a review from flotter July 25, 2023 14:38
Copy link

@flotter flotter left a comment

Choose a reason for hiding this comment

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

Proposed changes looks great, thanks!

Copy link
Contributor

@niemeyer niemeyer left a comment

Choose a reason for hiding this comment

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

Some additional notes.

internal/archive/credentials.go Outdated Show resolved Hide resolved
func findCredentialsInDir(repoURL string, credsDir string) (creds credentials, err error) {
contents, err := os.ReadDir(credsDir)
if err != nil {
if os.IsNotExist(err) {
Copy link
Contributor

Choose a reason for hiding this comment

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

As a good rule of thumb, any function that is requested to perform a given task must either succeed at its task, or return an error informing that it could not. That's even more true when the function actually returns an error, as the caller will imply that unless there's an error, its intention was successful. The task of this function is to find credentials in a directory, so it must either succeed at that, or return an error informing that it cannot perform its requested task.

The call site, though, may choose to ignore a given scenario. For handling some of those cases we have error results that can be more easily verified, either directly or via a function.

}
if len(confFiles) == 0 {
err = errors.Join(errs...)
return
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to the earlier point on errors here: the loop above may skip all the way through, and we end up with no credentials and no error either.

Copy link
Contributor Author

@woky woky Aug 8, 2023

Choose a reason for hiding this comment

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

As I commented above, I've introduced ErrCredentialsNotFound error. The function now returns this error when no credentials are found. @niemeyer Please resolve the thread if appropriate.

// directory specified by CHISEL_AUTH_DIR environment variable if it's
// non-empty or /etc/apt/auth.conf.d.
func findCredentials(repoUrl string) (credentials, error) {
credsDir := "/etc/apt/auth.conf.d"
Copy link
Contributor

Choose a reason for hiding this comment

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

Per earlier comments, the default path should be a global variable, or a constant, according to your preference, and the logic must be tested according to these notes too. This function, and in particular the use of CHISEL_AUTH_DIR, looks untested?

err = fmt.Errorf("cannot parse archive URL: %w", err)
return
}
if query == nil { // creds.Empty() == false
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be improved a bit so that the intention is more clear. If the point here is that we already have credentials, we should check creds explicitly instead of checking query and assuming credentials are set in a comment.

Copy link
Contributor Author

@woky woky Aug 8, 2023

Choose a reason for hiding this comment

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

Replaced with !creds.Empty(). I can't rely on err != nil here as that alone does not tell me whether the URL contained credentials. @niemeyer Please resolve the thread if appropriate.

return
}

errs := make([]error, 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the intention here? Why is this not just var errs []error?

Copy link
Contributor Author

@woky woky Aug 8, 2023

Choose a reason for hiding this comment

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

As part of the change to return ErrCredentialsNotFound when no credentials are found, non-fatal errors are now logged instead so the function no longer collects them, and this variable was removed. @niemeyer Please resolve the thread if appropriate.

internal/archive/credentials.go Show resolved Hide resolved
internal/archive/credentials.go Outdated Show resolved Hide resolved

if err = findCredsInFile(query, f, &creds); err != nil {
errs = append(errs, fmt.Errorf("cannot parse credentials file %s: %w", fpath, err))
} else if !creds.Empty() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's please have a more clear interface on findCredsInFile, making it return the credentials when it finds them, as usual in Go, instead of having C-style output parameters unnecessarily. We should also not have to test creds for being empty. Per earlier notes, if the error is nil, the intent of the function must have been fulfilled.

Copy link
Contributor Author

@woky woky Aug 8, 2023

Choose a reason for hiding this comment

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

As I commented above, I've introduced ErrCredentialsNotFound error. The function findCredsInFile was renamed to findCredentialsInternal and it now returns either a non-nil pointer to credentials or a non-nil error when an error occurs or credentials are not found. @niemeyer Please resolve the thread if appropriate.

// [3] https://salsa.debian.org/apt-team/apt/-/blob/4e04cbaf/methods/aptmethod.h#L560
// [4] https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html
// [5] https://daniel.haxx.se/blog/2022/05/31/netrc-pains/
func findCredsInFile(query *credentialsQuery, netrc io.Reader, creds *credentials) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Per note above, which of these is input and which of these is output? Let's please clean up the function prototype to reflect that, and have the error result reflect intent, per notes above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Commented about this function in the above thread. @niemeyer Please resolve the thread if appropriate.

@woky woky requested review from niemeyer and cjdcordeiro August 8, 2023 12:19
@woky woky force-pushed the pro-new/netrc-parser branch from 1bcbb91 to ddefc45 Compare August 9, 2023 19:56
Copy link
Contributor

@niemeyer niemeyer 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 implementatino and the tuning!

// [2] https://salsa.debian.org/apt-team/apt/-/blob/d9039b24/apt-pkg/contrib/netrc.cc
// [3] https://salsa.debian.org/apt-team/apt/-/blob/4e04cbaf/methods/aptmethod.h#L560
// [4] https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html
// [5] https://daniel.haxx.se/blog/2022/05/31/netrc-pains/
Copy link
Contributor

Choose a reason for hiding this comment

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

While we're still here, thanks for the extensive documentation and references above.

@niemeyer niemeyer merged commit 86bcf61 into canonical:main Aug 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Priority Look at me first
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants