diff --git a/auth/auth.go b/auth/auth.go index d6ce8b4c..40657e53 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,15 +1,8 @@ package auth import ( - "encoding/base64" - "encoding/json" - "fmt" - - "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" "github.com/gravitl/netmaker/models" - "golang.org/x/crypto/bcrypt" - "golang.org/x/exp/slog" "golang.org/x/oauth2" ) @@ -22,88 +15,11 @@ var ( auth_provider *oauth2.Config ) -// IsOauthUser - returns -func IsOauthUser(user *models.User) error { - var currentValue, err = FetchPassValue("") - if err != nil { - return err - } - var bCryptErr = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(currentValue)) - return bCryptErr -} - -func FetchPassValue(newValue string) (string, error) { - - type valueHolder struct { - Value string `json:"value" bson:"value"` - } - newValueHolder := valueHolder{} - var currentValue, err = logic.FetchAuthSecret() - if err != nil { - return "", err - } - var unmarshErr = json.Unmarshal([]byte(currentValue), &newValueHolder) - if unmarshErr != nil { - return "", unmarshErr - } - - var b64CurrentValue, b64Err = base64.StdEncoding.DecodeString(newValueHolder.Value) - if b64Err != nil { - logger.Log(0, "could not decode pass") - return "", nil - } - return string(b64CurrentValue), nil -} - -// == private == - -func addUser(email string) error { - var hasSuperAdmin, err = logic.HasSuperAdmin() - if err != nil { - slog.Error("error checking for existence of admin user during OAuth login for", "email", email, "error", err) - return err - } // generate random password to adapt to current model - var newPass, fetchErr = FetchPassValue("") - if fetchErr != nil { - slog.Error("failed to get password", "error", fetchErr.Error()) - return fetchErr - } - var newUser = models.User{ - UserName: email, - Password: newPass, - } - if !hasSuperAdmin { // must be first attempt, create a superadmin - logger.Log(0, "creating superadmin") - if err = logic.CreateSuperAdmin(&newUser); err != nil { - slog.Error("error creating super admin from user", "email", email, "error", err) - } else { - slog.Info("superadmin created from user", "email", email) - } - } else { // otherwise add to db as admin..? - // TODO: add ability to add users with preemptive permissions - newUser.IsAdmin = false - if err = logic.CreateUser(&newUser); err != nil { - logger.Log(0, "error creating user,", email, "; user not added", "error", err.Error()) - } else { - logger.Log(0, "user created from ", email) - } - } - return nil -} - -func isUserIsAllowed(username, network string, shouldAddUser bool) (*models.User, error) { +func isUserIsAllowed(username, network string) (*models.User, error) { user, err := logic.GetUser(username) - if err != nil && shouldAddUser { // user must not exist, so try to make one - if err = addUser(username); err != nil { - logger.Log(0, "failed to add user", username, "during a node SSO network join on network", network) - // response := returnErrTemplate(user.UserName, "failed to add user", state, reqKeyIf) - // w.WriteHeader(http.StatusInternalServerError) - // w.Write(response) - return nil, fmt.Errorf("failed to add user to system") - } - logger.Log(0, "user", username, "was added during a node SSO network join on network", network) - user, _ = logic.GetUser(username) + if err != nil { // user must not exist, so try to make one + return &models.User{}, err } return user, nil diff --git a/auth/host_session.go b/auth/host_session.go index d6869ed0..62e9d438 100644 --- a/auth/host_session.go +++ b/auth/host_session.go @@ -85,24 +85,24 @@ func SessionHandler(conn *websocket.Conn) { return } req.Pass = req.Host.ID.String() - user, err := logic.GetUser(req.User) - if err != nil { - logger.Log(0, "failed to get user", req.User, "from database") - err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) - if err != nil { - logger.Log(0, "error during message writing:", err.Error()) - } - return - } - if !user.IsAdmin && !user.IsSuperAdmin { - logger.Log(0, "user", req.User, "is neither an admin or superadmin. denying registeration") - conn.WriteMessage(messageType, []byte("cannot register with a non-admin or non-superadmin")) - err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) - if err != nil { - logger.Log(0, "error during message writing:", err.Error()) - } - return - } + // user, err := logic.GetUser(req.User) + // if err != nil { + // logger.Log(0, "failed to get user", req.User, "from database") + // err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + // if err != nil { + // logger.Log(0, "error during message writing:", err.Error()) + // } + // return + // } + // if !user.IsAdmin && !user.IsSuperAdmin { + // logger.Log(0, "user", req.User, "is neither an admin or superadmin. denying registeration") + // conn.WriteMessage(messageType, []byte("cannot register with a non-admin or non-superadmin")) + // err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + // if err != nil { + // logger.Log(0, "error during message writing:", err.Error()) + // } + // return + // } if err = netcache.Set(stateStr, req); err != nil { // give the user's host access in the DB logger.Log(0, "machine failed to complete join on network,", registerMessage.Network, "-", err.Error()) @@ -197,7 +197,7 @@ func SessionHandler(conn *websocket.Conn) { for _, newNet := range currentNetworks { if !logic.StringSliceContains(hostNets, newNet) { if len(result.User) > 0 { - _, err := isUserIsAllowed(result.User, newNet, false) + _, err := isUserIsAllowed(result.User, newNet) if err != nil { logger.Log(0, "unauthorized user", result.User, "attempted to register to network", newNet) handleHostRegErr(conn, err) diff --git a/config/config.go b/config/config.go index 591c8e3d..4f390e37 100644 --- a/config/config.go +++ b/config/config.go @@ -94,6 +94,11 @@ type ServerConfig struct { CacheEnabled string `yaml:"caching_enabled"` EndpointDetection bool `json:"endpoint_detection"` AllowedEmailDomains string `yaml:"allowed_email_domains"` + EmailSenderAddr string `json:"email_sender_addr"` + EmailSenderAuth string `json:"email_sender_auth"` + EmailSenderType string `json:"email_sender_type"` + SmtpHost string `json:"smtp_host"` + SmtpPort int `json:"smtp_port"` MetricInterval string `yaml:"metric_interval"` } diff --git a/controllers/controller.go b/controllers/controller.go index 749baa06..75423b6e 100644 --- a/controllers/controller.go +++ b/controllers/controller.go @@ -17,7 +17,9 @@ import ( ) // HttpMiddlewares - middleware functions for REST interactions -var HttpMiddlewares []mux.MiddlewareFunc +var HttpMiddlewares = []mux.MiddlewareFunc{ + userMiddleWare, +} // HttpHandlers - handler functions for REST interactions var HttpHandlers = []interface{}{ @@ -39,7 +41,6 @@ func HandleRESTRequests(wg *sync.WaitGroup, ctx context.Context) { defer wg.Done() r := mux.NewRouter() - // Currently allowed dev origin is all. Should change in prod // should consider analyzing the allowed methods further headersOk := handlers.AllowedHeaders( diff --git a/controllers/ext_client.go b/controllers/ext_client.go index d5f16f60..b9825692 100644 --- a/controllers/ext_client.go +++ b/controllers/ext_client.go @@ -128,18 +128,6 @@ func getExtClient(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), client) { - // check if user has access to extclient - slog.Error("failed to get extclient", "network", network, "clientID", - clientid, "error", errors.New("access is denied")) - logic.ReturnErrorResponse( - w, - r, - logic.FormatError(errors.New("access is denied"), "forbidden"), - ) - return - - } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(client) @@ -170,16 +158,6 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), client) { - slog.Error("failed to get extclient", "network", networkid, "clientID", - clientid, "error", errors.New("access is denied")) - logic.ReturnErrorResponse( - w, - r, - logic.FormatError(errors.New("access is denied"), "forbidden"), - ) - return - } gwnode, err := logic.GetNodeByID(client.IngressGatewayID) if err != nil { @@ -445,12 +423,6 @@ func createExtClient(w http.ResponseWriter, r *http.Request) { return } userName = caller.UserName - if _, ok := caller.RemoteGwIDs[nodeid]; (!caller.IsAdmin && !caller.IsSuperAdmin) && !ok { - err = errors.New("permission denied") - slog.Error("failed to create extclient", "error", err) - logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden")) - return - } // check if user has a config already for remote access client extclients, err := logic.GetNetworkExtClients(node.Network) if err != nil { @@ -567,7 +539,6 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) { return } clientid := params["clientid"] - network := params["network"] oldExtClient, err := logic.GetExtClientByName(clientid) if err != nil { slog.Error( @@ -582,18 +553,6 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), oldExtClient) { - // check if user has access to extclient - slog.Error("failed to get extclient", "network", network, "clientID", - clientid, "error", errors.New("access is denied")) - logic.ReturnErrorResponse( - w, - r, - logic.FormatError(errors.New("access is denied"), "forbidden"), - ) - return - - } if oldExtClient.ClientID == update.ClientID { if err := validateCustomExtClient(&update, false); err != nil { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) @@ -729,16 +688,6 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), extclient) { - slog.Error("user not allowed to delete", "network", network, "clientID", - clientid, "error", errors.New("access is denied")) - logic.ReturnErrorResponse( - w, - r, - logic.FormatError(errors.New("access is denied"), "forbidden"), - ) - return - } ingressnode, err := logic.GetNodeByID(extclient.IngressGatewayID) if err != nil { logger.Log( diff --git a/controllers/hosts.go b/controllers/hosts.go index 7a5d3912..9a2d4bc4 100644 --- a/controllers/hosts.go +++ b/controllers/hosts.go @@ -79,12 +79,53 @@ func upgradeHost(w http.ResponseWriter, r *http.Request) { // @Success 200 {array} models.ApiHost // @Failure 500 {object} models.ErrorResponse func getHosts(w http.ResponseWriter, r *http.Request) { - currentHosts, err := logic.GetAllHosts() + w.Header().Set("Content-Type", "application/json") + currentHosts := []models.Host{} + username := r.Header.Get("user") + user, err := logic.GetUser(username) if err != nil { - logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error()) - logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } + userPlatformRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + return + } + respHostsMap := make(map[string]struct{}) + if !userPlatformRole.FullAccess { + nodes, err := logic.GetAllNodes() + if err != nil { + logger.Log(0, "error fetching all nodes info: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + filteredNodes := logic.GetFilteredNodesByUserAccess(*user, nodes) + if len(filteredNodes) > 0 { + currentHostsMap, err := logic.GetHostsMap() + if err != nil { + logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + for _, node := range filteredNodes { + if _, ok := respHostsMap[node.HostID.String()]; ok { + continue + } + if host, ok := currentHostsMap[node.HostID.String()]; ok { + currentHosts = append(currentHosts, host) + respHostsMap[host.ID.String()] = struct{}{} + } + } + + } + } else { + currentHosts, err = logic.GetAllHosts() + if err != nil { + logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + } + apiHosts := logic.GetAllHostsAPI(currentHosts[:]) logger.Log(2, r.Header.Get("user"), "fetched all hosts") logic.SortApiHosts(apiHosts[:]) @@ -194,6 +235,19 @@ 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 new file mode 100644 index 00000000..733db69d --- /dev/null +++ b/controllers/middleware.go @@ -0,0 +1,105 @@ +package controller + +import ( + "net/http" + "net/url" + "strings" + + "github.com/gorilla/mux" + "github.com/gravitl/netmaker/logger" + "github.com/gravitl/netmaker/logic" + "github.com/gravitl/netmaker/models" +) + +func userMiddleWare(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var params = mux.Vars(r) + route, err := mux.CurrentRoute(r).GetPathTemplate() + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + r.Header.Set("IS_GLOBAL_ACCESS", "no") + r.Header.Set("TARGET_RSRC", "") + r.Header.Set("RSRC_TYPE", "") + r.Header.Set("TARGET_RSRC_ID", "") + r.Header.Set("NET_ID", params["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, "users") { + r.Header.Set("TARGET_RSRC", models.UserRsrc.String()) + } + if strings.Contains(route, "ingress") { + r.Header.Set("TARGET_RSRC", models.RemoteAccessGwRsrc.String()) + } + if strings.Contains(route, "createrelay") || strings.Contains(route, "deleterelay") { + r.Header.Set("TARGET_RSRC", models.RelayRsrc.String()) + } + + if strings.Contains(route, "gateway") { + r.Header.Set("TARGET_RSRC", models.EgressGwRsrc.String()) + } + if strings.Contains(route, "networks") { + r.Header.Set("TARGET_RSRC", models.NetworkRsrc.String()) + } + if strings.Contains(route, "acls") { + r.Header.Set("TARGET_RSRC", models.AclRsrc.String()) + } + if strings.Contains(route, "extclients") { + r.Header.Set("TARGET_RSRC", models.ExtClientsRsrc.String()) + } + if strings.Contains(route, "enrollment-keys") { + r.Header.Set("TARGET_RSRC", models.EnrollmentKeysRsrc.String()) + } + if strings.Contains(route, "metrics") { + r.Header.Set("TARGET_RSRC", models.MetricRsrc.String()) + } + if keyID, ok := params["keyID"]; ok { + r.Header.Set("TARGET_RSRC_ID", keyID) + } + if nodeID, ok := params["nodeid"]; ok && r.Header.Get("TARGET_RSRC") != models.ExtClientsRsrc.String() { + r.Header.Set("TARGET_RSRC_ID", nodeID) + } + if strings.Contains(route, "failover") { + r.Header.Set("TARGET_RSRC", models.FailOverRsrc.String()) + nodeID := r.Header.Get("TARGET_RSRC_ID") + node, _ := logic.GetNodeByID(nodeID) + r.Header.Set("NET_ID", node.Network) + + } + if hostID, ok := params["hostid"]; ok { + r.Header.Set("TARGET_RSRC_ID", hostID) + } + if clientID, ok := params["clientid"]; ok { + r.Header.Set("TARGET_RSRC_ID", clientID) + } + if netID, ok := params["networkname"]; ok { + if !strings.Contains(route, "acls") { + r.Header.Set("TARGET_RSRC_ID", netID) + } + r.Header.Set("NET_ID", params["networkname"]) + } + + if userID, ok := params["username"]; ok { + r.Header.Set("TARGET_RSRC_ID", userID) + } else { + username, _ := url.QueryUnescape(r.URL.Query().Get("username")) + if username != "" { + r.Header.Set("TARGET_RSRC_ID", username) + } + } + if r.Header.Get("NET_ID") == "" && (r.Header.Get("TARGET_RSRC_ID") == "" || + r.Header.Get("TARGET_RSRC") == models.EnrollmentKeysRsrc.String() || + 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")) + logger.Log(0, "URL ------> ", route) + handler.ServeHTTP(w, r) + }) +} diff --git a/controllers/network.go b/controllers/network.go index fd8b95de..c94017f2 100644 --- a/controllers/network.go +++ b/controllers/network.go @@ -58,7 +58,13 @@ func getNetworks(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - + 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) logger.Log(2, r.Header.Get("user"), "fetched networks.") logic.SortNetworks(allnetworks[:]) w.WriteHeader(http.StatusOK) @@ -402,6 +408,7 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, errtype)) return } + go logic.DeleteNetworkRoles(network) //delete network from allocated ip map go logic.RemoveNetworkFromAllocatedIpMap(network) @@ -476,6 +483,7 @@ func createNetwork(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) return } + logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID)) //add new network to allocated ip map go logic.AddNetworkToAllocatedIpMap(network.NetID) diff --git a/controllers/network_test.go b/controllers/network_test.go index aed9e288..4320b6e3 100644 --- a/controllers/network_test.go +++ b/controllers/network_test.go @@ -26,9 +26,9 @@ func TestMain(m *testing.M) { database.InitializeDatabase() defer database.CloseDB() logic.CreateSuperAdmin(&models.User{ - UserName: "admin", - Password: "password", - IsAdmin: true, + UserName: "admin", + Password: "password", + PlatformRoleID: models.SuperAdminRole, }) peerUpdate := make(chan *models.Node) go logic.ManageZombies(context.Background(), peerUpdate) diff --git a/controllers/node.go b/controllers/node.go index f7962d2e..3790e2f4 100644 --- a/controllers/node.go +++ b/controllers/node.go @@ -21,24 +21,15 @@ var hostIDHeader = "host-id" func nodeHandlers(r *mux.Router) { - r.HandleFunc("/api/nodes", Authorize(false, false, "user", http.HandlerFunc(getAllNodes))). - Methods(http.MethodGet) - r.HandleFunc("/api/nodes/{network}", Authorize(false, true, "network", http.HandlerFunc(getNetworkNodes))). - Methods(http.MethodGet) - r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(getNode))). - Methods(http.MethodGet) - r.HandleFunc("/api/nodes/{network}/{nodeid}", logic.SecurityCheck(true, http.HandlerFunc(updateNode))). - Methods(http.MethodPut) - r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(deleteNode))). - Methods(http.MethodDelete) - r.HandleFunc("/api/nodes/{network}/{nodeid}/creategateway", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceEgress, http.HandlerFunc(createEgressGateway)))). - Methods(http.MethodPost) - r.HandleFunc("/api/nodes/{network}/{nodeid}/deletegateway", logic.SecurityCheck(true, http.HandlerFunc(deleteEgressGateway))). - Methods(http.MethodDelete) - r.HandleFunc("/api/nodes/{network}/{nodeid}/createingress", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceIngress, http.HandlerFunc(createIngressGateway)))). - Methods(http.MethodPost) - r.HandleFunc("/api/nodes/{network}/{nodeid}/deleteingress", logic.SecurityCheck(true, http.HandlerFunc(deleteIngressGateway))). - Methods(http.MethodDelete) + r.HandleFunc("/api/nodes", logic.SecurityCheck(true, http.HandlerFunc(getAllNodes))).Methods(http.MethodGet) + r.HandleFunc("/api/nodes/{network}", logic.SecurityCheck(true, http.HandlerFunc(getNetworkNodes))).Methods(http.MethodGet) + r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(getNode))).Methods(http.MethodGet) + r.HandleFunc("/api/nodes/{network}/{nodeid}", logic.SecurityCheck(true, http.HandlerFunc(updateNode))).Methods(http.MethodPut) + r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(deleteNode))).Methods(http.MethodDelete) + r.HandleFunc("/api/nodes/{network}/{nodeid}/creategateway", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceEgress, http.HandlerFunc(createEgressGateway)))).Methods(http.MethodPost) + r.HandleFunc("/api/nodes/{network}/{nodeid}/deletegateway", logic.SecurityCheck(true, http.HandlerFunc(deleteEgressGateway))).Methods(http.MethodDelete) + r.HandleFunc("/api/nodes/{network}/{nodeid}/createingress", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceIngress, http.HandlerFunc(createIngressGateway)))).Methods(http.MethodPost) + r.HandleFunc("/api/nodes/{network}/{nodeid}/deleteingress", logic.SecurityCheck(true, http.HandlerFunc(deleteIngressGateway))).Methods(http.MethodDelete) r.HandleFunc("/api/nodes/adm/{network}/authenticate", authenticate).Methods(http.MethodPost) r.HandleFunc("/api/v1/nodes/migrate", migrate).Methods(http.MethodPost) } @@ -277,6 +268,61 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } + username := r.Header.Get("user") + user, err := logic.GetUser(username) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + userPlatformRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + filteredNodes := []models.Node{} + if !userPlatformRole.FullAccess { + nodesMap := make(map[string]struct{}) + networkRoles := user.NetworkRoles[models.NetworkID(networkName)] + for networkRoleID := range networkRoles { + userPermTemplate, err := logic.GetRole(networkRoleID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + if userPermTemplate.FullAccess { + break + } + if rsrcPerms, ok := userPermTemplate.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok { + if _, ok := rsrcPerms[models.AllRemoteAccessGwRsrcID]; ok { + for _, node := range nodes { + if _, ok := nodesMap[node.ID.String()]; ok { + continue + } + if node.IsIngressGateway { + nodesMap[node.ID.String()] = struct{}{} + filteredNodes = append(filteredNodes, node) + } + } + } else { + for gwID, scope := range rsrcPerms { + if _, ok := nodesMap[gwID.String()]; ok { + continue + } + if scope.Read { + gwNode, err := logic.GetNodeByID(gwID.String()) + if err == nil && gwNode.IsIngressGateway { + filteredNodes = append(filteredNodes, gwNode) + } + } + } + } + } + + } + } + if len(filteredNodes) > 0 { + nodes = filteredNodes + } // returns all the nodes in JSON/API format apiNodes := logic.GetAllNodesAPI(nodes[:]) @@ -294,22 +340,26 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) { // Not quite sure if this is necessary. Probably necessary based on front end but may want to review after iteration 1 if it's being used or not func getAllNodes(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - user, err := logic.GetUser(r.Header.Get("user")) - if err != nil && r.Header.Get("ismasterkey") != "yes" { - logger.Log(0, r.Header.Get("user"), - "error fetching user info: ", err.Error()) + var nodes []models.Node + nodes, err := logic.GetAllNodes() + if err != nil { + logger.Log(0, "error fetching all nodes info: ", err.Error()) logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - var nodes []models.Node - if user.IsAdmin || r.Header.Get("ismasterkey") == "yes" { - nodes, err = logic.GetAllNodes() - if err != nil { - logger.Log(0, "error fetching all nodes info: ", err.Error()) - logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) - return - } + username := r.Header.Get("user") + user, err := logic.GetUser(username) + if err != nil { + return } + userPlatformRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + return + } + if !userPlatformRole.FullAccess { + nodes = logic.GetFilteredNodesByUserAccess(*user, 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") @@ -561,25 +611,6 @@ func deleteIngressGateway(w http.ResponseWriter, r *http.Request) { return } - if servercfg.IsPro { - go func() { - users, err := logic.GetUsersDB() - if err == nil { - for _, user := range users { - if _, ok := user.RemoteGwIDs[nodeid]; ok { - delete(user.RemoteGwIDs, nodeid) - err = logic.UpsertUser(user) - if err != nil { - slog.Error("failed to get user", "user", user.UserName, "error", err) - } - } - } - } else { - slog.Error("failed to get users", "error", err) - } - }() - } - apiNode := node.ConvertToAPINode() logger.Log(1, r.Header.Get("user"), "deleted ingress gateway", nodeid) w.WriteHeader(http.StatusOK) diff --git a/controllers/server.go b/controllers/server.go index f2a059ad..84f732b9 100644 --- a/controllers/server.go +++ b/controllers/server.go @@ -38,10 +38,10 @@ func serverHandlers(r *mux.Router) { ).Methods(http.MethodPost) r.HandleFunc("/api/server/getconfig", allowUsers(http.HandlerFunc(getConfig))). Methods(http.MethodGet) - r.HandleFunc("/api/server/getserverinfo", Authorize(true, false, "node", http.HandlerFunc(getServerInfo))). + r.HandleFunc("/api/server/getserverinfo", logic.SecurityCheck(true, http.HandlerFunc(getServerInfo))). Methods(http.MethodGet) r.HandleFunc("/api/server/status", getStatus).Methods(http.MethodGet) - r.HandleFunc("/api/server/usage", Authorize(true, false, "user", http.HandlerFunc(getUsage))). + r.HandleFunc("/api/server/usage", logic.SecurityCheck(false, http.HandlerFunc(getUsage))). Methods(http.MethodGet) } diff --git a/controllers/user.go b/controllers/user.go index 3b5c67b9..72925bc2 100644 --- a/controllers/user.go +++ b/controllers/user.go @@ -5,11 +5,12 @@ import ( "errors" "fmt" "net/http" + "net/url" + "reflect" "github.com/gorilla/mux" "github.com/gorilla/websocket" "github.com/gravitl/netmaker/auth" - "github.com/gravitl/netmaker/database" "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" "github.com/gravitl/netmaker/models" @@ -28,24 +29,12 @@ func userHandlers(r *mux.Router) { r.HandleFunc("/api/users/adm/transfersuperadmin/{username}", logic.SecurityCheck(true, http.HandlerFunc(transferSuperAdmin))). Methods(http.MethodPost) r.HandleFunc("/api/users/adm/authenticate", authenticateUser).Methods(http.MethodPost) - r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(updateUser))). - Methods(http.MethodPut) - r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceUsers, http.HandlerFunc(createUser)))). - Methods(http.MethodPost) - r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(deleteUser))). - Methods(http.MethodDelete) - r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))). - Methods(http.MethodGet) - r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))). - Methods(http.MethodGet) - r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(getPendingUsers))). - Methods(http.MethodGet) - r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(deleteAllPendingUsers))). - Methods(http.MethodDelete) - r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(deletePendingUser))). - Methods(http.MethodDelete) - r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(approvePendingUser))). - Methods(http.MethodPost) + r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(updateUser))).Methods(http.MethodPut) + r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceUsers, http.HandlerFunc(createUser)))).Methods(http.MethodPost) + r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(deleteUser))).Methods(http.MethodDelete) + r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).Methods(http.MethodGet) + r.HandleFunc("/api/v1/users", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserV1)))).Methods(http.MethodGet) + r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))).Methods(http.MethodGet) } @@ -94,14 +83,24 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) { logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized")) return } - if !(user.IsAdmin || user.IsSuperAdmin) { - logic.ReturnErrorResponse( - response, - request, - logic.FormatError(errors.New("only admins can access dashboard"), "unauthorized"), - ) + role, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized")) return } + if role.DenyDashboardAccess { + logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized")) + return + } + } + user, err := logic.GetUser(authRequest.UserName) + if err != nil { + logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized")) + return + } + if logic.IsOauthUser(user) == nil { + logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("user is registered via SSO"), "badrequest")) + return } username := authRequest.UserName jwt, err := logic.VerifyAuthRequest(authRequest) @@ -224,11 +223,55 @@ func getUser(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(user) } -// @Summary Get all users -// @Router /api/users [get] -// @Tags Users -// @Success 200 {array} models.User -// @Failure 500 {object} models.ErrorResponse +// swagger:route GET /api/v1/users user getUserV1 +// +// Get an individual user with role info. +// +// Schemes: https +// +// Security: +// oauth +// +// Responses: +// 200: ReturnUserWithRolesAndGroups +func getUserV1(w http.ResponseWriter, r *http.Request) { + // set header. + w.Header().Set("Content-Type", "application/json") + usernameFetched, _ := url.QueryUnescape(r.URL.Query().Get("username")) + if usernameFetched == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), "badrequest")) + return + } + user, err := logic.GetReturnUser(usernameFetched) + if err != nil { + logger.Log(0, usernameFetched, "failed to fetch user: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + userRoleTemplate, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + resp := models.ReturnUserWithRolesAndGroups{ + ReturnUser: user, + PlatformRole: userRoleTemplate, + } + logger.Log(2, r.Header.Get("user"), "fetched user", usernameFetched) + logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched user with role info") +} + +// swagger:route GET /api/users user getUsers +// +// Get all users. +// +// Schemes: https +// +// Security: +// oauth +// +// Responses: +// 200: userBodyResponse func getUsers(w http.ResponseWriter, r *http.Request) { // set header. w.Header().Set("Content-Type", "application/json") @@ -297,15 +340,8 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) { if err != nil { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) } - if !caller.IsSuperAdmin { - logic.ReturnErrorResponse( - w, - r, - logic.FormatError( - errors.New("only superadmin can assign the superadmin role to another user"), - "forbidden", - ), - ) + if caller.PlatformRoleID != models.SuperAdminRole { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only superadmin can assign the superadmin role to another user"), "forbidden")) return } var params = mux.Vars(r) @@ -316,15 +352,8 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) return } - if !u.IsAdmin { - logic.ReturnErrorResponse( - w, - r, - logic.FormatError( - errors.New("only admins can be promoted to superadmin role"), - "forbidden", - ), - ) + if u.PlatformRoleID != models.AdminRole { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only admins can be promoted to superadmin role"), "forbidden")) return } if !servercfg.IsBasicAuthEnabled() { @@ -336,16 +365,14 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) { return } - u.IsSuperAdmin = true - u.IsAdmin = false + u.PlatformRoleID = models.SuperAdminRole err = logic.UpsertUser(*u) if err != nil { slog.Error("error updating user to superadmin: ", "user", u.UserName, "error", err.Error()) logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - caller.IsSuperAdmin = false - caller.IsAdmin = true + caller.PlatformRoleID = models.AdminRole err = logic.UpsertUser(*caller) if err != nil { slog.Error("error demoting user to admin: ", "user", caller.UserName, "error", err.Error()) @@ -369,7 +396,7 @@ func createUser(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") caller, err := logic.GetUser(r.Header.Get("user")) if err != nil { - logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) return } var user models.User @@ -380,27 +407,34 @@ func createUser(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) return } - if !caller.IsSuperAdmin && user.IsAdmin { - err = errors.New("only superadmin can create admin users") - slog.Error("error creating new user: ", "user", user.UserName, "error", err) - logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden")) + + if user.PlatformRoleID == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("platform role is missing"), "badrequest")) return } - if user.IsSuperAdmin { + userRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + err = errors.New("error fetching role " + user.PlatformRoleID.String() + " " + err.Error()) + slog.Error("error creating new user: ", "user", user.UserName, "error", err) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + if userRole.ID == models.SuperAdminRole { err = errors.New("additional superadmins cannot be created") slog.Error("error creating new user: ", "user", user.UserName, "error", err) logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden")) return } - if !servercfg.IsPro && !user.IsAdmin { - logic.ReturnErrorResponse( - w, - r, - logic.FormatError( - errors.New("non-admins users can only be created on Pro version"), - "forbidden", - ), - ) + + if caller.PlatformRoleID != models.SuperAdminRole && user.PlatformRoleID == models.AdminRole { + err = errors.New("only superadmin can create admin users") + slog.Error("error creating new user: ", "user", user.UserName, "error", err) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden")) + return + } + + if !servercfg.IsPro && user.PlatformRoleID != models.AdminRole { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("non-admins users can only be created on Pro version"), "forbidden")) return } @@ -410,6 +444,8 @@ func createUser(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) return } + logic.DeleteUserInvite(user.UserName) + logic.DeletePendingUser(user.UserName) slog.Info("user was created", "username", user.UserName) json.NewEncoder(w).Encode(logic.ToReturnUser(user)) } @@ -472,55 +508,22 @@ func updateUser(w http.ResponseWriter, r *http.Request) { } if !ismaster && !selfUpdate { - if caller.IsAdmin && user.IsSuperAdmin { - slog.Error( - "non-superadmin user", - "caller", - caller.UserName, - "attempted to update superadmin user", - username, - ) - logic.ReturnErrorResponse( - w, - r, - logic.FormatError(errors.New("cannot update superadmin user"), "forbidden"), - ) + if caller.PlatformRoleID == models.AdminRole && user.PlatformRoleID == models.SuperAdminRole { + slog.Error("non-superadmin user", "caller", caller.UserName, "attempted to update superadmin user", username) + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update superadmin user"), "forbidden")) return } - if !caller.IsAdmin && !caller.IsSuperAdmin { - slog.Error( - "operation not allowed", - "caller", - caller.UserName, - "attempted to update user", - username, - ) - logic.ReturnErrorResponse( - w, - r, - logic.FormatError(errors.New("cannot update superadmin user"), "forbidden"), - ) + if caller.PlatformRoleID != models.AdminRole && caller.PlatformRoleID != models.SuperAdminRole { + slog.Error("operation not allowed", "caller", caller.UserName, "attempted to update user", username) + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update superadmin user"), "forbidden")) return } - if caller.IsAdmin && user.IsAdmin { - slog.Error( - "admin user cannot update another admin", - "caller", - caller.UserName, - "attempted to update admin user", - username, - ) - logic.ReturnErrorResponse( - w, - r, - logic.FormatError( - errors.New("admin user cannot update another admin"), - "forbidden", - ), - ) + if caller.PlatformRoleID == models.AdminRole && user.PlatformRoleID == models.AdminRole { + slog.Error("admin user cannot update another admin", "caller", caller.UserName, "attempted to update admin user", username) + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("admin user cannot update another admin"), "forbidden")) return } - if caller.IsAdmin && userchange.IsAdmin { + if caller.PlatformRoleID == models.AdminRole && userchange.PlatformRoleID == models.AdminRole { err = errors.New("admin user cannot update role of an another user to admin") slog.Error( "failed to update user", @@ -537,45 +540,39 @@ func updateUser(w http.ResponseWriter, r *http.Request) { } if !ismaster && selfUpdate { - if user.IsAdmin != userchange.IsAdmin || user.IsSuperAdmin != userchange.IsSuperAdmin { - slog.Error( - "user cannot change his own role", - "caller", - caller.UserName, - "attempted to update user role", - username, - ) - logic.ReturnErrorResponse( - w, - r, - logic.FormatError(errors.New("user not allowed to self assign role"), "forbidden"), - ) + if user.PlatformRoleID != userchange.PlatformRoleID { + slog.Error("user cannot change his own role", "caller", caller.UserName, "attempted to update user role", username) + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not allowed to self assign role"), "forbidden")) return } + if servercfg.IsPro { + // user cannot update his own roles and groups + if len(user.NetworkRoles) != len(userchange.NetworkRoles) || !reflect.DeepEqual(user.NetworkRoles, userchange.NetworkRoles) { + err = errors.New("user cannot update self update their network roles") + slog.Error("failed to update user", "caller", caller.UserName, "attempted to update user", username, "error", err) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden")) + return + } + // user cannot update his own roles and groups + if len(user.UserGroups) != len(userchange.UserGroups) || !reflect.DeepEqual(user.UserGroups, userchange.UserGroups) { + err = errors.New("user cannot update self update their groups") + slog.Error("failed to update user", "caller", caller.UserName, "attempted to update user", username, "error", err) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden")) + return + } + } + } if ismaster { - if !user.IsSuperAdmin && userchange.IsSuperAdmin { - slog.Error( - "operation not allowed", - "caller", - logic.MasterUser, - "attempted to update user role to superadmin", - username, - ) - logic.ReturnErrorResponse( - w, - r, - logic.FormatError( - errors.New("attempted to update user role to superadmin"), - "forbidden", - ), - ) + if user.PlatformRoleID != models.SuperAdminRole && userchange.PlatformRoleID == models.SuperAdminRole { + slog.Error("operation not allowed", "caller", logic.MasterUser, "attempted to update user role to superadmin", username) + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("attempted to update user role to superadmin"), "forbidden")) return } } - if auth.IsOauthUser(user) == nil && userchange.Password != "" { + if logic.IsOauthUser(user) == nil && userchange.Password != "" { err := fmt.Errorf("cannot update user's password for an oauth user %s", username) logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden")) return @@ -608,6 +605,12 @@ func deleteUser(w http.ResponseWriter, r *http.Request) { if err != nil { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) } + callerUserRole, err := logic.GetRole(caller.PlatformRoleID) + if err != nil { + slog.Error("failed to get role ", "role", callerUserRole.ID, "error", err) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } username := params["username"] user, err := logic.GetUser(username) if err != nil { @@ -616,7 +619,13 @@ func deleteUser(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - if user.IsSuperAdmin { + userRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + slog.Error("failed to get role ", "role", userRole.ID, "error", err) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + if userRole.ID == models.SuperAdminRole { slog.Error( "failed to delete user: ", "user", username, "error", "superadmin cannot be deleted") logic.ReturnErrorResponse( @@ -626,8 +635,8 @@ func deleteUser(w http.ResponseWriter, r *http.Request) { ) return } - if !caller.IsSuperAdmin { - if caller.IsAdmin && user.IsAdmin { + if callerUserRole.ID != models.SuperAdminRole { + if callerUserRole.ID == models.AdminRole && userRole.ID == models.AdminRole { slog.Error( "failed to delete user: ", "user", @@ -667,10 +676,14 @@ func deleteUser(w http.ResponseWriter, r *http.Request) { } for _, extclient := range extclients { if extclient.OwnerID == user.UserName { - err = logic.DeleteExtClient(extclient.Network, extclient.ClientID) + err = logic.DeleteExtClientAndCleanup(extclient) if err != nil { slog.Error("failed to delete extclient", - "id", extclient.ClientID, "owner", user.UserName, "error", err) + "id", extclient.ClientID, "owner", username, "error", err) + } else { + if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil { + slog.Error("error setting ext peers: " + err.Error()) + } } } } @@ -697,139 +710,3 @@ func socketHandler(w http.ResponseWriter, r *http.Request) { // Start handling the session go auth.SessionHandler(conn) } - -// @Summary Get all pending users -// @Router /api/users_pending [get] -// @Tags Users -// @Success 200 {array} models.User -// @Failure 500 {object} models.ErrorResponse -func getPendingUsers(w http.ResponseWriter, r *http.Request) { - // set header. - w.Header().Set("Content-Type", "application/json") - - users, err := logic.ListPendingUsers() - if err != nil { - logger.Log(0, "failed to fetch users: ", err.Error()) - logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) - return - } - - logic.SortUsers(users[:]) - logger.Log(2, r.Header.Get("user"), "fetched pending users") - json.NewEncoder(w).Encode(users) -} - -// @Summary Approve a pending user -// @Router /api/users_pending/user/{username} [post] -// @Tags Users -// @Param username path string true "Username of the pending user to approve" -// @Success 200 {string} string -// @Failure 500 {object} models.ErrorResponse -func approvePendingUser(w http.ResponseWriter, r *http.Request) { - // set header. - w.Header().Set("Content-Type", "application/json") - var params = mux.Vars(r) - username := params["username"] - users, err := logic.ListPendingUsers() - - if err != nil { - logger.Log(0, "failed to fetch users: ", err.Error()) - logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) - return - } - for _, user := range users { - if user.UserName == username { - var newPass, fetchErr = auth.FetchPassValue("") - if fetchErr != nil { - logic.ReturnErrorResponse(w, r, logic.FormatError(fetchErr, "internal")) - return - } - if err = logic.CreateUser(&models.User{ - UserName: user.UserName, - Password: newPass, - }); err != nil { - logic.ReturnErrorResponse( - w, - r, - logic.FormatError(fmt.Errorf("failed to create user: %s", err), "internal"), - ) - return - } - err = logic.DeletePendingUser(username) - if err != nil { - logic.ReturnErrorResponse( - w, - r, - logic.FormatError( - fmt.Errorf("failed to delete pending user: %s", err), - "internal", - ), - ) - return - } - break - } - } - logic.ReturnSuccessResponse(w, r, "approved "+username) -} - -// @Summary Delete a pending user -// @Router /api/users_pending/user/{username} [delete] -// @Tags Users -// @Param username path string true "Username of the pending user to delete" -// @Success 200 {string} string -// @Failure 500 {object} models.ErrorResponse -func deletePendingUser(w http.ResponseWriter, r *http.Request) { - // set header. - w.Header().Set("Content-Type", "application/json") - var params = mux.Vars(r) - username := params["username"] - users, err := logic.ListPendingUsers() - - if err != nil { - logger.Log(0, "failed to fetch users: ", err.Error()) - logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) - return - } - for _, user := range users { - if user.UserName == username { - err = logic.DeletePendingUser(username) - if err != nil { - logic.ReturnErrorResponse( - w, - r, - logic.FormatError( - fmt.Errorf("failed to delete pending user: %s", err), - "internal", - ), - ) - return - } - break - } - } - logic.ReturnSuccessResponse(w, r, "deleted pending "+username) -} - -// @Summary Delete all pending users -// @Router /api/users_pending [delete] -// @Tags Users -// @Success 200 {string} string -// @Failure 500 {object} models.ErrorResponse -func deleteAllPendingUsers(w http.ResponseWriter, r *http.Request) { - // set header. - w.Header().Set("Content-Type", "application/json") - err := database.DeleteAllRecords(database.PENDING_USERS_TABLE_NAME) - if err != nil { - logic.ReturnErrorResponse( - w, - r, - logic.FormatError( - errors.New("failed to delete all pending users "+err.Error()), - "internal", - ), - ) - return - } - logic.ReturnSuccessResponse(w, r, "cleared all pending users") -} diff --git a/controllers/user_test.go b/controllers/user_test.go index b1517ac4..6c65fa38 100644 --- a/controllers/user_test.go +++ b/controllers/user_test.go @@ -66,7 +66,7 @@ func prepareUserRequest(t *testing.T, userForBody models.User, userNameForParam func haveOnlyOneUser(t *testing.T, user models.User) { deleteAllUsers(t) var err error - if user.IsSuperAdmin { + if user.PlatformRoleID == models.SuperAdminRole { err = logic.CreateSuperAdmin(&user) } else { err = logic.CreateUser(&user) @@ -104,7 +104,7 @@ func TestHasSuperAdmin(t *testing.T) { assert.False(t, found) }) t.Run("superadmin user", func(t *testing.T) { - var user = models.User{UserName: "superadmin", Password: "password", IsSuperAdmin: true} + var user = models.User{UserName: "superadmin", Password: "password", PlatformRoleID: models.SuperAdminRole} err := logic.CreateUser(&user) assert.Nil(t, err) found, err := logic.HasSuperAdmin() @@ -112,7 +112,7 @@ func TestHasSuperAdmin(t *testing.T) { assert.True(t, found) }) t.Run("multiple superadmins", func(t *testing.T) { - var user = models.User{UserName: "superadmin1", Password: "password", IsSuperAdmin: true} + var user = models.User{UserName: "superadmin1", Password: "password", PlatformRoleID: models.SuperAdminRole} err := logic.CreateUser(&user) assert.Nil(t, err) found, err := logic.HasSuperAdmin() @@ -123,7 +123,7 @@ func TestHasSuperAdmin(t *testing.T) { func TestCreateUser(t *testing.T) { deleteAllUsers(t) - user := models.User{UserName: "admin", Password: "password", IsAdmin: true} + user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole} t.Run("NoUser", func(t *testing.T) { err := logic.CreateUser(&user) assert.Nil(t, err) @@ -161,7 +161,7 @@ func TestDeleteUser(t *testing.T) { assert.False(t, deleted) }) t.Run("Existing User", func(t *testing.T) { - user := models.User{UserName: "admin", Password: "password", IsAdmin: true} + user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole} if err := logic.CreateUser(&user); err != nil { t.Fatal(err) } @@ -221,7 +221,7 @@ func TestValidateUser(t *testing.T) { func TestGetUser(t *testing.T) { deleteAllUsers(t) - user := models.User{UserName: "admin", Password: "password", IsAdmin: true} + user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole} t.Run("NonExistantUser", func(t *testing.T) { admin, err := logic.GetUser("admin") @@ -241,8 +241,8 @@ func TestGetUser(t *testing.T) { func TestGetUsers(t *testing.T) { deleteAllUsers(t) - adminUser := models.User{UserName: "admin", Password: "password", IsAdmin: true} - user := models.User{UserName: "admin", Password: "password", IsAdmin: false} + adminUser := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole} + user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole} t.Run("NonExistantUser", func(t *testing.T) { admin, err := logic.GetUsers() @@ -269,7 +269,7 @@ func TestGetUsers(t *testing.T) { assert.Equal(t, true, u.IsAdmin) } else { assert.Equal(t, user.UserName, u.UserName) - assert.Equal(t, user.IsAdmin, u.IsAdmin) + assert.Equal(t, user.PlatformRoleID, u.PlatformRoleID) } } }) @@ -278,8 +278,8 @@ func TestGetUsers(t *testing.T) { func TestUpdateUser(t *testing.T) { deleteAllUsers(t) - user := models.User{UserName: "admin", Password: "password", IsAdmin: true} - newuser := models.User{UserName: "hello", Password: "world", IsAdmin: true} + user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole} + newuser := models.User{UserName: "hello", Password: "world", PlatformRoleID: models.AdminRole} t.Run("NonExistantUser", func(t *testing.T) { admin, err := logic.UpdateUser(&newuser, &user) assert.EqualError(t, err, "could not find any records") @@ -322,7 +322,7 @@ func TestUpdateUser(t *testing.T) { func TestVerifyAuthRequest(t *testing.T) { deleteAllUsers(t) - user := models.User{UserName: "admin", Password: "password", IsSuperAdmin: false, IsAdmin: true} + user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole} var authRequest models.UserAuthParams t.Run("EmptyUserName", func(t *testing.T) { authRequest.UserName = "" @@ -346,7 +346,7 @@ func TestVerifyAuthRequest(t *testing.T) { assert.EqualError(t, err, "incorrect credentials") }) t.Run("Non-Admin", func(t *testing.T) { - user.IsAdmin = false + user.PlatformRoleID = models.ServiceUser user.Password = "somepass" user.UserName = "nonadmin" if err := logic.CreateUser(&user); err != nil { diff --git a/database/database.go b/database/database.go index dd2b2af1..f8508b3f 100644 --- a/database/database.go +++ b/database/database.go @@ -25,6 +25,8 @@ const ( DELETED_NODES_TABLE_NAME = "deletednodes" // USERS_TABLE_NAME - users table USERS_TABLE_NAME = "users" + // USER_PERMISSIONS_TABLE_NAME - user permissions table + USER_PERMISSIONS_TABLE_NAME = "user_permissions" // CERTS_TABLE_NAME - certificates table CERTS_TABLE_NAME = "certs" // DNS_TABLE_NAME - dns table @@ -63,6 +65,8 @@ const ( HOST_ACTIONS_TABLE_NAME = "hostactions" // PENDING_USERS_TABLE_NAME - table name for pending users PENDING_USERS_TABLE_NAME = "pending_users" + // USER_INVITES - table for user invites + USER_INVITES_TABLE_NAME = "user_invites" // == ERROR CONSTS == // NO_RECORD - no singular result found NO_RECORD = "no result found" @@ -146,6 +150,8 @@ func createTables() { CreateTable(ENROLLMENT_KEYS_TABLE_NAME) CreateTable(HOST_ACTIONS_TABLE_NAME) CreateTable(PENDING_USERS_TABLE_NAME) + CreateTable(USER_PERMISSIONS_TABLE_NAME) + CreateTable(USER_INVITES_TABLE_NAME) } func CreateTable(tableName string) error { diff --git a/functions/helpers_test.go b/functions/helpers_test.go index 4dc3d529..ecc34330 100644 --- a/functions/helpers_test.go +++ b/functions/helpers_test.go @@ -26,9 +26,9 @@ func TestMain(m *testing.M) { database.InitializeDatabase() defer database.CloseDB() logic.CreateSuperAdmin(&models.User{ - UserName: "superadmin", - Password: "password", - IsSuperAdmin: true, + UserName: "superadmin", + Password: "password", + PlatformRoleID: models.SuperAdminRole, }) peerUpdate := make(chan *models.Node) go logic.ManageZombies(context.Background(), peerUpdate) diff --git a/go.mod b/go.mod index b2bbb9ba..092cb923 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,9 @@ require ( github.com/guumaster/tablewriter v0.0.10 github.com/matryer/is v1.4.1 github.com/olekukonko/tablewriter v0.0.5 + github.com/resendlabs/resend-go v1.7.0 github.com/spf13/cobra v1.8.1 + gopkg.in/mail.v2 v2.3.1 ) require ( @@ -52,6 +54,7 @@ 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 + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect ) require ( diff --git a/go.sum b/go.sum index 3d5ac3a6..adad40a3 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posthog/posthog-go v1.2.18 h1:2CBA0LOB0up+gon+xpeXuhFw69gZpjAYxQoBBGwiDWw= github.com/posthog/posthog-go v1.2.18/go.mod h1:QjlpryJtfYLrZF2GUkAhejH4E7WlDbdKkvOi5hLmkdg= +github.com/resendlabs/resend-go v1.7.0 h1:DycOqSXtw2q7aB+Nt9DDJUDtaYcrNPGn1t5RFposas0= +github.com/resendlabs/resend-go v1.7.0/go.mod h1:yip1STH7Bqfm4fD0So5HgyNbt5taG5Cplc4xXxETyLI= 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= @@ -137,8 +139,12 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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.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/logic/auth.go b/logic/auth.go index 87fc3095..3e2623ff 100644 --- a/logic/auth.go +++ b/logic/auth.go @@ -49,8 +49,7 @@ func HasSuperAdmin() (bool, error) { if err != nil { continue } - if user.IsSuperAdmin { - superUser = user + if user.PlatformRoleID == models.SuperAdminRole || user.IsSuperAdmin { return true, nil } } @@ -106,18 +105,58 @@ func GetUsers() ([]models.ReturnUser, error) { return users, err } +// IsOauthUser - returns +func IsOauthUser(user *models.User) error { + var currentValue, err = FetchPassValue("") + if err != nil { + return err + } + var bCryptErr = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(currentValue)) + return bCryptErr +} + +func FetchPassValue(newValue string) (string, error) { + + type valueHolder struct { + Value string `json:"value" bson:"value"` + } + newValueHolder := valueHolder{} + var currentValue, err = FetchAuthSecret() + if err != nil { + return "", err + } + var unmarshErr = json.Unmarshal([]byte(currentValue), &newValueHolder) + if unmarshErr != nil { + return "", unmarshErr + } + + var b64CurrentValue, b64Err = base64.StdEncoding.DecodeString(newValueHolder.Value) + if b64Err != nil { + logger.Log(0, "could not decode pass") + return "", nil + } + return string(b64CurrentValue), nil +} + // CreateUser - creates a user func CreateUser(user *models.User) error { // check if user exists if _, err := GetUser(user.UserName); err == nil { return errors.New("user exists") } + SetUserDefaults(user) + if err := IsGroupsValid(user.UserGroups); err != nil { + return errors.New("invalid groups: " + err.Error()) + } + if err := IsNetworkRolesValid(user.NetworkRoles); err != nil { + return errors.New("invalid network roles: " + err.Error()) + } + var err = ValidateUser(user) if err != nil { logger.Log(0, "failed to validate user", err.Error()) return err } - // encrypt that password so we never see it again hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 5) if err != nil { @@ -126,15 +165,16 @@ func CreateUser(user *models.User) error { } // set password to encrypted password user.Password = string(hash) - - tokenString, _ := CreateUserJWT(user.UserName, user.IsSuperAdmin, user.IsAdmin) - if tokenString == "" { - logger.Log(0, "failed to generate token") + user.AuthType = models.BasicAuth + if IsOauthUser(user) == nil { + user.AuthType = models.OAuth + } + _, err = CreateUserJWT(user.UserName, user.PlatformRoleID) + if err != nil { + logger.Log(0, "failed to generate token", err.Error()) return err } - SetUserDefaults(user) - // connect db data, err := json.Marshal(user) if err != nil { @@ -159,8 +199,7 @@ func CreateSuperAdmin(u *models.User) error { if hassuperadmin { return errors.New("superadmin user already exists") } - u.IsSuperAdmin = true - u.IsAdmin = false + u.PlatformRoleID = models.SuperAdminRole return CreateUser(u) } @@ -189,7 +228,7 @@ func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) { } // Create a new JWT for the node - tokenString, err := CreateUserJWT(authRequest.UserName, result.IsSuperAdmin, result.IsAdmin) + tokenString, err := CreateUserJWT(authRequest.UserName, result.PlatformRoleID) if err != nil { slog.Error("error creating jwt", "error", err) return "", err @@ -250,8 +289,17 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) { user.Password = userchange.Password } - user.IsAdmin = userchange.IsAdmin - + if err := IsGroupsValid(userchange.UserGroups); err != nil { + return userchange, errors.New("invalid groups: " + err.Error()) + } + if err := IsNetworkRolesValid(userchange.NetworkRoles); err != nil { + return userchange, errors.New("invalid network roles: " + err.Error()) + } + // Reset Gw Access for service users + go UpdateUserGwAccess(*user, *userchange) + user.PlatformRoleID = userchange.PlatformRoleID + user.UserGroups = userchange.UserGroups + user.NetworkRoles = userchange.NetworkRoles err := ValidateUser(user) if err != nil { return &models.User{}, err @@ -274,12 +322,17 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) { // ValidateUser - validates a user model func ValidateUser(user *models.User) error { + // check if role is valid + _, err := GetRole(user.PlatformRoleID) + if err != nil { + return err + } v := validator.New() _ = v.RegisterValidation("in_charset", func(fl validator.FieldLevel) bool { isgood := user.NameInCharSet() return isgood }) - err := v.Struct(user) + err = v.Struct(user) if err != nil { for _, e := range err.(validator.ValidationErrors) { diff --git a/logic/gateway.go b/logic/gateway.go index 0d99af88..87a41105 100644 --- a/logic/gateway.go +++ b/logic/gateway.go @@ -178,6 +178,30 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq 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 } @@ -231,6 +255,11 @@ func DeleteIngressGateway(nodeid string) (models.Node, []models.ExtClient, error 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 } @@ -264,10 +293,8 @@ func IsUserAllowedAccessToExtClient(username string, client models.ExtClient) bo if err != nil { return false } - if !user.IsAdmin && !user.IsSuperAdmin { - if user.UserName != client.OwnerID { - return false - } + if user.UserName != client.OwnerID { + return false } return true } diff --git a/logic/hosts.go b/logic/hosts.go index d0282bb4..0fa8887e 100644 --- a/logic/hosts.go +++ b/logic/hosts.go @@ -269,6 +269,19 @@ 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 b435dcaf..41181fcd 100644 --- a/logic/jwts.go +++ b/logic/jwts.go @@ -53,12 +53,11 @@ func CreateJWT(uuid string, macAddress string, network string) (response string, } // CreateUserJWT - creates a user jwt token -func CreateUserJWT(username string, issuperadmin, isadmin bool) (response string, err error) { +func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) { expirationTime := time.Now().Add(servercfg.GetServerConfig().JwtValidityDuration) claims := &models.UserClaims{ - UserName: username, - IsSuperAdmin: issuperadmin, - IsAdmin: isadmin, + UserName: username, + Role: role, RegisteredClaims: jwt.RegisteredClaims{ Issuer: "Netmaker", Subject: fmt.Sprintf("user|%s", username), @@ -87,6 +86,47 @@ func VerifyJWT(bearerToken string) (username string, issuperadmin, isadmin bool, return VerifyUserToken(token) } +func GetUserNameFromToken(authtoken string) (username string, err error) { + claims := &models.UserClaims{} + var tokenSplit = strings.Split(authtoken, " ") + var tokenString = "" + + if len(tokenSplit) < 2 { + return "", Unauthorized_Err + } else { + tokenString = tokenSplit[1] + } + if tokenString == servercfg.GetMasterKey() && servercfg.GetMasterKey() != "" { + return MasterUser, nil + } + + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return jwtSecretKey, nil + }) + if err != nil { + return "", Unauthorized_Err + } + + if token != nil && token.Valid { + var user *models.User + // check that user exists + user, err = GetUser(claims.UserName) + if err != nil { + return "", err + } + if user.UserName != "" { + return user.UserName, nil + } + if user.PlatformRoleID != claims.Role { + return "", Unauthorized_Err + } + err = errors.New("user does not exist") + } else { + err = Unauthorized_Err + } + return "", err +} + // VerifyUserToken func will used to Verify the JWT Token while using APIS func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin bool, err error) { claims := &models.UserClaims{} @@ -107,7 +147,8 @@ func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin return "", false, false, err } if user.UserName != "" { - return user.UserName, user.IsSuperAdmin, user.IsAdmin, nil + return user.UserName, user.PlatformRoleID == models.SuperAdminRole, + user.PlatformRoleID == models.AdminRole, nil } err = errors.New("user does not exist") } diff --git a/logic/nodes.go b/logic/nodes.go index 7510cdbd..75ad16a8 100644 --- a/logic/nodes.go +++ b/logic/nodes.go @@ -196,6 +196,10 @@ 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 diff --git a/logic/security.go b/logic/security.go index 37be1f44..aa692236 100644 --- a/logic/security.go +++ b/logic/security.go @@ -2,9 +2,11 @@ package logic import ( "net/http" + "net/url" "strings" "github.com/gorilla/mux" + "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/models" "github.com/gravitl/netmaker/servercfg" ) @@ -17,20 +19,42 @@ const ( Unauthorized_Err = models.Error(Unauthorized_Msg) ) +var NetworkPermissionsCheck = func(username string, r *http.Request) error { return nil } +var GlobalPermissionsCheck = func(username string, r *http.Request) error { return nil } + // SecurityCheck - Check if user has appropriate permissions func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { r.Header.Set("ismaster", "no") + logger.Log(0, "next", r.URL.String()) + isGlobalAccesss := r.Header.Get("IS_GLOBAL_ACCESS") == "yes" bearerToken := r.Header.Get("Authorization") - username, err := UserPermissions(reqAdmin, bearerToken) + username, err := GetUserNameFromToken(bearerToken) if err != nil { - ReturnErrorResponse(w, r, FormatError(err, err.Error())) + logger.Log(0, "next 1", r.URL.String(), err.Error()) + ReturnErrorResponse(w, r, FormatError(err, "unauthorized")) return } // detect masteradmin if username == MasterUser { r.Header.Set("ismaster", "yes") + } else { + if isGlobalAccesss { + err = GlobalPermissionsCheck(username, r) + } else { + err = NetworkPermissionsCheck(username, r) + } + } + w.Header().Set("TARGET_RSRC", r.Header.Get("TARGET_RSRC")) + w.Header().Set("TARGET_RSRC_ID", r.Header.Get("TARGET_RSRC_ID")) + w.Header().Set("RSRC_TYPE", r.Header.Get("RSRC_TYPE")) + w.Header().Set("IS_GLOBAL_ACCESS", r.Header.Get("IS_GLOBAL_ACCESS")) + w.Header().Set("Access-Control-Allow-Origin", "*") + if err != nil { + w.Header().Set("ACCESS_PERM", err.Error()) + ReturnErrorResponse(w, r, FormatError(err, "forbidden")) + return } r.Header.Set("user", username) next.ServeHTTP(w, r) @@ -75,7 +99,11 @@ func ContinueIfUserMatch(next http.Handler) http.HandlerFunc { } var params = mux.Vars(r) var requestedUser = params["username"] + if requestedUser == "" { + requestedUser, _ = url.QueryUnescape(r.URL.Query().Get("username")) + } if requestedUser != r.Header.Get("user") { + logger.Log(0, "next 2", r.URL.String(), errorResponse.Message) ReturnErrorResponse(w, r, errorResponse) return } diff --git a/logic/user_mgmt.go b/logic/user_mgmt.go new file mode 100644 index 00000000..6f20cd71 --- /dev/null +++ b/logic/user_mgmt.go @@ -0,0 +1,75 @@ +package logic + +import ( + "encoding/json" + + "github.com/gravitl/netmaker/database" + "github.com/gravitl/netmaker/models" +) + +// Pre-Define Permission Templates for default Roles +var SuperAdminPermissionTemplate = models.UserRolePermissionTemplate{ + ID: models.SuperAdminRole, + Default: true, + FullAccess: true, +} + +var AdminPermissionTemplate = models.UserRolePermissionTemplate{ + ID: models.AdminRole, + Default: true, + FullAccess: true, +} + +var GetFilteredNodesByUserAccess = func(user models.User, nodes []models.Node) (filteredNodes []models.Node) { + return +} + +var CreateRole = func(r models.UserRolePermissionTemplate) error { + return nil +} + +var DeleteRole = func(r models.UserRoleID, force bool) error { + return nil +} + +var FilterNetworksByRole = func(allnetworks []models.Network, user models.User) []models.Network { + return allnetworks +} + +var IsGroupsValid = func(groups map[models.UserGroupID]struct{}) error { + return nil +} +var IsNetworkRolesValid = func(networkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) error { + return nil +} + +var UpdateUserGwAccess = func(currentUser, changeUser models.User) {} + +var UpdateRole = func(r models.UserRolePermissionTemplate) error { return nil } + +var InitialiseRoles = userRolesInit +var DeleteNetworkRoles = func(netID string) {} +var CreateDefaultNetworkRolesAndGroups = func(netID models.NetworkID) {} + +// GetRole - fetches role template by id +func GetRole(roleID models.UserRoleID) (models.UserRolePermissionTemplate, error) { + // check if role already exists + data, err := database.FetchRecord(database.USER_PERMISSIONS_TABLE_NAME, roleID.String()) + if err != nil { + return models.UserRolePermissionTemplate{}, err + } + ur := models.UserRolePermissionTemplate{} + err = json.Unmarshal([]byte(data), &ur) + if err != nil { + return ur, err + } + return ur, nil +} + +func userRolesInit() { + d, _ := json.Marshal(SuperAdminPermissionTemplate) + database.Insert(SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) + d, _ = json.Marshal(AdminPermissionTemplate) + database.Insert(AdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) + +} diff --git a/logic/users.go b/logic/users.go index 987556e2..168fd928 100644 --- a/logic/users.go +++ b/logic/users.go @@ -41,10 +41,13 @@ func GetReturnUser(username string) (models.ReturnUser, error) { // ToReturnUser - gets a user as a return user func ToReturnUser(user models.User) models.ReturnUser { return models.ReturnUser{ - UserName: user.UserName, - IsSuperAdmin: user.IsSuperAdmin, - IsAdmin: user.IsAdmin, - RemoteGwIDs: user.RemoteGwIDs, + UserName: user.UserName, + PlatformRoleID: user.PlatformRoleID, + AuthType: user.AuthType, + UserGroups: user.UserGroups, + NetworkRoles: user.NetworkRoles, + RemoteGwIDs: user.RemoteGwIDs, + LastLoginTime: user.LastLoginTime, } } @@ -53,6 +56,12 @@ func SetUserDefaults(user *models.User) { if user.RemoteGwIDs == nil { user.RemoteGwIDs = make(map[string]struct{}) } + if len(user.NetworkRoles) == 0 { + user.NetworkRoles = make(map[models.NetworkID]map[models.UserRoleID]struct{}) + } + if len(user.UserGroups) == 0 { + user.UserGroups = make(map[models.UserGroupID]struct{}) + } } // SortUsers - Sorts slice of Users by username @@ -119,3 +128,66 @@ func ListPendingUsers() ([]models.ReturnUser, error) { } return pendingUsers, nil } + +func GetUserMap() (map[string]models.User, error) { + userMap := make(map[string]models.User) + records, err := database.FetchRecords(database.USERS_TABLE_NAME) + if err != nil && !database.IsEmptyRecord(err) { + return userMap, err + } + for _, record := range records { + u := models.User{} + err = json.Unmarshal([]byte(record), &u) + if err == nil { + userMap[u.UserName] = u + } + } + return userMap, nil +} + +func InsertUserInvite(invite models.UserInvite) error { + data, err := json.Marshal(invite) + if err != nil { + return err + } + return database.Insert(invite.Email, string(data), database.USER_INVITES_TABLE_NAME) +} + +func GetUserInvite(email string) (in models.UserInvite, err error) { + d, err := database.FetchRecord(database.USER_INVITES_TABLE_NAME, email) + if err != nil { + return + } + err = json.Unmarshal([]byte(d), &in) + return +} + +func ListUserInvites() ([]models.UserInvite, error) { + invites := []models.UserInvite{} + records, err := database.FetchRecords(database.USER_INVITES_TABLE_NAME) + if err != nil && !database.IsEmptyRecord(err) { + return invites, err + } + for _, record := range records { + in := models.UserInvite{} + err = json.Unmarshal([]byte(record), &in) + if err == nil { + invites = append(invites, in) + } + } + return invites, nil +} + +func DeleteUserInvite(email string) error { + return database.DeleteRecord(database.USER_INVITES_TABLE_NAME, email) +} +func ValidateAndApproveUserInvite(email, code string) error { + in, err := GetUserInvite(email) + if err != nil { + return err + } + if code != in.InviteCode { + return errors.New("invalid code") + } + return nil +} diff --git a/main.go b/main.go index ec5a6c3d..027b51c7 100644 --- a/main.go +++ b/main.go @@ -102,7 +102,7 @@ func initialize() { // Client Mode Prereq Check migrate.Run() logic.SetJWTSecret() - + logic.InitialiseRoles() err = serverctl.SetDefaults() if err != nil { logger.FatalLog("error setting defaults: ", err.Error()) diff --git a/migrate/migrate.go b/migrate/migrate.go index e1474b86..1675f4ee 100644 --- a/migrate/migrate.go +++ b/migrate/migrate.go @@ -21,6 +21,7 @@ import ( func Run() { updateEnrollmentKeys() assignSuperAdmin() + syncUsers() updateHosts() updateNodes() updateAcls() @@ -43,8 +44,7 @@ func assignSuperAdmin() { if err != nil { log.Fatal("error getting user", "user", owner, "error", err.Error()) } - user.IsSuperAdmin = true - user.IsAdmin = false + user.PlatformRoleID = models.SuperAdminRole err = logic.UpsertUser(*user) if err != nil { log.Fatal( @@ -64,8 +64,8 @@ func assignSuperAdmin() { slog.Error("error getting user", "user", u.UserName, "error", err.Error()) continue } + user.PlatformRoleID = models.SuperAdminRole user.IsSuperAdmin = true - user.IsAdmin = false err = logic.UpsertUser(*user) if err != nil { slog.Error( @@ -311,3 +311,109 @@ func MigrateEmqx() { } } + +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 { + 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, + }, + }, + }, + }) + } + + } + } + } + } + } + + users, err := logic.GetUsersDB() + if err == nil { + for _, user := range users { + user := user + if user.PlatformRoleID == models.AdminRole && !user.IsAdmin { + user.IsAdmin = true + logic.UpsertUser(user) + } + if user.PlatformRoleID == models.SuperAdminRole && !user.IsSuperAdmin { + user.IsSuperAdmin = true + logic.UpsertUser(user) + } + if user.PlatformRoleID.String() != "" { + continue + } + user.AuthType = models.BasicAuth + if logic.IsOauthUser(&user) == nil { + user.AuthType = models.OAuth + } + if len(user.NetworkRoles) == 0 { + user.NetworkRoles = make(map[models.NetworkID]map[models.UserRoleID]struct{}) + } + if len(user.UserGroups) == 0 { + user.UserGroups = make(map[models.UserGroupID]struct{}) + } + if user.IsSuperAdmin { + user.PlatformRoleID = models.SuperAdminRole + + } else if user.IsAdmin { + user.PlatformRoleID = models.AdminRole + } else { + 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) + } + } + } +} diff --git a/models/structs.go b/models/structs.go index 59427533..decb6557 100644 --- a/models/structs.go +++ b/models/structs.go @@ -23,39 +23,6 @@ type AuthParams struct { Password string `json:"password"` } -// User struct - struct for Users -type User struct { - UserName string `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"` - Password string `json:"password" bson:"password" validate:"required,min=5"` - IsAdmin bool `json:"isadmin" bson:"isadmin"` - IsSuperAdmin bool `json:"issuperadmin"` - RemoteGwIDs map[string]struct{} `json:"remote_gw_ids"` - LastLoginTime time.Time `json:"last_login_time"` -} - -// ReturnUser - return user struct -type ReturnUser struct { - UserName string `json:"username"` - IsAdmin bool `json:"isadmin"` - IsSuperAdmin bool `json:"issuperadmin"` - RemoteGwIDs map[string]struct{} `json:"remote_gw_ids"` - LastLoginTime time.Time `json:"last_login_time"` -} - -// UserAuthParams - user auth params struct -type UserAuthParams struct { - UserName string `json:"username"` - Password string `json:"password"` -} - -// UserClaims - user claims struct -type UserClaims struct { - IsAdmin bool - IsSuperAdmin bool - UserName string - jwt.RegisteredClaims -} - // IngressGwUsers - struct to hold users on a ingress gw type IngressGwUsers struct { NodeID string `json:"node_id"` @@ -381,3 +348,8 @@ const ( type GetClientConfReqDto struct { PreferredIp string `json:"preferred_ip"` } + +type RsrcURLInfo struct { + Method string + Path string +} diff --git a/models/user_mgmt.go b/models/user_mgmt.go new file mode 100644 index 00000000..3efa81bf --- /dev/null +++ b/models/user_mgmt.go @@ -0,0 +1,199 @@ +package models + +import ( + "fmt" + "time" + + jwt "github.com/golang-jwt/jwt/v4" +) + +type NetworkID string +type RsrcType string +type RsrcID string +type UserRoleID string +type UserGroupID string +type AuthType string + +var ( + BasicAuth AuthType = "basic_auth" + OAuth AuthType = "oauth" +) + +func (r RsrcType) String() string { + return string(r) +} + +func (rid RsrcID) String() string { + return string(rid) +} + +func GetRAGRoleName(netID, hostName string) string { + return fmt.Sprintf("netID-%s-rag-%s", netID, hostName) +} + +func GetRAGRoleID(netID, hostID string) UserRoleID { + return UserRoleID(fmt.Sprintf("netID-%s-rag-%s", netID, hostID)) +} + +var RsrcTypeMap = map[RsrcType]struct{}{ + HostRsrc: {}, + RelayRsrc: {}, + RemoteAccessGwRsrc: {}, + ExtClientsRsrc: {}, + InetGwRsrc: {}, + EgressGwRsrc: {}, + NetworkRsrc: {}, + EnrollmentKeysRsrc: {}, + UserRsrc: {}, + AclRsrc: {}, + DnsRsrc: {}, + FailOverRsrc: {}, +} + +const AllNetworks NetworkID = "all_networks" +const ( + HostRsrc RsrcType = "hosts" + RelayRsrc RsrcType = "relays" + RemoteAccessGwRsrc RsrcType = "remote_access_gw" + ExtClientsRsrc RsrcType = "extclients" + InetGwRsrc RsrcType = "inet_gw" + EgressGwRsrc RsrcType = "egress" + NetworkRsrc RsrcType = "networks" + EnrollmentKeysRsrc RsrcType = "enrollment_key" + UserRsrc RsrcType = "users" + AclRsrc RsrcType = "acl" + DnsRsrc RsrcType = "dns" + FailOverRsrc RsrcType = "fail_over" + MetricRsrc RsrcType = "metrics" +) + +const ( + AllHostRsrcID RsrcID = "all_host" + AllRelayRsrcID RsrcID = "all_relay" + AllRemoteAccessGwRsrcID RsrcID = "all_remote_access_gw" + AllExtClientsRsrcID RsrcID = "all_extclients" + AllInetGwRsrcID RsrcID = "all_inet_gw" + AllEgressGwRsrcID RsrcID = "all_egress" + AllNetworkRsrcID RsrcID = "all_network" + AllEnrollmentKeysRsrcID RsrcID = "all_enrollment_key" + AllUserRsrcID RsrcID = "all_user" + AllDnsRsrcID RsrcID = "all_dns" + AllFailOverRsrcID RsrcID = "all_fail_over" + AllAclsRsrcID RsrcID = "all_acls" +) + +// Pre-Defined User Roles + +const ( + SuperAdminRole UserRoleID = "super-admin" + AdminRole UserRoleID = "admin" + ServiceUser UserRoleID = "service-user" + PlatformUser UserRoleID = "platform-user" + NetworkAdmin UserRoleID = "network-admin" + NetworkUser UserRoleID = "network-user" +) + +func (r UserRoleID) String() string { + return string(r) +} + +func (g UserGroupID) String() string { + return string(g) +} + +func (n NetworkID) String() string { + return string(n) +} + +type RsrcPermissionScope struct { + Create bool `json:"create"` + Read bool `json:"read"` + Update bool `json:"update"` + Delete bool `json:"delete"` + VPNaccess bool `json:"vpn_access"` + SelfOnly bool `json:"self_only"` +} + +type UserRolePermissionTemplate struct { + ID UserRoleID `json:"id"` + UiName string `json:"ui_name"` + Default bool `json:"default"` + DenyDashboardAccess bool `json:"deny_dashboard_access"` + FullAccess bool `json:"full_access"` + NetworkID NetworkID `json:"network_id"` + NetworkLevelAccess map[RsrcType]map[RsrcID]RsrcPermissionScope `json:"network_level_access"` + GlobalLevelAccess map[RsrcType]map[RsrcID]RsrcPermissionScope `json:"global_level_access"` +} + +type CreateGroupReq struct { + Group UserGroup `json:"user_group"` + Members []string `json:"members"` +} + +type UserGroup struct { + ID UserGroupID `json:"id"` + NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"` + MetaData string `json:"meta_data"` +} + +// User struct - struct for Users +type User struct { + UserName string `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"` + Password string `json:"password" bson:"password" validate:"required,min=5"` + IsAdmin bool `json:"isadmin" bson:"isadmin"` // deprecated + IsSuperAdmin bool `json:"issuperadmin"` // deprecated + RemoteGwIDs map[string]struct{} `json:"remote_gw_ids"` // deprecated + AuthType AuthType `json:"auth_type"` + UserGroups map[UserGroupID]struct{} `json:"user_group_ids"` + PlatformRoleID UserRoleID `json:"platform_role_id"` + NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"` + LastLoginTime time.Time `json:"last_login_time"` +} + +type ReturnUserWithRolesAndGroups struct { + ReturnUser + PlatformRole UserRolePermissionTemplate `json:"platform_role"` +} + +// ReturnUser - return user struct +type ReturnUser struct { + UserName string `json:"username"` + IsAdmin bool `json:"isadmin"` + IsSuperAdmin bool `json:"issuperadmin"` + AuthType AuthType `json:"auth_type"` + RemoteGwIDs map[string]struct{} `json:"remote_gw_ids"` // deprecated + UserGroups map[UserGroupID]struct{} `json:"user_group_ids"` + PlatformRoleID UserRoleID `json:"platform_role_id"` + NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"` + LastLoginTime time.Time `json:"last_login_time"` +} + +// UserAuthParams - user auth params struct +type UserAuthParams struct { + UserName string `json:"username"` + Password string `json:"password"` +} + +// UserClaims - user claims struct +type UserClaims struct { + Role UserRoleID + UserName string + jwt.RegisteredClaims +} + +type InviteUsersReq struct { + UserEmails []string `json:"user_emails"` + PlatformRoleID string `json:"platform_role_id"` + UserGroups map[UserGroupID]struct{} `json:"user_group_ids"` + NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"` +} + +// UserInvite - model for user invite +type UserInvite struct { + Email string `json:"email"` + PlatformRoleID string `json:"platform_role_id"` + UserGroups map[UserGroupID]struct{} `json:"user_group_ids"` + NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"` + InviteCode string `json:"invite_code"` + InviteURL string `json:"invite_url"` +} diff --git a/pro/auth/azure-ad.go b/pro/auth/azure-ad.go index 62ecedc2..b0981aae 100644 --- a/pro/auth/azure-ad.go +++ b/pro/auth/azure-ad.go @@ -8,11 +8,11 @@ import ( "net/http" "strings" - "github.com/gravitl/netmaker/auth" "github.com/gravitl/netmaker/database" "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" "github.com/gravitl/netmaker/models" + proLogic "github.com/gravitl/netmaker/pro/logic" "github.com/gravitl/netmaker/servercfg" "golang.org/x/oauth2" "golang.org/x/oauth2/microsoft" @@ -67,27 +67,50 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { handleOauthNotConfigured(w) return } - if !isEmailAllowed(content.UserPrincipalName) { - handleOauthUserNotAllowedToSignUp(w) - return + + var inviteExists bool + // check if invite exists for User + in, err := logic.GetUserInvite(content.UserPrincipalName) + if err == nil { + inviteExists = true } // check if user approval is already pending - if logic.IsPendingUser(content.UserPrincipalName) { + if !inviteExists && logic.IsPendingUser(content.UserPrincipalName) { handleOauthUserSignUpApprovalPending(w) return } + _, err = logic.GetUser(content.UserPrincipalName) if err != nil { if database.IsEmptyRecord(err) { // user must not exist, so try to make one - err = logic.InsertPendingUser(&models.User{ - UserName: content.UserPrincipalName, - }) - if err != nil { - handleSomethingWentWrong(w) + if inviteExists { + // create user + user, err := proLogic.PrepareOauthUserFromInvite(in) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + if err = logic.CreateUser(&user); err != nil { + handleSomethingWentWrong(w) + return + } + logic.DeleteUserInvite(user.UserName) + logic.DeletePendingUser(content.UserPrincipalName) + } else { + if !isEmailAllowed(content.UserPrincipalName) { + handleOauthUserNotAllowedToSignUp(w) + return + } + err = logic.InsertPendingUser(&models.User{ + UserName: content.UserPrincipalName, + }) + if err != nil { + handleSomethingWentWrong(w) + return + } + handleFirstTimeOauthUserSignUp(w) return } - handleFirstTimeOauthUserSignUp(w) - return } else { handleSomethingWentWrong(w) return @@ -98,11 +121,16 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { handleOauthUserNotFound(w) return } - if !(user.IsSuperAdmin || user.IsAdmin) { + userRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + handleSomethingWentWrong(w) + return + } + if userRole.DenyDashboardAccess { handleOauthUserNotAllowed(w) return } - var newPass, fetchErr = auth.FetchPassValue("") + var newPass, fetchErr = logic.FetchPassValue("") if fetchErr != nil { return } diff --git a/pro/auth/error.go b/pro/auth/error.go index 6a460563..b2a5efcd 100644 --- a/pro/auth/error.go +++ b/pro/auth/error.go @@ -1,60 +1,114 @@ package auth -import "net/http" +import ( + "fmt" + "net/http" + + "github.com/gravitl/netmaker/servercfg" +) + +var htmlBaseTemplate = ` + + + + + + + Netmaker :: SSO + + + -// == define error HTML here == -const oauthNotConfigured = ` -

