/* Copyright (c) 2018 VMware, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package session import ( "bytes" "context" "errors" "flag" "fmt" "io" "io/ioutil" "net/http" "net/url" "os" "strings" "time" "github.com/vmware/govmomi/govc/cli" "github.com/vmware/govmomi/govc/flags" "github.com/vmware/govmomi/session" "github.com/vmware/govmomi/sts" "github.com/vmware/govmomi/vapi/rest" "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/methods" "github.com/vmware/govmomi/vim25/soap" ) type login struct { *flags.ClientFlag *flags.OutputFlag clone bool issue bool renew bool long bool vapi bool ticket string life time.Duration cookie string token string ext string method string } func init() { cli.Register("session.login", &login{}) } func (cmd *login) Register(ctx context.Context, f *flag.FlagSet) { cmd.ClientFlag, ctx = flags.NewClientFlag(ctx) cmd.ClientFlag.Register(ctx, f) cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx) cmd.OutputFlag.Register(ctx, f) f.BoolVar(&cmd.clone, "clone", false, "Acquire clone ticket") f.BoolVar(&cmd.issue, "issue", false, "Issue SAML token") f.BoolVar(&cmd.renew, "renew", false, "Renew SAML token") f.BoolVar(&cmd.vapi, "r", false, "REST login") f.DurationVar(&cmd.life, "lifetime", time.Minute*10, "SAML token lifetime") f.BoolVar(&cmd.long, "l", false, "Output session cookie") f.StringVar(&cmd.ticket, "ticket", "", "Use clone ticket for login") f.StringVar(&cmd.cookie, "cookie", "", "Set HTTP cookie for an existing session") f.StringVar(&cmd.token, "token", "", "Use SAML token for login or as issue identity") f.StringVar(&cmd.ext, "extension", "", "Extension name") f.StringVar(&cmd.method, "X", "", "HTTP method") } func (cmd *login) Process(ctx context.Context) error { if err := cmd.OutputFlag.Process(ctx); err != nil { return err } return cmd.ClientFlag.Process(ctx) } func (cmd *login) Usage() string { return "[PATH]" } func (cmd *login) Description() string { return `Session login. The session.login command is optional, all other govc commands will auto login when given credentials. The session.login command can be used to: - Persist a session without writing to disk via the '-cookie' flag - Acquire a clone ticket - Login using a clone ticket - Login using a vCenter Extension certificate - Issue a SAML token - Renew a SAML token - Login using a SAML token - Avoid passing credentials to other govc commands - Send an authenticated raw HTTP request The session.login command can be used for authenticated curl-style HTTP requests when a PATH arg is given. PATH may also contain a query string. The '-u' flag (GOVC_URL) is used for the URL scheme, host and port. The request method (-X) defaults to GET. When set to POST, PUT or PATCH, a request body must be provided via stdin. Examples: govc session.login -u root:password@host # Creates a cached session in ~/.govmomi/sessions govc session.ls -u root@host # Use the cached session with another command ticket=$(govc session.login -u root@host -clone) govc session.login -u root@host -ticket $ticket govc session.login -u host -extension com.vmware.vsan.health -cert rui.crt -key rui.key token=$(govc session.login -u host -cert user.crt -key user.key -issue) # HoK token bearer=$(govc session.login -u user:pass@host -issue) # Bearer token token=$(govc session.login -u host -cert user.crt -key user.key -issue -token "$bearer") govc session.login -u host -cert user.crt -key user.key -token "$token" token=$(govc session.login -u host -cert user.crt -key user.key -renew -lifetime 24h -token "$token") # HTTP requests govc session.login -r -X GET /api/vcenter/namespace-management/clusters | jq . govc session.login -r -X POST /rest/vcenter/cluster/modules <<<'{"spec": {"cluster": "domain-c9"}}'` } type ticketResult struct { cmd *login Ticket string `json:",omitempty"` Token string `json:",omitempty"` Cookie string `json:",omitempty"` } func (r *ticketResult) Write(w io.Writer) error { var output []string for _, val := range []string{r.Ticket, r.Token, r.Cookie} { if val != "" { output = append(output, val) } } if len(output) == 0 { return nil } fmt.Fprintln(w, strings.Join(output, " ")) return nil } // Logout is called by cli.Run() // We override ClientFlag's Logout impl to avoid ending a session when -persist-session=false, // otherwise Logout would invalidate the cookie and/or ticket. func (cmd *login) Logout(ctx context.Context) error { if cmd.long || cmd.clone || cmd.issue { return nil } return cmd.ClientFlag.Logout(ctx) } func (cmd *login) cloneSession(ctx context.Context, c *vim25.Client) error { return session.NewManager(c).CloneSession(ctx, cmd.ticket) } func (cmd *login) issueToken(ctx context.Context, vc *vim25.Client) (string, error) { c, err := sts.NewClient(ctx, vc) if err != nil { return "", err } c.RoundTripper = cmd.RoundTripper(c.Client) req := sts.TokenRequest{ Certificate: c.Certificate(), Userinfo: cmd.Session.URL.User, Renewable: true, Delegatable: true, ActAs: cmd.token != "", Token: cmd.token, Lifetime: cmd.life, } issue := c.Issue if cmd.renew { issue = c.Renew } s, err := issue(ctx, req) if err != nil { return "", err } if req.Token != "" { duration := s.Lifetime.Expires.Sub(s.Lifetime.Created) if duration < req.Lifetime { // The granted lifetime is that of the bearer token, which is 5min max. // Extend the lifetime via Renew. req.Token = s.Token if s, err = c.Renew(ctx, req); err != nil { return "", err } } } return s.Token, nil } func (cmd *login) loginByToken(ctx context.Context, c *vim25.Client) error { header := soap.Header{ Security: &sts.Signer{ Certificate: c.Certificate(), Token: cmd.token, }, } // something behind the LoginByToken scene requires a version from /sdk/vimServiceVersions.xml // in the SOAPAction header. For example, if vim25.Version is "7.0" but the service version is "6.3", // LoginByToken fails with: 'VersionMismatchFaultCode: Unsupported version URI "urn:vim25/7.0"' if c.Version == vim25.Version { _ = c.UseServiceVersion() } return session.NewManager(c).LoginByToken(c.WithHeader(ctx, header)) } func (cmd *login) loginRestByToken(ctx context.Context, c *rest.Client) error { signer := &sts.Signer{ Certificate: c.Certificate(), Token: cmd.token, } return c.LoginByToken(c.WithSigner(ctx, signer)) } func (cmd *login) loginByExtension(ctx context.Context, c *vim25.Client) error { return session.NewManager(c).LoginExtensionByCertificate(ctx, cmd.ext) } func (cmd *login) setCookie(ctx context.Context, c *vim25.Client) error { url := c.URL() jar := c.Client.Jar cookies := jar.Cookies(url) add := true cookie := &http.Cookie{ Name: soap.SessionCookieName, } for _, e := range cookies { if e.Name == cookie.Name { add = false cookie = e break } } if cmd.cookie == "" { // This is the cookie from Set-Cookie after a Login or CloneSession cmd.cookie = cookie.Value } else { // The cookie flag is set, set the HTTP header and skip Login() cookie.Value = cmd.cookie if add { cookies = append(cookies, cookie) } jar.SetCookies(url, cookies) // Check the session is still valid _, err := methods.GetCurrentTime(ctx, c) if err != nil { return err } } return nil } func (cmd *login) setRestCookie(ctx context.Context, c *rest.Client) error { if cmd.cookie == "" { cmd.cookie = c.SessionID() } else { c.SessionID(cmd.cookie) // Check the session is still valid s, err := c.Session(ctx) if err != nil { return err } if s == nil { return errors.New(http.StatusText(http.StatusUnauthorized)) } } return nil } func nologinSOAP(_ context.Context, _ *vim25.Client) error { return nil } func nologinREST(_ context.Context, _ *rest.Client) error { return nil } func (cmd *login) Run(ctx context.Context, f *flag.FlagSet) error { if cmd.renew { cmd.issue = true } switch { case cmd.ticket != "": cmd.Session.LoginSOAP = cmd.cloneSession case cmd.cookie != "": if cmd.vapi { cmd.Session.LoginSOAP = nologinSOAP cmd.Session.LoginREST = cmd.setRestCookie } else { cmd.Session.LoginSOAP = cmd.setCookie cmd.Session.LoginREST = nologinREST } case cmd.token != "": cmd.Session.LoginSOAP = cmd.loginByToken cmd.Session.LoginREST = cmd.loginRestByToken case cmd.ext != "": cmd.Session.LoginSOAP = cmd.loginByExtension case cmd.issue: cmd.Session.LoginSOAP = nologinSOAP cmd.Session.LoginREST = nologinREST } c, err := cmd.Client() if err != nil { return err } r := &ticketResult{cmd: cmd} switch { case cmd.clone: m := session.NewManager(c) r.Ticket, err = m.AcquireCloneTicket(ctx) if err != nil { return err } case cmd.issue: r.Token, err = cmd.issueToken(ctx, c) if err != nil { return err } return cmd.WriteResult(r) } var rc *rest.Client if cmd.vapi { rc, err = cmd.RestClient() if err != nil { return err } } if f.NArg() == 1 { u, err := url.Parse(f.Arg(0)) if err != nil { return err } vc := c.URL() u.Scheme = vc.Scheme u.Host = vc.Host var body io.Reader switch cmd.method { case http.MethodPost, http.MethodPut, http.MethodPatch: // strings.Reader here as /api wants a Content-Length header b, err := ioutil.ReadAll(os.Stdin) if err != nil { return err } body = bytes.NewReader(b) default: body = strings.NewReader("") } req, err := http.NewRequest(cmd.method, u.String(), body) if err != nil { return err } if cmd.vapi { return rc.Do(ctx, req, cmd.Out) } return c.Do(ctx, req, func(res *http.Response) error { if res.StatusCode != http.StatusOK { return errors.New(res.Status) } _, err := io.Copy(cmd.Out, res.Body) return err }) } if cmd.cookie == "" { if cmd.vapi { _ = cmd.setRestCookie(ctx, rc) } else { _ = cmd.setCookie(ctx, c) } if cmd.cookie == "" { return flag.ErrHelp } } if cmd.long { r.Cookie = cmd.cookie } return cmd.WriteResult(r) }