refactor: split coderd/gitauth into two, add cli/gitauth (#9479)

* refactor: split coderd/gitauth into two, add cli/gitauth

Ref: #9380
This commit is contained in:
Mathias Fredriksson
2023-09-01 18:41:22 +03:00
committed by GitHub
parent d8718c3818
commit 702b064cac
7 changed files with 5 additions and 5 deletions

View File

@ -1,70 +0,0 @@
package gitauth
import (
"net/url"
"regexp"
"strings"
"golang.org/x/xerrors"
)
// https://github.com/microsoft/vscode/blob/328646ebc2f5016a1c67e0b23a0734bd598ec5a8/extensions/git/src/askpass-main.ts#L46
var hostReplace = regexp.MustCompile(`^["']+|["':]+$`)
// CheckCommand returns true if the command arguments and environment
// match those when the GIT_ASKPASS command is invoked by git.
func CheckCommand(args, env []string) bool {
if len(args) != 1 || (!strings.HasPrefix(args[0], "Username ") && !strings.HasPrefix(args[0], "Password ")) {
return false
}
for _, e := range env {
if strings.HasPrefix(e, "GIT_PREFIX=") {
return true
}
}
return false
}
// ParseAskpass returns the user and host from a git askpass prompt. For
// example: "user1" and "https://github.com". Note that for HTTP
// protocols, the URL will never contain a path.
//
// For details on how the prompt is formatted, see `credential_ask_one`:
// https://github.com/git/git/blob/bbe21b64a08f89475d8a3818e20c111378daa621/credential.c#L173-L191
func ParseAskpass(prompt string) (user string, host string, err error) {
parts := strings.Fields(prompt)
if len(parts) < 3 {
return "", "", xerrors.Errorf("askpass prompt must contain 3 words; got %d: %q", len(parts), prompt)
}
switch parts[0] {
case "Username", "Password":
default:
return "", "", xerrors.Errorf("unknown prompt type: %q", prompt)
}
host = parts[2]
host = hostReplace.ReplaceAllString(host, "")
// Validate the input URL to ensure it's in an expected format.
u, err := url.Parse(host)
if err != nil {
return "", "", xerrors.Errorf("parse host failed: %w", err)
}
switch u.Scheme {
case "http", "https":
default:
return "", "", xerrors.Errorf("unsupported scheme: %q", u.Scheme)
}
if u.Host == "" {
return "", "", xerrors.Errorf("host is empty")
}
user = u.User.Username()
u.User = nil
host = u.String()
return user, host, nil
}

View File

@ -1,72 +0,0 @@
package gitauth_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/gitauth"
)
func TestCheckCommand(t *testing.T) {
t.Parallel()
t.Run("Success", func(t *testing.T) {
t.Parallel()
valid := gitauth.CheckCommand([]string{"Username "}, []string{"GIT_PREFIX=/example"})
require.True(t, valid)
})
t.Run("Failure", func(t *testing.T) {
t.Parallel()
valid := gitauth.CheckCommand([]string{}, []string{})
require.False(t, valid)
})
}
func TestParse(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
in string
wantUser string
wantHost string
}{
{
in: "Username for 'https://github.com': ",
wantUser: "",
wantHost: "https://github.com",
},
{
in: "Username for 'https://enterprise.github.com': ",
wantUser: "",
wantHost: "https://enterprise.github.com",
},
{
in: "Username for 'http://wow.io': ",
wantUser: "",
wantHost: "http://wow.io",
},
{
in: "Password for 'https://myuser@github.com': ",
wantUser: "myuser",
wantHost: "https://github.com",
},
{
in: "Password for 'https://myuser@enterprise.github.com': ",
wantUser: "myuser",
wantHost: "https://enterprise.github.com",
},
{
in: "Password for 'http://myuser@wow.io': ",
wantUser: "myuser",
wantHost: "http://wow.io",
},
} {
tc := tc
t.Run(tc.in, func(t *testing.T) {
t.Parallel()
user, host, err := gitauth.ParseAskpass(tc.in)
require.NoError(t, err)
require.Equal(t, tc.wantUser, user)
require.Equal(t, tc.wantHost, host)
})
}
}