Your Netmaker server does not have OAuth configured.

-

Please visit the docs here to learn how to.

+ + %s + + ` -const oauthStateInvalid = ` - -

Invalid OAuth Session. Please re-try again.

- -` +var oauthNotConfigured = fmt.Sprintf(htmlBaseTemplate, `

Your Netmaker server does not have OAuth configured.

+

Please visit the docs here to learn how to.

`) -const userNotAllowed = ` - -

Only administrators can access the Dashboard. Please contact your administrator to elevate your account.

-

Non-Admins can access the netmaker networks using RemoteAccessClient.

- - -` +var oauthStateInvalid = fmt.Sprintf(htmlBaseTemplate, `

Invalid OAuth Session. Please re-try again.

`) -const userFirstTimeSignUp = ` - -

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

- - -` +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.

`) -const userSignUpApprovalPending = ` - -

Your account is yet to be approved. Please contact your administrator for access.

- - -` +var userFirstTimeSignUp = fmt.Sprintf(htmlBaseTemplate, `

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

`) -const userNotFound = ` - -

User Not Found.

- -` +var userSignUpApprovalPending = fmt.Sprintf(htmlBaseTemplate, `

Your account is yet to be approved. Please contact your administrator for access.

`) -const somethingwentwrong = ` - -

Something went wrong. Contact Admin.

- -` +var userNotFound = fmt.Sprintf(htmlBaseTemplate, `

User Not Found.

`) -const notallowedtosignup = ` - -

Your email is not allowed. Please contact your administrator.

- -` +var somethingwentwrong = fmt.Sprintf(htmlBaseTemplate, `

Something went wrong. Contact Admin.

`) + +var notallowedtosignup = fmt.Sprintf(htmlBaseTemplate, `

Your email is not allowed. Please contact your administrator.

`) func handleOauthUserNotFound(response http.ResponseWriter) { response.Header().Set("Content-Type", "text/html; charset=utf-8") diff --git a/pro/auth/github.go b/pro/auth/github.go index 67223de4..c62fd1b7 100644 --- a/pro/auth/github.go +++ b/pro/auth/github.go @@ -8,11 +8,11 @@ import ( "net/http" "strings" - "github.com/gravitl/netmaker/auth" "github.com/gravitl/netmaker/database" "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" "github.com/gravitl/netmaker/models" + proLogic "github.com/gravitl/netmaker/pro/logic" "github.com/gravitl/netmaker/servercfg" "golang.org/x/oauth2" "golang.org/x/oauth2/github" @@ -67,27 +67,49 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { handleOauthNotConfigured(w) return } - if !isEmailAllowed(content.Login) { - handleOauthUserNotAllowedToSignUp(w) - return + + var inviteExists bool + // check if invite exists for User + in, err := logic.GetUserInvite(content.Login) + if err == nil { + inviteExists = true } // check if user approval is already pending - if logic.IsPendingUser(content.Login) { + if !inviteExists && logic.IsPendingUser(content.Login) { handleOauthUserSignUpApprovalPending(w) return } _, err = logic.GetUser(content.Login) if err != nil { if database.IsEmptyRecord(err) { // user must not exist, so try to make one - err = logic.InsertPendingUser(&models.User{ - UserName: content.Login, - }) - if err != nil { - handleSomethingWentWrong(w) + if inviteExists { + // create user + user, err := proLogic.PrepareOauthUserFromInvite(in) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + if err = logic.CreateUser(&user); err != nil { + handleSomethingWentWrong(w) + return + } + logic.DeleteUserInvite(user.UserName) + logic.DeletePendingUser(content.Login) + } else { + if !isEmailAllowed(content.Login) { + handleOauthUserNotAllowedToSignUp(w) + return + } + err = logic.InsertPendingUser(&models.User{ + UserName: content.Login, + }) + if err != nil { + handleSomethingWentWrong(w) + return + } + handleFirstTimeOauthUserSignUp(w) return } - handleFirstTimeOauthUserSignUp(w) - return } else { handleSomethingWentWrong(w) return @@ -98,11 +120,16 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { handleOauthUserNotFound(w) return } - if !(user.IsSuperAdmin || user.IsAdmin) { + userRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + handleSomethingWentWrong(w) + return + } + if userRole.DenyDashboardAccess { handleOauthUserNotAllowed(w) return } - var newPass, fetchErr = auth.FetchPassValue("") + var newPass, fetchErr = logic.FetchPassValue("") if fetchErr != nil { return } diff --git a/pro/auth/google.go b/pro/auth/google.go index 6bf8af29..e4dbc2ae 100644 --- a/pro/auth/google.go +++ b/pro/auth/google.go @@ -9,11 +9,11 @@ import ( "strings" "time" - "github.com/gravitl/netmaker/auth" "github.com/gravitl/netmaker/database" "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" "github.com/gravitl/netmaker/models" + proLogic "github.com/gravitl/netmaker/pro/logic" "github.com/gravitl/netmaker/servercfg" "golang.org/x/oauth2" "golang.org/x/oauth2/google" @@ -45,7 +45,7 @@ func handleGoogleLogin(w http.ResponseWriter, r *http.Request) { handleOauthNotConfigured(w) return } - + logger.Log(0, "Setting OAuth State ", oauth_state_string) if err := logic.SetState(oauth_state_string); err != nil { handleOauthNotConfigured(w) return @@ -58,7 +58,7 @@ func handleGoogleLogin(w http.ResponseWriter, r *http.Request) { func handleGoogleCallback(w http.ResponseWriter, r *http.Request) { var rState, rCode = getStateAndCode(r) - + logger.Log(0, "Fetched OAuth State ", rState) var content, err = getGoogleUserInfo(rState, rCode) if err != nil { logger.Log(1, "error when getting user info from google:", err.Error()) @@ -69,43 +69,78 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) { handleOauthNotConfigured(w) return } - if !isEmailAllowed(content.Email) { - handleOauthUserNotAllowedToSignUp(w) - return + logger.Log(0, "CALLBACK ----> 1") + + logger.Log(0, "CALLBACK ----> 2") + var inviteExists bool + // check if invite exists for User + in, err := logic.GetUserInvite(content.Email) + if err == nil { + inviteExists = true } + logger.Log(0, fmt.Sprintf("CALLBACK ----> 3 %v", inviteExists)) // check if user approval is already pending - if logic.IsPendingUser(content.Email) { + if !inviteExists && logic.IsPendingUser(content.Email) { handleOauthUserSignUpApprovalPending(w) return } + logger.Log(0, "CALLBACK ----> 4") _, err = logic.GetUser(content.Email) if err != nil { if database.IsEmptyRecord(err) { // user must not exist, so try to make one - err = logic.InsertPendingUser(&models.User{ - UserName: content.Email, - }) - if err != nil { - handleSomethingWentWrong(w) + if inviteExists { + // create user + user, err := proLogic.PrepareOauthUserFromInvite(in) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + logger.Log(0, "CALLBACK ----> 4.0") + + if err = logic.CreateUser(&user); err != nil { + handleSomethingWentWrong(w) + return + } + logic.DeleteUserInvite(user.UserName) + logic.DeletePendingUser(content.Email) + } else { + if !isEmailAllowed(content.Email) { + handleOauthUserNotAllowedToSignUp(w) + return + } + err = logic.InsertPendingUser(&models.User{ + UserName: content.Email, + }) + if err != nil { + handleSomethingWentWrong(w) + return + } + handleFirstTimeOauthUserSignUp(w) return } - handleFirstTimeOauthUserSignUp(w) - return + } else { handleSomethingWentWrong(w) return } } + logger.Log(0, "CALLBACK ----> 6") user, err := logic.GetUser(content.Email) if err != nil { logger.Log(0, "error fetching user: ", err.Error()) handleOauthUserNotFound(w) return } - if !(user.IsSuperAdmin || user.IsAdmin) { + userRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + handleSomethingWentWrong(w) + return + } + if userRole.DenyDashboardAccess { handleOauthUserNotAllowed(w) return } - var newPass, fetchErr = auth.FetchPassValue("") + var newPass, fetchErr = logic.FetchPassValue("") if fetchErr != nil { return } @@ -151,6 +186,7 @@ func getGoogleUserInfo(state string, code string) (*OAuthUser, error) { if err != nil { return nil, fmt.Errorf("failed reading response body: %s", err.Error()) } + logger.Log(0, fmt.Sprintf("---------------> USERINFO: %v, token: %s", string(contents), token.AccessToken)) var userInfo = &OAuthUser{} if err = json.Unmarshal(contents, userInfo); err != nil { return nil, fmt.Errorf("failed parsing email from response data: %s", err.Error()) diff --git a/pro/auth/headless_callback.go b/pro/auth/headless_callback.go index 7fb04aa6..de0627ca 100644 --- a/pro/auth/headless_callback.go +++ b/pro/auth/headless_callback.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" - "github.com/gravitl/netmaker/auth" "github.com/gravitl/netmaker/database" "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" @@ -78,7 +77,7 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) { return } } - newPass, fetchErr := auth.FetchPassValue("") + newPass, fetchErr := logic.FetchPassValue("") if fetchErr != nil { return } diff --git a/pro/auth/oidc.go b/pro/auth/oidc.go index a2bf5739..72dc2b95 100644 --- a/pro/auth/oidc.go +++ b/pro/auth/oidc.go @@ -8,11 +8,11 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" - "github.com/gravitl/netmaker/auth" "github.com/gravitl/netmaker/database" "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" "github.com/gravitl/netmaker/models" + proLogic "github.com/gravitl/netmaker/pro/logic" "github.com/gravitl/netmaker/servercfg" "golang.org/x/oauth2" ) @@ -80,27 +80,49 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) { handleOauthNotConfigured(w) return } - if !isEmailAllowed(content.Email) { - handleOauthUserNotAllowedToSignUp(w) - return + + var inviteExists bool + // check if invite exists for User + in, err := logic.GetUserInvite(content.Login) + if err == nil { + inviteExists = true } // check if user approval is already pending - if logic.IsPendingUser(content.Email) { + if !inviteExists && logic.IsPendingUser(content.Email) { handleOauthUserSignUpApprovalPending(w) return } _, err = logic.GetUser(content.Email) if err != nil { if database.IsEmptyRecord(err) { // user must not exist, so try to make one - err = logic.InsertPendingUser(&models.User{ - UserName: content.Email, - }) - if err != nil { - handleSomethingWentWrong(w) + if inviteExists { + // create user + user, err := proLogic.PrepareOauthUserFromInvite(in) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + if err = logic.CreateUser(&user); err != nil { + handleSomethingWentWrong(w) + return + } + logic.DeleteUserInvite(user.UserName) + logic.DeletePendingUser(content.Email) + } else { + if !isEmailAllowed(content.Email) { + handleOauthUserNotAllowedToSignUp(w) + return + } + err = logic.InsertPendingUser(&models.User{ + UserName: content.Email, + }) + if err != nil { + handleSomethingWentWrong(w) + return + } + handleFirstTimeOauthUserSignUp(w) return } - handleFirstTimeOauthUserSignUp(w) - return } else { handleSomethingWentWrong(w) return @@ -111,11 +133,16 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) { handleOauthUserNotFound(w) return } - if !(user.IsSuperAdmin || user.IsAdmin) { + userRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + handleSomethingWentWrong(w) + return + } + if userRole.DenyDashboardAccess { handleOauthUserNotAllowed(w) return } - var newPass, fetchErr = auth.FetchPassValue("") + var newPass, fetchErr = logic.FetchPassValue("") if fetchErr != nil { return } diff --git a/pro/auth/register_callback.go b/pro/auth/register_callback.go index c7edd958..a2085c40 100644 --- a/pro/auth/register_callback.go +++ b/pro/auth/register_callback.go @@ -10,6 +10,7 @@ import ( "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" "github.com/gravitl/netmaker/logic/pro/netcache" + "github.com/gravitl/netmaker/models" ) var ( @@ -73,7 +74,7 @@ func HandleHostSSOCallback(w http.ResponseWriter, r *http.Request) { handleOauthUserNotFound(w) return } - if !user.IsAdmin && !user.IsSuperAdmin { + if user.PlatformRoleID != models.AdminRole && user.PlatformRoleID != models.SuperAdminRole { response := returnErrTemplate(userClaims.getUserName(), "only admin users can register using SSO", state, reqKeyIf) w.WriteHeader(http.StatusForbidden) w.Write(response) diff --git a/pro/controllers/relay.go b/pro/controllers/relay.go index 289dc531..415cf1cc 100644 --- a/pro/controllers/relay.go +++ b/pro/controllers/relay.go @@ -19,12 +19,9 @@ import ( // RelayHandlers - handle Pro Relays func RelayHandlers(r *mux.Router) { - r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", controller.Authorize(false, true, "user", http.HandlerFunc(createRelay))). - Methods(http.MethodPost) - r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", controller.Authorize(false, true, "user", http.HandlerFunc(deleteRelay))). - Methods(http.MethodDelete) - r.HandleFunc("/api/v1/host/{hostid}/failoverme", controller.Authorize(true, false, "host", http.HandlerFunc(failOverME))). - Methods(http.MethodPost) + r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", logic.SecurityCheck(true, http.HandlerFunc(createRelay))).Methods(http.MethodPost) + r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", logic.SecurityCheck(true, http.HandlerFunc(deleteRelay))).Methods(http.MethodDelete) + r.HandleFunc("/api/v1/host/{hostid}/failoverme", controller.Authorize(true, false, "host", http.HandlerFunc(failOverME))).Methods(http.MethodPost) } // @Summary Create a relay diff --git a/pro/controllers/users.go b/pro/controllers/users.go index 987e9274..9387e1b2 100644 --- a/pro/controllers/users.go +++ b/pro/controllers/users.go @@ -1,34 +1,630 @@ package controllers import ( + "context" "encoding/json" "errors" "fmt" "net/http" + "net/url" "github.com/gorilla/mux" + "github.com/gravitl/netmaker/database" "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" "github.com/gravitl/netmaker/models" "github.com/gravitl/netmaker/mq" - "github.com/gravitl/netmaker/pro/auth" + proAuth "github.com/gravitl/netmaker/pro/auth" + "github.com/gravitl/netmaker/pro/email" + proLogic "github.com/gravitl/netmaker/pro/logic" "github.com/gravitl/netmaker/servercfg" "golang.org/x/exp/slog" ) func UserHandlers(r *mux.Router) { - r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(attachUserToRemoteAccessGw))). - Methods(http.MethodPost) - r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(removeUserFromRemoteAccessGW))). - Methods(http.MethodDelete) - r.HandleFunc("/api/users/{username}/remote_access_gw", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserRemoteAccessGws)))). - Methods(http.MethodGet) - r.HandleFunc("/api/users/ingress/{ingress_id}", logic.SecurityCheck(true, http.HandlerFunc(ingressGatewayUsers))). - Methods(http.MethodGet) - r.HandleFunc("/api/oauth/login", auth.HandleAuthLogin).Methods(http.MethodGet) - r.HandleFunc("/api/oauth/callback", auth.HandleAuthCallback).Methods(http.MethodGet) - r.HandleFunc("/api/oauth/headless", auth.HandleHeadlessSSO) - r.HandleFunc("/api/oauth/register/{regKey}", auth.RegisterHostSSO).Methods(http.MethodGet) + + r.HandleFunc("/api/oauth/login", proAuth.HandleAuthLogin).Methods(http.MethodGet) + r.HandleFunc("/api/oauth/callback", proAuth.HandleAuthCallback).Methods(http.MethodGet) + r.HandleFunc("/api/oauth/headless", proAuth.HandleHeadlessSSO) + r.HandleFunc("/api/oauth/register/{regKey}", proAuth.RegisterHostSSO).Methods(http.MethodGet) + + // User Role Handlers + r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(listRoles))).Methods(http.MethodGet) + r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(getRole))).Methods(http.MethodGet) + r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(createRole))).Methods(http.MethodPost) + r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(updateRole))).Methods(http.MethodPut) + r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(deleteRole))).Methods(http.MethodDelete) + + // User Group Handlers + r.HandleFunc("/api/v1/users/groups", logic.SecurityCheck(true, http.HandlerFunc(listUserGroups))).Methods(http.MethodGet) + r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(getUserGroup))).Methods(http.MethodGet) + r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(createUserGroup))).Methods(http.MethodPost) + r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(updateUserGroup))).Methods(http.MethodPut) + r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(deleteUserGroup))).Methods(http.MethodDelete) + + // User Invite Handlers + r.HandleFunc("/api/v1/users/invite", userInviteVerify).Methods(http.MethodGet) + r.HandleFunc("/api/v1/users/invite-signup", userInviteSignUp).Methods(http.MethodPost) + r.HandleFunc("/api/v1/users/invite", logic.SecurityCheck(true, http.HandlerFunc(inviteUsers))).Methods(http.MethodPost) + r.HandleFunc("/api/v1/users/invites", logic.SecurityCheck(true, http.HandlerFunc(listUserInvites))).Methods(http.MethodGet) + r.HandleFunc("/api/v1/users/invite", logic.SecurityCheck(true, http.HandlerFunc(deleteUserInvite))).Methods(http.MethodDelete) + r.HandleFunc("/api/v1/users/invites", logic.SecurityCheck(true, http.HandlerFunc(deleteAllUserInvites))).Methods(http.MethodDelete) + + r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(getPendingUsers))).Methods(http.MethodGet) + r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(deleteAllPendingUsers))).Methods(http.MethodDelete) + r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(deletePendingUser))).Methods(http.MethodDelete) + r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(approvePendingUser))).Methods(http.MethodPost) + + r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(attachUserToRemoteAccessGw))).Methods(http.MethodPost) + r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(removeUserFromRemoteAccessGW))).Methods(http.MethodDelete) + r.HandleFunc("/api/users/{username}/remote_access_gw", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserRemoteAccessGwsV1)))).Methods(http.MethodGet) + r.HandleFunc("/api/users/ingress/{ingress_id}", logic.SecurityCheck(true, http.HandlerFunc(ingressGatewayUsers))).Methods(http.MethodGet) + +} + +// swagger:route POST /api/v1/users/invite-signup user userInviteSignUp +// +// user signup via invite. +// +// Schemes: https +// +// Responses: +// 200: ReturnSuccessResponse +func userInviteSignUp(w http.ResponseWriter, r *http.Request) { + email, _ := url.QueryUnescape(r.URL.Query().Get("email")) + code, _ := url.QueryUnescape(r.URL.Query().Get("invite_code")) + in, err := logic.GetUserInvite(email) + if err != nil { + logger.Log(0, "failed to fetch users: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + if code != in.InviteCode { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid invite code"), "badrequest")) + return + } + // check if user already exists + _, err = logic.GetUser(email) + if err == nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user already exists"), "badrequest")) + return + } + var user models.User + err = json.NewDecoder(r.Body).Decode(&user) + if err != nil { + logger.Log(0, user.UserName, "error decoding request body: ", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + if user.UserName != email { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username not matching with invite"), "badrequest")) + return + } + if user.Password == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("password cannot be empty"), "badrequest")) + return + } + + user.UserGroups = in.UserGroups + user.PlatformRoleID = models.UserRoleID(in.PlatformRoleID) + if user.PlatformRoleID == "" { + user.PlatformRoleID = models.ServiceUser + } + user.NetworkRoles = in.NetworkRoles + err = logic.CreateUser(&user) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + // delete invite + logic.DeleteUserInvite(email) + logic.DeletePendingUser(email) + w.Header().Set("Access-Control-Allow-Origin", "*") + logic.ReturnSuccessResponse(w, r, "created user successfully "+email) +} + +// swagger:route GET /api/v1/users/invite user userInviteVerify +// +// verfies user invite. +// +// Schemes: https +// +// Responses: +// 200: ReturnSuccessResponse +func userInviteVerify(w http.ResponseWriter, r *http.Request) { + email, _ := url.QueryUnescape(r.URL.Query().Get("email")) + code, _ := url.QueryUnescape(r.URL.Query().Get("invite_code")) + err := logic.ValidateAndApproveUserInvite(email, code) + if err != nil { + logger.Log(0, "failed to fetch users: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + logic.ReturnSuccessResponse(w, r, "invite is valid") +} + +// swagger:route POST /api/v1/users/invite user inviteUsers +// +// invite users. +// +// Schemes: https +// +// Security: +// oauth +// +// Responses: +// 200: userBodyResponse +func inviteUsers(w http.ResponseWriter, r *http.Request) { + var inviteReq models.InviteUsersReq + err := json.NewDecoder(r.Body).Decode(&inviteReq) + if err != nil { + slog.Error("error decoding request body", "error", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + callerUserName := r.Header.Get("user") + caller, err := logic.GetUser(callerUserName) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "notfound")) + return + } + if inviteReq.PlatformRoleID == models.SuperAdminRole.String() { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("super admin cannot be invited"), "badrequest")) + return + } + if inviteReq.PlatformRoleID == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("platform role id cannot be empty"), "badrequest")) + return + } + if (inviteReq.PlatformRoleID == models.AdminRole.String() || + inviteReq.PlatformRoleID == models.SuperAdminRole.String()) && caller.PlatformRoleID != models.SuperAdminRole { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only superadmin can invite admin users"), "forbidden")) + return + } + //validate Req + err = proLogic.IsGroupsValid(inviteReq.UserGroups) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = proLogic.IsNetworkRolesValid(inviteReq.NetworkRoles) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + + // check platform role + _, err = logic.GetRole(models.UserRoleID(inviteReq.PlatformRoleID)) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + for _, inviteeEmail := range inviteReq.UserEmails { + // check if user with email exists, then ignore + _, err := logic.GetUser(inviteeEmail) + if err == nil { + // user exists already, so ignore + continue + } + invite := models.UserInvite{ + Email: inviteeEmail, + PlatformRoleID: inviteReq.PlatformRoleID, + UserGroups: inviteReq.UserGroups, + NetworkRoles: inviteReq.NetworkRoles, + InviteCode: logic.RandomString(8), + } + u, err := url.Parse(fmt.Sprintf("%s/invite?email=%s&invite_code=%s", + servercfg.GetFrontendURL(), url.QueryEscape(invite.Email), url.QueryEscape(invite.InviteCode))) + if err != nil { + slog.Error("failed to parse to invite url", "error", err) + return + } + invite.InviteURL = u.String() + err = logic.InsertUserInvite(invite) + if err != nil { + slog.Error("failed to insert invite for user", "email", invite.Email, "error", err) + } + // notify user with magic link + go func(invite models.UserInvite) { + // Set E-Mail body. You can set plain text or html with text/html + + e := email.UserInvitedMail{ + BodyBuilder: &email.EmailBodyBuilderWithH1HeadlineAndImage{}, + InviteURL: invite.InviteURL, + } + n := email.Notification{ + RecipientMail: invite.Email, + } + err = email.GetClient().SendEmail(context.Background(), n, e) + if err != nil { + slog.Error("failed to send email invite", "user", invite.Email, "error", err) + } + }(invite) + } + +} + +// swagger:route GET /api/v1/users/invites user listUserInvites +// +// lists all pending invited users. +// +// Schemes: https +// +// Security: +// oauth +// +// Responses: +// 200: ReturnSuccessResponseWithJson +func listUserInvites(w http.ResponseWriter, r *http.Request) { + usersInvites, err := logic.ListUserInvites() + if err != nil { + logger.Log(0, "failed to fetch users: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + logic.ReturnSuccessResponseWithJson(w, r, usersInvites, "fetched pending user invites") +} + +// swagger:route DELETE /api/v1/users/invite user deleteUserInvite +// +// delete pending invite. +// +// Schemes: https +// +// Security: +// oauth +// +// Responses: +// 200: ReturnSuccessResponse +func deleteUserInvite(w http.ResponseWriter, r *http.Request) { + email, _ := url.QueryUnescape(r.URL.Query().Get("invitee_email")) + err := logic.DeleteUserInvite(email) + if err != nil { + logger.Log(0, "failed to delete user invite: ", email, err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + logic.ReturnSuccessResponse(w, r, "deleted user invite") +} + +// swagger:route DELETE /api/v1/users/invites user deleteAllUserInvites +// +// deletes all pending invites. +// +// Schemes: https +// +// Security: +// oauth +// +// Responses: +// 200: ReturnSuccessResponse +func deleteAllUserInvites(w http.ResponseWriter, r *http.Request) { + err := database.DeleteAllRecords(database.USER_INVITES_TABLE_NAME) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending user invites "+err.Error()), "internal")) + return + } + logic.ReturnSuccessResponse(w, r, "cleared all pending user invites") +} + +// swagger:route GET /api/v1/user/groups user listUserGroups +// +// Get all user groups. +// +// Schemes: https +// +// Security: +// oauth +// +// Responses: +// 200: userBodyResponse +func listUserGroups(w http.ResponseWriter, r *http.Request) { + groups, err := proLogic.ListUserGroups() + if err != nil { + logic.ReturnErrorResponse(w, r, models.ErrorResponse{ + Code: http.StatusInternalServerError, + Message: err.Error(), + }) + return + } + logic.ReturnSuccessResponseWithJson(w, r, groups, "successfully fetched user groups") +} + +// swagger:route GET /api/v1/user/group user getUserGroup +// +// Get user group. +// +// Schemes: https +// +// Security: +// oauth +// +// Responses: +// 200: userBodyResponse +func getUserGroup(w http.ResponseWriter, r *http.Request) { + + gid, _ := url.QueryUnescape(r.URL.Query().Get("group_id")) + if gid == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("group id is required"), "badrequest")) + return + } + group, err := proLogic.GetUserGroup(models.UserGroupID(gid)) + if err != nil { + logic.ReturnErrorResponse(w, r, models.ErrorResponse{ + Code: http.StatusInternalServerError, + Message: err.Error(), + }) + return + } + logic.ReturnSuccessResponseWithJson(w, r, group, "successfully fetched user group") +} + +// swagger:route POST /api/v1/user/group user createUserGroup +// +// Create user groups. +// +// Schemes: https +// +// Security: +// oauth +// +// Responses: +// 200: userBodyResponse +func createUserGroup(w http.ResponseWriter, r *http.Request) { + var userGroupReq models.CreateGroupReq + err := json.NewDecoder(r.Body).Decode(&userGroupReq) + if err != nil { + slog.Error("error decoding request body", "error", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = proLogic.ValidateCreateGroupReq(userGroupReq.Group) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = proLogic.CreateUserGroup(userGroupReq.Group) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + for _, userID := range userGroupReq.Members { + user, err := logic.GetUser(userID) + if err != nil { + continue + } + if len(user.UserGroups) == 0 { + user.UserGroups = make(map[models.UserGroupID]struct{}) + } + user.UserGroups[userGroupReq.Group.ID] = struct{}{} + logic.UpsertUser(*user) + } + logic.ReturnSuccessResponseWithJson(w, r, userGroupReq.Group, "created user group") +} + +// swagger:route PUT /api/v1/user/group user updateUserGroup +// +// Update user group. +// +// Schemes: https +// +// Security: +// oauth +// +// Responses: +// 200: userBodyResponse +func updateUserGroup(w http.ResponseWriter, r *http.Request) { + var userGroup models.UserGroup + err := json.NewDecoder(r.Body).Decode(&userGroup) + if err != nil { + slog.Error("error decoding request body", "error", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + // fetch curr group + currUserG, err := proLogic.GetUserGroup(userGroup.ID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = proLogic.ValidateUpdateGroupReq(userGroup) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = proLogic.UpdateUserGroup(userGroup) + if err != nil { + 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") +} + +// swagger:route DELETE /api/v1/user/group user deleteUserGroup +// +// delete user group. +// +// Schemes: https +// +// Security: +// oauth +// +// Responses: +// 200: userBodyResponse +// +// @Summary Delete user group. +// @Router /api/v1/user/group [delete] +// @Tags Users +// @Param group_id param string true "group id required to delete the role" +// @Success 200 {string} string +// @Failure 500 {object} models.ErrorResponse +func deleteUserGroup(w http.ResponseWriter, r *http.Request) { + + gid, _ := url.QueryUnescape(r.URL.Query().Get("group_id")) + if gid == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest")) + return + } + userG, err := proLogic.GetUserGroup(models.UserGroupID(gid)) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest")) + return + } + err = proLogic.DeleteUserGroup(models.UserGroupID(gid)) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + go proLogic.UpdatesUserGwAccessOnGrpUpdates(userG.NetworkRoles, make(map[models.NetworkID]map[models.UserRoleID]struct{})) + logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user group") +} + +// @Summary lists all user roles. +// @Router /api/v1/user/roles [get] +// @Tags Users +// @Param role_id param string true "roleid required to get the role details" +// @Success 200 {object} []models.UserRolePermissionTemplate +// @Failure 500 {object} models.ErrorResponse +func listRoles(w http.ResponseWriter, r *http.Request) { + platform, _ := url.QueryUnescape(r.URL.Query().Get("platform")) + var roles []models.UserRolePermissionTemplate + var err error + if platform == "true" { + roles, err = proLogic.ListPlatformRoles() + } else { + roles, err = proLogic.ListNetworkRoles() + } + if err != nil { + logic.ReturnErrorResponse(w, r, models.ErrorResponse{ + Code: http.StatusInternalServerError, + Message: err.Error(), + }) + return + } + + logic.ReturnSuccessResponseWithJson(w, r, roles, "successfully fetched user roles permission templates") +} + +// @Summary Get user role permission template. +// @Router /api/v1/user/role [get] +// @Tags Users +// @Param role_id param string true "roleid required to get the role details" +// @Success 200 {object} models.UserRolePermissionTemplate +// @Failure 500 {object} models.ErrorResponse +func getRole(w http.ResponseWriter, r *http.Request) { + rid, _ := url.QueryUnescape(r.URL.Query().Get("role_id")) + if rid == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest")) + return + } + role, err := logic.GetRole(models.UserRoleID(rid)) + if err != nil { + logic.ReturnErrorResponse(w, r, models.ErrorResponse{ + Code: http.StatusInternalServerError, + Message: err.Error(), + }) + return + } + logic.ReturnSuccessResponseWithJson(w, r, role, "successfully fetched user role permission templates") +} + +// @Summary Create user role permission template. +// @Router /api/v1/user/role [post] +// @Tags Users +// @Param body models.UserRolePermissionTemplate true "user role template" +// @Success 200 {object} models.UserRolePermissionTemplate +// @Failure 500 {object} models.ErrorResponse +func createRole(w http.ResponseWriter, r *http.Request) { + var userRole models.UserRolePermissionTemplate + err := json.NewDecoder(r.Body).Decode(&userRole) + if err != nil { + slog.Error("error decoding request body", "error", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = proLogic.ValidateCreateRoleReq(&userRole) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + userRole.Default = false + userRole.GlobalLevelAccess = make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope) + err = proLogic.CreateRole(userRole) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + logic.ReturnSuccessResponseWithJson(w, r, userRole, "created user role") +} + +// @Summary Update user role permission template. +// @Router /api/v1/user/role [put] +// @Tags Users +// @Param body models.UserRolePermissionTemplate true "user role template" +// @Success 200 {object} userBodyResponse +// @Failure 500 {object} models.ErrorResponse +func updateRole(w http.ResponseWriter, r *http.Request) { + var userRole models.UserRolePermissionTemplate + err := json.NewDecoder(r.Body).Decode(&userRole) + if err != nil { + slog.Error("error decoding request body", "error", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + currRole, err := logic.GetRole(userRole.ID) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = proLogic.ValidateUpdateRoleReq(&userRole) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + userRole.GlobalLevelAccess = make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope) + err = proLogic.UpdateRole(userRole) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + // reset configs for service user + go proLogic.UpdatesUserGwAccessOnRoleUpdates(currRole.NetworkLevelAccess, userRole.NetworkLevelAccess, string(userRole.NetworkID)) + logic.ReturnSuccessResponseWithJson(w, r, userRole, "updated user role") +} + +// @Summary Delete user role permission template. +// @Router /api/v1/user/role [delete] +// @Tags Users +// @Param role_id param string true "roleid required to delete the role" +// @Success 200 {string} string +// @Failure 500 {object} models.ErrorResponse +func deleteRole(w http.ResponseWriter, r *http.Request) { + + rid, _ := url.QueryUnescape(r.URL.Query().Get("role_id")) + if rid == "" { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest")) + return + } + role, err := logic.GetRole(models.UserRoleID(rid)) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest")) + return + } + err = proLogic.DeleteRole(models.UserRoleID(rid), false) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + go proLogic.UpdatesUserGwAccessOnRoleUpdates(role.NetworkLevelAccess, make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope), role.NetworkID.String()) + logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user role") } // @Summary Attach user to a remote access gateway @@ -72,15 +668,8 @@ func attachUserToRemoteAccessGw(w http.ResponseWriter, r *http.Request) { ) return } - if user.IsAdmin || user.IsSuperAdmin { - logic.ReturnErrorResponse( - w, - r, - logic.FormatError( - errors.New("superadmins/admins have access to all gateways"), - "badrequest", - ), - ) + if user.PlatformRoleID == models.AdminRole || user.PlatformRoleID == models.SuperAdminRole { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("superadmins/admins have access to all gateways"), "badrequest")) return } node, err := logic.GetNodeByID(remoteGwID) @@ -207,31 +796,30 @@ func removeUserFromRemoteAccessGW(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(logic.ToReturnUser(*user)) } -// @Summary Get user's remote access gateways +// @Summary Get Users Remote Access Gw. // @Router /api/users/{username}/remote_access_gw [get] -// @Tags PRO -// @Accept json -// @Produce json -// @Param username path string true "Username" -// @Param remote_access_clientid query string false "Remote Access Client ID" -// @Param from_mobile query boolean false "Request from mobile" -// @Success 200 {array} models.UserRemoteGws -// @Failure 400 {object} models.ErrorResponse +// @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 getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) { +func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) { // set header. w.Header().Set("Content-Type", "application/json") - + logger.Log(0, "------------> 1. getUserRemoteAccessGwsV1") var params = mux.Vars(r) username := params["username"] if username == "" { - logic.ReturnErrorResponse( - w, - r, - logic.FormatError(errors.New("required params username"), "badrequest"), - ) + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params username"), "badrequest")) return } + logger.Log(0, "------------> 2. getUserRemoteAccessGwsV1") + 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 + } + logger.Log(0, "------------> 3. getUserRemoteAccessGwsV1") remoteAccessClientID := r.URL.Query().Get("remote_access_clientid") var req models.UserRemoteGwsReq if remoteAccessClientID == "" { @@ -242,115 +830,32 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) { return } } + logger.Log(0, "------------> 4. getUserRemoteAccessGwsV1") reqFromMobile := r.URL.Query().Get("from_mobile") == "true" if req.RemoteAccessClientID == "" && remoteAccessClientID == "" { - logic.ReturnErrorResponse( - w, - r, - logic.FormatError(errors.New("remote access client id cannot be empty"), "badrequest"), - ) + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("remote access client id cannot be empty"), "badrequest")) return } if req.RemoteAccessClientID == "" { req.RemoteAccessClientID = remoteAccessClientID } userGws := make(map[string][]models.UserRemoteGws) - 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 - } + logger.Log(0, "------------> 5. getUserRemoteAccessGwsV1") allextClients, err := logic.GetAllExtClients() if err != nil { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - - processedAdminNodeIds := make(map[string]struct{}) + logger.Log(0, "------------> 6. getUserRemoteAccessGwsV1") + userGwNodes := proLogic.GetUserRAGNodes(*user) + logger.Log(0, fmt.Sprintf("1. User Gw Nodes: %+v", userGwNodes)) for _, extClient := range allextClients { - if extClient.RemoteAccessClientID == req.RemoteAccessClientID && - extClient.OwnerID == username { - node, err := logic.GetNodeByID(extClient.IngressGatewayID) - if err != nil { - continue - } - if node.PendingDelete { - continue - } - if !node.IsIngressGateway { - continue - } - host, err := logic.GetHost(node.HostID.String()) - if err != nil { - continue - } - network, err := logic.GetNetwork(node.Network) - if err != nil { - slog.Error("failed to get node network", "error", err) - } - - if _, ok := user.RemoteGwIDs[node.ID.String()]; (!user.IsAdmin && !user.IsSuperAdmin) && - ok { - gws := userGws[node.Network] - extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient) - gws = append(gws, models.UserRemoteGws{ - GwID: node.ID.String(), - GWName: host.Name, - Network: node.Network, - GwClient: extClient, - 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}, - }) - userGws[node.Network] = gws - delete(user.RemoteGwIDs, node.ID.String()) - } else { - gws := userGws[node.Network] - extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient) - gws = append(gws, models.UserRemoteGws{ - GwID: node.ID.String(), - GWName: host.Name, - Network: node.Network, - GwClient: extClient, - 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}, - }) - userGws[node.Network] = gws - processedAdminNodeIds[node.ID.String()] = struct{}{} - } + node, ok := userGwNodes[extClient.IngressGatewayID] + if !ok { + continue } - } + if extClient.RemoteAccessClientID == req.RemoteAccessClientID && extClient.OwnerID == username { - // add remaining gw nodes to resp - if !user.IsAdmin && !user.IsSuperAdmin { - for gwID := range user.RemoteGwIDs { - node, err := logic.GetNodeByID(gwID) - if err != nil { - continue - } - if !node.IsIngressGateway { - continue - } - if node.PendingDelete { - continue - } host, err := logic.GetHost(node.HostID.String()) if err != nil { continue @@ -359,12 +864,15 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) { if err != nil { slog.Error("failed to get node network", "error", err) } - gws := userGws[node.Network] + gws := userGws[node.Network] + extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient) gws = append(gws, models.UserRemoteGws{ GwID: node.ID.String(), GWName: host.Name, Network: node.Network, + GwClient: extClient, + Connected: true, IsInternetGateway: node.IsInternetGateway, GwPeerPublicKey: host.PublicKey.String(), GwListenPort: logic.GetPeerListenPort(host), @@ -373,43 +881,49 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) { NetworkAddresses: []string{network.AddressRange, network.AddressRange6}, }) userGws[node.Network] = gws - } - } else { - allNodes, err := logic.GetAllNodes() - if err != nil { - slog.Error("failed to fetch all nodes", "error", err) - logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) - return - } - for _, node := range allNodes { - _, ok := processedAdminNodeIds[node.ID.String()] - if node.IsIngressGateway && !node.PendingDelete && !ok { - host, err := logic.GetHost(node.HostID.String()) - if err != nil { - slog.Error("failed to fetch host", "error", err) - continue - } - network, err := logic.GetNetwork(node.Network) - if err != nil { - slog.Error("failed to get node network", "error", err) - } - gws := userGws[node.Network] - - gws = append(gws, models.UserRemoteGws{ - GwID: node.ID.String(), - GWName: host.Name, - Network: node.Network, - IsInternetGateway: node.IsInternetGateway, - GwPeerPublicKey: host.PublicKey.String(), - GwListenPort: logic.GetPeerListenPort(host), - Metadata: node.Metadata, - AllowedEndpoints: getAllowedRagEndpoints(&node, host), - NetworkAddresses: []string{network.AddressRange, network.AddressRange6}, - }) - userGws[node.Network] = gws - } + delete(userGwNodes, node.ID.String()) } } + logger.Log(0, fmt.Sprintf("2. User Gw Nodes: %+v", userGwNodes)) + // add remaining gw nodes to resp + for gwID := range userGwNodes { + logger.Log(0, "RAG ---> 1") + node, err := logic.GetNodeByID(gwID) + if err != nil { + continue + } + if !node.IsIngressGateway { + continue + } + if node.PendingDelete { + continue + } + logger.Log(0, "RAG ---> 2") + host, err := logic.GetHost(node.HostID.String()) + if err != nil { + continue + } + network, err := logic.GetNetwork(node.Network) + if err != nil { + slog.Error("failed to get node network", "error", err) + } + logger.Log(0, "RAG ---> 3") + gws := userGws[node.Network] + + gws = append(gws, models.UserRemoteGws{ + GwID: node.ID.String(), + GWName: host.Name, + Network: node.Network, + IsInternetGateway: node.IsInternetGateway, + GwPeerPublicKey: host.PublicKey.String(), + GwListenPort: logic.GetPeerListenPort(host), + Metadata: node.Metadata, + AllowedEndpoints: getAllowedRagEndpoints(&node, host), + NetworkAddresses: []string{network.AddressRange, network.AddressRange6}, + }) + userGws[node.Network] = gws + } + if reqFromMobile { // send resp in array format userGwsArr := []models.UserRemoteGws{} @@ -478,3 +992,114 @@ func getAllowedRagEndpoints(ragNode *models.Node, ragHost *models.Host) []string } return endpoints } + +// @Summary Get all pending users +// @Router /api/users_pending [get] +// @Tags Users +// @Success 200 {array} models.User +// @Failure 500 {object} models.ErrorResponse +func getPendingUsers(w http.ResponseWriter, r *http.Request) { + // set header. + w.Header().Set("Content-Type", "application/json") + + users, err := logic.ListPendingUsers() + if err != nil { + logger.Log(0, "failed to fetch users: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + + logic.SortUsers(users[:]) + logger.Log(2, r.Header.Get("user"), "fetched pending users") + json.NewEncoder(w).Encode(users) +} + +// @Summary Approve a pending user +// @Router /api/users_pending/user/{username} [post] +// @Tags Users +// @Param username path string true "Username of the pending user to approve" +// @Success 200 {string} string +// @Failure 500 {object} models.ErrorResponse +func approvePendingUser(w http.ResponseWriter, r *http.Request) { + // set header. + w.Header().Set("Content-Type", "application/json") + var params = mux.Vars(r) + username := params["username"] + users, err := logic.ListPendingUsers() + + if err != nil { + logger.Log(0, "failed to fetch users: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + for _, user := range users { + if user.UserName == username { + var newPass, fetchErr = logic.FetchPassValue("") + if fetchErr != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(fetchErr, "internal")) + return + } + if err = logic.CreateUser(&models.User{ + UserName: user.UserName, + Password: newPass, + PlatformRoleID: models.ServiceUser, + }); err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to create user: %s", err), "internal")) + return + } + err = logic.DeletePendingUser(username) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete pending user: %s", err), "internal")) + return + } + break + } + } + logic.ReturnSuccessResponse(w, r, "approved "+username) +} + +// @Summary Delete a pending user +// @Router /api/users_pending/user/{username} [delete] +// @Tags Users +// @Param username path string true "Username of the pending user to delete" +// @Success 200 {string} string +// @Failure 500 {object} models.ErrorResponse +func deletePendingUser(w http.ResponseWriter, r *http.Request) { + // set header. + w.Header().Set("Content-Type", "application/json") + var params = mux.Vars(r) + username := params["username"] + users, err := logic.ListPendingUsers() + + if err != nil { + logger.Log(0, "failed to fetch users: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + for _, user := range users { + if user.UserName == username { + err = logic.DeletePendingUser(username) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete pending user: %s", err), "internal")) + return + } + break + } + } + logic.ReturnSuccessResponse(w, r, "deleted pending "+username) +} + +// @Summary Delete all pending users +// @Router /api/users_pending [delete] +// @Tags Users +// @Success 200 {string} string +// @Failure 500 {object} models.ErrorResponse +func deleteAllPendingUsers(w http.ResponseWriter, r *http.Request) { + // set header. + err := database.DeleteAllRecords(database.PENDING_USERS_TABLE_NAME) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending users "+err.Error()), "internal")) + return + } + logic.ReturnSuccessResponse(w, r, "cleared all pending users") +} diff --git a/pro/email/email.go b/pro/email/email.go new file mode 100644 index 00000000..65054842 --- /dev/null +++ b/pro/email/email.go @@ -0,0 +1,53 @@ +package email + +import ( + "context" + + "github.com/gravitl/netmaker/servercfg" +) + +type EmailSenderType string + +var client EmailSender + +const ( + Smtp EmailSenderType = "smtp" + Resend EmailSenderType = "resend" +) + +func init() { + switch EmailSenderType(servercfg.EmailSenderType()) { + case Smtp: + client = &SmtpSender{ + SmtpHost: servercfg.GetSmtpHost(), + SmtpPort: servercfg.GetSmtpPort(), + SenderEmail: servercfg.GetSenderEmail(), + SenderPass: servercfg.GetEmaiSenderAuth(), + } + case Resend: + client = NewResendEmailSenderFromConfig() + } + client = GetClient() +} + +// EmailSender - an interface for sending emails based on notifications and mail templates +type EmailSender interface { + // SendEmail - sends an email based on a context, notification and mail template + SendEmail(ctx context.Context, notification Notification, email Mail) error +} + +type Mail interface { + GetBody(info Notification) string + GetSubject(info Notification) string +} + +// Notification - struct for notification details +type Notification struct { + RecipientMail string + RecipientName string + ProductName string +} + +func GetClient() (e EmailSender) { + return client +} diff --git a/pro/email/invite.go b/pro/email/invite.go new file mode 100644 index 00000000..c2b39bd2 --- /dev/null +++ b/pro/email/invite.go @@ -0,0 +1,27 @@ +package email + +import ( + "fmt" +) + +// UserInvitedMail - mail for users that are invited to a tenant +type UserInvitedMail struct { + BodyBuilder EmailBodyBuilder + InviteURL string +} + +// GetSubject - gets the subject of the email +func (UserInvitedMail) GetSubject(info Notification) string { + return "Netmaker: Pending Invitation" +} + +// GetBody - gets the body of the email +func (invite UserInvitedMail) GetBody(info Notification) string { + + return invite.BodyBuilder. + WithHeadline("Join Netmaker from this invite!"). + WithParagraph("Hello from Netmaker,"). + WithParagraph("You have been invited to join Netmaker."). + WithParagraph(fmt.Sprintf("Join Using This Invite Link Netmaker", invite.InviteURL)). + Build() +} diff --git a/pro/email/resend.go b/pro/email/resend.go new file mode 100644 index 00000000..1125bc7e --- /dev/null +++ b/pro/email/resend.go @@ -0,0 +1,55 @@ +package email + +import ( + "context" + "fmt" + + "github.com/gravitl/netmaker/servercfg" + "github.com/resendlabs/resend-go" +) + +// ResendEmailSender - implementation of EmailSender using Resend (https://resend.com) +type ResendEmailSender struct { + client ResendClient + from string +} + +// ResendClient - dependency interface for resend client +type ResendClient interface { + Send(*resend.SendEmailRequest) (resend.SendEmailResponse, error) +} + +// NewResendEmailSender - constructs a ResendEmailSender +func NewResendEmailSender(client ResendClient, from string) ResendEmailSender { + return ResendEmailSender{client: client, from: from} +} + +// NewResendEmailSender - constructs a ResendEmailSender from config +// TODO let main.go handle this and use dependency injection instead of calling this function +func NewResendEmailSenderFromConfig() ResendEmailSender { + key, from := servercfg.GetEmaiSenderAuth(), servercfg.GetSenderEmail() + resender := resend.NewClient(key) + return NewResendEmailSender(resender.Emails, from) +} + +// SendEmail - sends an email using resend-go (https://github.com/resendlabs/resend-go) +func (es ResendEmailSender) SendEmail(ctx context.Context, notification Notification, email Mail) error { + var ( + from = es.from + to = notification.RecipientMail + subject = email.GetSubject(notification) + body = email.GetBody(notification) + ) + params := resend.SendEmailRequest{ + From: from, + To: []string{to}, + Subject: subject, + Html: body, + } + _, err := es.client.Send(¶ms) + if err != nil { + return fmt.Errorf("failed sending mail via resend: %w", err) + } + + return nil +} diff --git a/pro/email/smtp.go b/pro/email/smtp.go new file mode 100644 index 00000000..89965ca5 --- /dev/null +++ b/pro/email/smtp.go @@ -0,0 +1,42 @@ +package email + +import ( + "context" + "crypto/tls" + + gomail "gopkg.in/mail.v2" +) + +type SmtpSender struct { + SmtpHost string + SmtpPort int + SenderEmail string + SenderPass string +} + +func (s *SmtpSender) SendEmail(ctx context.Context, n Notification, e Mail) error { + m := gomail.NewMessage() + + // Set E-Mail sender + m.SetHeader("From", s.SenderEmail) + + // Set E-Mail receivers + m.SetHeader("To", n.RecipientMail) + // Set E-Mail subject + m.SetHeader("Subject", e.GetSubject(n)) + // Set E-Mail body. You can set plain text or html with text/html + m.SetBody("text/html", e.GetBody(n)) + // Settings for SMTP server + d := gomail.NewDialer(s.SmtpHost, s.SmtpPort, s.SenderEmail, s.SenderPass) + + // This is only needed when SSL/TLS certificate is not valid on server. + // In production this should be set to false. + d.TLSConfig = &tls.Config{InsecureSkipVerify: true} + + // Now send E-Mail + if err := d.DialAndSend(m); err != nil { + return err + } + + return nil +} diff --git a/pro/email/utils.go b/pro/email/utils.go new file mode 100644 index 00000000..69a58fe0 --- /dev/null +++ b/pro/email/utils.go @@ -0,0 +1,567 @@ +package email + +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 + WithSignature() EmailBodyBuilder + Build() string +} + +type EmailBodyBuilderWithH1HeadlineAndImage struct { + headline string + paragraphs []string + hasSignature bool +} + +func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithHeadline(text string) EmailBodyBuilder { + b.headline = text + return b +} + +func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithParagraph(text string) EmailBodyBuilder { + b.paragraphs = append(b.paragraphs, text) + return b +} + +func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithSignature() EmailBodyBuilder { + b.hasSignature = true + return b +} + +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, "") + + signature := "" + if b.hasSignature { + signature = styledSignature() + } + + return ` + + + + + + + + + + + + + + + + + + + + ` + signature + ` + ` +} + +func styledSignature() string { + return ` + ` +} + +func styledParagraph(text string) string { + return `

+ ` + text + ` +

` +} + +func GetMailSignature() string { + return styledSignature() +} diff --git a/pro/initialize.go b/pro/initialize.go index 8da33bfc..948f8457 100644 --- a/pro/initialize.go +++ b/pro/initialize.go @@ -119,6 +119,19 @@ func InitPro() { logic.GetAllowedIpForInetNodeClient = proLogic.GetAllowedIpForInetNodeClient mq.UpdateMetrics = proLogic.MQUpdateMetrics mq.UpdateMetricsFallBack = proLogic.MQUpdateMetricsFallBack + logic.GetFilteredNodesByUserAccess = proLogic.GetFilteredNodesByUserAccess + logic.CreateRole = proLogic.CreateRole + logic.UpdateRole = proLogic.UpdateRole + logic.DeleteRole = proLogic.DeleteRole + logic.NetworkPermissionsCheck = proLogic.NetworkPermissionsCheck + logic.GlobalPermissionsCheck = proLogic.GlobalPermissionsCheck + logic.DeleteNetworkRoles = proLogic.DeleteNetworkRoles + logic.CreateDefaultNetworkRolesAndGroups = proLogic.CreateDefaultNetworkRolesAndGroups + logic.FilterNetworksByRole = proLogic.FilterNetworksByRole + logic.IsGroupsValid = proLogic.IsGroupsValid + logic.IsNetworkRolesValid = proLogic.IsNetworkRolesValid + logic.InitialiseRoles = proLogic.UserRolesInit + logic.UpdateUserGwAccess = proLogic.UpdateUserGwAccess } func retrieveProLogo() string { diff --git a/pro/logic/security.go b/pro/logic/security.go new file mode 100644 index 00000000..3225c269 --- /dev/null +++ b/pro/logic/security.go @@ -0,0 +1,195 @@ +package logic + +import ( + "errors" + "fmt" + "net/http" + + "github.com/gravitl/netmaker/logger" + "github.com/gravitl/netmaker/logic" + "github.com/gravitl/netmaker/models" +) + +func NetworkPermissionsCheck(username string, r *http.Request) error { + // at this point global checks should be completed + user, err := logic.GetUser(username) + if err != nil { + return err + } + logger.Log(0, "NET MIDDL----> 1") + userRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + return errors.New("access denied") + } + if userRole.FullAccess { + return nil + } + logger.Log(0, "NET MIDDL----> 2") + // get info from header to determine the target rsrc + targetRsrc := r.Header.Get("TARGET_RSRC") + targetRsrcID := r.Header.Get("TARGET_RSRC_ID") + netID := r.Header.Get("NET_ID") + if targetRsrc == "" { + return errors.New("target rsrc is missing") + } + if netID == "" { + return errors.New("network id is missing") + } + if r.Method == "" { + r.Method = http.MethodGet + } + if targetRsrc == models.MetricRsrc.String() { + return nil + } + + // check if user has scope for target resource + // TODO - differentitate between global scope and network scope apis + // check for global network role + if netRoles, ok := user.NetworkRoles[models.AllNetworks]; ok { + for netRoleID := range netRoles { + err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID) + if err == nil { + return nil + } + } + } + netRoles := user.NetworkRoles[models.NetworkID(netID)] + for netRoleID := range netRoles { + err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID) + if err == nil { + return nil + } + } + for groupID := range user.UserGroups { + userG, err := GetUserGroup(groupID) + if err == nil { + netRoles := userG.NetworkRoles[models.NetworkID(netID)] + for netRoleID := range netRoles { + err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID) + if err == nil { + return nil + } + } + } + } + + return errors.New("access denied") +} + +func checkNetworkAccessPermissions(netRoleID models.UserRoleID, username, reqScope, targetRsrc, targetRsrcID, netID string) error { + networkPermissionScope, err := logic.GetRole(netRoleID) + if err != nil { + return err + } + logger.Log(0, "NET MIDDL----> 3", string(netRoleID)) + if networkPermissionScope.FullAccess { + return nil + } + rsrcPermissionScope, ok := networkPermissionScope.NetworkLevelAccess[models.RsrcType(targetRsrc)] + if targetRsrc == models.HostRsrc.String() && !ok { + rsrcPermissionScope, ok = networkPermissionScope.NetworkLevelAccess[models.RemoteAccessGwRsrc] + } + if !ok { + return errors.New("access denied") + } + logger.Log(0, "NET MIDDL----> 4", string(netRoleID)) + if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[models.RsrcID(fmt.Sprintf("all_%s", targetRsrc))]; ok { + // handle extclient apis here + if models.RsrcType(targetRsrc) == models.ExtClientsRsrc && allRsrcsTypePermissionScope.SelfOnly && targetRsrcID != "" { + extclient, err := logic.GetExtClient(targetRsrcID, netID) + if err != nil { + return err + } + if !logic.IsUserAllowedAccessToExtClient(username, extclient) { + return errors.New("access denied") + } + } + err = checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, reqScope) + if err == nil { + return nil + } + + } + if targetRsrc == models.HostRsrc.String() { + if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[models.RsrcID(fmt.Sprintf("all_%s", models.RemoteAccessGwRsrc))]; ok { + err = checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, reqScope) + if err == nil { + return nil + } + } + } + logger.Log(0, "NET MIDDL----> 5", string(netRoleID)) + if targetRsrcID == "" { + return errors.New("target rsrc id is empty") + } + if scope, ok := rsrcPermissionScope[models.RsrcID(targetRsrcID)]; ok { + err = checkPermissionScopeWithReqMethod(scope, reqScope) + if err == nil { + return nil + } + } + logger.Log(0, "NET MIDDL----> 6", string(netRoleID)) + return errors.New("access denied") +} + +func GlobalPermissionsCheck(username string, r *http.Request) error { + user, err := logic.GetUser(username) + if err != nil { + return err + } + userRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + return errors.New("access denied") + } + if userRole.FullAccess { + return nil + } + targetRsrc := r.Header.Get("TARGET_RSRC") + targetRsrcID := r.Header.Get("TARGET_RSRC_ID") + if targetRsrc == "" { + return errors.New("target rsrc is missing") + } + if r.Method == "" { + r.Method = http.MethodGet + } + if targetRsrc == models.MetricRsrc.String() { + return nil + } + if (targetRsrc == models.HostRsrc.String() || targetRsrc == models.NetworkRsrc.String()) && r.Method == http.MethodGet && targetRsrcID == "" { + return nil + } + if targetRsrc == models.UserRsrc.String() && username == targetRsrcID && (r.Method != http.MethodDelete) { + return nil + } + rsrcPermissionScope, ok := userRole.GlobalLevelAccess[models.RsrcType(targetRsrc)] + if !ok { + return fmt.Errorf("access denied to %s", targetRsrc) + } + if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[models.RsrcID(fmt.Sprintf("all_%s", targetRsrc))]; ok { + return checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, r.Method) + + } + if targetRsrcID == "" { + return errors.New("target rsrc id is missing") + } + if scope, ok := rsrcPermissionScope[models.RsrcID(targetRsrcID)]; ok { + return checkPermissionScopeWithReqMethod(scope, r.Method) + } + return errors.New("access denied") +} + +func checkPermissionScopeWithReqMethod(scope models.RsrcPermissionScope, reqmethod string) error { + if reqmethod == http.MethodGet && scope.Read { + return nil + } + if (reqmethod == http.MethodPatch || reqmethod == http.MethodPut) && scope.Update { + return nil + } + if reqmethod == http.MethodDelete && scope.Delete { + return nil + } + if reqmethod == http.MethodPost && scope.Create { + return nil + } + return errors.New("operation not permitted") +} diff --git a/pro/logic/user_mgmt.go b/pro/logic/user_mgmt.go new file mode 100644 index 00000000..243f3a97 --- /dev/null +++ b/pro/logic/user_mgmt.go @@ -0,0 +1,1057 @@ +package logic + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/gravitl/netmaker/database" + "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" + "golang.org/x/exp/slog" +) + +var ServiceUserPermissionTemplate = models.UserRolePermissionTemplate{ + ID: models.ServiceUser, + Default: true, + FullAccess: false, + DenyDashboardAccess: true, +} + +var PlatformUserUserPermissionTemplate = models.UserRolePermissionTemplate{ + ID: models.PlatformUser, + Default: true, + FullAccess: false, +} + +var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{ + ID: models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkAdmin)), + Default: true, + FullAccess: true, + NetworkID: models.AllNetworks, +} + +var NetworkUserAllPermissionTemplate = models.UserRolePermissionTemplate{ + ID: models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkUser)), + Default: true, + FullAccess: false, + NetworkID: models.AllNetworks, + NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{ + models.RemoteAccessGwRsrc: { + models.AllRemoteAccessGwRsrcID: models.RsrcPermissionScope{ + Read: true, + VPNaccess: true, + }, + }, + models.ExtClientsRsrc: { + models.AllExtClientsRsrcID: models.RsrcPermissionScope{ + Read: true, + Create: true, + Update: true, + Delete: true, + SelfOnly: true, + }, + }, + }, +} + +func UserRolesInit() { + d, _ := json.Marshal(logic.SuperAdminPermissionTemplate) + database.Insert(logic.SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) + d, _ = json.Marshal(logic.AdminPermissionTemplate) + database.Insert(logic.AdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) + d, _ = json.Marshal(ServiceUserPermissionTemplate) + database.Insert(ServiceUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) + d, _ = json.Marshal(PlatformUserUserPermissionTemplate) + database.Insert(PlatformUserUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) + d, _ = json.Marshal(NetworkAdminAllPermissionTemplate) + database.Insert(NetworkAdminAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) + d, _ = json.Marshal(NetworkUserAllPermissionTemplate) + database.Insert(NetworkUserAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) + +} + +func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) { + var NetworkAdminPermissionTemplate = models.UserRolePermissionTemplate{ + ID: models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)), + Default: true, + NetworkID: netID, + FullAccess: true, + NetworkLevelAccess: make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope), + } + + var NetworkUserPermissionTemplate = models.UserRolePermissionTemplate{ + ID: models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)), + Default: true, + FullAccess: false, + NetworkID: netID, + DenyDashboardAccess: false, + NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{ + models.RemoteAccessGwRsrc: { + models.AllRemoteAccessGwRsrcID: models.RsrcPermissionScope{ + Read: true, + VPNaccess: true, + }, + }, + models.ExtClientsRsrc: { + models.AllExtClientsRsrcID: models.RsrcPermissionScope{ + Read: true, + Create: true, + Update: true, + Delete: true, + SelfOnly: true, + }, + }, + }, + } + d, _ := json.Marshal(NetworkAdminPermissionTemplate) + database.Insert(NetworkAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) + d, _ = json.Marshal(NetworkUserPermissionTemplate) + database.Insert(NetworkUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) + + // create default network groups + var NetworkAdminGroup = models.UserGroup{ + ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)), + NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{ + netID: { + models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)): {}, + }, + }, + MetaData: "The network role was automatically created by Netmaker.", + } + var NetworkUserGroup = models.UserGroup{ + ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)), + NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{ + netID: { + models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)): {}, + }, + }, + MetaData: "The network role was automatically created by Netmaker.", + } + 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) { + users, err := logic.GetUsersDB() + if err != nil { + return + } + defaultUserGrp := fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser) + defaultAdminGrp := fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin) + for _, user := range users { + var upsert bool + if _, ok := user.NetworkRoles[models.NetworkID(netID)]; ok { + delete(user.NetworkRoles, models.NetworkID(netID)) + upsert = true + } + if _, ok := user.UserGroups[models.UserGroupID(defaultUserGrp)]; ok { + delete(user.UserGroups, models.UserGroupID(defaultUserGrp)) + upsert = true + } + if _, ok := user.UserGroups[models.UserGroupID(defaultAdminGrp)]; ok { + delete(user.UserGroups, models.UserGroupID(defaultAdminGrp)) + upsert = true + } + if upsert { + logic.UpsertUser(user) + } + } + database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, defaultUserGrp) + database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, defaultAdminGrp) + userGs, _ := ListUserGroups() + for _, userGI := range userGs { + if _, ok := userGI.NetworkRoles[models.NetworkID(netID)]; ok { + delete(userGI.NetworkRoles, models.NetworkID(netID)) + UpdateUserGroup(userGI) + } + } + + roles, _ := ListNetworkRoles() + for _, role := range roles { + if role.NetworkID.String() == netID { + database.DeleteRecord(database.USER_PERMISSIONS_TABLE_NAME, role.ID.String()) + } + } +} + +// ListNetworkRoles - lists user network roles permission templates +func ListNetworkRoles() ([]models.UserRolePermissionTemplate, error) { + data, err := database.FetchRecords(database.USER_PERMISSIONS_TABLE_NAME) + if err != nil && !database.IsEmptyRecord(err) { + return []models.UserRolePermissionTemplate{}, err + } + userRoles := []models.UserRolePermissionTemplate{} + for _, dataI := range data { + userRole := models.UserRolePermissionTemplate{} + err := json.Unmarshal([]byte(dataI), &userRole) + if err != nil { + continue + } + if userRole.NetworkID == "" { + continue + } + userRoles = append(userRoles, userRole) + } + return userRoles, nil +} + +// ListPlatformRoles - lists user platform roles permission templates +func ListPlatformRoles() ([]models.UserRolePermissionTemplate, error) { + data, err := database.FetchRecords(database.USER_PERMISSIONS_TABLE_NAME) + if err != nil && !database.IsEmptyRecord(err) { + return []models.UserRolePermissionTemplate{}, err + } + userRoles := []models.UserRolePermissionTemplate{} + for _, dataI := range data { + userRole := models.UserRolePermissionTemplate{} + err := json.Unmarshal([]byte(dataI), &userRole) + if err != nil { + continue + } + if userRole.NetworkID != "" { + continue + } + userRoles = append(userRoles, userRole) + } + return userRoles, nil +} + +func ValidateCreateRoleReq(userRole *models.UserRolePermissionTemplate) error { + // check if role exists with this id + _, err := logic.GetRole(userRole.ID) + if err == nil { + return fmt.Errorf("role with id `%s` exists already", userRole.ID.String()) + } + if len(userRole.NetworkLevelAccess) > 0 { + for rsrcType := range userRole.NetworkLevelAccess { + if _, ok := models.RsrcTypeMap[rsrcType]; !ok { + return errors.New("invalid rsrc type " + rsrcType.String()) + } + if rsrcType == models.RemoteAccessGwRsrc { + userRsrcPermissions := userRole.NetworkLevelAccess[models.RemoteAccessGwRsrc] + var vpnAccess bool + for _, scope := range userRsrcPermissions { + if scope.VPNaccess { + vpnAccess = true + break + } + } + if vpnAccess { + userRole.NetworkLevelAccess[models.ExtClientsRsrc] = map[models.RsrcID]models.RsrcPermissionScope{ + models.AllExtClientsRsrcID: { + Read: true, + Create: true, + Update: true, + Delete: true, + SelfOnly: true, + }, + } + + } + + } + } + } + if userRole.NetworkID == "" { + return errors.New("only network roles are allowed to be created") + } + return nil +} + +func ValidateUpdateRoleReq(userRole *models.UserRolePermissionTemplate) error { + roleInDB, err := logic.GetRole(userRole.ID) + if err != nil { + return err + } + if roleInDB.NetworkID != userRole.NetworkID { + return errors.New("network id mismatch") + } + if roleInDB.Default { + return errors.New("cannot update default role") + } + if len(userRole.NetworkLevelAccess) > 0 { + for rsrcType := range userRole.NetworkLevelAccess { + if _, ok := models.RsrcTypeMap[rsrcType]; !ok { + return errors.New("invalid rsrc type " + rsrcType.String()) + } + if rsrcType == models.RemoteAccessGwRsrc { + userRsrcPermissions := userRole.NetworkLevelAccess[models.RemoteAccessGwRsrc] + var vpnAccess bool + for _, scope := range userRsrcPermissions { + if scope.VPNaccess { + vpnAccess = true + break + } + } + if vpnAccess { + userRole.NetworkLevelAccess[models.ExtClientsRsrc] = map[models.RsrcID]models.RsrcPermissionScope{ + models.AllExtClientsRsrcID: { + Read: true, + Create: true, + Update: true, + Delete: true, + SelfOnly: true, + }, + } + + } + + } + } + } + return nil +} + +// CreateRole - inserts new role into DB +func CreateRole(r models.UserRolePermissionTemplate) error { + // check if role already exists + if r.ID.String() == "" { + return errors.New("role id cannot be empty") + } + _, err := database.FetchRecord(database.USER_PERMISSIONS_TABLE_NAME, r.ID.String()) + if err == nil { + return errors.New("role already exists") + } + d, err := json.Marshal(r) + if err != nil { + return err + } + return database.Insert(r.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) +} + +// UpdateRole - updates role template +func UpdateRole(r models.UserRolePermissionTemplate) error { + if r.ID.String() == "" { + return errors.New("role id cannot be empty") + } + _, err := database.FetchRecord(database.USER_PERMISSIONS_TABLE_NAME, r.ID.String()) + if err != nil { + return err + } + d, err := json.Marshal(r) + if err != nil { + return err + } + return database.Insert(r.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) +} + +// DeleteRole - deletes user role +func DeleteRole(rid models.UserRoleID, force bool) error { + if rid.String() == "" { + return errors.New("role id cannot be empty") + } + users, err := logic.GetUsersDB() + if err != nil { + return err + } + role, err := logic.GetRole(rid) + if err != nil { + return err + } + if role.NetworkID == "" { + return errors.New("cannot delete platform role") + } + // allow deletion of default network roles if network doesn't exist + if role.NetworkID == models.AllNetworks { + return errors.New("cannot delete default network role") + } + // check if network exists + exists, _ := logic.NetworkExists(role.NetworkID.String()) + if role.Default { + if exists && !force { + return errors.New("cannot delete default role") + } + } + for _, user := range users { + for userG := range user.UserGroups { + ug, err := GetUserGroup(userG) + if err == nil { + if role.NetworkID != "" { + for netID, networkRoles := range ug.NetworkRoles { + if _, ok := networkRoles[rid]; ok { + delete(networkRoles, rid) + ug.NetworkRoles[netID] = networkRoles + UpdateUserGroup(ug) + } + + } + } + + } + } + + if user.PlatformRoleID == rid { + err = errors.New("active roles cannot be deleted.switch existing users to a new role before deleting") + return err + } + if role.NetworkID != "" { + for netID, networkRoles := range user.NetworkRoles { + if _, ok := networkRoles[rid]; ok { + delete(networkRoles, rid) + user.NetworkRoles[netID] = networkRoles + logic.UpsertUser(user) + } + + } + } + } + + return database.DeleteRecord(database.USER_PERMISSIONS_TABLE_NAME, rid.String()) +} + +func ValidateCreateGroupReq(g models.UserGroup) error { + + // check if network roles are valid + for _, roleMap := range g.NetworkRoles { + for roleID := range roleMap { + role, err := logic.GetRole(roleID) + if err != nil { + return fmt.Errorf("invalid network role %s", roleID) + } + if role.NetworkID == "" { + return errors.New("platform role cannot be used as network role") + } + } + } + return nil +} +func ValidateUpdateGroupReq(g models.UserGroup) error { + + for networkID := range g.NetworkRoles { + userRolesMap := g.NetworkRoles[networkID] + for roleID := range userRolesMap { + netRole, err := logic.GetRole(roleID) + if err != nil { + err = fmt.Errorf("invalid network role") + return err + } + if netRole.NetworkID == "" { + return errors.New("platform role cannot be used as network role") + } + } + } + return nil +} + +// CreateUserGroup - creates new user group +func CreateUserGroup(g models.UserGroup) error { + // check if role already exists + if g.ID == "" { + return errors.New("group id cannot be empty") + } + _, err := database.FetchRecord(database.USER_GROUPS_TABLE_NAME, g.ID.String()) + if err == nil { + return errors.New("group already exists") + } + d, err := json.Marshal(g) + if err != nil { + return err + } + return database.Insert(g.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME) +} + +// GetUserGroup - fetches user group +func GetUserGroup(gid models.UserGroupID) (models.UserGroup, error) { + d, err := database.FetchRecord(database.USER_GROUPS_TABLE_NAME, gid.String()) + if err != nil { + return models.UserGroup{}, err + } + var ug models.UserGroup + err = json.Unmarshal([]byte(d), &ug) + if err != nil { + return ug, err + } + return ug, nil +} + +// ListUserGroups - lists user groups +func ListUserGroups() ([]models.UserGroup, error) { + data, err := database.FetchRecords(database.USER_GROUPS_TABLE_NAME) + if err != nil && !database.IsEmptyRecord(err) { + return []models.UserGroup{}, err + } + userGroups := []models.UserGroup{} + for _, dataI := range data { + userGroup := models.UserGroup{} + err := json.Unmarshal([]byte(dataI), &userGroup) + if err != nil { + continue + } + userGroups = append(userGroups, userGroup) + } + return userGroups, nil +} + +// UpdateUserGroup - updates new user group +func UpdateUserGroup(g models.UserGroup) error { + // check if group exists + if g.ID == "" { + return errors.New("group id cannot be empty") + } + _, err := database.FetchRecord(database.USER_GROUPS_TABLE_NAME, g.ID.String()) + if err != nil { + return err + } + d, err := json.Marshal(g) + if err != nil { + return err + } + return database.Insert(g.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME) +} + +// DeleteUserGroup - deletes user group +func DeleteUserGroup(gid models.UserGroupID) error { + users, err := logic.GetUsersDB() + if err != nil { + return err + } + for _, user := range users { + delete(user.UserGroups, gid) + logic.UpsertUser(user) + } + return database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, gid.String()) +} + +func HasNetworkRsrcScope(permissionTemplate models.UserRolePermissionTemplate, netid string, rsrcType models.RsrcType, rsrcID models.RsrcID, op string) bool { + if permissionTemplate.FullAccess { + return true + } + + rsrcScope, ok := permissionTemplate.NetworkLevelAccess[rsrcType] + if !ok { + return false + } + _, ok = rsrcScope[rsrcID] + return ok +} +func GetUserRAGNodes(user models.User) (gws map[string]models.Node) { + gws = make(map[string]models.Node) + userGwAccessScope := GetUserNetworkRolesWithRemoteVPNAccess(user) + logger.Log(0, fmt.Sprintf("User Gw Access Scope: %+v", userGwAccessScope)) + _, allNetAccess := userGwAccessScope["*"] + nodes, err := logic.GetAllNodes() + if err != nil { + return + } + for _, node := range nodes { + if node.IsIngressGateway && !node.PendingDelete { + if allNetAccess { + gws[node.ID.String()] = node + } else { + gwRsrcMap := userGwAccessScope[models.NetworkID(node.Network)] + scope, ok := gwRsrcMap[models.AllRemoteAccessGwRsrcID] + if !ok { + if scope, ok = gwRsrcMap[models.RsrcID(node.ID.String())]; !ok { + continue + } + } + if scope.VPNaccess { + gws[node.ID.String()] = node + } + + } + } + } + return +} + +// GetUserNetworkRoles - get user network roles +func GetUserNetworkRolesWithRemoteVPNAccess(user models.User) (gwAccess map[models.NetworkID]map[models.RsrcID]models.RsrcPermissionScope) { + gwAccess = make(map[models.NetworkID]map[models.RsrcID]models.RsrcPermissionScope) + platformRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + return + } + if platformRole.FullAccess { + gwAccess[models.NetworkID("*")] = make(map[models.RsrcID]models.RsrcPermissionScope) + return + } + if _, ok := user.NetworkRoles[models.AllNetworks]; ok { + gwAccess[models.NetworkID("*")] = make(map[models.RsrcID]models.RsrcPermissionScope) + } + if len(user.UserGroups) > 0 { + for gID := range user.UserGroups { + userG, err := GetUserGroup(gID) + if err != nil { + continue + } + for netID, roleMap := range userG.NetworkRoles { + for roleID := range roleMap { + role, err := logic.GetRole(roleID) + if err == nil { + if role.FullAccess { + gwAccess[netID] = map[models.RsrcID]models.RsrcPermissionScope{ + models.AllRemoteAccessGwRsrcID: { + Create: true, + Read: true, + Update: true, + VPNaccess: true, + Delete: true, + }, + models.AllExtClientsRsrcID: { + Create: true, + Read: true, + Update: true, + Delete: true, + }, + } + break + } + if rsrcsMap, ok := role.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok { + if permissions, ok := rsrcsMap[models.AllRemoteAccessGwRsrcID]; ok && permissions.VPNaccess { + if len(gwAccess[netID]) == 0 { + gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope) + } + gwAccess[netID][models.AllRemoteAccessGwRsrcID] = permissions + break + } else { + for gwID, scope := range rsrcsMap { + if scope.VPNaccess { + if len(gwAccess[netID]) == 0 { + gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope) + } + gwAccess[netID][gwID] = scope + } + } + } + + } + + } + } + } + } + } + for netID, roleMap := range user.NetworkRoles { + for roleID := range roleMap { + role, err := logic.GetRole(roleID) + if err == nil { + if role.FullAccess { + gwAccess[netID] = map[models.RsrcID]models.RsrcPermissionScope{ + models.AllRemoteAccessGwRsrcID: { + Create: true, + Read: true, + Update: true, + VPNaccess: true, + Delete: true, + }, + models.AllExtClientsRsrcID: { + Create: true, + Read: true, + Update: true, + Delete: true, + }, + } + break + } + if rsrcsMap, ok := role.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok { + if permissions, ok := rsrcsMap[models.AllRemoteAccessGwRsrcID]; ok && permissions.VPNaccess { + if len(gwAccess[netID]) == 0 { + gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope) + } + gwAccess[netID][models.AllRemoteAccessGwRsrcID] = permissions + break + } else { + for gwID, scope := range rsrcsMap { + if scope.VPNaccess { + if len(gwAccess[netID]) == 0 { + gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope) + } + gwAccess[netID][gwID] = scope + } + } + } + + } + + } + } + } + + return +} + +func GetFilteredNodesByUserAccess(user models.User, nodes []models.Node) (filteredNodes []models.Node) { + + nodesMap := make(map[string]struct{}) + allNetworkRoles := make(map[models.UserRoleID]struct{}) + + if len(user.NetworkRoles) > 0 { + for _, netRoles := range user.NetworkRoles { + for netRoleI := range netRoles { + allNetworkRoles[netRoleI] = struct{}{} + } + } + } + if _, ok := user.NetworkRoles[models.AllNetworks]; ok { + return nodes + } + if len(user.UserGroups) > 0 { + for userGID := range user.UserGroups { + userG, err := GetUserGroup(userGID) + if err == nil { + if len(userG.NetworkRoles) > 0 { + if _, ok := userG.NetworkRoles[models.AllNetworks]; ok { + return nodes + } + for _, netRoles := range userG.NetworkRoles { + for netRoleI := range netRoles { + allNetworkRoles[netRoleI] = struct{}{} + } + } + } + } + } + } + for networkRoleID := range allNetworkRoles { + userPermTemplate, err := logic.GetRole(networkRoleID) + if err != nil { + continue + } + networkNodes := logic.GetNetworkNodesMemory(nodes, userPermTemplate.NetworkID.String()) + if userPermTemplate.FullAccess { + for _, node := range networkNodes { + if _, ok := nodesMap[node.ID.String()]; ok { + continue + } + nodesMap[node.ID.String()] = struct{}{} + filteredNodes = append(filteredNodes, node) + } + + continue + } + if rsrcPerms, ok := userPermTemplate.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok { + if _, ok := rsrcPerms[models.AllRemoteAccessGwRsrcID]; ok { + for _, node := range networkNodes { + if _, ok := nodesMap[node.ID.String()]; ok { + continue + } + if node.IsIngressGateway { + nodesMap[node.ID.String()] = struct{}{} + filteredNodes = append(filteredNodes, node) + } + } + } else { + for gwID, scope := range rsrcPerms { + if _, ok := nodesMap[gwID.String()]; ok { + continue + } + if scope.Read { + gwNode, err := logic.GetNodeByID(gwID.String()) + if err == nil && gwNode.IsIngressGateway { + nodesMap[gwNode.ID.String()] = struct{}{} + filteredNodes = append(filteredNodes, gwNode) + } + } + } + } + } + + } + return +} + +func FilterNetworksByRole(allnetworks []models.Network, user models.User) []models.Network { + platformRole, err := logic.GetRole(user.PlatformRoleID) + if err != nil { + return []models.Network{} + } + if !platformRole.FullAccess { + allNetworkRoles := make(map[models.NetworkID]struct{}) + if len(user.NetworkRoles) > 0 { + for netID := range user.NetworkRoles { + if netID == models.AllNetworks { + return allnetworks + } + allNetworkRoles[netID] = struct{}{} + + } + } + if len(user.UserGroups) > 0 { + for userGID := range user.UserGroups { + userG, err := GetUserGroup(userGID) + if err == nil { + if len(userG.NetworkRoles) > 0 { + for netID := range userG.NetworkRoles { + if netID == models.AllNetworks { + return allnetworks + } + allNetworkRoles[netID] = struct{}{} + + } + } + } + } + } + filteredNetworks := []models.Network{} + for _, networkI := range allnetworks { + if _, ok := allNetworkRoles[models.NetworkID(networkI.NetID)]; ok { + filteredNetworks = append(filteredNetworks, networkI) + } + } + allnetworks = filteredNetworks + } + return allnetworks +} + +func IsGroupsValid(groups map[models.UserGroupID]struct{}) error { + + for groupID := range groups { + _, 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 { + + if netID != models.AllNetworks { + _, err := logic.GetNetwork(netID.String()) + if err != nil { + return fmt.Errorf("failed to fetch network %s ", netID) + } + } + for netRoleID := range netRoles { + role, err := logic.GetRole(netRoleID) + if err != nil { + return fmt.Errorf("failed to fetch role %s ", netRoleID) + } + if role.NetworkID == "" { + return fmt.Errorf("cannot use platform as network role %s", netRoleID) + } + } + } + return nil +} + +// PrepareOauthUserFromInvite - init oauth user before create +func PrepareOauthUserFromInvite(in models.UserInvite) (models.User, error) { + var newPass, fetchErr = logic.FetchPassValue("") + if fetchErr != nil { + return models.User{}, fetchErr + } + user := models.User{ + UserName: in.Email, + Password: newPass, + } + user.UserGroups = in.UserGroups + user.NetworkRoles = in.NetworkRoles + user.PlatformRoleID = models.UserRoleID(in.PlatformRoleID) + if user.PlatformRoleID == "" { + user.PlatformRoleID = models.ServiceUser + } + return user, nil +} + +func UpdatesUserGwAccessOnRoleUpdates(currNetworkAccess, + changeNetworkAccess map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope, netID string) { + networkChangeMap := make(map[models.RsrcID]models.RsrcPermissionScope) + for rsrcType, RsrcPermsMap := range currNetworkAccess { + if rsrcType != models.RemoteAccessGwRsrc { + continue + } + if _, ok := changeNetworkAccess[rsrcType]; !ok { + for rsrcID, scope := range RsrcPermsMap { + networkChangeMap[rsrcID] = scope + } + } else { + for rsrcID, scope := range RsrcPermsMap { + if _, ok := changeNetworkAccess[rsrcType][rsrcID]; !ok { + networkChangeMap[rsrcID] = scope + } + } + } + } + + extclients, err := logic.GetAllExtClients() + if err != nil { + slog.Error("failed to fetch extclients", "error", err) + return + } + userMap, err := logic.GetUserMap() + if err != nil { + return + } + for _, extclient := range extclients { + if extclient.Network != netID { + continue + } + if _, ok := networkChangeMap[models.AllRemoteAccessGwRsrcID]; ok { + if user, ok := userMap[extclient.OwnerID]; ok { + if user.PlatformRoleID != models.ServiceUser { + continue + } + err = logic.DeleteExtClientAndCleanup(extclient) + if err != nil { + slog.Error("failed to delete extclient", + "id", extclient.ClientID, "owner", user.UserName, "error", err) + } else { + if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil { + slog.Error("error setting ext peers: " + err.Error()) + } + } + } + continue + } + if _, ok := networkChangeMap[models.RsrcID(extclient.IngressGatewayID)]; ok { + if user, ok := userMap[extclient.OwnerID]; ok { + if user.PlatformRoleID != models.ServiceUser { + continue + } + err = logic.DeleteExtClientAndCleanup(extclient) + if err != nil { + slog.Error("failed to delete extclient", + "id", extclient.ClientID, "owner", user.UserName, "error", err) + } else { + if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil { + slog.Error("error setting ext peers: " + err.Error()) + } + } + } + + } + + } + if servercfg.IsDNSMode() { + logic.SetDNS() + } +} + +func UpdatesUserGwAccessOnGrpUpdates(currNetworkRoles, changeNetworkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) { + networkChangeMap := make(map[models.NetworkID]map[models.UserRoleID]struct{}) + for netID, networkUserRoles := range currNetworkRoles { + if _, ok := changeNetworkRoles[netID]; !ok { + for netRoleID := range networkUserRoles { + if _, ok := networkChangeMap[netID]; !ok { + networkChangeMap[netID] = make(map[models.UserRoleID]struct{}) + } + networkChangeMap[netID][netRoleID] = struct{}{} + } + } else { + for netRoleID := range networkUserRoles { + if _, ok := changeNetworkRoles[netID][netRoleID]; !ok { + if _, ok := networkChangeMap[netID]; !ok { + networkChangeMap[netID] = make(map[models.UserRoleID]struct{}) + } + networkChangeMap[netID][netRoleID] = struct{}{} + } + } + } + } + extclients, err := logic.GetAllExtClients() + if err != nil { + slog.Error("failed to fetch extclients", "error", err) + return + } + userMap, err := logic.GetUserMap() + if err != nil { + return + } + for _, extclient := range extclients { + + if _, ok := networkChangeMap[models.NetworkID(extclient.Network)]; ok { + if user, ok := userMap[extclient.OwnerID]; ok { + if user.PlatformRoleID != models.ServiceUser { + continue + } + err = logic.DeleteExtClientAndCleanup(extclient) + if err != nil { + slog.Error("failed to delete extclient", + "id", extclient.ClientID, "owner", user.UserName, "error", err) + } else { + if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil { + slog.Error("error setting ext peers: " + err.Error()) + } + } + } + + } + + } + if servercfg.IsDNSMode() { + logic.SetDNS() + } + +} + +func UpdateUserGwAccess(currentUser, changeUser models.User) { + if changeUser.PlatformRoleID != models.ServiceUser { + return + } + + networkChangeMap := make(map[models.NetworkID]map[models.UserRoleID]struct{}) + for netID, networkUserRoles := range currentUser.NetworkRoles { + if _, ok := changeUser.NetworkRoles[netID]; !ok { + for netRoleID := range networkUserRoles { + if _, ok := networkChangeMap[netID]; !ok { + networkChangeMap[netID] = make(map[models.UserRoleID]struct{}) + } + networkChangeMap[netID][netRoleID] = struct{}{} + } + } else { + for netRoleID := range networkUserRoles { + if _, ok := changeUser.NetworkRoles[netID][netRoleID]; !ok { + if _, ok := networkChangeMap[netID]; !ok { + networkChangeMap[netID] = make(map[models.UserRoleID]struct{}) + } + networkChangeMap[netID][netRoleID] = struct{}{} + } + } + } + } + for gID := range currentUser.UserGroups { + if _, ok := changeUser.UserGroups[gID]; ok { + continue + } + userG, err := GetUserGroup(gID) + if err == nil { + for netID, networkUserRoles := range userG.NetworkRoles { + for netRoleID := range networkUserRoles { + if _, ok := networkChangeMap[netID]; !ok { + networkChangeMap[netID] = make(map[models.UserRoleID]struct{}) + } + networkChangeMap[netID][netRoleID] = struct{}{} + } + } + } + } + if len(networkChangeMap) == 0 { + return + } + // TODO - cleanup gw access when role and groups are updated + //removedGwAccess + extclients, err := logic.GetAllExtClients() + if err != nil { + slog.Error("failed to fetch extclients", "error", err) + return + } + for _, extclient := range extclients { + if extclient.OwnerID == currentUser.UserName { + if _, ok := networkChangeMap[models.NetworkID(extclient.Network)]; ok { + err = logic.DeleteExtClientAndCleanup(extclient) + if err != nil { + slog.Error("failed to delete extclient", + "id", extclient.ClientID, "owner", changeUser.UserName, "error", err) + } else { + if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil { + slog.Error("error setting ext peers: " + err.Error()) + } + } + } + + } + } + if servercfg.IsDNSMode() { + logic.SetDNS() + } + +} diff --git a/scripts/netmaker.default.env b/scripts/netmaker.default.env index 4ccb71c2..876c2a4b 100644 --- a/scripts/netmaker.default.env +++ b/scripts/netmaker.default.env @@ -75,3 +75,14 @@ RAC_AUTO_DISABLE=false CACHING_ENABLED=true # if turned on netclient checks if peers are reachable over private/LAN address, and choose that as peer endpoint ENDPOINT_DETECTION=true +# config for sending emails +# mail server host +SMTP_HOST=smtp.gmail.com +# mail server port +SMTP_PORT=587 +# sender email +EMAIL_SENDER_ADDR= +# sender email auth +EMAIL_SENDER_AUTH= +# mail sender type (smtp or resend) +EMAIL_SENDER_TYPE=smtp \ No newline at end of file diff --git a/scripts/nm-quick.sh b/scripts/nm-quick.sh index e94cd259..ab18eed5 100755 --- a/scripts/nm-quick.sh +++ b/scripts/nm-quick.sh @@ -231,6 +231,7 @@ save_config() { ( fi if [ -n "$NETMAKER_BASE_DOMAIN" ]; then save_config_item NM_DOMAIN "$NETMAKER_BASE_DOMAIN" + save_config_item FRONTEND_URL "https://dashboard.$NETMAKER_BASE_DOMAIN" fi save_config_item UI_IMAGE_TAG "$IMAGE_TAG" # version-specific entries diff --git a/servercfg/serverconf.go b/servercfg/serverconf.go index 3090f33a..5a5916d2 100644 --- a/servercfg/serverconf.go +++ b/servercfg/serverconf.go @@ -242,6 +242,59 @@ func GetPublicBrokerEndpoint() string { } } +func GetSmtpHost() string { + v := "" + if fromEnv := os.Getenv("SMTP_HOST"); fromEnv != "" { + v = fromEnv + } else if fromCfg := config.Config.Server.SmtpHost; fromCfg != "" { + v = fromCfg + } + return v +} + +func GetSmtpPort() int { + v := 587 + if fromEnv := os.Getenv("SMTP_PORT"); fromEnv != "" { + port, err := strconv.Atoi(fromEnv) + if err == nil { + v = port + } + } else if fromCfg := config.Config.Server.SmtpPort; fromCfg != 0 { + v = fromCfg + } + return v +} + +func GetSenderEmail() string { + v := "" + if fromEnv := os.Getenv("EMAIL_SENDER_ADDR"); fromEnv != "" { + v = fromEnv + } else if fromCfg := config.Config.Server.EmailSenderAddr; fromCfg != "" { + v = fromCfg + } + return v +} + +func GetEmaiSenderAuth() string { + v := "" + if fromEnv := os.Getenv("EMAIL_SENDER_AUTH"); fromEnv != "" { + v = fromEnv + } else if fromCfg := config.Config.Server.EmailSenderAddr; fromCfg != "" { + v = fromCfg + } + return v +} + +func EmailSenderType() string { + s := "" + if fromEnv := os.Getenv("EMAIL_SENDER_TYPE"); fromEnv != "" { + s = fromEnv + } else if fromCfg := config.Config.Server.EmailSenderType; fromCfg != "" { + s = fromCfg + } + return s +} + // GetOwnerEmail - gets the owner email (saas) func GetOwnerEmail() string { return os.Getenv("SAAS_OWNER_EMAIL") @@ -472,7 +525,7 @@ func GetPublicIP() (string, error) { break } } - if err == nil && endpoint == "" { + if endpoint == "" { err = errors.New("public address not found") } return endpoint, err