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