chore: track version files, docs, web app split, scripts (missed commits from March work)
This commit is contained in:
@@ -37,7 +37,8 @@ func (s *Server) HandleAdminSettings(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// allowlist
|
||||
switch req.Key {
|
||||
case "advanced_impersonate", "advanced_force_network", "advanced_cross_tenant":
|
||||
case "advanced_impersonate", "advanced_force_network", "advanced_cross_tenant",
|
||||
"advanced_override_enabled", "allow_apikey_mutate_sdwan", "allow_operator_kick_cross_node", "allow_connect_cross_tenant":
|
||||
default:
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"invalid key"}`)
|
||||
return
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GET /api/v1/admin/audit?tenant=3&limit=50&offset=0
|
||||
// GET /api/v1/admin/audit?tenant=3&limit=50&offset=0&action=node_kick&actor_type=session&actor_id=12&target_type=node&target_id=nodeA&from=1700000000&to=1700003600
|
||||
func (s *Server) HandleAdminAudit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
@@ -30,7 +30,27 @@ func (s *Server) HandleAdminAudit(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID = i
|
||||
}
|
||||
}
|
||||
logs, err := s.store.ListAuditLogs(tenantID, limit, offset)
|
||||
|
||||
action := r.URL.Query().Get("action")
|
||||
actorType := r.URL.Query().Get("actor_type")
|
||||
actorID := r.URL.Query().Get("actor_id")
|
||||
targetType := r.URL.Query().Get("target_type")
|
||||
targetID := r.URL.Query().Get("target_id")
|
||||
|
||||
fromTs := int64(0)
|
||||
toTs := int64(0)
|
||||
if v := r.URL.Query().Get("from"); v != "" {
|
||||
if i, err := strconv.ParseInt(v, 10, 64); err == nil && i > 0 {
|
||||
fromTs = i
|
||||
}
|
||||
}
|
||||
if v := r.URL.Query().Get("to"); v != "" {
|
||||
if i, err := strconv.ParseInt(v, 10, 64); err == nil && i > 0 {
|
||||
toTs = i
|
||||
}
|
||||
}
|
||||
|
||||
logs, err := s.store.ListAuditLogs(tenantID, limit, offset, action, actorType, actorID, targetType, targetID, fromTs, toTs)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AccessContext struct {
|
||||
@@ -63,3 +64,66 @@ func (s *Server) ResolveTenantAccessToken(tok string) (*AccessContext, bool) {
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// TenantCan implements action-level RBAC for tenant-scoped APIs.
|
||||
func TenantCan(ac *AccessContext, action string) bool {
|
||||
if ac == nil {
|
||||
return false
|
||||
}
|
||||
switch action {
|
||||
case "view":
|
||||
return (ac.Kind == "session" && (ac.Role == "admin" || ac.Role == "operator")) || ac.Kind == "apikey"
|
||||
case "mutate_node_meta", "mutate_sdwan", "mutate_connect", "mutate_node_control":
|
||||
return ac.Kind == "session" && (ac.Role == "admin" || ac.Role == "operator")
|
||||
case "enroll_manage":
|
||||
return ac.Kind == "session" && ac.Role == "admin"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func settingOn(v string) bool {
|
||||
v = strings.TrimSpace(strings.ToLower(v))
|
||||
return v == "1" || v == "true" || v == "on" || v == "yes"
|
||||
}
|
||||
|
||||
// TenantCanWithOverride evaluates baseline RBAC then optional advanced override switches.
|
||||
// Return values: allowed, usedOverride
|
||||
func (s *Server) TenantCanWithOverride(ac *AccessContext, action string) (bool, bool) {
|
||||
if TenantCan(ac, action) {
|
||||
return true, false
|
||||
}
|
||||
if ac == nil || s == nil || s.store == nil || ac.TenantID <= 0 {
|
||||
return false, false
|
||||
}
|
||||
if ac.Kind == "master" {
|
||||
return false, false
|
||||
}
|
||||
// global override gate
|
||||
if v, _, err := s.store.GetSetting("advanced_override_enabled"); err != nil || !settingOn(v) {
|
||||
return false, false
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "mutate_sdwan":
|
||||
if ac.Kind == "apikey" {
|
||||
if v, _, err := s.store.GetSetting("allow_apikey_mutate_sdwan"); err == nil && settingOn(v) {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
case "mutate_connect":
|
||||
if ac.Kind == "session" && ac.Role == "operator" {
|
||||
if v, _, err := s.store.GetSetting("allow_connect_cross_tenant"); err == nil && settingOn(v) {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
case "mutate_node_control":
|
||||
if ac.Kind == "session" && ac.Role == "operator" {
|
||||
if v, _, err := s.store.GetSetting("allow_operator_kick_cross_node"); err == nil && settingOn(v) {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, false
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -22,6 +23,10 @@ func (s *Server) HandleNodeMeta(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusUnauthorized, `{"error":1,"message":"unauthorized"}`)
|
||||
return
|
||||
}
|
||||
if !TenantCan(ac, "mutate_node_meta") {
|
||||
writeJSON(w, http.StatusForbidden, `{"error":1,"message":"forbidden"}`)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(r.URL.Path, "/alias") {
|
||||
var req struct {
|
||||
@@ -36,6 +41,12 @@ func (s *Server) HandleNodeMeta(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"`+err.Error()+`"}`)
|
||||
return
|
||||
}
|
||||
actorID := "0"
|
||||
if ac != nil {
|
||||
actorID = fmt.Sprintf("%d", ac.UserID)
|
||||
}
|
||||
detail := fmt.Sprintf("tenant=%d node_uuid=%s alias=%s", ac.TenantID, req.NodeUUID, req.Alias)
|
||||
_ = s.store.AddAuditLog(ac.Kind, actorID, "node_alias_update", "node", req.NodeUUID, detail, r.RemoteAddr)
|
||||
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
|
||||
return
|
||||
}
|
||||
@@ -53,6 +64,12 @@ func (s *Server) HandleNodeMeta(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"`+err.Error()+`"}`)
|
||||
return
|
||||
}
|
||||
actorID := "0"
|
||||
if ac != nil {
|
||||
actorID = fmt.Sprintf("%d", ac.UserID)
|
||||
}
|
||||
detail := fmt.Sprintf("tenant=%d node_uuid=%s virtual_ip=%s", ac.TenantID, req.NodeUUID, req.VirtualIP)
|
||||
_ = s.store.AddAuditLog(ac.Kind, actorID, "node_ip_update", "node", req.NodeUUID, detail, r.RemoteAddr)
|
||||
|
||||
nodes := s.GetOnlineNodesByTenant(ac.TenantID)
|
||||
affectedNode := ""
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/openp2p-cn/inp2p/pkg/protocol"
|
||||
)
|
||||
@@ -35,17 +36,50 @@ func (s *Server) SetSDWANTenant(tenantID int64, cfg protocol.SDWANConfig, actorT
|
||||
return errors.New("hub node must be online and relay-enabled")
|
||||
}
|
||||
}
|
||||
|
||||
cfg = s.rewriteTenantSDWAN(tenantID, cfg)
|
||||
|
||||
if err := s.sdwan.saveTenant(tenantID, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if actorType != "" && s.store != nil {
|
||||
detail := fmt.Sprintf("mode=%s hub=%s nodes=%d subnetProxies=%d", cfg.Mode, cfg.HubNode, len(cfg.Nodes), len(cfg.SubnetProxies))
|
||||
detail := fmt.Sprintf("mode=%s hub=%s nodes=%d subnetProxies=%d gateway=%s", cfg.Mode, cfg.HubNode, len(cfg.Nodes), len(cfg.SubnetProxies), cfg.GatewayCIDR)
|
||||
_ = s.store.AddAuditLog(actorType, actorID, "sdwan_update", "tenant", fmt.Sprintf("%d", tenantID), detail, ip)
|
||||
}
|
||||
s.broadcastSDWANTenant(tenantID, s.sdwan.getTenant(tenantID))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) rewriteTenantSDWAN(tenantID int64, cfg protocol.SDWANConfig) protocol.SDWANConfig {
|
||||
if s.store != nil {
|
||||
if ten, err := s.store.GetTenantByID(tenantID); err == nil && ten != nil && ten.Subnet != "" {
|
||||
cfg.GatewayCIDR = ten.Subnet
|
||||
// keep routes aligned with subnet
|
||||
found := false
|
||||
for _, r := range cfg.Routes {
|
||||
if strings.TrimSpace(r) == ten.Subnet {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
cfg.Routes = []string{ten.Subnet}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (s *Server) ReconcileTenantSDWAN(tenantID int64) {
|
||||
cfg := s.sdwan.getTenant(tenantID)
|
||||
cfg = s.rewriteTenantSDWAN(tenantID, cfg)
|
||||
if err := s.sdwan.saveTenant(tenantID, cfg); err != nil {
|
||||
log.Printf("[sdwan] reconcile tenant=%d failed: %v", tenantID, err)
|
||||
return
|
||||
}
|
||||
s.broadcastSDWANTenant(tenantID, cfg)
|
||||
}
|
||||
|
||||
func (s *Server) broadcastSDWAN(cfg protocol.SDWANConfig) {
|
||||
if !cfg.Enabled || cfg.GatewayCIDR == "" {
|
||||
return
|
||||
|
||||
@@ -262,7 +262,7 @@ func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Verify token: master token OR tenant API key (DB) OR node_secret (DB)
|
||||
valid := s.tokens[loginReq.Token]
|
||||
log.Printf("[server] login check: token=%d, cfg.Token=%d, valid=%v", loginReq.Token, s.cfg.Token, valid)
|
||||
log.Printf("[server] login check: node=%s valid=%v", loginReq.Node, valid)
|
||||
var tenantID int64
|
||||
if !valid && s.store != nil {
|
||||
// try api key (string) or node secret
|
||||
|
||||
@@ -170,7 +170,8 @@ func (s *Server) HandleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if ac := GetAccessContext(r); ac != nil {
|
||||
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "user_create", "tenant", fmt.Sprintf("%d", req.TenantID), req.Email, r.RemoteAddr)
|
||||
detail := fmt.Sprintf("username=%s role=%s", req.Email, req.Role)
|
||||
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "user_create", "tenant", fmt.Sprintf("%d", req.TenantID), detail, r.RemoteAddr)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user