feat: user roles - admins and users limited to the machines given to them, user editor with machines, password sign-in and its lock; direct address without Cloudflare as the agents' fallback and in the installer; version 0.1.2

This commit is contained in:
redtea committed 2026-10-15 10:56:19 +03:00
1 parent 36dc12b69d
commit 021da93229
29 files changed
+844 -97

No files matched your search

+53 -14
View File
@@ -71,6 +71,7 @@ func New(opts Options) (*Server, error) {
if err := opts.DB.ensureDefaults(); err != nil {
return nil, err
}
mainHost = func() string { return directHost(opts.DB.HostAddress()) }
staticFS, _ := fs.Sub(webFS, "web/static")
assets := assetHashes(staticFS)
funcs := template.FuncMap{
@@ -139,13 +140,19 @@ func (s *Server) tunnelListenAddr(port int) string {
}
// tunnelAdvert is the tunnel address handed to agents as "host:port". The host
// is empty unless one is set in Settings; agents then use the panel's host.
// is the one set in Settings, else the direct address's (the main one may be
// behind a proxy that carries no raw TCP), else empty: agents then use the
// panel's host.
func (s *Server) tunnelAdvert() string {
port := s.TunnelPort()
if port == 0 {
return ""
}
return net.JoinHostPort(s.db.TunnelHost(), strconv.Itoa(port))
host := s.db.TunnelHost()
if host == "" {
host = directHost(s.db.DirectAddress())
}
return net.JoinHostPort(host, strconv.Itoa(port))
}
// setTunnelPort moves the direct tunnel to another port. Connected agents are
@@ -229,15 +236,17 @@ func (s *Server) Handler() http.Handler {
mux.Handle("POST /agents/{id}/user", s.requireAdmin(s.handleAgentSetUser))
mux.Handle("POST /agents/{id}/transport", s.requireAdmin(s.handleAgentTransport))
mux.Handle("POST /agents/{id}/users/add", s.requireAdmin(s.handleAgentAddUser))
mux.Handle("POST /agents/{id}/delete", s.requireAdmin(s.handleAgentDelete))
mux.Handle("GET /settings", s.requireAdmin(s.handleSettings))
mux.Handle("POST /settings/host", s.requireAdmin(s.handleSetHost))
mux.Handle("POST /settings/tunnel", s.requireAdmin(s.handleSetTunnel))
mux.Handle("POST /settings/secret/rotate", s.requireAdmin(s.handleRotateSecret))
mux.Handle("POST /settings/install/code", s.requireAdmin(s.handleInstallCode))
mux.Handle("POST /settings/admins/add", s.requireAdmin(s.handleAddAdmin))
mux.Handle("POST /settings/admins/delete", s.requireAdmin(s.handleDeleteAdmin))
mux.Handle("POST /settings/admins/keys/reset", s.requireAdmin(s.handleResetAdminKeys))
mux.Handle("POST /agents/{id}/delete", s.adminsOnly(s.handleAgentDelete))
mux.Handle("GET /settings", s.adminsOnly(s.handleSettings))
mux.Handle("POST /settings/host", s.adminsOnly(s.handleSetHost))
mux.Handle("POST /settings/tunnel", s.adminsOnly(s.handleSetTunnel))
mux.Handle("POST /settings/secret/rotate", s.adminsOnly(s.handleRotateSecret))
mux.Handle("POST /settings/install/code", s.adminsOnly(s.handleInstallCode))
mux.Handle("POST /settings/admins/add", s.adminsOnly(s.handleAddAdmin))
mux.Handle("POST /settings/admins/delete", s.adminsOnly(s.handleDeleteAdmin))
mux.Handle("GET /api/panel/users/{id}/agents", s.adminsOnly(s.handleUserAgents))
mux.Handle("POST /api/panel/users/{id}", s.adminsOnly(s.handleUserEdit))
mux.Handle("POST /settings/admins/keys/reset", s.adminsOnly(s.handleResetAdminKeys))
mux.Handle("GET /api/panel/account", s.requireAdmin(s.handleAccount))
mux.Handle("POST /api/panel/account/keys/begin", s.requireAdmin(s.handleKeyRegisterBegin))
mux.Handle("POST /api/panel/account/keys/finish", s.requireAdmin(s.handleKeyRegisterFinish))
@@ -247,9 +256,9 @@ func (s *Server) Handler() http.Handler {
mux.Handle("GET /api/panel/pin", s.requireAdmin(s.handlePinState))
mux.Handle("POST /api/panel/pin/check", s.requireAdmin(s.handlePinCheck))
mux.Handle("POST /api/panel/account/password/change", s.requireAdmin(s.handlePasswordChange))
mux.Handle("POST /settings/ufw/toggle", s.requireAdmin(s.handleUfwToggle))
mux.Handle("POST /settings/ufw/add", s.requireAdmin(s.handleUfwAdd))
mux.Handle("POST /settings/ufw/revoke", s.requireAdmin(s.handleUfwRevoke))
mux.Handle("POST /settings/ufw/toggle", s.adminsOnly(s.handleUfwToggle))
mux.Handle("POST /settings/ufw/add", s.adminsOnly(s.handleUfwAdd))
mux.Handle("POST /settings/ufw/revoke", s.adminsOnly(s.handleUfwRevoke))
// Panel terminal over HTTP long-polling (admin-only).
mux.Handle("POST /api/panel/term/{agentID}/open", s.requireAdmin(s.handlePanelOpen))
@@ -317,6 +326,36 @@ func (s *Server) requireAdmin(h http.HandlerFunc) http.Handler {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// A user who is not an admin uses only the machines given to them:
// any other is as if it were not there.
if !admin.IsAdmin() {
p := r.URL.Path
if strings.HasPrefix(p, "/agents/") || strings.HasPrefix(p, "/api/panel/agents/") || strings.HasPrefix(p, "/api/panel/term/") {
id := r.PathValue("id")
if id == "" {
id = r.PathValue("agentID")
}
if id != "" && id != "presence" && !s.db.CanUseAgent(admin, id) {
http.NotFound(w, r)
return
}
}
}
h(w, r)
})
}
// adminsOnly lets only admins through: the settings, users, the firewall.
func (s *Server) adminsOnly(h http.HandlerFunc) http.Handler {
return s.requireAdmin(func(w http.ResponseWriter, r *http.Request) {
if !s.currentAdmin(r).IsAdmin() {
if r.Method == http.MethodGet && !strings.HasPrefix(r.URL.Path, "/api/") {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
http.Error(w, "только для администраторов", http.StatusForbidden)
return
}
h(w, r)
})
}