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  }