NET-1227: User Mgmt V2 (#3055)

* user mgmt models

* define user roles

* define models for new user mgmt and groups

* oauth debug log

* initialize user role after db conn

* print oauth token in debug log

* user roles CRUD apis

* user groups CRUD Apis

* additional api checks

* add additional scopes

* add additional scopes url

* add additional scopes url

* rm additional scopes url

* setup middlleware permission checks

* integrate permission check into middleware

* integrate permission check into middleware

* check for headers for subjects

* refactor user role models

* refactor user groups models

* add new user to pending user via RAC login

* untracked

* allow multiple groups for an user

* change json tag

* add debug headers

* refer network controls form roles, add debug headers

* refer network controls form roles, add debug headers

* replace auth checks, add network id to role model

* nodes handler

* migration funcs

* invoke sync users migration func

* add debug logs

* comment middleware

* fix get all nodes api

* add debug logs

* fix middleware error nil check

* add new func to get username from jwt

* fix jwt parsing

* abort on error

* allow multiple network roles

* allow multiple network roles

* add migration func

* return err if jwt parsing fails

* set global check to true when accessing user apis

* set netid for acls api calls

* set netid for acls api calls

* update role and groups routes

* add validation checks

* add invite flow apis and magic links

* add invited user via oauth signup automatically

* create invited user on oauth signup, with groups in the invite

* add group validation for user invite

* update create user handler with new role mgmt

* add validation checks

* create user invites tables

* add error logging for email invite

* fix invite singup url

* debug log

* get query params from url

* get query params from url

* add query escape

* debug log

* debug log

* fix user signup via invite api

* set admin field for backward compatbility

* use new role id for user apis

* deprecate use of old admin fields

* deprecate usage of old user fields

* add user role as service user if empty

* setup email sender

* delete invite after user singup

* add plaform user role

* redirect on invite verification link

* fix invite redirect

* temporary redirect

* fix invite redirect

* point invite link to frontend

* fix query params lookup

* add resend support, configure email interface types

* fix groups and user creation

* validate user groups, add check for metrics api in middleware

* add invite url to invite model

* migrate rac apis to new user mgmt

* handle network nodes

* add platform user to default role

* fix user role migration

* add default on rag creation and cleanup after deletion

* fix rac apis

* change to invite code param

* filter nodes and hosts based on user network access

* extend create user group req to accomodate users

* filter network based on user access

* format oauth error

* move user roles and groups

* fix get user v1 api

* move user mgmt func to pro

* add user auth type to user model

* fix roles init

* remove platform role from group object

* list only platform roles

* add network roles to invite req

* create default groups and roles

* fix middleware for global access

* create default role

* fix nodes filter with global network roles

* block selfupdate of groups and network roles

* delete netID if net roles are empty

* validate user roles nd groups on update

* set extclient permission scope when rag vpn access is set

* allow deletion of roles and groups

* replace _ with - in role naming convention

* fix failover middleware mgmt

* format oauth templates

* fetch route temaplate

* return err if user wrong login type

* check user groups on rac apis

* fix rac apis

* fix resp msg

* add validation checks for admin invite

* return oauth type

* format group err msg

* fix html tag

* clean up default groups

* create default rag role

* add UI name to roles

* remove default net group from user when deleted

* reorder migration funcs

* fix duplicacy of hosts

* check old field for migration

* from pro to ce make all secondary users admins

* from pro to ce make all secondary users admins

* revert: from pro to ce make all secondary users admins

* make sure downgrades work

* fix pending users approval

* fix duplicate hosts

* fix duplicate hosts entries

* fix cache reference issue

* feat: configure FRONTEND_URL during installation

* disable user vpn access when network roles are modified

* rm vpn acces when roles or groups are deleted

* add http to frontend url

* revert crypto version

* downgrade crytpo version

* add platform id check on user invites

---------

Co-authored-by: the_aceix <aceixsmartx@gmail.com>
This commit is contained in:
Abhishek K 2024-08-20 17:08:56 +05:30 committed by GitHub
parent fb40cd7d56
commit 2e8d95e80e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 4279 additions and 860 deletions

View file

@ -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

View file

@ -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)

View file

@ -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"`
}

View file

@ -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(

View file

@ -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(

View file

@ -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())

105
controllers/middleware.go Normal file
View file

@ -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)
})
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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)

3
go.mod
View file

@ -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 (

6
go.sum
View file

@ -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=

View file

@ -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) {

View file

@ -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
}

View file

@ -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

View file

@ -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")
}

View file

@ -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

View file

@ -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
}

75
logic/user_mgmt.go Normal file
View file

@ -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)
}

