// Helper function to send notification to a given user func TrySendNotificationToUser(userId string, subject, body string, mailer Mailer) (err error) { dbUser, err := user.FindOne(user.ById(userId)) if err != nil { return fmt.Errorf("Error finding user %v: %v", userId, err) } else if dbUser == nil { return fmt.Errorf("User %v not found", userId) } else { return TrySendNotification([]string{dbUser.Email()}, subject, body, mailer) } }
// UserMiddleware is middleware which checks for session tokens on the Request // and looks up and attaches a user for that token if one is found. func UserMiddleware(um auth.UserManager) func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { token := "" var err error // Grab token auth from cookies for _, cookie := range r.Cookies() { if cookie.Name == evergreen.AuthTokenCookie { if token, err = url.QueryUnescape(cookie.Value); err == nil { break } } } // Grab API auth details from header var authDataAPIKey, authDataName string if len(r.Header["Api-Key"]) > 0 { authDataAPIKey = r.Header["Api-Key"][0] } if len(r.Header["Auth-Username"]) > 0 { authDataName = r.Header["Auth-Username"][0] } if len(authDataName) == 0 && len(r.Header["Api-User"]) > 0 { authDataName = r.Header["Api-User"][0] } if len(token) > 0 { dbUser, err := um.GetUserByToken(token) if err != nil { evergreen.Logger.Logf(slogger.INFO, "Error getting user %v: %v", authDataName, err) } else { // Get the user's full details from the DB or create them if they don't exists dbUser, err := model.GetOrCreateUser(dbUser.Username(), dbUser.DisplayName(), dbUser.Email()) if err != nil { evergreen.Logger.Logf(slogger.INFO, "Error looking up user %v: %v", dbUser.Username(), err) } else { context.Set(r, RequestUser, dbUser) } } } else if len(authDataAPIKey) > 0 { dbUser, err := user.FindOne(user.ById(authDataName)) if dbUser != nil && err == nil { if dbUser.APIKey != authDataAPIKey { http.Error(rw, "Unauthorized - invalid API key", http.StatusUnauthorized) return } context.Set(r, RequestUser, dbUser) } else { evergreen.Logger.Logf(slogger.ERROR, "Error getting user: %v", err) } } next(rw, r) } }
// UserMiddleware checks for session tokens on the request, then looks up and attaches a user // for that token if one is found. func UserMiddleware(um auth.UserManager) func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { err := r.ParseForm() if err != nil { http.Error(w, "can't parse form?", http.StatusBadRequest) return } // Note: at this point the "token" is actually a json object in string form, // containing both the username and token. token := r.FormValue("id_token") if len(token) == 0 { next(w, r) return } authData := struct { Name string `json:"auth_user"` Token string `json:"auth_token"` APIKey string `json:"api_key"` }{} if err := util.ReadJSONInto(ioutil.NopCloser(strings.NewReader(token)), &authData); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if len(authData.Token) > 0 { // legacy auth - token lookup authedUser, err := um.GetUserByToken(authData.Token) if err != nil { evergreen.Logger.Logf(slogger.ERROR, "Error getting user: %v", err) } else { // Get the user's full details from the DB or create them if they don't exists dbUser, err := model.GetOrCreateUser(authedUser.Username(), authedUser.DisplayName(), authedUser.Email()) if err != nil { evergreen.Logger.Logf(slogger.ERROR, "Error looking up user %v: %v", authedUser.Username(), err) } else { context.Set(r, apiUserKey, dbUser) } } } else if len(authData.APIKey) > 0 { dbUser, err := user.FindOne(user.ById(authData.Name)) if dbUser != nil && err == nil { if dbUser.APIKey != authData.APIKey { http.Error(w, "Unauthorized - invalid API key", http.StatusUnauthorized) return } context.Set(r, apiUserKey, dbUser) } else { evergreen.Logger.Logf(slogger.ERROR, "Error getting user: %v", err) } } next(w, r) } }
// construct the change information // struct from a given version struct func constructChangeInfo(v *version.Version, notification *NotificationKey) (changeInfo *ChangeInfo) { changeInfo = &ChangeInfo{} switch notification.NotificationRequester { case evergreen.RepotrackerVersionRequester: changeInfo.Project = v.Identifier changeInfo.Author = v.Author changeInfo.Message = v.Message changeInfo.Revision = v.Revision changeInfo.Email = v.AuthorEmail case evergreen.PatchVersionRequester: // get the author and description from the patch request patch, err := patch.FindOne(patch.ByVersion(v.Id)) if err != nil { evergreen.Logger.Errorf(slogger.ERROR, "Error finding patch for version %v: %v", v.Id, err) return } if patch == nil { evergreen.Logger.Errorf(slogger.ERROR, "%v notification was unable to locate patch with version: %v", notification, v.Id) return } // get the display name and email for this user dbUser, err := user.FindOne(user.ById(patch.Author)) if err != nil { evergreen.Logger.Errorf(slogger.ERROR, "Error finding user %v: %v", patch.Author, err) changeInfo.Author = patch.Author changeInfo.Email = patch.Author } else if dbUser == nil { evergreen.Logger.Errorf(slogger.ERROR, "User %v not found", patch.Author) changeInfo.Author = patch.Author changeInfo.Email = patch.Author } else { changeInfo.Email = dbUser.Email() changeInfo.Author = dbUser.DisplayName() } changeInfo.Project = patch.Project changeInfo.Message = patch.Description changeInfo.Revision = patch.Id.Hex() } return }
// LoadClient places the evergreen command line client on the host, places a copy of the user's // settings onto the host, and makes the binary appear in the $PATH when the user logs in. // If successful, returns an instance of LoadClientResult which contains the paths where the // binary and config file were written to. func (init *HostInit) LoadClient(target *host.Host) (*LoadClientResult, error) { // Make sure we have the binary we want to upload - if it hasn't been built for the given // architecture, fail early cliBinaryPath, err := LocateCLIBinary(init.Settings, target.Distro.Arch) if err != nil { return nil, fmt.Errorf("couldn't locate CLI binary for upload: %v", err) } if target.ProvisionOptions == nil { return nil, fmt.Errorf("ProvisionOptions is nil") } if target.ProvisionOptions.OwnerId == "" { return nil, fmt.Errorf("OwnerId not set") } // get the information about the owner of the host owner, err := user.FindOne(user.ById(target.ProvisionOptions.OwnerId)) if err != nil { return nil, fmt.Errorf("couldn't fetch owner %v for host: %v", target.ProvisionOptions.OwnerId, err) } // 1. mkdir the destination directory on the host, // and modify ~/.profile so the target binary will be on the $PATH targetDir := "cli_bin" hostSSHInfo, err := util.ParseSSHInfo(target.Host) if err != nil { return nil, fmt.Errorf("error parsing ssh info %v: %v", target.Host, err) } cloudHost, err := providers.GetCloudHost(target, init.Settings) if err != nil { return nil, fmt.Errorf("Failed to get cloud host for %v: %v", target.Id, err) } sshOptions, err := cloudHost.GetSSHOptions() if err != nil { return nil, fmt.Errorf("Error getting ssh options for host %v: %v", target.Id, err) } sshOptions = append(sshOptions, "-o", "UserKnownHostsFile=/dev/null") mkdirOutput := &util.CappedWriter{&bytes.Buffer{}, 1024 * 1024} // Create the directory for the binary to be uploaded into. // Also, make a best effort to add the binary's location to $PATH upon login. If we can't do // this successfully, the command will still succeed, it just means that the user will have to // use an absolute path (or manually set $PATH in their shell) to execute it. makeShellCmd := &command.RemoteCommand{ CmdString: fmt.Sprintf("mkdir -m 777 -p ~/%s && (echo 'PATH=$PATH:~/%s' >> ~/.profile || true; echo 'PATH=$PATH:~/%s' >> ~/.bash_profile || true)", targetDir, targetDir, targetDir), Stdout: mkdirOutput, Stderr: mkdirOutput, RemoteHostName: hostSSHInfo.Hostname, User: target.User, Options: append([]string{"-p", hostSSHInfo.Port}, sshOptions...), } scpOut := &util.CappedWriter{&bytes.Buffer{}, 1024 * 1024} // run the make shell command with a timeout err = util.RunFunctionWithTimeout(makeShellCmd.Run, 30*time.Second) if err != nil { return nil, fmt.Errorf("error running setup command for cli, %v: '%v'", mkdirOutput.Buffer.String(), err) } // place the binary into the directory scpSetupCmd := &command.ScpCommand{ Source: cliBinaryPath, Dest: fmt.Sprintf("~/%s/evergreen", targetDir), Stdout: scpOut, Stderr: scpOut, RemoteHostName: hostSSHInfo.Hostname, User: target.User, Options: append([]string{"-P", hostSSHInfo.Port}, sshOptions...), } // run the command to scp the setup script with a timeout err = util.RunFunctionWithTimeout(scpSetupCmd.Run, 3*time.Minute) if err != nil { return nil, fmt.Errorf("error running SCP command for cli, %v: '%v'", scpOut.Buffer.String(), err) } // 4. Write a settings file for the user that owns the host, and scp it to the directory outputStruct := model.CLISettings{ User: owner.Id, APIKey: owner.APIKey, APIServerHost: init.Settings.ApiUrl + "/api", UIServerHost: init.Settings.Ui.Url, } outputJSON, err := json.Marshal(outputStruct) if err != nil { return nil, err } tempFileName, err := util.WriteTempFile("", outputJSON) if err != nil { return nil, err } defer os.Remove(tempFileName) scpYmlCommand := &command.ScpCommand{ Source: tempFileName, Dest: fmt.Sprintf("~/%s/.evergreen.yml", targetDir), Stdout: scpOut, Stderr: scpOut, RemoteHostName: hostSSHInfo.Hostname, User: target.User, Options: append([]string{"-P", hostSSHInfo.Port}, sshOptions...), } err = util.RunFunctionWithTimeout(scpYmlCommand.Run, 30*time.Second) if err != nil { return nil, fmt.Errorf("error running SCP command for evergreen.yml, %v: '%v'", scpOut.Buffer.String(), err) } return &LoadClientResult{ BinaryPath: fmt.Sprintf("~/%s/evergreen", targetDir), ConfigPath: fmt.Sprintf("~/%s/.evergreen.yml", targetDir), }, nil }
func (uis *UIServer) requestNewHost(w http.ResponseWriter, r *http.Request) { authedUser := MustHaveUser(r) putParams := struct { Distro string `json:"distro"` KeyName string `json:"key_name"` PublicKey string `json:"public_key"` SaveKey bool `json:"save_key"` UserData string `json:"userdata"` }{} if err := util.ReadJSONInto(r.Body, &putParams); err != nil { http.Error(w, fmt.Sprintf("Bad json in request: %v", err), http.StatusBadRequest) return } opts := spawn.Options{ Distro: putParams.Distro, UserName: authedUser.Username(), PublicKey: putParams.PublicKey, UserData: putParams.UserData, } spawner := spawn.New(&uis.Settings) if err := spawner.Validate(opts); err != nil { errCode := http.StatusBadRequest if _, ok := err.(spawn.BadOptionsErr); !ok { errCode = http.StatusInternalServerError } uis.LoggedError(w, r, errCode, err) return } // save the supplied public key if needed if putParams.SaveKey { dbuser, err := user.FindOne(user.ById(authedUser.Username())) if err != nil { uis.LoggedError(w, r, http.StatusInternalServerError, fmt.Errorf("Error fetching user: %v", err)) return } err = model.AddUserPublicKey(dbuser.Id, putParams.KeyName, putParams.PublicKey) if err != nil { uis.LoggedError(w, r, http.StatusInternalServerError, fmt.Errorf("Error saving public key: %v", err)) return } PushFlash(uis.CookieStore, r, w, NewSuccessFlash("Public key successfully saved.")) } // Start a background goroutine that handles host creation/setup. go func() { host, err := spawner.CreateHost(opts) if err != nil { evergreen.Logger.Logf(slogger.ERROR, "error spawning host: %v", err) mailErr := notify.TrySendNotificationToUser(authedUser.Email(), fmt.Sprintf("Spawning failed"), err.Error(), notify.ConstructMailer(uis.Settings.Notify)) if mailErr != nil { evergreen.Logger.Logf(slogger.ERROR, "Failed to send notification: %v", mailErr) } if host != nil { // a host was inserted - we need to clean it up dErr := host.SetDecommissioned() if err != nil { evergreen.Logger.Logf(slogger.ERROR, "Failed to set host %v decommissioned: %v", host.Id, dErr) } } return } }() PushFlash(uis.CookieStore, r, w, NewSuccessFlash("Host spawned")) uis.WriteJSON(w, http.StatusOK, "Host successfully spawned") return }
// spawnHostExpirationWarnings is a notificationBuilder to build any necessary // warnings about hosts that will be expiring soon (but haven't expired yet) func spawnHostExpirationWarnings(settings *evergreen.Settings) ([]notification, error) { evergreen.Logger.Logf(slogger.INFO, "Building spawned host expiration"+ " warnings...") // sanity check, since the thresholds are supplied in code if len(spawnWarningThresholds) == 0 { evergreen.Logger.Logf(slogger.WARN, "there are no currently set warning"+ " thresholds for spawned hosts - users will not receive emails"+ " warning them of imminent host expiration") return nil, nil } // assumed to be the first warning threshold (the least recent one) firstWarningThreshold := spawnWarningThresholds[len(spawnWarningThresholds)-1] // find all spawned hosts that have passed at least one warning threshold now := time.Now() thresholdTime := now.Add(firstWarningThreshold) hosts, err := host.Find(host.ByExpiringBetween(now, thresholdTime)) if err != nil { return nil, fmt.Errorf("error finding spawned hosts that will be"+ " expiring soon: %v", err) } // the eventual list of warning notifications to be sent warnings := []notification{} for _, h := range hosts { // figure out the most recent expiration notification threshold the host // has crossed threshold := lastWarningThresholdCrossed(&h) // for keying into the host's notifications map thresholdKey := strconv.Itoa(int(threshold.Minutes())) // if a notification has already been sent for the threshold for this // host, skip it if h.Notifications[thresholdKey] { continue } evergreen.Logger.Logf(slogger.INFO, "Warning needs to be sent for threshold"+ " '%v' for host %v", thresholdKey, h.Id) // fetch information about the user we are notifying userToNotify, err := user.FindOne(user.ById(h.StartedBy)) if err != nil { return nil, fmt.Errorf("error finding user to notify by Id %v: %v", h.StartedBy, err) } // if we didn't find a user (in the case of testing) set the timezone to "" // to avoid triggering a nil pointer exception timezone := "" if userToNotify != nil { timezone = userToNotify.Settings.Timezone } var expirationTimeFormatted string // use our fetched information to load proper time zone to notify the user with // (if time zone is empty, defaults to UTC) loc, err := time.LoadLocation(timezone) if err != nil { evergreen.Logger.Logf(slogger.ERROR, "Error loading timezone for email format with user_id %v: %v", userToNotify.Id, err) expirationTimeFormatted = h.ExpirationTime.Format(time.RFC1123) } else { expirationTimeFormatted = h.ExpirationTime.In(loc).Format(time.RFC1123) } // we need to send a notification for the threshold for this host hostNotification := notification{ recipient: h.StartedBy, subject: fmt.Sprintf("%v host termination reminder", h.Distro.Id), message: fmt.Sprintf("Your %v host with id %v will be terminated"+ " at %v, in %v minutes. Visit %v to extend its lifetime.", h.Distro.Id, h.Id, expirationTimeFormatted, h.ExpirationTime.Sub(time.Now()), settings.Ui.Url+"/ui/spawn"), threshold: thresholdKey, host: h, callback: func(h host.Host, thresholdKey string) error { return h.SetExpirationNotification(thresholdKey) }, } // add it to the list warnings = append(warnings, hostNotification) } evergreen.Logger.Logf(slogger.INFO, "Built %v warnings about imminently"+ " expiring hosts", len(warnings)) return warnings, nil }