Browse Source

fix(NET-897): uniform client and node acls (#2803)

Aceix 1 year ago
parent
commit
100b778449
5 changed files with 384 additions and 2 deletions
  1. 21 1
      controllers/ext_client.go
  2. 194 1
      controllers/network.go
  3. 19 0
      logic/clients.go
  4. 123 0
      migrate/migrate.go
  5. 27 0
      pro/logic/ext_acls.go

+ 21 - 1
controllers/ext_client.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"net"
 	"net/http"
+	"reflect"
 	"strconv"
 	"strings"
 
@@ -14,6 +15,7 @@ import (
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/servercfg"
 
 	"github.com/gravitl/netmaker/models"
@@ -503,7 +505,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 	var changedID = update.ClientID != oldExtClient.ClientID
 
-	if len(update.DeniedACLs) != len(oldExtClient.DeniedACLs) {
+	if !reflect.DeepEqual(update.DeniedACLs, oldExtClient.DeniedACLs) {
 		sendPeerUpdate = true
 		logic.SetClientACLs(&oldExtClient, update.DeniedACLs)
 	}
@@ -609,6 +611,24 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// delete client acls
+	var networkAcls acls.ACLContainer
+	networkAcls, err = networkAcls.Get(acls.ContainerID(network))
+	if err != nil {
+		slog.Error("failed to get network acls", "err", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for objId := range networkAcls {
+		delete(networkAcls[objId], acls.AclID(clientid))
+	}
+	delete(networkAcls, acls.AclID(clientid))
+	if _, err = networkAcls.Save(acls.ContainerID(network)); err != nil {
+		slog.Error("failed to update network acls", "err", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
 	go func() {
 		if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
 			logger.Log(1, "error setting ext peers on "+ingressnode.ID.String()+": "+err.Error())

+ 194 - 1
controllers/network.go

@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"strings"
 
+	"github.com/google/uuid"
 	"github.com/gorilla/mux"
 	"golang.org/x/exp/slog"
 
@@ -17,6 +18,7 @@ import (
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/servercfg"
 )
 
 func networkHandlers(r *mux.Router) {
@@ -27,6 +29,7 @@ func networkHandlers(r *mux.Router) {
 	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(updateNetwork))).Methods(http.MethodPut)
 	// ACLs
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(updateNetworkACL))).Methods(http.MethodPut)
+	r.HandleFunc("/api/networks/{networkname}/acls/v2", logic.SecurityCheck(true, http.HandlerFunc(updateNetworkACLv2))).Methods(http.MethodPut)
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(getNetworkACL))).Methods(http.MethodGet)
 }
 
@@ -129,7 +132,7 @@ func updateNetworkACL(w http.ResponseWriter, r *http.Request) {
 	// send peer updates
 	go func() {
 		if err = mq.PublishPeerUpdate(false); err != nil {
-			logger.Log(0, "failed to publish peer update after ACL update on", netname)
+			logger.Log(0, "failed to publish peer update after ACL update on network:", netname)
 		}
 	}()
 
@@ -137,6 +140,196 @@ func updateNetworkACL(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(newNetACL)
 }
 
+// swagger:route PUT /api/networks/{networkname}/acls/v2 networks updateNetworkACL
+//
+// Update a network ACL (Access Control List).
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: aclContainerResponse
+func updateNetworkACLv2(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	netname := params["networkname"]
+	var networkACLChange acls.ACLContainer
+	networkACLChange, err := networkACLChange.Get(acls.ContainerID(netname))
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to fetch ACLs for network [%s]: %v", netname, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	err = json.NewDecoder(r.Body).Decode(&networkACLChange)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	// clone req body to use as return data successful update
+	retData := make(acls.ACLContainer)
+	data, err := json.Marshal(networkACLChange)
+	if err != nil {
+		slog.Error("failed to marshal networkACLChange whiles cloning", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	err = json.Unmarshal(data, &retData)
+	if err != nil {
+		slog.Error("failed to unmarshal networkACLChange whiles cloning", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	allNodes, err := logic.GetAllNodes()
+	if err != nil {
+		slog.Error("failed to fetch all nodes", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	networkNodes := make([]models.Node, 0)
+	for _, node := range allNodes {
+		if node.Network == netname {
+			networkNodes = append(networkNodes, node)
+		}
+	}
+	networkNodesIdMap := make(map[string]models.Node)
+	for _, node := range networkNodes {
+		networkNodesIdMap[node.ID.String()] = node
+	}
+	networkClients, err := logic.GetNetworkExtClients(netname)
+	if err != nil {
+		slog.Error("failed to fetch network clients", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	networkClientsMap := make(map[string]models.ExtClient)
+	for _, client := range networkClients {
+		networkClientsMap[client.ClientID] = client
+	}
+
+	// keep track of ingress gateways to disconnect from their clients
+	// this is required because PublishPeerUpdate only somehow does not stop communication
+	// between blocked clients and their ingress
+	assocClientsToDisconnectPerHost := make(map[uuid.UUID][]models.ExtClient)
+
+	// update client acls and then, remove client acls from req data to pass to existing functions
+	for id, acl := range networkACLChange {
+		// for node acls
+		if _, ok := networkNodesIdMap[string(id)]; ok {
+			nodeId := string(id)
+			// check acl update, then remove client entries
+			for id2 := range acl {
+				if _, ok := networkNodesIdMap[string(id2)]; !ok {
+					// update client acl
+					clientId := string(id2)
+					if client, ok := networkClientsMap[clientId]; ok {
+						if client.DeniedACLs == nil {
+							client.DeniedACLs = make(map[string]struct{})
+						}
+						if acl[acls.AclID(clientId)] == acls.NotAllowed {
+							client.DeniedACLs[nodeId] = struct{}{}
+						} else {
+							delete(client.DeniedACLs, string(nodeId))
+						}
+						networkClientsMap[clientId] = client
+					}
+				}
+			}
+		} else {
+			// for client acls
+			clientId := string(id)
+			for id2 := range acl {
+				if _, ok := networkNodesIdMap[string(id2)]; !ok {
+					// update client acl
+					clientId2 := string(id2)
+					if client, ok := networkClientsMap[clientId]; ok {
+						if client.DeniedACLs == nil {
+							client.DeniedACLs = make(map[string]struct{})
+						}
+						{
+							// TODO: review this when client-to-client acls are supported
+							// if acl[acls.AclID(clientId2)] == acls.NotAllowed {
+							// 	client.DeniedACLs[clientId2] = struct{}{}
+							// } else {
+							// 	delete(client.DeniedACLs, clientId2)
+							// }
+							delete(client.DeniedACLs, clientId2)
+						}
+						networkClientsMap[clientId] = client
+					}
+				} else {
+					nodeId2 := string(id2)
+					if networkClientsMap[clientId].IngressGatewayID == nodeId2 && acl[acls.AclID(nodeId2)] == acls.NotAllowed {
+						assocClientsToDisconnectPerHost[networkNodesIdMap[nodeId2].HostID] = append(assocClientsToDisconnectPerHost[networkNodesIdMap[nodeId2].HostID], networkClientsMap[clientId])
+					}
+				}
+			}
+		}
+	}
+
+	// update each client in db for pro servers
+	if servercfg.IsPro {
+		for _, client := range networkClientsMap {
+			client := client
+			err := logic.DeleteExtClient(client.Network, client.ClientID)
+			if err != nil {
+				slog.Error("failed to delete client during update", "client", client.ClientID, "error", err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+				return
+			}
+			err = logic.SaveExtClient(&client)
+			if err != nil {
+				slog.Error("failed to save client during update", "client", client.ClientID, "error", err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+				return
+			}
+		}
+	}
+
+	_, err = networkACLChange.Save(acls.ContainerID(netname))
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to update ACLs for network [%s]: %v", netname, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	logger.Log(1, r.Header.Get("user"), "updated ACLs for network", netname)
+
+	// send peer updates
+	go func() {
+		if err = mq.PublishPeerUpdate(false); err != nil {
+			logger.Log(0, "failed to publish peer update after ACL update on network:", netname)
+		}
+
+		// update ingress gateways of associated clients
+		hosts, err := logic.GetAllHosts()
+		if err != nil {
+			slog.Error("failed to fetch hosts after network ACL update. skipping publish extclients ACL", "network", netname)
+			return
+		}
+		hostsMap := make(map[uuid.UUID]models.Host)
+		for _, host := range hosts {
+			hostsMap[host.ID] = host
+		}
+		for hostId, clients := range assocClientsToDisconnectPerHost {
+			if host, ok := hostsMap[hostId]; ok {
+				if err = mq.PublishSingleHostPeerUpdate(&host, allNodes, nil, clients, false); err != nil {
+					slog.Error("failed to publish peer update to ingress after ACL update on network", "network", netname, "host", hostId)
+				}
+			}
+		}
+	}()
+
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(networkACLChange)
+}
+
 // swagger:route GET /api/networks/{networkname}/acls networks getNetworkACL
 //
 // Get a network ACL (Access Control List).

+ 19 - 0
logic/clients.go

@@ -4,7 +4,9 @@ import (
 	"errors"
 	"sort"
 
+	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
+	"golang.org/x/exp/slog"
 )
 
 // functions defined here, handle client ACLs, should be set on ee
@@ -23,6 +25,23 @@ var (
 		return true
 	}
 	SetClientDefaultACLs = func(ec *models.ExtClient) error {
+		// allow all on CE
+		networkAcls := acls.ACLContainer{}
+		networkAcls, err := networkAcls.Get(acls.ContainerID(ec.Network))
+		if err != nil {
+			slog.Error("failed to get network acls", "error", err)
+			return err
+		}
+		networkAcls[acls.AclID(ec.ClientID)] = acls.ACL{}
+		for objId := range networkAcls {
+			networkAcls[objId][acls.AclID(ec.ClientID)] = acls.Allowed
+			networkAcls[acls.AclID(ec.ClientID)][objId] = acls.Allowed
+		}
+		delete(networkAcls[acls.AclID(ec.ClientID)], acls.AclID(ec.ClientID))
+		if _, err = networkAcls.Save(acls.ContainerID(ec.Network)); err != nil {
+			slog.Error("failed to update network acls", "error", err)
+			return err
+		}
 		return nil
 	}
 	SetClientACLs = func(ec *models.ExtClient, newACLs map[string]struct{}) {

+ 123 - 0
migrate/migrate.go

@@ -2,6 +2,7 @@ package migrate
 
 import (
 	"encoding/json"
+	"fmt"
 	"log"
 
 	"golang.org/x/exp/slog"
@@ -9,6 +10,7 @@ import (
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 )
@@ -19,6 +21,7 @@ func Run() {
 	assignSuperAdmin()
 	updateHosts()
 	updateNodes()
+	updateAcls()
 }
 
 func assignSuperAdmin() {
@@ -167,3 +170,123 @@ func removeInterGw(egressRanges []string) ([]string, bool) {
 	}
 	return egressRanges, update
 }
+
+func updateAcls() {
+	// get all networks
+	networks, err := logic.GetNetworks()
+	if err != nil {
+		slog.Error("acls migration failed. error getting networks", "error", err)
+		return
+	}
+
+	// get current acls per network
+	for _, network := range networks {
+		var networkAcl acls.ACLContainer
+		networkAcl, err := networkAcl.Get(acls.ContainerID(network.NetID))
+		if err != nil {
+			if database.IsEmptyRecord(err) {
+				continue
+			}
+			slog.Error(fmt.Sprintf("error during acls migration. error getting acls for network: %s", network.NetID), "error", err)
+			continue
+		}
+		// convert old acls to new acls with clients
+		// TODO: optimise O(n^2) operation
+		clients, err := logic.GetNetworkExtClients(network.NetID)
+		if err != nil {
+			slog.Error(fmt.Sprintf("error during acls migration. error getting clients for network: %s", network.NetID), "error", err)
+			continue
+		}
+		clientsIdMap := make(map[string]struct{})
+		for _, client := range clients {
+			clientsIdMap[client.ClientID] = struct{}{}
+		}
+		nodeIdsMap := make(map[string]struct{})
+		for nodeId := range networkAcl {
+			nodeIdsMap[string(nodeId)] = struct{}{}
+		}
+		/*
+			initially, networkACL has only node acls so we add client acls to it
+			final shape:
+			{
+				"node1": {
+					"node2": 2,
+					"client1": 2,
+					"client2": 1,
+				},
+				"node2": {
+					"node1": 2,
+					"client1": 2,
+					"client2": 1,
+				},
+				"client1": {
+					"node1": 2,
+					"node2": 2,
+					"client2": 1,
+				},
+				"client2": {
+					"node1": 1,
+					"node2": 1,
+					"client1": 1,
+				},
+			}
+		*/
+		for _, client := range clients {
+			networkAcl[acls.AclID(client.ClientID)] = acls.ACL{}
+			// add client values to node acls and create client acls with node values
+			for id, nodeAcl := range networkAcl {
+				// skip if not a node
+				if _, ok := nodeIdsMap[string(id)]; !ok {
+					continue
+				}
+				if nodeAcl == nil {
+					slog.Warn("acls migration bad data: nil node acl", "node", id, "network", network.NetID)
+					continue
+				}
+				nodeAcl[acls.AclID(client.ClientID)] = acls.Allowed
+				networkAcl[acls.AclID(client.ClientID)][id] = acls.Allowed
+				if client.DeniedACLs == nil {
+					continue
+				} else if _, ok := client.DeniedACLs[string(id)]; ok {
+					nodeAcl[acls.AclID(client.ClientID)] = acls.NotAllowed
+					networkAcl[acls.AclID(client.ClientID)][id] = acls.NotAllowed
+				}
+			}
+			// add clients to client acls response
+			for _, c := range clients {
+				if c.ClientID == client.ClientID {
+					continue
+				}
+				networkAcl[acls.AclID(client.ClientID)][acls.AclID(c.ClientID)] = acls.Allowed
+				if client.DeniedACLs == nil {
+					continue
+				} else if _, ok := client.DeniedACLs[c.ClientID]; ok {
+					networkAcl[acls.AclID(client.ClientID)][acls.AclID(c.ClientID)] = acls.NotAllowed
+				}
+			}
+			// delete oneself from its own acl
+			delete(networkAcl[acls.AclID(client.ClientID)], acls.AclID(client.ClientID))
+		}
+
+		// remove non-existent client and node acls
+		for objId := range networkAcl {
+			if _, ok := nodeIdsMap[string(objId)]; ok {
+				continue
+			}
+			if _, ok := clientsIdMap[string(objId)]; ok {
+				continue
+			}
+			// remove all occurances of objId from all acls
+			for objId2 := range networkAcl {
+				delete(networkAcl[objId2], objId)
+			}
+			delete(networkAcl, objId)
+		}
+
+		// save new acls
+		if _, err := networkAcl.Save(acls.ContainerID(network.NetID)); err != nil {
+			slog.Error(fmt.Sprintf("error during acls migration. error saving new acls for network: %s", network.NetID), "error", err)
+			continue
+		}
+	}
+}

+ 27 - 0
pro/logic/ext_acls.go

@@ -5,6 +5,7 @@ import (
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
+	"golang.org/x/exp/slog"
 )
 
 // DenyClientNode - add a denied node to an ext client's list
@@ -55,14 +56,40 @@ func SetClientDefaultACLs(ec *models.ExtClient) error {
 	if err != nil {
 		return err
 	}
+	var networkAcls acls.ACLContainer
+	networkAcls, err = networkAcls.Get(acls.ContainerID(ec.Network))
+	if err != nil {
+		slog.Error("failed to get network acls", "error", err)
+		return err
+	}
+	networkAcls[acls.AclID(ec.ClientID)] = acls.ACL{}
 	for i := range networkNodes {
 		currNode := networkNodes[i]
 		if network.DefaultACL == "no" || currNode.DefaultACL == "no" {
 			DenyClientNode(ec, currNode.ID.String())
+			networkAcls[acls.AclID(ec.ClientID)][acls.AclID(currNode.ID.String())] = acls.NotAllowed
+			networkAcls[acls.AclID(currNode.ID.String())][acls.AclID(ec.ClientID)] = acls.NotAllowed
 		} else {
 			RemoveDeniedNodeFromClient(ec, currNode.ID.String())
+			networkAcls[acls.AclID(ec.ClientID)][acls.AclID(currNode.ID.String())] = acls.Allowed
+			networkAcls[acls.AclID(currNode.ID.String())][acls.AclID(ec.ClientID)] = acls.Allowed
 		}
 	}
+	networkClients, err := logic.GetNetworkExtClients(ec.Network)
+	if err != nil {
+		slog.Error("failed to get network clients", "error", err)
+		return err
+	}
+	for _, client := range networkClients {
+		// TODO: revisit when client-client acls are supported
+		networkAcls[acls.AclID(ec.ClientID)][acls.AclID(client.ClientID)] = acls.Allowed
+		networkAcls[acls.AclID(client.ClientID)][acls.AclID(ec.ClientID)] = acls.Allowed
+	}
+	delete(networkAcls[acls.AclID(ec.ClientID)], acls.AclID(ec.ClientID)) // remove oneself
+	if _, err = networkAcls.Save(acls.ContainerID(ec.Network)); err != nil {
+		slog.Error("failed to update network acls", "error", err)
+		return err
+	}
 	return nil
 }