netmaker/pro/controllers/users.go
Vishal Dalwadi 614cf77b5a
NET-1991: Add IDP sync functionality. (#3428)
* feat: api access tokens

* revoke all user tokens

* redefine access token api routes, add auto egress option to enrollment keys

* add server settings apis, add db table for settigs

* handle server settings updates

* switch to using settings from DB

* fix sever settings migration

* revet force migration for settings

* fix server settings database write

* fix revoked tokens to be unauthorized

* remove unused functions

* convert access token to sql schema

* switch access token to sql schema

* fix merge conflicts

* fix server settings types

* bypass basic auth setting for super admin

* add TODO comment

* feat(go): add types for idp package;

* feat(go): import azure sdk;

* feat(go): add stub for google workspace client;

* feat(go): implement azure ad client;

* feat(go): sync users and groups using idp client;

* publish peer update on settings update

* feat(go): read creds from env vars;

* feat(go): add api endpoint to trigger idp sync;

* fix(go): sync member changes;

* fix(go): handle error;

* fix(go): set correct response type;

* feat(go): support disabling user accounts;

1. Add api endpoints to enable and disable user accounts.
2. Add checks in authenticators to prevent disabled users from logging in.
3. Add checks in middleware to prevent api usage by disabled users.

* feat(go): use string slice for group members;

* feat(go): sync user account status from idp;

* feat(go): import google admin sdk;

* feat(go): add support for google workspace idp;

* feat(go): initialize idp client on sync;

* feat(go): sync from idp periodically;

* feat(go): improvements for google idp;

1. Use the impersonate package to authenticate.
2. Use Pages method to get all data.

* chore(go): import style changes from migration branch;

1. Singular file names for table schema.
2. No table name method.
3. Use .Model instead of .Table.
4. No unnecessary tagging.

* remove nat check on egress gateway request

* Revert "remove nat check on egress gateway request"

This reverts commit 0aff12a189.

* feat(go): add db middleware;

* feat(go): restore method;

* feat(go): add user access token schema;

* fix user auth api:

* re initalise oauth and email config

* feat(go): fetch idp creds from server settings;

* feat(go): add filters for users and groups;

* feat(go): skip sync from idp if disabled;

* feat(go): add endpoint to remove idp integration;

* feat(go): import all users if no filters;

* feat(go): assign service-user role on sync;

* feat(go): remove microsoft-go-sdk;

* feat(go): add display name field for user;

* fix(go): set account disabled correctly;

* fix(go): update user if display name changes;

* fix(go): remove auth provider when removing idp integration;

* fix(go): ignore display name if empty;

* feat(go): add idp sync interval setting;

* fix(go): error on invalid auth provider;

* fix(go): no error if no user on group delete;

* fix(go): check superadmin using platform role id;

* feat(go): add display name and account disabled to return user as well;

* feat(go): tidy go mod after merge;

* feat(go): reinitialize auth provider and idp sync hook;

* fix(go): merge error;

* fix(go): merge error;

* feat(go): use id as the external provider id;

* fix(go): comments;

* feat(go): add function to return pending users;

* feat(go): prevent external id erasure;

* fix(go): user and group sync errors;

* chore(go): cleanup;

* fix(go): delete only oauth users;

* feat(go): use uuid group id;

* export ipd id to in rest api

* feat(go): don't use uuid for default groups;

* feat(go): migrate group only if id not uuid;

* chore(go): go mod tidy;

---------

Co-authored-by: abhishek9686 <abhi281342@gmail.com>
Co-authored-by: Abhishek K <abhishek@netmaker.io>
Co-authored-by: the_aceix <aceixsmartx@gmail.com>
2025-05-21 13:48:15 +05:30

1658 lines
53 KiB
Go

package controllers
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/gravitl/netmaker/database"
"github.com/gravitl/netmaker/logger"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/mq"
proAuth "github.com/gravitl/netmaker/pro/auth"
"github.com/gravitl/netmaker/pro/email"
proLogic "github.com/gravitl/netmaker/pro/logic"
"github.com/gravitl/netmaker/servercfg"
"github.com/gravitl/netmaker/utils"
"golang.org/x/exp/slog"
)
func UserHandlers(r *mux.Router) {
r.HandleFunc("/api/oauth/login", proAuth.HandleAuthLogin).Methods(http.MethodGet)
r.HandleFunc("/api/oauth/callback", proAuth.HandleAuthCallback).Methods(http.MethodGet)
r.HandleFunc("/api/oauth/headless", proAuth.HandleHeadlessSSO)
r.HandleFunc("/api/oauth/register/{regKey}", proAuth.RegisterHostSSO).Methods(http.MethodGet)
// User Role Handlers
r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(getRole))).Methods(http.MethodGet)
r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(createRole))).Methods(http.MethodPost)
r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(updateRole))).Methods(http.MethodPut)
r.HandleFunc("/api/v1/users/role", logic.SecurityCheck(true, http.HandlerFunc(deleteRole))).Methods(http.MethodDelete)
// User Group Handlers
r.HandleFunc("/api/v1/users/groups", logic.SecurityCheck(true, http.HandlerFunc(listUserGroups))).Methods(http.MethodGet)
r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(getUserGroup))).Methods(http.MethodGet)
r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(createUserGroup))).Methods(http.MethodPost)
r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(updateUserGroup))).Methods(http.MethodPut)
r.HandleFunc("/api/v1/users/group", logic.SecurityCheck(true, http.HandlerFunc(deleteUserGroup))).Methods(http.MethodDelete)
// User Invite Handlers
r.HandleFunc("/api/v1/users/invite", userInviteVerify).Methods(http.MethodGet)
r.HandleFunc("/api/v1/users/invite-signup", userInviteSignUp).Methods(http.MethodPost)
r.HandleFunc("/api/v1/users/invite", logic.SecurityCheck(true, http.HandlerFunc(inviteUsers))).Methods(http.MethodPost)
r.HandleFunc("/api/v1/users/invites", logic.SecurityCheck(true, http.HandlerFunc(listUserInvites))).Methods(http.MethodGet)
r.HandleFunc("/api/v1/users/invite", logic.SecurityCheck(true, http.HandlerFunc(deleteUserInvite))).Methods(http.MethodDelete)
r.HandleFunc("/api/v1/users/invites", logic.SecurityCheck(true, http.HandlerFunc(deleteAllUserInvites))).Methods(http.MethodDelete)
r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(getPendingUsers))).Methods(http.MethodGet)
r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(deleteAllPendingUsers))).Methods(http.MethodDelete)
r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(deletePendingUser))).Methods(http.MethodDelete)
r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(approvePendingUser))).Methods(http.MethodPost)
r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(attachUserToRemoteAccessGw))).Methods(http.MethodPost)
r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(removeUserFromRemoteAccessGW))).Methods(http.MethodDelete)
r.HandleFunc("/api/users/{username}/remote_access_gw", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserRemoteAccessGwsV1)))).Methods(http.MethodGet)
r.HandleFunc("/api/users/ingress/{ingress_id}", logic.SecurityCheck(true, http.HandlerFunc(ingressGatewayUsers))).Methods(http.MethodGet)
r.HandleFunc("/api/idp/sync", logic.SecurityCheck(true, http.HandlerFunc(syncIDP))).Methods(http.MethodPost)
r.HandleFunc("/api/idp", logic.SecurityCheck(true, http.HandlerFunc(removeIDPIntegration))).Methods(http.MethodDelete)
}
// swagger:route POST /api/v1/users/invite-signup user userInviteSignUp
//
// user signup via invite.
//
// Schemes: https
//
// Responses:
// 200: ReturnSuccessResponse
func userInviteSignUp(w http.ResponseWriter, r *http.Request) {
email := r.URL.Query().Get("email")
code := r.URL.Query().Get("invite_code")
in, err := logic.GetUserInvite(email)
if err != nil {
logger.Log(0, "failed to fetch users: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
if code != in.InviteCode {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid invite code"), "badrequest"))
return
}
// check if user already exists
_, err = logic.GetUser(email)
if err == nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user already exists"), "badrequest"))
return
}
var user models.User
err = json.NewDecoder(r.Body).Decode(&user)
if err != nil {
logger.Log(0, user.UserName, "error decoding request body: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
if user.UserName != email {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username not matching with invite"), "badrequest"))
return
}
if user.Password == "" {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("password cannot be empty"), "badrequest"))
return
}
user.UserGroups = in.UserGroups
user.PlatformRoleID = models.UserRoleID(in.PlatformRoleID)
if user.PlatformRoleID == "" {
user.PlatformRoleID = models.ServiceUser
}
user.NetworkRoles = in.NetworkRoles
err = logic.CreateUser(&user)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
// delete invite
logic.DeleteUserInvite(email)
logic.DeletePendingUser(email)
w.Header().Set("Access-Control-Allow-Origin", "*")
logic.ReturnSuccessResponse(w, r, "created user successfully "+email)
}
// swagger:route GET /api/v1/users/invite user userInviteVerify
//
// verfies user invite.
//
// Schemes: https
//
// Responses:
// 200: ReturnSuccessResponse
func userInviteVerify(w http.ResponseWriter, r *http.Request) {
email := r.URL.Query().Get("email")
code := r.URL.Query().Get("invite_code")
err := logic.ValidateAndApproveUserInvite(email, code)
if err != nil {
logger.Log(0, "failed to fetch users: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.ReturnSuccessResponse(w, r, "invite is valid")
}
// swagger:route POST /api/v1/users/invite user inviteUsers
//
// invite users.
//
// Schemes: https
//
// Security:
// oauth
//
// Responses:
// 200: userBodyResponse
func inviteUsers(w http.ResponseWriter, r *http.Request) {
var inviteReq models.InviteUsersReq
err := json.NewDecoder(r.Body).Decode(&inviteReq)
if err != nil {
slog.Error("error decoding request body", "error",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
callerUserName := r.Header.Get("user")
if r.Header.Get("ismaster") != "yes" {
caller, err := logic.GetUser(callerUserName)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "notfound"))
return
}
if inviteReq.PlatformRoleID == models.SuperAdminRole.String() {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("super admin cannot be invited"), "badrequest"))
return
}
if inviteReq.PlatformRoleID == "" {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("platform role id cannot be empty"), "badrequest"))
return
}
if (inviteReq.PlatformRoleID == models.AdminRole.String() ||
inviteReq.PlatformRoleID == models.SuperAdminRole.String()) && caller.PlatformRoleID != models.SuperAdminRole {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only superadmin can invite admin users"), "forbidden"))
return
}
}
//validate Req
err = proLogic.IsGroupsValid(inviteReq.UserGroups)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
err = proLogic.IsNetworkRolesValid(inviteReq.NetworkRoles)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
// check platform role
_, err = logic.GetRole(models.UserRoleID(inviteReq.PlatformRoleID))
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
for _, inviteeEmail := range inviteReq.UserEmails {
// check if user with email exists, then ignore
if !email.IsValid(inviteeEmail) {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid email "+inviteeEmail), "badrequest"))
return
}
_, err := logic.GetUser(inviteeEmail)
if err == nil {
// user exists already, so ignore
continue
}
invite := models.UserInvite{
Email: inviteeEmail,
PlatformRoleID: inviteReq.PlatformRoleID,
UserGroups: inviteReq.UserGroups,
NetworkRoles: inviteReq.NetworkRoles,
InviteCode: logic.RandomString(8),
}
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",
frontendURL, url.QueryEscape(invite.Email), url.QueryEscape(invite.InviteCode)))
if err != nil {
slog.Error("failed to parse to invite url", "error", err)
return
}
if servercfg.DeployedByOperator() {
u, err = url.Parse(fmt.Sprintf("%s/invite?tenant_id=%s&email=%s&invite_code=%s",
proLogic.GetAccountsUIHost(), url.QueryEscape(servercfg.GetNetmakerTenantID()), url.QueryEscape(invite.Email), url.QueryEscape(invite.InviteCode)))
if err != nil {
slog.Error("failed to parse to invite url", "error", err)
return
}
}
invite.InviteURL = u.String()
err = logic.InsertUserInvite(invite)
if err != nil {
slog.Error("failed to insert invite for user", "email", invite.Email, "error", err)
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: callerUserName,
Name: callerUserName,
Type: models.UserSub,
},
TriggeredBy: callerUserName,
Target: models.Subject{
ID: inviteeEmail,
Name: inviteeEmail,
Type: models.UserInviteSub,
},
Origin: models.Dashboard,
})
// notify user with magic link
go func(invite models.UserInvite) {
// Set E-Mail body. You can set plain text or html with text/html
e := email.UserInvitedMail{
BodyBuilder: &email.EmailBodyBuilderWithH1HeadlineAndImage{},
InviteURL: invite.InviteURL,
PlatformRoleID: invite.PlatformRoleID,
}
n := email.Notification{
RecipientMail: invite.Email,
}
err = email.GetClient().SendEmail(context.Background(), n, e)
if err != nil {
slog.Error("failed to send email invite", "user", invite.Email, "error", err)
}
}(invite)
}
logic.ReturnSuccessResponse(w, r, "triggered user invites")
}
// swagger:route GET /api/v1/users/invites user listUserInvites
//
// lists all pending invited users.
//
// Schemes: https
//
// Security:
// oauth
//
// Responses:
// 200: ReturnSuccessResponseWithJson
func listUserInvites(w http.ResponseWriter, r *http.Request) {
usersInvites, err := logic.ListUserInvites()
if err != nil {
logger.Log(0, "failed to fetch users: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.ReturnSuccessResponseWithJson(w, r, usersInvites, "fetched pending user invites")
}
// swagger:route DELETE /api/v1/users/invite user deleteUserInvite
//
// delete pending invite.
//
// Schemes: https
//
// Security:
// oauth
//
// Responses:
// 200: ReturnSuccessResponse
func deleteUserInvite(w http.ResponseWriter, r *http.Request) {
email := r.URL.Query().Get("invitee_email")
err := logic.DeleteUserInvite(email)
if err != nil {
logger.Log(0, "failed to delete user invite: ", email, err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: email,
Name: email,
Type: models.UserInviteSub,
},
Origin: models.Dashboard,
})
logic.ReturnSuccessResponse(w, r, "deleted user invite")
}
// swagger:route DELETE /api/v1/users/invites user deleteAllUserInvites
//
// deletes all pending invites.
//
// Schemes: https
//
// Security:
// oauth
//
// Responses:
// 200: ReturnSuccessResponse
func deleteAllUserInvites(w http.ResponseWriter, r *http.Request) {
err := database.DeleteAllRecords(database.USER_INVITES_TABLE_NAME)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending user invites "+err.Error()), "internal"))
return
}
logic.ReturnSuccessResponse(w, r, "cleared all pending user invites")
}
// swagger:route GET /api/v1/user/groups user listUserGroups
//
// Get all user groups.
//
// Schemes: https
//
// Security:
// oauth
//
// Responses:
// 200: userBodyResponse
func listUserGroups(w http.ResponseWriter, r *http.Request) {
groups, err := proLogic.ListUserGroups()
if err != nil {
logic.ReturnErrorResponse(w, r, models.ErrorResponse{
Code: http.StatusInternalServerError,
Message: err.Error(),
})
return
}
logic.ReturnSuccessResponseWithJson(w, r, groups, "successfully fetched user groups")
}
// swagger:route GET /api/v1/user/group user getUserGroup
//
// Get user group.
//
// Schemes: https
//
// Security:
// oauth
//
// Responses:
// 200: userBodyResponse
func getUserGroup(w http.ResponseWriter, r *http.Request) {
gid := r.URL.Query().Get("group_id")
if gid == "" {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("group id is required"), "badrequest"))
return
}
group, err := proLogic.GetUserGroup(models.UserGroupID(gid))
if err != nil {
logic.ReturnErrorResponse(w, r, models.ErrorResponse{
Code: http.StatusInternalServerError,
Message: err.Error(),
})
return
}
logic.ReturnSuccessResponseWithJson(w, r, group, "successfully fetched user group")
}
// swagger:route POST /api/v1/user/group user createUserGroup
//
// Create user groups.
//
// Schemes: https
//
// Security:
// oauth
//
// Responses:
// 200: userBodyResponse
func createUserGroup(w http.ResponseWriter, r *http.Request) {
var userGroupReq models.CreateGroupReq
err := json.NewDecoder(r.Body).Decode(&userGroupReq)
if err != nil {
slog.Error("error decoding request body", "error",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
err = proLogic.ValidateCreateGroupReq(userGroupReq.Group)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
err = proLogic.CreateUserGroup(userGroupReq.Group)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
networks, err := logic.GetNetworks()
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
for _, network := range networks {
acl := models.Acl{
ID: uuid.New().String(),
Name: fmt.Sprintf("%s group", userGroupReq.Group.Name),
MetaData: "This Policy allows user group to communicate with all gateways",
Default: false,
ServiceType: models.Any,
NetworkID: models.NetworkID(network.NetID),
Proto: models.ALL,
RuleType: models.UserPolicy,
Src: []models.AclPolicyTag{
{
ID: models.UserGroupAclID,
Value: userGroupReq.Group.ID.String(),
},
},
Dst: []models.AclPolicyTag{
{
ID: models.NodeTagID,
Value: fmt.Sprintf("%s.%s", models.NetworkID(network.NetID), models.GwTagName),
}},
AllowedDirection: models.TrafficDirectionUni,
Enabled: true,
CreatedBy: "auto",
CreatedAt: time.Now().UTC(),
}
err = logic.InsertAcl(acl)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
}
for _, userID := range userGroupReq.Members {
user, err := logic.GetUser(userID)
if err != nil {
continue
}
if len(user.UserGroups) == 0 {
user.UserGroups = make(map[models.UserGroupID]struct{})
}
user.UserGroups[userGroupReq.Group.ID] = struct{}{}
logic.UpsertUser(*user)
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: userGroupReq.Group.ID.String(),
Name: userGroupReq.Group.Name,
Type: models.UserGroupSub,
},
Origin: models.Dashboard,
})
logic.ReturnSuccessResponseWithJson(w, r, userGroupReq.Group, "created user group")
}
// swagger:route PUT /api/v1/user/group user updateUserGroup
//
// Update user group.
//
// Schemes: https
//
// Security:
// oauth
//
// Responses:
// 200: userBodyResponse
func updateUserGroup(w http.ResponseWriter, r *http.Request) {
var userGroup models.UserGroup
err := json.NewDecoder(r.Body).Decode(&userGroup)
if err != nil {
slog.Error("error decoding request body", "error",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
// fetch curr group
currUserG, err := proLogic.GetUserGroup(userGroup.ID)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
if currUserG.Default {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update default user group"), "badrequest"))
return
}
err = proLogic.ValidateUpdateGroupReq(userGroup)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
userGroup.ExternalIdentityProviderID = currUserG.ExternalIdentityProviderID
err = proLogic.UpdateUserGroup(userGroup)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Update,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: userGroup.ID.String(),
Name: userGroup.Name,
Type: models.UserGroupSub,
},
Diff: models.Diff{
Old: currUserG,
New: userGroup,
},
Origin: models.Dashboard,
})
// reset configs for service user
go proLogic.UpdatesUserGwAccessOnGrpUpdates(currUserG.NetworkRoles, userGroup.NetworkRoles)
logic.ReturnSuccessResponseWithJson(w, r, userGroup, "updated user group")
}
// swagger:route DELETE /api/v1/user/group user deleteUserGroup
//
// delete user group.
//
// Schemes: https
//
// Security:
// oauth
//
// Responses:
// 200: userBodyResponse
//
// @Summary Delete user group.
// @Router /api/v1/user/group [delete]
// @Tags Users
// @Param group_id query string true "group id required to delete the role"
// @Success 200 {string} string
// @Failure 500 {object} models.ErrorResponse
func deleteUserGroup(w http.ResponseWriter, r *http.Request) {
gid := r.URL.Query().Get("group_id")
if gid == "" {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("group id is required"), "badrequest"))
return
}
userG, err := proLogic.GetUserGroup(models.UserGroupID(gid))
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to fetch group details"), "badrequest"))
return
}
if userG.Default {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot delete default user group"), "badrequest"))
return
}
err = proLogic.DeleteUserGroup(models.UserGroupID(gid))
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: userG.ID.String(),
Name: userG.Name,
Type: models.UserGroupSub,
},
Origin: models.Dashboard,
})
go proLogic.UpdatesUserGwAccessOnGrpUpdates(userG.NetworkRoles, make(map[models.NetworkID]map[models.UserRoleID]struct{}))
logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user group")
}
// @Summary lists all user roles.
// @Router /api/v1/user/roles [get]
// @Tags Users
// @Param role_id query string true "roleid required to get the role details"
// @Success 200 {object} []models.UserRolePermissionTemplate
// @Failure 500 {object} models.ErrorResponse
func ListRoles(w http.ResponseWriter, r *http.Request) {
platform := r.URL.Query().Get("platform")
var roles []models.UserRolePermissionTemplate
var err error
if platform == "true" {
roles, err = logic.ListPlatformRoles()
} else {
roles, err = proLogic.ListNetworkRoles()
}
if err != nil {
logic.ReturnErrorResponse(w, r, models.ErrorResponse{
Code: http.StatusInternalServerError,
Message: err.Error(),
})
return
}
logic.ReturnSuccessResponseWithJson(w, r, roles, "successfully fetched user roles permission templates")
}
// @Summary Get user role permission template.
// @Router /api/v1/user/role [get]
// @Tags Users
// @Param role_id query string true "roleid required to get the role details"
// @Success 200 {object} models.UserRolePermissionTemplate
// @Failure 500 {object} models.ErrorResponse
func getRole(w http.ResponseWriter, r *http.Request) {
rid := r.URL.Query().Get("role_id")
if rid == "" {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
return
}
role, err := logic.GetRole(models.UserRoleID(rid))
if err != nil {
logic.ReturnErrorResponse(w, r, models.ErrorResponse{
Code: http.StatusInternalServerError,
Message: err.Error(),
})
return
}
logic.ReturnSuccessResponseWithJson(w, r, role, "successfully fetched user role permission templates")
}
// @Summary Create user role permission template.
// @Router /api/v1/user/role [post]
// @Tags Users
// @Param body body models.UserRolePermissionTemplate true "user role template"
// @Success 200 {object} models.UserRolePermissionTemplate
// @Failure 500 {object} models.ErrorResponse
func createRole(w http.ResponseWriter, r *http.Request) {
var userRole models.UserRolePermissionTemplate
err := json.NewDecoder(r.Body).Decode(&userRole)
if err != nil {
slog.Error("error decoding request body", "error",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
err = proLogic.ValidateCreateRoleReq(&userRole)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
userRole.Default = false
userRole.GlobalLevelAccess = make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope)
err = proLogic.CreateRole(userRole)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: userRole.ID.String(),
Name: userRole.Name,
Type: models.UserRoleSub,
},
Origin: models.ClientApp,
})
logic.ReturnSuccessResponseWithJson(w, r, userRole, "created user role")
}
// @Summary Update user role permission template.
// @Router /api/v1/user/role [put]
// @Tags Users
// @Param body body models.UserRolePermissionTemplate true "user role template"
// @Success 200 {object} models.UserRolePermissionTemplate
// @Failure 500 {object} models.ErrorResponse
func updateRole(w http.ResponseWriter, r *http.Request) {
var userRole models.UserRolePermissionTemplate
err := json.NewDecoder(r.Body).Decode(&userRole)
if err != nil {
slog.Error("error decoding request body", "error",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
currRole, err := logic.GetRole(userRole.ID)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
err = proLogic.ValidateUpdateRoleReq(&userRole)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
userRole.GlobalLevelAccess = make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope)
err = proLogic.UpdateRole(userRole)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Update,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: userRole.ID.String(),
Name: userRole.Name,
Type: models.UserRoleSub,
},
Diff: models.Diff{
Old: currRole,
New: userRole,
},
Origin: models.Dashboard,
})
// reset configs for service user
go proLogic.UpdatesUserGwAccessOnRoleUpdates(currRole.NetworkLevelAccess, userRole.NetworkLevelAccess, string(userRole.NetworkID))
logic.ReturnSuccessResponseWithJson(w, r, userRole, "updated user role")
}
// @Summary Delete user role permission template.
// @Router /api/v1/user/role [delete]
// @Tags Users
// @Param role_id query string true "roleid required to delete the role"
// @Success 200 {string} string
// @Failure 500 {object} models.ErrorResponse
func deleteRole(w http.ResponseWriter, r *http.Request) {
rid := r.URL.Query().Get("role_id")
if rid == "" {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
return
}
role, err := logic.GetRole(models.UserRoleID(rid))
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
return
}
err = proLogic.DeleteRole(models.UserRoleID(rid), false)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: role.ID.String(),
Name: role.Name,
Type: models.UserRoleSub,
},
Origin: models.Dashboard,
})
go proLogic.UpdatesUserGwAccessOnRoleUpdates(role.NetworkLevelAccess, make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope), role.NetworkID.String())
logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user role")
}
// @Summary Attach user to a remote access gateway
// @Router /api/users/{username}/remote_access_gw/{remote_access_gateway_id} [post]
// @Tags PRO
// @Accept json
// @Produce json
// @Param username path string true "Username"
// @Param remote_access_gateway_id path string true "Remote Access Gateway ID"
// @Success 200 {object} models.ReturnUser
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
func attachUserToRemoteAccessGw(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
username := params["username"]
remoteGwID := params["remote_access_gateway_id"]
if username == "" || remoteGwID == "" {
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(
errors.New("required params `username` and `remote_access_gateway_id`"),
"badrequest",
),
)
return
}
user, err := logic.GetUser(username)
if err != nil {
slog.Error("failed to fetch user: ", "username", username, "error", err.Error())
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(
fmt.Errorf("failed to fetch user %s, error: %v", username, err),
"badrequest",
),
)
return
}
if user.PlatformRoleID == models.AdminRole || user.PlatformRoleID == models.SuperAdminRole {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("superadmins/admins have access to all gateways"), "badrequest"))
return
}
node, err := logic.GetNodeByID(remoteGwID)
if err != nil {
slog.Error("failed to fetch gateway node", "nodeID", remoteGwID, "error", err)
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(
fmt.Errorf("failed to fetch remote access gateway node, error: %v", err),
"badrequest",
),
)
return
}
if !node.IsIngressGateway {
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(fmt.Errorf("node is not a remote access gateway"), "badrequest"),
)
return
}
if user.RemoteGwIDs == nil {
user.RemoteGwIDs = make(map[string]struct{})
}
user.RemoteGwIDs[node.ID.String()] = struct{}{}
err = logic.UpsertUser(*user)
if err != nil {
slog.Error("failed to update user's gateways", "user", username, "error", err)
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(
fmt.Errorf("failed to fetch remote access gateway node,error: %v", err),
"badrequest",
),
)
return
}
json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
}
// @Summary Remove user from a remote access gateway
// @Router /api/users/{username}/remote_access_gw/{remote_access_gateway_id} [delete]
// @Tags PRO
// @Accept json
// @Produce json
// @Param username path string true "Username"
// @Param remote_access_gateway_id path string true "Remote Access Gateway ID"
// @Success 200 {object} models.ReturnUser
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
func removeUserFromRemoteAccessGW(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
username := params["username"]
remoteGwID := params["remote_access_gateway_id"]
if username == "" || remoteGwID == "" {
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(
errors.New("required params `username` and `remote_access_gateway_id`"),
"badrequest",
),
)
return
}
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
}
delete(user.RemoteGwIDs, remoteGwID)
go func(user models.User, remoteGwID string) {
extclients, err := logic.GetAllExtClients()
if err != nil {
slog.Error("failed to fetch extclients", "error", err)
return
}
for _, extclient := range extclients {
if extclient.OwnerID == user.UserName && remoteGwID == extclient.IngressGatewayID {
err = logic.DeleteExtClientAndCleanup(extclient)
if err != nil {
slog.Error("failed to delete extclient",
"id", extclient.ClientID, "owner", user.UserName, "error", err)
} else {
if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
slog.Error("error setting ext peers: " + err.Error())
}
}
}
}
if servercfg.IsDNSMode() {
logic.SetDNS()
}
}(*user, remoteGwID)
err = logic.UpsertUser(*user)
if err != nil {
slog.Error("failed to update user gateways", "user", username, "error", err)
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(
errors.New("failed to fetch remote access gaetway node "+err.Error()),
"badrequest",
),
)
return
}
json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
}
// @Summary Get Users Remote Access Gw Networks.
// @Router /api/users/{username}/remote_access_gw [get]
// @Tags Users
// @Param username path string true "Username to fetch all the gateways with access"
// @Success 200 {object} map[string][]models.UserRemoteGws
// @Failure 500 {object} models.ErrorResponse
func getUserRemoteAccessNetworks(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
username := r.Header.Get("user")
user, err := logic.GetUser(username)
if err != nil {
logger.Log(0, username, "failed to fetch user: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest"))
return
}
userGws := make(map[string][]models.UserRemoteGws)
networks := []models.Network{}
networkMap := make(map[string]struct{})
userGwNodes := proLogic.GetUserRAGNodes(*user)
for _, node := range userGwNodes {
network, err := logic.GetNetwork(node.Network)
if err != nil {
slog.Error("failed to get node network", "error", err)
continue
}
if _, ok := networkMap[network.NetID]; ok {
continue
}
networkMap[network.NetID] = struct{}{}
networks = append(networks, network)
}
slog.Debug("returned user gws", "user", username, "gws", userGws)
logic.ReturnSuccessResponseWithJson(w, r, networks, "fetched user accessible networks")
}
// @Summary Get Users Remote Access Gw Networks.
// @Router /api/users/{username}/remote_access_gw [get]
// @Tags Users
// @Param username path string true "Username to fetch all the gateways with access"
// @Success 200 {object} map[string][]models.UserRemoteGws
// @Failure 500 {object} models.ErrorResponse
func getUserRemoteAccessNetworkGateways(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
username := r.Header.Get("user")
user, err := logic.GetUser(username)
if err != nil {
logger.Log(0, username, "failed to fetch user: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest"))
return
}
network := params["network"]
if network == "" {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params network"), "badrequest"))
return
}
userGws := []models.UserRAGs{}
userGwNodes := proLogic.GetUserRAGNodes(*user)
for _, node := range userGwNodes {
if node.Network != network {
continue
}
host, err := logic.GetHost(node.HostID.String())
if err != nil {
continue
}
userGws = append(userGws, models.UserRAGs{
GwID: node.ID.String(),
GWName: host.Name,
Network: node.Network,
IsInternetGateway: node.EgressDetails.IsInternetGateway,
Metadata: node.Metadata,
})
}
slog.Debug("returned user gws", "user", username, "gws", userGws)
logic.ReturnSuccessResponseWithJson(w, r, userGws, "fetched user accessible gateways in network "+network)
}
// @Summary Get Users Remote Access Gw Networks.
// @Router /api/users/{username}/remote_access_gw [get]
// @Tags Users
// @Param username path string true "Username to fetch all the gateways with access"
// @Success 200 {object} map[string][]models.UserRemoteGws
// @Failure 500 {object} models.ErrorResponse
func getRemoteAccessGatewayConf(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
username := r.Header.Get("user")
user, err := logic.GetUser(username)
if err != nil {
logger.Log(0, username, "failed to fetch user: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest"))
return
}
remoteGwID := params["access_point_id"]
if remoteGwID == "" {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params access_point_id"), "badrequest"))
return
}
var req models.UserRemoteGwsReq
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
slog.Error("error decoding request body: ", "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
userGwNodes := proLogic.GetUserRAGNodes(*user)
if _, ok := userGwNodes[remoteGwID]; !ok {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("access denied"), "forbidden"))
return
}
node, err := logic.GetNodeByID(remoteGwID)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch gw node %s, error: %v", remoteGwID, err), "badrequest"))
return
}
host, err := logic.GetHost(node.HostID.String())
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch gw host %s, error: %v", remoteGwID, err), "badrequest"))
return
}
network, err := logic.GetNetwork(node.Network)
if err != nil {
slog.Error("failed to get node network", "error", err)
}
var userConf models.ExtClient
allextClients, err := logic.GetAllExtClients()
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
for _, extClient := range allextClients {
if extClient.Network != network.NetID || extClient.IngressGatewayID != node.ID.String() {
continue
}
if extClient.RemoteAccessClientID == req.RemoteAccessClientID && extClient.OwnerID == username {
userConf = extClient
userConf.AllowedIPs = logic.GetExtclientAllowedIPs(extClient)
}
}
if userConf.ClientID == "" {
// create a new conf
userConf.OwnerID = user.UserName
userConf.RemoteAccessClientID = req.RemoteAccessClientID
userConf.IngressGatewayID = node.ID.String()
// set extclient dns to ingressdns if extclient dns is not explicitly set
if (userConf.DNS == "") && (node.IngressDNS != "") {
userConf.DNS = node.IngressDNS
}
userConf.Network = node.Network
host, err := logic.GetHost(node.HostID.String())
if err != nil {
logger.Log(0, r.Header.Get("user"),
fmt.Sprintf("failed to get ingress gateway host for node [%s] info: %v", node.ID, err))
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
listenPort := logic.GetPeerListenPort(host)
if host.EndpointIP.To4() == nil {
userConf.IngressGatewayEndpoint = fmt.Sprintf("[%s]:%d", host.EndpointIPv6.String(), listenPort)
} else {
userConf.IngressGatewayEndpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), listenPort)
}
userConf.Enabled = true
parentNetwork, err := logic.GetNetwork(node.Network)
if err == nil { // check if parent network default ACL is enabled (yes) or not (no)
userConf.Enabled = parentNetwork.DefaultACL == "yes"
}
userConf.Tags = make(map[models.TagID]struct{})
// userConf.Tags[models.TagID(fmt.Sprintf("%s.%s", userConf.Network,
// models.RemoteAccessTagName))] = struct{}{}
if err = logic.CreateExtClient(&userConf); err != nil {
slog.Error(
"failed to create extclient",
"user",
r.Header.Get("user"),
"network",
node.Network,
"error",
err,
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
}
userGw := models.UserRemoteGws{
GwID: node.ID.String(),
GWName: host.Name,
Network: node.Network,
GwClient: userConf,
Connected: true,
IsInternetGateway: node.EgressDetails.IsInternetGateway,
GwPeerPublicKey: host.PublicKey.String(),
GwListenPort: logic.GetPeerListenPort(host),
Metadata: node.Metadata,
AllowedEndpoints: getAllowedRagEndpoints(&node, host),
NetworkAddresses: []string{network.AddressRange, network.AddressRange6},
DnsAddress: node.IngressDNS,
Addresses: utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
}
slog.Debug("returned user gw config", "user", user.UserName, "gws", userGw)
logic.ReturnSuccessResponseWithJson(w, r, userGw, "fetched user config to gw "+remoteGwID)
}
// @Summary Get Users Remote Access Gw.
// @Router /api/users/{username}/remote_access_gw [get]
// @Tags Users
// @Param username path string true "Username to fetch all the gateways with access"
// @Success 200 {object} map[string][]models.UserRemoteGws
// @Failure 500 {object} models.ErrorResponse
func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
username := params["username"]
if username == "" {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params username"), "badrequest"))
return
}
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
}
remoteAccessClientID := r.URL.Query().Get("remote_access_clientid")
var req models.UserRemoteGwsReq
if remoteAccessClientID == "" {
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
slog.Error("error decoding request body: ", "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
}
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"))
return
}
if req.RemoteAccessClientID == "" {
req.RemoteAccessClientID = remoteAccessClientID
}
userGws := make(map[string][]models.UserRemoteGws)
allextClients, err := logic.GetAllExtClients()
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
userGwNodes := proLogic.GetUserRAGNodes(*user)
for _, extClient := range allextClients {
node, ok := userGwNodes[extClient.IngressGatewayID]
if !ok {
continue
}
if extClient.RemoteAccessClientID == req.RemoteAccessClientID && extClient.OwnerID == username {
host, err := logic.GetHost(node.HostID.String())
if err != nil {
continue
}
network, err := logic.GetNetwork(node.Network)
if err != nil {
slog.Error("failed to get node network", "error", err)
continue
}
nodesWithStatus := logic.AddStatusToNodes([]models.Node{node}, false)
if len(nodesWithStatus) > 0 {
node = nodesWithStatus[0]
}
gws := userGws[node.Network]
if extClient.DNS == "" {
extClient.DNS = node.IngressDNS
}
extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient)
gws = append(gws, models.UserRemoteGws{
GwID: node.ID.String(),
GWName: host.Name,
Network: node.Network,
GwClient: extClient,
Connected: true,
IsInternetGateway: node.EgressDetails.IsInternetGateway,
GwPeerPublicKey: host.PublicKey.String(),
GwListenPort: logic.GetPeerListenPort(host),
Metadata: node.Metadata,
AllowedEndpoints: getAllowedRagEndpoints(&node, host),
NetworkAddresses: []string{network.AddressRange, network.AddressRange6},
Status: node.Status,
DnsAddress: node.IngressDNS,
Addresses: utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
})
userGws[node.Network] = gws
delete(userGwNodes, node.ID.String())
}
}
// add remaining gw nodes to resp
for gwID := range userGwNodes {
node, err := logic.GetNodeByID(gwID)
if err != nil {
continue
}
if !node.IsIngressGateway {
continue
}
if node.PendingDelete {
continue
}
host, err := logic.GetHost(node.HostID.String())
if err != nil {
continue
}
nodesWithStatus := logic.AddStatusToNodes([]models.Node{node}, false)
if len(nodesWithStatus) > 0 {
node = nodesWithStatus[0]
}
network, err := logic.GetNetwork(node.Network)
if err != nil {
slog.Error("failed to get node network", "error", err)
}
gws := userGws[node.Network]
gws = append(gws, models.UserRemoteGws{
GwID: node.ID.String(),
GWName: host.Name,
Network: node.Network,
IsInternetGateway: node.EgressDetails.IsInternetGateway,
GwPeerPublicKey: host.PublicKey.String(),
GwListenPort: logic.GetPeerListenPort(host),
Metadata: node.Metadata,
AllowedEndpoints: getAllowedRagEndpoints(&node, host),
NetworkAddresses: []string{network.AddressRange, network.AddressRange6},
Status: node.Status,
DnsAddress: node.IngressDNS,
Addresses: utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
})
userGws[node.Network] = gws
}
if reqFromMobile {
// send resp in array format
userGwsArr := []models.UserRemoteGws{}
for _, userGwI := range userGws {
userGwsArr = append(userGwsArr, userGwI...)
}
logic.ReturnSuccessResponseWithJson(w, r, userGwsArr, "fetched gateways for user"+username)
return
}
slog.Debug("returned user gws", "user", username, "gws", userGws)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(userGws)
}
// @Summary List users attached to an remote access gateway
// @Router /api/nodes/{network}/{nodeid}/ingress/users [get]
// @Tags PRO
// @Accept json
// @Produce json
// @Param ingress_id path string true "Ingress Gateway ID"
// @Success 200 {array} models.IngressGwUsers
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
func ingressGatewayUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
ingressID := params["ingress_id"]
node, err := logic.GetNodeByID(ingressID)
if err != nil {
slog.Error("failed to get ingress node", "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
gwUsers, err := logic.GetIngressGwUsers(node)
if err != nil {
slog.Error(
"failed to get users on ingress gateway",
"nodeid",
ingressID,
"network",
node.Network,
"user",
r.Header.Get("user"),
"error",
err,
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(gwUsers)
}
func getAllowedRagEndpoints(ragNode *models.Node, ragHost *models.Host) []string {
endpoints := []string{}
if len(ragHost.EndpointIP) > 0 {
endpoints = append(endpoints, ragHost.EndpointIP.String())
}
if len(ragHost.EndpointIPv6) > 0 {
endpoints = append(endpoints, ragHost.EndpointIPv6.String())
}
if servercfg.IsPro {
for _, ip := range ragNode.AdditionalRagIps {
endpoints = append(endpoints, ip.String())
}
}
return endpoints
}
// @Summary Get all pending users
// @Router /api/users_pending [get]
// @Tags Users
// @Success 200 {array} models.User
// @Failure 500 {object} models.ErrorResponse
func getPendingUsers(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
users, err := logic.ListPendingReturnUsers()
if err != nil {
logger.Log(0, "failed to fetch users: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.SortUsers(users[:])
logger.Log(2, r.Header.Get("user"), "fetched pending users")
json.NewEncoder(w).Encode(users)
}
// @Summary Approve a pending user
// @Router /api/users_pending/user/{username} [post]
// @Tags Users
// @Param username path string true "Username of the pending user to approve"
// @Success 200 {string} string
// @Failure 500 {object} models.ErrorResponse
func approvePendingUser(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
username := params["username"]
users, err := logic.ListPendingUsers()
if err != nil {
logger.Log(0, "failed to fetch users: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
for _, user := range users {
if user.UserName == username {
var newPass, fetchErr = logic.FetchPassValue("")
if fetchErr != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(fetchErr, "internal"))
return
}
if err = logic.CreateUser(&models.User{
UserName: user.UserName,
ExternalIdentityProviderID: user.ExternalIdentityProviderID,
Password: newPass,
AuthType: user.AuthType,
PlatformRoleID: models.ServiceUser,
}); err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to create user: %s", err), "internal"))
return
}
err = logic.DeletePendingUser(username)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete pending user: %s", err), "internal"))
return
}
break
}
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: username,
Name: username,
Type: models.PendingUserSub,
},
Origin: models.Dashboard,
})
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.ListPendingReturnUsers()
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.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: username,
Name: username,
Type: models.PendingUserSub,
},
Origin: models.Dashboard,
})
logic.ReturnSuccessResponse(w, r, "deleted pending "+username)
}
// @Summary Delete all pending users
// @Router /api/users_pending [delete]
// @Tags Users
// @Success 200 {string} string
// @Failure 500 {object} models.ErrorResponse
func deleteAllPendingUsers(w http.ResponseWriter, r *http.Request) {
// set header.
err := database.DeleteAllRecords(database.PENDING_USERS_TABLE_NAME)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending users "+err.Error()), "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.DeleteAll,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.PendingUserSub,
},
Origin: models.Dashboard,
})
logic.ReturnSuccessResponse(w, r, "cleared all pending users")
}
// @Summary Sync users and groups from idp.
// @Router /api/idp/sync [post]
// @Tags IDP
// @Success 200 {object} models.SuccessResponse
func syncIDP(w http.ResponseWriter, r *http.Request) {
go func() {
err := proAuth.SyncFromIDP()
if err != nil {
logger.Log(0, "failed to sync from idp: ", err.Error())
} else {
logger.Log(0, "sync from idp complete")
}
}()
logic.ReturnSuccessResponse(w, r, "starting sync from idp")
}
// @Summary Remove idp integration.
// @Router /api/idp [delete]
// @Tags IDP
// @Success 200 {object} models.SuccessResponse
// @Failure 500 {object} models.ErrorResponse
func removeIDPIntegration(w http.ResponseWriter, r *http.Request) {
superAdmin, err := logic.GetSuperAdmin()
if err != nil {
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(fmt.Errorf("failed to get superadmin: %v", err), "internal"),
)
return
}
if superAdmin.AuthType == models.OAuth {
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(fmt.Errorf("cannot remove idp integration with superadmin oauth user"), "badrequest"),
)
return
}
settings := logic.GetServerSettings()
settings.AuthProvider = ""
settings.OIDCIssuer = ""
settings.ClientID = ""
settings.ClientSecret = ""
settings.SyncEnabled = false
settings.GoogleAdminEmail = ""
settings.GoogleSACredsJson = ""
settings.AzureTenant = ""
settings.UserFilters = nil
settings.GroupFilters = nil
err = logic.UpsertServerSettings(settings)
if err != nil {
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(fmt.Errorf("failed to remove idp integration: %v", err), "internal"),
)
return
}
proAuth.ResetAuthProvider()
proAuth.ResetIDPSyncHook()
go func() {
err := proAuth.SyncFromIDP()
if err != nil {
logger.Log(0, "failed to sync from idp: ", err.Error())
} else {
logger.Log(0, "sync from idp complete")
}
}()
logic.ReturnSuccessResponse(w, r, "removed idp integration successfully")
}