diff --git a/cli/mobycli/exec.go b/cli/mobycli/exec.go index 367b0ce3..552987f8 100644 --- a/cli/mobycli/exec.go +++ b/cli/mobycli/exec.go @@ -77,10 +77,14 @@ func Exec(root *cobra.Command) { fmt.Fprintln(os.Stderr, err) os.Exit(1) } - command := metrics.GetCommand(os.Args[1:]) - if command == "build" && !metrics.HasQuietFlag(os.Args[1:]) { + commandArgs := os.Args[1:] + command := metrics.GetCommand(commandArgs) + if command == "build" && !metrics.HasQuietFlag(commandArgs) { utils.DisplayScanSuggestMsg() } + if command == "login" && !metrics.HasQuietFlag(commandArgs) { + displayPATSuggestMsg(commandArgs) + } metrics.Track(store.DefaultContextType, os.Args[1:], compose.SuccessStatus) os.Exit(0) diff --git a/cli/mobycli/pat_suggest.go b/cli/mobycli/pat_suggest.go new file mode 100644 index 00000000..24e5363c --- /dev/null +++ b/cli/mobycli/pat_suggest.go @@ -0,0 +1,75 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package mobycli + +import ( + "fmt" + "os" + "strings" + + "github.com/docker/cli/cli/config" + "github.com/docker/docker/registry" + "github.com/hashicorp/go-uuid" +) + +const ( + // patSuggestMsg is a message to suggest the use of PAT (personal access tokens). + patSuggestMsg = `Logging in with your password grants your terminal complete access to your account. +For better security, log in with a limited-privilege personal access token. Learn more at https://docs.docker.com/docker-hub/access-tokens/` + + // patPrefix represents a docker personal access token prefix. + patPrefix = "dckrp_" +) + +// displayPATSuggestMsg displays a message suggesting users to use PATs instead of passwords to reduce scope. +func displayPATSuggestMsg(cmdArgs []string) { + if os.Getenv("DOCKER_PAT_SUGGEST") == "false" { + return + } + if !isUsingDefaultRegistry(cmdArgs) { + return + } + authCfg, err := config.LoadDefaultConfigFile(os.Stderr).GetAuthConfig(registry.IndexServer) + if err != nil { + return + } + if !isUsingPassword(authCfg.Password) { + return + } + fmt.Fprintf(os.Stderr, "\n"+patSuggestMsg+"\n") +} + +func isUsingDefaultRegistry(cmdArgs []string) bool { + for i := 1; i < len(cmdArgs); i++ { + if strings.HasPrefix(cmdArgs[i], "-") { + i++ + continue + } + return cmdArgs[i] == registry.IndexServer + } + return true +} + +func isUsingPassword(pass string) bool { + if _, err := uuid.ParseUUID(pass); err == nil { + return false + } + if strings.HasPrefix(pass, patPrefix) { + return false + } + return true +} diff --git a/cli/mobycli/pat_suggest_test.go b/cli/mobycli/pat_suggest_test.go new file mode 100644 index 00000000..8d229829 --- /dev/null +++ b/cli/mobycli/pat_suggest_test.go @@ -0,0 +1,94 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package mobycli + +import ( + "testing" + + "github.com/docker/docker/registry" + "gotest.tools/assert" +) + +func TestIsUsingDefaultRegistry(t *testing.T) { + testCases := []struct { + name string + input []string + expected bool + }{ + { + "without flags", + []string{"login"}, + true, + }, + { + "login with flags", + []string{"login", "-u", "test", "-p", "testpass"}, + true, + }, + { + "login to default registry", + []string{"login", registry.IndexServer}, + true, + }, + { + "login to different registry", + []string{"login", "registry.example.com"}, + false, + }, + { + "login with flags to different registry", + []string{"login", "-u", "test", "-p", "testpass", "registry.example.com"}, + false, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result := isUsingDefaultRegistry(testCase.input) + assert.Equal(t, testCase.expected, result) + }) + } +} + +func TestIsUsingPassword(t *testing.T) { + testCases := []struct { + name string + input string + expected bool + }{ + { + "regular password", + "mypass", + true, + }, + { + "personal access token", + "1508b8bd-b80c-452d-9a7a-ee5607c41bcd", + false, + }, + { + "prefixed personal access token", + "dckrp_ee5607c41bcd", + false, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result := isUsingPassword(testCase.input) + assert.Equal(t, testCase.expected, result) + }) + } +}