Skip to content

Commit

Permalink
add conveniences to credential handling (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakecoffman authored Nov 9, 2022
1 parent 03180f4 commit 1f16b47
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
tmp
testdata/caches
cache
out.yaml
48 changes: 45 additions & 3 deletions cmd/dependabot/internal/cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,52 @@ func processInput(input *model.Input) {
job.SecurityAdvisories = []model.Advisory{}
}

// Process environment variables in the scenario file
// As a convenience, fill in a git_source if credentials are in the environment and a git_source
// doesn't already exist. This way the user doesn't run out of calls from being anonymous.
hasLocalToken := os.Getenv("LOCAL_GITHUB_ACCESS_TOKEN") != ""
var isGitSourceInCreds bool
for _, cred := range input.Credentials {
for key, value := range cred {
cred[key] = os.ExpandEnv(value)
if cred["type"] == "git_source" {
isGitSourceInCreds = true
break
}
}
if hasLocalToken && !isGitSourceInCreds {
log.Println("Inserting $LOCAL_GITHUB_ACCESS_TOKEN into credentials")
input.Credentials = append(input.Credentials, model.Credential{
"type": "git_source",
"host": "github.com",
"username": "x-access-token",
"password": "$LOCAL_GITHUB_ACCESS_TOKEN",
})
if len(input.Job.CredentialsMetadata) > 0 {
// Add the metadata since the next section will be skipped.
input.Job.CredentialsMetadata = append(input.Job.CredentialsMetadata, map[string]any{
"type": "git_source",
"host": "github.com",
})
}
}

// As a convenience, fill credentials-metadata if credentials are provided
// which is what happens in production. This way the user doesn't have to
// specify credentials-metadata in the scenario file unless they want to.
if len(input.Job.CredentialsMetadata) == 0 {
log.Println("Adding missing credentials-metadata into job definition")
for _, credential := range input.Credentials {
entry := map[string]any{
"type": credential["type"],
}
if credential["host"] != nil {
entry["host"] = credential["host"]
}
if credential["url"] != nil {
entry["url"] = credential["url"]
}
if credential["replaces-base"] != nil {
entry["replaces-base"] = credential["replaces-base"]
}
input.Job.CredentialsMetadata = append(input.Job.CredentialsMetadata, entry)
}
}
}
Expand Down
58 changes: 58 additions & 0 deletions cmd/dependabot/internal/cmd/update_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package cmd

import (
"os"
"reflect"
"testing"

"github.com/dependabot/cli/internal/model"
)

func Test_processInput(t *testing.T) {
t.Run("initializes some fields", func(t *testing.T) {
os.Setenv("LOCAL_GITHUB_ACCESS_TOKEN", "")

var input model.Input
processInput(&input)

if input.Job.ExistingPullRequests == nil {
t.Error("expected existing pull requests to be initialized")
}
if input.Job.IgnoreConditions == nil {
t.Error("expected ignore conditions to be initialized")
}
if input.Job.SecurityAdvisories == nil {
t.Error("expected security advisories to be initialized")
}
if len(input.Credentials) != 0 {
t.Fatal("expected NO credentials to be added")
}
})

t.Run("adds git_source to credentials when local token is present", func(t *testing.T) {
var input model.Input
os.Setenv("LOCAL_GITHUB_ACCESS_TOKEN", "token")
// Adding a dummy metadata to test the inner if
input.Job.CredentialsMetadata = []model.Credential{{}}

processInput(&input)

if len(input.Credentials) != 1 {
t.Fatal("expected credentials to be added")
}
if !reflect.DeepEqual(input.Credentials[0], model.Credential{
"type": "git_source",
"host": "github.com",
"username": "x-access-token",
"password": "$LOCAL_GITHUB_ACCESS_TOKEN",
}) {
t.Error("expected credentials to be added")
}
if !reflect.DeepEqual(input.Job.CredentialsMetadata[1], model.Credential{
"type": "git_source",
"host": "github.com",
}) {
t.Error("expected credentials metadata to be added")
}
})
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.0 // indirect
golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
gotest.tools/v3 v3.3.0 // indirect
)
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
Expand Down
4 changes: 3 additions & 1 deletion internal/infra/config.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package infra

import "github.com/dependabot/cli/internal/model"

// ConfigFilePath is the path to proxy config file.
const ConfigFilePath = "/config.json"

// Config is the structure of the proxy's config file
type Config struct {
Credentials []map[string]string `json:"all_credentials"`
Credentials []model.Credential `json:"all_credentials"`
CA CertificateAuthority `json:"ca"`
}

Expand Down
37 changes: 26 additions & 11 deletions internal/infra/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type RunParams struct {
// expectations asserted at the end of a test
Expected []model.Output
// credentials passed to the proxy
Creds []map[string]string
Creds []model.Credential
// local directory used for caching
CacheDir string
// write output to a file
Expand Down Expand Up @@ -63,16 +63,6 @@ func Run(params RunParams) error {
api := server.NewAPI(params.Expected)
defer api.Stop()

token := os.Getenv("LOCAL_GITHUB_ACCESS_TOKEN")
if token != "" {
params.Creds = append(params.Creds, map[string]string{
"type": "git_source",
"host": "github.com",
"username": "x-access-token",
"password": token,
})
}

var outFile *os.File
if params.Output != "" {
var err error
Expand All @@ -85,6 +75,8 @@ func Run(params RunParams) error {
defer outFile.Close()
}

expandEnvironmentVariables(api, &params)

if err := runContainers(ctx, params, api); err != nil {
return err
}
Expand Down Expand Up @@ -124,6 +116,29 @@ func Run(params RunParams) error {
return nil
}

func expandEnvironmentVariables(api *server.API, params *RunParams) {
api.Actual.Input.Credentials = params.Creds

// Make a copy of the credentials, so we don't inject them into the output file.
params.Creds = []model.Credential{}
for _, cred := range api.Actual.Input.Credentials {
newCred := model.Credential{}
for k, v := range cred {
newCred[k] = v
}
params.Creds = append(params.Creds, newCred)
}

// Add the actual credentials from the environment.
for _, cred := range params.Creds {
for key, value := range cred {
if valueString, ok := value.(string); ok {
cred[key] = os.ExpandEnv(valueString)
}
}
}
}

func generateIgnoreConditions(params *RunParams, actual *model.Scenario) error {
for _, out := range actual.Output {
if out.Type == "create_pull_request" {
Expand Down
64 changes: 64 additions & 0 deletions internal/infra/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,50 @@ import (
"bytes"
"context"
"io"
"os"
"reflect"
"testing"

"github.com/dependabot/cli/internal/server"

"gopkg.in/yaml.v3"

"github.com/dependabot/cli/internal/model"
"github.com/docker/docker/api/types"
"github.com/moby/moby/client"
)

func Test_expandEnvironmentVariables(t *testing.T) {
t.Run("injects environment variables", func(t *testing.T) {
os.Setenv("ENV1", "value1")
os.Setenv("ENV2", "value2")
api := &server.API{}
params := &RunParams{
Creds: []model.Credential{{
"type": "test",
"url": "url",
"username": "$ENV1",
"pass": "$ENV2",
}},
}

expandEnvironmentVariables(api, params)

if params.Creds[0]["username"] != "value1" {
t.Error("expected username to be injected", params.Creds[0]["username"])
}
if params.Creds[0]["pass"] != "value2" {
t.Error("expected pass to be injected", params.Creds[0]["pass"])
}
if api.Actual.Input.Credentials[0]["username"] != "$ENV1" {
t.Error("expected username NOT to be injected", api.Actual.Input.Credentials[0]["username"])
}
if api.Actual.Input.Credentials[0]["pass"] != "$ENV2" {
t.Error("expected pass NOT to be injected", api.Actual.Input.Credentials[0]["pass"])
}
})
}

func Test_generateIgnoreConditions(t *testing.T) {
const (
outputFileName = "test_output"
Expand Down Expand Up @@ -107,6 +143,14 @@ func TestRun(t *testing.T) {
_, _ = cli.ImageRemove(ctx, UpdaterImageName, types.ImageRemoveOptions{})
}()

cred := model.Credential{
"type": "git_source",
"host": "github.com",
"username": "x-access-token",
"password": "$LOCAL_GITHUB_ACCESS_TOKEN",
}

os.Setenv("LOCAL_GITHUB_ACCESS_TOKEN", "test-token")
err = Run(RunParams{
PullImages: true,
Job: &model.Job{
Expand All @@ -115,10 +159,30 @@ func TestRun(t *testing.T) {
Repo: "org/name",
},
},
Creds: []model.Credential{cred},
Output: "out.yaml",
})
if err != nil {
t.Error(err)
}

f, err := os.Open("out.yaml")
if err != nil {
t.Fatal(err)
}
defer f.Close()

var output model.Scenario
if err = yaml.NewDecoder(f).Decode(&output); err != nil {
t.Fatal(err)
}

if !reflect.DeepEqual(output.Input.Credentials, []model.Credential{cred}) {
t.Error("unexpected credentials", output.Input.Credentials)
}
if output.Input.Credentials[0]["password"] != "$LOCAL_GITHUB_ACCESS_TOKEN" {
t.Error("expected password to be masked")
}
}

const dockerFile = `
Expand Down
38 changes: 20 additions & 18 deletions internal/model/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ package model

// Job is the data that is passed to the updater.
type Job struct {
PackageManager string `json:"package-manager" yaml:"package-manager"`
AllowedUpdates []Allowed `json:"allowed-updates" yaml:"allowed-updates,omitempty"`
Dependencies []string `json:"dependencies" yaml:"dependencies,omitempty"`
ExistingPullRequests [][]ExistingPR `json:"existing-pull-requests" yaml:"existing-pull-requests,omitempty"`
Experiments Experiment `json:"experiments" yaml:"experiments,omitempty"`
IgnoreConditions []Condition `json:"ignore-conditions" yaml:"ignore-conditions,omitempty"`
LockfileOnly bool `json:"lockfile-only" yaml:"lockfile-only,omitempty"`
RequirementsUpdateStrategy *string `json:"requirements-update-strategy" yaml:"requirements-update-strategy,omitempty"`
SecurityAdvisories []Advisory `json:"security-advisories" yaml:"security-advisories,omitempty"`
SecurityUpdatesOnly bool `json:"security-updates-only" yaml:"security-updates-only,omitempty"`
Source Source `json:"source" yaml:"source"`
UpdateSubdependencies bool `json:"update-subdependencies" yaml:"update-subdependencies,omitempty"`
UpdatingAPullRequest bool `json:"updating-a-pull-request" yaml:"updating-a-pull-request,omitempty"`
VendorDependencies bool `json:"vendor-dependencies" yaml:"vendor-dependencies,omitempty"`
RejectExternalCode bool `json:"reject-external-code" yaml:"reject-external-code,omitempty"`
CommitMessageOptions *CommitOptions `json:"commit-message-options" yaml:"commit-message-options,omitempty"`
CredentialsMetadata []map[string]any `json:"credentials-metadata" yaml:"credentials-metadata,omitempty"`
MaxUpdaterRunTime int `json:"max-updater-run-time" yaml:"max-updater-run-time,omitempty"`
PackageManager string `json:"package-manager" yaml:"package-manager"`
AllowedUpdates []Allowed `json:"allowed-updates" yaml:"allowed-updates,omitempty"`
Dependencies []string `json:"dependencies" yaml:"dependencies,omitempty"`
ExistingPullRequests [][]ExistingPR `json:"existing-pull-requests" yaml:"existing-pull-requests,omitempty"`
Experiments Experiment `json:"experiments" yaml:"experiments,omitempty"`
IgnoreConditions []Condition `json:"ignore-conditions" yaml:"ignore-conditions,omitempty"`
LockfileOnly bool `json:"lockfile-only" yaml:"lockfile-only,omitempty"`
RequirementsUpdateStrategy *string `json:"requirements-update-strategy" yaml:"requirements-update-strategy,omitempty"`
SecurityAdvisories []Advisory `json:"security-advisories" yaml:"security-advisories,omitempty"`
SecurityUpdatesOnly bool `json:"security-updates-only" yaml:"security-updates-only,omitempty"`
Source Source `json:"source" yaml:"source"`
UpdateSubdependencies bool `json:"update-subdependencies" yaml:"update-subdependencies,omitempty"`
UpdatingAPullRequest bool `json:"updating-a-pull-request" yaml:"updating-a-pull-request,omitempty"`
VendorDependencies bool `json:"vendor-dependencies" yaml:"vendor-dependencies,omitempty"`
RejectExternalCode bool `json:"reject-external-code" yaml:"reject-external-code,omitempty"`
CommitMessageOptions *CommitOptions `json:"commit-message-options" yaml:"commit-message-options,omitempty"`
CredentialsMetadata []Credential `json:"credentials-metadata" yaml:"credentials-metadata,omitempty"`
MaxUpdaterRunTime int `json:"max-updater-run-time" yaml:"max-updater-run-time,omitempty"`
}

// Source is a reference to some source code
Expand Down Expand Up @@ -86,3 +86,5 @@ type CommitOptions struct {
PrefixDevelopment string `json:"prefix-development,omitempty" yaml:"prefix-development,omitempty"`
IncludeScope *string `json:"include-scope,omitempty" yaml:"include-scope,omitempty"`
}

type Credential map[string]any
2 changes: 1 addition & 1 deletion internal/model/scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Input struct {
// Job is the data given to the updater
Job Job `yaml:"job"`
// Credentials is the registry info and tokens to pass to the Proxy
Credentials []map[string]string `yaml:"credentials,omitempty"`
Credentials []Credential `yaml:"credentials,omitempty"`
}

// Output is the expected output given the inputs
Expand Down

0 comments on commit 1f16b47

Please sign in to comment.