View file

@ -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
}

View file

@ -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())

View file

@ -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)
}
}
}
}

View file

@ -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
}

199
models/user_mgmt.go Normal file
View file

@ -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"`
}

View file

@ -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
}

View file

@ -1,60 +1,114 @@
package auth
import "net/http"
import (
"fmt"
"net/http"
"github.com/gravitl/netmaker/servercfg"
)
var htmlBaseTemplate = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Netmaker :: SSO</title>
<script type="text/javascript">
function redirect()
{
window.location.href="` + servercfg.GetFrontendURL() + `";
}
</script>
<style>
html,
body {
margin: 0px;
padding: 0px;
}
body {
height: 100vh;
overflow: hidden;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
}
#logo {
width: 150px;
}
h3 {
margin-bottom: 3rem;
color: rgb(25, 135, 84);
font-size: xx-large;
}
h4 {
margin-bottom: 0px;
}
p {
margin-top: 0px;
margin-bottom: 0px;
}
.back-to-login-btn {
background: #5E5DF0;
border-radius: 999px;
box-shadow: #5E5DF0 0 10px 20px -10px;
box-sizing: border-box;
color: #FFFFFF;
cursor: pointer;
font-family: Inter,Helvetica,"Apple Color Emoji","Segoe UI Emoji",NotoColorEmoji,"Noto Color Emoji","Segoe UI Symbol","Android Emoji",EmojiSymbols,-apple-system,system-ui,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 24px;
opacity: 1;
outline: 0 solid transparent;
padding: 8px 18px;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
width: fit-content;
word-break: break-word;
border: 0;
margin: 20px;
}
</style>
</head>
// == define error HTML here ==
const oauthNotConfigured = `<!DOCTYPE html><html>
<body>
<h3>Your Netmaker server does not have OAuth configured.</h3>
<p>Please visit the docs <a href="https://docs.netmaker.org/oauth.html" target="_blank" rel="noopener">here</a> to learn how to.</p>
<img
src="https://raw.githubusercontent.com/gravitl/netmaker-docs/master/images/netmaker-github/netmaker-teal.png"
alt="netmaker logo"
id="logo"
>
%s
<button class="back-to-login-btn" onClick="redirect()" role="button">Back To Login</button>
</body>
</html>`
const oauthStateInvalid = `<!DOCTYPE html><html>
<body>
<h3>Invalid OAuth Session. Please re-try again.</h3>
</body>
</html>`
var oauthNotConfigured = fmt.Sprintf(htmlBaseTemplate, `<h2>Your Netmaker server does not have OAuth configured.</h2>
<p>Please visit the docs <a href="https://docs.netmaker.org/oauth.html" target="_blank" rel="noopener">here</a> to learn how to.</p>`)
const userNotAllowed = `<!DOCTYPE html><html>
<body>
<h3>Only administrators can access the Dashboard. Please contact your administrator to elevate your account.</h3>
<p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/pro/rac.html" target="_blank" rel="noopener">RemoteAccessClient.</a></p>
</body>
</html>
`
var oauthStateInvalid = fmt.Sprintf(htmlBaseTemplate, `<h2>Invalid OAuth Session. Please re-try again.</h2>`)
const userFirstTimeSignUp = `<!DOCTYPE html><html>
<body>
<h3>Thank you for signing up. Please contact your administrator for access.</h3>
</body>
</html>
`
var userNotAllowed = fmt.Sprintf(htmlBaseTemplate, `<h2>Your account does not have access to the dashboard. Please contact your administrator for more information about your account.</h2>
<p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/pro/rac.html" target="_blank" rel="noopener">RemoteAccessClient.</a></p>`)
const userSignUpApprovalPending = `<!DOCTYPE html><html>
<body>
<h3>Your account is yet to be approved. Please contact your administrator for access.</h3>
</body>
</html>
`
var userFirstTimeSignUp = fmt.Sprintf(htmlBaseTemplate, `<h2>Thank you for signing up. Please contact your administrator for access.</h2>`)
const userNotFound = `<!DOCTYPE html><html>
<body>
<h3>User Not Found.</h3>
</body>
</html>`
var userSignUpApprovalPending = fmt.Sprintf(htmlBaseTemplate, `<h2>Your account is yet to be approved. Please contact your administrator for access.</h2>`)
const somethingwentwrong = `<!DOCTYPE html><html>
<body>
<h3>Something went wrong. Contact Admin.</h3>
</body>
</html>`
var userNotFound = fmt.Sprintf(htmlBaseTemplate, `<h2>User Not Found.</h2>`)
const notallowedtosignup = `<!DOCTYPE html><html>
<body>
<h3>Your email is not allowed. Please contact your administrator.</h3>
</body>
</html>`
var somethingwentwrong = fmt.Sprintf(htmlBaseTemplate, `<h2>Something went wrong. Contact Admin.</h2>`)
var notallowedtosignup = fmt.Sprintf(htmlBaseTemplate, `<h2>Your email is not allowed. Please contact your administrator.</h2>`)
func handleOauthUserNotFound(response http.ResponseWriter) {
response.Header().Set("Content-Type", "text/html; charset=utf-8")

View file

@ -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
}

