1package tokenupdate
2
3import (
4	"flag"
5	"fmt"
6
7	"github.com/hashicorp/consul/api"
8	"github.com/hashicorp/consul/command/acl"
9	"github.com/hashicorp/consul/command/flags"
10	"github.com/mitchellh/cli"
11)
12
13func New(ui cli.Ui) *cmd {
14	c := &cmd{UI: ui}
15	c.init()
16	return c
17}
18
19type cmd struct {
20	UI    cli.Ui
21	flags *flag.FlagSet
22	http  *flags.HTTPFlags
23	help  string
24
25	tokenID       string
26	policyIDs     []string
27	policyNames   []string
28	description   string
29	mergePolicies bool
30	showMeta      bool
31	upgradeLegacy bool
32}
33
34func (c *cmd) init() {
35	c.flags = flag.NewFlagSet("", flag.ContinueOnError)
36	c.flags.BoolVar(&c.showMeta, "meta", false, "Indicates that token metadata such "+
37		"as the content hash and raft indices should be shown for each entry")
38	c.flags.BoolVar(&c.mergePolicies, "merge-policies", false, "Merge the new policies "+
39		"with the existing policies")
40	c.flags.StringVar(&c.tokenID, "id", "", "The Accessor ID of the token to read. "+
41		"It may be specified as a unique ID prefix but will error if the prefix "+
42		"matches multiple token Accessor IDs")
43	c.flags.StringVar(&c.description, "description", "", "A description of the token")
44	c.flags.Var((*flags.AppendSliceValue)(&c.policyIDs), "policy-id", "ID of a "+
45		"policy to use for this token. May be specified multiple times")
46	c.flags.Var((*flags.AppendSliceValue)(&c.policyNames), "policy-name", "Name of a "+
47		"policy to use for this token. May be specified multiple times")
48	c.flags.BoolVar(&c.upgradeLegacy, "upgrade-legacy", false, "Add new polices "+
49		"to a legacy token replacing all existing rules. This will cause the legacy "+
50		"token to behave exactly like a new token but keep the same Secret.\n"+
51		"WARNING: you must ensure that the new policy or policies specified grant "+
52		"equivalent or appropriate access for the existing clients using this token.")
53
54	c.http = &flags.HTTPFlags{}
55	flags.Merge(c.flags, c.http.ClientFlags())
56	flags.Merge(c.flags, c.http.ServerFlags())
57	c.help = flags.Usage(help, c.flags)
58}
59
60func (c *cmd) Run(args []string) int {
61	if err := c.flags.Parse(args); err != nil {
62		return 1
63	}
64
65	if c.tokenID == "" {
66		c.UI.Error(fmt.Sprintf("Cannot update a token without specifying the -id parameter"))
67		return 1
68	}
69
70	client, err := c.http.APIClient()
71	if err != nil {
72		c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
73		return 1
74	}
75
76	tokenID, err := acl.GetTokenIDFromPartial(client, c.tokenID)
77	if err != nil {
78		c.UI.Error(fmt.Sprintf("Error determining token ID: %v", err))
79		return 1
80	}
81
82	token, _, err := client.ACL().TokenRead(tokenID, nil)
83	if err != nil {
84		c.UI.Error(fmt.Sprintf("Error when retrieving current token: %v", err))
85		return 1
86	}
87
88	if c.upgradeLegacy {
89		if token.Rules == "" {
90			// This is just for convenience it should actually be harmless to allow it
91			// to go through anyway.
92			c.UI.Error(fmt.Sprintf("Can't use -upgrade-legacy on a non-legacy token"))
93			return 1
94		}
95		// Reset the rules to nothing forcing this to be updated as a non-legacy
96		// token but with same secret.
97		token.Rules = ""
98	}
99
100	if c.description != "" {
101		// Only update description if the user specified a new one. This does make
102		// it impossible to completely clear descriptions from CLI but that seems
103		// better than silently deleting descriptions when using command without
104		// manually giving the new description. If it's a real issue we can always
105		// add another explicit `-remove-description` flag but it feels like an edge
106		// case that's not going to be critical to anyone.
107		token.Description = c.description
108	}
109
110	if c.mergePolicies {
111		for _, policyName := range c.policyNames {
112			found := false
113			for _, link := range token.Policies {
114				if link.Name == policyName {
115					found = true
116					break
117				}
118			}
119
120			if !found {
121				// We could resolve names to IDs here but there isn't any reason why its would be better
122				// than allowing the agent to do it.
123				token.Policies = append(token.Policies, &api.ACLTokenPolicyLink{Name: policyName})
124			}
125		}
126
127		for _, policyID := range c.policyIDs {
128			policyID, err := acl.GetPolicyIDFromPartial(client, policyID)
129			if err != nil {
130				c.UI.Error(fmt.Sprintf("Error resolving policy ID %s: %v", policyID, err))
131				return 1
132			}
133			found := false
134
135			for _, link := range token.Policies {
136				if link.ID == policyID {
137					found = true
138					break
139				}
140			}
141
142			if !found {
143				token.Policies = append(token.Policies, &api.ACLTokenPolicyLink{ID: policyID})
144			}
145		}
146	} else {
147		token.Policies = nil
148
149		for _, policyName := range c.policyNames {
150			// We could resolve names to IDs here but there isn't any reason why its would be better
151			// than allowing the agent to do it.
152			token.Policies = append(token.Policies, &api.ACLTokenPolicyLink{Name: policyName})
153		}
154
155		for _, policyID := range c.policyIDs {
156			policyID, err := acl.GetPolicyIDFromPartial(client, policyID)
157			if err != nil {
158				c.UI.Error(fmt.Sprintf("Error resolving policy ID %s: %v", policyID, err))
159				return 1
160			}
161			token.Policies = append(token.Policies, &api.ACLTokenPolicyLink{ID: policyID})
162		}
163	}
164
165	token, _, err = client.ACL().TokenUpdate(token, nil)
166	if err != nil {
167		c.UI.Error(fmt.Sprintf("Failed to update token %s: %v", tokenID, err))
168		return 1
169	}
170
171	c.UI.Info("Token updated successfully.")
172	acl.PrintToken(token, c.UI, c.showMeta)
173	return 0
174}
175
176func (c *cmd) Synopsis() string {
177	return synopsis
178}
179
180func (c *cmd) Help() string {
181	return flags.Usage(c.help, nil)
182}
183
184const synopsis = "Update an ACL Token"
185const help = `
186Usage: consul acl token update [options]
187
188    This command will update a token. Some parts such as marking the token local
189    cannot be changed.
190
191    Update a token description and take the policies from the existing token:
192
193        $ consul acl token update -id abcd -description "replication" -merge-policies
194
195      Update all editable fields of the token:
196
197          $ consul acl token update -id abcd -description "replication" -policy-name "token-replication"
198`
199