diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 3533bccd0..09fb90c10 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -31,6 +31,7 @@ body: label: Version description: What version are you running? options: + - v0.26.0 - v0.25.0 - v0.24.3 - v0.24.2 diff --git a/.github/workflows/docker-builder.yml b/.github/workflows/docker-builder.yml index 1513364a5..5d401783e 100644 --- a/.github/workflows/docker-builder.yml +++ b/.github/workflows/docker-builder.yml @@ -7,10 +7,6 @@ on: description: 'Docker tag to use (default: latest)' required: false default: 'latest' - push: - branches: - - 'develop' - jobs: go-builder: runs-on: ubuntu-latest diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 66f274b6f..3ecd9feca 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -36,7 +36,7 @@ jobs: run: echo "timestamp=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT - name: Create Pull Request - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: "Update documentation ${{ steps.timestamp.outputs.timestamp }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9acd2f1a..a9b90a91b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version-file: 'go.mod' - name: Build run: | env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build main.go @@ -29,7 +29,7 @@ jobs: - name: Setup go uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version-file: 'go.mod' - name: Build run: | cd cli @@ -46,7 +46,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version-file: 'go.mod' - name: run tests run: | go vet ./... @@ -66,9 +66,9 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version-file: 'go.mod' - name: run static checks run: | sudo apt update - go install honnef.co/go/tools/cmd/staticcheck@v0.4.7 + go install honnef.co/go/tools/cmd/staticcheck@latest { ~/go/bin/staticcheck -tags=ee ./... ; } diff --git a/Dockerfile b/Dockerfile index 3d80f5b99..f012ac3fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,12 @@ #first stage - builder -FROM gravitl/go-builder AS builder +FROM gravitl/go-builder:1.23.0 AS builder ARG tags WORKDIR /app COPY . . RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} . # RUN go build -tags=ee . -o netmaker main.go -FROM alpine:3.20.2 +FROM alpine:3.20.3 # add a c lib # set the working directory diff --git a/Dockerfile-quick b/Dockerfile-quick index 328f3a32e..10f4f9808 100644 --- a/Dockerfile-quick +++ b/Dockerfile-quick @@ -1,5 +1,5 @@ #first stage - builder -FROM alpine:3.20.2 +FROM alpine:3.20.3 ARG version WORKDIR /app COPY ./netmaker /root/netmaker diff --git a/README.md b/README.md index fcd2b5ffe..137d66284 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@

- + @@ -51,18 +51,20 @@ If you're looking for a managed service, you can get started with just few clicks, visit [netmaker.io](https://account.netmaker.io) to create your netmaker server. -# Self-Hosted Quick Start +# Self-Hosted Open Source Quick Start These are the instructions for deploying a Netmaker server on your own cloud VM as quickly as possible. For more detailed instructions, visit the [Install Docs](https://docs.netmaker.io/docs/server-installation/quick-install#quick-install-script). 1. Get a cloud VM with Ubuntu 22.04 and a public IP. 2. Open ports 443, 80, 3479, 8089 and 51821-51830/udp on the VM firewall and in cloud security settings. 3. (recommended) Prepare DNS - Set a wildcard subdomain in your DNS settings for Netmaker, e.g. *.netmaker.example.com, which points to your VM's public IP. -4. Run the script: +4. Run the script to setup open source version of Netmaker: + +`sudo wget -qO /root/nm-quick.sh https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/nm-quick.sh && sudo chmod +x /root/nm-quick.sh && sudo /root/nm-quick.sh` + +**

To Install Self-Hosted PRO Version - https://docs.netmaker.io/docs/server-installation/netmaker-professional-setup
** -`sudo wget -qO /root/nm-quick.sh https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/nm-quick.sh && sudo chmod +x /root/nm-quick.sh && sudo /root/nm-quick.sh` -This script by default installs PRO version with 14-day trial, check out these instructions for post trial period https://docs.netmaker.io/docs/server-installation/quick-install#after-the-trial-period-ends. It also gives you the option to use your own domain (recommended) or an auto-generated domain.

diff --git a/auth/host_session.go b/auth/host_session.go index 62e9d4387..7a3929240 100644 --- a/auth/host_session.go +++ b/auth/host_session.go @@ -222,7 +222,7 @@ func SessionHandler(conn *websocket.Conn) { if err = conn.WriteMessage(messageType, reponseData); err != nil { logger.Log(0, "error during message writing:", err.Error()) } - go CheckNetRegAndHostUpdate(netsToAdd[:], &result.Host, uuid.Nil) + go CheckNetRegAndHostUpdate(netsToAdd[:], &result.Host, uuid.Nil, []models.TagID{}) case <-timeout: // the read from req.answerCh has timed out logger.Log(0, "timeout signal recv,exiting oauth socket conn") break @@ -236,7 +236,7 @@ func SessionHandler(conn *websocket.Conn) { } // CheckNetRegAndHostUpdate - run through networks and send a host update -func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uuid.UUID) { +func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uuid.UUID, tags []models.TagID) { // publish host update through MQ for i := range networks { network := networks[i] @@ -246,6 +246,14 @@ func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uui logger.Log(0, "failed to add host to network:", h.ID.String(), h.Name, network, err.Error()) continue } + if len(tags) > 0 { + newNode.Tags = make(map[models.TagID]struct{}) + for _, tagI := range tags { + newNode.Tags[tagI] = struct{}{} + } + logic.UpsertNode(newNode) + } + if relayNodeId != uuid.Nil && !newNode.IsRelayed { // check if relay node exists and acting as relay relaynode, err := logic.GetNodeByID(relayNodeId.String()) diff --git a/cli/cmd/user/groups.go b/cli/cmd/user/groups.go index 0406083e1..0cb8db0a2 100644 --- a/cli/cmd/user/groups.go +++ b/cli/cmd/user/groups.go @@ -56,7 +56,7 @@ var userGroupCreateCmd = &cobra.Command{ Short: "create user group", Long: `create user group`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println("CLI doesn't support creation of groups currently. Visit the dashboard to create one or refer to our api documentation https://docs.v2.netmaker.io/reference") + fmt.Println("CLI doesn't support creation of groups currently. Visit the dashboard to create one or refer to our api documentation https://docs.netmaker.io/api") }, } diff --git a/cli/cmd/user/roles.go b/cli/cmd/user/roles.go index 2bb880ef5..fbb8a5920 100644 --- a/cli/cmd/user/roles.go +++ b/cli/cmd/user/roles.go @@ -58,7 +58,7 @@ var userRoleCreateCmd = &cobra.Command{ Short: "create user role", Long: `create user role`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println("CLI doesn't support creation of roles currently. Visit the dashboard to create one or refer to our api documentation https://docs.v2.netmaker.io/reference") + fmt.Println("CLI doesn't support creation of roles currently. Visit the dashboard to create one or refer to our api documentation https://docs.netmaker.io/api") }, } diff --git a/compose/docker-compose.netclient.yml b/compose/docker-compose.netclient.yml index a488fa006..da6fc14f4 100644 --- a/compose/docker-compose.netclient.yml +++ b/compose/docker-compose.netclient.yml @@ -3,7 +3,7 @@ version: "3.4" services: netclient: container_name: netclient - image: 'gravitl/netclient:v0.25.0' + image: 'gravitl/netclient:v0.26.0' hostname: netmaker-1 network_mode: host restart: on-failure diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index 3111f1f5f..e32481393 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -41,7 +41,7 @@ services: restart: always caddy: - image: caddy:2.6.2 + image: caddy:2.8.4 container_name: caddy env_file: ./netmaker.env restart: unless-stopped diff --git a/config/config.go b/config/config.go index 061109f6d..f9acaf980 100644 --- a/config/config.go +++ b/config/config.go @@ -100,6 +100,8 @@ type ServerConfig struct { SmtpHost string `json:"smtp_host"` SmtpPort int `json:"smtp_port"` MetricInterval string `yaml:"metric_interval"` + ManageDNS bool `yaml:"manage_dns"` + DefaultDomain string `yaml:"default_domain"` } // SQLConfig - Generic SQL Config diff --git a/controllers/acls.go b/controllers/acls.go new file mode 100644 index 000000000..727811fb5 --- /dev/null +++ b/controllers/acls.go @@ -0,0 +1,230 @@ +package controller + +import ( + "encoding/json" + "errors" + "net/http" + "net/url" + "time" + + "github.com/google/uuid" + "github.com/gorilla/mux" + "github.com/gravitl/netmaker/logger" + "github.com/gravitl/netmaker/logic" + "github.com/gravitl/netmaker/models" + "github.com/gravitl/netmaker/mq" +) + +func aclHandlers(r *mux.Router) { + r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(getAcls))). + Methods(http.MethodGet) + r.HandleFunc("/api/v1/acls/policy_types", logic.SecurityCheck(true, http.HandlerFunc(aclPolicyTypes))). + Methods(http.MethodGet) + r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(createAcl))). + Methods(http.MethodPost) + r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(updateAcl))). + Methods(http.MethodPut) + r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(deleteAcl))). + Methods(http.MethodDelete) + r.HandleFunc("/api/v1/acls/debug", logic.SecurityCheck(true, http.HandlerFunc(aclDebug))). + Methods(http.MethodGet) +} + +// @Summary List Acl Policy types +// @Router /api/v1/acls/policy_types [get] +// @Tags ACL +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func aclPolicyTypes(w http.ResponseWriter, r *http.Request) { + resp := models.AclPolicyTypes{ + RuleTypes: []models.AclPolicyType{ + models.DevicePolicy, + models.UserPolicy, + }, + SrcGroupTypes: []models.AclGroupType{ + models.UserAclID, + models.UserGroupAclID, + models.DeviceAclID, + }, + DstGroupTypes: []models.AclGroupType{ + models.DeviceAclID, + // models.NetmakerIPAclID, + // models.NetmakerSubNetRangeAClID, + }, + } + logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched acls types") +} + +func aclDebug(w http.ResponseWriter, r *http.Request) { + nodeID, _ := url.QueryUnescape(r.URL.Query().Get("node")) + peerID, _ := url.QueryUnescape(r.URL.Query().Get("peer")) + node, err := logic.GetNodeByID(nodeID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + peer, err := logic.GetNodeByID(peerID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + allowed := logic.IsNodeAllowedToCommunicate(node, peer) + logic.ReturnSuccessResponseWithJson(w, r, allowed, "fetched all acls in the network ") +} + +// @Summary List Acls in a network +// @Router /api/v1/acls [get] +// @Tags ACL +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func getAcls(w http.ResponseWriter, r *http.Request) { + netID := r.URL.Query().Get("network") + if netID == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network id param is missing"), "badrequest")) + return + } + // check if network exists + _, err := logic.GetNetwork(netID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + acls, err := logic.ListAcls(models.NetworkID(netID)) + if err != nil { + logger.Log(0, r.Header.Get("user"), "failed to get all network acl entries: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + logic.SortAclEntrys(acls[:]) + logic.ReturnSuccessResponseWithJson(w, r, acls, "fetched all acls in the network "+netID) +} + +// @Summary Create Acl +// @Router /api/v1/acls [post] +// @Tags ACL +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func createAcl(w http.ResponseWriter, r *http.Request) { + var req models.Acl + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + logger.Log(0, "error decoding request body: ", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + user, err := logic.GetUser(r.Header.Get("user")) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = logic.ValidateCreateAclReq(req) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + + acl := req + acl.ID = uuid.New().String() + acl.CreatedBy = user.UserName + acl.CreatedAt = time.Now().UTC() + acl.Default = false + if acl.RuleType == models.DevicePolicy { + acl.AllowedDirection = models.TrafficDirectionBi + } else { + acl.AllowedDirection = models.TrafficDirectionUni + } + // validate create acl policy + if !logic.IsAclPolicyValid(acl) { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy"), "badrequest")) + return + } + err = logic.InsertAcl(acl) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + acl, err = logic.GetAcl(acl.ID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + go mq.PublishPeerUpdate(false) + logic.ReturnSuccessResponseWithJson(w, r, acl, "created acl successfully") +} + +// @Summary Update Acl +// @Router /api/v1/acls [put] +// @Tags ACL +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func updateAcl(w http.ResponseWriter, r *http.Request) { + var updateAcl models.UpdateAclRequest + err := json.NewDecoder(r.Body).Decode(&updateAcl) + if err != nil { + logger.Log(0, "error decoding request body: ", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + + acl, err := logic.GetAcl(updateAcl.ID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + if !logic.IsAclPolicyValid(updateAcl.Acl) { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy"), "badrequest")) + return + } + if updateAcl.Acl.NetworkID != acl.NetworkID { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy, network id mismatch"), "badrequest")) + return + } + if !acl.Default && updateAcl.NewName != "" { + //check if policy exists with same name + updateAcl.Acl.Name = updateAcl.NewName + } + err = logic.UpdateAcl(updateAcl.Acl, acl) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + go mq.PublishPeerUpdate(false) + logic.ReturnSuccessResponse(w, r, "updated acl "+acl.Name) +} + +// @Summary Delete Acl +// @Router /api/v1/acls [delete] +// @Tags ACL +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func deleteAcl(w http.ResponseWriter, r *http.Request) { + aclID, _ := url.QueryUnescape(r.URL.Query().Get("acl_id")) + if aclID == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("acl id is required"), "badrequest")) + return + } + acl, err := logic.GetAcl(aclID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + if acl.Default { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot delete default policy"), "badrequest")) + return + } + err = logic.DeleteAcl(acl) + if err != nil { + logic.ReturnErrorResponse(w, r, + logic.FormatError(errors.New("cannot delete default policy"), "internal")) + return + } + go mq.PublishPeerUpdate(false) + logic.ReturnSuccessResponse(w, r, "deleted acl "+acl.Name) +} diff --git a/controllers/controller.go b/controllers/controller.go index 75423b6e1..317536dd8 100644 --- a/controllers/controller.go +++ b/controllers/controller.go @@ -34,6 +34,8 @@ var HttpHandlers = []interface{}{ loggerHandlers, hostHandlers, enrollmentKeyHandlers, + tagHandlers, + aclHandlers, legacyHandlers, } diff --git a/controllers/dns.go b/controllers/dns.go index b1fc71fc3..cc1d70abf 100644 --- a/controllers/dns.go +++ b/controllers/dns.go @@ -11,6 +11,7 @@ import ( "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" "github.com/gravitl/netmaker/models" + "github.com/gravitl/netmaker/mq" "github.com/gravitl/netmaker/servercfg" ) @@ -24,6 +25,8 @@ func dnsHandlers(r *mux.Router) { Methods(http.MethodGet) r.HandleFunc("/api/dns/adm/{network}", logic.SecurityCheck(true, http.HandlerFunc(getDNS))). Methods(http.MethodGet) + r.HandleFunc("/api/dns/adm/{network}/sync", logic.SecurityCheck(true, http.HandlerFunc(syncDNS))). + Methods(http.MethodPost) r.HandleFunc("/api/dns/{network}", logic.SecurityCheck(true, http.HandlerFunc(createDNS))). Methods(http.MethodPost) r.HandleFunc("/api/dns/adm/pushdns", logic.SecurityCheck(true, http.HandlerFunc(pushDNS))). @@ -147,6 +150,7 @@ func createDNS(w http.ResponseWriter, r *http.Request) { var entry models.DNSEntry var params = mux.Vars(r) + netID := params["network"] _ = json.NewDecoder(r.Body).Decode(&entry) entry.Network = params["network"] @@ -176,6 +180,10 @@ func createDNS(w http.ResponseWriter, r *http.Request) { } } + if servercfg.GetManageDNS() { + mq.SendDNSSyncByNetwork(netID) + } + logger.Log(1, "new DNS record added:", entry.Name) logger.Log(2, r.Header.Get("user"), fmt.Sprintf("DNS entry is set: %+v", entry)) @@ -197,6 +205,7 @@ func deleteDNS(w http.ResponseWriter, r *http.Request) { // get params var params = mux.Vars(r) + netID := params["network"] entrytext := params["domain"] + "." + params["network"] err := logic.DeleteDNS(params["domain"], params["network"]) @@ -216,6 +225,10 @@ func deleteDNS(w http.ResponseWriter, r *http.Request) { } } + if servercfg.GetManageDNS() { + mq.SendDNSSyncByNetwork(netID) + } + json.NewEncoder(w).Encode(entrytext + " deleted.") } @@ -264,3 +277,38 @@ func pushDNS(w http.ResponseWriter, r *http.Request) { logger.Log(1, r.Header.Get("user"), "pushed DNS updates to nameserver") json.NewEncoder(w).Encode("DNS Pushed to CoreDNS") } + +// @Summary Sync DNS entries for a given network +// @Router /api/dns/adm/{network}/sync [post] +// @Tags DNS +// @Accept json +// @Success 200 {string} string "DNS Sync completed successfully" +// @Failure 400 {object} models.ErrorResponse +// @Failure 500 {object} models.ErrorResponse +func syncDNS(w http.ResponseWriter, r *http.Request) { + // Set header + w.Header().Set("Content-Type", "application/json") + if !servercfg.GetManageDNS() { + logic.ReturnErrorResponse( + w, + r, + logic.FormatError(errors.New("manage DNS is set to false"), "badrequest"), + ) + return + } + var params = mux.Vars(r) + netID := params["network"] + k, err := logic.GetDNS(netID) + if err == nil && len(k) > 0 { + err = mq.PushSyncDNS(k) + } + + if err != nil { + logger.Log(0, r.Header.Get("user"), + fmt.Sprintf("Failed to Sync DNS entries to network %s: %v", netID, err)) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + logger.Log(1, r.Header.Get("user"), "DNS Sync complelted successfully") + json.NewEncoder(w).Encode("DNS Sync completed successfully") +} diff --git a/controllers/enrollmentkeys.go b/controllers/enrollmentkeys.go index dc6669bd6..1ab9498ea 100644 --- a/controllers/enrollmentkeys.go +++ b/controllers/enrollmentkeys.go @@ -72,7 +72,7 @@ func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) { func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) keyID := params["keyID"] - err := logic.DeleteEnrollmentKey(keyID) + err := logic.DeleteEnrollmentKey(keyID, false) if err != nil { logger.Log(0, r.Header.Get("user"), "failed to remove enrollment key: ", err.Error()) logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) @@ -156,8 +156,10 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) { newTime, enrollmentKeyBody.Networks, enrollmentKeyBody.Tags, + enrollmentKeyBody.Groups, enrollmentKeyBody.Unlimited, relayId, + false, ) if err != nil { logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error()) @@ -206,7 +208,7 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) { } } - newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId) + newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId, enrollmentKeyBody.Groups) if err != nil { slog.Error("failed to update enrollment key", "error", err) logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) @@ -307,6 +309,7 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) { return } } + if err = logic.CreateHost(&newHost); err != nil { logger.Log( 0, @@ -355,5 +358,5 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(&response) // notify host of changes, peer and node updates - go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, &newHost, enrollmentKey.Relay) + go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, &newHost, enrollmentKey.Relay, enrollmentKey.Groups) } diff --git a/controllers/ext_client.go b/controllers/ext_client.go index b98256927..aabb5103e 100644 --- a/controllers/ext_client.go +++ b/controllers/ext_client.go @@ -244,6 +244,9 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) { if network.DefaultKeepalive != 0 { keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive)) } + if gwnode.IngressPersistentKeepalive != 0 { + keepalive = "PersistentKeepalive = " + strconv.Itoa(int(gwnode.IngressPersistentKeepalive)) + } gwendpoint := "" if preferredIp == "" { @@ -284,11 +287,30 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) { } else if gwnode.IngressDNS != "" { defaultDNS = "DNS = " + gwnode.IngressDNS } + // if servercfg.GetManageDNS() { + // if gwnode.Address6.IP != nil { + // if defaultDNS == "" { + // defaultDNS = "DNS = " + gwnode.Address6.IP.String() + // } else { + // defaultDNS = defaultDNS + ", " + gwnode.Address6.IP.String() + // } + // } + // if gwnode.Address.IP != nil { + // if defaultDNS == "" { + // defaultDNS = "DNS = " + gwnode.Address.IP.String() + // } else { + // defaultDNS = defaultDNS + ", " + gwnode.Address.IP.String() + // } + // } + // } defaultMTU := 1420 if host.MTU != 0 { defaultMTU = host.MTU } + if gwnode.IngressMTU != 0 { + defaultMTU = int(gwnode.IngressMTU) + } postUp := strings.Builder{} if client.PostUp != "" && params["type"] != "qr" { @@ -446,13 +468,14 @@ func createExtClient(w http.ResponseWriter, r *http.Request) { extclient.OwnerID = userName extclient.RemoteAccessClientID = customExtClient.RemoteAccessClientID extclient.IngressGatewayID = nodeid - + extclient.Network = node.Network + extclient.Tags = make(map[models.TagID]struct{}) + extclient.Tags[models.TagID(fmt.Sprintf("%s.%s", extclient.Network, + models.RemoteAccessTagName))] = struct{}{} // set extclient dns to ingressdns if extclient dns is not explicitly set if (extclient.DNS == "") && (node.IngressDNS != "") { extclient.DNS = node.IngressDNS } - - extclient.Network = node.Network host, err := logic.GetHost(node.HostID.String()) if err != nil { logger.Log(0, r.Header.Get("user"), @@ -531,6 +554,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) { var update models.CustomExtClient //var oldExtClient models.ExtClient var sendPeerUpdate bool + var replacePeers bool err := json.NewDecoder(r.Body).Decode(&update) if err != nil { logger.Log(0, r.Header.Get("user"), "error decoding request body: ", @@ -588,6 +612,11 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) { if update.Enabled != oldExtClient.Enabled { sendPeerUpdate = true } + if update.PublicKey != oldExtClient.PublicKey { + //remove old peer entry + sendPeerUpdate = true + replacePeers = true + } newclient := logic.UpdateExtClient(&oldExtClient, &update) if err := logic.DeleteExtClient(oldExtClient.Network, oldExtClient.ClientID); err != nil { slog.Error( @@ -627,6 +656,11 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) { if changedID && servercfg.IsDNSMode() { logic.SetDNS() } + if replacePeers { + if err := mq.PublishDeletedClientPeerUpdate(&oldExtClient); err != nil { + slog.Error("error deleting old ext peers", "error", err.Error()) + } + } if sendPeerUpdate { // need to send a peer update to the ingress node as enablement of one of it's clients has changed ingressNode, err := logic.GetNodeByID(newclient.IngressGatewayID) if err == nil { @@ -735,7 +769,7 @@ func validateCustomExtClient(customExtClient *models.CustomExtClient, checkID bo //validate clientid if customExtClient.ClientID != "" { if err := isValid(customExtClient.ClientID, checkID); err != nil { - return fmt.Errorf("client validatation: %v", err) + return fmt.Errorf("client validation: %v", err) } } //extclient.ClientID = customExtClient.ClientID diff --git a/controllers/hosts.go b/controllers/hosts.go index 349894331..a1015cf95 100644 --- a/controllers/hosts.go +++ b/controllers/hosts.go @@ -167,6 +167,8 @@ func pull(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } + + sendPeerUpdate := false for _, nodeID := range host.Nodes { node, err := logic.GetNodeByID(nodeID) if err != nil { @@ -174,7 +176,13 @@ func pull(w http.ResponseWriter, r *http.Request) { continue } if node.FailedOverBy != uuid.Nil { - go logic.ResetFailedOverPeer(&node) + logic.ResetFailedOverPeer(&node) + sendPeerUpdate = true + } + } + if sendPeerUpdate { + if err := mq.PublishPeerUpdate(true); err != nil { + logger.Log(0, "fail to publish peer update: ", err.Error()) } } allNodes, err := logic.GetAllNodes() @@ -245,19 +253,6 @@ func updateHost(w http.ResponseWriter, r *http.Request) { newHost := newHostData.ConvertAPIHostToNMHost(currHost) - if newHost.Name != currHost.Name { - // update any rag role ids - for _, nodeID := range newHost.Nodes { - node, err := logic.GetNodeByID(nodeID) - if err == nil && node.IsIngressGateway { - role, err := logic.GetRole(models.GetRAGRoleID(node.Network, currHost.ID.String())) - if err == nil { - role.UiName = models.GetRAGRoleName(node.Network, newHost.Name) - logic.UpdateRole(role) - } - } - } - } logic.UpdateHost(newHost, currHost) // update the in memory struct values if err = logic.UpsertHost(newHost); err != nil { logger.Log(0, r.Header.Get("user"), "failed to update a host:", err.Error()) diff --git a/controllers/middleware.go b/controllers/middleware.go index fb2bef683..98cb85716 100644 --- a/controllers/middleware.go +++ b/controllers/middleware.go @@ -27,13 +27,20 @@ func userMiddleWare(handler http.Handler) http.Handler { r.Header.Set("TARGET_RSRC", "") r.Header.Set("RSRC_TYPE", "") r.Header.Set("TARGET_RSRC_ID", "") + r.Header.Set("RAC", "") r.Header.Set("NET_ID", params["network"]) + if r.URL.Query().Get("network") != "" { + r.Header.Set("NET_ID", r.URL.Query().Get("network")) + } if strings.Contains(route, "hosts") || strings.Contains(route, "nodes") { r.Header.Set("TARGET_RSRC", models.HostRsrc.String()) } if strings.Contains(route, "dns") { r.Header.Set("TARGET_RSRC", models.DnsRsrc.String()) } + if strings.Contains(route, "rac") { + r.Header.Set("RAC", "true") + } if strings.Contains(route, "users") { r.Header.Set("TARGET_RSRC", models.UserRsrc.String()) } @@ -53,6 +60,9 @@ func userMiddleWare(handler http.Handler) http.Handler { if strings.Contains(route, "acls") { r.Header.Set("TARGET_RSRC", models.AclRsrc.String()) } + if strings.Contains(route, "tags") { + r.Header.Set("TARGET_RSRC", models.TagRsrc.String()) + } if strings.Contains(route, "extclients") { r.Header.Set("TARGET_RSRC", models.ExtClientsRsrc.String()) } @@ -101,7 +111,6 @@ func userMiddleWare(handler http.Handler) http.Handler { r.Header.Get("TARGET_RSRC") == models.UserRsrc.String()) { r.Header.Set("IS_GLOBAL_ACCESS", "yes") } - r.Header.Set("RSRC_TYPE", r.Header.Get("TARGET_RSRC")) handler.ServeHTTP(w, r) }) diff --git a/controllers/network.go b/controllers/network.go index acb479ec9..86a6e3403 100644 --- a/controllers/network.go +++ b/controllers/network.go @@ -24,6 +24,8 @@ import ( func networkHandlers(r *mux.Router) { r.HandleFunc("/api/networks", logic.SecurityCheck(true, http.HandlerFunc(getNetworks))). Methods(http.MethodGet) + r.HandleFunc("/api/v1/networks/stats", logic.SecurityCheck(true, http.HandlerFunc(getNetworksStats))). + Methods(http.MethodGet) r.HandleFunc("/api/networks", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceNetworks, http.HandlerFunc(createNetwork)))). Methods(http.MethodPost) r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(getNetwork))). @@ -74,6 +76,48 @@ func getNetworks(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(allnetworks) } +// @Summary Lists all networks with stats +// @Router /api/v1/networks/stats [get] +// @Tags Networks +// @Security oauth +// @Produce json +// @Success 200 {object} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func getNetworksStats(w http.ResponseWriter, r *http.Request) { + + var err error + allnetworks, err := logic.GetNetworks() + if err != nil && !database.IsEmptyRecord(err) { + slog.Error("failed to fetch networks", "error", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + if r.Header.Get("ismaster") != "yes" { + username := r.Header.Get("user") + user, err := logic.GetUser(username) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + allnetworks = logic.FilterNetworksByRole(allnetworks, *user) + } + allNodes, err := logic.GetAllNodes() + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + netstats := []models.NetworkStatResp{} + logic.SortNetworks(allnetworks[:]) + for _, network := range allnetworks { + netstats = append(netstats, models.NetworkStatResp{ + Network: network, + Hosts: len(logic.GetNetworkNodesMemory(allNodes, network.NetID)), + }) + } + logger.Log(2, r.Header.Get("user"), "fetched networks.") + logic.ReturnSuccessResponseWithJson(w, r, netstats, "fetched networks with stats") +} + // @Summary Get a network // @Router /api/networks/{networkname} [get] // @Tags Networks @@ -412,6 +456,7 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) { return } go logic.DeleteNetworkRoles(network) + go logic.DeleteDefaultNetworkPolicies(models.NetworkID(network)) //delete network from allocated ip map go logic.RemoveNetworkFromAllocatedIpMap(network) @@ -487,7 +532,8 @@ func createNetwork(w http.ResponseWriter, r *http.Request) { return } logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID)) - + logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID)) + logic.CreateDefaultTags(models.NetworkID(network.NetID)) //add new network to allocated ip map go logic.AddNetworkToAllocatedIpMap(network.NetID) diff --git a/controllers/node.go b/controllers/node.go index fd6f5d902..d7f2e2569 100644 --- a/controllers/node.go +++ b/controllers/node.go @@ -326,6 +326,7 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) { if len(filteredNodes) > 0 { nodes = filteredNodes } + nodes = logic.AddStaticNodestoList(nodes) // returns all the nodes in JSON/API format apiNodes := logic.GetAllNodesAPI(nodes[:]) @@ -363,7 +364,9 @@ func getAllNodes(w http.ResponseWriter, r *http.Request) { if !userPlatformRole.FullAccess { nodes = logic.GetFilteredNodesByUserAccess(*user, nodes) } + } + nodes = logic.AddStaticNodestoList(nodes) // return all the nodes in JSON/API format apiNodes := logic.GetAllNodesAPI(nodes[:]) logger.Log(3, r.Header.Get("user"), "fetched all nodes they have access to") @@ -587,6 +590,7 @@ func createIngressGateway(w http.ResponseWriter, r *http.Request) { if err := mq.NodeUpdate(&node); err != nil { slog.Error("error publishing node update to node", "node", node.ID, "error", err) } + mq.PublishPeerUpdate(false) }() } @@ -631,6 +635,7 @@ func deleteIngressGateway(w http.ResponseWriter, r *http.Request) { if err := mq.PublishSingleHostPeerUpdate(host, allNodes, nil, removedClients[:], false, nil); err != nil { slog.Error("publishSingleHostUpdate", "host", host.Name, "error", err) } + mq.PublishPeerUpdate(false) if err := mq.NodeUpdate(&node); err != nil { slog.Error( "error publishing node update to node", @@ -746,6 +751,7 @@ func updateNode(w http.ResponseWriter, r *http.Request) { logger.Log(0, "error during node ACL update for node", newNode.ID.String()) } } + mq.PublishPeerUpdate(false) if servercfg.IsDNSMode() { logic.SetDNS() } diff --git a/controllers/regex.go b/controllers/regex.go index 2c1f0c51e..488e7dbc6 100644 --- a/controllers/regex.go +++ b/controllers/regex.go @@ -6,10 +6,10 @@ import ( ) var ( - errInvalidExtClientPubKey = errors.New("incorrect ext client public key") - errInvalidExtClientID = errors.New("ext client ID must be alphanumderic and/or dashes and less that 15 chars") - errInvalidExtClientExtraIP = errors.New("ext client extra ip must be a valid cidr") - errInvalidExtClientDNS = errors.New("ext client dns must be a valid ip address") + errInvalidExtClientPubKey = errors.New("incorrect client public key") + errInvalidExtClientID = errors.New("node name must be alphanumderic and/or dashes and less that 15 chars") + errInvalidExtClientExtraIP = errors.New("client extra ip must be a valid cidr") + errInvalidExtClientDNS = errors.New("client dns must be a valid ip address") errDuplicateExtClientName = errors.New("duplicate client name") ) diff --git a/controllers/server.go b/controllers/server.go index 84f732b95..10f548280 100644 --- a/controllers/server.go +++ b/controllers/server.go @@ -3,6 +3,7 @@ package controller import ( "encoding/json" "net/http" + "os" "strings" "syscall" "time" @@ -17,6 +18,8 @@ import ( "github.com/gravitl/netmaker/servercfg" ) +var cpuProfileLog *os.File + func serverHandlers(r *mux.Router) { // r.HandleFunc("/api/server/addnetwork/{network}", securityCheckServer(true, http.HandlerFunc(addNetwork))).Methods(http.MethodPost) r.HandleFunc( @@ -43,6 +46,21 @@ func serverHandlers(r *mux.Router) { r.HandleFunc("/api/server/status", getStatus).Methods(http.MethodGet) r.HandleFunc("/api/server/usage", logic.SecurityCheck(false, http.HandlerFunc(getUsage))). Methods(http.MethodGet) + r.HandleFunc("/api/server/cpu_profile", logic.SecurityCheck(false, http.HandlerFunc(cpuProfile))). + Methods(http.MethodPost) +} + +func cpuProfile(w http.ResponseWriter, r *http.Request) { + start := r.URL.Query().Get("action") == "start" + if start { + os.Remove("/root/data/cpu.prof") + cpuProfileLog = logic.StartCPUProfiling() + } else { + if cpuProfileLog != nil { + logic.StopCPUProfiling(cpuProfileLog) + cpuProfileLog = nil + } + } } func getUsage(w http.ResponseWriter, _ *http.Request) { diff --git a/controllers/tags.go b/controllers/tags.go new file mode 100644 index 000000000..633dab968 --- /dev/null +++ b/controllers/tags.go @@ -0,0 +1,231 @@ +package controller + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/gravitl/netmaker/logger" + "github.com/gravitl/netmaker/logic" + "github.com/gravitl/netmaker/models" + "github.com/gravitl/netmaker/mq" +) + +func tagHandlers(r *mux.Router) { + r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(getTags))). + Methods(http.MethodGet) + r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(createTag))). + Methods(http.MethodPost) + r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(updateTag))). + Methods(http.MethodPut) + r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(deleteTag))). + Methods(http.MethodDelete) + +} + +// @Summary List Tags in a network +// @Router /api/v1/tags [get] +// @Tags TAG +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func getTags(w http.ResponseWriter, r *http.Request) { + netID, _ := url.QueryUnescape(r.URL.Query().Get("network")) + if netID == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network id param is missing"), "badrequest")) + return + } + // check if network exists + _, err := logic.GetNetwork(netID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + tags, err := logic.ListTagsWithNodes(models.NetworkID(netID)) + if err != nil { + logger.Log(0, r.Header.Get("user"), "failed to get all network tag entries: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + logic.SortTagEntrys(tags[:]) + logic.ReturnSuccessResponseWithJson(w, r, tags, "fetched all tags in the network "+netID) +} + +// @Summary Create Tag +// @Router /api/v1/tags [post] +// @Tags TAG +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func createTag(w http.ResponseWriter, r *http.Request) { + var req models.CreateTagReq + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + logger.Log(0, "error decoding request body: ", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + user, err := logic.GetUser(r.Header.Get("user")) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + // check if tag network exists + _, err = logic.GetNetwork(req.Network.String()) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to get network details for "+req.Network.String()), "badrequest")) + return + } + // check if tag exists + tag := models.Tag{ + ID: models.TagID(fmt.Sprintf("%s.%s", req.Network, req.TagName)), + TagName: req.TagName, + Network: req.Network, + CreatedBy: user.UserName, + CreatedAt: time.Now(), + } + _, err = logic.GetTag(tag.ID) + if err == nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("tag with id %s exists already", tag.TagName), "badrequest")) + return + } + // validate name + err = logic.CheckIDSyntax(tag.TagName) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = logic.InsertTag(tag) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + go func() { + for _, node := range req.TaggedNodes { + if node.IsStatic { + extclient, err := logic.GetExtClient(node.StaticNode.ClientID, node.StaticNode.Network) + if err == nil && extclient.RemoteAccessClientID == "" { + if extclient.Tags == nil { + extclient.Tags = make(map[models.TagID]struct{}) + } + extclient.Tags[tag.ID] = struct{}{} + logic.SaveExtClient(&extclient) + } + continue + } + node, err := logic.GetNodeByID(node.ID) + if err != nil { + continue + } + if node.Tags == nil { + node.Tags = make(map[models.TagID]struct{}) + } + node.Tags[tag.ID] = struct{}{} + logic.UpsertNode(&node) + } + }() + go mq.PublishPeerUpdate(false) + + var res models.TagListRespNodes = models.TagListRespNodes{ + Tag: tag, + UsedByCnt: len(req.TaggedNodes), + TaggedNodes: req.TaggedNodes, + } + + logic.ReturnSuccessResponseWithJson(w, r, res, "created tag successfully") +} + +// @Summary Update Tag +// @Router /api/v1/tags [put] +// @Tags TAG +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func updateTag(w http.ResponseWriter, r *http.Request) { + var updateTag models.UpdateTagReq + err := json.NewDecoder(r.Body).Decode(&updateTag) + if err != nil { + logger.Log(0, "error decoding request body: ", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + + tag, err := logic.GetTag(updateTag.ID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + updateTag.NewName = strings.TrimSpace(updateTag.NewName) + var newID models.TagID + if updateTag.NewName != "" { + // validate name + err = logic.CheckIDSyntax(updateTag.NewName) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + newID = models.TagID(fmt.Sprintf("%s.%s", tag.Network, updateTag.NewName)) + tag.ID = newID + tag.TagName = updateTag.NewName + err = logic.InsertTag(tag) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + // delete old Tag entry + logic.DeleteTag(updateTag.ID, false) + } + go func() { + logic.UpdateTag(updateTag, newID) + if updateTag.NewName != "" { + logic.UpdateDeviceTag(updateTag.ID, newID, tag.Network) + } + mq.PublishPeerUpdate(false) + }() + + var res models.TagListRespNodes = models.TagListRespNodes{ + Tag: tag, + UsedByCnt: len(updateTag.TaggedNodes), + TaggedNodes: updateTag.TaggedNodes, + } + + logic.ReturnSuccessResponseWithJson(w, r, res, "updated tags") +} + +// @Summary Delete Tag +// @Router /api/v1/tags [delete] +// @Tags TAG +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func deleteTag(w http.ResponseWriter, r *http.Request) { + tagID, _ := url.QueryUnescape(r.URL.Query().Get("tag_id")) + if tagID == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest")) + return + } + tag, err := logic.GetTag(models.TagID(tagID)) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = logic.DeleteTag(models.TagID(tagID), true) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + + go func() { + logic.RemoveDeviceTagFromAclPolicies(tag.ID, tag.Network) + logic.RemoveTagFromEnrollmentKeys(tag.ID) + mq.PublishPeerUpdate(false) + }() + logic.ReturnSuccessResponse(w, r, "deleted tag "+tagID) +} diff --git a/controllers/user.go b/controllers/user.go index c48a1b1da..8f3375cc8 100644 --- a/controllers/user.go +++ b/controllers/user.go @@ -451,6 +451,7 @@ func createUser(w http.ResponseWriter, r *http.Request) { } logic.DeleteUserInvite(user.UserName) logic.DeletePendingUser(user.UserName) + go mq.PublishPeerUpdate(false) slog.Info("user was created", "username", user.UserName) json.NewEncoder(w).Encode(logic.ToReturnUser(user)) } @@ -590,6 +591,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) return } + go mq.PublishPeerUpdate(false) logger.Log(1, username, "was updated") json.NewEncoder(w).Encode(logic.ToReturnUser(*user)) } @@ -692,6 +694,7 @@ func deleteUser(w http.ResponseWriter, r *http.Request) { } } } + mq.PublishPeerUpdate(false) if servercfg.IsDNSMode() { logic.SetDNS() } diff --git a/database/database.go b/database/database.go index f8508b3f0..6590e0063 100644 --- a/database/database.go +++ b/database/database.go @@ -47,6 +47,8 @@ const ( GENERATED_TABLE_NAME = "generated" // NODE_ACLS_TABLE_NAME - stores the node ACL rules NODE_ACLS_TABLE_NAME = "nodeacls" + // ACLS_TABLE_NAME - table for acls v2 + ACLS_TABLE_NAME = "acls" // SSO_STATE_CACHE - holds sso session information for OAuth2 sign-ins SSO_STATE_CACHE = "ssostatecache" // METRICS_TABLE_NAME - stores network metrics @@ -67,6 +69,8 @@ const ( PENDING_USERS_TABLE_NAME = "pending_users" // USER_INVITES - table for user invites USER_INVITES_TABLE_NAME = "user_invites" + // TAG_TABLE_NAME - table for tags + TAG_TABLE_NAME = "tags" // == ERROR CONSTS == // NO_RECORD - no singular result found NO_RECORD = "no result found" @@ -152,6 +156,8 @@ func createTables() { CreateTable(PENDING_USERS_TABLE_NAME) CreateTable(USER_PERMISSIONS_TABLE_NAME) CreateTable(USER_INVITES_TABLE_NAME) + CreateTable(TAG_TABLE_NAME) + CreateTable(ACLS_TABLE_NAME) } func CreateTable(tableName string) error { diff --git a/docker/Caddyfile b/docker/Caddyfile index 1226b72b3..9f5b35652 100644 --- a/docker/Caddyfile +++ b/docker/Caddyfile @@ -25,6 +25,10 @@ https://api.{$NM_DOMAIN} { } # MQ -wss://broker.{$NM_DOMAIN} { - reverse_proxy ws://mq:8883 # For EMQX websockets use `reverse_proxy ws://mq:8083` +broker.{$NM_DOMAIN} { + @ws { + header Connection *Upgrade* + header Upgrade websocket + } + reverse_proxy @ws mq:8883 # For EMQX websockets use `reverse_proxy @ws mq:8083` } diff --git a/docker/Caddyfile-pro b/docker/Caddyfile-pro index e986a8be5..226f61709 100644 --- a/docker/Caddyfile-pro +++ b/docker/Caddyfile-pro @@ -40,6 +40,10 @@ https://api.{$NM_DOMAIN} { } # MQ -wss://broker.{$NM_DOMAIN} { - reverse_proxy ws://mq:8883 +broker.{$NM_DOMAIN} { + @ws { + header Connection *Upgrade* + header Upgrade websocket + } + reverse_proxy @ws mq:8883 } diff --git a/docker/Dockerfile-go-builder b/docker/Dockerfile-go-builder index 0a19535fe..982f1e6ff 100644 --- a/docker/Dockerfile-go-builder +++ b/docker/Dockerfile-go-builder @@ -1,4 +1,4 @@ -FROM golang:1.20.13-alpine3.19 +FROM golang:1.23.0-alpine3.20 ARG version RUN apk add build-base WORKDIR /app diff --git a/docs/Authentication.md b/docs/Authentication.md index b1a7a4445..bd084a97c 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -7,4 +7,4 @@ Call the api/users/adm/authenticate endpoint (see documentation below for detail Note: While a MasterKey exists (configurable via env var or config file), it should be considered a backup option, used only when server access is lost. By default, this key is "secret key," but it's crucial to change this and keep it secure in your instance. -For more information on configuration and security best practices, refer to the [Netmaker documentation](https://docs.netmaker.org/index.html). +For more information on configuration and security best practices, refer to the [Netmaker documentation](https://docs.netmaker.io/). diff --git a/go.mod b/go.mod index b5bcfe909..b337d784a 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,26 @@ module github.com/gravitl/netmaker -go 1.19 +go 1.23 require ( github.com/eclipse/paho.mqtt.golang v1.4.3 - github.com/go-playground/validator/v10 v10.22.0 + github.com/go-playground/validator/v10 v10.22.1 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 github.com/lib/pq v1.10.9 - github.com/mattn/go-sqlite3 v1.14.22 + github.com/mattn/go-sqlite3 v1.14.24 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa - github.com/seancfoley/ipaddress-go v1.6.0 + github.com/seancfoley/ipaddress-go v1.7.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/stretchr/testify v1.9.0 github.com/txn2/txeh v1.5.5 - golang.org/x/crypto v0.23.0 - golang.org/x/net v0.23.0 // indirect - golang.org/x/oauth2 v0.21.0 - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/crypto v0.28.0 + golang.org/x/net v0.27.0 // indirect + golang.org/x/oauth2 v0.23.0 + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb gopkg.in/yaml.v3 v3.0.1 ) @@ -28,7 +28,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 github.com/c-robinson/iplib v1.0.8 - github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0 + github.com/posthog/posthog-go v1.2.24 ) require ( @@ -38,6 +38,7 @@ require ( ) require ( + github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e github.com/guumaster/tablewriter v0.0.10 github.com/matryer/is v1.4.1 github.com/olekukonko/tablewriter v0.0.5 @@ -53,7 +54,6 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/seancfoley/bintree v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect ) @@ -66,5 +66,5 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sync v0.7.0 // indirect + golang.org/x/sync v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index 33d5e376a..b59242e92 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,10 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2Qx cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/c-robinson/iplib v1.0.8 h1:exDRViDyL9UBLcfmlxxkY5odWX5092nPsQIykHXhIn4= github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo= github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -21,18 +19,21 @@ github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcP github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= -github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40= +github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -55,26 +56,24 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0 h1:Y2hUrkfuM0on62KZOci/VLijlkdF/yeWU262BQgvcjE= -github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU= +github.com/posthog/posthog-go v1.2.24 h1:A+iG4saBJemo++VDlcWovbYf8KFFNUfrCoJtsc40RPA= +github.com/posthog/posthog-go v1.2.24/go.mod h1:uYC2l1Yktc8E+9FAHJ9QZG4vQf/NHJPD800Hsm7DzoM= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa h1:hxMLFbj+F444JAS5nUQxTDZwUxwCRqg3WkNqhiDzXrM= github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/seancfoley/bintree v1.3.1 h1:cqmmQK7Jm4aw8gna0bP+huu5leVOgHGSJBEpUx3EXGI= github.com/seancfoley/bintree v1.3.1/go.mod h1:hIUabL8OFYyFVTQ6azeajbopogQc2l5C/hiXMcemWNU= -github.com/seancfoley/ipaddress-go v1.6.0 h1:9z7yGmOnV4P2ML/dlR/kCJiv5tp8iHOOetJvxJh/R5w= -github.com/seancfoley/ipaddress-go v1.6.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/seancfoley/ipaddress-go v1.7.0 h1:vWp3SR3k+HkV3aKiNO2vEe6xbVxS0x/Ixw6hgyP238s= +github.com/seancfoley/ipaddress-go v1.7.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= @@ -87,15 +86,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4= github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4= -github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= -github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -105,15 +101,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -122,8 +118,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -135,8 +131,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -150,7 +146,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/k8s/client/netclient-daemonset.yaml b/k8s/client/netclient-daemonset.yaml index f1a3f407d..cc7b744a0 100644 --- a/k8s/client/netclient-daemonset.yaml +++ b/k8s/client/netclient-daemonset.yaml @@ -16,7 +16,7 @@ spec: hostNetwork: true containers: - name: netclient - image: gravitl/netclient:v0.25.0 + image: gravitl/netclient:v0.26.0 env: - name: TOKEN value: "TOKEN_VALUE" diff --git a/k8s/client/netclient.yaml b/k8s/client/netclient.yaml index 8ab9ec97b..9aa3f396d 100644 --- a/k8s/client/netclient.yaml +++ b/k8s/client/netclient.yaml @@ -28,7 +28,7 @@ spec: # - "" containers: - name: netclient - image: gravitl/netclient:v0.25.0 + image: gravitl/netclient:v0.26.0 env: - name: TOKEN value: "TOKEN_VALUE" diff --git a/k8s/server/netmaker-ui.yaml b/k8s/server/netmaker-ui.yaml index 5600bea41..3212ec10a 100644 --- a/k8s/server/netmaker-ui.yaml +++ b/k8s/server/netmaker-ui.yaml @@ -15,7 +15,7 @@ spec: spec: containers: - name: netmaker-ui - image: gravitl/netmaker-ui:v0.25.0 + image: gravitl/netmaker-ui:v0.26.0 ports: - containerPort: 443 env: diff --git a/logic/acls.go b/logic/acls.go new file mode 100644 index 000000000..334e6f160 --- /dev/null +++ b/logic/acls.go @@ -0,0 +1,650 @@ +package logic + +import ( + "encoding/json" + "errors" + "fmt" + "sort" + "sync" + "time" + + "github.com/gravitl/netmaker/database" + "github.com/gravitl/netmaker/models" + "github.com/gravitl/netmaker/servercfg" +) + +var ( + aclCacheMutex = &sync.RWMutex{} + aclCacheMap = make(map[string]models.Acl) +) + +// CreateDefaultAclNetworkPolicies - create default acl network policies +func CreateDefaultAclNetworkPolicies(netID models.NetworkID) { + if netID.String() == "" { + return + } + if !IsAclExists(fmt.Sprintf("%s.%s", netID, "all-nodes")) { + defaultDeviceAcl := models.Acl{ + ID: fmt.Sprintf("%s.%s", netID, "all-nodes"), + Name: "All Nodes", + MetaData: "This Policy allows all nodes in the network to communicate with each other", + Default: true, + NetworkID: netID, + RuleType: models.DevicePolicy, + Src: []models.AclPolicyTag{ + { + ID: models.DeviceAclID, + Value: "*", + }}, + Dst: []models.AclPolicyTag{ + { + ID: models.DeviceAclID, + Value: "*", + }}, + AllowedDirection: models.TrafficDirectionBi, + Enabled: true, + CreatedBy: "auto", + CreatedAt: time.Now().UTC(), + } + InsertAcl(defaultDeviceAcl) + } + if !IsAclExists(fmt.Sprintf("%s.%s", netID, "all-users")) { + defaultUserAcl := models.Acl{ + ID: fmt.Sprintf("%s.%s", netID, "all-users"), + Default: true, + Name: "All Users", + MetaData: "This policy gives access to everything in the network for an user", + NetworkID: netID, + RuleType: models.UserPolicy, + Src: []models.AclPolicyTag{ + { + ID: models.UserAclID, + Value: "*", + }, + }, + Dst: []models.AclPolicyTag{{ + ID: models.DeviceAclID, + Value: "*", + }}, + AllowedDirection: models.TrafficDirectionUni, + Enabled: true, + CreatedBy: "auto", + CreatedAt: time.Now().UTC(), + } + InsertAcl(defaultUserAcl) + } + + if !IsAclExists(fmt.Sprintf("%s.%s", netID, "all-remote-access-gws")) { + defaultUserAcl := models.Acl{ + ID: fmt.Sprintf("%s.%s", netID, "all-remote-access-gws"), + Default: true, + Name: "All Remote Access Gateways", + NetworkID: netID, + RuleType: models.DevicePolicy, + Src: []models.AclPolicyTag{ + { + ID: models.DeviceAclID, + Value: fmt.Sprintf("%s.%s", netID, models.RemoteAccessTagName), + }, + }, + Dst: []models.AclPolicyTag{ + { + ID: models.DeviceAclID, + Value: "*", + }, + }, + AllowedDirection: models.TrafficDirectionBi, + Enabled: true, + CreatedBy: "auto", + CreatedAt: time.Now().UTC(), + } + InsertAcl(defaultUserAcl) + } + CreateDefaultUserPolicies(netID) +} + +// DeleteDefaultNetworkPolicies - deletes all default network acl policies +func DeleteDefaultNetworkPolicies(netId models.NetworkID) { + acls, _ := ListAcls(netId) + for _, acl := range acls { + if acl.NetworkID == netId && acl.Default { + DeleteAcl(acl) + } + } +} + +// ValidateCreateAclReq - validates create req for acl +func ValidateCreateAclReq(req models.Acl) error { + // check if acl network exists + _, err := GetNetwork(req.NetworkID.String()) + if err != nil { + return errors.New("failed to get network details for " + req.NetworkID.String()) + } + // err = CheckIDSyntax(req.Name) + // if err != nil { + // return err + // } + return nil +} + +func listAclFromCache() (acls []models.Acl) { + aclCacheMutex.RLock() + defer aclCacheMutex.RUnlock() + for _, acl := range aclCacheMap { + acls = append(acls, acl) + } + return +} + +func storeAclInCache(a models.Acl) { + aclCacheMutex.Lock() + defer aclCacheMutex.Unlock() + aclCacheMap[a.ID] = a +} + +func removeAclFromCache(a models.Acl) { + aclCacheMutex.Lock() + defer aclCacheMutex.Unlock() + delete(aclCacheMap, a.ID) +} + +func getAclFromCache(aID string) (a models.Acl, ok bool) { + aclCacheMutex.RLock() + defer aclCacheMutex.RUnlock() + a, ok = aclCacheMap[aID] + return +} + +// InsertAcl - creates acl policy +func InsertAcl(a models.Acl) error { + d, err := json.Marshal(a) + if err != nil { + return err + } + err = database.Insert(a.ID, string(d), database.ACLS_TABLE_NAME) + if err == nil && servercfg.CacheEnabled() { + storeAclInCache(a) + } + return err +} + +// GetAcl - gets acl info by id +func GetAcl(aID string) (models.Acl, error) { + a := models.Acl{} + if servercfg.CacheEnabled() { + var ok bool + a, ok = getAclFromCache(aID) + if ok { + return a, nil + } + } + d, err := database.FetchRecord(database.ACLS_TABLE_NAME, aID) + if err != nil { + return a, err + } + err = json.Unmarshal([]byte(d), &a) + if err != nil { + return a, err + } + if servercfg.CacheEnabled() { + storeAclInCache(a) + } + return a, nil +} + +// IsAclExists - checks if acl exists +func IsAclExists(aclID string) bool { + _, err := GetAcl(aclID) + return err == nil +} + +// IsAclPolicyValid - validates if acl policy is valid +func IsAclPolicyValid(acl models.Acl) bool { + //check if src and dst are valid + + switch acl.RuleType { + case models.UserPolicy: + // src list should only contain users + for _, srcI := range acl.Src { + + if srcI.ID == "" || srcI.Value == "" { + return false + } + if srcI.Value == "*" { + continue + } + if srcI.ID != models.UserAclID && srcI.ID != models.UserGroupAclID { + return false + } + // check if user group is valid + if srcI.ID == models.UserAclID { + _, err := GetUser(srcI.Value) + if err != nil { + return false + } + + } else if srcI.ID == models.UserGroupAclID { + err := IsGroupValid(models.UserGroupID(srcI.Value)) + if err != nil { + return false + } + // check if group belongs to this network + netGrps := GetUserGroupsInNetwork(acl.NetworkID) + if _, ok := netGrps[models.UserGroupID(srcI.Value)]; !ok { + return false + } + } + + } + for _, dstI := range acl.Dst { + + if dstI.ID == "" || dstI.Value == "" { + return false + } + if dstI.ID != models.DeviceAclID { + return false + } + if dstI.Value == "*" { + continue + } + // check if tag is valid + _, err := GetTag(models.TagID(dstI.Value)) + if err != nil { + return false + } + } + case models.DevicePolicy: + for _, srcI := range acl.Src { + if srcI.ID == "" || srcI.Value == "" { + return false + } + if srcI.ID != models.DeviceAclID { + return false + } + if srcI.Value == "*" { + continue + } + // check if tag is valid + _, err := GetTag(models.TagID(srcI.Value)) + if err != nil { + return false + } + } + for _, dstI := range acl.Dst { + + if dstI.ID == "" || dstI.Value == "" { + return false + } + if dstI.ID != models.DeviceAclID { + return false + } + if dstI.Value == "*" { + continue + } + // check if tag is valid + _, err := GetTag(models.TagID(dstI.Value)) + if err != nil { + return false + } + } + } + return true +} + +// UpdateAcl - updates allowed fields on acls and commits to DB +func UpdateAcl(newAcl, acl models.Acl) error { + if !acl.Default { + acl.Name = newAcl.Name + acl.Src = newAcl.Src + acl.Dst = newAcl.Dst + } + acl.Enabled = newAcl.Enabled + d, err := json.Marshal(acl) + if err != nil { + return err + } + err = database.Insert(acl.ID, string(d), database.ACLS_TABLE_NAME) + if err == nil && servercfg.CacheEnabled() { + storeAclInCache(acl) + } + return err +} + +// UpsertAcl - upserts acl +func UpsertAcl(acl models.Acl) error { + d, err := json.Marshal(acl) + if err != nil { + return err + } + err = database.Insert(acl.ID, string(d), database.ACLS_TABLE_NAME) + if err == nil && servercfg.CacheEnabled() { + storeAclInCache(acl) + } + return err +} + +// DeleteAcl - deletes acl policy +func DeleteAcl(a models.Acl) error { + err := database.DeleteRecord(database.ACLS_TABLE_NAME, a.ID) + if err == nil && servercfg.CacheEnabled() { + removeAclFromCache(a) + } + return err +} + +// GetDefaultPolicy - fetches default policy in the network by ruleType +func GetDefaultPolicy(netID models.NetworkID, ruleType models.AclPolicyType) (models.Acl, error) { + aclID := "all-users" + if ruleType == models.DevicePolicy { + aclID = "all-nodes" + } + acl, err := GetAcl(fmt.Sprintf("%s.%s", netID, aclID)) + if err != nil { + return models.Acl{}, errors.New("default rule not found") + } + if acl.Enabled { + return acl, nil + } + // check if there are any custom all policies + policies, _ := ListAcls(netID) + for _, policy := range policies { + if !policy.Enabled { + continue + } + if policy.RuleType == ruleType { + dstMap := convAclTagToValueMap(policy.Dst) + srcMap := convAclTagToValueMap(policy.Src) + if _, ok := srcMap["*"]; ok { + if _, ok := dstMap["*"]; ok { + return policy, nil + } + } + } + + } + + return acl, nil +} + +func listAcls() (acls []models.Acl) { + if servercfg.CacheEnabled() && len(aclCacheMap) > 0 { + return listAclFromCache() + } + + data, err := database.FetchRecords(database.ACLS_TABLE_NAME) + if err != nil && !database.IsEmptyRecord(err) { + return []models.Acl{} + } + + for _, dataI := range data { + acl := models.Acl{} + err := json.Unmarshal([]byte(dataI), &acl) + if err != nil { + continue + } + acls = append(acls, acl) + if servercfg.CacheEnabled() { + storeAclInCache(acl) + } + } + return +} + +// ListUserPolicies - lists all acl policies enforced on an user +func ListUserPolicies(u models.User) []models.Acl { + allAcls := listAcls() + userAcls := []models.Acl{} + for _, acl := range allAcls { + + if acl.RuleType == models.UserPolicy { + srcMap := convAclTagToValueMap(acl.Src) + if _, ok := srcMap[u.UserName]; ok { + userAcls = append(userAcls, acl) + } else { + // check for user groups + for gID := range u.UserGroups { + if _, ok := srcMap[gID.String()]; ok { + userAcls = append(userAcls, acl) + break + } + } + } + + } + } + return userAcls +} + +// listPoliciesOfUser - lists all user acl policies applied to user in an network +func listPoliciesOfUser(user models.User, netID models.NetworkID) []models.Acl { + allAcls := listAcls() + userAcls := []models.Acl{} + for _, acl := range allAcls { + if acl.NetworkID == netID && acl.RuleType == models.UserPolicy { + srcMap := convAclTagToValueMap(acl.Src) + if _, ok := srcMap[user.UserName]; ok { + userAcls = append(userAcls, acl) + continue + } + for netRole := range user.NetworkRoles { + if _, ok := srcMap[netRole.String()]; ok { + userAcls = append(userAcls, acl) + continue + } + } + for userG := range user.UserGroups { + if _, ok := srcMap[userG.String()]; ok { + userAcls = append(userAcls, acl) + continue + } + } + + } + } + return userAcls +} + +// listDevicePolicies - lists all device policies in a network +func listDevicePolicies(netID models.NetworkID) []models.Acl { + allAcls := listAcls() + deviceAcls := []models.Acl{} + for _, acl := range allAcls { + if acl.NetworkID == netID && acl.RuleType == models.DevicePolicy { + deviceAcls = append(deviceAcls, acl) + } + } + return deviceAcls +} + +// ListAcls - lists all acl policies +func ListAcls(netID models.NetworkID) ([]models.Acl, error) { + + allAcls := listAcls() + netAcls := []models.Acl{} + for _, acl := range allAcls { + if acl.NetworkID == netID { + netAcls = append(netAcls, acl) + } + } + return netAcls, nil +} + +func convAclTagToValueMap(acltags []models.AclPolicyTag) map[string]struct{} { + aclValueMap := make(map[string]struct{}) + for _, aclTagI := range acltags { + aclValueMap[aclTagI.Value] = struct{}{} + } + return aclValueMap +} + +// IsUserAllowedToCommunicate - check if user is allowed to communicate with peer +func IsUserAllowedToCommunicate(userName string, peer models.Node) bool { + if peer.IsStatic { + peer = peer.StaticNode.ConvertToStaticNode() + } + acl, _ := GetDefaultPolicy(models.NetworkID(peer.Network), models.UserPolicy) + if acl.Enabled { + return true + } + user, err := GetUser(userName) + if err != nil { + return false + } + + policies := listPoliciesOfUser(*user, models.NetworkID(peer.Network)) + for _, policy := range policies { + if !policy.Enabled { + continue + } + dstMap := convAclTagToValueMap(policy.Dst) + if _, ok := dstMap["*"]; ok { + return true + } + for tagID := range peer.Tags { + if _, ok := dstMap[tagID.String()]; ok { + return true + } + } + + } + return false +} + +// IsNodeAllowedToCommunicate - check node is allowed to communicate with the peer +func IsNodeAllowedToCommunicate(node, peer models.Node) bool { + if node.IsStatic { + node = node.StaticNode.ConvertToStaticNode() + } + if peer.IsStatic { + peer = peer.StaticNode.ConvertToStaticNode() + } + // check default policy if all allowed return true + defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy) + if err == nil { + if defaultPolicy.Enabled { + return true + } + } + + // list device policies + policies := listDevicePolicies(models.NetworkID(peer.Network)) + for _, policy := range policies { + if !policy.Enabled { + continue + } + srcMap := convAclTagToValueMap(policy.Src) + dstMap := convAclTagToValueMap(policy.Dst) + // fmt.Printf("\n======> SRCMAP: %+v\n", srcMap) + // fmt.Printf("\n======> DSTMAP: %+v\n", dstMap) + // fmt.Printf("\n======> node Tags: %+v\n", node.Tags) + // fmt.Printf("\n======> peer Tags: %+v\n", peer.Tags) + for tagID := range node.Tags { + if _, ok := dstMap[tagID.String()]; ok { + if _, ok := srcMap["*"]; ok { + return true + } + for tagID := range peer.Tags { + if _, ok := srcMap[tagID.String()]; ok { + return true + } + } + } + if _, ok := srcMap[tagID.String()]; ok { + if _, ok := dstMap["*"]; ok { + return true + } + for tagID := range peer.Tags { + if _, ok := dstMap[tagID.String()]; ok { + return true + } + } + } + } + for tagID := range peer.Tags { + if _, ok := dstMap[tagID.String()]; ok { + if _, ok := srcMap["*"]; ok { + return true + } + for tagID := range node.Tags { + + if _, ok := srcMap[tagID.String()]; ok { + return true + } + } + } + if _, ok := srcMap[tagID.String()]; ok { + if _, ok := dstMap["*"]; ok { + return true + } + for tagID := range node.Tags { + if _, ok := dstMap[tagID.String()]; ok { + return true + } + } + } + } + } + return false +} + +// SortTagEntrys - Sorts slice of Tag entries by their id +func SortAclEntrys(acls []models.Acl) { + sort.Slice(acls, func(i, j int) bool { + return acls[i].Name < acls[j].Name + }) +} + +// UpdateDeviceTag - updates device tag on acl policies +func UpdateDeviceTag(OldID, newID models.TagID, netID models.NetworkID) { + acls := listDevicePolicies(netID) + update := false + for _, acl := range acls { + for i, srcTagI := range acl.Src { + if srcTagI.ID == models.DeviceAclID { + if OldID.String() == srcTagI.Value { + acl.Src[i].Value = newID.String() + update = true + } + } + } + for i, dstTagI := range acl.Dst { + if dstTagI.ID == models.DeviceAclID { + if OldID.String() == dstTagI.Value { + acl.Dst[i].Value = newID.String() + update = true + } + } + } + if update { + UpsertAcl(acl) + } + } +} + +// RemoveDeviceTagFromAclPolicies - remove device tag from acl policies +func RemoveDeviceTagFromAclPolicies(tagID models.TagID, netID models.NetworkID) error { + acls := listDevicePolicies(netID) + update := false + for _, acl := range acls { + for i, srcTagI := range acl.Src { + if srcTagI.ID == models.DeviceAclID { + if tagID.String() == srcTagI.Value { + acl.Src = append(acl.Src[:i], acl.Src[i+1:]...) + update = true + } + } + } + for i, dstTagI := range acl.Dst { + if dstTagI.ID == models.DeviceAclID { + if tagID.String() == dstTagI.Value { + acl.Dst = append(acl.Dst[:i], acl.Dst[i+1:]...) + update = true + } + } + } + if update { + UpsertAcl(acl) + } + } + return nil +} diff --git a/logic/acls/common.go b/logic/acls/common.go index 9296e3b80..bb35123a3 100644 --- a/logic/acls/common.go +++ b/logic/acls/common.go @@ -2,6 +2,7 @@ package acls import ( "encoding/json" + "maps" "sync" "github.com/gravitl/netmaker/database" @@ -133,7 +134,7 @@ func fetchACLContainer(containerID ContainerID) (ACLContainer, error) { defer AclMutex.RUnlock() if servercfg.CacheEnabled() { if aclContainer, ok := fetchAclContainerFromCache(containerID); ok { - return aclContainer, nil + return maps.Clone(aclContainer), nil } } aclJson, err := fetchACLContainerJson(ContainerID(containerID)) @@ -147,7 +148,7 @@ func fetchACLContainer(containerID ContainerID) (ACLContainer, error) { if servercfg.CacheEnabled() { storeAclContainerInCache(containerID, currentNetworkACL) } - return currentNetworkACL, nil + return maps.Clone(currentNetworkACL), nil } // fetchACLContainerJson - fetch the current ACL of given container except in json string diff --git a/logic/acls/nodeacls/retrieve.go b/logic/acls/nodeacls/retrieve.go index d5fa68c40..4411c5b22 100644 --- a/logic/acls/nodeacls/retrieve.go +++ b/logic/acls/nodeacls/retrieve.go @@ -3,6 +3,7 @@ package nodeacls import ( "encoding/json" "fmt" + "maps" "sync" "github.com/gravitl/netmaker/logic/acls" @@ -67,5 +68,5 @@ func FetchAllACLs(networkID NetworkID) (acls.ACLContainer, error) { if err != nil { return nil, err } - return currentNetworkACL, nil + return maps.Clone(currentNetworkACL), nil } diff --git a/logic/auth.go b/logic/auth.go index 3da9f605d..031e5fd14 100644 --- a/logic/auth.go +++ b/logic/auth.go @@ -186,7 +186,7 @@ func CreateUser(user *models.User) error { logger.Log(0, "failed to insert user", err.Error()) return err } - + AddGlobalNetRolesToAdmins(*user) return nil } @@ -305,6 +305,7 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) { } user.UserGroups = userchange.UserGroups user.NetworkRoles = userchange.NetworkRoles + AddGlobalNetRolesToAdmins(*user) err := ValidateUser(user) if err != nil { return &models.User{}, err diff --git a/logic/clients.go b/logic/clients.go index 7331ee615..d9eca8c9d 100644 --- a/logic/clients.go +++ b/logic/clients.go @@ -32,7 +32,7 @@ var ( slog.Error("failed to get network acls", "error", err) return err } - networkAcls[acls.AclID(ec.ClientID)] = acls.ACL{} + networkAcls[acls.AclID(ec.ClientID)] = make(acls.ACL) for objId := range networkAcls { networkAcls[objId][acls.AclID(ec.ClientID)] = acls.Allowed networkAcls[acls.AclID(ec.ClientID)][objId] = acls.Allowed diff --git a/logic/enrollmentkey.go b/logic/enrollmentkey.go index d3c48a011..25cf0d6d3 100644 --- a/logic/enrollmentkey.go +++ b/logic/enrollmentkey.go @@ -37,7 +37,7 @@ var ( ) // CreateEnrollmentKey - creates a new enrollment key in db -func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, unlimited bool, relay uuid.UUID) (*models.EnrollmentKey, error) { +func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, groups []models.TagID, unlimited bool, relay uuid.UUID, defaultKey bool) (*models.EnrollmentKey, error) { newKeyID, err := getUniqueEnrollmentID() if err != nil { return nil, err @@ -51,6 +51,8 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string Tags: []string{}, Type: models.Undefined, Relay: relay, + Groups: groups, + Default: defaultKey, } if uses > 0 { k.UsesRemaining = uses @@ -89,7 +91,7 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string } // UpdateEnrollmentKey - updates an existing enrollment key's associated relay -func UpdateEnrollmentKey(keyId string, relayId uuid.UUID) (*models.EnrollmentKey, error) { +func UpdateEnrollmentKey(keyId string, relayId uuid.UUID, groups []models.TagID) (*models.EnrollmentKey, error) { key, err := GetEnrollmentKey(keyId) if err != nil { return nil, err @@ -109,7 +111,7 @@ func UpdateEnrollmentKey(keyId string, relayId uuid.UUID) (*models.EnrollmentKey } key.Relay = relayId - + key.Groups = groups if err = upsertEnrollmentKey(&key); err != nil { return nil, err } @@ -151,11 +153,14 @@ func deleteEnrollmentkeyFromCache(key string) { } // DeleteEnrollmentKey - delete's a given enrollment key by value -func DeleteEnrollmentKey(value string) error { - _, err := GetEnrollmentKey(value) +func DeleteEnrollmentKey(value string, force bool) error { + key, err := GetEnrollmentKey(value) if err != nil { return err } + if key.Default && !force { + return errors.New("cannot delete default network key") + } err = database.DeleteRecord(database.ENROLLMENT_KEYS_TABLE_NAME, value) if err == nil { if servercfg.CacheEnabled() { @@ -310,3 +315,23 @@ func getEnrollmentKeysMap() (map[string]models.EnrollmentKey, error) { } return currentKeys, nil } + +func RemoveTagFromEnrollmentKeys(deletedTagID models.TagID) { + keys, _ := GetAllEnrollmentKeys() + for _, key := range keys { + newTags := []models.TagID{} + update := false + for _, tagID := range key.Groups { + if tagID == deletedTagID { + update = true + continue + } + newTags = append(newTags, tagID) + } + if update { + key.Groups = newTags + upsertEnrollmentKey(&key) + } + + } +} diff --git a/logic/enrollmentkey_test.go b/logic/enrollmentkey_test.go index 677c47141..92b4c5e28 100644 --- a/logic/enrollmentkey_test.go +++ b/logic/enrollmentkey_test.go @@ -14,35 +14,35 @@ func TestCreateEnrollmentKey(t *testing.T) { database.InitializeDatabase() defer database.CloseDB() t.Run("Can_Not_Create_Key", func(t *testing.T) { - newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, false, uuid.Nil) + newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, false, uuid.Nil, false) assert.Nil(t, newKey) assert.NotNil(t, err) assert.ErrorIs(t, err, models.ErrInvalidEnrollmentKey) }) t.Run("Can_Create_Key_Uses", func(t *testing.T) { - newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil) + newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false) assert.Nil(t, err) assert.Equal(t, 1, newKey.UsesRemaining) assert.True(t, newKey.IsValid()) }) t.Run("Can_Create_Key_Time", func(t *testing.T) { - newKey, err := CreateEnrollmentKey(0, time.Now().Add(time.Minute), nil, nil, false, uuid.Nil) + newKey, err := CreateEnrollmentKey(0, time.Now().Add(time.Minute), nil, nil, nil, false, uuid.Nil, false) assert.Nil(t, err) assert.True(t, newKey.IsValid()) }) t.Run("Can_Create_Key_Unlimited", func(t *testing.T) { - newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, true, uuid.Nil) + newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false) assert.Nil(t, err) assert.True(t, newKey.IsValid()) }) t.Run("Can_Create_Key_WithNetworks", func(t *testing.T) { - newKey, err := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil) + newKey, err := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false) assert.Nil(t, err) assert.True(t, newKey.IsValid()) assert.True(t, len(newKey.Networks) == 2) }) t.Run("Can_Create_Key_WithTags", func(t *testing.T) { - newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, []string{"tag1", "tag2"}, true, uuid.Nil) + newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, []string{"tag1", "tag2"}, nil, true, uuid.Nil, false) assert.Nil(t, err) assert.True(t, newKey.IsValid()) assert.True(t, len(newKey.Tags) == 2) @@ -62,10 +62,10 @@ func TestCreateEnrollmentKey(t *testing.T) { func TestDelete_EnrollmentKey(t *testing.T) { database.InitializeDatabase() defer database.CloseDB() - newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil) + newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false) t.Run("Can_Delete_Key", func(t *testing.T) { assert.True(t, newKey.IsValid()) - err := DeleteEnrollmentKey(newKey.Value) + err := DeleteEnrollmentKey(newKey.Value, false) assert.Nil(t, err) oldKey, err := GetEnrollmentKey(newKey.Value) assert.Equal(t, oldKey, models.EnrollmentKey{}) @@ -73,7 +73,7 @@ func TestDelete_EnrollmentKey(t *testing.T) { assert.Equal(t, err, EnrollmentErrors.NoKeyFound) }) t.Run("Can_Not_Delete_Invalid_Key", func(t *testing.T) { - err := DeleteEnrollmentKey("notakey") + err := DeleteEnrollmentKey("notakey", false) assert.NotNil(t, err) assert.Equal(t, err, EnrollmentErrors.NoKeyFound) }) @@ -83,7 +83,7 @@ func TestDelete_EnrollmentKey(t *testing.T) { func TestDecrement_EnrollmentKey(t *testing.T) { database.InitializeDatabase() defer database.CloseDB() - newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil) + newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false) t.Run("Check_initial_uses", func(t *testing.T) { assert.True(t, newKey.IsValid()) assert.Equal(t, newKey.UsesRemaining, 1) @@ -107,9 +107,9 @@ func TestDecrement_EnrollmentKey(t *testing.T) { func TestUsability_EnrollmentKey(t *testing.T) { database.InitializeDatabase() defer database.CloseDB() - key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil) - key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, false, uuid.Nil) - key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, true, uuid.Nil) + key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false) + key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, nil, false, uuid.Nil, false) + key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false) t.Run("Check if valid use key can be used", func(t *testing.T) { assert.Equal(t, key1.UsesRemaining, 1) ok := TryToUseEnrollmentKey(key1) @@ -145,7 +145,7 @@ func removeAllEnrollments() { func TestTokenize_EnrollmentKeys(t *testing.T) { database.InitializeDatabase() defer database.CloseDB() - newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil) + newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false) const defaultValue = "MwE5MwE5MwE5MwE5MwE5MwE5MwE5MwE5" const b64value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9" const serverAddr = "api.myserver.com" @@ -178,7 +178,7 @@ func TestTokenize_EnrollmentKeys(t *testing.T) { func TestDeTokenize_EnrollmentKeys(t *testing.T) { database.InitializeDatabase() defer database.CloseDB() - newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil) + newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false) const b64Value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9" const serverAddr = "api.myserver.com" diff --git a/logic/extpeers.go b/logic/extpeers.go index c619dde94..fcb422243 100644 --- a/logic/extpeers.go +++ b/logic/extpeers.go @@ -69,7 +69,7 @@ func GetEgressRangesOnNetwork(client *models.ExtClient) ([]string, error) { } } } - extclients := GetGwExtclients(client.IngressGatewayID, client.Network) + extclients, _ := GetNetworkExtClients(client.Network) for _, extclient := range extclients { if extclient.ClientID == client.ClientID { continue @@ -136,6 +136,12 @@ func DeleteExtClientAndCleanup(extClient models.ExtClient) error { return nil } +//TODO - enforce extclient-to-extclient on ingress gw +/* 1. fetch all non-user static nodes +a. check against each user node, if allowed add rule + +*/ + // GetNetworkExtClients - gets the ext clients of given network func GetNetworkExtClients(network string) ([]models.ExtClient, error) { var extclients []models.ExtClient @@ -329,6 +335,7 @@ func UpdateExtClient(old *models.ExtClient, update *models.CustomExtClient) mode // replace any \r\n with \n in postup and postdown from HTTP request new.PostUp = strings.Replace(update.PostUp, "\r\n", "\n", -1) new.PostDown = strings.Replace(update.PostDown, "\r\n", "\n", -1) + new.Tags = update.Tags return new } @@ -395,6 +402,206 @@ func ToggleExtClientConnectivity(client *models.ExtClient, enable bool) (models. return newClient, nil } +func GetStaticNodeIps(node models.Node) (ips []net.IP) { + defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy) + defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy) + + extclients := GetStaticNodesByNetwork(models.NetworkID(node.Network), false) + for _, extclient := range extclients { + if extclient.IsUserNode && defaultUserPolicy.Enabled { + continue + } + if !extclient.IsUserNode && defaultDevicePolicy.Enabled { + continue + } + if extclient.StaticNode.Address != "" { + ips = append(ips, extclient.StaticNode.AddressIPNet4().IP) + } + if extclient.StaticNode.Address6 != "" { + ips = append(ips, extclient.StaticNode.AddressIPNet6().IP) + } + } + return +} + +func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) { + // fetch user access to static clients via policies + defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy) + defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy) + nodes, _ := GetNetworkNodes(node.Network) + nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network), true)...) + //fmt.Printf("=====> NODES: %+v \n\n", nodes) + userNodes := GetStaticUserNodesByNetwork(models.NetworkID(node.Network)) + //fmt.Printf("=====> USER NODES %+v \n\n", userNodes) + for _, userNodeI := range userNodes { + for _, peer := range nodes { + if peer.IsUserNode { + continue + } + if IsUserAllowedToCommunicate(userNodeI.StaticNode.OwnerID, peer) { + if peer.IsStatic { + if userNodeI.StaticNode.Address != "" { + if !defaultUserPolicy.Enabled { + rules = append(rules, models.FwRule{ + SrcIP: userNodeI.StaticNode.AddressIPNet4(), + DstIP: peer.StaticNode.AddressIPNet4(), + Allow: true, + }) + } + rules = append(rules, models.FwRule{ + SrcIP: peer.StaticNode.AddressIPNet4(), + DstIP: userNodeI.StaticNode.AddressIPNet4(), + Allow: true, + }) + } + if userNodeI.StaticNode.Address6 != "" { + if !defaultUserPolicy.Enabled { + rules = append(rules, models.FwRule{ + SrcIP: userNodeI.StaticNode.AddressIPNet6(), + DstIP: peer.StaticNode.AddressIPNet6(), + Allow: true, + }) + } + + rules = append(rules, models.FwRule{ + SrcIP: peer.StaticNode.AddressIPNet6(), + DstIP: userNodeI.StaticNode.AddressIPNet6(), + Allow: true, + }) + } + if len(peer.StaticNode.ExtraAllowedIPs) > 0 { + for _, additionalAllowedIPNet := range peer.StaticNode.ExtraAllowedIPs { + _, ipNet, err := net.ParseCIDR(additionalAllowedIPNet) + if err != nil { + continue + } + if ipNet.IP.To4() != nil { + rules = append(rules, models.FwRule{ + SrcIP: userNodeI.StaticNode.AddressIPNet4(), + DstIP: *ipNet, + Allow: true, + }) + } else { + rules = append(rules, models.FwRule{ + SrcIP: userNodeI.StaticNode.AddressIPNet6(), + DstIP: *ipNet, + Allow: true, + }) + } + + } + + } + } else { + + if userNodeI.StaticNode.Address != "" { + if !defaultUserPolicy.Enabled { + rules = append(rules, models.FwRule{ + SrcIP: userNodeI.StaticNode.AddressIPNet4(), + DstIP: net.IPNet{ + IP: peer.Address.IP, + Mask: net.CIDRMask(32, 32), + }, + Allow: true, + }) + } + } + + if userNodeI.StaticNode.Address6 != "" { + rules = append(rules, models.FwRule{ + SrcIP: userNodeI.StaticNode.AddressIPNet6(), + DstIP: net.IPNet{ + IP: peer.Address6.IP, + Mask: net.CIDRMask(128, 128), + }, + Allow: true, + }) + } + } + + } + } + } + + if defaultDevicePolicy.Enabled { + return + } + for _, nodeI := range nodes { + if !nodeI.IsStatic || nodeI.IsUserNode { + continue + } + for _, peer := range nodes { + if peer.StaticNode.ClientID == nodeI.StaticNode.ClientID || peer.IsUserNode { + continue + } + if IsNodeAllowedToCommunicate(nodeI, peer) { + if peer.IsStatic { + if nodeI.StaticNode.Address != "" { + rules = append(rules, models.FwRule{ + SrcIP: nodeI.StaticNode.AddressIPNet4(), + DstIP: peer.StaticNode.AddressIPNet4(), + Allow: true, + }) + } + if nodeI.StaticNode.Address6 != "" { + rules = append(rules, models.FwRule{ + SrcIP: nodeI.StaticNode.AddressIPNet6(), + DstIP: peer.StaticNode.AddressIPNet6(), + Allow: true, + }) + } + if len(peer.StaticNode.ExtraAllowedIPs) > 0 { + for _, additionalAllowedIPNet := range peer.StaticNode.ExtraAllowedIPs { + _, ipNet, err := net.ParseCIDR(additionalAllowedIPNet) + if err != nil { + continue + } + if ipNet.IP.To4() != nil { + rules = append(rules, models.FwRule{ + SrcIP: nodeI.StaticNode.AddressIPNet4(), + DstIP: *ipNet, + Allow: true, + }) + } else { + rules = append(rules, models.FwRule{ + SrcIP: nodeI.StaticNode.AddressIPNet6(), + DstIP: *ipNet, + Allow: true, + }) + } + + } + + } + } else { + if nodeI.StaticNode.Address != "" { + rules = append(rules, models.FwRule{ + SrcIP: nodeI.StaticNode.AddressIPNet4(), + DstIP: net.IPNet{ + IP: peer.Address.IP, + Mask: net.CIDRMask(32, 32), + }, + Allow: true, + }) + } + if nodeI.StaticNode.Address6 != "" { + rules = append(rules, models.FwRule{ + SrcIP: nodeI.StaticNode.AddressIPNet6(), + DstIP: net.IPNet{ + IP: peer.Address6.IP, + Mask: net.CIDRMask(128, 128), + }, + Allow: true, + }) + } + } + + } + } + } + return +} + func GetExtPeers(node, peer *models.Node) ([]wgtypes.PeerConfig, []models.IDandAddr, []models.EgressNetworkRoutes, error) { var peers []wgtypes.PeerConfig var idsAndAddr []models.IDandAddr @@ -412,6 +619,16 @@ func GetExtPeers(node, peer *models.Node) ([]wgtypes.PeerConfig, []models.IDandA if !IsClientNodeAllowed(&extPeer, peer.ID.String()) { continue } + if extPeer.RemoteAccessClientID == "" { + if !IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), *peer) { + continue + } + } else { + if !IsUserAllowedToCommunicate(extPeer.OwnerID, *peer) { + continue + } + } + pubkey, err := wgtypes.ParseKey(extPeer.PublicKey) if err != nil { logger.Log(1, "error parsing ext pub key:", err.Error()) @@ -483,8 +700,8 @@ func getExtPeerEgressRoute(node models.Node, extPeer models.ExtClient) (egressRo return } -func getExtpeersExtraRoutes(node models.Node, network string) (egressRoutes []models.EgressNetworkRoutes) { - extPeers, err := GetNetworkExtClients(network) +func getExtpeerEgressRanges(node models.Node) (ranges, ranges6 []net.IPNet) { + extPeers, err := GetNetworkExtClients(node.Network) if err != nil { return } @@ -492,6 +709,36 @@ func getExtpeersExtraRoutes(node models.Node, network string) (egressRoutes []mo if len(extPeer.ExtraAllowedIPs) == 0 { continue } + if !IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), node) { + continue + } + for _, allowedRange := range extPeer.ExtraAllowedIPs { + _, ipnet, err := net.ParseCIDR(allowedRange) + if err == nil { + if ipnet.IP.To4() != nil { + ranges = append(ranges, *ipnet) + } else { + ranges6 = append(ranges6, *ipnet) + } + + } + } + } + return +} + +func getExtpeersExtraRoutes(node models.Node) (egressRoutes []models.EgressNetworkRoutes) { + extPeers, err := GetNetworkExtClients(node.Network) + if err != nil { + return + } + for _, extPeer := range extPeers { + if len(extPeer.ExtraAllowedIPs) == 0 { + continue + } + if !IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), node) { + continue + } egressRoutes = append(egressRoutes, getExtPeerEgressRoute(node, extPeer)...) } return @@ -528,3 +775,64 @@ func GetExtclientAllowedIPs(client models.ExtClient) (allowedIPs []string) { } return } + +func GetStaticUserNodesByNetwork(network models.NetworkID) (staticNode []models.Node) { + extClients, err := GetAllExtClients() + if err != nil { + return + } + for _, extI := range extClients { + if extI.Network == network.String() { + if extI.RemoteAccessClientID != "" { + n := models.Node{ + IsStatic: true, + StaticNode: extI, + IsUserNode: extI.RemoteAccessClientID != "", + } + staticNode = append(staticNode, n) + } + } + } + + return +} + +func GetStaticNodesByNetwork(network models.NetworkID, onlyWg bool) (staticNode []models.Node) { + extClients, err := GetAllExtClients() + if err != nil { + return + } + for _, extI := range extClients { + if extI.Network == network.String() { + if onlyWg && extI.RemoteAccessClientID != "" { + continue + } + n := models.Node{ + IsStatic: true, + StaticNode: extI, + IsUserNode: extI.RemoteAccessClientID != "", + } + staticNode = append(staticNode, n) + } + } + + return +} + +func GetStaticNodesByGw(gwNode models.Node) (staticNode []models.Node) { + extClients, err := GetAllExtClients() + if err != nil { + return + } + for _, extI := range extClients { + if extI.IngressGatewayID == gwNode.ID.String() { + n := models.Node{ + IsStatic: true, + StaticNode: extI, + IsUserNode: extI.RemoteAccessClientID != "", + } + staticNode = append(staticNode, n) + } + } + return +} diff --git a/logic/gateway.go b/logic/gateway.go index 87a41105b..a92682c5e 100644 --- a/logic/gateway.go +++ b/logic/gateway.go @@ -2,6 +2,7 @@ package logic import ( "errors" + "fmt" "time" "github.com/gravitl/netmaker/database" @@ -74,7 +75,7 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro return models.Node{}, errors.New(host.OS + " is unsupported for egress gateways") } if host.FirewallInUse == models.FIREWALL_NONE { - return models.Node{}, errors.New("firewall is not supported for egress gateways. please install iptables or nftables on the device in order to use this feature") + return models.Node{}, errors.New("please install iptables or nftables on the device") } for i := len(gateway.Ranges) - 1; i >= 0; i-- { // check if internet gateway IPv4 @@ -149,9 +150,6 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq if host.OS != "linux" { return models.Node{}, errors.New("ingress can only be created on linux based node") } - if host.FirewallInUse == models.FIREWALL_NONE { - return models.Node{}, errors.New("firewall is not supported for ingress gateways") - } network, err := GetParentNetwork(netid) if err != nil { @@ -164,6 +162,14 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq node.IngressGatewayRange = network.AddressRange node.IngressGatewayRange6 = network.AddressRange6 node.IngressDNS = ingress.ExtclientDNS + node.IngressPersistentKeepalive = 20 + if ingress.PersistentKeepalive != 0 { + node.IngressPersistentKeepalive = ingress.PersistentKeepalive + } + node.IngressMTU = 1420 + if ingress.MTU != 0 { + node.IngressMTU = ingress.MTU + } if servercfg.IsPro { if _, exists := FailOverExists(node.Network); exists { ResetFailedOverPeer(&node) @@ -174,34 +180,14 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq if node.Metadata == "" { node.Metadata = "This host can be used for remote access" } + if node.Tags == nil { + node.Tags = make(map[models.TagID]struct{}) + } + node.Tags[models.TagID(fmt.Sprintf("%s.%s", netid, models.RemoteAccessTagName))] = struct{}{} err = UpsertNode(&node) if err != nil { return models.Node{}, err } - // create network role for this gateway - CreateRole(models.UserRolePermissionTemplate{ - ID: models.GetRAGRoleID(node.Network, host.ID.String()), - UiName: models.GetRAGRoleName(node.Network, host.Name), - NetworkID: models.NetworkID(node.Network), - Default: true, - NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{ - models.RemoteAccessGwRsrc: { - models.RsrcID(node.ID.String()): models.RsrcPermissionScope{ - Read: true, - VPNaccess: true, - }, - }, - models.ExtClientsRsrc: { - models.AllExtClientsRsrcID: models.RsrcPermissionScope{ - Read: true, - Create: true, - Update: true, - Delete: true, - SelfOnly: true, - }, - }, - }, - }) err = SetNetworkNodesLastModified(netid) return node, err } @@ -249,17 +235,14 @@ func DeleteIngressGateway(nodeid string) (models.Node, []models.ExtClient, error if !servercfg.IsPro { node.IsInternetGateway = false } + delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.RemoteAccessTagName))) node.IngressGatewayRange = "" node.Metadata = "" err = UpsertNode(&node) if err != nil { return models.Node{}, removedClients, err } - host, err := GetHost(node.HostID.String()) - if err != nil { - return models.Node{}, removedClients, err - } - go DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true) + err = SetNetworkNodesLastModified(node.Network) return node, removedClients, err } diff --git a/logic/hosts.go b/logic/hosts.go index 0fa8887e9..8fd125c52 100644 --- a/logic/hosts.go +++ b/logic/hosts.go @@ -10,6 +10,7 @@ import ( "github.com/google/uuid" "golang.org/x/crypto/bcrypt" + "golang.org/x/exp/slog" "github.com/gravitl/netmaker/database" "github.com/gravitl/netmaker/logger" @@ -254,14 +255,31 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool) currHost.WgPublicListenPort = newHost.WgPublicListenPort sendPeerUpdate = true } + isEndpointChanged := false if currHost.EndpointIP.String() != newHost.EndpointIP.String() { currHost.EndpointIP = newHost.EndpointIP sendPeerUpdate = true + isEndpointChanged = true } if currHost.EndpointIPv6.String() != newHost.EndpointIPv6.String() { currHost.EndpointIPv6 = newHost.EndpointIPv6 sendPeerUpdate = true + isEndpointChanged = true } + + if isEndpointChanged { + for _, nodeID := range currHost.Nodes { + node, err := GetNodeByID(nodeID) + if err != nil { + slog.Error("failed to get node:", "id", node.ID, "error", err) + continue + } + if node.FailedOverBy != uuid.Nil { + ResetFailedOverPeer(&node) + } + } + } + currHost.DaemonInstalled = newHost.DaemonInstalled currHost.Debug = newHost.Debug currHost.Verbosity = newHost.Verbosity @@ -269,19 +287,7 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool) currHost.IsStaticPort = newHost.IsStaticPort currHost.IsStatic = newHost.IsStatic currHost.MTU = newHost.MTU - if newHost.Name != currHost.Name { - // update any rag role ids - for _, nodeID := range newHost.Nodes { - node, err := GetNodeByID(nodeID) - if err == nil && node.IsIngressGateway { - role, err := GetRole(models.GetRAGRoleID(node.Network, currHost.ID.String())) - if err == nil { - role.UiName = models.GetRAGRoleName(node.Network, newHost.Name) - UpdateRole(role) - } - } - } - } + currHost.Name = newHost.Name if len(newHost.NatType) > 0 && newHost.NatType != currHost.NatType { currHost.NatType = newHost.NatType diff --git a/logic/jwts.go b/logic/jwts.go index 41181fcd6..a70bbbd2d 100644 --- a/logic/jwts.go +++ b/logic/jwts.go @@ -56,8 +56,9 @@ func CreateJWT(uuid string, macAddress string, network string) (response string, func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) { expirationTime := time.Now().Add(servercfg.GetServerConfig().JwtValidityDuration) claims := &models.UserClaims{ - UserName: username, - Role: role, + UserName: username, + Role: role, + RacAutoDisable: servercfg.GetRacAutoDisable() && (role != models.SuperAdminRole && role != models.AdminRole), RegisteredClaims: jwt.RegisteredClaims{ Issuer: "Netmaker", Subject: fmt.Sprintf("user|%s", username), diff --git a/logic/networks.go b/logic/networks.go index 14ad794e5..1617889d5 100644 --- a/logic/networks.go +++ b/logic/networks.go @@ -8,9 +8,11 @@ import ( "sort" "strings" "sync" + "time" "github.com/c-robinson/iplib" validator "github.com/go-playground/validator/v10" + "github.com/google/uuid" "github.com/gravitl/netmaker/database" "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic/acls/nodeacls" @@ -175,6 +177,17 @@ func DeleteNetwork(network string) error { if err != nil { logger.Log(1, "failed to remove the node acls during network delete for network,", network) } + // Delete default network enrollment key + keys, _ := GetAllEnrollmentKeys() + for _, key := range keys { + if key.Tags[0] == network { + if key.Default { + DeleteEnrollmentKey(key.Value, true) + break + } + + } + } nodeCount, err := GetNetworkNonServerNodeCount(network) if nodeCount == 0 || database.IsEmptyRecord(err) { // delete server nodes first then db records @@ -233,6 +246,17 @@ func CreateNetwork(network models.Network) (models.Network, error) { storeNetworkInCache(network.NetID, network) } + _, _ = CreateEnrollmentKey( + 0, + time.Time{}, + []string{network.NetID}, + []string{network.NetID}, + []models.TagID{}, + true, + uuid.Nil, + true, + ) + return network, nil } diff --git a/logic/nodes.go b/logic/nodes.go index 75ad16a82..34eebe2e4 100644 --- a/logic/nodes.go +++ b/logic/nodes.go @@ -196,10 +196,6 @@ func DeleteNode(node *models.Node, purge bool) error { if err := DeleteGatewayExtClients(node.ID.String(), node.Network); err != nil { slog.Error("failed to delete ext clients", "nodeid", node.ID.String(), "error", err.Error()) } - host, err := GetHost(node.HostID.String()) - if err == nil { - go DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true) - } } if node.IsRelayed { // cleanup node from relayednodes on relay node @@ -378,6 +374,20 @@ func GetAllNodes() ([]models.Node, error) { return nodes, nil } +func AddStaticNodestoList(nodes []models.Node) []models.Node { + netMap := make(map[string]struct{}) + for _, node := range nodes { + if _, ok := netMap[node.Network]; ok { + continue + } + if node.IsIngressGateway { + nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network), false)...) + netMap[node.Network] = struct{}{} + } + } + return nodes +} + // GetNetworkByNode - gets the network model from a node func GetNetworkByNode(node *models.Node) (models.Network, error) { @@ -393,7 +403,7 @@ func GetNetworkByNode(node *models.Node) (models.Network, error) { } // SetNodeDefaults - sets the defaults of a node to avoid empty fields -func SetNodeDefaults(node *models.Node) { +func SetNodeDefaults(node *models.Node, resetConnected bool) { parentNetwork, _ := GetNetworkByNode(node) _, cidr, err := net.ParseCIDR(parentNetwork.AddressRange) @@ -414,8 +424,14 @@ func SetNodeDefaults(node *models.Node) { node.SetLastModified() node.SetLastCheckIn() - node.SetDefaultConnected() + + if resetConnected { + node.SetDefaultConnected() + } node.SetExpirationDateTime() + if node.Tags == nil { + node.Tags = make(map[models.TagID]struct{}) + } } // GetRecordKey - get record key @@ -461,7 +477,7 @@ func GetDeletedNodeByID(uuid string) (models.Node, error) { return models.Node{}, err } - SetNodeDefaults(&node) + SetNodeDefaults(&node, true) return node, nil } @@ -531,7 +547,7 @@ func createNode(node *models.Node) error { } } - SetNodeDefaults(node) + SetNodeDefaults(node, true) defaultACLVal := acls.Allowed parentNetwork, err := GetNetwork(node.Network) @@ -694,3 +710,109 @@ func GetAllFailOvers() ([]models.Node, error) { } return igs, nil } + +func GetTagMapWithNodes() (tagNodesMap map[models.TagID][]models.Node) { + tagNodesMap = make(map[models.TagID][]models.Node) + nodes, _ := GetAllNodes() + for _, nodeI := range nodes { + if nodeI.Tags == nil { + continue + } + for nodeTagID := range nodeI.Tags { + tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI) + } + } + return +} + +func GetTagMapWithNodesByNetwork(netID models.NetworkID) (tagNodesMap map[models.TagID][]models.Node) { + tagNodesMap = make(map[models.TagID][]models.Node) + nodes, _ := GetNetworkNodes(netID.String()) + for _, nodeI := range nodes { + if nodeI.Tags == nil { + continue + } + for nodeTagID := range nodeI.Tags { + tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI) + } + } + return AddTagMapWithStaticNodes(netID, tagNodesMap) +} + +func AddTagMapWithStaticNodes(netID models.NetworkID, + tagNodesMap map[models.TagID][]models.Node) map[models.TagID][]models.Node { + extclients, err := GetNetworkExtClients(netID.String()) + if err != nil { + return tagNodesMap + } + for _, extclient := range extclients { + if extclient.Tags == nil || extclient.RemoteAccessClientID != "" { + continue + } + for tagID := range extclient.Tags { + tagNodesMap[tagID] = append(tagNodesMap[tagID], models.Node{ + IsStatic: true, + StaticNode: extclient, + }) + } + + } + return tagNodesMap +} + +func GetNodesWithTag(tagID models.TagID) map[string]models.Node { + nMap := make(map[string]models.Node) + tag, err := GetTag(tagID) + if err != nil { + return nMap + } + nodes, _ := GetNetworkNodes(tag.Network.String()) + for _, nodeI := range nodes { + if nodeI.Tags == nil { + continue + } + if _, ok := nodeI.Tags[tagID]; ok { + nMap[nodeI.ID.String()] = nodeI + } + } + return AddStaticNodesWithTag(tag, nMap) +} + +func AddStaticNodesWithTag(tag models.Tag, nMap map[string]models.Node) map[string]models.Node { + extclients, err := GetNetworkExtClients(tag.Network.String()) + if err != nil { + return nMap + } + for _, extclient := range extclients { + if extclient.RemoteAccessClientID != "" { + continue + } + if _, ok := extclient.Tags[tag.ID]; ok { + nMap[extclient.ClientID] = models.Node{ + IsStatic: true, + StaticNode: extclient, + } + } + + } + return nMap +} + +func GetStaticNodeWithTag(tagID models.TagID) map[string]models.Node { + nMap := make(map[string]models.Node) + tag, err := GetTag(tagID) + if err != nil { + return nMap + } + extclients, err := GetNetworkExtClients(tag.Network.String()) + if err != nil { + return nMap + } + for _, extclient := range extclients { + nMap[extclient.ClientID] = models.Node{ + IsStatic: true, + StaticNode: extclient, + } + } + return nMap +} diff --git a/logic/peers.go b/logic/peers.go index 77e76a2a9..e88f48630 100644 --- a/logic/peers.go +++ b/logic/peers.go @@ -74,7 +74,8 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N ServerVersion: servercfg.GetVersion(), ServerAddrs: []models.ServerAddr{}, FwUpdate: models.FwUpdate{ - EgressInfo: make(map[string]models.EgressInfo), + EgressInfo: make(map[string]models.EgressInfo), + IngressInfo: make(map[string]models.IngressInfo), }, PeerIDs: make(models.PeerMap, 0), Peers: []wgtypes.PeerConfig{}, @@ -182,7 +183,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N }) } if peer.IsIngressGateway { - hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, getExtpeersExtraRoutes(node, peer.Network)...) + hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, getExtpeersExtraRoutes(node)...) } _, isFailOverPeer := node.FailOverPeers[peer.ID.String()] if servercfg.IsPro { @@ -241,6 +242,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N !peer.PendingDelete && peer.Connected && nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) && + IsNodeAllowedToCommunicate(node, peer) && (deletedNode == nil || (deletedNode != nil && peer.ID.String() != deletedNode.ID.String())) { peerConfig.AllowedIPs = allowedips // only append allowed IPs if valid connection } @@ -287,8 +289,23 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N var extPeerIDAndAddrs []models.IDandAddr var egressRoutes []models.EgressNetworkRoutes if node.IsIngressGateway { + hostPeerUpdate.FwUpdate.IsIngressGw = true extPeers, extPeerIDAndAddrs, egressRoutes, err = GetExtPeers(&node, &node) if err == nil { + defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy) + defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy) + if !defaultDevicePolicy.Enabled || !defaultUserPolicy.Enabled { + ingFwUpdate := models.IngressInfo{ + IngressID: node.ID.String(), + Network: node.NetworkRange, + Network6: node.NetworkRange6, + AllowAll: defaultDevicePolicy.Enabled && defaultUserPolicy.Default, + StaticNodeIps: GetStaticNodeIps(node), + Rules: GetFwRulesOnIngressGateway(node), + } + ingFwUpdate.EgressRanges, ingFwUpdate.EgressRanges6 = getExtpeerEgressRanges(node) + hostPeerUpdate.FwUpdate.IngressInfo[node.ID.String()] = ingFwUpdate + } hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, egressRoutes...) hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, extPeers...) for _, extPeerIdAndAddr := range extPeerIDAndAddrs { @@ -391,6 +408,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N } } + hostPeerUpdate.ManageDNS = servercfg.GetManageDNS() return hostPeerUpdate, nil } @@ -425,6 +443,7 @@ func GetAllowedIPs(node, peer *models.Node, metrics *models.Metrics) []net.IPNet logger.Log(2, "could not retrieve ext peers for ", peer.ID.String(), err.Error()) } for _, extPeer := range extPeers { + allowedips = append(allowedips, extPeer.AllowedIPs...) } } diff --git a/logic/proc.go b/logic/proc.go new file mode 100644 index 000000000..fec258d75 --- /dev/null +++ b/logic/proc.go @@ -0,0 +1,24 @@ +package logic + +import ( + "os" + "runtime/pprof" + + "github.com/gravitl/netmaker/logger" +) + +func StartCPUProfiling() *os.File { + f, err := os.OpenFile("/root/data/cpu.prof", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755) + if err != nil { + logger.Log(0, "could not create CPU profile: ", err.Error()) + } + if err := pprof.StartCPUProfile(f); err != nil { + logger.Log(0, "could not start CPU profile: ", err.Error()) + } + return f +} + +func StopCPUProfiling(f *os.File) { + pprof.StopCPUProfile() + f.Close() +} diff --git a/logic/tags.go b/logic/tags.go new file mode 100644 index 000000000..7cdf0f324 --- /dev/null +++ b/logic/tags.go @@ -0,0 +1,290 @@ +package logic + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" + "sort" + "sync" + "time" + + "github.com/gravitl/netmaker/database" + "github.com/gravitl/netmaker/models" + "golang.org/x/exp/slog" +) + +var tagMutex = &sync.RWMutex{} + +// GetTag - fetches tag info +func GetTag(tagID models.TagID) (models.Tag, error) { + data, err := database.FetchRecord(database.TAG_TABLE_NAME, tagID.String()) + if err != nil { + return models.Tag{}, err + } + tag := models.Tag{} + err = json.Unmarshal([]byte(data), &tag) + if err != nil { + return tag, err + } + return tag, nil +} + +// InsertTag - creates new tag +func InsertTag(tag models.Tag) error { + tagMutex.Lock() + defer tagMutex.Unlock() + _, err := database.FetchRecord(database.TAG_TABLE_NAME, tag.ID.String()) + if err == nil { + return fmt.Errorf("tag `%s` exists already", tag.ID) + } + d, err := json.Marshal(tag) + if err != nil { + return err + } + return database.Insert(tag.ID.String(), string(d), database.TAG_TABLE_NAME) +} + +// DeleteTag - delete tag, will also untag hosts +func DeleteTag(tagID models.TagID, removeFromPolicy bool) error { + tagMutex.Lock() + defer tagMutex.Unlock() + // cleanUp tags on hosts + tag, err := GetTag(tagID) + if err != nil { + return err + } + nodes, err := GetNetworkNodes(tag.Network.String()) + if err != nil { + return err + } + for _, nodeI := range nodes { + nodeI := nodeI + if _, ok := nodeI.Tags[tagID]; ok { + delete(nodeI.Tags, tagID) + UpsertNode(&nodeI) + } + } + if removeFromPolicy { + // remove tag used on acl policy + go RemoveDeviceTagFromAclPolicies(tagID, tag.Network) + } + extclients, _ := GetNetworkExtClients(tag.Network.String()) + for _, extclient := range extclients { + if _, ok := extclient.Tags[tagID]; ok { + delete(extclient.Tags, tagID) + SaveExtClient(&extclient) + } + } + return database.DeleteRecord(database.TAG_TABLE_NAME, tagID.String()) +} + +// ListTagsWithHosts - lists all tags with tagged hosts +func ListTagsWithNodes(netID models.NetworkID) ([]models.TagListResp, error) { + tags, err := ListNetworkTags(netID) + if err != nil { + return []models.TagListResp{}, err + } + tagsNodeMap := GetTagMapWithNodesByNetwork(netID) + resp := []models.TagListResp{} + for _, tagI := range tags { + tagRespI := models.TagListResp{ + Tag: tagI, + UsedByCnt: len(tagsNodeMap[tagI.ID]), + TaggedNodes: GetAllNodesAPI(tagsNodeMap[tagI.ID]), + } + resp = append(resp, tagRespI) + } + return resp, nil +} + +// ListTags - lists all tags from DB +func ListTags() ([]models.Tag, error) { + tagMutex.RLock() + defer tagMutex.RUnlock() + data, err := database.FetchRecords(database.TAG_TABLE_NAME) + if err != nil && !database.IsEmptyRecord(err) { + return []models.Tag{}, err + } + tags := []models.Tag{} + for _, dataI := range data { + tag := models.Tag{} + err := json.Unmarshal([]byte(dataI), &tag) + if err != nil { + continue + } + tags = append(tags, tag) + } + return tags, nil +} + +// ListTags - lists all tags from DB +func ListNetworkTags(netID models.NetworkID) ([]models.Tag, error) { + tagMutex.RLock() + defer tagMutex.RUnlock() + data, err := database.FetchRecords(database.TAG_TABLE_NAME) + if err != nil && !database.IsEmptyRecord(err) { + return []models.Tag{}, err + } + tags := []models.Tag{} + for _, dataI := range data { + tag := models.Tag{} + err := json.Unmarshal([]byte(dataI), &tag) + if err != nil { + continue + } + if tag.Network == netID { + tags = append(tags, tag) + } + + } + return tags, nil +} + +// UpdateTag - updates and syncs hosts with tag update +func UpdateTag(req models.UpdateTagReq, newID models.TagID) { + tagMutex.Lock() + defer tagMutex.Unlock() + var err error + tagNodesMap := GetNodesWithTag(req.ID) + for _, apiNode := range req.TaggedNodes { + node := models.Node{} + var nodeID string + if apiNode.IsStatic { + if apiNode.StaticNode.RemoteAccessClientID != "" { + continue + } + extclient, err := GetExtClient(apiNode.StaticNode.ClientID, apiNode.StaticNode.Network) + if err != nil { + continue + } + node.IsStatic = true + nodeID = extclient.ClientID + node.StaticNode = extclient + } else { + node, err = GetNodeByID(apiNode.ID) + if err != nil { + continue + } + nodeID = node.ID.String() + } + + if _, ok := tagNodesMap[nodeID]; !ok { + if node.StaticNode.Tags == nil { + node.StaticNode.Tags = make(map[models.TagID]struct{}) + } + if node.Tags == nil { + node.Tags = make(map[models.TagID]struct{}) + } + if newID != "" { + if node.IsStatic { + node.StaticNode.Tags[newID] = struct{}{} + SaveExtClient(&node.StaticNode) + } else { + node.Tags[newID] = struct{}{} + UpsertNode(&node) + } + + } else { + if node.IsStatic { + node.StaticNode.Tags[req.ID] = struct{}{} + SaveExtClient(&node.StaticNode) + } else { + node.Tags[req.ID] = struct{}{} + UpsertNode(&node) + } + } + } else { + if newID != "" { + delete(node.Tags, req.ID) + delete(node.StaticNode.Tags, req.ID) + if node.IsStatic { + node.StaticNode.Tags[newID] = struct{}{} + SaveExtClient(&node.StaticNode) + } else { + node.Tags[newID] = struct{}{} + UpsertNode(&node) + } + } + delete(tagNodesMap, nodeID) + } + + } + for _, deletedTaggedNode := range tagNodesMap { + delete(deletedTaggedNode.Tags, req.ID) + delete(deletedTaggedNode.StaticNode.Tags, req.ID) + if deletedTaggedNode.IsStatic { + SaveExtClient(&deletedTaggedNode.StaticNode) + } else { + UpsertNode(&deletedTaggedNode) + } + } + go func(req models.UpdateTagReq) { + if newID != "" { + tagNodesMap = GetNodesWithTag(req.ID) + for _, nodeI := range tagNodesMap { + nodeI := nodeI + if nodeI.StaticNode.Tags == nil { + nodeI.StaticNode.Tags = make(map[models.TagID]struct{}) + } + if nodeI.Tags == nil { + nodeI.Tags = make(map[models.TagID]struct{}) + } + delete(nodeI.Tags, req.ID) + delete(nodeI.StaticNode.Tags, req.ID) + nodeI.Tags[newID] = struct{}{} + nodeI.StaticNode.Tags[newID] = struct{}{} + if nodeI.IsStatic { + SaveExtClient(&nodeI.StaticNode) + } else { + UpsertNode(&nodeI) + } + } + } + }(req) + +} + +// SortTagEntrys - Sorts slice of Tag entries by their id +func SortTagEntrys(tags []models.TagListResp) { + sort.Slice(tags, func(i, j int) bool { + return tags[i].ID < tags[j].ID + }) +} + +func CheckIDSyntax(id string) error { + if id == "" { + return errors.New("name is required") + } + if len(id) < 3 { + return errors.New("name should have min 3 characters") + } + reg, err := regexp.Compile("^[a-zA-Z-]+$") + if err != nil { + return err + } + if !reg.MatchString(id) { + return errors.New("invalid name. allowed characters are [a-zA-Z-]") + } + return nil +} + +func CreateDefaultTags(netID models.NetworkID) { + // create tag for remote access gws in the network + tag := models.Tag{ + ID: models.TagID(fmt.Sprintf("%s.%s", netID.String(), models.RemoteAccessTagName)), + TagName: models.RemoteAccessTagName, + Network: netID, + CreatedBy: "auto", + CreatedAt: time.Now(), + } + _, err := GetTag(tag.ID) + if err == nil { + return + } + err = InsertTag(tag) + if err != nil { + slog.Error("failed to create remote access gw tag", "error", err.Error()) + return + } +} diff --git a/logic/user_mgmt.go b/logic/user_mgmt.go index 8727cb57a..56395c78c 100644 --- a/logic/user_mgmt.go +++ b/logic/user_mgmt.go @@ -39,17 +39,28 @@ var FilterNetworksByRole = func(allnetworks []models.Network, user models.User) var IsGroupsValid = func(groups map[models.UserGroupID]struct{}) error { return nil } +var IsGroupValid = func(groupID models.UserGroupID) error { + return nil +} var IsNetworkRolesValid = func(networkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) error { return nil } +var MigrateUserRoleAndGroups = func(u models.User) { + +} + var UpdateUserGwAccess = func(currentUser, changeUser models.User) {} var UpdateRole = func(r models.UserRolePermissionTemplate) error { return nil } var InitialiseRoles = userRolesInit +var IntialiseGroups = func() {} var DeleteNetworkRoles = func(netID string) {} var CreateDefaultNetworkRolesAndGroups = func(netID models.NetworkID) {} +var CreateDefaultUserPolicies = func(netID models.NetworkID) {} +var GetUserGroupsInNetwork = func(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { return } +var AddGlobalNetRolesToAdmins = func(u models.User) {} // GetRole - fetches role template by id func GetRole(roleID models.UserRoleID) (models.UserRolePermissionTemplate, error) { diff --git a/main.go b/main.go index 027b51c72..10bb52b8b 100644 --- a/main.go +++ b/main.go @@ -27,10 +27,10 @@ import ( "golang.org/x/exp/slog" ) -var version = "v0.25.0" +var version = "v0.26.0" // @title NetMaker -// @version 0.24.3 +// @version 0.26.0 // @description NetMaker API Docs // @tag.name APIUsage // @tag.description.markdown @@ -103,6 +103,7 @@ func initialize() { // Client Mode Prereq Check logic.SetJWTSecret() logic.InitialiseRoles() + logic.IntialiseGroups() err = serverctl.SetDefaults() if err != nil { logger.FatalLog("error setting defaults: ", err.Error()) diff --git a/migrate/migrate.go b/migrate/migrate.go index 6612d4ddc..51e74ab4c 100644 --- a/migrate/migrate.go +++ b/migrate/migrate.go @@ -8,6 +8,7 @@ import ( "golang.org/x/exp/slog" + "github.com/google/uuid" "github.com/gravitl/netmaker/database" "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" @@ -21,6 +22,7 @@ import ( func Run() { updateEnrollmentKeys() assignSuperAdmin() + createDefaultTagsAndPolicies() removeOldUserGrps() syncUsers() updateHosts() @@ -122,6 +124,35 @@ func updateEnrollmentKeys() { } } + + existingKeys, err := logic.GetAllEnrollmentKeys() + if err != nil { + return + } + // check if any tags are duplicate + existingTags := make(map[string]struct{}) + for _, existingKey := range existingKeys { + for _, t := range existingKey.Tags { + existingTags[t] = struct{}{} + } + } + networks, _ := logic.GetNetworks() + for _, network := range networks { + if _, ok := existingTags[network.NetID]; ok { + continue + } + _, _ = logic.CreateEnrollmentKey( + 0, + time.Time{}, + []string{network.NetID}, + []string{network.NetID}, + []models.TagID{}, + true, + uuid.Nil, + true, + ) + + } } func removeOldUserGrps() { @@ -166,6 +197,26 @@ func updateNodes() { return } for _, node := range nodes { + node := node + if node.Tags == nil { + node.Tags = make(map[models.TagID]struct{}) + logic.UpsertNode(&node) + } + if node.IsIngressGateway { + tagID := models.TagID(fmt.Sprintf("%s.%s", node.Network, + models.RemoteAccessTagName)) + if node.Tags == nil { + node.Tags = make(map[models.TagID]struct{}) + } + if _, ok := node.Tags[tagID]; !ok { + node.Tags[tagID] = struct{}{} + logic.UpsertNode(&node) + } + host, err := logic.GetHost(node.HostID.String()) + if err == nil { + go logic.DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true) + } + } if node.IsEgressGateway { egressRanges, update := removeInterGw(node.EgressGatewayRanges) if update { @@ -175,6 +226,18 @@ func updateNodes() { } } } + extclients, _ := logic.GetAllExtClients() + for _, extclient := range extclients { + tagID := models.TagID(fmt.Sprintf("%s.%s", extclient.Network, + models.RemoteAccessTagName)) + if extclient.Tags == nil { + extclient.Tags = make(map[models.TagID]struct{}) + } + if _, ok := extclient.Tags[tagID]; !ok { + extclient.Tags[tagID] = struct{}{} + logic.SaveExtClient(&extclient) + } + } } func removeInterGw(egressRanges []string) ([]string, bool) { @@ -330,42 +393,8 @@ func syncUsers() { // create default network user roles for existing networks if servercfg.IsPro { networks, _ := logic.GetNetworks() - nodes, err := logic.GetAllNodes() - if err == nil { - for _, netI := range networks { - logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(netI.NetID)) - networkNodes := logic.GetNetworkNodesMemory(nodes, netI.NetID) - for _, networkNodeI := range networkNodes { - if networkNodeI.IsIngressGateway { - h, err := logic.GetHost(networkNodeI.HostID.String()) - if err == nil { - logic.CreateRole(models.UserRolePermissionTemplate{ - ID: models.GetRAGRoleID(networkNodeI.Network, h.ID.String()), - UiName: models.GetRAGRoleName(networkNodeI.Network, h.Name), - NetworkID: models.NetworkID(netI.NetID), - NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{ - models.RemoteAccessGwRsrc: { - models.RsrcID(networkNodeI.ID.String()): models.RsrcPermissionScope{ - Read: true, - VPNaccess: true, - }, - }, - models.ExtClientsRsrc: { - models.AllExtClientsRsrcID: models.RsrcPermissionScope{ - Read: true, - Create: true, - Update: true, - Delete: true, - SelfOnly: true, - }, - }, - }, - }) - } - - } - } - } + for _, netI := range networks { + logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(netI.NetID)) } } @@ -382,6 +411,8 @@ func syncUsers() { logic.UpsertUser(user) } if user.PlatformRoleID.String() != "" { + logic.MigrateUserRoleAndGroups(user) + logic.AddGlobalNetRolesToAdmins(user) continue } user.AuthType = models.BasicAuth @@ -403,32 +434,20 @@ func syncUsers() { user.PlatformRoleID = models.ServiceUser } logic.UpsertUser(user) - if len(user.RemoteGwIDs) > 0 { - // define user roles for network - // assign relevant network role to user - for remoteGwID := range user.RemoteGwIDs { - gwNode, err := logic.GetNodeByID(remoteGwID) - if err != nil { - continue - } - h, err := logic.GetHost(gwNode.HostID.String()) - if err != nil { - continue - } - r, err := logic.GetRole(models.GetRAGRoleID(gwNode.Network, h.ID.String())) - if err != nil { - continue - } - if netRoles, ok := user.NetworkRoles[models.NetworkID(gwNode.Network)]; ok { - netRoles[r.ID] = struct{}{} - } else { - user.NetworkRoles[models.NetworkID(gwNode.Network)] = map[models.UserRoleID]struct{}{ - r.ID: {}, - } - } - } - logic.UpsertUser(user) - } + logic.AddGlobalNetRolesToAdmins(user) + logic.MigrateUserRoleAndGroups(user) } } + +} + +func createDefaultTagsAndPolicies() { + networks, err := logic.GetNetworks() + if err != nil { + return + } + for _, network := range networks { + logic.CreateDefaultTags(models.NetworkID(network.NetID)) + logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID)) + } } diff --git a/models/acl.go b/models/acl.go new file mode 100644 index 000000000..d8c302ca9 --- /dev/null +++ b/models/acl.go @@ -0,0 +1,72 @@ +package models + +import ( + "time" +) + +// AllowedTrafficDirection - allowed direction of traffic +type AllowedTrafficDirection int + +const ( + // TrafficDirectionUni implies traffic is only allowed in one direction (src --> dst) + TrafficDirectionUni AllowedTrafficDirection = iota + // TrafficDirectionBi implies traffic is allowed both direction (src <--> dst ) + TrafficDirectionBi +) + +type AclPolicyType string + +const ( + UserPolicy AclPolicyType = "user-policy" + DevicePolicy AclPolicyType = "device-policy" +) + +type AclPolicyTag struct { + ID AclGroupType `json:"id"` + Value string `json:"value"` +} + +type AclGroupType string + +const ( + UserAclID AclGroupType = "user" + UserGroupAclID AclGroupType = "user-group" + DeviceAclID AclGroupType = "tag" + NetmakerIPAclID AclGroupType = "ip" + NetmakerSubNetRangeAClID AclGroupType = "ipset" +) + +func (g AclGroupType) String() string { + return string(g) +} + +type UpdateAclRequest struct { + Acl + NewName string `json:"new_name"` +} + +type AclPolicy struct { + TypeID AclPolicyType + PrefixTagUser AclGroupType +} + +type Acl struct { + ID string `json:"id"` + Default bool `json:"default"` + MetaData string `json:"meta_data"` + Name string `json:"name"` + NetworkID NetworkID `json:"network_id"` + RuleType AclPolicyType `json:"policy_type"` + Src []AclPolicyTag `json:"src_type"` + Dst []AclPolicyTag `json:"dst_type"` + AllowedDirection AllowedTrafficDirection `json:"allowed_traffic_direction"` + Enabled bool `json:"enabled"` + CreatedBy string `json:"created_by"` + CreatedAt time.Time `json:"created_at"` +} + +type AclPolicyTypes struct { + RuleTypes []AclPolicyType `json:"policy_types"` + SrcGroupTypes []AclGroupType `json:"src_grp_types"` + DstGroupTypes []AclGroupType `json:"dst_grp_types"` +} diff --git a/models/api_node.go b/models/api_node.go index e7005f327..30e08c639 100644 --- a/models/api_node.go +++ b/models/api_node.go @@ -10,33 +10,35 @@ import ( // ApiNode is a stripped down Node DTO that exposes only required fields to external systems type ApiNode struct { - ID string `json:"id,omitempty" validate:"required,min=5,id_unique"` - HostID string `json:"hostid,omitempty" validate:"required,min=5,id_unique"` - Address string `json:"address" validate:"omitempty,cidrv4"` - Address6 string `json:"address6" validate:"omitempty,cidrv6"` - LocalAddress string `json:"localaddress" validate:"omitempty,cidr"` - AllowedIPs []string `json:"allowedips"` - LastModified int64 `json:"lastmodified"` - ExpirationDateTime int64 `json:"expdatetime"` - LastCheckIn int64 `json:"lastcheckin"` - LastPeerUpdate int64 `json:"lastpeerupdate"` - Network string `json:"network"` - NetworkRange string `json:"networkrange"` - NetworkRange6 string `json:"networkrange6"` - IsRelayed bool `json:"isrelayed"` - IsRelay bool `json:"isrelay"` - RelayedBy string `json:"relayedby" bson:"relayedby" yaml:"relayedby"` - RelayedNodes []string `json:"relaynodes" yaml:"relayedNodes"` - IsEgressGateway bool `json:"isegressgateway"` - IsIngressGateway bool `json:"isingressgateway"` - EgressGatewayRanges []string `json:"egressgatewayranges"` - EgressGatewayNatEnabled bool `json:"egressgatewaynatenabled"` - DNSOn bool `json:"dnson"` - IngressDns string `json:"ingressdns"` - Server string `json:"server"` - Connected bool `json:"connected"` - PendingDelete bool `json:"pendingdelete"` - Metadata string `json:"metadata"` + ID string `json:"id,omitempty" validate:"required,min=5,id_unique"` + HostID string `json:"hostid,omitempty" validate:"required,min=5,id_unique"` + Address string `json:"address" validate:"omitempty,cidrv4"` + Address6 string `json:"address6" validate:"omitempty,cidrv6"` + LocalAddress string `json:"localaddress" validate:"omitempty,cidr"` + AllowedIPs []string `json:"allowedips"` + LastModified int64 `json:"lastmodified"` + ExpirationDateTime int64 `json:"expdatetime"` + LastCheckIn int64 `json:"lastcheckin"` + LastPeerUpdate int64 `json:"lastpeerupdate"` + Network string `json:"network"` + NetworkRange string `json:"networkrange"` + NetworkRange6 string `json:"networkrange6"` + IsRelayed bool `json:"isrelayed"` + IsRelay bool `json:"isrelay"` + RelayedBy string `json:"relayedby" bson:"relayedby" yaml:"relayedby"` + RelayedNodes []string `json:"relaynodes" yaml:"relayedNodes"` + IsEgressGateway bool `json:"isegressgateway"` + IsIngressGateway bool `json:"isingressgateway"` + EgressGatewayRanges []string `json:"egressgatewayranges"` + EgressGatewayNatEnabled bool `json:"egressgatewaynatenabled"` + DNSOn bool `json:"dnson"` + IngressDns string `json:"ingressdns"` + IngressPersistentKeepalive int32 `json:"ingresspersistentkeepalive"` + IngressMTU int32 `json:"ingressmtu"` + Server string `json:"server"` + Connected bool `json:"connected"` + PendingDelete bool `json:"pendingdelete"` + Metadata string `json:"metadata"` // == PRO == DefaultACL string `json:"defaultacl,omitempty" validate:"checkyesornoorunset"` IsFailOver bool `json:"is_fail_over"` @@ -46,6 +48,10 @@ type ApiNode struct { InetNodeReq InetNodeReq `json:"inet_node_req" yaml:"inet_node_req"` InternetGwID string `json:"internetgw_node_id" yaml:"internetgw_node_id"` AdditionalRagIps []string `json:"additional_rag_ips" yaml:"additional_rag_ips"` + Tags map[TagID]struct{} `json:"tags" yaml:"tags"` + IsStatic bool `json:"is_static"` + IsUserNode bool `json:"is_user_node"` + StaticNode ExtClient `json:"static_node"` } // ApiNode.ConvertToServerNode - converts an api node to a server node @@ -72,6 +78,8 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node { convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6 convertedNode.DNSOn = a.DNSOn convertedNode.IngressDNS = a.IngressDns + convertedNode.IngressPersistentKeepalive = a.IngressPersistentKeepalive + convertedNode.IngressMTU = a.IngressMTU convertedNode.IsInternetGateway = a.IsInternetGateway convertedNode.EgressGatewayRequest = currentNode.EgressGatewayRequest convertedNode.EgressGatewayNatEnabled = currentNode.EgressGatewayNatEnabled @@ -119,6 +127,7 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node { } convertedNode.AdditionalRagIps = append(convertedNode.AdditionalRagIps, ragIp) } + convertedNode.Tags = a.Tags return &convertedNode } @@ -162,6 +171,8 @@ func (nm *Node) ConvertToAPINode() *ApiNode { apiNode.EgressGatewayNatEnabled = nm.EgressGatewayNatEnabled apiNode.DNSOn = nm.DNSOn apiNode.IngressDns = nm.IngressDNS + apiNode.IngressPersistentKeepalive = nm.IngressPersistentKeepalive + apiNode.IngressMTU = nm.IngressMTU apiNode.Server = nm.Server apiNode.Connected = nm.Connected apiNode.PendingDelete = nm.PendingDelete @@ -174,9 +185,13 @@ func (nm *Node) ConvertToAPINode() *ApiNode { apiNode.FailedOverBy = nm.FailedOverBy apiNode.Metadata = nm.Metadata apiNode.AdditionalRagIps = []string{} + apiNode.Tags = nm.Tags for _, ip := range nm.AdditionalRagIps { apiNode.AdditionalRagIps = append(apiNode.AdditionalRagIps, ip.String()) } + apiNode.IsStatic = nm.IsStatic + apiNode.IsUserNode = nm.IsUserNode + apiNode.StaticNode = nm.StaticNode return &apiNode } diff --git a/models/dnsEntry.go b/models/dnsEntry.go index 11e9dd6b4..596d92258 100644 --- a/models/dnsEntry.go +++ b/models/dnsEntry.go @@ -42,8 +42,8 @@ type DNSUpdate struct { // DNSEntry - a DNS entry represented as struct type DNSEntry struct { - Address string `json:"address" validate:"ip"` - Address6 string `json:"address6"` + Address string `json:"address" validate:"omitempty,ip"` + Address6 string `json:"address6" validate:"omitempty,ip"` Name string `json:"name" validate:"required,name_unique,min=1,max=192,whitespace"` Network string `json:"network" validate:"network_exists"` } diff --git a/models/enrollment_key.go b/models/enrollment_key.go index e775344df..f133d7558 100644 --- a/models/enrollment_key.go +++ b/models/enrollment_key.go @@ -52,6 +52,8 @@ type EnrollmentKey struct { Token string `json:"token,omitempty"` // B64 value of EnrollmentToken Type KeyType `json:"type"` Relay uuid.UUID `json:"relay"` + Groups []TagID `json:"groups"` + Default bool `json:"default"` } // APIEnrollmentKey - used to create enrollment keys via API @@ -63,6 +65,7 @@ type APIEnrollmentKey struct { Tags []string `json:"tags" validate:"required,dive,min=3,max=32"` Type KeyType `json:"type"` Relay string `json:"relay"` + Groups []TagID `json:"groups"` } // RegisterResponse - the response to a successful enrollment register diff --git a/models/extclient.go b/models/extclient.go index 9d67207d3..a6214c34a 100644 --- a/models/extclient.go +++ b/models/extclient.go @@ -20,6 +20,7 @@ type ExtClient struct { RemoteAccessClientID string `json:"remote_access_client_id"` // unique ID (MAC address) of RAC machine PostUp string `json:"postup" bson:"postup"` PostDown string `json:"postdown" bson:"postdown"` + Tags map[TagID]struct{} `json:"tags"` } // CustomExtClient - struct for CustomExtClient params @@ -33,4 +34,17 @@ type CustomExtClient struct { RemoteAccessClientID string `json:"remote_access_client_id"` // unique ID (MAC address) of RAC machine PostUp string `json:"postup" bson:"postup" validate:"max=1024"` PostDown string `json:"postdown" bson:"postdown" validate:"max=1024"` + Tags map[TagID]struct{} `json:"tags"` +} + +func (ext *ExtClient) ConvertToStaticNode() Node { + + return Node{ + CommonNode: CommonNode{ + Network: ext.Network, + }, + Tags: ext.Tags, + IsStatic: true, + StaticNode: *ext, + } } diff --git a/models/metrics.go b/models/metrics.go index 686e3e969..459c7f17c 100644 --- a/models/metrics.go +++ b/models/metrics.go @@ -14,15 +14,17 @@ type Metrics struct { // Metric - holds a metric for data between nodes type Metric struct { - NodeName string `json:"node_name" bson:"node_name" yaml:"node_name"` - Uptime int64 `json:"uptime" bson:"uptime" yaml:"uptime"` - TotalTime int64 `json:"totaltime" bson:"totaltime" yaml:"totaltime"` - Latency int64 `json:"latency" bson:"latency" yaml:"latency"` - TotalReceived int64 `json:"totalreceived" bson:"totalreceived" yaml:"totalreceived"` - TotalSent int64 `json:"totalsent" bson:"totalsent" yaml:"totalsent"` - ActualUptime time.Duration `json:"actualuptime" bson:"actualuptime" yaml:"actualuptime"` - PercentUp float64 `json:"percentup" bson:"percentup" yaml:"percentup"` - Connected bool `json:"connected" bson:"connected" yaml:"connected"` + NodeName string `json:"node_name" bson:"node_name" yaml:"node_name"` + Uptime int64 `json:"uptime" bson:"uptime" yaml:"uptime"` + TotalTime int64 `json:"totaltime" bson:"totaltime" yaml:"totaltime"` + Latency int64 `json:"latency" bson:"latency" yaml:"latency"` + TotalReceived int64 `json:"totalreceived" bson:"totalreceived" yaml:"totalreceived"` + LastTotalReceived int64 `json:"lasttotalreceived" bson:"lasttotalreceived" yaml:"lasttotalreceived"` + TotalSent int64 `json:"totalsent" bson:"totalsent" yaml:"totalsent"` + LastTotalSent int64 `json:"lasttotalsent" bson:"lasttotalsent" yaml:"lasttotalsent"` + ActualUptime time.Duration `json:"actualuptime" bson:"actualuptime" yaml:"actualuptime"` + PercentUp float64 `json:"percentup" bson:"percentup" yaml:"percentup"` + Connected bool `json:"connected" bson:"connected" yaml:"connected"` } // IDandAddr - struct to hold ID and primary Address diff --git a/models/mqtt.go b/models/mqtt.go index c0d52d9c2..f0048a014 100644 --- a/models/mqtt.go +++ b/models/mqtt.go @@ -24,12 +24,25 @@ type HostPeerUpdate struct { FwUpdate FwUpdate `json:"fw_update"` ReplacePeers bool `json:"replace_peers"` EndpointDetection bool `json:"endpoint_detection"` + ManageDNS bool `yaml:"manage_dns"` +} + +type FwRule struct { + SrcIP net.IPNet + DstIP net.IPNet + Allow bool } // IngressInfo - struct for ingress info type IngressInfo struct { - ExtPeers map[string]ExtClientInfo `json:"ext_peers" yaml:"ext_peers"` - EgressRanges []string `json:"egress_ranges" yaml:"egress_ranges"` + IngressID string `json:"ingress_id"` + Network net.IPNet `json:"network"` + Network6 net.IPNet `json:"network6"` + StaticNodeIps []net.IP `json:"static_node_ips"` + Rules []FwRule `json:"rules"` + AllowAll bool `json:"allow_all"` + EgressRanges []net.IPNet `json:"egress_ranges"` + EgressRanges6 []net.IPNet `json:"egress_ranges6"` } // EgressInfo - struct for egress info @@ -77,8 +90,10 @@ type KeyUpdate struct { // FwUpdate - struct for firewall updates type FwUpdate struct { - IsEgressGw bool `json:"is_egress_gw"` - EgressInfo map[string]EgressInfo `json:"egress_info"` + IsEgressGw bool `json:"is_egress_gw"` + IsIngressGw bool `json:"is_ingress_gw"` + EgressInfo map[string]EgressInfo `json:"egress_info"` + IngressInfo map[string]IngressInfo `json:"ingress_info"` } // FailOverMeReq - struct for failover req diff --git a/models/names.go b/models/names.go index f5c1301fe..75f6a16c6 100644 --- a/models/names.go +++ b/models/names.go @@ -1,242 +1,18 @@ package models import ( - "math/rand" "time" -) - -// NAMES - list of names 4-7 chars in length -var NAMES = []string{ - "logic", - "warrant", - "iconic", - "threat", - "strike", - "boy", - "vital", - "unity", - "audio", - "schemer", - "depth", - "gravitl", - "mystic", - "donkey", - "atomic", - "turtle", - "monkey", - "rabbit", - "static", - "mosaic", - "elite", - "stonks", - "doggy", - "python", - "mohawk", - "arctic", - "rival", - "vibes", - "delay", - "bridge", - "weeble", - "combat", - "animal", - "wobble", - "rubble", - "bucket", - "proof", - "worker", - "beetle", - "racket", - "equal", - "panda", - "antics", - "strong", - "forum", - "koala", - "anchor", - "ornery", - "indigo", - "schism", - "dragon", - "knight", - "bishop", - "laser", - "rhino", - "clutch", - "shark", - "leader", - "young", - "robot", - "squish", - "chimp", - "rocket", - "space", - "queen", - "royalty", - "flush", - "earth", - "planet", - "heart", - "droplet", - "dillon", - "saturn", - "pluto", - "school", - "alien", - "matte", - "dingo", - "meercat", - "cookie", - "snack", - "goose", - "pepper", - "melissa", - "alex", - "elon", - "yeet", - "meh", - "walrus", - "avatar", - "chicken", - "proton", - "mohawk", - "tattoo", - "zebra", - "star", - "butter", - "tango", - "homie", - "rambo", - "cosmo", - "bubbles", - "hulk", - "pluto", - "scooby", - "thanos", - "yoda", - "draco", - "goofy", - "ditto", - "puff", - "duck", - "mouse", - "akita", - "water", - "hound", - "baby", - "spider", - "squid", - "roach", - "crab", - "cougar", - "cyborg", - "android", - "being", - "ninja", - "unicorn", - "zombie", - "warrior", - "zamboni", - "life", - "marine", - "node", - "mother", - "father", - "tesla", -} -// SMALL_NAMES - list of small (4 char or less) names -var SMALL_NAMES = []string{ - "ace", - "odd", - "hot", - "ill", - "root", - "sudo", - "moon", - "beef", - "bro", - "dank", - "red", - "gold", - "big", - "old", - "og", - "best", - "blue", - "lil", - "mom", - "bot", - "evil", - "good", - "holy", - "rad", - "bad", - "sad", - "mad", - "chad", - "pre", - "post", - "foot", - "soft", - "hard", - "lite", - "dark", - "true", - "toy", - "soy", - "rude", - "nice", - "fun", - "fat", - "pro", - "sly", - "tan", - "pet", - "fine", - "main", - "last", - "wide", - "free", - "open", - "poor", - "rich", - "next", - "real", - "long", - "huge", - "wild", - "sick", - "weak", - "firm", - "pink", - "okay", - "dull", - "loud", - "lazy", - "dumb", - "tidy", - "idle", - "bony", - "cute", - "oily", - "lame", - "mega", - "limp", - "wavy", - "edgy", - "nosy", - "zany", - "base", - "cold", -} + "github.com/goombaio/namegenerator" +) var logoString = retrieveLogo() // GenerateNodeName - generates a random node name func GenerateNodeName() string { - rand.Seed(time.Now().UnixNano()) - return SMALL_NAMES[rand.Intn(len(SMALL_NAMES))] + "-" + NAMES[seededRand.Intn(len(NAMES))] + seed := time.Now().UTC().UnixNano() + nameGenerator := namegenerator.NewNameGenerator(seed) + return nameGenerator.Generate() } // RetrieveLogo - retrieves the ascii art logo for Netmaker diff --git a/models/network.go b/models/network.go index c29b45d74..32d95f865 100644 --- a/models/network.go +++ b/models/network.go @@ -97,3 +97,8 @@ func (network *Network) GetNetworkNetworkCIDR6() *net.IPNet { _, netCidr, _ := net.ParseCIDR(network.AddressRange6) return netCidr } + +type NetworkStatResp struct { + Network + Hosts int `json:"hosts"` +} diff --git a/models/node.go b/models/node.go index a15bc6c92..0ca699dbb 100644 --- a/models/node.go +++ b/models/node.go @@ -77,16 +77,18 @@ type CommonNode struct { // Node - a model of a network node type Node struct { CommonNode - PendingDelete bool `json:"pendingdelete" bson:"pendingdelete" yaml:"pendingdelete"` - LastModified time.Time `json:"lastmodified" bson:"lastmodified" yaml:"lastmodified"` - LastCheckIn time.Time `json:"lastcheckin" bson:"lastcheckin" yaml:"lastcheckin"` - LastPeerUpdate time.Time `json:"lastpeerupdate" bson:"lastpeerupdate" yaml:"lastpeerupdate"` - ExpirationDateTime time.Time `json:"expdatetime" bson:"expdatetime" yaml:"expdatetime"` - EgressGatewayNatEnabled bool `json:"egressgatewaynatenabled" bson:"egressgatewaynatenabled" yaml:"egressgatewaynatenabled"` - EgressGatewayRequest EgressGatewayRequest `json:"egressgatewayrequest" bson:"egressgatewayrequest" yaml:"egressgatewayrequest"` - IngressGatewayRange string `json:"ingressgatewayrange" bson:"ingressgatewayrange" yaml:"ingressgatewayrange"` - IngressGatewayRange6 string `json:"ingressgatewayrange6" bson:"ingressgatewayrange6" yaml:"ingressgatewayrange6"` - Metadata string `json:"metadata"` + PendingDelete bool `json:"pendingdelete" bson:"pendingdelete" yaml:"pendingdelete"` + LastModified time.Time `json:"lastmodified" bson:"lastmodified" yaml:"lastmodified"` + LastCheckIn time.Time `json:"lastcheckin" bson:"lastcheckin" yaml:"lastcheckin"` + LastPeerUpdate time.Time `json:"lastpeerupdate" bson:"lastpeerupdate" yaml:"lastpeerupdate"` + ExpirationDateTime time.Time `json:"expdatetime" bson:"expdatetime" yaml:"expdatetime"` + EgressGatewayNatEnabled bool `json:"egressgatewaynatenabled" bson:"egressgatewaynatenabled" yaml:"egressgatewaynatenabled"` + EgressGatewayRequest EgressGatewayRequest `json:"egressgatewayrequest" bson:"egressgatewayrequest" yaml:"egressgatewayrequest"` + IngressGatewayRange string `json:"ingressgatewayrange" bson:"ingressgatewayrange" yaml:"ingressgatewayrange"` + IngressGatewayRange6 string `json:"ingressgatewayrange6" bson:"ingressgatewayrange6" yaml:"ingressgatewayrange6"` + IngressPersistentKeepalive int32 `json:"ingresspersistentkeepalive" bson:"ingresspersistentkeepalive" yaml:"ingresspersistentkeepalive"` + IngressMTU int32 `json:"ingressmtu" bson:"ingressmtu" yaml:"ingressmtu"` + Metadata string `json:"metadata"` // == PRO == DefaultACL string `json:"defaultacl,omitempty" bson:"defaultacl,omitempty" yaml:"defaultacl,omitempty" validate:"checkyesornoorunset"` OwnerID string `json:"ownerid,omitempty" bson:"ownerid,omitempty" yaml:"ownerid,omitempty"` @@ -97,6 +99,10 @@ type Node struct { InetNodeReq InetNodeReq `json:"inet_node_req" yaml:"inet_node_req"` InternetGwID string `json:"internetgw_node_id" yaml:"internetgw_node_id"` AdditionalRagIps []net.IP `json:"additional_rag_ips" yaml:"additional_rag_ips" swaggertype:"array,number"` + Tags map[TagID]struct{} `json:"tags" yaml:"tags"` + IsStatic bool `json:"is_static"` + IsUserNode bool `json:"is_user_node"` + StaticNode ExtClient `json:"static_node"` } // LegacyNode - legacy struct for node model diff --git a/models/structs.go b/models/structs.go index decb65572..2a723adf9 100644 --- a/models/structs.go +++ b/models/structs.go @@ -45,6 +45,16 @@ type UserRemoteGws struct { NetworkAddresses []string `json:"network_addresses"` } +// UserRAGs - struct for user access gws +type UserRAGs struct { + GwID string `json:"remote_access_gw_id"` + GWName string `json:"gw_name"` + Network string `json:"network"` + Connected bool `json:"connected"` + IsInternetGateway bool `json:"is_internet_gateway"` + Metadata string `json:"metadata"` +} + // UserRemoteGwsReq - struct to hold user remote acccess gws req type UserRemoteGwsReq struct { RemoteAccessClientID string `json:"remote_access_clientid"` @@ -163,9 +173,11 @@ type HostRelayRequest struct { // IngressRequest - ingress request struct type IngressRequest struct { - ExtclientDNS string `json:"extclientdns"` - IsInternetGateway bool `json:"is_internet_gw"` - Metadata string `json:"metadata"` + ExtclientDNS string `json:"extclientdns"` + IsInternetGateway bool `json:"is_internet_gw"` + Metadata string `json:"metadata"` + PersistentKeepalive int32 `json:"persistentkeepalive"` + MTU int32 `json:"mtu"` } // InetNodeReq - exit node request struct @@ -254,6 +266,8 @@ type ServerConfig struct { IsPro bool `yaml:"isee" json:"Is_EE"` TrafficKey []byte `yaml:"traffickey"` MetricInterval string `yaml:"metric_interval"` + ManageDNS bool `yaml:"manage_dns"` + DefaultDomain string `yaml:"default_domain"` } // User.NameInCharset - returns if name is in charset below or not diff --git a/models/tags.go b/models/tags.go new file mode 100644 index 000000000..9fcb449da --- /dev/null +++ b/models/tags.go @@ -0,0 +1,52 @@ +package models + +import ( + "fmt" + "time" +) + +type TagID string + +const ( + RemoteAccessTagName = "remote-access-gws" +) + +func (id TagID) String() string { + return string(id) +} + +func (t Tag) GetIDFromName() string { + return fmt.Sprintf("%s.%s", t.Network, t.TagName) +} + +type Tag struct { + ID TagID `json:"id"` + TagName string `json:"tag_name"` + Network NetworkID `json:"network"` + CreatedBy string `json:"created_by"` + CreatedAt time.Time `json:"created_at"` +} + +type CreateTagReq struct { + TagName string `json:"tag_name"` + Network NetworkID `json:"network"` + TaggedNodes []ApiNode `json:"tagged_nodes"` +} + +type TagListResp struct { + Tag + UsedByCnt int `json:"used_by_count"` + TaggedNodes []ApiNode `json:"tagged_nodes"` +} + +type TagListRespNodes struct { + Tag + UsedByCnt int `json:"used_by_count"` + TaggedNodes []ApiNode `json:"tagged_nodes"` +} + +type UpdateTagReq struct { + Tag + NewName string `json:"new_name"` + TaggedNodes []ApiNode `json:"tagged_nodes"` +} diff --git a/models/user_mgmt.go b/models/user_mgmt.go index a87a0f4b8..7debd6e22 100644 --- a/models/user_mgmt.go +++ b/models/user_mgmt.go @@ -62,6 +62,7 @@ const ( EnrollmentKeysRsrc RsrcType = "enrollment_key" UserRsrc RsrcType = "users" AclRsrc RsrcType = "acl" + TagRsrc RsrcType = "tag" DnsRsrc RsrcType = "dns" FailOverRsrc RsrcType = "fail_over" MetricRsrc RsrcType = "metrics" @@ -116,8 +117,9 @@ type RsrcPermissionScope struct { type UserRolePermissionTemplate struct { ID UserRoleID `json:"id"` - UiName string `json:"ui_name"` + Name string `json:"name"` Default bool `json:"default"` + MetaData string `json:"meta_data"` DenyDashboardAccess bool `json:"deny_dashboard_access"` FullAccess bool `json:"full_access"` NetworkID NetworkID `json:"network_id"` @@ -132,6 +134,8 @@ type CreateGroupReq struct { type UserGroup struct { ID UserGroupID `json:"id"` + Default bool `json:"default"` + Name string `json:"name"` NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"` MetaData string `json:"meta_data"` } @@ -177,8 +181,9 @@ type UserAuthParams struct { // UserClaims - user claims struct type UserClaims struct { - Role UserRoleID - UserName string + Role UserRoleID + UserName string + RacAutoDisable bool jwt.RegisteredClaims } diff --git a/mq/publishers.go b/mq/publishers.go index 099eb4a05..3b47390a8 100644 --- a/mq/publishers.go +++ b/mq/publishers.go @@ -23,6 +23,10 @@ func PublishPeerUpdate(replacePeers bool) error { return nil } + if servercfg.GetManageDNS() { + sendDNSSync() + } + hosts, err := logic.GetAllHosts() if err != nil { logger.Log(1, "err getting all hosts", err.Error()) @@ -249,3 +253,55 @@ func sendPeers() { } } } + +func SendDNSSyncByNetwork(network string) error { + + k, err := logic.GetDNS(network) + if err == nil && len(k) > 0 { + err = PushSyncDNS(k) + if err != nil { + slog.Warn("error publishing dns entry data for network ", network, err.Error()) + } + } + + return err +} + +func sendDNSSync() error { + + networks, err := logic.GetNetworks() + if err == nil && len(networks) > 0 { + for _, v := range networks { + k, err := logic.GetDNS(v.NetID) + if err == nil && len(k) > 0 { + err = PushSyncDNS(k) + if err != nil { + slog.Warn("error publishing dns entry data for network ", v.NetID, err.Error()) + } + } + } + return nil + } + return err +} + +func PushSyncDNS(dnsEntries []models.DNSEntry) error { + logger.Log(2, "----> Pushing Sync DNS") + data, err := json.Marshal(dnsEntries) + if err != nil { + return errors.New("failed to marshal DNS entries: " + err.Error()) + } + if mqclient == nil || !mqclient.IsConnectionOpen() { + return errors.New("cannot publish ... mqclient not connected") + } + if token := mqclient.Publish(fmt.Sprintf("host/dns/sync/%s", dnsEntries[0].Network), 0, true, data); !token.WaitTimeout(MQ_TIMEOUT*time.Second) || token.Error() != nil { + var err error + if token.Error() == nil { + err = errors.New("connection timeout") + } else { + err = token.Error() + } + return err + } + return nil +} diff --git a/pro/auth/error.go b/pro/auth/error.go index eb49b8769..9fb532d95 100644 --- a/pro/auth/error.go +++ b/pro/auth/error.go @@ -93,12 +93,12 @@ var htmlBaseTemplate = ` ` var oauthNotConfigured = fmt.Sprintf(htmlBaseTemplate, `

Your Netmaker server does not have OAuth configured.

-

Please visit the docs here to learn how to.

`) +

Please visit the docs here to learn how to.

`) var oauthStateInvalid = fmt.Sprintf(htmlBaseTemplate, `

Invalid OAuth Session. Please re-try again.

`) var userNotAllowed = fmt.Sprintf(htmlBaseTemplate, `

Your account does not have access to the dashboard. Please contact your administrator for more information about your account.

-

Non-Admins can access the netmaker networks using RemoteAccessClient.

`) +

Non-Admins can access the netmaker networks using our Remote Access Client.

`) var userFirstTimeSignUp = fmt.Sprintf(htmlBaseTemplate, `

Thank you for signing up. Please contact your administrator for access.

`) diff --git a/pro/auth/templates.go b/pro/auth/templates.go index 52b9cd52e..10bee9ad8 100644 --- a/pro/auth/templates.go +++ b/pro/auth/templates.go @@ -118,7 +118,7 @@ var ssoErrCallbackTemplate = template.Must(

Error reason: {.Verb}

Your Netmaker server may not have SSO configured properly. - Please visit the docs for more information. + Please visit the docs for more information.

If you feel this is a mistake, please contact your network administrator. diff --git a/pro/controllers/failover.go b/pro/controllers/failover.go index 946e753e4..71a72791f 100644 --- a/pro/controllers/failover.go +++ b/pro/controllers/failover.go @@ -203,6 +203,14 @@ func failOverME(w http.ResponseWriter, r *http.Request) { ) return } + if peerNode.IsFailOver { + logic.ReturnErrorResponse( + w, + r, + logic.FormatError(errors.New("peer is acting as failover"), "badrequest"), + ) + return + } if node.IsFailOver { logic.ReturnErrorResponse( w, diff --git a/pro/controllers/rac.go b/pro/controllers/rac.go new file mode 100644 index 000000000..0d1b127fd --- /dev/null +++ b/pro/controllers/rac.go @@ -0,0 +1,14 @@ +package controllers + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/gravitl/netmaker/logic" +) + +func RacHandlers(r *mux.Router) { + r.HandleFunc("/api/v1/rac/networks", logic.SecurityCheck(false, http.HandlerFunc(getUserRemoteAccessNetworks))).Methods(http.MethodGet) + r.HandleFunc("/api/v1/rac/network/{network}/access_points", logic.SecurityCheck(false, http.HandlerFunc(getUserRemoteAccessNetworkGateways))).Methods(http.MethodGet) + r.HandleFunc("/api/v1/rac/access_point/{access_point_id}/config", logic.SecurityCheck(false, http.HandlerFunc(getRemoteAccessGatewayConf))).Methods(http.MethodGet) +} diff --git a/pro/controllers/users.go b/pro/controllers/users.go index c8119b79e..31099d0ce 100644 --- a/pro/controllers/users.go +++ b/pro/controllers/users.go @@ -250,8 +250,9 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) { // Set E-Mail body. You can set plain text or html with text/html e := email.UserInvitedMail{ - BodyBuilder: &email.EmailBodyBuilderWithH1HeadlineAndImage{}, - InviteURL: invite.InviteURL, + BodyBuilder: &email.EmailBodyBuilderWithH1HeadlineAndImage{}, + InviteURL: invite.InviteURL, + PlatformRoleID: invite.PlatformRoleID, } n := email.Notification{ RecipientMail: invite.Email, @@ -263,7 +264,6 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) { }(invite) } logic.ReturnSuccessResponse(w, r, "triggered user invites") - } // swagger:route GET /api/v1/users/invites user listUserInvites @@ -451,6 +451,10 @@ func updateUserGroup(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) return } + if currUserG.Default { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update default user group"), "badrequest")) + return + } err = proLogic.ValidateUpdateGroupReq(userGroup) if err != nil { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) @@ -461,6 +465,7 @@ func updateUserGroup(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } + // reset configs for service user go proLogic.UpdatesUserGwAccessOnGrpUpdates(currUserG.NetworkRoles, userGroup.NetworkRoles) logic.ReturnSuccessResponseWithJson(w, r, userGroup, "updated user group") @@ -496,6 +501,10 @@ func deleteUserGroup(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to fetch group details"), "badrequest")) return } + if userG.Default { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot delete default user group"), "badrequest")) + return + } err = proLogic.DeleteUserGroup(models.UserGroupID(gid)) if err != nil { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) @@ -816,6 +825,221 @@ func removeUserFromRemoteAccessGW(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(logic.ToReturnUser(*user)) } +// @Summary Get Users Remote Access Gw Networks. +// @Router /api/users/{username}/remote_access_gw [get] +// @Tags Users +// @Param username path string true "Username to fetch all the gateways with access" +// @Success 200 {object} map[string][]models.UserRemoteGws +// @Failure 500 {object} models.ErrorResponse +func getUserRemoteAccessNetworks(w http.ResponseWriter, r *http.Request) { + // set header. + w.Header().Set("Content-Type", "application/json") + username := r.Header.Get("user") + user, err := logic.GetUser(username) + if err != nil { + logger.Log(0, username, "failed to fetch user: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest")) + return + } + userGws := make(map[string][]models.UserRemoteGws) + networks := []models.Network{} + networkMap := make(map[string]struct{}) + userGwNodes := proLogic.GetUserRAGNodes(*user) + for _, node := range userGwNodes { + network, err := logic.GetNetwork(node.Network) + if err != nil { + slog.Error("failed to get node network", "error", err) + continue + } + if _, ok := networkMap[network.NetID]; ok { + continue + } + networkMap[network.NetID] = struct{}{} + networks = append(networks, network) + } + + slog.Debug("returned user gws", "user", username, "gws", userGws) + logic.ReturnSuccessResponseWithJson(w, r, networks, "fetched user accessible networks") +} + +// @Summary Get Users Remote Access Gw Networks. +// @Router /api/users/{username}/remote_access_gw [get] +// @Tags Users +// @Param username path string true "Username to fetch all the gateways with access" +// @Success 200 {object} map[string][]models.UserRemoteGws +// @Failure 500 {object} models.ErrorResponse +func getUserRemoteAccessNetworkGateways(w http.ResponseWriter, r *http.Request) { + // set header. + w.Header().Set("Content-Type", "application/json") + var params = mux.Vars(r) + username := r.Header.Get("user") + user, err := logic.GetUser(username) + if err != nil { + logger.Log(0, username, "failed to fetch user: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest")) + return + } + network := params["network"] + if network == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params network"), "badrequest")) + return + } + userGws := []models.UserRAGs{} + + userGwNodes := proLogic.GetUserRAGNodes(*user) + for _, node := range userGwNodes { + if node.Network != network { + continue + } + + host, err := logic.GetHost(node.HostID.String()) + if err != nil { + continue + } + + userGws = append(userGws, models.UserRAGs{ + GwID: node.ID.String(), + GWName: host.Name, + Network: node.Network, + IsInternetGateway: node.IsInternetGateway, + Metadata: node.Metadata, + }) + + } + + slog.Debug("returned user gws", "user", username, "gws", userGws) + logic.ReturnSuccessResponseWithJson(w, r, userGws, "fetched user accessible gateways in network "+network) +} + +// @Summary Get Users Remote Access Gw Networks. +// @Router /api/users/{username}/remote_access_gw [get] +// @Tags Users +// @Param username path string true "Username to fetch all the gateways with access" +// @Success 200 {object} map[string][]models.UserRemoteGws +// @Failure 500 {object} models.ErrorResponse +func getRemoteAccessGatewayConf(w http.ResponseWriter, r *http.Request) { + // set header. + w.Header().Set("Content-Type", "application/json") + var params = mux.Vars(r) + username := r.Header.Get("user") + user, err := logic.GetUser(username) + if err != nil { + logger.Log(0, username, "failed to fetch user: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest")) + return + } + remoteGwID := params["access_point_id"] + if remoteGwID == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params access_point_id"), "badrequest")) + return + } + var req models.UserRemoteGwsReq + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + slog.Error("error decoding request body: ", "error", err) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + + userGwNodes := proLogic.GetUserRAGNodes(*user) + if _, ok := userGwNodes[remoteGwID]; !ok { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("access denied"), "forbidden")) + return + } + node, err := logic.GetNodeByID(remoteGwID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch gw node %s, error: %v", remoteGwID, err), "badrequest")) + return + } + host, err := logic.GetHost(node.HostID.String()) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch gw host %s, error: %v", remoteGwID, err), "badrequest")) + return + } + network, err := logic.GetNetwork(node.Network) + if err != nil { + slog.Error("failed to get node network", "error", err) + } + var userConf models.ExtClient + allextClients, err := logic.GetAllExtClients() + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + for _, extClient := range allextClients { + if extClient.Network != network.NetID || extClient.IngressGatewayID != node.ID.String() { + continue + } + if extClient.RemoteAccessClientID == req.RemoteAccessClientID && extClient.OwnerID == username { + userConf = extClient + userConf.AllowedIPs = logic.GetExtclientAllowedIPs(extClient) + } + } + if userConf.ClientID == "" { + // create a new conf + userConf.OwnerID = user.UserName + userConf.RemoteAccessClientID = req.RemoteAccessClientID + userConf.IngressGatewayID = node.ID.String() + + // set extclient dns to ingressdns if extclient dns is not explicitly set + if (userConf.DNS == "") && (node.IngressDNS != "") { + userConf.DNS = node.IngressDNS + } + + userConf.Network = node.Network + host, err := logic.GetHost(node.HostID.String()) + if err != nil { + logger.Log(0, r.Header.Get("user"), + fmt.Sprintf("failed to get ingress gateway host for node [%s] info: %v", node.ID, err)) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + listenPort := logic.GetPeerListenPort(host) + if host.EndpointIP.To4() == nil { + userConf.IngressGatewayEndpoint = fmt.Sprintf("[%s]:%d", host.EndpointIPv6.String(), listenPort) + } else { + userConf.IngressGatewayEndpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), listenPort) + } + userConf.Enabled = true + parentNetwork, err := logic.GetNetwork(node.Network) + if err == nil { // check if parent network default ACL is enabled (yes) or not (no) + userConf.Enabled = parentNetwork.DefaultACL == "yes" + } + userConf.Tags = make(map[models.TagID]struct{}) + userConf.Tags[models.TagID(fmt.Sprintf("%s.%s", userConf.Network, + models.RemoteAccessTagName))] = struct{}{} + if err = logic.CreateExtClient(&userConf); err != nil { + slog.Error( + "failed to create extclient", + "user", + r.Header.Get("user"), + "network", + node.Network, + "error", + err, + ) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + } + userGw := models.UserRemoteGws{ + GwID: node.ID.String(), + GWName: host.Name, + Network: node.Network, + GwClient: userConf, + Connected: true, + IsInternetGateway: node.IsInternetGateway, + GwPeerPublicKey: host.PublicKey.String(), + GwListenPort: logic.GetPeerListenPort(host), + Metadata: node.Metadata, + AllowedEndpoints: getAllowedRagEndpoints(&node, host), + NetworkAddresses: []string{network.AddressRange, network.AddressRange6}, + } + + slog.Debug("returned user gw config", "user", user.UserName, "gws", userGw) + logic.ReturnSuccessResponseWithJson(w, r, userGw, "fetched user config to gw "+remoteGwID) +} + // @Summary Get Users Remote Access Gw. // @Router /api/users/{username}/remote_access_gw [get] // @Tags Users @@ -876,6 +1100,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) { network, err := logic.GetNetwork(node.Network) if err != nil { slog.Error("failed to get node network", "error", err) + continue } gws := userGws[node.Network] diff --git a/pro/email/invite.go b/pro/email/invite.go index 86018462c..721b3387e 100644 --- a/pro/email/invite.go +++ b/pro/email/invite.go @@ -2,65 +2,60 @@ package email import ( "fmt" + "github.com/gravitl/netmaker/models" + proLogic "github.com/gravitl/netmaker/pro/logic" "github.com/gravitl/netmaker/servercfg" ) // UserInvitedMail - mail for users that are invited to a tenant type UserInvitedMail struct { - BodyBuilder EmailBodyBuilder - InviteURL string + BodyBuilder EmailBodyBuilder + InviteURL string + PlatformRoleID string } // GetSubject - gets the subject of the email func (UserInvitedMail) GetSubject(info Notification) string { - return "You're invited to join Netmaker" + return "Connect to Your Secure Network Using Netmaker" } // GetBody - gets the body of the email func (invite UserInvitedMail) GetBody(info Notification) string { + downloadLink := "https://www.netmaker.io/download" + supportEmail := "support@netmaker.io" + + dashboardURL := fmt.Sprintf("https://dashboard.%s", servercfg.GetNmBaseDomain()) if servercfg.DeployedByOperator() { - return invite.BodyBuilder. - WithParagraph("Hi there,"). - WithParagraph("
"). - WithParagraph("Great news! Your colleague has invited you to join their Netmaker SaaS Tenant."). - WithParagraph("Click the button to accept your invitation:"). - WithParagraph("
"). - WithParagraph(fmt.Sprintf("Accept Invitation", invite.InviteURL)). - WithParagraph("
"). - WithParagraph("Why you'll love Netmaker:"). - WithParagraph("

"). - WithParagraph("Got questions? Our team is here to help you every step of the way."). - WithParagraph("
"). - WithParagraph("Welcome aboard,"). - WithParagraph("

The Netmaker Team

"). - WithParagraph("P.S. Curious to learn more before accepting? Check out our quick start tutorial at netmaker.io/tutorials"). - Build() + dashboardURL = fmt.Sprintf("%s/dashboard?tenant_id=%s", proLogic.GetAccountsUIHost(), servercfg.GetNetmakerTenantID()) + } + + content := invite.BodyBuilder. + WithParagraph("Hi,"). + WithParagraph("You've been invited to access a secure network via Netmaker's Remote Access Client (RAC). Follow these simple steps to get connected:"). + WithHtml("
    "). + WithHtml(fmt.Sprintf("
  1. Click here to accept your invitation and setup your account.
  2. ", invite.InviteURL)). + WithHtml("
    "). + WithHtml(fmt.Sprintf("
  3. Download the Remote Access Client (RAC).
  4. ", downloadLink)) + + if invite.PlatformRoleID == models.AdminRole.String() || invite.PlatformRoleID == models.PlatformUser.String() { + content = content. + WithHtml("
    "). + WithHtml(fmt.Sprintf("
  5. Access the Netmaker Dashboard - use it to manage your network settings and view network status.
  6. ", dashboardURL)) + } + + connectionID := servercfg.GetNetmakerTenantID() + if !servercfg.DeployedByOperator() { + connectionID = fmt.Sprintf("api.%s", servercfg.GetNmBaseDomain()) } - return invite.BodyBuilder. - WithParagraph("Hi there,"). - WithParagraph("
    "). - WithParagraph("Great news! Your colleague has invited you to join their Netmaker network."). - WithParagraph("Click the button to accept your invitation:"). - WithParagraph("
    "). - WithParagraph(fmt.Sprintf("Accept Invitation", invite.InviteURL)). - WithParagraph("
    "). - WithParagraph("Why you'll love Netmaker:"). - WithParagraph(""). - WithParagraph("Got questions? Our team is here to help you every step of the way."). - WithParagraph("
    "). - WithParagraph("Welcome aboard,"). - WithParagraph("

    The Netmaker Team

    "). - WithParagraph("P.S. Curious to learn more before accepting? Check out our quick start tutorial at netmaker.io/tutorials"). + return content. + WithHtml("
"). + WithParagraph("Important Information:"). + WithHtml(""). + WithParagraph(fmt.Sprintf("If you have any questions or need assistance, please contact our support team at %s.", supportEmail, supportEmail)). + WithParagraph("Best Regards,"). + WithParagraph("The Netmaker Team"). Build() } diff --git a/pro/email/utils.go b/pro/email/utils.go index b689a8beb..4ab221d6a 100644 --- a/pro/email/utils.go +++ b/pro/email/utils.go @@ -4,27 +4,20 @@ import "strings" // mail related images hosted on github var ( - nLogoTeal = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/N_Teal.png" netmakerLogoTeal = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/netmaker-logo-2.png" - netmakerMeshLogo = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/netmaker-mesh.png" - linkedinIcon = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/linkedin2x.png" - discordIcon = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/discord-logo-png-7617.png" - githubIcon = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/Octocat.png" - mailIcon = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-mail-24.png" - addressIcon = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-address-16.png" - linkIcon = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-hyperlink-64.png" ) type EmailBodyBuilder interface { WithHeadline(text string) EmailBodyBuilder WithParagraph(text string) EmailBodyBuilder + WithHtml(text string) EmailBodyBuilder WithSignature() EmailBodyBuilder Build() string } type EmailBodyBuilderWithH1HeadlineAndImage struct { headline string - paragraphs []string + bodyContent []string hasSignature bool } @@ -34,7 +27,12 @@ func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithHeadline(text string) Email } func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithParagraph(text string) EmailBodyBuilder { - b.paragraphs = append(b.paragraphs, text) + b.bodyContent = append(b.bodyContent, styledParagraph(text)) + return b +} + +func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithHtml(text string) EmailBodyBuilder { + b.bodyContent = append(b.bodyContent, text) return b } @@ -44,524 +42,134 @@ func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithSignature() EmailBodyBuilde } func (b *EmailBodyBuilderWithH1HeadlineAndImage) Build() string { - // map paragraphs to styled paragraphs - styledParagraphsSlice := make([]string, len(b.paragraphs)) - for i, paragraph := range b.paragraphs { - styledParagraphsSlice[i] = styledParagraph(paragraph) - } - // join styled paragraphs - styledParagraphsString := strings.Join(styledParagraphsSlice, "") + bodyContent := strings.Join(b.bodyContent, "") - signature := "" - if b.hasSignature { - signature = styledSignature() - } + // TODO: Edit design to add signature. + //signature := "" + //if b.hasSignature { + // signature = styledSignature() + //} return ` - - - - - - - - - - - - - - - - - - - - ` + signature + ` - ` + + + + + + Simple Transactional Email + + + + + + + + + + + +` +} + +func styledParagraph(text string) string { + return `

` + text + `

` } diff --git a/pro/initialize.go b/pro/initialize.go index 1c6ba8a1c..f6ae65670 100644 --- a/pro/initialize.go +++ b/pro/initialize.go @@ -33,16 +33,17 @@ func InitPro() { proControllers.UserHandlers, proControllers.FailOverHandlers, proControllers.InetHandlers, + proControllers.RacHandlers, ) controller.ListRoles = proControllers.ListRoles logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() { // == License Handling == - enableLicenseHook := false - licenseKeyValue := servercfg.GetLicenseKey() - netmakerTenantID := servercfg.GetNetmakerTenantID() - if licenseKeyValue != "" && netmakerTenantID != "" { - enableLicenseHook = true - } + enableLicenseHook := true + // licenseKeyValue := servercfg.GetLicenseKey() + // netmakerTenantID := servercfg.GetNetmakerTenantID() + // if licenseKeyValue != "" && netmakerTenantID != "" { + // enableLicenseHook = true + // } if !enableLicenseHook { err := initTrial() if err != nil { @@ -130,9 +131,15 @@ func InitPro() { logic.CreateDefaultNetworkRolesAndGroups = proLogic.CreateDefaultNetworkRolesAndGroups logic.FilterNetworksByRole = proLogic.FilterNetworksByRole logic.IsGroupsValid = proLogic.IsGroupsValid + logic.IsGroupValid = proLogic.IsGroupValid logic.IsNetworkRolesValid = proLogic.IsNetworkRolesValid logic.InitialiseRoles = proLogic.UserRolesInit logic.UpdateUserGwAccess = proLogic.UpdateUserGwAccess + logic.CreateDefaultUserPolicies = proLogic.CreateDefaultUserPolicies + logic.MigrateUserRoleAndGroups = proLogic.MigrateUserRoleAndGroups + logic.IntialiseGroups = proLogic.UserGroupsInit + logic.AddGlobalNetRolesToAdmins = proLogic.AddGlobalNetRolesToAdmins + logic.GetUserGroupsInNetwork = proLogic.GetUserGroupsInNetwork } func retrieveProLogo() string { diff --git a/pro/logic/ext_acls.go b/pro/logic/ext_acls.go index 6ea62dd41..3c626820c 100644 --- a/pro/logic/ext_acls.go +++ b/pro/logic/ext_acls.go @@ -62,7 +62,7 @@ func SetClientDefaultACLs(ec *models.ExtClient) error { slog.Error("failed to get network acls", "error", err) return err } - networkAcls[acls.AclID(ec.ClientID)] = acls.ACL{} + networkAcls[acls.AclID(ec.ClientID)] = make(acls.ACL) for i := range networkNodes { currNode := networkNodes[i] if network.DefaultACL == "no" || currNode.DefaultACL == "no" { diff --git a/pro/logic/metrics.go b/pro/logic/metrics.go index dfcd4d6a2..81e61018c 100644 --- a/pro/logic/metrics.go +++ b/pro/logic/metrics.go @@ -2,7 +2,6 @@ package logic import ( "encoding/json" - "math" "sync" "time" @@ -209,15 +208,17 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) { currMetric.TotalTime += oldMetric.TotalTime currMetric.Uptime += oldMetric.Uptime // get the total uptime for this connection - if currMetric.TotalReceived < oldMetric.TotalReceived { + totalRecv := currMetric.TotalReceived + totalSent := currMetric.TotalSent + if currMetric.TotalReceived < oldMetric.TotalReceived && currMetric.TotalReceived < oldMetric.LastTotalReceived { currMetric.TotalReceived += oldMetric.TotalReceived } else { - currMetric.TotalReceived += int64(math.Abs(float64(currMetric.TotalReceived) - float64(oldMetric.TotalReceived))) + currMetric.TotalReceived = currMetric.TotalReceived - oldMetric.LastTotalReceived + oldMetric.TotalReceived } - if currMetric.TotalSent < oldMetric.TotalSent { + if currMetric.TotalSent < oldMetric.TotalSent && currMetric.TotalSent < oldMetric.LastTotalSent { currMetric.TotalSent += oldMetric.TotalSent } else { - currMetric.TotalSent += int64(math.Abs(float64(currMetric.TotalSent) - float64(oldMetric.TotalSent))) + currMetric.TotalSent = currMetric.TotalSent - oldMetric.LastTotalSent + oldMetric.TotalSent } if currMetric.Uptime == 0 || currMetric.TotalTime == 0 { @@ -228,6 +229,8 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) { totalUpMinutes := currMetric.Uptime * ncutils.CheckInInterval currMetric.ActualUptime = time.Duration(totalUpMinutes) * time.Minute delete(oldMetrics.Connectivity, k) // remove from old data + currMetric.LastTotalReceived = totalRecv + currMetric.LastTotalSent = totalSent newMetrics.Connectivity[k] = currMetric } diff --git a/pro/logic/migrate.go b/pro/logic/migrate.go new file mode 100644 index 000000000..fedef3c9b --- /dev/null +++ b/pro/logic/migrate.go @@ -0,0 +1,68 @@ +package logic + +import ( + "fmt" + + "github.com/gravitl/netmaker/logic" + "github.com/gravitl/netmaker/models" +) + +func MigrateUserRoleAndGroups(user models.User) { + var err error + if user.PlatformRoleID == models.AdminRole || user.PlatformRoleID == models.SuperAdminRole { + return + } + if len(user.RemoteGwIDs) > 0 { + // define user roles for network + // assign relevant network role to user + for remoteGwID := range user.RemoteGwIDs { + gwNode, err := logic.GetNodeByID(remoteGwID) + if err != nil { + continue + } + var g models.UserGroup + if user.PlatformRoleID == models.ServiceUser { + g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp", gwNode.Network, models.NetworkUser))) + } else { + g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp", + gwNode.Network, models.NetworkAdmin))) + } + if err != nil { + continue + } + user.UserGroups[g.ID] = struct{}{} + } + } + if len(user.NetworkRoles) > 0 { + for netID, netRoles := range user.NetworkRoles { + var g models.UserGroup + adminAccess := false + for netRoleID := range netRoles { + permTemplate, err := logic.GetRole(netRoleID) + if err == nil { + if permTemplate.FullAccess { + adminAccess = true + } + } + } + + if user.PlatformRoleID == models.ServiceUser { + g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser))) + } else { + role := models.NetworkUser + if adminAccess { + role = models.NetworkAdmin + } + g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp", + netID, role))) + } + if err != nil { + continue + } + user.UserGroups[g.ID] = struct{}{} + user.NetworkRoles = make(map[models.NetworkID]map[models.UserRoleID]struct{}) + } + + } + logic.UpsertUser(user) +} diff --git a/pro/logic/security.go b/pro/logic/security.go index fcc6d73cd..fbe0105d8 100644 --- a/pro/logic/security.go +++ b/pro/logic/security.go @@ -50,6 +50,9 @@ func NetworkPermissionsCheck(username string, r *http.Request) error { if targetRsrc == "" { return errors.New("target rsrc is missing") } + if r.Header.Get("RAC") == "true" && r.Method == http.MethodGet { + return nil + } if netID == "" { return errors.New("network id is missing") } @@ -79,8 +82,17 @@ func NetworkPermissionsCheck(username string, r *http.Request) error { } } for groupID := range user.UserGroups { + userG, err := GetUserGroup(groupID) if err == nil { + if netRoles, ok := userG.NetworkRoles[models.AllNetworks]; ok { + for netRoleID := range netRoles { + err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID) + if err == nil { + return nil + } + } + } netRoles := userG.NetworkRoles[models.NetworkID(netID)] for netRoleID := range netRoles { err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID) diff --git a/pro/logic/user_mgmt.go b/pro/logic/user_mgmt.go index 1d336cc17..c3a6534ff 100644 --- a/pro/logic/user_mgmt.go +++ b/pro/logic/user_mgmt.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "time" "github.com/gravitl/netmaker/database" "github.com/gravitl/netmaker/logger" @@ -29,6 +30,8 @@ var PlatformUserUserPermissionTemplate = models.UserRolePermissionTemplate{ var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{ ID: models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkAdmin)), + Name: "Network Admins", + MetaData: "can manage configuration of all networks", Default: true, FullAccess: true, NetworkID: models.AllNetworks, @@ -36,6 +39,8 @@ var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{ var NetworkUserAllPermissionTemplate = models.UserRolePermissionTemplate{ ID: models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkUser)), + Name: "Network Users", + MetaData: "cannot access the admin console, but can connect to nodes in your networks via Remote Access Client.", Default: true, FullAccess: false, NetworkID: models.AllNetworks, @@ -74,12 +79,44 @@ func UserRolesInit() { } +func UserGroupsInit() { + // create default network groups + var NetworkGlobalAdminGroup = models.UserGroup{ + ID: models.UserGroupID(fmt.Sprintf("global-%s-grp", models.NetworkAdmin)), + Default: true, + Name: "All Networks Admin Group", + MetaData: "can manage configuration of all networks", + NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{ + models.AllNetworks: { + models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkAdmin)): {}, + }, + }, + } + var NetworkGlobalUserGroup = models.UserGroup{ + ID: models.UserGroupID(fmt.Sprintf("global-%s-grp", models.NetworkUser)), + Name: "All Networks User Group", + Default: true, + NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{ + models.NetworkID(models.AllNetworks): { + models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkUser)): {}, + }, + }, + MetaData: "cannot access the admin console, but can connect to nodes in your networks via Remote Access Client.", + } + d, _ := json.Marshal(NetworkGlobalAdminGroup) + database.Insert(NetworkGlobalAdminGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME) + d, _ = json.Marshal(NetworkGlobalUserGroup) + database.Insert(NetworkGlobalUserGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME) +} + func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) { if netID.String() == "" { return } var NetworkAdminPermissionTemplate = models.UserRolePermissionTemplate{ ID: models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)), + Name: fmt.Sprintf("%s Admin", netID), + MetaData: fmt.Sprintf("can manage your network `%s` configuration.", netID), Default: true, NetworkID: netID, FullAccess: true, @@ -88,6 +125,8 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) { var NetworkUserPermissionTemplate = models.UserRolePermissionTemplate{ ID: models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)), + Name: fmt.Sprintf("%s User", netID), + MetaData: fmt.Sprintf("cannot access the admin console, but can connect to nodes in your network `%s` via Remote Access Client.", netID), Default: true, FullAccess: false, NetworkID: netID, @@ -117,27 +156,30 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) { // create default network groups var NetworkAdminGroup = models.UserGroup{ - ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)), + ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)), + Name: fmt.Sprintf("%s Admin Group", netID), NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{ netID: { models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)): {}, }, }, - MetaData: "The network group was automatically created by Netmaker.", + MetaData: fmt.Sprintf("can manage your network `%s` configuration including adding and removing devices.", netID), } var NetworkUserGroup = models.UserGroup{ - ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)), + ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)), + Name: fmt.Sprintf("%s User Group", netID), NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{ netID: { models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)): {}, }, }, - MetaData: "The network group was automatically created by Netmaker.", + MetaData: fmt.Sprintf("cannot access the admin console, but can connect to nodes in your network `%s` via Remote Access Client.", netID), } d, _ = json.Marshal(NetworkAdminGroup) database.Insert(NetworkAdminGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME) d, _ = json.Marshal(NetworkUserGroup) database.Insert(NetworkUserGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME) + } func DeleteNetworkRoles(netID string) { @@ -403,7 +445,6 @@ func ValidateCreateGroupReq(g models.UserGroup) error { return nil } func ValidateUpdateGroupReq(g models.UserGroup) error { - for networkID := range g.NetworkRoles { userRolesMap := g.NetworkRoles[networkID] for roleID := range userRolesMap { @@ -511,6 +552,49 @@ func HasNetworkRsrcScope(permissionTemplate models.UserRolePermissionTemplate, n _, ok = rsrcScope[rsrcID] return ok } + +func GetUserRAGNodesV1(user models.User) (gws map[string]models.Node) { + gws = make(map[string]models.Node) + nodes, err := logic.GetAllNodes() + if err != nil { + return + } + if user.PlatformRoleID == models.AdminRole || user.PlatformRoleID == models.SuperAdminRole { + for _, node := range nodes { + if node.IsIngressGateway { + gws[node.ID.String()] = node + } + + } + } + tagNodesMap := logic.GetTagMapWithNodes() + accessPolices := logic.ListUserPolicies(user) + for _, policyI := range accessPolices { + if !policyI.Enabled { + continue + } + for _, dstI := range policyI.Dst { + if dstI.Value == "*" { + networkNodes := logic.GetNetworkNodesMemory(nodes, policyI.NetworkID.String()) + for _, node := range networkNodes { + if node.IsIngressGateway { + gws[node.ID.String()] = node + } + } + } + if nodes, ok := tagNodesMap[models.TagID(dstI.Value)]; ok { + for _, node := range nodes { + if node.IsIngressGateway { + gws[node.ID.String()] = node + } + + } + } + } + } + return +} + func GetUserRAGNodes(user models.User) (gws map[string]models.Node) { gws = make(map[string]models.Node) userGwAccessScope := GetUserNetworkRolesWithRemoteVPNAccess(user) @@ -555,6 +639,7 @@ func GetUserNetworkRolesWithRemoteVPNAccess(user models.User) (gwAccess map[mode } if _, ok := user.NetworkRoles[models.AllNetworks]; ok { gwAccess[models.NetworkID("*")] = make(map[models.RsrcID]models.RsrcPermissionScope) + return } if len(user.UserGroups) > 0 { for gID := range user.UserGroups { @@ -562,6 +647,10 @@ func GetUserNetworkRolesWithRemoteVPNAccess(user models.User) (gwAccess map[mode if err != nil { continue } + if _, ok := userG.NetworkRoles[models.AllNetworks]; ok { + gwAccess[models.NetworkID("*")] = make(map[models.RsrcID]models.RsrcPermissionScope) + return + } for netID, roleMap := range userG.NetworkRoles { for roleID := range roleMap { role, err := logic.GetRole(roleID) @@ -662,7 +751,9 @@ func GetFilteredNodesByUserAccess(user models.User, nodes []models.Node) (filter nodesMap := make(map[string]struct{}) allNetworkRoles := make(map[models.UserRoleID]struct{}) - + defer func() { + filteredNodes = logic.AddStaticNodestoList(filteredNodes) + }() if len(user.NetworkRoles) > 0 { for _, netRoles := range user.NetworkRoles { for netRoleI := range netRoles { @@ -671,7 +762,8 @@ func GetFilteredNodesByUserAccess(user models.User, nodes []models.Node) (filter } } if _, ok := user.NetworkRoles[models.AllNetworks]; ok { - return nodes + filteredNodes = nodes + return } if len(user.UserGroups) > 0 { for userGID := range user.UserGroups { @@ -679,7 +771,8 @@ func GetFilteredNodesByUserAccess(user models.User, nodes []models.Node) (filter if err == nil { if len(userG.NetworkRoles) > 0 { if _, ok := userG.NetworkRoles[models.AllNetworks]; ok { - return nodes + filteredNodes = nodes + return } for _, netRoles := range userG.NetworkRoles { for netRoleI := range netRoles { @@ -792,6 +885,16 @@ func IsGroupsValid(groups map[models.UserGroupID]struct{}) error { return nil } +func IsGroupValid(groupID models.UserGroupID) error { + + _, err := GetUserGroup(groupID) + if err != nil { + return fmt.Errorf("user group `%s` not found", groupID) + } + + return nil +} + func IsNetworkRolesValid(networkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) error { for netID, netRoles := range networkRoles { @@ -1036,3 +1139,97 @@ func UpdateUserGwAccess(currentUser, changeUser models.User) { } } + +func CreateDefaultUserPolicies(netID models.NetworkID) { + if netID.String() == "" { + return + } + + if !logic.IsAclExists(fmt.Sprintf("%s.%s-grp", netID, models.NetworkAdmin)) { + defaultUserAcl := models.Acl{ + ID: fmt.Sprintf("%s.%s-grp", netID, models.NetworkAdmin), + Name: "Network Admin", + MetaData: "This Policy allows all network admins to communicate with all remote access gateways", + Default: true, + NetworkID: netID, + RuleType: models.UserPolicy, + Src: []models.AclPolicyTag{ + { + ID: models.UserGroupAclID, + Value: fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin), + }, + { + ID: models.UserGroupAclID, + Value: fmt.Sprintf("global-%s-grp", models.NetworkAdmin), + }, + }, + Dst: []models.AclPolicyTag{ + { + ID: models.DeviceAclID, + Value: fmt.Sprintf("%s.%s", netID, models.RemoteAccessTagName), + }}, + AllowedDirection: models.TrafficDirectionUni, + Enabled: true, + CreatedBy: "auto", + CreatedAt: time.Now().UTC(), + } + logic.InsertAcl(defaultUserAcl) + } + + if !logic.IsAclExists(fmt.Sprintf("%s.%s-grp", netID, models.NetworkUser)) { + defaultUserAcl := models.Acl{ + ID: fmt.Sprintf("%s.%s-grp", netID, models.NetworkUser), + Name: "Network User", + MetaData: "This Policy allows all network users to communicate with all remote access gateways", + Default: true, + NetworkID: netID, + RuleType: models.UserPolicy, + Src: []models.AclPolicyTag{ + { + ID: models.UserGroupAclID, + Value: fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser), + }, + { + ID: models.UserGroupAclID, + Value: fmt.Sprintf("global-%s-grp", models.NetworkUser), + }, + }, + + Dst: []models.AclPolicyTag{ + { + ID: models.DeviceAclID, + Value: fmt.Sprintf("%s.%s", netID, models.RemoteAccessTagName), + }}, + AllowedDirection: models.TrafficDirectionUni, + Enabled: true, + CreatedBy: "auto", + CreatedAt: time.Now().UTC(), + } + logic.InsertAcl(defaultUserAcl) + } + +} + +func GetUserGroupsInNetwork(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { + groups, _ := ListUserGroups() + networkGrps = make(map[models.UserGroupID]models.UserGroup) + for _, grp := range groups { + if _, ok := grp.NetworkRoles[models.AllNetworks]; ok { + networkGrps[grp.ID] = grp + continue + } + if _, ok := grp.NetworkRoles[netID]; ok { + networkGrps[grp.ID] = grp + } + } + return +} + +func AddGlobalNetRolesToAdmins(u models.User) { + if u.PlatformRoleID != models.SuperAdminRole && u.PlatformRoleID != models.AdminRole { + return + } + u.UserGroups = make(map[models.UserGroupID]struct{}) + u.UserGroups[models.UserGroupID(fmt.Sprintf("global-%s-grp", models.NetworkAdmin))] = struct{}{} + logic.UpsertUser(u) +} diff --git a/pro/remote_access_client.go b/pro/remote_access_client.go index c46115bc8..16a38a85f 100644 --- a/pro/remote_access_client.go +++ b/pro/remote_access_client.go @@ -43,13 +43,18 @@ func racAutoDisableHook() error { currentTime := time.Now() validityDuration := servercfg.GetJwtValidityDuration() for _, user := range users { + if user.PlatformRoleID == models.AdminRole || + user.PlatformRoleID == models.SuperAdminRole { + continue + } if !currentTime.After(user.LastLoginTime.Add(validityDuration)) { continue } for _, client := range clients { + if client.RemoteAccessClientID == "" { + continue + } if (client.OwnerID == user.UserName) && - user.PlatformRoleID != models.SuperAdminRole && - user.PlatformRoleID != models.AdminRole && client.Enabled { slog.Info(fmt.Sprintf("disabling ext client %s for user %s due to RAC autodisabling", client.ClientID, client.OwnerID)) if err := disableExtClient(&client); err != nil { diff --git a/pro/types.go b/pro/types.go index ae31cafe0..213d77bf8 100644 --- a/pro/types.go +++ b/pro/types.go @@ -4,7 +4,7 @@ package pro import ( - "fmt" + "errors" ) const ( @@ -13,7 +13,7 @@ const ( server_id_key = "nm-server-id" ) -var errValidation = fmt.Errorf(license_validation_err_msg) +var errValidation = errors.New(license_validation_err_msg) // LicenseKey - the license key struct representation with associated data type LicenseKey struct { @@ -32,14 +32,14 @@ type LicenseKey struct { // ValidatedLicense - the validated license struct type ValidatedLicense struct { - LicenseValue string `json:"license_value" binding:"required"` // license that validation is being requested for + LicenseValue string `json:"license_value" binding:"required"` // license that validation is being requested for EncryptedLicense string `json:"encrypted_license" binding:"required"` // to be decrypted by Netmaker using Netmaker server's private key } // LicenseSecret - the encrypted struct for sending user-id type LicenseSecret struct { AssociatedID string `json:"associated_id" binding:"required"` // UUID for user foreign key to User table - Usage Usage `json:"limits" binding:"required"` + Usage Usage `json:"limits" binding:"required"` } // Usage - struct for license usage @@ -71,9 +71,9 @@ func (l *Usage) SetDefaults() { // ValidateLicenseRequest - used for request to validate license endpoint type ValidateLicenseRequest struct { - LicenseKey string `json:"license_key" binding:"required"` + LicenseKey string `json:"license_key" binding:"required"` NmServerPubKey string `json:"nm_server_pub_key" binding:"required"` // Netmaker server public key used to send data back to Netmaker for the Netmaker server to decrypt (eg output from validating license) - EncryptedPart string `json:"secret" binding:"required"` + EncryptedPart string `json:"secret" binding:"required"` } type licenseResponseCache struct { diff --git a/release.md b/release.md index 7f32b5843..f2a6d82aa 100644 --- a/release.md +++ b/release.md @@ -1,21 +1,21 @@ -# Netmaker v0.25.0 +# Netmaker v0.26.0 ## Whats New ✨ -- Advanced User Management with Network Roles and Groups -- User Invitation via Email and Magic Links +- New ACLs and Tag Management System +- Managed DNS system (Linux) +- Simplified User Mgmt With Default Roles and Groups (Hidden away network roles) +- New Add a Node Flow for netclient and static wireguard files ## What's Fixed/Improved 🛠 - -- Scalability Improvements -- Optimised Traffic Flow Over MQ -- Improved Peer Updates with Batching +- Metrics Data +- FailOver Stability Fixes +- Scalability Fixes ## Known Issues 🐞 -- Erratic Traffic Data In Metrics. - Adding Custom Private/Public Key For Remote Access Gw Clients Doesn't Get Propagated To Other Peers. - IPv6 DNS Entries Are Not Working. - Stale Peer On The Interface, When Forced Removed From Multiple Networks At Once. -- Can Still Ping Domain Name Even When DNS Toggle Is Switched Off. +- Can Still Ping The Domain Name Even When The DNS Toggle Is Switched Off. - WireGuard DNS issue on most flavours of Ubuntu 24.04 and some other newer Linux distributions. The issue is affecting the Remote Access Client (RAC) and the plain WireGuard external clients. Workaround can be found here https://help.netmaker.io/en/articles/9612016-extclient-rac-dns-issue-on-ubuntu-24-04. diff --git a/scripts/netmaker.default.env b/scripts/netmaker.default.env index 26bb1bb98..b8a65a1fa 100644 --- a/scripts/netmaker.default.env +++ b/scripts/netmaker.default.env @@ -90,4 +90,7 @@ EMAIL_SENDER_PASSWORD= PEER_UPDATE_BATCH=true # batch peer update size when PEER_UPDATE_BATCH is enabled PEER_UPDATE_BATCH_SIZE=50 - +# default domain for internal DNS lookup +DEFAULT_DOMAIN=netmaker.hosted +# managed dns setting, set to true to resolve dns entries on netmaker network +MANAGE_DNS=false diff --git a/scripts/nm-quick.sh b/scripts/nm-quick.sh index 473242520..232eb3f02 100755 --- a/scripts/nm-quick.sh +++ b/scripts/nm-quick.sh @@ -167,12 +167,12 @@ configure_netclient() { nmctl host update $HOST_ID --default sleep 5 nmctl node create_remote_access_gateway netmaker $NODE_ID - #setup failOver - sleep 5 - curl --location --request POST "https://api.${NETMAKER_BASE_DOMAIN}/api/v1/node/${NODE_ID}/failover" --header "Authorization: Bearer ${MASTER_KEY}" + sleep 2 # create network for internet access vpn if [ "$INSTALL_TYPE" = "pro" ]; then + #setup failOver + curl --location --request POST "https://api.${NETMAKER_BASE_DOMAIN}/api/v1/node/${NODE_ID}/failover" --header "Authorization: Bearer ${MASTER_KEY}" INET_NODE_ID=$(sudo cat /etc/netclient/nodes.json | jq -r '."internet-access-vpn".id') nmctl node create_remote_access_gateway internet-access-vpn $INET_NODE_ID out=$(nmctl node list -o json | jq -r '.[] | select(.id=='\"$INET_NODE_ID\"') | .ingressdns = "8.8.8.8"') @@ -181,7 +181,6 @@ configure_netclient() { curl --location --request PUT "https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/internet-access-vpn/${INET_NODE_ID}" --data "$out" --header "Authorization: Bearer ${MASTER_KEY}" curl --location --request POST "https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/internet-access-vpn/${INET_NODE_ID}/inet_gw" --data '{}' --header "Authorization: Bearer ${MASTER_KEY}" fi - set -e } @@ -593,52 +592,11 @@ set_install_vars() { done fi wait_seconds 1 - unset GET_MQ_USERNAME - unset GET_MQ_PASSWORD - unset CONFIRM_MQ_PASSWORD - echo "Enter Credentials For MQ..." - - read -p "MQ Username (click 'enter' to use 'netmaker'): " GET_MQ_USERNAME - if [ -z "$GET_MQ_USERNAME" ]; then - echo "using default username for mq" - MQ_USERNAME="netmaker" - else - MQ_USERNAME="$GET_MQ_USERNAME" - fi - - if test -z "$MQ_PASSWORD"; then - MQ_PASSWORD=$( + MQ_USERNAME="netmaker" + MQ_PASSWORD=$( tr -dc A-Za-z0-9