mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
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:
committed by
GitHub
parent
d8718c3818
commit
702b064cac
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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"])
|
||||
}
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user