mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 09:48:32 -05:00
main: Docker credential helper support for OCI registry auth
Our OCI credentials policy layer expects to be provided with an implementation of the Docker credential helper protocol as part of its "credentials lookup environment". Since we're already using ORAS-Go for everything else we'll just wrap their implementation of this protocol here too, and then translate the result into our own type since we've been intentionally avoiding making ORAS-Go types part of any of our exported package APIs. Because this is the concrete implementation of an interface we introduced so that unit tests elsewhere could fake it, it's pretty awkward to fully test this implementation without the overhead of having a test build its own credential helper executable dynamically to run on the platform where the test program is running. ORAS-Go already has its own tests for this functionality, so as a pragmatic compromise here we just focus on testing that we're attempting to run the executable that the protocol expects us to execute, but detecting that through an error result rather than through a success result. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
@@ -12,9 +12,12 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
orasRemote "oras.land/oras-go/v2/registry/remote"
|
||||
orasAuth "oras.land/oras-go/v2/registry/remote/auth"
|
||||
orasCreds "oras.land/oras-go/v2/registry/remote/credentials"
|
||||
orasCredsTrace "oras.land/oras-go/v2/registry/remote/credentials/trace"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/command/cliconfig/ociauthconfig"
|
||||
"github.com/opentofu/opentofu/internal/getmodules"
|
||||
@@ -114,6 +117,44 @@ var _ ociauthconfig.CredentialsLookupEnvironment = ociCredentialsLookupEnv{}
|
||||
|
||||
// QueryDockerCredentialHelper implements ociauthconfig.CredentialsLookupEnvironment.
|
||||
func (o ociCredentialsLookupEnv) QueryDockerCredentialHelper(ctx context.Context, helperName string, serverURL string) (ociauthconfig.DockerCredentialHelperGetResult, error) {
|
||||
// TODO: Implement this
|
||||
return ociauthconfig.DockerCredentialHelperGetResult{}, fmt.Errorf("support for Docker-style credential helpers is not yet available")
|
||||
// (just because this type name is very long to keep repeating in full)
|
||||
type Result = ociauthconfig.DockerCredentialHelperGetResult
|
||||
|
||||
// We currently use the ORAS-Go implementation of the Docker
|
||||
// credential helper protocol, because we already depend on
|
||||
// that library for our OCI registry interactions elsewhere.
|
||||
// ORAS refers to this protocol as "native store", rather
|
||||
// than "Docker-style Credential Helper", but it's the
|
||||
// same protocol nonetheless.
|
||||
|
||||
ctx = orasCredsTrace.WithExecutableTrace(ctx, &orasCredsTrace.ExecutableTrace{
|
||||
ExecuteStart: func(executableName, action string) {
|
||||
log.Printf("[DEBUG] Executing docker-style credentials helper %q for %s", helperName, serverURL)
|
||||
},
|
||||
ExecuteDone: func(executableName, action string, err error) {
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Docker-style credential helper %q failed for %s: %s", helperName, serverURL, err)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
store := orasCreds.NewNativeStore(helperName)
|
||||
creds, err := store.Get(ctx, serverURL)
|
||||
if err != nil {
|
||||
return Result{}, fmt.Errorf("%q credential helper failed: %w", helperName, err)
|
||||
}
|
||||
if creds.AccessToken != "" || creds.RefreshToken != "" {
|
||||
// A little awkward: orasAuth.Credential is a more general type than
|
||||
// what the Docker credential helper needs: it has fields for OAuth-style
|
||||
// credentials even though the credential helper protocol only supports
|
||||
// username/password style. So for completeness/robustness we check
|
||||
// the OAuth fields and fail if they are set, but it should not actually
|
||||
// be possible for them to be set in practice.
|
||||
return Result{}, fmt.Errorf("%q credential helper returned OAuth-style credentials, but only username/password-style is allowed from a credential helper", helperName)
|
||||
}
|
||||
return Result{
|
||||
ServerURL: serverURL,
|
||||
Username: creds.Username,
|
||||
Secret: creds.Password,
|
||||
}, nil
|
||||
}
|
||||
|
||||
53
cmd/tofu/oci_distribution_test.go
Normal file
53
cmd/tofu/oci_distribution_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOCICredentialsLookupEnv_DockerCredHelper(t *testing.T) {
|
||||
// This ociCredentialsLookupEnv is the concrete implementation of
|
||||
// the abstraction that is created in package ociauthconfig, and
|
||||
// so it depends directly on a real (non-substitutable)
|
||||
// implementation of Docker credential helpers.
|
||||
//
|
||||
// Properly testing this would require a functioning credential
|
||||
// helper executable, which is difficult to arrange for in a
|
||||
// portable manner to allow this test to run across multiple
|
||||
// platforms. It's just a thin wrapper around the ORAS-Go
|
||||
// implementation anyway, and that has its own tests upstream
|
||||
// so this test just settles for the compromise of making a
|
||||
// call that we expect to fail and checking that it fails in
|
||||
// the way we expect, to give confidence that we really did
|
||||
// ask the ORAS-Go library to attempt to fetch credentials.
|
||||
|
||||
// To prevent this from accidentally executing some real
|
||||
// credential helper that might be installed on the system
|
||||
// where the tests are running, we'll temporarily override
|
||||
// the PATH environment variable to include only an empty
|
||||
// directory.
|
||||
emptyDir := os.TempDir()
|
||||
os.Setenv("PATH", emptyDir)
|
||||
|
||||
env := ociCredentialsLookupEnv{}
|
||||
_, err := env.QueryDockerCredentialHelper(t.Context(), "fake-for-testing", "https://example.com")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("unexpected success; want error")
|
||||
}
|
||||
|
||||
// The exact details of the error message can vary between
|
||||
// platforms, but it should always mention that it was
|
||||
// trying to execute the specified credential helper
|
||||
// executable.
|
||||
wantErr := `docker-credential-fake-for-testing`
|
||||
if gotErr := err.Error(); !strings.Contains(gotErr, wantErr) {
|
||||
t.Errorf("wrong error\ngot: %s\nwant substring: %s", gotErr, wantErr)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user