Skip to content

Commit

Permalink
WIP: hacking --credential
Browse files Browse the repository at this point in the history
  • Loading branch information
thockin committed Sep 12, 2023
1 parent 5b85926 commit c92bc55
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 9 deletions.
121 changes: 115 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package main // import "k8s.io/git-sync/cmd/git-sync"
import (
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -105,6 +106,13 @@ const (

const defaultDirMode = os.FileMode(0775) // subject to umask

type credential struct {
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`
PasswordFile string `json:"password-file"`
}

func envString(def string, key string, alts ...string) string {
if val := os.Getenv(key); val != "" {
return val
Expand Down Expand Up @@ -135,6 +143,54 @@ func envStringArray(def string, key string, alts ...string) []string {
return parse(def)
}

func envStringArrayJSONOrError(def string, key string, alts ...string) ([]string, error) {
parse := func(key, val string) ([]string, error) {
s := strings.TrimSpace(val)
if s == "" {
return nil, nil
}
// If it tastes like an object...
if s[0] == '{' {
return []string{s}, nil
}
// If it tastes like an array...
if s[0] == '[' {
// Parse into an array of "stuff to decode later".
var arr []json.RawMessage
if err := json.Unmarshal([]byte(s), &arr); err != nil {
return nil, fmt.Errorf("ERROR: invalid JSON list env %s=%q: %w", key, val, err)
}
// Re-encode as []string
ret := []string{}
for _, rm := range arr {
ret = append(ret, string(rm))
}
return ret, nil
}
return nil, fmt.Errorf("ERROR: invalid JSON env %s=%q: not a list or object", key, val)
}

if val := os.Getenv(key); val != "" {
return parse(key, val)
}
for _, alt := range alts {
if val := os.Getenv(alt); val != "" {
fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key)
return parse(alt, val)
}
}
return parse("", def)
}
func envStringArrayJSON(def string, key string, alts ...string) []string {
val, err := envStringArrayJSONOrError(def, key, alts...)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
return nil
}
return val
}

func envBoolOrError(def bool, key string, alts ...string) (bool, error) {
parse := func(key, val string) (bool, error) {
parsed, err := strconv.ParseBool(val)
Expand Down Expand Up @@ -449,6 +505,9 @@ func main() {
flPasswordFile := pflag.String("password-file",
envString("", "GITSYNC_PASSWORD_FILE", "GIT_SYNC_PASSWORD_FILE"),
"the file from which the password or personal access token for git auth will be sourced")
flCredentials := pflag.StringArray("credential",
envStringArrayJSON("", "GITSYNC_CREDENTIAL"),
"one or more credentials (see --man for details) available for authentication")

flSSH := pflag.Bool("ssh",
envBool(false, "GITSYNC_SSH", "GIT_SYNC_SSH"),
Expand Down Expand Up @@ -696,6 +755,33 @@ func main() {
handleConfigError(log, true, "ERROR: --password or --password-file must be set when --username is specified")
}
}
//FIXME: mutex wih flCredentials?

credentials := []credential{}
if len(*flCredentials) > 0 {
for _, s := range *flCredentials {
cred := credential{}
if err := json.Unmarshal([]byte(s), &cred); err != nil {
handleConfigError(log, true, "ERROR: can't parse --credential payload: %v", err)
}
if cred.URL == "" {
//FIXME: can it default to --repo?
handleConfigError(log, true, "ERROR: --credential URL must be specified")
}
if cred.Username == "" {
handleConfigError(log, true, "ERROR: --credential username must be specified")
}
if cred.Password == "" && cred.PasswordFile == "" {
handleConfigError(log, true, "ERROR: --credential password or password-file must be set")
}
if cred.Password != "" && cred.PasswordFile != "" {
handleConfigError(log, true, "ERROR: only one of --credential password and password-file may be specified")
}
//FIXME: askpass for this purpose, too?
//FIXME: make safe for logs
credentials = append(credentials, cred)
}
}

if *flSSH {
if *flUsername != "" {
Expand All @@ -707,6 +793,7 @@ func main() {
if *flPasswordFile != "" {
handleConfigError(log, true, "ERROR: only one of --ssh and --password-file may be specified")
}
//FIXME: mutex wih flCredentials?
if *flAskPassURL != "" {
handleConfigError(log, true, "ERROR: only one of --ssh and --askpass-url may be specified")
}
Expand Down Expand Up @@ -825,6 +912,7 @@ func main() {
os.Exit(1)
}

// FIXME: merge into flCredentials
if *flUsername != "" {
if *flPasswordFile != "" {
passwordFileBytes, err := os.ReadFile(*flPasswordFile)
Expand All @@ -835,6 +923,17 @@ func main() {
*flPassword = string(passwordFileBytes)
}
}
//FIXME: merge
for _, cred := range credentials {
if cred.PasswordFile != "" {
passwordFileBytes, err := os.ReadFile(cred.PasswordFile)
if err != nil {
log.Error(err, "can't read password file", "file", cred.PasswordFile)
os.Exit(1)
}
cred.Password = string(passwordFileBytes)
}
}

if *flSSH {
if err := git.SetupGitSSH(*flSSHKnownHosts, *flSSHKeyFiles, *flSSHKnownHostsFile); err != nil {
Expand Down Expand Up @@ -956,9 +1055,15 @@ func main() {

// Craft a function that can be called to refresh credentials when needed.
refreshCreds := func(ctx context.Context) error {
//FIXME: still mutually exclusive?
// These should all be mutually-exclusive configs.
if *flUsername != "" {
if err := git.StoreCredentials(ctx, *flUsername, *flPassword); err != nil {
if err := git.StoreCredentials(ctx, git.repo, *flUsername, *flPassword); err != nil {
return err
}
}
for _, cred := range credentials {
if err := git.StoreCredentials(ctx, cred.URL, cred.Username, cred.Password); err != nil {
return err
}
}
Expand Down Expand Up @@ -1930,12 +2035,12 @@ func md5sum(s string) string {
return fmt.Sprintf("%x", h.Sum(nil))
}

// StoreCredentials stores the username and password for later use.
func (git *repoSync) StoreCredentials(ctx context.Context, username, password string) error {
// StoreCredentials stores a username and password for later use.
func (git *repoSync) StoreCredentials(ctx context.Context, url, username, password string) error {
git.log.V(1).Info("storing git credentials")
git.log.V(9).Info("md5 of credentials", "username", md5sum(username), "password", md5sum(password))
git.log.V(9).Info("md5 of credentials", "url", url, "username", md5sum(username), "password", md5sum(password))

creds := fmt.Sprintf("url=%v\nusername=%v\npassword=%v\n", git.repo, username, password)
creds := fmt.Sprintf("url=%v\nusername=%v\npassword=%v\n", url, username, password)
_, _, err := git.RunWithStdin(ctx, "", creds, "credential", "approve")
if err != nil {
return fmt.Errorf("can't configure git credentials: %w", err)
Expand Down Expand Up @@ -2049,7 +2154,8 @@ func (git *repoSync) CallAskPassURL(ctx context.Context) error {
}
}

if err := git.StoreCredentials(ctx, username, password); err != nil {
//FIXME: support multiple?
if err := git.StoreCredentials(ctx, git.repo, username, password); err != nil {
return err
}

Expand Down Expand Up @@ -2329,6 +2435,9 @@ OPTIONS
Use a git cookiefile (/etc/git-secret/cookie_file) for
authentication.
--credential <string>, $GITSYNC_CREDENTIAL
FIXME
--depth <int>, $GITSYNC_DEPTH
Create a shallow clone with history truncated to the specified
number of commits. If not specified, this defaults to syncing a
Expand Down
39 changes: 39 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,45 @@ func TestEnvDuration(t *testing.T) {
}
}

func TestEnvStringArrayJSON(t *testing.T) {
mkslice := func(args ...string) []string {
return args
}

cases := []struct {
value string
def string
exp []string
err bool
}{
{"", "", nil, false},
{" ", "", nil, false},
{"", `{"a string"}`, mkslice(`{"a string"}`), false},
{"{}", "", mkslice("{}"), false},
{" {} ", "", mkslice("{}"), false},
{`{"a string"}`, "", mkslice(`{"a string"}`), false},
{"[]", "", []string{}, false},
{" [] ", "", []string{}, false},
{`["a string"]`, "", mkslice(`"a string"`), false},
{`[{"a": "string"}]`, "", mkslice(`{"a": "string"}`), false},
{`[{"a": "string"}, {"b": "smart"}]`, "", mkslice(`{"a": "string"}`, `{"b": "smart"}`), false},
{"a string", "", nil, true},
}

for _, testCase := range cases {
os.Setenv(testKey, testCase.value)
val, err := envStringArrayJSONOrError(testCase.def, testKey)
if err != nil && !testCase.err {
t.Fatalf("%q: unexpected error: %v", testCase.value, err)
}
if err == nil && testCase.err {
t.Fatalf("%q: unexpected success", testCase.value)
}
if !reflect.DeepEqual(val, testCase.exp) {
t.Fatalf("%q: expected %+#v but %+#v returned", testCase.value, testCase.exp, val)
}
}
}
func TestMakeAbsPath(t *testing.T) {
cases := []struct {
path string
Expand Down
6 changes: 3 additions & 3 deletions test_e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2879,7 +2879,6 @@ function e2e::submodule_sync_over_http_different_passwords() {
git -C "$NESTED_SUBMODULE" add nested-submodule.file
git -C "$NESTED_SUBMODULE" commit -aqm "init nested-submodule.file"

set -x
# Run a git-over-SSH server. Use password "test1".
echo 'test:$apr1$cXiFWR90$Pmoz7T8kEmlpC9Bpj4MX3.' > "$WORK/htpasswd.1"
CTR_SUBSUB=$(docker_run \
Expand Down Expand Up @@ -2931,8 +2930,9 @@ function e2e::submodule_sync_over_http_different_passwords() {
--repo="http://$IP/repo" \
--root="$ROOT" \
--link="link" \
--username="test" \
--password="test3" \
--credential="{ \"url\": \"http://$IP_SUBSUB/repo\", \"username\": \"test\", \"password\": \"test1\" }" \
--credential="{ \"url\": \"http://$IP_SUB/repo\", \"username\": \"test\", \"password\": \"test2\" }" \
--credential="{ \"url\": \"http://$IP/repo\", \"username\": \"test\", \"password\": \"test3\" }" \
&
wait_for_sync "${MAXWAIT}"
assert_link_exists "$ROOT/link"
Expand Down

0 comments on commit c92bc55

Please sign in to comment.