chore: track version files, docs, web app split, scripts (missed commits from March work)

This commit is contained in:
2026-09-13 08:41:35 +08:00
parent 84c709bbcc
commit edf76db56b
33 changed files with 3082 additions and 504 deletions
+218 -14
View File
@@ -215,6 +215,13 @@ func main() {
fmt.Fprintf(w, `{"error":401,"message":"unauthorized"}`)
return
}
// only tenant session/apikey can pass
if ac.TenantID <= 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(w, `{"error":401,"message":"unauthorized"}`)
return
}
r = r.WithContext(context.WithValue(r.Context(), server.ServerCtxKeyAccess{}, ac))
next(w, r)
}
@@ -269,7 +276,14 @@ func main() {
// Serve Static Web Console
webDir := "/root/.openclaw/workspace/inp2p/web"
mux.Handle("/", http.FileServer(http.Dir(webDir)))
fileServer := http.FileServer(http.Dir(webDir))
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 防止前端资源缓存导致“登录后瞬间白屏”(新旧 JS 混用)
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
fileServer.ServeHTTP(w, r)
}))
// Tenant APIs (API key auth inside handlers)
mux.HandleFunc("/api/v1/admin/tenants", adminMiddleware(srv.HandleAdminCreateTenant))
@@ -336,6 +350,7 @@ func main() {
}
u, err := srv.Store().VerifyUserPasswordGlobal(reqUser.Username, reqUser.Password)
if err != nil || u == nil || u.Status != 1 {
_ = srv.Store().AddAuditLog("auth", reqUser.Username, "auth_login_failed", "user", reqUser.Username, "invalid_credentials", r.RemoteAddr)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(w, `{"error":1,"message":"invalid credentials"}`)
@@ -348,6 +363,7 @@ func main() {
fmt.Fprintf(w, `{"error":1,"message":"create session failed"}`)
return
}
_ = srv.Store().AddAuditLog("session", fmt.Sprintf("%d", u.ID), "auth_login_success", "tenant", fmt.Sprintf("%d", u.TenantID), fmt.Sprintf("role=%s", u.Role), r.RemoteAddr)
ten, _ := srv.Store().GetTenantByID(u.TenantID)
resp := struct {
Error int `json:"error"`
@@ -380,15 +396,108 @@ func main() {
return
}
w.Header().Set("Content-Type", "application/json")
// tenant filter by session/apikey
tenantID := getTenantID(r)
if tenantID > 0 {
nodes := srv.GetOnlineNodesByTenant(tenantID)
_ = json.NewEncoder(w).Encode(map[string]any{"nodes": listNodesOut(nodes, tenantID)})
online := srv.GetOnlineNodesByTenant(tenantID)
onlineOut := listNodesOut(online, tenantID)
byName := map[string]map[string]any{}
for _, it := range onlineOut {
name, _ := it["name"].(string)
if name == "" {
continue
}
it["online"] = true
it["status"] = "online"
byName[name] = it
}
if srv.Store() != nil {
if creds, err := srv.Store().ListNodeCredentialsByTenant(tenantID); err == nil {
for _, nc := range creds {
if _, ok := byName[nc.NodeName]; ok {
continue
}
item := map[string]any{
"name": nc.NodeName,
"displayName": nc.NodeName,
"publicIP": "",
"publicPort": 0,
"natType": "unknown",
"tenantId": nc.TenantID,
"version": "",
"relayEnabled": false,
"superRelay": false,
"loginTime": time.Time{},
"lastHeartbeat": time.Time{},
"nodeUUID": nc.NodeUUID,
"alias": nc.Alias,
"virtualIP": nc.VirtualIP,
"online": false,
"status": "offline",
}
if nc.Alias != "" {
item["displayName"] = nc.Alias
}
byName[nc.NodeName] = item
}
}
}
merged := make([]map[string]any, 0, len(byName))
for _, v := range byName {
merged = append(merged, v)
}
_ = json.NewEncoder(w).Encode(map[string]any{"nodes": merged})
return
}
nodes := srv.GetOnlineNodes()
_ = json.NewEncoder(w).Encode(map[string]any{"nodes": listNodesOut(nodes, 0)})
online := srv.GetOnlineNodes()
onlineOut := listNodesOut(online, 0)
byName := map[string]map[string]any{}
for _, it := range onlineOut {
name, _ := it["name"].(string)
if name == "" {
continue
}
it["online"] = true
it["status"] = "online"
byName[name] = it
}
if srv.Store() != nil {
if creds, err := srv.Store().ListNodeCredentialsAll(); err == nil {
for _, nc := range creds {
if _, ok := byName[nc.NodeName]; ok {
continue
}
item := map[string]any{
"name": nc.NodeName,
"displayName": nc.NodeName,
"publicIP": "",
"publicPort": 0,
"natType": "unknown",
"tenantId": nc.TenantID,
"version": "",
"relayEnabled": false,
"superRelay": false,
"loginTime": time.Time{},
"lastHeartbeat": time.Time{},
"nodeUUID": nc.NodeUUID,
"alias": nc.Alias,
"virtualIP": nc.VirtualIP,
"online": false,
"status": "offline",
}
if nc.Alias != "" {
item["displayName"] = nc.Alias
}
byName[nc.NodeName] = item
}
}
}
merged := make([]map[string]any, 0, len(byName))
for _, v := range byName {
merged = append(merged, v)
}
_ = json.NewEncoder(w).Encode(map[string]any{"nodes": merged})
}))
mux.HandleFunc("/api/v1/sdwans", tenantMiddleware(func(w http.ResponseWriter, r *http.Request) {
@@ -443,11 +552,32 @@ func main() {
tenantID := getTenantID(r)
if tenantID > 0 {
ac := server.GetAccessContext(r)
allowed, usedOverride := srv.TenantCanWithOverride(ac, "mutate_sdwan")
if !allowed {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if usedOverride && ac != nil && srv.Store() != nil {
detail := fmt.Sprintf("action=mutate_sdwan tenant=%d", tenantID)
_ = srv.Store().AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "rbac_override_used", "tenant", fmt.Sprintf("%d", tenantID), detail, r.RemoteAddr)
}
actorType, actorID := "", ""
if ac != nil {
actorType = ac.Kind
actorID = fmt.Sprintf("%d", ac.UserID)
}
// lock tenant gatewayCIDR to assigned subnet (tenant cannot modify)
if srv.Store() != nil {
ten, terr := srv.Store().GetTenantByID(tenantID)
if terr == nil && ten != nil && ten.Subnet != "" {
req.GatewayCIDR = ten.Subnet
if len(req.Routes) == 0 {
req.Routes = []string{ten.Subnet}
}
}
}
if err := srv.SetSDWANTenant(tenantID, req, actorType, actorID, r.RemoteAddr); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -478,19 +608,35 @@ func main() {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
if tenantID > 0 {
ac := server.GetAccessContext(r)
allowed, usedOverride := srv.TenantCanWithOverride(ac, "mutate_node_control")
if !allowed {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if usedOverride && ac != nil && srv.Store() != nil {
detail := fmt.Sprintf("action=mutate_node_control tenant=%d", tenantID)
_ = srv.Store().AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "rbac_override_used", "tenant", fmt.Sprintf("%d", tenantID), detail, r.RemoteAddr)
}
}
node := srv.GetNode(req.Node)
if node == nil {
http.Error(w, "node not found", http.StatusNotFound)
return
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
if tenantID > 0 && node.TenantID != tenantID {
http.Error(w, "node not found", http.StatusNotFound)
return
}
// Push to client
_ = node.Conn.Write(protocol.MsgPush, protocol.SubPushConfig, req.Apps)
if ac := server.GetAccessContext(r); ac != nil && srv.Store() != nil {
detail := fmt.Sprintf("node=%s apps=%d", req.Node, len(req.Apps))
_ = srv.Store().AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "node_apps_push", "node", req.Node, detail, r.RemoteAddr)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"error": 0, "message": "config push sent"})
}))
@@ -508,18 +654,28 @@ func main() {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
ac := server.GetAccessContext(r)
if tenantID > 0 {
if !server.TenantCan(ac, "mutate_node_control") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
}
node := srv.GetNode(req.Node)
if node == nil {
http.Error(w, "node not found or offline", http.StatusNotFound)
return
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
if tenantID > 0 && node.TenantID != tenantID {
http.Error(w, "node not found", http.StatusNotFound)
return
}
node.Conn.Close()
if ac != nil && srv.Store() != nil {
_ = srv.Store().AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "node_kick", "node", req.Node, "", r.RemoteAddr)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"error": 0, "message": "node kicked"})
}))
@@ -541,14 +697,34 @@ func main() {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
ac := server.GetAccessContext(r)
if tenantID > 0 {
allowed, usedOverride := srv.TenantCanWithOverride(ac, "mutate_connect")
if !allowed {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if usedOverride && ac != nil && srv.Store() != nil {
detail := fmt.Sprintf("action=mutate_connect tenant=%d", tenantID)
_ = srv.Store().AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "rbac_override_used", "tenant", fmt.Sprintf("%d", tenantID), detail, r.RemoteAddr)
}
}
fromNode := srv.GetNode(req.From)
if fromNode == nil {
if ac != nil && srv.Store() != nil {
detail := fmt.Sprintf("from=%s to=%s app=%s src=%d dst=%d reason=source_offline", req.From, req.To, req.AppName, req.SrcPort, req.DstPort)
_ = srv.Store().AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "connect_request_denied", "tenant", fmt.Sprintf("%d", tenantID), detail, r.RemoteAddr)
}
http.Error(w, "source node offline", http.StatusNotFound)
return
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
if tenantID > 0 && fromNode.TenantID != tenantID {
if ac != nil && srv.Store() != nil {
detail := fmt.Sprintf("from=%s to=%s app=%s src=%d dst=%d reason=tenant_mismatch_source", req.From, req.To, req.AppName, req.SrcPort, req.DstPort)
_ = srv.Store().AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "connect_request_denied", "tenant", fmt.Sprintf("%d", tenantID), detail, r.RemoteAddr)
}
http.Error(w, "node not found", http.StatusNotFound)
return
}
@@ -561,13 +737,41 @@ func main() {
DstPort: req.DstPort,
Enabled: 1,
}
// enforce same-tenant target
// enforce same-tenant target by default; can be relaxed only with explicit advanced switch
if tenantID > 0 {
allowCrossTenant := false
if ac != nil && ac.Kind == "session" && ac.Role == "operator" {
if v, _, err := srv.Store().GetSetting("advanced_override_enabled"); err == nil && (v == "1" || v == "true") {
if v2, _, err2 := srv.Store().GetSetting("allow_connect_cross_tenant"); err2 == nil && (v2 == "1" || v2 == "true") {
allowCrossTenant = true
}
}
}
toNode := srv.GetNode(req.To)
if toNode == nil || toNode.TenantID != tenantID {
if toNode == nil {
if ac != nil && srv.Store() != nil {
detail := fmt.Sprintf("from=%s to=%s app=%s src=%d dst=%d reason=target_offline", req.From, req.To, req.AppName, req.SrcPort, req.DstPort)
_ = srv.Store().AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "connect_request_denied", "tenant", fmt.Sprintf("%d", tenantID), detail, r.RemoteAddr)
}
http.Error(w, "node not found", http.StatusNotFound)
return
}
if !allowCrossTenant && toNode.TenantID != tenantID {
if ac != nil && srv.Store() != nil {
detail := fmt.Sprintf("from=%s to=%s app=%s src=%d dst=%d reason=tenant_mismatch_target", req.From, req.To, req.AppName, req.SrcPort, req.DstPort)
_ = srv.Store().AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "connect_request_denied", "tenant", fmt.Sprintf("%d", tenantID), detail, r.RemoteAddr)
}
http.Error(w, "node not found", http.StatusNotFound)
return
}
if allowCrossTenant && ac != nil && srv.Store() != nil {
detail := fmt.Sprintf("action=connect_cross_tenant tenant=%d from=%s to=%s", tenantID, req.From, req.To)
_ = srv.Store().AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "rbac_override_used", "tenant", fmt.Sprintf("%d", tenantID), detail, r.RemoteAddr)
}
}
if ac := server.GetAccessContext(r); ac != nil && srv.Store() != nil {
detail := fmt.Sprintf("from=%s to=%s app=%s src=%d dst=%d", req.From, req.To, req.AppName, req.SrcPort, req.DstPort)
_ = srv.Store().AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "connect_request", "tenant", fmt.Sprintf("%d", tenantID), detail, r.RemoteAddr)
}
if err := srv.PushConnect(fromNode, req.To, app); err != nil {
w.Header().Set("Content-Type", "application/json")