Example #1
0
// 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)
	}
}
Example #2
0
// 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)
	}
}
Example #3
0
// 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)
	}
}
Example #4
0
// 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
}
Example #5
0
// 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
}
Example #6
0
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

}
Example #7
0
// 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
}