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 `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ` + b.headline + `
+ |
+
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ` + styledParagraphsString + `
+
+
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Get In Touch With Us
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+ ` + 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