Repository: docker/compose-cli
Pull request #1943 (2021-07-22 20:41:34)

Add a post-login message to promote personal access tokens
**What I did**
- Add a post-login message to promote Personal Access Tokens https://docs.docker.com/docker-hub/access-tokens/
- Message will only appear if:
    1- User is using default registry
    2- User successfully logged in
    3- User is not using a Personal Access Token

```sh
> docker login -u amineck
Password:
Login Succeeded

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/
````



Patch:
diff --git a/.github/workflows/cli-release.yaml b/.github/workflows/cli-release.yaml
index 4df63e51..1bfd480c 100644
--- a/.github/workflows/cli-release.yaml
+++ b/.github/workflows/cli-release.yaml
@@ -1,17 +1,11 @@
 name: Releaser
 
 on:
-  workflow_dispatch:
-    inputs:
-      tag:
-        description: 'Release Tag'
-        required: true
-      dry-run:
-        description: 'Dry run'
-        required: false
-        default: 'true'
+  push:
+    tags:
+      - "v1*"
 jobs:
-  release:
+  upload-release:
     runs-on: ubuntu-latest
     steps:
       - name: Set up Go 1.16
@@ -42,9 +36,7 @@ jobs:
         run: cp packaging/* bin/
 
       - uses: ncipollo/release-action@v1
-        if: ${{ github.event.inputs.dry-run != 'true' }}
         with:
           artifacts: "bin/*"
           prerelease: true
           token: ${{ secrets.GITHUB_TOKEN }}
-          tag: ${{ github.event.inputs.tag }}
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/go.mod b/go.mod
index 9414495d..77724f6e 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,7 @@ require (
 	github.com/awslabs/goformation/v4 v4.15.6
 	github.com/buger/goterm v1.0.0
 	github.com/cnabio/cnab-to-oci v0.3.1-beta1
-	github.com/compose-spec/compose-go v0.0.0-20210722130045-6e1e1c2b26de
+	github.com/compose-spec/compose-go v0.0.0-20210706130854-69459d4976b5
 	github.com/containerd/console v1.0.2
 	github.com/containerd/containerd v1.5.0
 	github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e
diff --git a/go.sum b/go.sum
index e7102c0c..f1f118c3 100644
--- a/go.sum
+++ b/go.sum
@@ -248,8 +248,8 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht
 github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
 github.com/codahale/hdrhistogram v0.0.0-20160425231609-f8ad88b59a58/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
-github.com/compose-spec/compose-go v0.0.0-20210722130045-6e1e1c2b26de h1:CtO+cawqWl/liAfVwxANlO/PK9s7mtpJMXwjwoKxWYw=
-github.com/compose-spec/compose-go v0.0.0-20210722130045-6e1e1c2b26de/go.mod h1:5V65rPnTvvQagtoMxTneJ2QicLq6ZRQQ7fOgPN226fo=
+github.com/compose-spec/compose-go v0.0.0-20210706130854-69459d4976b5 h1:PpI72CT1bcVPNZyqI1HI8UhQVRVtqLb2tdwi5WphN3c=
+github.com/compose-spec/compose-go v0.0.0-20210706130854-69459d4976b5/go.mod h1:5V65rPnTvvQagtoMxTneJ2QicLq6ZRQQ7fOgPN226fo=
 github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
 github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
 github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=


Test patch:
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)
+		})
+	}
+}