View File

@ -1,81 +0,0 @@
package gitauth
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"github.com/adrg/xdg"
"github.com/spf13/afero"
"golang.org/x/xerrors"
)
// OverrideVSCodeConfigs overwrites a few properties to consume
// GIT_ASKPASS from the host instead of VS Code-specific authentication.
func OverrideVSCodeConfigs(fs afero.Fs) error {
home, err := os.UserHomeDir()
if err != nil {
return err
}
mutate := func(m map[string]interface{}) {
// This prevents VS Code from overriding GIT_ASKPASS, which
// we use to automatically authenticate Git providers.
m["git.useIntegratedAskPass"] = false
// This prevents VS Code from using it's own GitHub authentication
// which would circumvent cloning with Coder-configured providers.
m["github.gitAuthentication"] = false
}
for _, configPath := range []string{
// code-server's default configuration path.
filepath.Join(xdg.DataHome, "code-server", "Machine", "settings.json"),
// vscode-remote's default configuration path.
filepath.Join(home, ".vscode-server", "data", "Machine", "settings.json"),
} {
_, err := fs.Stat(configPath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return xerrors.Errorf("stat %q: %w", configPath, err)
}
m := map[string]interface{}{}
mutate(m)
data, err := json.MarshalIndent(m, "", "\t")
if err != nil {
return xerrors.Errorf("marshal: %w", err)
}
err = fs.MkdirAll(filepath.Dir(configPath), 0o700)
if err != nil {
return xerrors.Errorf("mkdir all: %w", err)
}
err = afero.WriteFile(fs, configPath, data, 0o600)
if err != nil {
return xerrors.Errorf("write %q: %w", configPath, err)
}
continue
}
data, err := afero.ReadFile(fs, configPath)
if err != nil {
return xerrors.Errorf("read %q: %w", configPath, err)
}
mapping := map[string]interface{}{}
err = json.Unmarshal(data, &mapping)
if err != nil {
return xerrors.Errorf("unmarshal %q: %w", configPath, err)
}
mutate(mapping)
data, err = json.MarshalIndent(mapping, "", "\t")
if err != nil {
return xerrors.Errorf("marshal %q: %w", configPath, err)
}
err = afero.WriteFile(fs, configPath, data, 0o600)
if err != nil {
return xerrors.Errorf("write %q: %w", configPath, err)
}
}
return nil
}

View File

@ -1,64 +0,0 @@
package gitauth_test
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/adrg/xdg"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/gitauth"
)
func TestOverrideVSCodeConfigs(t *testing.T) {
t.Parallel()
home, err := os.UserHomeDir()
require.NoError(t, err)
configPaths := []string{
filepath.Join(xdg.DataHome, "code-server", "Machine", "settings.json"),
filepath.Join(home, ".vscode-server", "data", "Machine", "settings.json"),
}
t.Run("Create", func(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
err := gitauth.OverrideVSCodeConfigs(fs)
require.NoError(t, err)
for _, configPath := range configPaths {
data, err := afero.ReadFile(fs, configPath)
require.NoError(t, err)
mapping := map[string]interface{}{}
err = json.Unmarshal(data, &mapping)
require.NoError(t, err)
require.Equal(t, false, mapping["git.useIntegratedAskPass"])
require.Equal(t, false, mapping["github.gitAuthentication"])
}
})
t.Run("Append", func(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
mapping := map[string]interface{}{
"hotdogs": "something",
}
data, err := json.Marshal(mapping)
require.NoError(t, err)
for _, configPath := range configPaths {
err = afero.WriteFile(fs, configPath, data, 0o600)
require.NoError(t, err)
}
err = gitauth.OverrideVSCodeConfigs(fs)
require.NoError(t, err)
for _, configPath := range configPaths {
data, err := afero.ReadFile(fs, configPath)
require.NoError(t, err)
mapping := map[string]interface{}{}
err = json.Unmarshal(data, &mapping)
require.NoError(t, err)
require.Equal(t, false, mapping["git.useIntegratedAskPass"])
require.Equal(t, false, mapping["github.gitAuthentication"])
require.Equal(t, "something", mapping["hotdogs"])
}
})
}