View file

@ -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())

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

@ -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

File diff suppressed because it is too large Load diff

53
pro/email/email.go Normal file
View file

@ -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
}

27
pro/email/invite.go Normal file
View file

@ -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 <a href=\"%s\">Netmaker</a>", invite.InviteURL)).
Build()
}

55
pro/email/resend.go Normal file
View file

@ -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(&params)
if err != nil {
return fmt.Errorf("failed sending mail via resend: %w", err)
}
return nil
}

42
pro/email/smtp.go Normal file
View file

@ -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
}

567
pro/email/utils.go Normal file
View file

@ -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 `
<!DOCTYPE html>
<html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" lang="en">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
<o:AllowPNG/>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<style>
*{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}@media (max-width:720px){.desktop_hide table.icons-inner{display:inline-block!important}.icons-inner{text-align:center}.icons-inner td{margin:0 auto}.image_block img.big,.row-content{width:100%!important}.mobile_hide{display:none}.stack .column{width:100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width:0;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}}
</style>
</head>
<body style="background-color:transparent;margin:0;padding:0;-webkit-text-size-adjust:none;text-size-adjust:none">
<table class="nl-container" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:transparent">
<tbody>
<tr>
<td>
<table class="row row-1" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tbody>
<tr>
<td>
<table class="row-content" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
<tbody>
<tr>
<td class="column column-1" width="50%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
<table class="image_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad" style="padding-left:15px;padding-right:15px;width:100%;padding-top:5px">
<div class="alignment" align="left" style="line-height:10px"><a href="https://www.netmaker.io/" target="_blank" style="outline:none" tabindex="-1"><img class="big" src="` + netmakerLogoTeal + `"
style="display:block;height:auto;border:0;width:333px;max-width:100%" width="333" alt="Netmaker" title="Netmaker"></a></div>
</td>
</tr>
</table>
<table class="divider_block block-3" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad" style="padding-bottom:10px;padding-left:5px;padding-right:5px;padding-top:10px">
<div class="alignment" align="center">
<table border="0" cellpadding="0" cellspacing="0"
role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
<td class="column column-2" width="50%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
<table class="empty_block block-2" width="100%" border="0"
cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad" style="padding-right:0;padding-bottom:5px;padding-left:0;padding-top:5px">
<div></div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table class="row row-2" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tbody>
<tr>
<td>
<table class="row-content stack" align="center"
border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
<tbody>
<tr>
<td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;padding-left:10px;padding-right:10px;vertical-align:top;padding-top:10px;padding-bottom:10px;border-top:0;border-right:0;border-bottom:0;border-left:0">
<table class="divider_block block-1" width="100%" border="0"
cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad">
<div class="alignment" align="center">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
class="row row-3" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tbody>
<tr>
<td>
<table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
<tbody>
<tr>
<td class="column column-1" width="50%"
style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
<table class="divider_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad" style="padding-bottom:20px;padding-left:20px;padding-right:20px;padding-top:25px">
<div class="alignment" align="center">
<table border="0" cellpadding="0"
cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
</tr>
</table>
</div>
</td>
</tr>
</table>
<table class="heading_block block-3" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad"
style="padding-bottom:15px;padding-left:10px;padding-right:10px;padding-top:10px;text-align:center;width:100%">
<h1 style="margin:0;color:#2b2d2d;direction:ltr;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:28px;font-weight:400;letter-spacing:normal;line-height:120%;text-align:left;margin-top:0;margin-bottom:0"><strong>` + b.headline + `</strong></h1>
</td>
</tr>
</table>
</td>
<td class="column column-2" width="50%"
style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
<table class="image_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad" style="width:100%;padding-right:0;padding-left:0;padding-top:5px;padding-bottom:5px">
<div class="alignment" align="center" style="line-height:10px"><img
src="` + netmakerMeshLogo + `" style="display:block;height:auto;border:0;width:350px;max-width:100%" width="350" alt="Netmaker Mesh"></div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table class="row row-4" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="mso-table-lspace:0;mso-table-rspace:0">
<tbody>
<tr>
<td>
<table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
<tbody>
<tr>
<td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
<table class="divider_block block-1" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad">
<div class="alignment" align="center">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table class="row row-5" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tbody>
<tr>
<td>
<table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="mso-table-lspace:0;mso-table-rspace:0;background-color:#0098a5;color:#000;border-top:2px solid transparent;border-right:2px solid transparent;border-left:2px solid transparent;border-bottom:2px solid transparent;border-radius:0;width:700px" width="700">
<tbody>
<tr>
<td class="column column-1" width="100%"
style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;border-bottom:0 solid #000;border-left:0 solid #000;border-right:0 solid #000;border-top:0 solid #000;vertical-align:top;padding-top:25px;padding-bottom:25px">
<table class="text_block block-3" width="100%" border="0"
cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word">
<tr>
<td class="pad" style="padding-bottom:10px;padding-left:50px;padding-right:50px;padding-top:10px">
<div style="font-family:Verdana,sans-serif">
<div class="txtTinyMce-wrapper" style="font-size:12px;mso-line-height-alt:18px;color:#393d47;line-height:1.5;font-family:Verdana,Geneva,sans-serif">
<p style="margin:0;font-size:12px;mso-line-height-alt:18px">&nbsp;</p>
` + styledParagraphsString + `
<p style="margin:0;mso-line-height-alt:18px">&nbsp;</p>
</div>
</div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table class="row row-6" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tbody>
<tr>
<td>
<table
class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
<tbody>
<tr>
<td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
<table class="divider_block block-1" width="100%" border="0"
cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad">
<div class="alignment" align="center">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
class="row row-7" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:#f7fafe">
<tbody>
<tr>
<td>
<table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
<tbody>
<tr>
<td class="column column-1" width="100%"
style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:25px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
<table class="divider_block block-1" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad">
<div class="alignment" align="center">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%"
style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table class="row row-8" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:#090660">
<tbody>
<tr>
<td>
<table class="row-content stack"
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
<tbody>
<tr>
<td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
<table class="text_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word">
<tr>
<td class="pad" style="padding-bottom:10px;padding-left:50px;padding-right:50px;padding-top:10px">
<div style="font-family:sans-serif">
<div class="txtTinyMce-wrapper" style="font-size:12px;mso-line-height-alt:18px;color:#6f7077;line-height:1.5;font-family:Arial,Helvetica Neue,Helvetica,sans-serif">
<p style="margin:0;font-size:12px;mso-line-height-alt:33px">
<span style="color:#ffffff;font-size:22px;">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Get In Touch With Us</span>
</p>
</div>
</div>
</td>
</tr>
</table>
<table class="social_block block-2" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad">
<div class="alignment" style="text-align:center">
<table class="social-table"
width="114.49624060150376px" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;display:inline-block">
<tr>
<td style="padding:0 2px 0 2px"><a href="https://www.linkedin.com/company/netmaker-inc/" target="_blank"><img src="` + linkedinIcon + `" width="32" height="32" alt="Linkedin" title="linkedin" style="display:block;height:auto;border:0"></a></td>
<td
style="padding:0 2px 0 2px"><a href="https://discord.gg/zRb9Vfhk8A" target="_blank"><img src="` + discordIcon + `" width="32" height="32" alt="Discord" title="Discord" style="display:block;height:auto;border:0"></a></td>
<td style="padding:0 2px 0 2px"><a href="https://github.com/gravitl/netmaker" target="_blank"><img
src="` + githubIcon + `" width="38.49624060150376" height="32" alt="Github" title="Github" style="display:block;height:auto;border:0"></a></td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table class="row row-9" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tbody>
<tr>
<td>
<table
class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
<tbody>
<tr>
<td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
<table class="icons_block block-1" width="100%" border="0"
cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="pad" style="vertical-align:middle;padding-bottom:5px;padding-top:5px;text-align:center;color:#9d9d9d;font-family:inherit;font-size:15px">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="alignment" style="vertical-align:middle;text-align:center">
<!--[if vml]>
<table align="left" cellpadding="0" cellspacing="0" role="presentation" style="display:inline-block;padding-left:0px;padding-right:0px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;">
<![endif]--><!--[if !vml]><!-->
<table class="icons-inner" style="mso-table-lspace:0;mso-table-rspace:0;display:inline-block;margin-right:-4px;padding-left:0;padding-right:0" cellpadding="0" cellspacing="0" role="presentation">
<!--<![endif]-->
</table>
</td></tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!-- End -->
</body>
` + signature + `
</html>`
}
func styledSignature() string {
return `
<footer style="display:block">
<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
<tbody>
<tr>
<td>
<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
<tbody>
<tr>
<td style="vertical-align: top;">
<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
<tbody>
<tr>
<td class="sc-TOsTZ kjYrri" style="text-align: center;"><img src="` + nLogoTeal + `" role="presentation" width="130" class="sc-cHGsZl bHiaRe" style="max-width: 130px; display: block;"></td>
</tr>
<tr>
<td height="30"></td>
</tr>
<tr>
<td style="text-align: center;">
<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial; display: inline-block;">
<tbody>
<tr style="text-align: center;">
<td><a href="https://www.linkedin.com/company/netmaker-inc/" color="#6a78d1" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px; background-color: rgb(106, 120, 209);"><img src="` + linkedinIcon + `" alt="Linkedin" color="#6a78d1" height="24" class="sc-bRBYWo ccSRck" style="background-color: rgb(106, 120, 209); max-width: 135px; display: block;"></a></td>
<td width="5">
<div></div>
</td>
<td><a href="https://discord.gg/zRb9Vfhk8A" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px;"><img src="` + discordIcon + `" alt="Discord" height="24" class="sc-bRBYWo ccSRck" style="max-width: 135px; display: block;"></a></td>
<td width="5">
<div></div>
</td>
<td><a href="https://github.com/gravitl/netmaker" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px;"><img src="` + githubIcon + `" alt="Github" height="24" class="sc-bRBYWo ccSRck" style="max-width: 135px; display: block;"></a></td>
<td width="5">
<div></div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
<td width="46">
<div></div>
</td>
<td style="padding: 0px; vertical-align: middle;">
<h3 color="#000000" class="sc-fBuWsC eeihxG" style="margin: 0px; font-size: 18px; color: rgb(0, 0, 0);"><span>Alex</span><span>&nbsp;</span><span>Feiszli</span></h3>
<p color="#000000" font-size="medium" class="sc-fMiknA bxZCMx" style="margin: 0px; color: rgb(0, 0, 0); font-size: 14px; line-height: 22px;"><span>Co-Founder &amp; CEO</span></p>
<p color="#000000" font-size="medium" class="sc-dVhcbM fghLuF" style="margin: 0px; font-weight: 500; color: rgb(0, 0, 0); font-size: 14px; line-height: 22px;"><span>Netmaker</span></p>
<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial; width: 100%;">
<tbody>
<tr>
<td height="30"></td>
</tr>
<tr>
<td color="#545af2" direction="horizontal" height="1" class="sc-jhAzac hmXDXQ" style="width: 100%; border-bottom: 1px solid rgb(84, 90, 242); border-left: none; display: block;"></td>
</tr>
<tr>
<td height="30"></td>
</tr>
</tbody>
</table>
<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
<tbody>
<tr height="25" style="vertical-align: middle;">
<td width="30" style="vertical-align: middle;">
<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
<tbody>
<tr>
<td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block"><img src="` + mailIcon + `" width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
</tr>
</tbody>
</table>
</td>
<td style="padding: 0px;"><a href="mailto:alex@netmaker.io" color="#000000" class="sc-gipzik iyhjGb" style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>alex@netmaker.io</span></a></td>
</tr>
<tr height="25" style="vertical-align: middle;">
<td width="30" style="vertical-align: middle;">
<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
<tbody>
<tr>
<td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block;"><img src="` + linkIcon + `" color="#545af2" width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
</tr>
</tbody>
</table>
</td>
<td style="padding: 0px;"><a href="https://www.netmaker.io/" color="#000000" class="sc-gipzik iyhjGb" style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>https://www.netmaker.io/</span></a></td>
</tr>
<tr height="25" style="vertical-align: middle;">
<td width="30" style="vertical-align: middle;">
<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
<tbody>
<tr>
<td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block;"><img src="` + addressIcon + `" width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
</tr>
</tbody>
</table>
</td>
<td style="padding: 0px;"><span color="#000000" class="sc-csuQGl CQhxV" style="font-size: 12px; color: rgb(0, 0, 0);"><span>1465 Sand Hill Rd.Suite 2014, Candler, NC 28715</span></span></td>
</tr>
</tbody>
</table>
<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
<tbody>
<tr>
<td height="30"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</footer>`
}
func styledParagraph(text string) string {
return `<p style="margin:0;mso-line-height-alt:22.5px">
<span style="color:#ffffff;font-size:15px;">` + text + `</span>
</p>`
}
func GetMailSignature() string {
return styledSignature()
}

View file

@ -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 {

195
pro/logic/security.go Normal file
View file

@ -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")
}

1057
pro/logic/user_mgmt.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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