1package authmethodupdate
2
3import (
4	"encoding/json"
5	"flag"
6	"fmt"
7	"io"
8	"strings"
9	"time"
10
11	"github.com/hashicorp/consul/api"
12	"github.com/hashicorp/consul/command/acl/authmethod"
13	"github.com/hashicorp/consul/command/flags"
14	"github.com/hashicorp/consul/command/helpers"
15	"github.com/mitchellh/cli"
16)
17
18func New(ui cli.Ui) *cmd {
19	c := &cmd{UI: ui}
20	c.init()
21	return c
22}
23
24type cmd struct {
25	UI    cli.Ui
26	flags *flag.FlagSet
27	http  *flags.HTTPFlags
28	help  string
29
30	name string
31
32	displayName   string
33	description   string
34	maxTokenTTL   time.Duration
35	tokenLocality string
36	config        string
37
38	k8sHost              string
39	k8sCACert            string
40	k8sServiceAccountJWT string
41
42	noMerge  bool
43	showMeta bool
44	format   string
45
46	testStdin io.Reader
47
48	enterpriseCmd
49}
50
51func (c *cmd) init() {
52	c.flags = flag.NewFlagSet("", flag.ContinueOnError)
53
54	c.flags.BoolVar(
55		&c.showMeta,
56		"meta",
57		false,
58		"Indicates that auth method metadata such "+
59			"as the raft indices should be shown for each entry.",
60	)
61
62	c.flags.StringVar(
63		&c.name,
64		"name",
65		"",
66		"The auth method name.",
67	)
68
69	c.flags.StringVar(
70		&c.displayName,
71		"display-name",
72		"",
73		"An optional name to use instead of the name when displaying this auth method in a UI.",
74	)
75
76	c.flags.StringVar(
77		&c.description,
78		"description",
79		"",
80		"A description of the auth method.",
81	)
82
83	c.flags.DurationVar(
84		&c.maxTokenTTL,
85		"max-token-ttl",
86		0,
87		"Duration of time all tokens created by this auth method should be valid for",
88	)
89	c.flags.StringVar(
90		&c.tokenLocality,
91		"token-locality",
92		"",
93		"Defines the kind of token that this auth method should produce. "+
94			"This can be either 'local' or 'global'. If empty the value of 'local' is assumed.",
95	)
96
97	c.flags.StringVar(
98		&c.config,
99		"config",
100		"",
101		"The configuration for the auth method. Must be JSON. The config is updated as one field"+
102			"May be prefixed with '@' to indicate that the value is a file path to load the config from. "+
103			"'-' may also be given to indicate that the config are available on stdin. ",
104	)
105
106	c.flags.StringVar(
107		&c.k8sHost,
108		"kubernetes-host",
109		"",
110		"Address of the Kubernetes API server. This flag is required for type=kubernetes.",
111	)
112	c.flags.StringVar(
113		&c.k8sCACert,
114		"kubernetes-ca-cert",
115		"",
116		"PEM encoded CA cert for use by the TLS client used to talk with the "+
117			"Kubernetes API. May be prefixed with '@' to indicate that the "+
118			"value is a file path to load the cert from. "+
119			"This flag is required for type=kubernetes.",
120	)
121	c.flags.StringVar(
122		&c.k8sServiceAccountJWT,
123		"kubernetes-service-account-jwt",
124		"",
125		"A Kubernetes service account JWT used to access the TokenReview API to "+
126			"validate other JWTs during login. "+
127			"This flag is required for type=kubernetes.",
128	)
129
130	c.flags.BoolVar(&c.noMerge, "no-merge", false, "Do not merge the current auth method "+
131		"information with what is provided to the command. Instead overwrite all fields "+
132		"with the exception of the name which is immutable.")
133
134	c.flags.StringVar(
135		&c.format,
136		"format",
137		authmethod.PrettyFormat,
138		fmt.Sprintf("Output format {%s}", strings.Join(authmethod.GetSupportedFormats(), "|")),
139	)
140
141	c.initEnterpriseFlags()
142
143	c.http = &flags.HTTPFlags{}
144	flags.Merge(c.flags, c.http.ClientFlags())
145	flags.Merge(c.flags, c.http.ServerFlags())
146	flags.Merge(c.flags, c.http.NamespaceFlags())
147	c.help = flags.Usage(help, c.flags)
148}
149
150func (c *cmd) Run(args []string) int {
151	if err := c.flags.Parse(args); err != nil {
152		return 1
153	}
154
155	if c.name == "" {
156		c.UI.Error(fmt.Sprintf("Cannot update an auth method without specifying the -name parameter"))
157		return 1
158	}
159
160	client, err := c.http.APIClient()
161	if err != nil {
162		c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
163		return 1
164	}
165
166	// Regardless of merge, we need to fetch the prior immutable fields first.
167	currentAuthMethod, _, err := client.ACL().AuthMethodRead(c.name, nil)
168	if err != nil {
169		c.UI.Error(fmt.Sprintf("Error when retrieving current auth method: %v", err))
170		return 1
171	} else if currentAuthMethod == nil {
172		c.UI.Error(fmt.Sprintf("Auth method not found with name %q", c.name))
173		return 1
174	}
175
176	if c.k8sCACert != "" {
177		c.k8sCACert, err = helpers.LoadDataSource(c.k8sCACert, c.testStdin)
178		if err != nil {
179			c.UI.Error(fmt.Sprintf("Invalid '-kubernetes-ca-cert' value: %v", err))
180			return 1
181		} else if c.k8sCACert == "" {
182			c.UI.Error(fmt.Sprintf("Kubernetes CA Cert is empty"))
183			return 1
184		}
185	}
186
187	var method *api.ACLAuthMethod
188	if c.noMerge {
189		method = &api.ACLAuthMethod{
190			Name:          currentAuthMethod.Name,
191			Type:          currentAuthMethod.Type,
192			DisplayName:   c.displayName,
193			Description:   c.description,
194			TokenLocality: c.tokenLocality,
195		}
196		if c.maxTokenTTL > 0 {
197			method.MaxTokenTTL = c.maxTokenTTL
198		}
199
200		if err := c.enterprisePopulateAuthMethod(method); err != nil {
201			c.UI.Error(err.Error())
202			return 1
203		}
204
205		if c.config != "" {
206			if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" {
207				c.UI.Error(fmt.Sprintf("Cannot use command line arguments with '-config' flag"))
208				return 1
209			}
210			data, err := helpers.LoadDataSource(c.config, c.testStdin)
211			if err != nil {
212				c.UI.Error(fmt.Sprintf("Error loading configuration file: %v", err))
213				return 1
214			}
215			if err := json.Unmarshal([]byte(data), &method.Config); err != nil {
216				c.UI.Error(fmt.Sprintf("Error parsing JSON for auth method config: %v", err))
217				return 1
218			}
219		}
220
221		if currentAuthMethod.Type == "kubernetes" {
222			if c.k8sHost == "" {
223				c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-host' flag"))
224				return 1
225			} else if c.k8sCACert == "" {
226				c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-ca-cert' flag"))
227				return 1
228			} else if c.k8sServiceAccountJWT == "" {
229				c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-service-account-jwt' flag"))
230				return 1
231			}
232
233			method.Config = map[string]interface{}{
234				"Host":              c.k8sHost,
235				"CACert":            c.k8sCACert,
236				"ServiceAccountJWT": c.k8sServiceAccountJWT,
237			}
238		}
239	} else {
240		methodCopy := *currentAuthMethod
241		method = &methodCopy
242		if c.description != "" {
243			method.Description = c.description
244		}
245		if c.displayName != "" {
246			method.DisplayName = c.displayName
247		}
248		if c.maxTokenTTL > 0 {
249			method.MaxTokenTTL = c.maxTokenTTL
250		}
251		if c.tokenLocality != "" {
252			method.TokenLocality = c.tokenLocality
253		}
254		if err := c.enterprisePopulateAuthMethod(method); err != nil {
255			c.UI.Error(err.Error())
256			return 1
257		}
258		if c.config != "" {
259			if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" {
260				c.UI.Error(fmt.Sprintf("Cannot use command line arguments with '-config' flag"))
261				return 1
262			}
263			data, err := helpers.LoadDataSource(c.config, c.testStdin)
264			if err != nil {
265				c.UI.Error(fmt.Sprintf("Error loading configuration file: %v", err))
266				return 1
267			}
268			// Don't attempt a deep merge.
269			method.Config = make(map[string]interface{})
270			if err := json.Unmarshal([]byte(data), &method.Config); err != nil {
271				c.UI.Error(fmt.Sprintf("Error parsing JSON for auth method config: %v", err))
272				return 1
273			}
274		}
275		if method.Config == nil {
276			method.Config = make(map[string]interface{})
277		}
278		if currentAuthMethod.Type == "kubernetes" {
279			if c.k8sHost != "" {
280				method.Config["Host"] = c.k8sHost
281			}
282			if c.k8sCACert != "" {
283				method.Config["CACert"] = c.k8sCACert
284			}
285			if c.k8sServiceAccountJWT != "" {
286				method.Config["ServiceAccountJWT"] = c.k8sServiceAccountJWT
287			}
288		}
289	}
290
291	method, _, err = client.ACL().AuthMethodUpdate(method, nil)
292	if err != nil {
293		c.UI.Error(fmt.Sprintf("Error updating auth method %q: %v", c.name, err))
294		return 1
295	}
296
297	formatter, err := authmethod.NewFormatter(c.format, c.showMeta)
298	if err != nil {
299		c.UI.Error(err.Error())
300		return 1
301	}
302
303	out, err := formatter.FormatAuthMethod(method)
304	if err != nil {
305		c.UI.Error(err.Error())
306		return 1
307	}
308	if out != "" {
309		c.UI.Info(out)
310	}
311
312	return 0
313}
314
315func (c *cmd) Synopsis() string {
316	return synopsis
317}
318
319func (c *cmd) Help() string {
320	return flags.Usage(c.help, nil)
321}
322
323const synopsis = "Update an ACL auth method"
324const help = `
325Usage: consul acl auth-method update -name NAME [options]
326
327  Updates an auth method. By default it will merge the auth method
328  information with its current state so that you do not have to provide all
329  parameters. This behavior can be disabled by passing -no-merge.
330
331  Update all editable fields of the auth method:
332
333    $ consul acl auth-method update -name "my-k8s" \
334                            -description "new description" \
335                            -kubernetes-host "https://new-apiserver.example.com:8443" \
336                            -kubernetes-ca-file /path/to/new-kube.ca.crt \
337                            -kubernetes-service-account-jwt "NEW_JWT_CONTENTS"
338`
339