Skip to content

Commit

Permalink
feat: Handling git credentials via credential helper
Browse files Browse the repository at this point in the history
  • Loading branch information
hferentschik committed Jan 20, 2020
1 parent 9345f7f commit d2c8419
Show file tree
Hide file tree
Showing 12 changed files with 667 additions and 55 deletions.
59 changes: 40 additions & 19 deletions pkg/cmd/step/git/credentials/step_git_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import (
"bytes"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"

"github.com/jenkins-x/jx/pkg/auth"
"github.com/jenkins-x/jx/pkg/cmd/opts/step"
"github.com/jenkins-x/jx/pkg/gits/credentialhelper"
"github.com/pkg/errors"

"github.com/jenkins-x/jx/pkg/cmd/helper"
Expand Down Expand Up @@ -37,6 +37,7 @@ type StepGitCredentialsOptions struct {
GitHubAppOwner string
GitKind string
CredentialsSecret string
CredentialHelper bool
}

type credentials struct {
Expand All @@ -57,6 +58,9 @@ var (
# generate the Git credentials to a output file
jx step git credentials -o /tmp/mycreds
# respond to a gitcredentials request
jx step git credentials --credential-helper
`)
)

Expand All @@ -82,6 +86,7 @@ func NewCmdStepGitCredentials(commonOpts *opts.CommonOptions) *cobra.Command {
cmd.Flags().StringVarP(&options.GitHubAppOwner, optionGitHubAppOwner, "g", "", "The owner (organisation or user name) if using GitHub App based tokens")
cmd.Flags().StringVarP(&options.CredentialsSecret, "credentials-secret", "s", "", "The secret name to read the credentials from")
cmd.Flags().StringVarP(&options.GitKind, "git-kind", "", "", "The git kind. e.g. github, bitbucketserver etc")
cmd.Flags().BoolVar(&options.CredentialHelper, "credential-helper", false, "respond to a gitcredentials request")
return cmd
}

Expand All @@ -108,13 +113,12 @@ func (o *StepGitCredentialsOptions) Run() error {
return errors.Wrapf(err, "failed to find secret '%s' in namespace '%s'", o.CredentialsSecret, ns)
}

creds := credentials{
user: string(secret.Data["user"]),
password: string(secret.Data["token"]),
serviceURL: string(secret.Data["url"]),
creds, err := credentialhelper.CreateGitCredentialFromURL(string(secret.Data["url"]), string(secret.Data["token"]), string(secret.Data["user"]))
if err != nil {
return errors.Wrap(err, "failed to create git credentials")
}

return o.createGitCredentialsFile(outFile, []credentials{creds})
return o.createGitCredentialsFile(outFile, []credentialhelper.GitCredential{creds})
}

gha, err := o.IsGitHubAppMode()
Expand Down Expand Up @@ -144,19 +148,37 @@ func (o *StepGitCredentialsOptions) Run() error {
if err != nil {
return errors.Wrap(err, "creating git credentials")
}
return o.createGitCredentialsFile(outFile, credentials)

if o.CredentialHelper {
helper, err := credentialhelper.CreateGitCredentialsHelper(os.Stdin, os.Stdout, credentials)
if err != nil {
return errors.Wrap(err, "unable to create git credential helper")
}
// the credential helper operation (get|store|remove) is passed as last argument to the helper
err = helper.Run(os.Args[len(os.Args)-1])
if err != nil {
return errors.Wrap(err, "error responding to git credential request")
}
return nil
} else {
outFile, err := o.determineOutputFile()
if err != nil {
return err
}

return o.createGitCredentialsFile(outFile, credentials)
}
}

func (o *StepGitCredentialsOptions) GitCredentialsFileData(credentials []credentials) ([]byte, error) {
func (o *StepGitCredentialsOptions) GitCredentialsFileData(credentials []credentialhelper.GitCredential) ([]byte, error) {
var buffer bytes.Buffer
for _, creds := range credentials {
u, err := url.Parse(creds.serviceURL)
for _, gitCredential := range credentials {
u, err := gitCredential.URL()
if err != nil {
log.Logger().Warnf("Ignoring invalid git service URL %q", creds.serviceURL)
log.Logger().Warnf("Ignoring incomplete git credentials %q", gitCredential)
continue
}

u.User = url.UserPassword(creds.user, creds.password)
buffer.WriteString(u.String() + "\n")
// Write the https protocol in case only https is set for completeness
if u.Scheme == "http" {
Expand Down Expand Up @@ -185,7 +207,7 @@ func (o *StepGitCredentialsOptions) determineOutputFile() (string, error) {
}

// CreateGitCredentialsFileFromUsernameAndToken creates the git credentials into file using the provided username, token & url
func (o *StepGitCredentialsOptions) createGitCredentialsFile(fileName string, credentials []credentials) error {
func (o *StepGitCredentialsOptions) createGitCredentialsFile(fileName string, credentials []credentialhelper.GitCredential) error {
data, err := o.GitCredentialsFileData(credentials)
if err != nil {
return errors.Wrap(err, "creating git credentials")
Expand All @@ -199,8 +221,8 @@ func (o *StepGitCredentialsOptions) createGitCredentialsFile(fileName string, cr
}

// CreateGitCredentialsFromAuthService creates the git credentials using the auth config service
func (o *StepGitCredentialsOptions) CreateGitCredentialsFromAuthService(authConfigSvc auth.ConfigService) ([]credentials, error) {
var credentialList []credentials
func (o *StepGitCredentialsOptions) CreateGitCredentialsFromAuthService(authConfigSvc auth.ConfigService) ([]credentialhelper.GitCredential, error) {
var credentialList []credentialhelper.GitCredential

cfg := authConfigSvc.Config()
if cfg == nil {
Expand Down Expand Up @@ -236,10 +258,9 @@ func (o *StepGitCredentialsOptions) CreateGitCredentialsFromAuthService(authConf
continue
}

credential := credentials{
user: username,
password: password,
serviceURL: server.URL,
credential, err := credentialhelper.CreateGitCredentialFromURL(server.URL, username, password)
if err != nil {
return nil, errors.Wrapf(err, "invalid git auth information")
}

credentialList = append(credentialList, credential)
Expand Down
8 changes: 2 additions & 6 deletions pkg/cmd/step/verify/step_verify_environments.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,8 @@ func (o *StepVerifyEnvironmentsOptions) pushDevEnvironmentUpdates(environmentRep
}
}

userDetails := provider.UserAuth()
authenticatedPushURL, err := gitter.CreateAuthenticatedURL(environmentRepo.CloneURL, &userDetails)
if err != nil {
return errors.Wrapf(err, "failed to create push URL for %s", environmentRepo.CloneURL)
}
err = gitter.Push(localRepoDir, authenticatedPushURL, true, "master")
// TODO issue-5772 determine final remote
err = gitter.Push(localRepoDir, "origin", true, "master")
if err != nil {
return errors.Wrapf(err, "unable to push %s to %s", localRepoDir, environmentRepo.URL)
}
Expand Down
122 changes: 122 additions & 0 deletions pkg/gits/credentialhelper/git_credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package credentialhelper

import (
"fmt"
"net/url"
"reflect"
"strings"

"github.com/jenkins-x/jx/pkg/util"
"github.com/pkg/errors"
)

// GitCredential represents the different parts of a git credential URL
// See also https://git-scm.com/docs/git-credential
type GitCredential struct {
Protocol string
Host string
Path string
Username string
Password string
}

// CreateGitCredential creates a CreateGitCredential instance from a slice of strings where each element is a key/value pair
// separated by '='.
func CreateGitCredential(lines []string) (GitCredential, error) {
var credential GitCredential

if lines == nil {
return credential, errors.New("no data lines provided")
}

fieldMap, err := util.ExtractKeyValuePairs(lines, "=")
if err != nil {
return credential, errors.Wrap(err, "unable to extract git credential parameters")
}
for key, value := range fieldMap {
v := reflect.ValueOf(&credential).Elem().FieldByName(strings.Title(key))
if v.IsValid() {
v.SetString(value)
} else {
return GitCredential{}, errors.Errorf("invalid key/value %s/%s", key, value)
}
}

return credential, nil
}

// CreateGitCredentialFromURL creates a CreateGitCredential instance from a URL and optional username and password.
func CreateGitCredentialFromURL(gitURL string, username string, password string) (GitCredential, error) {
var credential GitCredential

if gitURL == "" {
return credential, errors.New("url cannot be empty")
}

u, err := url.Parse(gitURL)
if err != nil {
return credential, errors.Wrapf(err, "unable to parse URL %s", gitURL)
}

credential.Protocol = u.Scheme
credential.Host = u.Host
credential.Path = u.Path
if username != "" {
credential.Username = username
}

if password != "" {
credential.Password = password
}

return credential, nil
}

// String returns a string representation of this instance according to the expected format of git credential helpers.
// See also https://git-scm.com/docs/git-credential
func (g *GitCredential) String() string {
answer := ""

value := reflect.ValueOf(g).Elem()
typeOfT := value.Type()

for i := 0; i < value.NumField(); i++ {
field := value.Field(i)
answer = answer + fmt.Sprintf("%s=%v\n", strings.ToLower(typeOfT.Field(i).Name), field.Interface())
}

answer = answer + "\n"

return answer
}

// Clones this GitCredential instance
func (g *GitCredential) Clone() GitCredential {
clone := GitCredential{}

value := reflect.ValueOf(g).Elem()
typeOfT := value.Type()
for i := 0; i < value.NumField(); i++ {
field := value.Field(i)
value := field.String()
v := reflect.ValueOf(&clone).Elem().FieldByName(typeOfT.Field(i).Name)
v.SetString(value)
}

return clone
}

// URL returns a URL from the data of this instance. If not enough information exist an error is returned
func (g *GitCredential) URL() (url.URL, error) {
urlAsString := g.Protocol + "://" + g.Host
if g.Path != "" {
urlAsString = urlAsString + "/" + g.Path
}
u, err := url.Parse(urlAsString)
if err != nil {
return url.URL{}, errors.Wrap(err, "unable to construct URL")
}

u.User = url.UserPassword(g.Username, g.Password)
return *u, nil
}
107 changes: 107 additions & 0 deletions pkg/gits/credentialhelper/git_credentialhelper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package credentialhelper

import (
"bufio"
"fmt"
"io"

"github.com/pkg/errors"
)

type GitCredentialsHelper struct {
in io.Reader
out io.Writer
knownCredentials []GitCredential
}

// CreateGitCredentialsHelper creates an instance of a git credential helper. It needs to get passed the handles to read
// the git credential data as well as write the response to. It also gets the list og known credentials.
func CreateGitCredentialsHelper(in io.Reader, out io.Writer, credentials []GitCredential) (*GitCredentialsHelper, error) {
if in == nil {
return nil, errors.New("in parameter cannot be nil")
}

if out == nil {
return nil, errors.New("out parameter cannot be nil")
}

if credentials == nil {
return nil, errors.New("credentials parameter cannot be nil")
}

return &GitCredentialsHelper{
in: in,
out: out,
knownCredentials: credentials,
}, nil
}

// Run executes the specified git credential helper operation which must be one of get, store or erase.
// NOTE: Currently only get is implemented.
func (h *GitCredentialsHelper) Run(op string) error {
var err error

switch op {
case "get":
err = h.Get()
case "store":
// not yet implemented (HF)
fmt.Println("")
case "erase":
// not yet implemented (HF)
fmt.Println("")
default:
err = errors.Errorf("Invalid git credential operation '%s'", op)
}

return err
}

func (h *GitCredentialsHelper) Get() error {
var data []string
scanner := bufio.NewScanner(h.in)
for scanner.Scan() {
data = append(data, scanner.Text())
}

if scanner.Err() != nil {
return errors.Wrap(scanner.Err(), "unable to read input from stdin")
}

gitCredential, err := CreateGitCredential(data)
if err != nil {
return errors.Wrap(scanner.Err(), "unable to create GitCredential struct")
}

answer := h.Fill(gitCredential)

_, err = fmt.Fprintf(h.out, answer.String())
if err != nil {
return errors.Wrap(err, "unable to write response to stdin")
}

return nil
}

// Fill creates a GitCredential instance based on a git credential helper request which represented by the passed queryCredential instance.
// If there is no auth information available an empty credential instance is returned
func (h *GitCredentialsHelper) Fill(queryCredential GitCredential) GitCredential {
for _, authCredential := range h.knownCredentials {
if queryCredential.Protocol != authCredential.Protocol {
continue
}

if queryCredential.Host != authCredential.Host {
continue
}

if queryCredential.Path != authCredential.Path {
continue
}

answer := authCredential.Clone()
return answer
}

return GitCredential{}
}
Loading

0 comments on commit d2c8419

Please sign in to comment.