diff --git a/controllers/ext_client.go b/controllers/ext_client.go index 89c6adbaa..5ca7c6979 100644 --- a/controllers/ext_client.go +++ b/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()) diff --git a/controllers/network.go b/controllers/network.go index 3910d127e..9fc6bc6d0 100644 --- a/controllers/network.go +++ b/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). diff --git a/logic/clients.go b/logic/clients.go index c11fb0cc9..7331ee615 100644 --- a/logic/clients.go +++ b/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{}) { diff --git a/migrate/migrate.go b/migrate/migrate.go index d100d93b9..92245a125 100644 --- a/migrate/migrate.go +++ b/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 + } + } +} diff --git a/pro/logic/ext_acls.go b/pro/logic/ext_acls.go index b96051ef6..6ea62dd41 100644 --- a/pro/logic/ext_acls.go +++ b/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 }