1/*
2Copyright (c) 2018 VMware, Inc. All Rights Reserved.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package session
18
19import (
20	"context"
21	"flag"
22	"fmt"
23	"io"
24	"net/http"
25	"strings"
26	"time"
27
28	"github.com/vmware/govmomi/govc/cli"
29	"github.com/vmware/govmomi/govc/flags"
30	"github.com/vmware/govmomi/session"
31	"github.com/vmware/govmomi/sts"
32	"github.com/vmware/govmomi/vim25"
33	"github.com/vmware/govmomi/vim25/methods"
34	"github.com/vmware/govmomi/vim25/soap"
35)
36
37type login struct {
38	*flags.ClientFlag
39	*flags.OutputFlag
40
41	clone  bool
42	issue  bool
43	renew  bool
44	long   bool
45	ticket string
46	life   time.Duration
47	cookie string
48	token  string
49	ext    string
50}
51
52func init() {
53	cli.Register("session.login", &login{})
54}
55
56func (cmd *login) Register(ctx context.Context, f *flag.FlagSet) {
57	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
58	cmd.ClientFlag.Register(ctx, f)
59	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
60	cmd.OutputFlag.Register(ctx, f)
61
62	f.BoolVar(&cmd.clone, "clone", false, "Acquire clone ticket")
63	f.BoolVar(&cmd.issue, "issue", false, "Issue SAML token")
64	f.BoolVar(&cmd.renew, "renew", false, "Renew SAML token")
65	f.DurationVar(&cmd.life, "lifetime", time.Minute*10, "SAML token lifetime")
66	f.BoolVar(&cmd.long, "l", false, "Output session cookie")
67	f.StringVar(&cmd.ticket, "ticket", "", "Use clone ticket for login")
68	f.StringVar(&cmd.cookie, "cookie", "", "Set HTTP cookie for an existing session")
69	f.StringVar(&cmd.token, "token", "", "Use SAML token for login or as issue identity")
70	f.StringVar(&cmd.ext, "extension", "", "Extension name")
71}
72
73func (cmd *login) Process(ctx context.Context) error {
74	if err := cmd.OutputFlag.Process(ctx); err != nil {
75		return err
76	}
77	return cmd.ClientFlag.Process(ctx)
78}
79
80func (cmd *login) Description() string {
81	return `Session login.
82
83The session.login command is optional, all other govc commands will auto login when given credentials.
84The session.login command can be used to:
85- Persist a session without writing to disk via the '-cookie' flag
86- Acquire a clone ticket
87- Login using a clone ticket
88- Login using a vCenter Extension certificate
89- Issue a SAML token
90- Renew a SAML token
91- Login using a SAML token
92- Avoid passing credentials to other govc commands
93
94Examples:
95  govc session.login -u root:password@host
96  ticket=$(govc session.login -u root@host -clone)
97  govc session.login -u root@host -ticket $ticket
98  govc session.login -u host -extension com.vmware.vsan.health -cert rui.crt -key rui.key
99  token=$(govc session.login -u host -cert user.crt -key user.key -issue) # HoK token
100  bearer=$(govc session.login -u user:pass@host -issue) # Bearer token
101  token=$(govc session.login -u host -cert user.crt -key user.key -issue -token "$bearer")
102  govc session.login -u host -cert user.crt -key user.key -token "$token"
103  token=$(govc session.login -u host -cert user.crt -key user.key -renew -lifetime 24h -token "$token")`
104}
105
106type ticketResult struct {
107	cmd    *login
108	Ticket string `json:",omitempty"`
109	Token  string `json:",omitempty"`
110	Cookie string `json:",omitempty"`
111}
112
113func (r *ticketResult) Write(w io.Writer) error {
114	var output []string
115
116	for _, val := range []string{r.Ticket, r.Token, r.Cookie} {
117		if val != "" {
118			output = append(output, val)
119		}
120	}
121
122	if len(output) == 0 {
123		return nil
124	}
125
126	fmt.Fprintln(w, strings.Join(output, " "))
127
128	return nil
129}
130
131// Logout is called by cli.Run()
132// We override ClientFlag's Logout impl to avoid ending a session when -persist-session=false,
133// otherwise Logout would invalidate the cookie and/or ticket.
134func (cmd *login) Logout(ctx context.Context) error {
135	if cmd.long || cmd.clone || cmd.issue {
136		return nil
137	}
138	return cmd.ClientFlag.Logout(ctx)
139}
140
141func (cmd *login) cloneSession(ctx context.Context, c *vim25.Client) error {
142	return session.NewManager(c).CloneSession(ctx, cmd.ticket)
143}
144
145func (cmd *login) issueToken(ctx context.Context, vc *vim25.Client) (string, error) {
146	c, err := sts.NewClient(ctx, vc)
147	if err != nil {
148		return "", err
149	}
150
151	req := sts.TokenRequest{
152		Certificate: c.Certificate(),
153		Userinfo:    cmd.Userinfo(),
154		Renewable:   true,
155		Token:       cmd.token,
156		Lifetime:    cmd.life,
157	}
158
159	if req.Certificate == nil {
160		req.Delegatable = true // Bearer token request
161	}
162
163	issue := c.Issue
164	if cmd.renew {
165		issue = c.Renew
166	}
167
168	s, err := issue(ctx, req)
169	if err != nil {
170		return "", err
171	}
172
173	if req.Token != "" {
174		duration := s.Lifetime.Expires.Sub(s.Lifetime.Created)
175		if duration < req.Lifetime {
176			// The granted lifetime is that of the bearer token, which is 5min max.
177			// Extend the lifetime via Renew.
178			req.Token = s.Token
179			if s, err = c.Renew(ctx, req); err != nil {
180				return "", err
181			}
182		}
183	}
184
185	return s.Token, nil
186}
187
188func (cmd *login) loginByToken(ctx context.Context, c *vim25.Client) error {
189	header := soap.Header{
190		Security: &sts.Signer{
191			Certificate: c.Certificate(),
192			Token:       cmd.token,
193		},
194	}
195
196	return session.NewManager(c).LoginByToken(c.WithHeader(ctx, header))
197}
198
199func (cmd *login) loginByExtension(ctx context.Context, c *vim25.Client) error {
200	return session.NewManager(c).LoginExtensionByCertificate(ctx, cmd.ext)
201}
202
203func (cmd *login) setCookie(ctx context.Context, c *vim25.Client) error {
204	url := c.URL()
205	jar := c.Client.Jar
206	cookies := jar.Cookies(url)
207	add := true
208
209	cookie := &http.Cookie{
210		Name: soap.SessionCookieName,
211	}
212
213	for _, e := range cookies {
214		if e.Name == cookie.Name {
215			add = false
216			cookie = e
217			break
218		}
219	}
220
221	if cmd.cookie == "" {
222		// This is the cookie from Set-Cookie after a Login or CloneSession
223		cmd.cookie = cookie.Value
224	} else {
225		// The cookie flag is set, set the HTTP header and skip Login()
226		cookie.Value = cmd.cookie
227		if add {
228			cookies = append(cookies, cookie)
229		}
230		jar.SetCookies(url, cookies)
231
232		// Check the session is still valid
233		_, err := methods.GetCurrentTime(ctx, c)
234		if err != nil {
235			return err
236		}
237	}
238
239	return nil
240}
241
242func (cmd *login) Run(ctx context.Context, f *flag.FlagSet) error {
243	if cmd.renew {
244		cmd.issue = true
245	}
246	switch {
247	case cmd.ticket != "":
248		cmd.Login = cmd.cloneSession
249	case cmd.cookie != "":
250		cmd.Login = cmd.setCookie
251	case cmd.token != "":
252		cmd.Login = cmd.loginByToken
253	case cmd.ext != "":
254		cmd.Login = cmd.loginByExtension
255	case cmd.issue:
256		cmd.Login = func(_ context.Context, _ *vim25.Client) error {
257			return nil
258		}
259	}
260
261	c, err := cmd.Client()
262	if err != nil {
263		return err
264	}
265
266	m := session.NewManager(c)
267	r := &ticketResult{cmd: cmd}
268
269	switch {
270	case cmd.clone:
271		r.Ticket, err = m.AcquireCloneTicket(ctx)
272		if err != nil {
273			return err
274		}
275	case cmd.issue:
276		r.Token, err = cmd.issueToken(ctx, c)
277		if err != nil {
278			return err
279		}
280		return cmd.WriteResult(r)
281	}
282
283	if cmd.cookie == "" {
284		_ = cmd.setCookie(ctx, c)
285		if cmd.cookie == "" {
286			return flag.ErrHelp
287		}
288	}
289
290	if cmd.long {
291		r.Cookie = cmd.cookie
292	}
293
294	return cmd.WriteResult(r)
295}
296