feat: terminal as a chosen OS user, create users from the panel (0.0.2)
- agent reports the OS user it runs as (enroll + poll header) - per-machine terminal user on the edit page; defaults to the agent's own user (root on Linux) - create a new user from the panel: name, password, optional sudo group - show the reason in the terminal when a shell fails to start - bump version to 0.0.2
This commit is contained in:
1 parent
be7fbabd9e
commit
3dde07f403
18 files changed
+530
-45
No files matched your search
@@ -1,9 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -152,13 +156,135 @@ func (s *Server) handleAgentEdit(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
s.render(w, "edit.html", map[string]any{
|
||||
data := map[string]any{
|
||||
"Agent": a,
|
||||
"CSRF": csrfToken(r),
|
||||
"ServerVersion": Version,
|
||||
"Outdated": a.Version != "" && a.Version != Version,
|
||||
"Updated": r.URL.Query().Get("updated") == "1",
|
||||
"UserOK": r.URL.Query().Get("uok"),
|
||||
"UserErr": r.URL.Query().Get("uerr"),
|
||||
}
|
||||
// The user list lives on the machine, so it is only known while it is online.
|
||||
if a.Online() {
|
||||
if users, err := s.agentUsers(id); err == nil {
|
||||
data["Users"] = users
|
||||
} else {
|
||||
data["UsersErr"] = err.Error()
|
||||
}
|
||||
}
|
||||
s.render(w, "edit.html", data)
|
||||
}
|
||||
|
||||
// usersTaskTimeout bounds how long the edit page waits for the agent.
|
||||
const usersTaskTimeout = 8 * time.Second
|
||||
|
||||
// agentUsers asks the agent who it runs as and which users it can log in as.
|
||||
func (s *Server) agentUsers(agentID string) (*proto.SysUsersResult, error) {
|
||||
taskID, ch := s.hub.CreateTask(agentID, proto.TaskSysUsers, nil)
|
||||
defer s.hub.DiscardTask(taskID)
|
||||
select {
|
||||
case res := <-ch:
|
||||
if !res.OK {
|
||||
// Agents older than 0.0.2 don't know this task.
|
||||
return nil, errors.New("клиент не поддерживает выбор пользователя — обновите его")
|
||||
}
|
||||
var out proto.SysUsersResult
|
||||
if err := json.Unmarshal(res.Data, &out); err != nil {
|
||||
return nil, errors.New("некорректный ответ клиента")
|
||||
}
|
||||
return &out, nil
|
||||
case <-time.After(usersTaskTimeout):
|
||||
return nil, errors.New("таймаут ожидания клиента")
|
||||
}
|
||||
}
|
||||
|
||||
// editUserRedirect returns to the edit page with a result banner.
|
||||
func editUserRedirect(w http.ResponseWriter, r *http.Request, id, key, msg string) {
|
||||
http.Redirect(w, r, "/agents/"+id+"/edit?"+key+"="+url.QueryEscape(msg), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleAgentSetUser picks the OS user terminals on this machine run as.
|
||||
func (s *Server) handleAgentSetUser(w http.ResponseWriter, r *http.Request) {
|
||||
if !checkCSRF(r) {
|
||||
http.Error(w, "bad csrf", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
id := r.PathValue("id")
|
||||
a, err := s.db.GetAgent(id)
|
||||
if err != nil || a == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(r.FormValue("user"))
|
||||
if !a.Online() {
|
||||
editUserRedirect(w, r, id, "uerr", "Машина офлайн — сменить пользователя нельзя.")
|
||||
return
|
||||
}
|
||||
// Only accept a user the machine itself reports, so a typo (or an agent too
|
||||
// old to honour the choice) can't silently leave the terminal as root.
|
||||
users, err := s.agentUsers(id)
|
||||
if err != nil {
|
||||
editUserRedirect(w, r, id, "uerr", err.Error())
|
||||
return
|
||||
}
|
||||
if !slices.Contains(users.Users, name) {
|
||||
editUserRedirect(w, r, id, "uerr", "Пользователя "+name+" нет на машине.")
|
||||
return
|
||||
}
|
||||
if name == users.Current {
|
||||
name = "" // follow the agent's own user
|
||||
}
|
||||
if err := s.db.SetAgentLoginUser(id, name); err != nil {
|
||||
editUserRedirect(w, r, id, "uerr", "Не удалось сохранить.")
|
||||
return
|
||||
}
|
||||
if admin := s.currentAdmin(r); admin != nil {
|
||||
s.db.Audit(admin.ID, id, "set_user:"+firstNonEmpty(name, users.Current))
|
||||
}
|
||||
editUserRedirect(w, r, id, "uok", "Терминал будет открываться от "+firstNonEmpty(name, users.Current)+".")
|
||||
}
|
||||
|
||||
// handleAgentAddUser creates a new OS user on the machine and switches the
|
||||
// terminal to it.
|
||||
func (s *Server) handleAgentAddUser(w http.ResponseWriter, r *http.Request) {
|
||||
if !checkCSRF(r) {
|
||||
http.Error(w, "bad csrf", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
id := r.PathValue("id")
|
||||
a, err := s.db.GetAgent(id)
|
||||
if err != nil || a == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if !a.Online() {
|
||||
editUserRedirect(w, r, id, "uerr", "Машина офлайн — создать пользователя нельзя.")
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
args, _ := json.Marshal(proto.SysUserAddArgs{
|
||||
Name: name,
|
||||
Password: r.FormValue("password"),
|
||||
Sudo: r.FormValue("sudo") == "1",
|
||||
})
|
||||
taskID, ch := s.hub.CreateTask(id, proto.TaskSysUserAdd, args)
|
||||
defer s.hub.DiscardTask(taskID)
|
||||
select {
|
||||
case res := <-ch:
|
||||
if !res.OK {
|
||||
editUserRedirect(w, r, id, "uerr", "Не удалось создать пользователя: "+res.Error)
|
||||
return
|
||||
}
|
||||
case <-time.After(30 * time.Second):
|
||||
editUserRedirect(w, r, id, "uerr", "Таймаут ожидания клиента.")
|
||||
return
|
||||
}
|
||||
_ = s.db.SetAgentLoginUser(id, name)
|
||||
if admin := s.currentAdmin(r); admin != nil {
|
||||
s.db.Audit(admin.ID, id, "create_user:"+name)
|
||||
}
|
||||
editUserRedirect(w, r, id, "uok", "Пользователь "+name+" создан и выбран для терминала.")
|
||||
}
|
||||
|
||||
// handleAgentUpdate asks the agent to download the latest binary and restart.
|
||||
|
||||
Reference in new issue
Block a user