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:
redtea committed 2026-10-13 01:28:57 +03:00
1 parent be7fbabd9e
commit 3dde07f403
18 files changed
+530 -45

No files matched your search

+127 -1
View File
@@ -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.