github.com/vmware/govmomi@v0.43.0/govc/session/login.go (about) 1 /* 2 Copyright (c) 2018 VMware, Inc. All Rights Reserved. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package session 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "flag" 24 "fmt" 25 "io" 26 "net/http" 27 "net/url" 28 "os" 29 "strings" 30 "time" 31 32 "github.com/vmware/govmomi/govc/cli" 33 "github.com/vmware/govmomi/govc/flags" 34 "github.com/vmware/govmomi/session" 35 "github.com/vmware/govmomi/sts" 36 "github.com/vmware/govmomi/vapi/rest" 37 "github.com/vmware/govmomi/vim25" 38 "github.com/vmware/govmomi/vim25/methods" 39 "github.com/vmware/govmomi/vim25/soap" 40 ) 41 42 type login struct { 43 *flags.ClientFlag 44 *flags.OutputFlag 45 46 clone bool 47 issue bool 48 renew bool 49 long bool 50 vapi bool 51 ticket string 52 life time.Duration 53 cookie string 54 token string 55 ext string 56 as string 57 method string 58 } 59 60 func init() { 61 cli.Register("session.login", &login{}) 62 } 63 64 func (cmd *login) Register(ctx context.Context, f *flag.FlagSet) { 65 cmd.ClientFlag, ctx = flags.NewClientFlag(ctx) 66 cmd.ClientFlag.Register(ctx, f) 67 cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx) 68 cmd.OutputFlag.Register(ctx, f) 69 70 f.BoolVar(&cmd.clone, "clone", false, "Acquire clone ticket") 71 f.BoolVar(&cmd.issue, "issue", false, "Issue SAML token") 72 f.BoolVar(&cmd.renew, "renew", false, "Renew SAML token") 73 f.BoolVar(&cmd.vapi, "r", false, "REST login") 74 f.DurationVar(&cmd.life, "lifetime", time.Minute*10, "SAML token lifetime") 75 f.BoolVar(&cmd.long, "l", false, "Output session cookie") 76 f.StringVar(&cmd.ticket, "ticket", "", "Use clone ticket for login") 77 f.StringVar(&cmd.cookie, "cookie", "", "Set HTTP cookie for an existing session") 78 f.StringVar(&cmd.token, "token", "", "Use SAML token for login or as issue identity") 79 f.StringVar(&cmd.ext, "extension", "", "Extension name") 80 f.StringVar(&cmd.as, "as", "", "Impersonate user") 81 f.StringVar(&cmd.method, "X", "", "HTTP method") 82 } 83 84 func (cmd *login) Process(ctx context.Context) error { 85 if err := cmd.OutputFlag.Process(ctx); err != nil { 86 return err 87 } 88 return cmd.ClientFlag.Process(ctx) 89 } 90 91 func (cmd *login) Usage() string { 92 return "[PATH]" 93 } 94 95 func (cmd *login) Description() string { 96 return `Session login. 97 98 The session.login command is optional, all other govc commands will auto login when given credentials. 99 The session.login command can be used to: 100 - Persist a session without writing to disk via the '-cookie' flag 101 - Acquire a clone ticket 102 - Login using a clone ticket 103 - Login using a vCenter Extension certificate 104 - Issue a SAML token 105 - Renew a SAML token 106 - Login using a SAML token 107 - Impersonate a user 108 - Avoid passing credentials to other govc commands 109 - Send an authenticated raw HTTP request 110 111 The session.login command can be used for authenticated curl-style HTTP requests when a PATH arg is given. 112 PATH may also contain a query string. The '-u' flag (GOVC_URL) is used for the URL scheme, host and port. 113 The request method (-X) defaults to GET. When set to POST, PUT or PATCH, a request body must be provided via stdin. 114 115 Examples: 116 govc session.login -u root:password@host # Creates a cached session in ~/.govmomi/sessions 117 govc session.ls -u root@host # Use the cached session with another command 118 ticket=$(govc session.login -u root@host -clone) 119 govc session.login -u root@host -ticket $ticket 120 govc session.login -u Administrator@vsphere.local:password@host -as other@vsphere.local 121 govc session.login -u host -extension com.vmware.vsan.health -cert rui.crt -key rui.key 122 token=$(govc session.login -u host -cert user.crt -key user.key -issue) # HoK token 123 bearer=$(govc session.login -u user:pass@host -issue) # Bearer token 124 token=$(govc session.login -u host -cert user.crt -key user.key -issue -token "$bearer") 125 govc session.login -u host -cert user.crt -key user.key -token "$token" 126 token=$(govc session.login -u host -cert user.crt -key user.key -renew -lifetime 24h -token "$token") 127 # HTTP requests 128 govc session.login -r -X GET /api/vcenter/namespace-management/clusters | jq . 129 govc session.login -r -X POST /rest/vcenter/cluster/modules <<<'{"spec": {"cluster": "domain-c9"}}'` 130 } 131 132 type ticketResult struct { 133 cmd *login 134 Ticket string `json:",omitempty"` 135 Token string `json:",omitempty"` 136 Cookie string `json:",omitempty"` 137 } 138 139 func (r *ticketResult) Write(w io.Writer) error { 140 var output []string 141 142 for _, val := range []string{r.Ticket, r.Token, r.Cookie} { 143 if val != "" { 144 output = append(output, val) 145 } 146 } 147 148 if len(output) == 0 { 149 return nil 150 } 151 152 fmt.Fprintln(w, strings.Join(output, " ")) 153 154 return nil 155 } 156 157 // Logout is called by cli.Run() 158 // We override ClientFlag's Logout impl to avoid ending a session when -persist-session=false, 159 // otherwise Logout would invalidate the cookie and/or ticket. 160 func (cmd *login) Logout(ctx context.Context) error { 161 if cmd.long || cmd.clone || cmd.issue { 162 return nil 163 } 164 return cmd.ClientFlag.Logout(ctx) 165 } 166 167 func (cmd *login) cloneSession(ctx context.Context, c *vim25.Client) error { 168 return session.NewManager(c).CloneSession(ctx, cmd.ticket) 169 } 170 171 func (cmd *login) issueToken(ctx context.Context, vc *vim25.Client) (string, error) { 172 c, err := sts.NewClient(ctx, vc) 173 if err != nil { 174 return "", err 175 } 176 c.RoundTripper = cmd.RoundTripper(c.Client) 177 178 req := sts.TokenRequest{ 179 Certificate: c.Certificate(), 180 Userinfo: cmd.Session.URL.User, 181 Renewable: true, 182 Delegatable: true, 183 ActAs: cmd.token != "", 184 Token: cmd.token, 185 Lifetime: cmd.life, 186 } 187 188 issue := c.Issue 189 if cmd.renew { 190 issue = c.Renew 191 } 192 193 s, err := issue(ctx, req) 194 if err != nil { 195 return "", err 196 } 197 198 if req.Token != "" { 199 duration := s.Lifetime.Expires.Sub(s.Lifetime.Created) 200 if duration < req.Lifetime { 201 // The granted lifetime is that of the bearer token, which is 5min max. 202 // Extend the lifetime via Renew. 203 req.Token = s.Token 204 if s, err = c.Renew(ctx, req); err != nil { 205 return "", err 206 } 207 } 208 } 209 210 return s.Token, nil 211 } 212 213 func (cmd *login) loginByToken(ctx context.Context, c *vim25.Client) error { 214 header := soap.Header{ 215 Security: &sts.Signer{ 216 Certificate: c.Certificate(), 217 Token: cmd.token, 218 }, 219 } 220 221 // something behind the LoginByToken scene requires a version from /sdk/vimServiceVersions.xml 222 // in the SOAPAction header. For example, if vim25.Version is "7.0" but the service version is "6.3", 223 // LoginByToken fails with: 'VersionMismatchFaultCode: Unsupported version URI "urn:vim25/7.0"' 224 if c.Version == vim25.Version { 225 _ = c.UseServiceVersion() 226 } 227 228 return session.NewManager(c).LoginByToken(c.WithHeader(ctx, header)) 229 } 230 231 func (cmd *login) loginRestByToken(ctx context.Context, c *rest.Client) error { 232 signer := &sts.Signer{ 233 Certificate: c.Certificate(), 234 Token: cmd.token, 235 } 236 237 return c.LoginByToken(c.WithSigner(ctx, signer)) 238 } 239 240 func (cmd *login) loginByExtension(ctx context.Context, c *vim25.Client) error { 241 return session.NewManager(c).LoginExtensionByCertificate(ctx, cmd.ext) 242 } 243 244 func (cmd *login) impersonateUser(ctx context.Context, c *vim25.Client) error { 245 m := session.NewManager(c) 246 if err := m.Login(ctx, cmd.Session.URL.User); err != nil { 247 return err 248 } 249 return m.ImpersonateUser(ctx, cmd.as) 250 } 251 252 func (cmd *login) setCookie(ctx context.Context, c *vim25.Client) error { 253 url := c.URL() 254 jar := c.Client.Jar 255 cookies := jar.Cookies(url) 256 add := true 257 258 cookie := &http.Cookie{ 259 Name: soap.SessionCookieName, 260 } 261 262 for _, e := range cookies { 263 if e.Name == cookie.Name { 264 add = false 265 cookie = e 266 break 267 } 268 } 269 270 if cmd.cookie == "" { 271 // This is the cookie from Set-Cookie after a Login or CloneSession 272 cmd.cookie = cookie.Value 273 } else { 274 // The cookie flag is set, set the HTTP header and skip Login() 275 cookie.Value = cmd.cookie 276 if add { 277 cookies = append(cookies, cookie) 278 } 279 jar.SetCookies(url, cookies) 280 281 // Check the session is still valid 282 _, err := methods.GetCurrentTime(ctx, c) 283 if err != nil { 284 return err 285 } 286 } 287 288 return nil 289 } 290 291 func (cmd *login) setRestCookie(ctx context.Context, c *rest.Client) error { 292 if cmd.cookie == "" { 293 cmd.cookie = c.SessionID() 294 } else { 295 c.SessionID(cmd.cookie) 296 297 // Check the session is still valid 298 s, err := c.Session(ctx) 299 if err != nil { 300 return err 301 } 302 if s == nil { 303 return errors.New(http.StatusText(http.StatusUnauthorized)) 304 } 305 } 306 307 return nil 308 } 309 310 func nologinSOAP(_ context.Context, _ *vim25.Client) error { 311 return nil 312 } 313 314 func nologinREST(_ context.Context, _ *rest.Client) error { 315 return nil 316 } 317 318 func (cmd *login) Run(ctx context.Context, f *flag.FlagSet) error { 319 if cmd.renew { 320 cmd.issue = true 321 } 322 switch { 323 case cmd.ticket != "": 324 cmd.Session.LoginSOAP = cmd.cloneSession 325 case cmd.cookie != "": 326 if cmd.vapi { 327 cmd.Session.LoginSOAP = nologinSOAP 328 cmd.Session.LoginREST = cmd.setRestCookie 329 } else { 330 cmd.Session.LoginSOAP = cmd.setCookie 331 cmd.Session.LoginREST = nologinREST 332 } 333 case cmd.token != "": 334 cmd.Session.LoginSOAP = cmd.loginByToken 335 cmd.Session.LoginREST = cmd.loginRestByToken 336 case cmd.ext != "": 337 cmd.Session.LoginSOAP = cmd.loginByExtension 338 case cmd.as != "": 339 cmd.Session.LoginSOAP = cmd.impersonateUser 340 case cmd.issue: 341 cmd.Session.LoginSOAP = nologinSOAP 342 cmd.Session.LoginREST = nologinREST 343 } 344 345 c, err := cmd.Client() 346 if err != nil { 347 return err 348 } 349 350 r := &ticketResult{cmd: cmd} 351 352 switch { 353 case cmd.clone: 354 m := session.NewManager(c) 355 r.Ticket, err = m.AcquireCloneTicket(ctx) 356 if err != nil { 357 return err 358 } 359 case cmd.issue: 360 r.Token, err = cmd.issueToken(ctx, c) 361 if err != nil { 362 return err 363 } 364 return cmd.WriteResult(r) 365 } 366 367 var rc *rest.Client 368 if cmd.vapi { 369 rc, err = cmd.RestClient() 370 if err != nil { 371 return err 372 } 373 } 374 375 if f.NArg() == 1 { 376 u, err := url.Parse(f.Arg(0)) 377 if err != nil { 378 return err 379 } 380 vc := c.URL() 381 u.Scheme = vc.Scheme 382 u.Host = vc.Host 383 384 var body io.Reader 385 386 switch cmd.method { 387 case http.MethodPost, http.MethodPut, http.MethodPatch: 388 // strings.Reader here as /api wants a Content-Length header 389 b, err := io.ReadAll(os.Stdin) 390 if err != nil { 391 return err 392 } 393 body = bytes.NewReader(b) 394 default: 395 body = strings.NewReader("") 396 } 397 398 req, err := http.NewRequest(cmd.method, u.String(), body) 399 if err != nil { 400 return err 401 } 402 403 if cmd.vapi { 404 return rc.Do(ctx, req, cmd.Out) 405 } 406 407 return c.Do(ctx, req, func(res *http.Response) error { 408 if res.StatusCode != http.StatusOK { 409 return errors.New(res.Status) 410 } 411 _, err := io.Copy(cmd.Out, res.Body) 412 return err 413 }) 414 } 415 416 if cmd.cookie == "" { 417 if cmd.vapi { 418 _ = cmd.setRestCookie(ctx, rc) 419 } else { 420 _ = cmd.setCookie(ctx, c) 421 } 422 if cmd.cookie == "" { 423 return flag.ErrHelp 424 } 425 } 426 427 if cmd.long { 428 r.Cookie = cmd.cookie 429 } 430 431 return cmd.WriteResult(r) 432 }