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 authored and David Conde committed Apr 7, 2020
1 parent c026cf0 commit bd4235c
Show file tree
Hide file tree
Showing 14 changed files with 749 additions and 301 deletions.
64 changes: 40 additions & 24 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,12 +37,7 @@ type StepGitCredentialsOptions struct {
GitHubAppOwner string
GitKind string
CredentialsSecret string
}

type credentials struct {
user string
password string
serviceURL string
CredentialHelper bool
}

var (
Expand All @@ -57,6 +52,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 +80,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 +107,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 +142,38 @@ func (o *StepGitCredentialsOptions) Run() error {
if err != nil {
return errors.Wrap(err, "creating git 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 err
}
return nil
}

outFile, err = o.determineOutputFile()
if err != nil {
return errors.Wrap(err, "unable to determine for git credentials")
}

return o.createGitCredentialsFile(outFile, credentials)
}

func (o *StepGitCredentialsOptions) GitCredentialsFileData(credentials []credentials) ([]byte, error) {
// GitCredentialsFileData takes the given git credentials and writes them into a byte array.
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 +202,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 +216,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 +253,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
15 changes: 11 additions & 4 deletions pkg/cmd/step/verify/step_verify_environments.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,12 +482,19 @@ func (o *StepVerifyEnvironmentsOptions) pushDevEnvironmentUpdates(environmentRep
}
}

userDetails := provider.UserAuth()
authenticatedPushURL, err := gitter.CreateAuthenticatedURL(environmentRepo.CloneURL, &userDetails)
remoteURL, err := gits.AddUserToURL(environmentRepo.CloneURL, provider.CurrentUsername())
if err != nil {
return errors.Wrapf(err, "failed to create push URL for %s", environmentRepo.CloneURL)
return errors.Wrapf(err, "unable to add username to git url %s", environmentRepo.CloneURL)
}
err = gitter.Push(localRepoDir, authenticatedPushURL, true, "master")
remoteName, err := gits.GetRemoteForURL(localRepoDir, remoteURL, gitter)
if err != nil {
return errors.Wrapf(err, "cannot determine remote name for %s", environmentRepo.CloneURL)
}
if remoteName == "" {
return errors.Wrapf(err, "no remote configured for %s", environmentRepo.CloneURL)
}

err = gitter.Push(localRepoDir, remoteName, true, "master")
if err != nil {
return errors.Wrapf(err, "unable to push %s to %s", localRepoDir, environmentRepo.URL)
}
Expand Down
125 changes: 125 additions & 0 deletions pkg/gits/credentialhelper/git_credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package credentialhelper

import (
"encoding/json"
"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")
}

data, err := json.Marshal(fieldMap)
if err != nil {
return GitCredential{}, errors.Wrapf(err, "unable to marshal git credential data")
}

err = json.Unmarshal(data, &credential)
if err != nil {
return GitCredential{}, errors.Wrapf(err, "unable unmarshal git credential data")
}

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
}

// Clone 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
}
Loading

0 comments on commit bd4235c

Please sign in to comment.