Browse code

Fix issue where secret ID is masked by name

This fix tries to address the issue in 28884 where
it is possible to mask the secret ID by name.

The reason was that searching a secret is based on name.
However, searching a secret should be done based on:
- Full ID
- Full Name
- Partial ID (prefix)

This fix addresses the issue by changing related implementation
in `getCliRequestedSecretIDs()`

An integration test has been added to cover the changes.

This fix fixes 28884

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2016/11/29 05:18:15
Showing 2 changed files
... ...
@@ -1,6 +1,9 @@
1 1
 package secret
2 2
 
3 3
 import (
4
+	"fmt"
5
+	"strings"
6
+
4 7
 	"github.com/docker/docker/api/types"
5 8
 	"github.com/docker/docker/api/types/filters"
6 9
 	"github.com/docker/docker/api/types/swarm"
... ...
@@ -8,10 +11,11 @@ import (
8 8
 	"golang.org/x/net/context"
9 9
 )
10 10
 
11
-func getSecretsByName(ctx context.Context, client client.APIClient, names []string) ([]swarm.Secret, error) {
11
+func getSecretsByNameOrIDPrefixes(ctx context.Context, client client.APIClient, terms []string) ([]swarm.Secret, error) {
12 12
 	args := filters.NewArgs()
13
-	for _, n := range names {
13
+	for _, n := range terms {
14 14
 		args.Add("names", n)
15
+		args.Add("id", n)
15 16
 	}
16 17
 
17 18
 	return client.SecretList(ctx, types.SecretListOptions{
... ...
@@ -19,29 +23,53 @@ func getSecretsByName(ctx context.Context, client client.APIClient, names []stri
19 19
 	})
20 20
 }
21 21
 
22
-func getCliRequestedSecretIDs(ctx context.Context, client client.APIClient, names []string) ([]string, error) {
23
-	ids := names
24
-
25
-	// attempt to lookup secret by name
26
-	secrets, err := getSecretsByName(ctx, client, ids)
22
+func getCliRequestedSecretIDs(ctx context.Context, client client.APIClient, terms []string) ([]string, error) {
23
+	secrets, err := getSecretsByNameOrIDPrefixes(ctx, client, terms)
27 24
 	if err != nil {
28 25
 		return nil, err
29 26
 	}
30 27
 
31
-	lookup := make(map[string]struct{})
32
-	for _, id := range ids {
33
-		lookup[id] = struct{}{}
34
-	}
35
-
36 28
 	if len(secrets) > 0 {
37
-		ids = []string{}
38
-
39
-		for _, s := range secrets {
40
-			if _, ok := lookup[s.Spec.Annotations.Name]; ok {
41
-				ids = append(ids, s.ID)
29
+		found := make(map[string]struct{})
30
+	next:
31
+		for _, term := range terms {
32
+			// attempt to lookup secret by full ID
33
+			for _, s := range secrets {
34
+				if s.ID == term {
35
+					found[s.ID] = struct{}{}
36
+					continue next
37
+				}
38
+			}
39
+			// attempt to lookup secret by full name
40
+			for _, s := range secrets {
41
+				if s.Spec.Annotations.Name == term {
42
+					found[s.ID] = struct{}{}
43
+					continue next
44
+				}
45
+			}
46
+			// attempt to lookup secret by partial ID (prefix)
47
+			// return error if more than one matches found (ambiguous)
48
+			n := 0
49
+			for _, s := range secrets {
50
+				if strings.HasPrefix(s.ID, term) {
51
+					found[s.ID] = struct{}{}
52
+					n++
53
+				}
54
+			}
55
+			if n > 1 {
56
+				return nil, fmt.Errorf("secret %s is ambiguous (%d matches found)", term, n)
42 57
 			}
43 58
 		}
59
+
60
+		// We already collected all the IDs found.
61
+		// Now we will remove duplicates by converting the map to slice
62
+		ids := []string{}
63
+		for id := range found {
64
+			ids = append(ids, id)
65
+		}
66
+
67
+		return ids, nil
44 68
 	}
45 69
 
46
-	return ids, nil
70
+	return terms, nil
47 71
 }
... ...
@@ -46,3 +46,61 @@ func (s *DockerSwarmSuite) TestSecretCreateWithLabels(c *check.C) {
46 46
 	c.Assert(secret.Spec.Labels["key1"], checker.Equals, "value1")
47 47
 	c.Assert(secret.Spec.Labels["key2"], checker.Equals, "value2")
48 48
 }
49
+
50
+// Test case for 28884
51
+func (s *DockerSwarmSuite) TestSecretCreateResolve(c *check.C) {
52
+	d := s.AddDaemon(c, true, true)
53
+
54
+	name := "foo"
55
+	id := d.createSecret(c, swarm.SecretSpec{
56
+		swarm.Annotations{
57
+			Name: name,
58
+		},
59
+		[]byte("foo"),
60
+	})
61
+	c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
62
+
63
+	fake := d.createSecret(c, swarm.SecretSpec{
64
+		swarm.Annotations{
65
+			Name: id,
66
+		},
67
+		[]byte("fake foo"),
68
+	})
69
+	c.Assert(fake, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", fake))
70
+
71
+	out, err := d.Cmd("secret", "ls")
72
+	c.Assert(err, checker.IsNil)
73
+	c.Assert(out, checker.Contains, name)
74
+	c.Assert(out, checker.Contains, fake)
75
+
76
+	out, err = d.Cmd("secret", "rm", id)
77
+	c.Assert(out, checker.Contains, id)
78
+
79
+	// Fake one will remain
80
+	out, err = d.Cmd("secret", "ls")
81
+	c.Assert(err, checker.IsNil)
82
+	c.Assert(out, checker.Not(checker.Contains), name)
83
+	c.Assert(out, checker.Contains, fake)
84
+
85
+	// Remove based on name prefix of the fake one
86
+	// (which is the same as the ID of foo one) should not work
87
+	// as search is only done based on:
88
+	// - Full ID
89
+	// - Full Name
90
+	// - Partial ID (prefix)
91
+	out, err = d.Cmd("secret", "rm", id[:5])
92
+	c.Assert(out, checker.Not(checker.Contains), id)
93
+	out, err = d.Cmd("secret", "ls")
94
+	c.Assert(err, checker.IsNil)
95
+	c.Assert(out, checker.Not(checker.Contains), name)
96
+	c.Assert(out, checker.Contains, fake)
97
+
98
+	// Remove based on ID prefix of the fake one should succeed
99
+	out, err = d.Cmd("secret", "rm", fake[:5])
100
+	c.Assert(out, checker.Contains, fake)
101
+	out, err = d.Cmd("secret", "ls")
102
+	c.Assert(err, checker.IsNil)
103
+	c.Assert(out, checker.Not(checker.Contains), name)
104
+	c.Assert(out, checker.Not(checker.Contains), id)
105
+	c.Assert(out, checker.Not(checker.Contains), fake)
106
+}