mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-20 02:09:26 -05:00
This new method collects all of the various different settings that describe the operator's chosen OCI credentials policy and returns a single object that encapsulates that policy. This is the method that will, in future commits, be used by package main to provide the credentials policy to any OCI-registry-related subsystems using dependency-inversion style. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
794 lines
30 KiB
Go
794 lines
30 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package cliconfig
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/opentofu/opentofu/internal/command/cliconfig/ociauthconfig"
|
|
)
|
|
|
|
func TestLoadConfig_ociDefaultCredentials(t *testing.T) {
|
|
// The keys in this map correspond to fixture names under
|
|
// the "testdata" directory.
|
|
tests := map[string]struct {
|
|
want *OCIDefaultCredentials
|
|
wantErr string
|
|
}{
|
|
"oci-default-credentials": {
|
|
&OCIDefaultCredentials{
|
|
DiscoverAmbientCredentials: true,
|
|
DockerStyleConfigFiles: []string{
|
|
"/foo/bar/auth.json",
|
|
},
|
|
DefaultDockerCredentialHelper: "osxkeychain",
|
|
},
|
|
``,
|
|
},
|
|
"oci-default-credentials.json": {
|
|
&OCIDefaultCredentials{
|
|
DiscoverAmbientCredentials: true,
|
|
DockerStyleConfigFiles: []string{
|
|
"/foo/bar/auth.json",
|
|
},
|
|
DefaultDockerCredentialHelper: "osxkeychain",
|
|
},
|
|
``,
|
|
},
|
|
"oci-default-credentials-defaults": {
|
|
&OCIDefaultCredentials{
|
|
DiscoverAmbientCredentials: true,
|
|
DockerStyleConfigFiles: nil, // represents "use the default search paths"
|
|
DefaultDockerCredentialHelper: "", // represents no default credential helper at all
|
|
},
|
|
``,
|
|
},
|
|
"oci-default-credentials-no-docker": {
|
|
&OCIDefaultCredentials{
|
|
DiscoverAmbientCredentials: true,
|
|
DockerStyleConfigFiles: []string{
|
|
// Must be non-nil empty, because nil represents
|
|
// "use the default search paths".
|
|
},
|
|
DefaultDockerCredentialHelper: "", // represents no default credential helper at all
|
|
},
|
|
``,
|
|
},
|
|
"oci-default-credentials-inconsistent": {
|
|
&OCIDefaultCredentials{
|
|
// The following is just a best-effort approximation of the
|
|
// configuration despite the errors, so it's not super important
|
|
// that it stay consistent in future releases but tested just
|
|
// so that if it _does_ change we can review and make sure that
|
|
// the change is reasonable.
|
|
DiscoverAmbientCredentials: false,
|
|
DockerStyleConfigFiles: []string{},
|
|
DefaultDockerCredentialHelper: "",
|
|
},
|
|
`disables discovery of ambient credentials, but also sets docker_style_config_files`,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
fixtureFile := filepath.Join("testdata", name)
|
|
gotConfig, diags := loadConfigFile(fixtureFile)
|
|
if diags.HasErrors() {
|
|
errStr := diags.Err().Error()
|
|
if test.wantErr == "" {
|
|
t.Errorf("unexpected errors: %s", errStr)
|
|
}
|
|
if !strings.Contains(errStr, test.wantErr) {
|
|
t.Errorf("missing expected error\nwant substring: %s\ngot: %s", test.wantErr, errStr)
|
|
}
|
|
} else if test.wantErr != "" {
|
|
t.Errorf("unexpected success\nwant error with substring: %s", test.wantErr)
|
|
}
|
|
|
|
var got *OCIDefaultCredentials
|
|
if len(gotConfig.OCIDefaultCredentials) > 0 {
|
|
got = gotConfig.OCIDefaultCredentials[0]
|
|
}
|
|
if diff := cmp.Diff(test.want, got); diff != "" {
|
|
t.Error("unexpected result\n" + diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("oci-default-credentials-duplicate", func(t *testing.T) {
|
|
// This one is different than all of the others because it
|
|
// only gets detected as invalid during the validation step,
|
|
// so that (in the normal case) we can check it only after
|
|
// we've merged all of the separate CLI config files together.
|
|
fixtureFile := filepath.Join("testdata", "oci-default-credentials-duplicate")
|
|
gotConfig, loadDiags := loadConfigFile(fixtureFile)
|
|
if loadDiags.HasErrors() {
|
|
t.Errorf("unexpected errors from loadConfigFile: %s", loadDiags.Err().Error())
|
|
}
|
|
|
|
validateDiags := gotConfig.Validate()
|
|
wantErr := `No more than one oci_default_credentials block may be specified`
|
|
if !validateDiags.HasErrors() {
|
|
t.Fatalf("unexpected success\nwant error with substring: %s", wantErr)
|
|
}
|
|
if errStr := validateDiags.Err().Error(); !strings.Contains(errStr, wantErr) {
|
|
t.Errorf("missing expected error\nwant substring: %s\ngot: %s", wantErr, errStr)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLoadConfig_ociCredentials(t *testing.T) {
|
|
// The keys in this map correspond to fixture names under
|
|
// the "testdata" directory.
|
|
tests := map[string]struct {
|
|
want []*OCIRepositoryCredentials
|
|
wantErr string
|
|
}{
|
|
"oci-credentials-basic": {
|
|
[]*OCIRepositoryCredentials{
|
|
{
|
|
RepositoryPrefix: "example.com",
|
|
Username: "foo",
|
|
Password: "bar",
|
|
},
|
|
},
|
|
``,
|
|
},
|
|
"oci-credentials-basic.json": {
|
|
[]*OCIRepositoryCredentials{
|
|
{
|
|
RepositoryPrefix: "example.com",
|
|
Username: "foo",
|
|
Password: "bar",
|
|
},
|
|
},
|
|
``,
|
|
},
|
|
"oci-credentials-oauth": {
|
|
[]*OCIRepositoryCredentials{
|
|
{
|
|
RepositoryPrefix: "example.com",
|
|
AccessToken: "foo",
|
|
RefreshToken: "bar",
|
|
},
|
|
},
|
|
``,
|
|
},
|
|
"oci-credentials-oauth.json": {
|
|
[]*OCIRepositoryCredentials{
|
|
{
|
|
RepositoryPrefix: "example.com",
|
|
AccessToken: "foo",
|
|
RefreshToken: "bar",
|
|
},
|
|
},
|
|
``,
|
|
},
|
|
"oci-credentials-credhelper": {
|
|
[]*OCIRepositoryCredentials{
|
|
{
|
|
RepositoryPrefix: "example.com",
|
|
DockerCredentialHelper: "osxkeychain",
|
|
},
|
|
},
|
|
``,
|
|
},
|
|
"oci-credentials-credhelper.json": {
|
|
[]*OCIRepositoryCredentials{
|
|
{
|
|
RepositoryPrefix: "example.com",
|
|
DockerCredentialHelper: "osxkeychain",
|
|
},
|
|
},
|
|
``,
|
|
},
|
|
"oci-credentials-multi": {
|
|
[]*OCIRepositoryCredentials{
|
|
{
|
|
RepositoryPrefix: "example.com",
|
|
Username: "foo",
|
|
Password: "bar",
|
|
},
|
|
{
|
|
RepositoryPrefix: "example.net",
|
|
Username: "baz",
|
|
Password: "beep",
|
|
},
|
|
},
|
|
``,
|
|
},
|
|
"oci-credentials-multi.json": {
|
|
[]*OCIRepositoryCredentials{
|
|
{
|
|
RepositoryPrefix: "example.com",
|
|
Username: "foo",
|
|
Password: "bar",
|
|
},
|
|
{
|
|
RepositoryPrefix: "example.net",
|
|
Username: "baz",
|
|
Password: "beep",
|
|
},
|
|
},
|
|
``,
|
|
},
|
|
"oci-credentials-empty": {
|
|
nil,
|
|
`must set either username+password, access_token+refresh_token, or docker_credentials_helper`,
|
|
},
|
|
"oci-credentials-mixedstyles": {
|
|
nil,
|
|
`must set only one group out of username+password, access_token+refresh_token, or docker_credentials_helper`,
|
|
},
|
|
"oci-credentials-basic-nopassword": {
|
|
nil,
|
|
`must set both username and password together when using static credentials`,
|
|
},
|
|
"oci-credentials-basic-nousername": {
|
|
nil,
|
|
`must set both username and password together when using static credentials`,
|
|
},
|
|
"oci-credentials-oauth-noaccess": {
|
|
nil,
|
|
`must set both access_token and refresh_token together when using OAuth-style credentials`,
|
|
},
|
|
"oci-credentials-oauth-norefresh": {
|
|
nil,
|
|
`must set both access_token and refresh_token together when using OAuth-style credentials`,
|
|
},
|
|
"oci-credentials-credhelper-badsyntax": {
|
|
nil,
|
|
`specifies the invalid Docker credential helper name "not/valid"`,
|
|
},
|
|
"oci-credentials-credhelper-repopath": {
|
|
nil,
|
|
`cannot set docker_credentials_helper with a repository path`,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
fixtureFile := filepath.Join("testdata", name)
|
|
gotConfig, diags := loadConfigFile(fixtureFile)
|
|
if diags.HasErrors() {
|
|
errStr := diags.Err().Error()
|
|
if test.wantErr == "" {
|
|
t.Errorf("unexpected errors: %s", errStr)
|
|
}
|
|
if !strings.Contains(errStr, test.wantErr) {
|
|
t.Errorf("missing expected error\nwant substring: %s\ngot: %s", test.wantErr, errStr)
|
|
}
|
|
} else if test.wantErr != "" {
|
|
t.Errorf("unexpected success\nwant error with substring: %s", test.wantErr)
|
|
}
|
|
|
|
got := gotConfig.OCIRepositoryCredentials
|
|
if diff := cmp.Diff(test.want, got); diff != "" {
|
|
t.Error("unexpected result\n" + diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("oci-credentials-duplicate", func(t *testing.T) {
|
|
// This one is different than all of the others because it
|
|
// only gets detected as invalid during the validation step,
|
|
// so that (in the normal case) we can check it only after
|
|
// we've merged all of the separate CLI config files together.
|
|
fixtureFile := filepath.Join("testdata", "oci-credentials-duplicate")
|
|
gotConfig, loadDiags := loadConfigFile(fixtureFile)
|
|
if loadDiags.HasErrors() {
|
|
t.Errorf("unexpected errors from loadConfigFile: %s", loadDiags.Err().Error())
|
|
}
|
|
|
|
validateDiags := gotConfig.Validate()
|
|
wantErr := `Duplicate oci_credentials block for "example.com"`
|
|
if !validateDiags.HasErrors() {
|
|
t.Fatalf("unexpected success\nwant error with substring: %s", wantErr)
|
|
}
|
|
if errStr := validateDiags.Err().Error(); !strings.Contains(errStr, wantErr) {
|
|
t.Errorf("missing expected error\nwant substring: %s\ngot: %s", wantErr, errStr)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConfigOCICredentialsPolicy(t *testing.T) {
|
|
// This test exercises various different combinations of OCI credentials
|
|
// policy configuration, verifying that the correct set of credentials gets
|
|
// selected in each case as a limited-scope integration test.
|
|
//
|
|
// This is the most comprehensive and complicated test of the OCI credentials
|
|
// policy functionality, and there are various more-focused unit tests both
|
|
// elsewhere in this package and in package ociauthconfig, so if you can test
|
|
// whatever you are trying to test in a more direct way using one of those
|
|
// other tests that would probably give us more specific feedback if
|
|
// something gets regressed in future.
|
|
//
|
|
// This test also intentionally focuses only on valid configuration cases,
|
|
// since the Config.OCICredentialsPolicy method is documented as allowed only
|
|
// after having successfully loaded and validated a CLI configuration. To
|
|
// test the handling of invalid configuration input, add cases to
|
|
// TestLoadConfig_ociDefaultCredentials and TestLoadConfig_ociCredentials instead.
|
|
|
|
type Subtest struct {
|
|
wantSpecificity ociauthconfig.CredentialsSpecificity
|
|
wantCredentials *ociauthconfig.Credentials
|
|
}
|
|
fixturesDir := filepath.Join("testdata", "oci-credentials-policy")
|
|
|
|
// The keys of the following map correspond to directories under
|
|
// testdata/oci-credentials-policy. Each directory should include
|
|
// zero or more files named with the ".tfrc" or ".tfrc.json" suffix,
|
|
// and can optionally include the following subdirectories:
|
|
// - home: represents a fake home directory to use when performing
|
|
// discovery of ambient credentials. This directory will be provided
|
|
// to the discovery code regardless of whether it exists, with the
|
|
// assumption that any test fixture that needs it will include it.
|
|
// - xdgconfig: if present, the absolute path to this directory is
|
|
// provided to the ambient config discovery code as the
|
|
// XDG_CONFIG_HOME environment variable.
|
|
// - xdgrun: if present, the absolute path to this directory is
|
|
// provided to the ambient config discovery code as the
|
|
// XDG_RUNTIME_DIR environment variable.
|
|
// Each directory name must end with a dash followed by a GOOS-style
|
|
// operating system name, which will be used to influence the
|
|
// platform-specific rules in the ambient config discovery logic.
|
|
tests := map[string]struct {
|
|
// wantConfigLocations is the set of expected config location names as reported
|
|
// by the CredentialsConfigLocationForUI method of each CredentialsConfig object.
|
|
//
|
|
// This is a baseline check to make sure we even discovered what we expected to
|
|
// discover, to give more explicit feedback about inconsistencies than we'd get
|
|
// just from the subtests, which only _indirectly_ test that the expected config
|
|
// locations are present.
|
|
wantConfigLocations []string
|
|
|
|
// The keys of this map are OCI repository addresses like "domainname/repository"
|
|
// which should be looked up against the credentials policy represented by the
|
|
// configuration of the parent test, and should produce the given results.
|
|
subtests map[string]Subtest
|
|
}{
|
|
"empty-linux": {
|
|
wantConfigLocations: nil,
|
|
subtests: map[string]Subtest{
|
|
"example.com": {
|
|
wantCredentials: nil,
|
|
},
|
|
"example.com/foo/bar": {
|
|
wantCredentials: nil,
|
|
},
|
|
},
|
|
},
|
|
"mixed-darwin": {
|
|
wantConfigLocations: []string{
|
|
`explicit oci_credentials "example.com" block`,
|
|
`explicit oci_credentials "example.com/foo" block`,
|
|
`oci_default_credentials block`,
|
|
`home/.config/containers/auth.json`,
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.org": {
|
|
// We have no blocks for this domain, so the global credential helper "wins".
|
|
// If this fails trying to use a credhelper called "superseded-by-explicit-config"
|
|
// then we incorrectly preferred the ambient config's setting for this.
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.org")),
|
|
},
|
|
"example.com": {
|
|
// This domain has a conflicting entry in the ambient Docker-style config,
|
|
// but the explicit configuration should "win".
|
|
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com user", "example.com password")),
|
|
},
|
|
"example.com/foo": {
|
|
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(1),
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com/foo user", "example.com/foo password")),
|
|
},
|
|
"example.com/bar": {
|
|
// These credentials come from the base64-encoded "auth" string in the
|
|
// ambient Docker-style config file.
|
|
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(1),
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("ambient-example.com-user", "ambient-password")),
|
|
},
|
|
"example.com/not-foo": {
|
|
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com user", "example.com password")),
|
|
},
|
|
"example.com/not-bar": {
|
|
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com user", "example.com password")),
|
|
},
|
|
},
|
|
},
|
|
"explicit-specificity-linux": {
|
|
wantConfigLocations: []string{
|
|
`explicit oci_credentials "example.com" block`,
|
|
`explicit oci_credentials "example.com/foo" block`,
|
|
`explicit oci_credentials "example.com/foo/bar" block`,
|
|
`explicit oci_credentials "example.net" block`,
|
|
`explicit oci_credentials "example.net/foo" block`,
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.org": {
|
|
wantCredentials: nil, // no credentials blocks for this domain at all
|
|
},
|
|
"example.com": {
|
|
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com user", "example.com password")),
|
|
},
|
|
"example.com/foo": {
|
|
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(1),
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com/foo user", "example.com/foo password")),
|
|
},
|
|
"example.com/foo/bar": {
|
|
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(2),
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com/foo/bar user", "example.com/foo/bar password")),
|
|
},
|
|
"example.com/foo/not-bar": {
|
|
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(1),
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com/foo user", "example.com/foo password")),
|
|
},
|
|
"example.com/not-foo/not-bar": {
|
|
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("example.com user", "example.com password")),
|
|
},
|
|
"example.net": {
|
|
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
"example.net/foo": {
|
|
wantSpecificity: ociauthconfig.RepositoryCredentialsSpecificity(1),
|
|
wantCredentials: ptrTo(ociauthconfig.NewOAuthCredentials("example.net/foo access", "example.net/foo refresh")),
|
|
},
|
|
"example.net/not-foo": {
|
|
wantSpecificity: ociauthconfig.DomainCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
},
|
|
},
|
|
"explicit-global-credhelper-linux": {
|
|
wantConfigLocations: []string{
|
|
"oci_default_credentials block",
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.com/foo/bar": {
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.com")),
|
|
},
|
|
"example.net": {
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
},
|
|
},
|
|
|
|
// These ambient-global-credhelper- tests are the main way we're exercising
|
|
// the code that deals with finding Docker-style configuration files, since
|
|
// a global credentials helper gives us a straightforward signal about whether
|
|
// we found the file or not. We don't need to duplicate all of these different
|
|
// search cases for other kinds of credentials source because we use the
|
|
// same discovery code regardless of what kind of configuration we might
|
|
// find in the files we find.
|
|
//
|
|
// Note that the logic for deciding which paths to search for Docker-like
|
|
// configuration files has its own unit test in package ociauthconfig, and so
|
|
// we don't necessarily need to cover all of the combinations of different file
|
|
// paths here too.
|
|
"ambient-global-credhelper-xdgconfig-linux": {
|
|
wantConfigLocations: []string{
|
|
filepath.Join("xdgconfig", "containers", "auth.json"),
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.com/foo/bar": {
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.com")),
|
|
},
|
|
"example.net": {
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
},
|
|
},
|
|
"ambient-global-credhelper-xdgrun-linux": {
|
|
wantConfigLocations: []string{
|
|
filepath.Join("xdgrun", "containers", "auth.json"),
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.net": {
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
},
|
|
},
|
|
"ambient-global-credhelper-xdgdefault-linux": {
|
|
wantConfigLocations: []string{
|
|
filepath.Join("home", ".config", "containers", "auth.json"),
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.net": {
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
},
|
|
},
|
|
"ambient-global-credhelper-docker-linux": {
|
|
wantConfigLocations: []string{
|
|
filepath.Join("home", ".docker", "config.json"),
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.net": {
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
},
|
|
},
|
|
"ambient-global-credhelper-dockerlegacy-linux": {
|
|
wantConfigLocations: []string{
|
|
filepath.Join("home", ".dockercfg"),
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.net": {
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
},
|
|
},
|
|
"ambient-global-credhelper-explicitpath-linux": {
|
|
wantConfigLocations: []string{
|
|
"explicitly-named.json",
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.net": {
|
|
// This should select the "fake" credential helper from the first
|
|
// location above, and thus ignore all of the other credential
|
|
// helper names configured in the other configuration files.
|
|
// If this fails trying to use a different credentials helper
|
|
// then that suggests that we're not respecting file preference order.
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
},
|
|
},
|
|
"ambient-global-credhelper-various-linux": {
|
|
wantConfigLocations: []string{
|
|
filepath.Join("xdgrun", "containers", "auth.json"),
|
|
// home/.config/containers/auth.json is ignored on Linux whenever XDG_CONFIG_HOME is set
|
|
filepath.Join("xdgconfig", "containers", "auth.json"),
|
|
filepath.Join("home", ".docker", "config.json"),
|
|
filepath.Join("home", ".dockercfg"),
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.net": {
|
|
// This should select the "fake" credential helper from the first
|
|
// location above, and thus ignore all of the other credential
|
|
// helper names configured in the other configuration files.
|
|
// If this fails trying to use a different credentials helper
|
|
// then that suggests that we're not respecting file preference order.
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
},
|
|
},
|
|
"ambient-global-credhelper-various-windows": {
|
|
wantConfigLocations: []string{
|
|
// the xdgrun/containers/auth.json file is ignored on Windows
|
|
filepath.Join("home", ".config", "containers", "auth.json"),
|
|
filepath.Join("xdgconfig", "containers", "auth.json"),
|
|
filepath.Join("home", ".docker", "config.json"),
|
|
filepath.Join("home", ".dockercfg"),
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.net": {
|
|
// This should select the "fake" credential helper from the first
|
|
// location above, and thus ignore all of the other credential
|
|
// helper names configured in the other configuration files.
|
|
// If this fails trying to use a different credentials helper
|
|
// then that suggests that we're not respecting file preference order.
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
},
|
|
},
|
|
"ambient-global-credhelper-various-darwin": {
|
|
wantConfigLocations: []string{
|
|
// the xdgrun/containers/auth.json file is ignored on macOS
|
|
filepath.Join("home", ".config", "containers", "auth.json"),
|
|
filepath.Join("xdgconfig", "containers", "auth.json"),
|
|
filepath.Join("home", ".docker", "config.json"),
|
|
filepath.Join("home", ".dockercfg"),
|
|
},
|
|
subtests: map[string]Subtest{
|
|
"example.net": {
|
|
// This should select the "fake" credential helper from the first
|
|
// location above, and thus ignore all of the other credential
|
|
// helper names configured in the other configuration files.
|
|
// If this fails trying to use a different credentials helper
|
|
// then that suggests that we're not respecting file preference order.
|
|
wantSpecificity: ociauthconfig.GlobalCredentialsSpecificity,
|
|
wantCredentials: ptrTo(ociauthconfig.NewBasicAuthCredentials("from-cred-helper", "for https://example.net")),
|
|
},
|
|
},
|
|
},
|
|
"ambient-totally-disabled-linux": {
|
|
wantConfigLocations: nil,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
var osName string
|
|
if lastDashIdx := strings.LastIndexByte(name, '-'); lastDashIdx == -1 {
|
|
t.Fatalf("test name does not include -osname suffix")
|
|
} else {
|
|
osName = name[lastDashIdx+1:]
|
|
}
|
|
|
|
configDir := filepath.Join(fixturesDir, name)
|
|
absConfigDir, err := filepath.Abs(configDir)
|
|
if err != nil {
|
|
t.Fatalf("can't get absolute path for %s", configDir)
|
|
}
|
|
cfg, diags := loadConfigDir(configDir)
|
|
if diags.HasErrors() {
|
|
t.Fatalf("errors loading config: %s", diags.Err().Error())
|
|
}
|
|
diags = cfg.Validate()
|
|
if diags.HasErrors() {
|
|
t.Fatalf("invalid config:\n%s", diags.Err().Error())
|
|
}
|
|
|
|
baseDir, err := filepath.Abs(configDir)
|
|
if err != nil {
|
|
t.Fatalf("cannot make path %q absolute: %s", configDir, err)
|
|
}
|
|
discoEnv := &fakeOCIConfigDiscoveryEnvironment{
|
|
osName: osName,
|
|
homePath: filepath.Join(baseDir, "home"),
|
|
xdgConfigHome: filepath.Join(baseDir, "xdgconfig"),
|
|
xdgRuntimeDir: filepath.Join(baseDir, "xdgrun"),
|
|
}
|
|
if _, err = os.Stat(discoEnv.xdgConfigHome); os.IsNotExist(err) {
|
|
discoEnv.xdgConfigHome = ""
|
|
}
|
|
if _, err = os.Stat(discoEnv.xdgRuntimeDir); os.IsNotExist(err) {
|
|
discoEnv.xdgRuntimeDir = ""
|
|
}
|
|
policy, err := cfg.ociCredentialsPolicy(t.Context(), discoEnv)
|
|
if err != nil {
|
|
t.Fatalf("error building credentials policy: %s", err)
|
|
}
|
|
var gotConfigLocations []string
|
|
for _, credCfg := range policy.AllConfigs() {
|
|
loc := credCfg.CredentialsConfigLocationForUI()
|
|
// Sometimes the locations are absolute file paths, so we'll make a best effort
|
|
// to re-relativize them so that our test table doesn't need to deal with variations
|
|
// in base directory between different dev environments. If this doesn't work then
|
|
// we'll assume it's a non-filepath-based location string, which is fine.
|
|
if relLoc, err := filepath.Rel(absConfigDir, loc); err == nil {
|
|
loc = relLoc
|
|
}
|
|
gotConfigLocations = append(gotConfigLocations, loc)
|
|
}
|
|
if diff := cmp.Diff(test.wantConfigLocations, gotConfigLocations); diff != "" {
|
|
t.Error("wrong configuration locations\n" + diff)
|
|
}
|
|
|
|
for subname, subtest := range test.subtests {
|
|
t.Run(subname, func(t *testing.T) {
|
|
registryDomain, repositoryPath, err := ociauthconfig.ParseRepositoryAddressPrefix(subname)
|
|
if err != nil {
|
|
t.Fatalf("subtest has invalid repository address: %s", err)
|
|
}
|
|
|
|
credSrc, err := policy.CredentialsSourceForRepository(t.Context(), registryDomain, repositoryPath)
|
|
if ociauthconfig.IsCredentialsNotFoundError(err) {
|
|
t.Logf("no credentials found: %s", err.Error())
|
|
if subtest.wantCredentials != nil {
|
|
t.Errorf("no credentials returned, but want %s", spew.Sdump(subtest.wantCredentials))
|
|
}
|
|
return // successfully found no credentials, as expected
|
|
} else if err != nil {
|
|
// This test is only for valid cases, so any other error is a test failure.
|
|
t.Fatalf("failed to get credentials source: %s", err)
|
|
}
|
|
if got, want := credSrc.CredentialsSpecificity(), subtest.wantSpecificity; got != want {
|
|
t.Errorf("wrong specificity\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
t.Logf("found credentials for %s/%s at specificity %#v", registryDomain, repositoryPath, credSrc.CredentialsSpecificity())
|
|
|
|
lookupEnv := &fakeOCICredLookupEnvironment{
|
|
regDomain: registryDomain,
|
|
}
|
|
creds, err := credSrc.Credentials(t.Context(), lookupEnv)
|
|
if ociauthconfig.IsCredentialsNotFoundError(err) {
|
|
t.Logf("no credentials found: %s", err.Error())
|
|
} else if err != nil && !ociauthconfig.IsCredentialsNotFoundError(err) {
|
|
// This test is only for valid cases, so any other error is a test failure.
|
|
t.Fatalf("failed to get credentials source: %s", err)
|
|
}
|
|
if diff := cmp.Diff(subtest.wantCredentials, &creds, cmpopts.EquateComparable(ociauthconfig.Credentials{})); diff != "" {
|
|
t.Error("wrong credentials\n" + diff)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type fakeOCIConfigDiscoveryEnvironment struct {
|
|
osName string
|
|
homePath string
|
|
xdgConfigHome string
|
|
xdgRuntimeDir string
|
|
}
|
|
|
|
var _ ociauthconfig.ConfigDiscoveryEnvironment = (*fakeOCIConfigDiscoveryEnvironment)(nil)
|
|
|
|
// EnvironmentVariableVal implements ociauthconfig.ConfigDiscoveryEnvironment.
|
|
func (e *fakeOCIConfigDiscoveryEnvironment) EnvironmentVariableVal(name string) string {
|
|
switch name {
|
|
case "XDG_CONFIG_HOME":
|
|
return e.xdgConfigHome
|
|
case "XDG_RUNTIME_DIR":
|
|
return e.xdgRuntimeDir
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// OperatingSystemName implements ociauthconfig.ConfigDiscoveryEnvironment.
|
|
func (e *fakeOCIConfigDiscoveryEnvironment) OperatingSystemName() string {
|
|
return e.osName
|
|
}
|
|
|
|
// ReadFile implements ociauthconfig.ConfigDiscoveryEnvironment.
|
|
func (e *fakeOCIConfigDiscoveryEnvironment) ReadFile(_ context.Context, path string) ([]byte, error) {
|
|
// We don't fake out the actual file reads, because we assume that the
|
|
// tests will ensure that all of the paths reported by other methods
|
|
// are absolute paths pointing into a test fixture directory.
|
|
return os.ReadFile(path)
|
|
}
|
|
|
|
// UserHomeDirPath implements ociauthconfig.ConfigDiscoveryEnvironment.
|
|
func (e *fakeOCIConfigDiscoveryEnvironment) UserHomeDirPath() string {
|
|
return e.homePath
|
|
}
|
|
|
|
type fakeOCICredLookupEnvironment struct {
|
|
regDomain string
|
|
}
|
|
|
|
// QueryDockerCredentialHelper implements ociauthconfig.CredentialsLookupEnvironment.
|
|
func (f *fakeOCICredLookupEnvironment) QueryDockerCredentialHelper(ctx context.Context, helperName string, serverURL string) (ociauthconfig.DockerCredentialHelperGetResult, error) {
|
|
if helperName != "fake" {
|
|
return ociauthconfig.DockerCredentialHelperGetResult{}, ociauthconfig.NewCredentialsNotFoundError(fmt.Errorf("only the 'fake' credential helper is available in this testing environment"))
|
|
}
|
|
if got, want := serverURL, "https://"+f.regDomain; got != want {
|
|
return ociauthconfig.DockerCredentialHelperGetResult{}, ociauthconfig.NewCredentialsNotFoundError(fmt.Errorf("fake credentials helper only has credentials for %s", want))
|
|
}
|
|
return ociauthconfig.DockerCredentialHelperGetResult{
|
|
ServerURL: serverURL,
|
|
Username: "from-cred-helper",
|
|
Secret: "for " + serverURL,
|
|
}, nil
|
|
}
|
|
|
|
// ptrTo is a helper to compensate for the fact that Go doesn't allow
|
|
// using the '&' operator unless the operand is directly addressable.
|
|
//
|
|
// Instead then, this function returns a pointer to a copy of the given
|
|
// value.
|
|
func ptrTo[T any](v T) *T {
|
|
return &v
|
|
}
|