From 2eb7954fa52ed2ef863b532c6311116984e17c2b Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 13 Feb 2025 17:00:24 -0800 Subject: [PATCH] ociauthconfig: Require valid syntax for Docker-style config auths Previously we just required the slash-separated segments to match without imposing any further constraint, but if the Docker-style config syntax evolves to allow other syntaxes here in future it'd be better for us to just ignore what we don't recognize rather than get confused into trying to match it in the current way. ParseRepositoryAddressPrefix is an exported function because package cliconfig will use it in a future commit to deal with our OpenTofu-specific equivalent of the "auths" objects: oci_credentials blocks in the CLI configuration. Signed-off-by: Martin Atkins --- .../docker_cli_credentials_config.go | 18 +++++- .../ociauthconfig/repository_addr.go | 62 +++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 internal/command/cliconfig/ociauthconfig/repository_addr.go diff --git a/internal/command/cliconfig/ociauthconfig/docker_cli_credentials_config.go b/internal/command/cliconfig/ociauthconfig/docker_cli_credentials_config.go index f9e6b8bf2d..9ba4f84a3c 100644 --- a/internal/command/cliconfig/ociauthconfig/docker_cli_credentials_config.go +++ b/internal/command/cliconfig/ociauthconfig/docker_cli_credentials_config.go @@ -182,12 +182,24 @@ func dockerCLIStyleAuthMatch(authsPropertyName string, wantRegistryDomain, wantR if authsPropertyName == "" { return NoCredentialsSpecificity // Invalid } - gotDomain, gotRepositoryPath, havePath := strings.Cut(authsPropertyName, "/") + var gotDomain, gotRepositoryPath string + if strings.Count(authsPropertyName, "/") > 0 { + var err error + gotDomain, gotRepositoryPath, err = ParseRepositoryAddressPrefix(authsPropertyName) + if err != nil { + // Since these Docker CLI-style config files are not exclusively for OpenTofu, + // we just silently ignore a property whose name we can't make sense of + // in case a future version of this format adds something new that we + // don't yet support. + return NoCredentialsSpecificity // Invalid + } + } else { + gotDomain = authsPropertyName + } if gotDomain != wantRegistryDomain { return NoCredentialsSpecificity // does not match } - if !havePath { - // Domain-only match fast path + if gotRepositoryPath == "" { return DomainCredentialsSpecificity // matches only the domain } // If authsPropertyName includes a path (that is: if gotRepositoryPath != "") diff --git a/internal/command/cliconfig/ociauthconfig/repository_addr.go b/internal/command/cliconfig/ociauthconfig/repository_addr.go new file mode 100644 index 0000000000..7f5b265929 --- /dev/null +++ b/internal/command/cliconfig/ociauthconfig/repository_addr.go @@ -0,0 +1,62 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ociauthconfig + +import ( + "fmt" + "strings" + + orasregistry "oras.land/oras-go/v2/registry" +) + +// ParseRepositoryAddressPrefix attempts to parse the given string as the compact +// "registry-domain/repository-path" format, returning the separate registry domain +// and repository path parts if it's valid. +// +// This function is intended for the repository address prefix syntax used for +// specifying "auths" in the Docker CLI-style config format, and for OpenTofu's +// own oci_credentials CLI configuration blocks that follow the same syntax. +// +// The registry-domain portion can optionally include a port number delimited +// by a colon, like "localhost:5000". The repository-path portion is optional, +// with an address without it referring to all repositories on the given +// registry domain. +func ParseRepositoryAddressPrefix(addr string) (registryDomain, repositoryPath string, err error) { + // For now we're relying on the ORAS implementation of parsing, but if we decide to + // move away from ORAS for other registry client purposes then we can either + // implement something similar inline here or find an alternative external library + // to use for this. + // We're actually using the _reference_ parser here, since a reference incorporates + // a repository address, but we'll reject after the fact any result that includes + // a tag or digest portion since we're not intending to accept addresses of specific + // artifacts. + + if strings.Count(addr, "/") != 0 { + // This seems to be an address with both a registry and a repository path. + ref, parseErr := orasregistry.ParseReference(addr) + if parseErr != nil { + // The ORAS function returns errors with sufficient context that any + // further decoration we might add here would be redundant. For example, + // this might return an error whose message is + // "invalid reference: invalid registry invalid:thing:blah". + return "", "", parseErr + } + if ref.Reference != "" { + return "", "", fmt.Errorf("invalid reference: artifact tag or digest not allowed") + } + return ref.Registry, ref.Repository, nil + } + + // If we get here then it seems like we have _just_ a domain part. ORAS does + // not have a separate function just for parsing a domain, so we'll borrow the + // validate function from its reference parser instead. + ref := &orasregistry.Reference{ + Registry: addr, + } + err = ref.ValidateRegistry() + // ValidateRegistry returns an error with a string like "invalid reference: invalid registry invalid:thing:blah" + return addr, "", err +}