From 5a4d0663da5095f1d8e8e0ca6020af2bbe9c0890 Mon Sep 17 00:00:00 2001 From: Abhishek K <32607604+abhishek9686@users.noreply.github.com> Date: Sun, 25 Aug 2024 07:25:40 +0530 Subject: [PATCH 01/20] NET-1227: User Cli cmds Update (#3064) * generalise smtp config * copy over smtp vars * env new line * fix master key api access * comment user tests * fix network and user invite for master key access * remove email sender type * user mgmt commands * check user role on CE * user role nmtcl cmds * user groups commands * fix role and groups command * fix user create cmd * add usage info * rm user role check * fix user update cmd * fix static check --- cli/cmd/user/create.go | 33 +++++++++-- cli/cmd/user/flags.go | 11 ++-- cli/cmd/user/groups.go | 118 ++++++++++++++++++++++++++++++++++++++++ cli/cmd/user/list.go | 10 +++- cli/cmd/user/roles.go | 121 +++++++++++++++++++++++++++++++++++++++++ cli/cmd/user/update.go | 36 ++++++++++-- cli/config/config.go | 2 +- cli/functions/user.go | 39 ++++++++++++- logic/auth.go | 6 +- 9 files changed, 356 insertions(+), 20 deletions(-) create mode 100644 cli/cmd/user/groups.go create mode 100644 cli/cmd/user/roles.go diff --git a/cli/cmd/user/create.go b/cli/cmd/user/create.go index dc1e2a0b..16af503d 100644 --- a/cli/cmd/user/create.go +++ b/cli/cmd/user/create.go @@ -1,6 +1,8 @@ package user import ( + "strings" + "github.com/gravitl/netmaker/cli/functions" "github.com/gravitl/netmaker/models" "github.com/spf13/cobra" @@ -12,18 +14,41 @@ var userCreateCmd = &cobra.Command{ Short: "Create a new user", Long: `Create a new user`, Run: func(cmd *cobra.Command, args []string) { - user := &models.User{UserName: username, Password: password, IsAdmin: admin} + user := &models.User{UserName: username, Password: password, PlatformRoleID: models.UserRoleID(platformID)} + if len(networkRoles) > 0 { + netRolesMap := make(map[models.NetworkID]map[models.UserRoleID]struct{}) + for netID, netRoles := range networkRoles { + roleMap := make(map[models.UserRoleID]struct{}) + for _, roleID := range strings.Split(netRoles, " ") { + roleMap[models.UserRoleID(roleID)] = struct{}{} + } + netRolesMap[models.NetworkID(netID)] = roleMap + } + user.NetworkRoles = netRolesMap + } + if len(groups) > 0 { + grMap := make(map[models.UserGroupID]struct{}) + for _, groupID := range groups { + grMap[models.UserGroupID(groupID)] = struct{}{} + } + user.UserGroups = grMap + } + functions.PrettyPrint(functions.CreateUser(user)) }, } func init() { + userCreateCmd.Flags().StringVar(&username, "name", "", "Name of the user") userCreateCmd.Flags().StringVar(&password, "password", "", "Password of the user") + userCreateCmd.Flags().StringVarP(&platformID, "platform-role", "r", models.ServiceUser.String(), + "Platform Role of the user; run `nmctl roles list` to see available user roles") userCreateCmd.MarkFlagRequired("name") userCreateCmd.MarkFlagRequired("password") - userCreateCmd.Flags().BoolVar(&admin, "admin", false, "Make the user an admin ?") - userCreateCmd.Flags().StringVar(&networks, "networks", "", "List of networks the user will access to (comma separated)") - userCreateCmd.Flags().StringVar(&groups, "groups", "", "List of user groups the user will be part of (comma separated)") + userCreateCmd.PersistentFlags().StringToStringVarP(&networkRoles, "network-roles", "n", nil, + "Mapping of networkID and list of roles user will be part of (comma separated)") + userCreateCmd.Flags().BoolVar(&admin, "admin", false, "Make the user an admin ? (deprecated v0.25.0 onwards)") + userCreateCmd.Flags().StringArrayVarP(&groups, "groups", "g", nil, "List of user groups the user will be part of (comma separated)") rootCmd.AddCommand(userCreateCmd) } diff --git a/cli/cmd/user/flags.go b/cli/cmd/user/flags.go index 57f07843..061e8f32 100644 --- a/cli/cmd/user/flags.go +++ b/cli/cmd/user/flags.go @@ -1,9 +1,10 @@ package user var ( - username string - password string - admin bool - networks string - groups string + username string + password string + platformID string + admin bool + networkRoles map[string]string + groups []string ) diff --git a/cli/cmd/user/groups.go b/cli/cmd/user/groups.go new file mode 100644 index 00000000..0406083e --- /dev/null +++ b/cli/cmd/user/groups.go @@ -0,0 +1,118 @@ +package user + +import ( + "fmt" + "os" + "strings" + + "github.com/gravitl/netmaker/cli/cmd/commons" + "github.com/gravitl/netmaker/cli/functions" + "github.com/guumaster/tablewriter" + "github.com/spf13/cobra" +) + +var userGroupCmd = &cobra.Command{ + Use: "group", + Args: cobra.NoArgs, + Short: "Manage User Groups", + Long: `Manage User Groups`, +} + +var userGroupListCmd = &cobra.Command{ + Use: "list", + Args: cobra.NoArgs, + Short: "List all user groups", + Long: `List all user groups`, + Run: func(cmd *cobra.Command, args []string) { + data := functions.ListUserGrps() + switch commons.OutputFormat { + case commons.JsonOutput: + functions.PrettyPrint(data) + default: + table := tablewriter.NewWriter(os.Stdout) + h := []string{"ID", "MetaData", "Network Roles"} + table.SetHeader(h) + for _, d := range data { + + roleInfoStr := "" + for netID, netRoleMap := range d.NetworkRoles { + roleList := []string{} + for roleID := range netRoleMap { + roleList = append(roleList, roleID.String()) + } + roleInfoStr += fmt.Sprintf("[%s]: %s", netID, strings.Join(roleList, ",")) + } + e := []string{d.ID.String(), d.MetaData, roleInfoStr} + table.Append(e) + } + table.Render() + } + }, +} + +var userGroupCreateCmd = &cobra.Command{ + Use: "create", + Args: cobra.NoArgs, + Short: "create user group", + Long: `create user group`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("CLI doesn't support creation of groups currently. Visit the dashboard to create one or refer to our api documentation https://docs.v2.netmaker.io/reference") + }, +} + +var userGroupDeleteCmd = &cobra.Command{ + Use: "delete [groupID]", + Args: cobra.ExactArgs(1), + Short: "delete user group", + Long: `delete user group`, + Run: func(cmd *cobra.Command, args []string) { + resp := functions.DeleteUserGrp(args[0]) + if resp != nil { + fmt.Println(resp.Message) + } + }, +} + +var userGroupGetCmd = &cobra.Command{ + Use: "get [groupID]", + Args: cobra.ExactArgs(1), + Short: "get user group", + Long: `get user group`, + Run: func(cmd *cobra.Command, args []string) { + data := functions.GetUserGrp(args[0]) + switch commons.OutputFormat { + case commons.JsonOutput: + functions.PrettyPrint(data) + default: + table := tablewriter.NewWriter(os.Stdout) + h := []string{"ID", "MetaData", "Network Roles"} + table.SetHeader(h) + roleInfoStr := "" + for netID, netRoleMap := range data.NetworkRoles { + roleList := []string{} + for roleID := range netRoleMap { + roleList = append(roleList, roleID.String()) + } + roleInfoStr += fmt.Sprintf("[%s]: %s", netID, strings.Join(roleList, ",")) + } + e := []string{data.ID.String(), data.MetaData, roleInfoStr} + table.Append(e) + table.Render() + } + }, +} + +func init() { + rootCmd.AddCommand(userGroupCmd) + // list roles cmd + userGroupCmd.AddCommand(userGroupListCmd) + + // create roles cmd + userGroupCmd.AddCommand(userGroupCreateCmd) + + // delete role cmd + userGroupCmd.AddCommand(userGroupDeleteCmd) + + // Get Role + userGroupCmd.AddCommand(userGroupGetCmd) +} diff --git a/cli/cmd/user/list.go b/cli/cmd/user/list.go index 694697a2..68308f62 100644 --- a/cli/cmd/user/list.go +++ b/cli/cmd/user/list.go @@ -2,7 +2,7 @@ package user import ( "os" - "strconv" + "strings" "github.com/gravitl/netmaker/cli/cmd/commons" "github.com/gravitl/netmaker/cli/functions" @@ -22,9 +22,13 @@ var userListCmd = &cobra.Command{ functions.PrettyPrint(data) default: table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Name", "SuperAdmin", "Admin"}) + table.SetHeader([]string{"Name", "Platform Role", "Groups"}) for _, d := range *data { - table.Append([]string{d.UserName, strconv.FormatBool(d.IsSuperAdmin), strconv.FormatBool(d.IsAdmin)}) + g := []string{} + for gID := range d.UserGroups { + g = append(g, gID.String()) + } + table.Append([]string{d.UserName, d.PlatformRoleID.String(), strings.Join(g, ",")}) } table.Render() } diff --git a/cli/cmd/user/roles.go b/cli/cmd/user/roles.go new file mode 100644 index 00000000..2bb880ef --- /dev/null +++ b/cli/cmd/user/roles.go @@ -0,0 +1,121 @@ +package user + +import ( + "fmt" + "os" + "strconv" + + "github.com/gravitl/netmaker/cli/cmd/commons" + "github.com/gravitl/netmaker/cli/functions" + "github.com/guumaster/tablewriter" + "github.com/spf13/cobra" +) + +var userRoleCmd = &cobra.Command{ + Use: "role", + Args: cobra.NoArgs, + Short: "Manage User Roles", + Long: `Manage User Roles`, +} + +// List Roles +var ( + platformRoles bool +) +var userRoleListCmd = &cobra.Command{ + Use: "list", + Args: cobra.NoArgs, + Short: "List all user roles", + Long: `List all user roles`, + Run: func(cmd *cobra.Command, args []string) { + data := functions.ListUserRoles() + switch commons.OutputFormat { + case commons.JsonOutput: + functions.PrettyPrint(data) + default: + table := tablewriter.NewWriter(os.Stdout) + h := []string{"ID", "Default", "Dashboard Access", "Full Access"} + + if !platformRoles { + h = append(h, "Network") + } + table.SetHeader(h) + for _, d := range data { + e := []string{d.ID.String(), strconv.FormatBool(d.Default), strconv.FormatBool(d.DenyDashboardAccess), strconv.FormatBool(d.FullAccess)} + if !platformRoles { + e = append(e, d.NetworkID.String()) + } + table.Append(e) + } + table.Render() + } + }, +} + +var userRoleCreateCmd = &cobra.Command{ + Use: "create", + Args: cobra.NoArgs, + Short: "create user role", + Long: `create user role`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("CLI doesn't support creation of roles currently. Visit the dashboard to create one or refer to our api documentation https://docs.v2.netmaker.io/reference") + }, +} + +var userRoleDeleteCmd = &cobra.Command{ + Use: "delete [roleID]", + Args: cobra.ExactArgs(1), + Short: "delete user role", + Long: `delete user role`, + Run: func(cmd *cobra.Command, args []string) { + resp := functions.DeleteUserRole(args[0]) + if resp != nil { + fmt.Println(resp.Message) + } + }, +} + +var userRoleGetCmd = &cobra.Command{ + Use: "get [roleID]", + Args: cobra.ExactArgs(1), + Short: "get user role", + Long: `get user role`, + Run: func(cmd *cobra.Command, args []string) { + d := functions.GetUserRole(args[0]) + switch commons.OutputFormat { + case commons.JsonOutput: + functions.PrettyPrint(d) + default: + table := tablewriter.NewWriter(os.Stdout) + h := []string{"ID", "Default Role", "Dashboard Access", "Full Access"} + + if d.NetworkID != "" { + h = append(h, "Network") + } + table.SetHeader(h) + e := []string{d.ID.String(), strconv.FormatBool(d.Default), strconv.FormatBool(!d.DenyDashboardAccess), strconv.FormatBool(d.FullAccess)} + if !platformRoles { + e = append(e, d.NetworkID.String()) + } + table.Append(e) + table.Render() + } + }, +} + +func init() { + rootCmd.AddCommand(userRoleCmd) + // list roles cmd + userRoleListCmd.Flags().BoolVar(&platformRoles, "platform-roles", true, + "set to false to list network roles. By default it will only list platform roles") + userRoleCmd.AddCommand(userRoleListCmd) + + // create roles cmd + userRoleCmd.AddCommand(userRoleCreateCmd) + + // delete role cmd + userRoleCmd.AddCommand(userRoleDeleteCmd) + + // Get Role + userRoleCmd.AddCommand(userRoleGetCmd) +} diff --git a/cli/cmd/user/update.go b/cli/cmd/user/update.go index 0a79f26a..31e9ccee 100644 --- a/cli/cmd/user/update.go +++ b/cli/cmd/user/update.go @@ -1,6 +1,8 @@ package user import ( + "strings" + "github.com/gravitl/netmaker/cli/functions" "github.com/gravitl/netmaker/models" "github.com/spf13/cobra" @@ -12,14 +14,40 @@ var userUpdateCmd = &cobra.Command{ Short: "Update a user", Long: `Update a user`, Run: func(cmd *cobra.Command, args []string) { - user := &models.User{UserName: args[0], IsAdmin: admin} + user := &models.User{UserName: args[0]} + if platformID != "" { + user.PlatformRoleID = models.UserRoleID(platformID) + } + if len(networkRoles) > 0 { + netRolesMap := make(map[models.NetworkID]map[models.UserRoleID]struct{}) + for netID, netRoles := range networkRoles { + roleMap := make(map[models.UserRoleID]struct{}) + for _, roleID := range strings.Split(netRoles, ",") { + roleMap[models.UserRoleID(roleID)] = struct{}{} + } + netRolesMap[models.NetworkID(netID)] = roleMap + } + user.NetworkRoles = netRolesMap + } + if len(groups) > 0 { + grMap := make(map[models.UserGroupID]struct{}) + for _, groupID := range groups { + grMap[models.UserGroupID(groupID)] = struct{}{} + } + user.UserGroups = grMap + } functions.PrettyPrint(functions.UpdateUser(user)) }, } func init() { - userUpdateCmd.Flags().BoolVar(&admin, "admin", false, "Make the user an admin ?") - userUpdateCmd.Flags().StringVar(&networks, "networks", "", "List of networks the user will access to (comma separated)") - userUpdateCmd.Flags().StringVar(&groups, "groups", "", "List of user groups the user will be part of (comma separated)") + + userUpdateCmd.Flags().StringVar(&password, "password", "", "Password of the user") + userUpdateCmd.Flags().StringVarP(&platformID, "platform-role", "r", "", + "Platform Role of the user; run `nmctl roles list` to see available user roles") + userUpdateCmd.PersistentFlags().StringToStringVarP(&networkRoles, "network-roles", "n", nil, + "Mapping of networkID and list of roles user will be part of (comma separated)") + userUpdateCmd.Flags().BoolVar(&admin, "admin", false, "Make the user an admin ? (deprecated v0.25.0 onwards)") + userUpdateCmd.Flags().StringArrayVarP(&groups, "groups", "g", nil, "List of user groups the user will be part of (comma separated)") rootCmd.AddCommand(userUpdateCmd) } diff --git a/cli/config/config.go b/cli/config/config.go index ba32c48d..444ad166 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -86,7 +86,7 @@ func GetCurrentContext() (name string, ctx Context) { return } } - log.Fatalf("No current context set, do so via `netmaker context use `") + log.Fatalf("No current context set, do so via `nmctl context use `") return } diff --git a/cli/functions/user.go b/cli/functions/user.go index b286bc4e..e5572bae 100644 --- a/cli/functions/user.go +++ b/cli/functions/user.go @@ -1,6 +1,8 @@ package functions import ( + "encoding/json" + "fmt" "net/http" "github.com/gravitl/netmaker/models" @@ -18,7 +20,7 @@ func CreateUser(payload *models.User) *models.User { // UpdateUser - update a user func UpdateUser(payload *models.User) *models.User { - return request[models.User](http.MethodPut, "/api/users/networks/"+payload.UserName, payload) + return request[models.User](http.MethodPut, "/api/users/"+payload.UserName, payload) } // DeleteUser - delete a user @@ -35,3 +37,38 @@ func GetUser(username string) *models.User { func ListUsers() *[]models.ReturnUser { return request[[]models.ReturnUser](http.MethodGet, "/api/users", nil) } + +func ListUserRoles() (roles []models.UserRolePermissionTemplate) { + resp := request[models.SuccessResponse](http.MethodGet, "/api/v1/users/roles", nil) + d, _ := json.Marshal(resp.Response) + json.Unmarshal(d, &roles) + return +} + +func DeleteUserRole(roleID string) *models.SuccessResponse { + return request[models.SuccessResponse](http.MethodDelete, fmt.Sprintf("/api/v1/users/role?role_id=%s", roleID), nil) +} +func GetUserRole(roleID string) (role models.UserRolePermissionTemplate) { + resp := request[models.SuccessResponse](http.MethodGet, fmt.Sprintf("/api/v1/users/role?role_id=%s", roleID), nil) + d, _ := json.Marshal(resp.Response) + json.Unmarshal(d, &role) + return +} + +func ListUserGrps() (groups []models.UserGroup) { + resp := request[models.SuccessResponse](http.MethodGet, "/api/v1/users/groups", nil) + d, _ := json.Marshal(resp.Response) + json.Unmarshal(d, &groups) + return +} + +func DeleteUserGrp(grpID string) *models.SuccessResponse { + return request[models.SuccessResponse](http.MethodDelete, fmt.Sprintf("/api/v1/users/group?group_id=%s", grpID), nil) +} + +func GetUserGrp(grpID string) (group models.UserGroup) { + resp := request[models.SuccessResponse](http.MethodGet, fmt.Sprintf("/api/v1/users/group?group_id=%s", grpID), nil) + d, _ := json.Marshal(resp.Response) + json.Unmarshal(d, &group) + return +} diff --git a/logic/auth.go b/logic/auth.go index 3e2623ff..7486d310 100644 --- a/logic/auth.go +++ b/logic/auth.go @@ -297,7 +297,9 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) { } // Reset Gw Access for service users go UpdateUserGwAccess(*user, *userchange) - user.PlatformRoleID = userchange.PlatformRoleID + if userchange.PlatformRoleID != "" { + user.PlatformRoleID = userchange.PlatformRoleID + } user.UserGroups = userchange.UserGroups user.NetworkRoles = userchange.NetworkRoles err := ValidateUser(user) @@ -325,7 +327,7 @@ func ValidateUser(user *models.User) error { // check if role is valid _, err := GetRole(user.PlatformRoleID) if err != nil { - return err + return errors.New("failed to fetch platform role " + user.PlatformRoleID.String()) } v := validator.New() _ = v.RegisterValidation("in_charset", func(fl validator.FieldLevel) bool { From f18937f1e8c8894664909709ad029cb7d80410a0 Mon Sep 17 00:00:00 2001 From: Abhishek K <32607604+abhishek9686@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:44:18 +0530 Subject: [PATCH 02/20] Net 1227 v1 (#3065) * generalise smtp config * copy over smtp vars * env new line * fix master key api access * comment user tests * fix network and user invite for master key access * remove email sender type * user mgmt commands * check user role on CE * user role nmtcl cmds * user groups commands * fix role and groups command * fix user create cmd * add usage info * rm user role check * fix user update cmd * fix static check * add backwards comptability support for extclient api for mobile --- controllers/middleware.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/controllers/middleware.go b/controllers/middleware.go index 733db69d..f359f9bc 100644 --- a/controllers/middleware.go +++ b/controllers/middleware.go @@ -19,6 +19,12 @@ func userMiddleWare(handler http.Handler) http.Handler { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) return } + if r.Method == http.MethodPost && route == "/api/extclients/{network}/{nodeid}" { + node, err := logic.GetNodeByID(params["nodeid"]) + if err == nil { + params["network"] = node.Network + } + } r.Header.Set("IS_GLOBAL_ACCESS", "no") r.Header.Set("TARGET_RSRC", "") r.Header.Set("RSRC_TYPE", "") From 936e1b4d4538aac8ad3fbdbb09a9c2a279272794 Mon Sep 17 00:00:00 2001 From: Abhishek K <32607604+abhishek9686@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:27:45 +0530 Subject: [PATCH 03/20] Net 1227 v1 (#3066) * generalise smtp config * copy over smtp vars * env new line * fix master key api access * comment user tests * fix network and user invite for master key access * remove email sender type * user mgmt commands * check user role on CE * user role nmtcl cmds * user groups commands * fix role and groups command * fix user create cmd * add usage info * rm user role check * fix user update cmd * fix static check * add backwards comptability support for extclient api for mobile * rm debug logs * set frontend url from base domain if empty --- controllers/middleware.go | 2 -- pro/auth/google.go | 8 -------- pro/controllers/users.go | 16 ++++++---------- pro/logic/security.go | 7 ------- pro/logic/user_mgmt.go | 2 +- servercfg/serverconf.go | 5 +++++ 6 files changed, 12 insertions(+), 28 deletions(-) diff --git a/controllers/middleware.go b/controllers/middleware.go index f359f9bc..bfc88aa4 100644 --- a/controllers/middleware.go +++ b/controllers/middleware.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/gorilla/mux" - "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" "github.com/gravitl/netmaker/models" ) @@ -105,7 +104,6 @@ func userMiddleWare(handler http.Handler) http.Handler { } r.Header.Set("RSRC_TYPE", r.Header.Get("TARGET_RSRC")) - logger.Log(0, "URL ------> ", route) handler.ServeHTTP(w, r) }) } diff --git a/pro/auth/google.go b/pro/auth/google.go index e4dbc2ae..94db3a7c 100644 --- a/pro/auth/google.go +++ b/pro/auth/google.go @@ -69,22 +69,17 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) { handleOauthNotConfigured(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 !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 @@ -95,7 +90,6 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - logger.Log(0, "CALLBACK ----> 4.0") if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) @@ -124,7 +118,6 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) { return } } - logger.Log(0, "CALLBACK ----> 6") user, err := logic.GetUser(content.Email) if err != nil { logger.Log(0, "error fetching user: ", err.Error()) @@ -186,7 +179,6 @@ func getGoogleUserInfo(state string, code string) (*OAuthUser, error) { if err != nil { return nil, fmt.Errorf("failed reading response body: %s", err.Error()) } - logger.Log(0, fmt.Sprintf("---------------> USERINFO: %v, token: %s", string(contents), token.AccessToken)) var userInfo = &OAuthUser{} if err = json.Unmarshal(contents, userInfo); err != nil { return nil, fmt.Errorf("failed parsing email from response data: %s", err.Error()) diff --git a/pro/controllers/users.go b/pro/controllers/users.go index 2a96d03c..91af1ad1 100644 --- a/pro/controllers/users.go +++ b/pro/controllers/users.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "github.com/gorilla/mux" "github.com/gravitl/netmaker/database" @@ -218,8 +219,12 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) { NetworkRoles: inviteReq.NetworkRoles, InviteCode: logic.RandomString(8), } + frontendURL := strings.TrimSuffix(servercfg.GetFrontendURL(), "/") + if frontendURL == "" { + frontendURL = fmt.Sprintf("https://dashboard.%s", servercfg.GetNmBaseDomain()) + } u, err := url.Parse(fmt.Sprintf("%s/invite?email=%s&invite_code=%s", - servercfg.GetFrontendURL(), url.QueryEscape(invite.Email), url.QueryEscape(invite.InviteCode))) + frontendURL, url.QueryEscape(invite.Email), url.QueryEscape(invite.InviteCode))) if err != nil { slog.Error("failed to parse to invite url", "error", err) return @@ -808,21 +813,18 @@ func removeUserFromRemoteAccessGW(w http.ResponseWriter, r *http.Request) { func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) { // set header. w.Header().Set("Content-Type", "application/json") - logger.Log(0, "------------> 1. getUserRemoteAccessGwsV1") var params = mux.Vars(r) username := params["username"] if username == "" { logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params username"), "badrequest")) return } - logger.Log(0, "------------> 2. getUserRemoteAccessGwsV1") user, err := logic.GetUser(username) if err != nil { logger.Log(0, username, "failed to fetch user: ", err.Error()) logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest")) return } - logger.Log(0, "------------> 3. getUserRemoteAccessGwsV1") remoteAccessClientID := r.URL.Query().Get("remote_access_clientid") var req models.UserRemoteGwsReq if remoteAccessClientID == "" { @@ -833,7 +835,6 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) { return } } - logger.Log(0, "------------> 4. getUserRemoteAccessGwsV1") reqFromMobile := r.URL.Query().Get("from_mobile") == "true" if req.RemoteAccessClientID == "" && remoteAccessClientID == "" { logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("remote access client id cannot be empty"), "badrequest")) @@ -843,13 +844,11 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) { req.RemoteAccessClientID = remoteAccessClientID } userGws := make(map[string][]models.UserRemoteGws) - logger.Log(0, "------------> 5. getUserRemoteAccessGwsV1") allextClients, err := logic.GetAllExtClients() if err != nil { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - logger.Log(0, "------------> 6. getUserRemoteAccessGwsV1") userGwNodes := proLogic.GetUserRAGNodes(*user) logger.Log(0, fmt.Sprintf("1. User Gw Nodes: %+v", userGwNodes)) for _, extClient := range allextClients { @@ -890,7 +889,6 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) { logger.Log(0, fmt.Sprintf("2. User Gw Nodes: %+v", userGwNodes)) // add remaining gw nodes to resp for gwID := range userGwNodes { - logger.Log(0, "RAG ---> 1") node, err := logic.GetNodeByID(gwID) if err != nil { continue @@ -901,7 +899,6 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) { if node.PendingDelete { continue } - logger.Log(0, "RAG ---> 2") host, err := logic.GetHost(node.HostID.String()) if err != nil { continue @@ -910,7 +907,6 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) { if err != nil { slog.Error("failed to get node network", "error", err) } - logger.Log(0, "RAG ---> 3") gws := userGws[node.Network] gws = append(gws, models.UserRemoteGws{ diff --git a/pro/logic/security.go b/pro/logic/security.go index 3225c269..508ac656 100644 --- a/pro/logic/security.go +++ b/pro/logic/security.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" - "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" "github.com/gravitl/netmaker/models" ) @@ -16,7 +15,6 @@ func NetworkPermissionsCheck(username string, r *http.Request) error { 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") @@ -24,7 +22,6 @@ func NetworkPermissionsCheck(username string, r *http.Request) error { 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") @@ -81,7 +78,6 @@ func checkNetworkAccessPermissions(netRoleID models.UserRoleID, username, reqSco if err != nil { return err } - logger.Log(0, "NET MIDDL----> 3", string(netRoleID)) if networkPermissionScope.FullAccess { return nil } @@ -92,7 +88,6 @@ func checkNetworkAccessPermissions(netRoleID models.UserRoleID, username, reqSco 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 != "" { @@ -118,7 +113,6 @@ func checkNetworkAccessPermissions(netRoleID models.UserRoleID, username, reqSco } } } - logger.Log(0, "NET MIDDL----> 5", string(netRoleID)) if targetRsrcID == "" { return errors.New("target rsrc id is empty") } @@ -128,7 +122,6 @@ func checkNetworkAccessPermissions(netRoleID models.UserRoleID, username, reqSco return nil } } - logger.Log(0, "NET MIDDL----> 6", string(netRoleID)) return errors.New("access denied") } diff --git a/pro/logic/user_mgmt.go b/pro/logic/user_mgmt.go index 243f3a97..5ff20fe9 100644 --- a/pro/logic/user_mgmt.go +++ b/pro/logic/user_mgmt.go @@ -533,7 +533,7 @@ func HasNetworkRsrcScope(permissionTemplate models.UserRolePermissionTemplate, n func GetUserRAGNodes(user models.User) (gws map[string]models.Node) { gws = make(map[string]models.Node) userGwAccessScope := GetUserNetworkRolesWithRemoteVPNAccess(user) - logger.Log(0, fmt.Sprintf("User Gw Access Scope: %+v", userGwAccessScope)) + logger.Log(3, fmt.Sprintf("User Gw Access Scope: %+v", userGwAccessScope)) _, allNetAccess := userGwAccessScope["*"] nodes, err := logic.GetAllNodes() if err != nil { diff --git a/servercfg/serverconf.go b/servercfg/serverconf.go index 98e5d5b4..6736d0c5 100644 --- a/servercfg/serverconf.go +++ b/servercfg/serverconf.go @@ -809,3 +809,8 @@ func GetAllowedEmailDomains() string { } return allowedDomains } + +// GetNmBaseDomain - fetches nm base domain +func GetNmBaseDomain() string { + return os.Getenv("NM_DOMAIN") +} From a39da31fa668c8c1d768c3a4fa24b49ba4a04730 Mon Sep 17 00:00:00 2001 From: Abhishek K <32607604+abhishek9686@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:51:13 +0530 Subject: [PATCH 04/20] add list roles to pro and ce (#3072) --- controllers/user.go | 24 ++++++++++++++++++++++++ logic/user_mgmt.go | 21 +++++++++++++++++++++ pro/controllers/users.go | 5 ++--- pro/initialize.go | 1 + pro/logic/user_mgmt.go | 21 --------------------- 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/controllers/user.go b/controllers/user.go index 72925bc2..1524b12d 100644 --- a/controllers/user.go +++ b/controllers/user.go @@ -23,6 +23,8 @@ var ( upgrader = websocket.Upgrader{} ) +var ListRoles = listRoles + func userHandlers(r *mux.Router) { r.HandleFunc("/api/users/adm/hassuperadmin", hasSuperAdmin).Methods(http.MethodGet) r.HandleFunc("/api/users/adm/createsuperadmin", createSuperAdmin).Methods(http.MethodPost) @@ -35,6 +37,7 @@ func userHandlers(r *mux.Router) { 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) + r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(ListRoles))).Methods(http.MethodGet) } @@ -710,3 +713,24 @@ func socketHandler(w http.ResponseWriter, r *http.Request) { // Start handling the session go auth.SessionHandler(conn) } + +// @Summary lists all user roles. +// @Router /api/v1/user/roles [get] +// @Tags Users +// @Param role_id param string true "roleid required to get the role details" +// @Success 200 {object} []models.UserRolePermissionTemplate +// @Failure 500 {object} models.ErrorResponse +func listRoles(w http.ResponseWriter, r *http.Request) { + var roles []models.UserRolePermissionTemplate + var err error + roles, err = logic.ListPlatformRoles() + if err != nil { + logic.ReturnErrorResponse(w, r, models.ErrorResponse{ + Code: http.StatusInternalServerError, + Message: err.Error(), + }) + return + } + + logic.ReturnSuccessResponseWithJson(w, r, roles, "successfully fetched user roles permission templates") +} diff --git a/logic/user_mgmt.go b/logic/user_mgmt.go index 6f20cd71..8727cb57 100644 --- a/logic/user_mgmt.go +++ b/logic/user_mgmt.go @@ -66,6 +66,27 @@ func GetRole(roleID models.UserRoleID) (models.UserRolePermissionTemplate, error return ur, nil } +// ListPlatformRoles - lists user platform roles permission templates +func ListPlatformRoles() ([]models.UserRolePermissionTemplate, error) { + data, err := database.FetchRecords(database.USER_PERMISSIONS_TABLE_NAME) + if err != nil && !database.IsEmptyRecord(err) { + return []models.UserRolePermissionTemplate{}, err + } + userRoles := []models.UserRolePermissionTemplate{} + for _, dataI := range data { + userRole := models.UserRolePermissionTemplate{} + err := json.Unmarshal([]byte(dataI), &userRole) + if err != nil { + continue + } + if userRole.NetworkID != "" { + continue + } + userRoles = append(userRoles, userRole) + } + return userRoles, nil +} + func userRolesInit() { d, _ := json.Marshal(SuperAdminPermissionTemplate) database.Insert(SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME) diff --git a/pro/controllers/users.go b/pro/controllers/users.go index 91af1ad1..393b2895 100644 --- a/pro/controllers/users.go +++ b/pro/controllers/users.go @@ -30,7 +30,6 @@ func UserHandlers(r *mux.Router) { r.HandleFunc("/api/oauth/register/{regKey}", proAuth.RegisterHostSSO).Methods(http.MethodGet) // User Role Handlers - r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(listRoles))).Methods(http.MethodGet) r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(getRole))).Methods(http.MethodGet) r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(createRole))).Methods(http.MethodPost) r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(updateRole))).Methods(http.MethodPut) @@ -499,12 +498,12 @@ func deleteUserGroup(w http.ResponseWriter, r *http.Request) { // @Param role_id param string true "roleid required to get the role details" // @Success 200 {object} []models.UserRolePermissionTemplate // @Failure 500 {object} models.ErrorResponse -func listRoles(w http.ResponseWriter, r *http.Request) { +func ListRoles(w http.ResponseWriter, r *http.Request) { platform, _ := url.QueryUnescape(r.URL.Query().Get("platform")) var roles []models.UserRolePermissionTemplate var err error if platform == "true" { - roles, err = proLogic.ListPlatformRoles() + roles, err = logic.ListPlatformRoles() } else { roles, err = proLogic.ListNetworkRoles() } diff --git a/pro/initialize.go b/pro/initialize.go index 948f8457..1c6ba8a1 100644 --- a/pro/initialize.go +++ b/pro/initialize.go @@ -34,6 +34,7 @@ func InitPro() { proControllers.FailOverHandlers, proControllers.InetHandlers, ) + controller.ListRoles = proControllers.ListRoles logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() { // == License Handling == enableLicenseHook := false diff --git a/pro/logic/user_mgmt.go b/pro/logic/user_mgmt.go index 5ff20fe9..d9218d22 100644 --- a/pro/logic/user_mgmt.go +++ b/pro/logic/user_mgmt.go @@ -201,27 +201,6 @@ func ListNetworkRoles() ([]models.UserRolePermissionTemplate, error) { return userRoles, nil } -// ListPlatformRoles - lists user platform roles permission templates -func ListPlatformRoles() ([]models.UserRolePermissionTemplate, error) { - data, err := database.FetchRecords(database.USER_PERMISSIONS_TABLE_NAME) - if err != nil && !database.IsEmptyRecord(err) { - return []models.UserRolePermissionTemplate{}, err - } - userRoles := []models.UserRolePermissionTemplate{} - for _, dataI := range data { - userRole := models.UserRolePermissionTemplate{} - err := json.Unmarshal([]byte(dataI), &userRole) - if err != nil { - continue - } - if userRole.NetworkID != "" { - continue - } - userRoles = append(userRoles, userRole) - } - return userRoles, nil -} - func ValidateCreateRoleReq(userRole *models.UserRolePermissionTemplate) error { // check if role exists with this id _, err := logic.GetRole(userRole.ID) From d95b96ad6e2fcdbc69c3db311461cbcef8bfc67f Mon Sep 17 00:00:00 2001 From: Abhishek K <32607604+abhishek9686@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:14:57 +0530 Subject: [PATCH 05/20] Net 1227 v2 (#3073) * add list roles to pro and ce * if not pro set user role to admin --- controllers/user.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/controllers/user.go b/controllers/user.go index 1524b12d..e1166576 100644 --- a/controllers/user.go +++ b/controllers/user.go @@ -410,6 +410,9 @@ func createUser(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) return } + if !servercfg.IsPro { + user.PlatformRoleID = models.AdminRole + } if user.PlatformRoleID == "" { logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("platform role is missing"), "badrequest")) From a4d528e26722e0201897c6b2d754dde557f48c3f Mon Sep 17 00:00:00 2001 From: Abhishek K <32607604+abhishek9686@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:29:32 +0530 Subject: [PATCH 06/20] Net 1227 v2 (#3074) * add list roles to pro and ce * if not pro set user role to admin * validate update user --- logic/auth.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/logic/auth.go b/logic/auth.go index 7486d310..65e3a5d7 100644 --- a/logic/auth.go +++ b/logic/auth.go @@ -268,6 +268,10 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) { if _, err := GetUser(user.UserName); err != nil { return &models.User{}, err } + err := ValidateUser(userchange) + if err != nil { + return &models.User{}, err + } queryUser := user.UserName if userchange.UserName != "" && user.UserName != userchange.UserName { @@ -302,12 +306,8 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) { } user.UserGroups = userchange.UserGroups user.NetworkRoles = userchange.NetworkRoles - err := ValidateUser(user) - if err != nil { - return &models.User{}, err - } - if err = database.DeleteRecord(database.USERS_TABLE_NAME, queryUser); err != nil { + if err := database.DeleteRecord(database.USERS_TABLE_NAME, queryUser); err != nil { return &models.User{}, err } data, err := json.Marshal(&user) From d532060c50e1e13a86daea1720f5df045faeb3b7 Mon Sep 17 00:00:00 2001 From: Abhishek K <32607604+abhishek9686@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:36:53 +0530 Subject: [PATCH 07/20] Net 1227 v2 (#3075) * add list roles to pro and ce * if not pro set user role to admin * validate update user * add separate validation check for password on update --- logic/auth.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/logic/auth.go b/logic/auth.go index 65e3a5d7..d0642cff 100644 --- a/logic/auth.go +++ b/logic/auth.go @@ -282,6 +282,9 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) { user.UserName = userchange.UserName } if userchange.Password != "" { + if len(userchange.Password) < 5 { + return &models.User{}, errors.New("password requires min 5 characters") + } // encrypt that password so we never see it again hash, err := bcrypt.GenerateFromPassword([]byte(userchange.Password), 5) @@ -306,8 +309,11 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) { } user.UserGroups = userchange.UserGroups user.NetworkRoles = userchange.NetworkRoles - - if err := database.DeleteRecord(database.USERS_TABLE_NAME, queryUser); err != nil { + err = ValidateUser(user) + if err != nil { + return &models.User{}, err + } + if err = database.DeleteRecord(database.USERS_TABLE_NAME, queryUser); err != nil { return &models.User{}, err } data, err := json.Marshal(&user) From 0a1558d5acefdf0d532a49dfdb681b3240ed6fc3 Mon Sep 17 00:00:00 2001 From: Abhishek K <32607604+abhishek9686@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:24:18 +0530 Subject: [PATCH 08/20] NET-1227: fix update password on update user handler (#3077) * add list roles to pro and ce * if not pro set user role to admin * validate update user * add separate validation check for password on update * remove validate check --- logic/auth.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/logic/auth.go b/logic/auth.go index d0642cff..3da9f605 100644 --- a/logic/auth.go +++ b/logic/auth.go @@ -268,10 +268,6 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) { if _, err := GetUser(user.UserName); err != nil { return &models.User{}, err } - err := ValidateUser(userchange) - if err != nil { - return &models.User{}, err - } queryUser := user.UserName if userchange.UserName != "" && user.UserName != userchange.UserName { @@ -309,7 +305,7 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) { } user.UserGroups = userchange.UserGroups user.NetworkRoles = userchange.NetworkRoles - err = ValidateUser(user) + err := ValidateUser(user) if err != nil { return &models.User{}, err } From 9ac78e15bcef1167e00fb250e234cc0c90f8658c Mon Sep 17 00:00:00 2001 From: Abhishek K Date: Tue, 27 Aug 2024 17:07:21 +0530 Subject: [PATCH 09/20] NET-1227: Fix Singup Flow with Github SSO (#3078) * add list roles to pro and ce * if not pro set user role to admin * validate update user * add separate validation check for password on update * remove validate check * fix github SSO with invite signup --- pro/auth/github.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pro/auth/github.go b/pro/auth/github.go index c62fd1b7..37c58452 100644 --- a/pro/auth/github.go +++ b/pro/auth/github.go @@ -67,10 +67,9 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { handleOauthNotConfigured(w) return } - var inviteExists bool // check if invite exists for User - in, err := logic.GetUserInvite(content.Login) + in, err := logic.GetUserInvite(content.Email) if err == nil { inviteExists = true } @@ -89,11 +88,12 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } + user.UserName = content.Login // overrides email with github id if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return } - logic.DeleteUserInvite(user.UserName) + logic.DeleteUserInvite(content.Email) logic.DeletePendingUser(content.Login) } else { if !isEmailAllowed(content.Login) { From 0463b17ea5e58816d057a61719fd1e5d54b5bb2f Mon Sep 17 00:00:00 2001 From: Abhishek K Date: Wed, 28 Aug 2024 14:14:26 +0530 Subject: [PATCH 10/20] NET-1227: Add Additional Oauth Scopes to fetch user email (#3079) * add list roles to pro and ce * if not pro set user role to admin * validate update user * add separate validation check for password on update * remove validate check * fix github SSO with invite signup * add oauth scopes for user email * remove debug log * fix azure ad --- go.mod | 3 +-- go.sum | 12 ------------ logic/security.go | 4 ---- pro/auth/auth.go | 12 ++++++++++++ pro/auth/azure-ad.go | 14 +++++++++----- pro/auth/github.go | 5 ++++- 6 files changed, 26 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 4a59f33f..b5bcfe90 100644 --- a/go.mod +++ b/go.mod @@ -38,11 +38,9 @@ require ( ) require ( - github.com/go-jose/go-jose/v3 v3.0.3 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 ) @@ -50,6 +48,7 @@ require ( require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/seancfoley/bintree v1.3.1 // indirect diff --git a/go.sum b/go.sum index 54809822..33d5e376 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= -github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= -github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -65,10 +63,6 @@ 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 v0.0.0-20211028072449-93c17c49e2b0 h1:Y2hUrkfuM0on62KZOci/VLijlkdF/yeWU262BQgvcjE= github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU= -github.com/posthog/posthog-go v1.2.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= @@ -102,8 +96,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -115,8 +107,6 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -134,8 +124,6 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/logic/security.go b/logic/security.go index aa692236..84f7a3cf 100644 --- a/logic/security.go +++ b/logic/security.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/gorilla/mux" - "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/models" "github.com/gravitl/netmaker/servercfg" ) @@ -27,12 +26,10 @@ 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 := GetUserNameFromToken(bearerToken) if err != nil { - logger.Log(0, "next 1", r.URL.String(), err.Error()) ReturnErrorResponse(w, r, FormatError(err, "unauthorized")) return } @@ -103,7 +100,6 @@ func ContinueIfUserMatch(next http.Handler) http.HandlerFunc { requestedUser, _ = url.QueryUnescape(r.URL.Query().Get("username")) } if requestedUser != r.Header.Get("user") { - logger.Log(0, "next 2", r.URL.String(), errorResponse.Message) ReturnErrorResponse(w, r, errorResponse) return } diff --git a/pro/auth/auth.go b/pro/auth/auth.go index 1fb0e3d6..49a34e73 100644 --- a/pro/auth/auth.go +++ b/pro/auth/auth.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/golang-jwt/jwt/v4" "github.com/gorilla/websocket" "github.com/gravitl/netmaker/logger" "github.com/gravitl/netmaker/logic" @@ -236,6 +237,17 @@ func getStateAndCode(r *http.Request) (string, string) { return state, code } +func getUserEmailFromClaims(token string) string { + accessToken, _ := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + return []byte(""), nil + }) + if accessToken == nil { + return "" + } + claims, _ := accessToken.Claims.(jwt.MapClaims) + return claims["email"].(string) +} + func (user *OAuthUser) getUserName() string { var userName string if user.Email != "" { diff --git a/pro/auth/azure-ad.go b/pro/auth/azure-ad.go index b0981aae..c41a96d8 100644 --- a/pro/auth/azure-ad.go +++ b/pro/auth/azure-ad.go @@ -33,7 +33,7 @@ func initAzureAD(redirectURL string, clientID string, clientSecret string) { RedirectURL: redirectURL, ClientID: clientID, ClientSecret: clientSecret, - Scopes: []string{"User.Read"}, + Scopes: []string{"User.Read", "email", "profile", "openid"}, Endpoint: microsoft.AzureADEndpoint(servercfg.GetAzureTenant()), } } @@ -67,10 +67,9 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { handleOauthNotConfigured(w) return } - var inviteExists bool // check if invite exists for User - in, err := logic.GetUserInvite(content.UserPrincipalName) + in, err := logic.GetUserInvite(content.Email) if err == nil { inviteExists = true } @@ -90,11 +89,12 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } + user.UserName = content.UserPrincipalName // override username with azure id if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return } - logic.DeleteUserInvite(user.UserName) + logic.DeleteUserInvite(content.Email) logic.DeletePendingUser(content.UserPrincipalName) } else { if !isEmailAllowed(content.UserPrincipalName) { @@ -166,8 +166,9 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) { } var httpReq, reqErr = http.NewRequest("GET", "https://graph.microsoft.com/v1.0/me", nil) if reqErr != nil { - return nil, fmt.Errorf("failed to create request to GitHub") + return nil, fmt.Errorf("failed to create request to microsoft") } + httpReq.Header.Set("Authorization", "Bearer "+token.AccessToken) response, err := http.DefaultClient.Do(httpReq) if err != nil { @@ -183,6 +184,9 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) { return nil, fmt.Errorf("failed parsing email from response data: %s", err.Error()) } userInfo.AccessToken = string(data) + if userInfo.Email == "" { + userInfo.Email = getUserEmailFromClaims(token.AccessToken) + } return userInfo, nil } diff --git a/pro/auth/github.go b/pro/auth/github.go index 37c58452..2b65ee2f 100644 --- a/pro/auth/github.go +++ b/pro/auth/github.go @@ -33,7 +33,7 @@ func initGithub(redirectURL string, clientID string, clientSecret string) { RedirectURL: redirectURL, ClientID: clientID, ClientSecret: clientSecret, - Scopes: []string{}, + Scopes: []string{"read:user", "user:email"}, Endpoint: github.Endpoint, } } @@ -186,6 +186,9 @@ func getGithubUserInfo(state string, code string) (*OAuthUser, error) { return nil, fmt.Errorf("failed parsing email from response data: %s", err.Error()) } userInfo.AccessToken = string(data) + if userInfo.Email == "" { + userInfo.Email = getUserEmailFromClaims(token.AccessToken) + } return userInfo, nil } From c10859a466755b8b00a1242407ab18d4cf0d7134 Mon Sep 17 00:00:00 2001 From: Abhishek K Date: Wed, 28 Aug 2024 14:34:34 +0530 Subject: [PATCH 11/20] v0.25.0 release notes (#3080) * release notes * Update release.md --- release.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/release.md b/release.md index 1ada3ae6..7f32b584 100644 --- a/release.md +++ b/release.md @@ -1,15 +1,14 @@ # Netmaker v0.25.0 ## Whats New ✨ -- Validation Checks For Egress Routes -- Network Change Detection System -- Removed Creation Of ACLs For EMQX +- Advanced User Management with Network Roles and Groups +- User Invitation via Email and Magic Links ## What's Fixed/Improved 🛠 -- Removed RAG Metadata Length Restriction + - Scalability Improvements - Optimised Traffic Flow Over MQ -- Improved Validation Checks For Internet GWS +- Improved Peer Updates with Batching ## Known Issues 🐞 @@ -18,5 +17,5 @@ - IPv6 DNS Entries Are Not Working. - Stale Peer On The Interface, When Forced Removed From Multiple Networks At Once. - Can Still Ping Domain Name Even When DNS Toggle Is Switched Off. -- WireGuard DNS issue on most flavors of Ubuntu 24.04 and some other newer Linux distributions. The issue is affecting the Remote Access Client (RAC) and the plain WireGuard external clients. Workaround can be found here https://help.netmaker.io/en/articles/9612016-extclient-rac-dns-issue-on-ubuntu-24-04. +- WireGuard DNS issue on most flavours of Ubuntu 24.04 and some other newer Linux distributions. The issue is affecting the Remote Access Client (RAC) and the plain WireGuard external clients. Workaround can be found here https://help.netmaker.io/en/articles/9612016-extclient-rac-dns-issue-on-ubuntu-24-04. From 19d7bb1ea290d00396cf968fddd6829feaab6d93 Mon Sep 17 00:00:00 2001 From: Aceix Date: Wed, 28 Aug 2024 13:22:59 +0000 Subject: [PATCH 12/20] fix: update yml references to json for v0.25.0 (#3083) --- scripts/nm-quick.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/nm-quick.sh b/scripts/nm-quick.sh index bf8c2826..76fdea56 100755 --- a/scripts/nm-quick.sh +++ b/scripts/nm-quick.sh @@ -129,7 +129,7 @@ setup_netclient() { echo "waiting for netclient to become available" local found=false - local file=/etc/netclient/nodes.yml + local file=/etc/netclient/nodes.json for ((a = 1; a <= 90; a++)); do if [ -f "$file" ]; then found=true @@ -147,13 +147,13 @@ setup_netclient() { # configure_netclient - configures server's netclient as a default host and an ingress gateway configure_netclient() { sleep 2 - NODE_ID=$(sudo cat /etc/netclient/nodes.yml | yq -r .netmaker.commonnode.id) + NODE_ID=$(sudo cat /etc/netclient/nodes.json | jq -r .netmaker.id) if [ "$NODE_ID" = "" ] || [ "$NODE_ID" = "null" ]; then echo "Error obtaining NODE_ID for the new network" exit 1 fi echo "register complete. New node ID: $NODE_ID" - HOST_ID=$(sudo cat /etc/netclient/netclient.yml | yq -r .host.id) + HOST_ID=$(sudo cat /etc/netclient/netclient.json | jq -r .id) if [ "$HOST_ID" = "" ] || [ "$HOST_ID" = "null" ]; then echo "Error obtaining HOST_ID for the new network" exit 1 From bbca20e463caabfcd327fc81efa3c740f04a3e92 Mon Sep 17 00:00:00 2001 From: Max Ma Date: Wed, 28 Aug 2024 15:28:07 +0200 Subject: [PATCH 13/20] NET-1565:fix extClient ip conflict issue (#3082) * fix extClient ip conflict issue * Update users.go --------- Co-authored-by: Abhishek K --- logic/networks.go | 32 ++++++++++++++++++++++++-------- pro/controllers/users.go | 2 -- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/logic/networks.go b/logic/networks.go index b87c6db8..14ad794e 100644 --- a/logic/networks.go +++ b/logic/networks.go @@ -42,19 +42,35 @@ func SetAllocatedIpMap() error { pMap := map[string]net.IP{} netName := v.NetID + //nodes nodes, err := GetNetworkNodes(netName) if err != nil { slog.Error("could not load node for network", netName, "error", err.Error()) - continue + } else { + for _, n := range nodes { + + if n.Address.IP != nil { + pMap[n.Address.IP.String()] = n.Address.IP + } + if n.Address6.IP != nil { + pMap[n.Address6.IP.String()] = n.Address6.IP + } + } + } - for _, n := range nodes { - - if n.Address.IP != nil { - pMap[n.Address.IP.String()] = n.Address.IP - } - if n.Address6.IP != nil { - pMap[n.Address6.IP.String()] = n.Address6.IP + //extClients + extClients, err := GetNetworkExtClients(netName) + if err != nil { + slog.Error("could not load extClient for network", netName, "error", err.Error()) + } else { + for _, extClient := range extClients { + if extClient.Address != "" { + pMap[extClient.Address] = net.ParseIP(extClient.Address) + } + if extClient.Address6 != "" { + pMap[extClient.Address6] = net.ParseIP(extClient.Address6) + } } } diff --git a/pro/controllers/users.go b/pro/controllers/users.go index 393b2895..c5076d3d 100644 --- a/pro/controllers/users.go +++ b/pro/controllers/users.go @@ -849,7 +849,6 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) { return } userGwNodes := proLogic.GetUserRAGNodes(*user) - logger.Log(0, fmt.Sprintf("1. User Gw Nodes: %+v", userGwNodes)) for _, extClient := range allextClients { node, ok := userGwNodes[extClient.IngressGatewayID] if !ok { @@ -885,7 +884,6 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) { delete(userGwNodes, node.ID.String()) } } - logger.Log(0, fmt.Sprintf("2. User Gw Nodes: %+v", userGwNodes)) // add remaining gw nodes to resp for gwID := range userGwNodes { node, err := logic.GetNodeByID(gwID) From 5ad21606f25f502489f15e1a5b87a8fcc5f21956 Mon Sep 17 00:00:00 2001 From: Sayan Mallick Date: Sun, 1 Sep 2024 22:06:49 +0530 Subject: [PATCH 14/20] Delete Droplet update --- .github/workflows/deletedroplets.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deletedroplets.yml b/.github/workflows/deletedroplets.yml index 04019038..63f25824 100644 --- a/.github/workflows/deletedroplets.yml +++ b/.github/workflows/deletedroplets.yml @@ -37,11 +37,25 @@ jobs: - name: delete droplets if: success() || failure() run: | - sleep 15m - curl -X DELETE \ + sleep 5m + response=$(curl -X DELETE \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \ - "https://api.digitalocean.com/v2/droplets?tag_name=$TAG" + -w "\n%{http_code}" \ + "https://api.digitalocean.com/v2/droplets?tag_name=$TAG") + + status_code=$(echo "$response" | tail -n1) + body=$(echo "$response" | sed '$d') + + echo "Response body: $body" + echo "Status code: $status_code" + + if [ "$status_code" -eq 204 ]; then + echo "Droplets deleted successfully" + else + echo "Failed to delete droplets. Status code: $status_code" + exit 1 + fi env: DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }} TAG: ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }} From ebce98448c76c19d0d7159743692dcd6b9ab359d Mon Sep 17 00:00:00 2001 From: abhishek9686 Date: Mon, 2 Sep 2024 09:23:28 +0530 Subject: [PATCH 15/20] use github apis to fetch user email --- models/user_mgmt.go | 21 +++++------ pro/auth/azure-ad.go | 40 ++++++++++++++------- pro/auth/github.go | 84 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 110 insertions(+), 35 deletions(-) diff --git a/models/user_mgmt.go b/models/user_mgmt.go index 3efa81bf..a928f528 100644 --- a/models/user_mgmt.go +++ b/models/user_mgmt.go @@ -138,16 +138,17 @@ type UserGroup struct { // 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"` + UserName string `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"` + ExternalProviderID string `json:"external_provider_id"` + 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 { diff --git a/pro/auth/azure-ad.go b/pro/auth/azure-ad.go index c41a96d8..fbe588ad 100644 --- a/pro/auth/azure-ad.go +++ b/pro/auth/azure-ad.go @@ -3,6 +3,7 @@ package auth import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -60,7 +61,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { var content, err = getAzureUserInfo(rState, rCode) if err != nil { logger.Log(1, "error when getting user info from azure:", err.Error()) - if strings.Contains(err.Error(), "invalid oauth state") { + if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") { handleOauthNotValid(w) return } @@ -74,12 +75,23 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { inviteExists = true } // check if user approval is already pending - if !inviteExists && logic.IsPendingUser(content.UserPrincipalName) { + if !inviteExists && logic.IsPendingUser(content.Email) { handleOauthUserSignUpApprovalPending(w) return } - - _, err = logic.GetUser(content.UserPrincipalName) + // if user exists with provider ID, convert them into email ID + user, err := logic.GetUser(content.UserPrincipalName) + if err == nil { + _, err := logic.GetUser(content.Email) + if err != nil { + user.UserName = content.Email + user.ExternalProviderID = content.UserPrincipalName + database.DeleteRecord(database.USERS_TABLE_NAME, content.UserPrincipalName) + d, _ := json.Marshal(user) + database.Insert(user.UserName, string(d), database.USERS_TABLE_NAME) + } + } + _, err = logic.GetUser(content.Email) if err != nil { if database.IsEmptyRecord(err) { // user must not exist, so try to make one if inviteExists { @@ -89,20 +101,20 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - user.UserName = content.UserPrincipalName // override username with azure id + user.ExternalProviderID = content.UserPrincipalName if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return } logic.DeleteUserInvite(content.Email) - logic.DeletePendingUser(content.UserPrincipalName) + logic.DeletePendingUser(content.Email) } else { - if !isEmailAllowed(content.UserPrincipalName) { + if !isEmailAllowed(content.Email) { handleOauthUserNotAllowedToSignUp(w) return } err = logic.InsertPendingUser(&models.User{ - UserName: content.UserPrincipalName, + UserName: content.Email, }) if err != nil { handleSomethingWentWrong(w) @@ -116,7 +128,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { return } } - user, err := logic.GetUser(content.UserPrincipalName) + user, err = logic.GetUser(content.Email) if err != nil { handleOauthUserNotFound(w) return @@ -136,7 +148,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { } // send a netmaker jwt token var authRequest = models.UserAuthParams{ - UserName: content.UserPrincipalName, + UserName: content.Email, Password: newPass, } @@ -146,8 +158,8 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { return } - logger.Log(1, "completed azure OAuth sigin in for", content.UserPrincipalName) - http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.UserPrincipalName, http.StatusPermanentRedirect) + logger.Log(1, "completed azure OAuth sigin in for", content.Email) + http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect) } func getAzureUserInfo(state string, code string) (*OAuthUser, error) { @@ -187,6 +199,10 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) { if userInfo.Email == "" { userInfo.Email = getUserEmailFromClaims(token.AccessToken) } + if userInfo.Email == "" { + err = errors.New("failed to fetch user email from SSO state") + return userInfo, err + } return userInfo, nil } diff --git a/pro/auth/github.go b/pro/auth/github.go index 2b65ee2f..1cb52cf9 100644 --- a/pro/auth/github.go +++ b/pro/auth/github.go @@ -3,6 +3,7 @@ package auth import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -60,7 +61,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { var content, err = getGithubUserInfo(rState, rCode) if err != nil { logger.Log(1, "error when getting user info from github:", err.Error()) - if strings.Contains(err.Error(), "invalid oauth state") { + if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") { handleOauthNotValid(w) return } @@ -74,11 +75,25 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { inviteExists = true } // check if user approval is already pending - if !inviteExists && logic.IsPendingUser(content.Login) { + if !inviteExists && logic.IsPendingUser(content.Email) { handleOauthUserSignUpApprovalPending(w) return } - _, err = logic.GetUser(content.Login) + // if user exists with provider ID, convert them into email ID + user, err := logic.GetUser(content.Login) + if err == nil { + // checks if user exists with email + _, err := logic.GetUser(content.Email) + if err != nil { + user.UserName = content.Email + user.ExternalProviderID = content.Login + database.DeleteRecord(database.USERS_TABLE_NAME, content.Login) + d, _ := json.Marshal(user) + database.Insert(user.UserName, string(d), database.USERS_TABLE_NAME) + } + + } + _, err = logic.GetUser(content.Email) if err != nil { if database.IsEmptyRecord(err) { // user must not exist, so try to make one if inviteExists { @@ -88,20 +103,20 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - user.UserName = content.Login // overrides email with github id + user.ExternalProviderID = content.Login if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return } logic.DeleteUserInvite(content.Email) - logic.DeletePendingUser(content.Login) + logic.DeletePendingUser(content.Email) } else { - if !isEmailAllowed(content.Login) { + if !isEmailAllowed(content.Email) { handleOauthUserNotAllowedToSignUp(w) return } err = logic.InsertPendingUser(&models.User{ - UserName: content.Login, + UserName: content.Email, }) if err != nil { handleSomethingWentWrong(w) @@ -115,7 +130,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { return } } - user, err := logic.GetUser(content.Login) + user, err = logic.GetUser(content.Email) if err != nil { handleOauthUserNotFound(w) return @@ -135,7 +150,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { } // send a netmaker jwt token var authRequest = models.UserAuthParams{ - UserName: content.Login, + UserName: content.Email, Password: newPass, } @@ -145,11 +160,11 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { return } - logger.Log(1, "completed github OAuth sigin in for", content.Login) - http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Login, http.StatusPermanentRedirect) + logger.Log(1, "completed github OAuth sigin in for", content.Email) + http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect) } -func getGithubUserInfo(state string, code string) (*OAuthUser, error) { +func getGithubUserInfo(state, code string) (*OAuthUser, error) { oauth_state_string, isValid := logic.IsStateValid(state) if (!isValid || state != oauth_state_string) && !isStateCached(state) { return nil, fmt.Errorf("invalid oauth state") @@ -187,7 +202,16 @@ func getGithubUserInfo(state string, code string) (*OAuthUser, error) { } userInfo.AccessToken = string(data) if userInfo.Email == "" { - userInfo.Email = getUserEmailFromClaims(token.AccessToken) + // if user's email is not made public, get the info from the github emails api + logger.Log(0, "=======> fetching user email from github api") + userInfo.Email, err = getGithubEmailsInfo(token.AccessToken) + if err != nil { + logger.Log(0, "failed to fetch user's email from github: ", err.Error()) + } + } + if userInfo.Email == "" { + err = errors.New("failed to fetch user email from SSO state") + return userInfo, err } return userInfo, nil } @@ -195,3 +219,37 @@ func getGithubUserInfo(state string, code string) (*OAuthUser, error) { func verifyGithubUser(token *oauth2.Token) bool { return token.Valid() } + +func getGithubEmailsInfo(accessToken string) (string, error) { + + var httpClient = &http.Client{} + var httpReq, reqErr = http.NewRequest("GET", "https://api.github.com/user/emails", nil) + if reqErr != nil { + return "", fmt.Errorf("failed to create request to GitHub") + } + httpReq.Header.Add("Accept", "application/vnd.github.v3+json") + httpReq.Header.Set("Authorization", "token "+accessToken) + response, err := httpClient.Do(httpReq) + if err != nil { + return "", fmt.Errorf("failed getting user info: %s", err.Error()) + } + defer response.Body.Close() + contents, err := io.ReadAll(response.Body) + if err != nil { + return "", fmt.Errorf("failed reading response body: %s", err.Error()) + } + + emailsInfo := []interface{}{} + err = json.Unmarshal(contents, &emailsInfo) + if err != nil { + return "", err + } + for _, info := range emailsInfo { + emailInfoMap := info.(map[string]interface{}) + if emailInfoMap["primary"].(bool) { + return emailInfoMap["email"].(string), nil + } + + } + return "", errors.New("email not found") +} From ed2a0a0a01e351d1fb8d722d2be6421385bffff3 Mon Sep 17 00:00:00 2001 From: abhishek9686 Date: Mon, 2 Sep 2024 10:57:10 +0530 Subject: [PATCH 16/20] fix oidc invite flow --- models/user_mgmt.go | 22 +++++++++++----------- pro/auth/azure-ad.go | 4 ++-- pro/auth/github.go | 4 ++-- pro/auth/google.go | 2 +- pro/auth/oidc.go | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/models/user_mgmt.go b/models/user_mgmt.go index a928f528..a87a0f4b 100644 --- a/models/user_mgmt.go +++ b/models/user_mgmt.go @@ -138,17 +138,17 @@ type UserGroup struct { // User struct - struct for Users type User struct { - UserName string `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"` - ExternalProviderID string `json:"external_provider_id"` - 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"` + UserName string `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"` + ExternalIdentityProviderID string `json:"external_identity_provider_id"` + 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 { diff --git a/pro/auth/azure-ad.go b/pro/auth/azure-ad.go index fbe588ad..7aa34953 100644 --- a/pro/auth/azure-ad.go +++ b/pro/auth/azure-ad.go @@ -85,7 +85,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { _, err := logic.GetUser(content.Email) if err != nil { user.UserName = content.Email - user.ExternalProviderID = content.UserPrincipalName + user.ExternalIdentityProviderID = content.UserPrincipalName database.DeleteRecord(database.USERS_TABLE_NAME, content.UserPrincipalName) d, _ := json.Marshal(user) database.Insert(user.UserName, string(d), database.USERS_TABLE_NAME) @@ -101,7 +101,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - user.ExternalProviderID = content.UserPrincipalName + user.ExternalIdentityProviderID = content.UserPrincipalName if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return diff --git a/pro/auth/github.go b/pro/auth/github.go index 1cb52cf9..5d2db594 100644 --- a/pro/auth/github.go +++ b/pro/auth/github.go @@ -86,7 +86,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { _, err := logic.GetUser(content.Email) if err != nil { user.UserName = content.Email - user.ExternalProviderID = content.Login + user.ExternalIdentityProviderID = content.Login database.DeleteRecord(database.USERS_TABLE_NAME, content.Login) d, _ := json.Marshal(user) database.Insert(user.UserName, string(d), database.USERS_TABLE_NAME) @@ -103,7 +103,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - user.ExternalProviderID = content.Login + user.ExternalIdentityProviderID = content.Login if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return diff --git a/pro/auth/google.go b/pro/auth/google.go index 94db3a7c..9ba9772c 100644 --- a/pro/auth/google.go +++ b/pro/auth/google.go @@ -90,7 +90,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - + user.ExternalIdentityProviderID = content.Email if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return diff --git a/pro/auth/oidc.go b/pro/auth/oidc.go index 72dc2b95..2fc71f66 100644 --- a/pro/auth/oidc.go +++ b/pro/auth/oidc.go @@ -80,10 +80,9 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) { handleOauthNotConfigured(w) return } - var inviteExists bool // check if invite exists for User - in, err := logic.GetUserInvite(content.Login) + in, err := logic.GetUserInvite(content.Email) if err == nil { inviteExists = true } @@ -102,6 +101,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } + user.ExternalIdentityProviderID = content.Email if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return From c4bfae77df6f4f861f989d768c3d70ac528d4a63 Mon Sep 17 00:00:00 2001 From: abhishek9686 Date: Mon, 2 Sep 2024 14:15:04 +0530 Subject: [PATCH 17/20] increase log verbose --- pro/auth/github.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pro/auth/github.go b/pro/auth/github.go index 5d2db594..e72dc439 100644 --- a/pro/auth/github.go +++ b/pro/auth/github.go @@ -203,7 +203,7 @@ func getGithubUserInfo(state, code string) (*OAuthUser, error) { userInfo.AccessToken = string(data) if userInfo.Email == "" { // if user's email is not made public, get the info from the github emails api - logger.Log(0, "=======> fetching user email from github api") + logger.Log(2, "fetching user email from github api") userInfo.Email, err = getGithubEmailsInfo(token.AccessToken) if err != nil { logger.Log(0, "failed to fetch user's email from github: ", err.Error()) From 021cebdd09d2755c06b78218f4f580bf6216786d Mon Sep 17 00:00:00 2001 From: Sayan Mallick Date: Mon, 2 Sep 2024 17:13:33 +0530 Subject: [PATCH 18/20] Updated DeleteDroplet workflow to use new token --- .github/workflows/deletedroplets.yml | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deletedroplets.yml b/.github/workflows/deletedroplets.yml index 63f25824..11577278 100644 --- a/.github/workflows/deletedroplets.yml +++ b/.github/workflows/deletedroplets.yml @@ -37,7 +37,7 @@ jobs: - name: delete droplets if: success() || failure() run: | - sleep 5m + sleep 1m response=$(curl -X DELETE \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \ @@ -56,8 +56,9 @@ jobs: echo "Failed to delete droplets. Status code: $status_code" exit 1 fi + sleep 1m env: - DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }} + DIGITALOCEAN_TOKEN: ${{ secrets.DO_TEST_TOKEN }} TAG: ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }} - name: mark server as available if: success() || failure() @@ -108,13 +109,28 @@ jobs: - name: delete droplets if: success() || failure() run: | - sleep 3h - curl -X DELETE \ + sleep 1m + response=$(curl -X DELETE \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \ - "https://api.digitalocean.com/v2/droplets?tag_name=$TAG" + -w "\n%{http_code}" \ + "https://api.digitalocean.com/v2/droplets?tag_name=$TAG") + + status_code=$(echo "$response" | tail -n1) + body=$(echo "$response" | sed '$d') + + echo "Response body: $body" + echo "Status code: $status_code" + + if [ "$status_code" -eq 204 ]; then + echo "Droplets deleted successfully" + else + echo "Failed to delete droplets. Status code: $status_code" + exit 1 + fi + sleep 1m env: - DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }} + DIGITALOCEAN_TOKEN: ${{ secrets.DO_TEST_TOKEN }} TAG: ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }} - name: mark server as available if: success() || failure() From 1bb349206091e849acc2cee01aab4e5644f4aca9 Mon Sep 17 00:00:00 2001 From: Sayan Mallick Date: Mon, 2 Sep 2024 19:27:59 +0530 Subject: [PATCH 19/20] Updated Deploy and Test Branch to have a manual trigger --- .github/workflows/branchtest.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/branchtest.yml b/.github/workflows/branchtest.yml index d2b22a05..4dafd9d5 100644 --- a/.github/workflows/branchtest.yml +++ b/.github/workflows/branchtest.yml @@ -2,6 +2,11 @@ name: Deploy and Test Branch on: workflow_dispatch: + inputs: + branches: + description: 'Branch to deploy and test' + required: true + default: 'develop' pull_request: types: [opened, synchronize, reopened] branches: [develop] @@ -28,7 +33,7 @@ jobs: uses: actions/checkout@v4 with: repository: gravitl/netclient - ref: develop + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.branch || 'develop' }} - name: check if branch exists id: getbranch run: | @@ -45,6 +50,6 @@ jobs: needs: [getbranch, skip-check] with: netclientbranch: ${{ needs.getbranch.outputs.netclientbranch }} - netmakerbranch: ${{ github.head_ref }} + netmakerbranch: ${{ github.event_name == 'workflow_dispatch' && inputs.branch || github.head_ref }} tag: ${{ github.run_id }}-${{ github.run_attempt }} secrets: inherit From 5066781566ad2a295218942ee3e5b124a876edaf Mon Sep 17 00:00:00 2001 From: abhishek9686 Date: Wed, 4 Sep 2024 12:12:28 +0530 Subject: [PATCH 20/20] build login link from nm domain --- pro/auth/error.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pro/auth/error.go b/pro/auth/error.go index b2a5efcd..eb49b876 100644 --- a/pro/auth/error.go +++ b/pro/auth/error.go @@ -18,7 +18,7 @@ var htmlBaseTemplate = `