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