mirror of
https://github.com/gravitl/netmaker.git
synced 2026-10-12 02:15:06 +00:00
2387 lines
81 KiB
Go
2387 lines
81 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"image/png"
|
|
"net/http"
|
|
"reflect"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/mux"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/gravitl/netmaker/auth"
|
|
"github.com/gravitl/netmaker/db"
|
|
dbtypes "github.com/gravitl/netmaker/db/types"
|
|
"github.com/gravitl/netmaker/logger"
|
|
"github.com/gravitl/netmaker/logic"
|
|
"github.com/gravitl/netmaker/middleware"
|
|
"github.com/gravitl/netmaker/models"
|
|
"github.com/gravitl/netmaker/mq"
|
|
"github.com/gravitl/netmaker/orchestrator"
|
|
"github.com/gravitl/netmaker/schema"
|
|
"github.com/gravitl/netmaker/scope"
|
|
"github.com/gravitl/netmaker/servercfg"
|
|
"github.com/pquerna/otp"
|
|
"github.com/pquerna/otp/totp"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"golang.org/x/exp/slog"
|
|
)
|
|
|
|
var (
|
|
upgrader = websocket.Upgrader{}
|
|
)
|
|
|
|
var ListRoles = listRoles
|
|
|
|
func userHandlers(r *mux.Router) {
|
|
r.HandleFunc("/api/v1/auth/discover", discoverLoginMethods).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/v1/auth/methods", middleware.InferScope(http.HandlerFunc(getAuthMethods))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/users/adm/hassuperadmin", middleware.Scope(scope.TenantScope, http.HandlerFunc(hasSuperAdmin))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/users/adm/createsuperadmin", middleware.Scope(scope.TenantScope, http.HandlerFunc(createSuperAdmin))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/users/adm/transfersuperadmin/{username}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(transferSuperAdmin)))).
|
|
Methods(http.MethodPost)
|
|
r.HandleFunc("/api/users/adm/authenticate", middleware.InferScope(http.HandlerFunc(authenticateUser))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/users/{username}/validate-identity", middleware.InferScope(logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(validateUserIdentity))))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/users/{username}/auth/init-totp", middleware.InferScope(logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(initiateTOTPSetup))))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/users/{username}/auth/complete-totp", middleware.InferScope(logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(completeTOTPSetup))))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/users/{username}/auth/verify-totp", middleware.InferScope(logic.PreAuthCheck(logic.ContinueIfUserMatch(http.HandlerFunc(verifyTOTP))))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/users/{username}", middleware.InferScope(logic.SecurityCheck(true, http.HandlerFunc(updateUser)))).Methods(http.MethodPut)
|
|
r.HandleFunc("/api/users/{username}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(createUser)))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/users/{username}", middleware.InferScope(logic.SecurityCheck(true, http.HandlerFunc(deleteUser)))).Methods(http.MethodDelete)
|
|
r.HandleFunc("/api/users/{username}", middleware.Scope(scope.TenantScope, logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser))))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/users/{username}/enable", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(enableUserAccount)))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/users/{username}/disable", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(disableUserAccount)))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/users/{username}/settings", middleware.InferScope(logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserSettings))))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/users/{username}/settings", middleware.InferScope(logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(updateUserSettings))))).Methods(http.MethodPut)
|
|
r.HandleFunc("/api/v1/users", middleware.InferScope(logic.SecurityCheck(false, logic.ContinueIfUserMatchOrAdmin(http.HandlerFunc(getUserV1))))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/users", middleware.InferScope(logic.SecurityCheck(true, http.HandlerFunc(getUsers)))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/v2/users", middleware.InferScope(logic.SecurityCheck(true, http.HandlerFunc(listUsers)))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/v1/users/bulk", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(bulkDeleteUsers)))).Methods(http.MethodDelete)
|
|
r.HandleFunc("/api/v1/users/bulk/status", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(bulkUpdateUserStatus)))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/v1/users/roles", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(ListRoles)))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/v1/users/access_token", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(createUserAccessToken)))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/v1/users/access_token", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(getUserAccessTokens)))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/v1/users/access_token", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(deleteUserAccessTokens)))).Methods(http.MethodDelete)
|
|
r.HandleFunc("/api/v1/users/logout", middleware.Scope(scope.TenantScope, logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(logout))))).Methods(http.MethodPost)
|
|
}
|
|
|
|
// @Summary Create a user API access token
|
|
// @Router /api/v1/users/access_token [post]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body schema.UserAccessToken true "Access token request"
|
|
// @Success 200 {object} models.SuccessfulUserLoginResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// Auth request consists of Mac Address and Password (from node that is authorizing
|
|
// in case of Master, auth is ignored and mac is set to "mastermac"
|
|
var req schema.UserAccessToken
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
logger.Log(0, "error decoding request body: ",
|
|
err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
if req.Name == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("name is required"), logic.BadReq))
|
|
return
|
|
}
|
|
if req.UserName == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), logic.BadReq))
|
|
return
|
|
}
|
|
caller := &schema.User{Username: r.Header.Get("user")}
|
|
err = caller.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
|
|
return
|
|
}
|
|
user := &schema.User{Username: req.UserName}
|
|
err = user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
|
|
return
|
|
}
|
|
if caller.Username != user.Username && caller.PlatformRoleID != schema.SuperAdminRole {
|
|
if caller.PlatformRoleID == schema.AdminRole {
|
|
if user.PlatformRoleID == schema.SuperAdminRole || user.PlatformRoleID == schema.AdminRole {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to create token for user "+user.Username), logic.Forbidden_Msg))
|
|
return
|
|
}
|
|
} else {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to create token for user "+user.Username), logic.Forbidden_Msg))
|
|
return
|
|
}
|
|
}
|
|
|
|
req.ID = uuid.New().String()
|
|
req.TenantID = scope.ID(r.Context())
|
|
req.CreatedBy = r.Header.Get("user")
|
|
req.CreatedAt = time.Now()
|
|
jwt, err := logic.CreateUserAccessJwtToken(r.Context(), user.Username, req.ExpiresAt, req.ID)
|
|
if jwt == "" {
|
|
// very unlikely that err is !nil and no jwt returned, but handle it anyways.
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(errors.New("error creating access token "+err.Error()), logic.Internal),
|
|
)
|
|
return
|
|
}
|
|
req.TenantID = scope.ID(r.Context())
|
|
err = req.Create(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(errors.New("error creating access token "+err.Error()), logic.Internal),
|
|
)
|
|
return
|
|
}
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.Create,
|
|
Source: models.Subject{
|
|
ID: caller.Username,
|
|
Name: caller.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: caller.Username,
|
|
Target: models.Subject{
|
|
ID: req.ID,
|
|
Name: req.Name,
|
|
Type: schema.UserAccessTokenSub,
|
|
Info: req,
|
|
},
|
|
Origin: schema.Dashboard,
|
|
})
|
|
logic.ReturnSuccessResponseWithJson(w, r, models.SuccessfulUserLoginResponse{
|
|
AuthToken: jwt,
|
|
UserName: req.UserName,
|
|
}, "api access token has generated for user "+req.UserName)
|
|
}
|
|
|
|
// @Summary Get user access tokens
|
|
// @Router /api/v1/users/access_token [get]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param username query string true "Username"
|
|
// @Success 200 {array} schema.UserAccessToken
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func getUserAccessTokens(w http.ResponseWriter, r *http.Request) {
|
|
username := r.URL.Query().Get("username")
|
|
if username == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), "badrequest"))
|
|
return
|
|
}
|
|
logic.ReturnSuccessResponseWithJson(w, r, (&schema.UserAccessToken{UserName: username}).ListByUser(r.Context()), "fetched api access tokens for user "+username)
|
|
}
|
|
|
|
// @Summary Delete user access tokens
|
|
// @Router /api/v1/users/access_token [delete]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param id query string true "Token ID"
|
|
// @Success 200 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
|
|
id := r.URL.Query().Get("id")
|
|
if id == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("id is required"), "badrequest"))
|
|
return
|
|
}
|
|
a := schema.UserAccessToken{
|
|
ID: id,
|
|
}
|
|
err := a.Get(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("token does not exist"), "badrequest"))
|
|
return
|
|
}
|
|
caller := &schema.User{Username: r.Header.Get("user")}
|
|
err = caller.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
|
|
return
|
|
}
|
|
user := &schema.User{Username: a.UserName}
|
|
err = user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
|
|
return
|
|
}
|
|
if caller.Username != user.Username && caller.PlatformRoleID != schema.SuperAdminRole {
|
|
if caller.PlatformRoleID == schema.AdminRole {
|
|
if user.PlatformRoleID == schema.SuperAdminRole || user.PlatformRoleID == schema.AdminRole {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to delete token of user "+user.Username), logic.Forbidden_Msg))
|
|
return
|
|
}
|
|
} else {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to delete token of user "+user.Username), logic.Forbidden_Msg))
|
|
return
|
|
}
|
|
}
|
|
|
|
err = (&schema.UserAccessToken{ID: id}).Delete(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(errors.New("error deleting access token "+err.Error()), "internal"),
|
|
)
|
|
return
|
|
}
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.Delete,
|
|
Source: models.Subject{
|
|
ID: caller.Username,
|
|
Name: caller.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: caller.Username,
|
|
Target: models.Subject{
|
|
ID: a.ID,
|
|
Name: a.Name,
|
|
Type: schema.UserAccessTokenSub,
|
|
Info: a,
|
|
},
|
|
Origin: schema.Dashboard,
|
|
Diff: models.Diff{
|
|
Old: a,
|
|
New: nil,
|
|
},
|
|
})
|
|
logic.ReturnSuccessResponseWithJson(w, r, nil, "revoked access token")
|
|
}
|
|
|
|
// @Summary Authenticate a user to retrieve an authorization token
|
|
// @Router /api/users/adm/authenticate [post]
|
|
// @Tags Auth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body models.UserAuthParams true "Authentication parameters"
|
|
// @Success 200 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func authenticateUser(response http.ResponseWriter, request *http.Request) {
|
|
appName := request.Header.Get("X-Application-Name")
|
|
if appName == "" {
|
|
appName = logic.NetmakerDesktopApp
|
|
}
|
|
|
|
// Auth request consists of Mac Address and Password (from node that is authorizing
|
|
// in case of Master, auth is ignored and mac is set to "mastermac"
|
|
var authRequest models.UserAuthParams
|
|
var errorResponse = models.ErrorResponse{
|
|
Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
|
|
}
|
|
decoder := json.NewDecoder(request.Body)
|
|
decoderErr := decoder.Decode(&authRequest)
|
|
defer request.Body.Close()
|
|
if decoderErr != nil {
|
|
logger.Log(0, "error decoding request body: ",
|
|
decoderErr.Error())
|
|
logic.ReturnErrorResponse(response, request, errorResponse)
|
|
return
|
|
}
|
|
|
|
user := &schema.User{Username: authRequest.UserName}
|
|
err := user.Get(request.Context())
|
|
if err != nil {
|
|
logger.Log(0, authRequest.UserName, "user validation failed: ",
|
|
err.Error())
|
|
logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
|
|
return
|
|
}
|
|
|
|
if user.PlatformRoleID == "" {
|
|
logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("user is not a member of this tenant"), "unauthorized"))
|
|
return
|
|
}
|
|
|
|
err = logic.ResolveInheritedAuth(request.Context(), user)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("incorrect credentials"), "unauthorized"))
|
|
return
|
|
}
|
|
|
|
if logic.IsOauthUser(request.Context(), user) == nil {
|
|
logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("user is registered via SSO"), "badrequest"))
|
|
return
|
|
}
|
|
|
|
if user.AccountDisabled {
|
|
err = errors.New("user account disabled")
|
|
logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
|
|
return
|
|
}
|
|
|
|
if scope.Level(request.Context()) == scope.TenantScope {
|
|
if user.PlatformRoleID != schema.SuperAdminRole && !logic.IsBasicAuthEnabled(request.Context()) {
|
|
logic.ReturnErrorResponse(
|
|
response,
|
|
request,
|
|
logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"),
|
|
)
|
|
return
|
|
}
|
|
}
|
|
|
|
if val := request.Header.Get("From-Ui"); val == "true" {
|
|
// request came from UI, if normal user block Login
|
|
role := &schema.UserRole{ID: user.PlatformRoleID}
|
|
err = role.GetPlatformRole(request.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
|
|
return
|
|
}
|
|
if role.DenyDashboardAccess {
|
|
logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
|
|
return
|
|
}
|
|
}
|
|
|
|
username := authRequest.UserName
|
|
jwt, err := logic.VerifyAuthRequest(request.Context(), authRequest, appName)
|
|
if err != nil {
|
|
logger.Log(0, username, "user validation failed: ",
|
|
err.Error())
|
|
logic.ReturnErrorResponse(response, request, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
if jwt == "" {
|
|
// very unlikely that err is !nil and no jwt returned, but handle it anyways.
|
|
logger.Log(0, username, "jwt token is empty")
|
|
logic.ReturnErrorResponse(
|
|
response,
|
|
request,
|
|
logic.FormatError(errors.New("no token returned"), "internal"),
|
|
)
|
|
return
|
|
}
|
|
|
|
var successResponse models.SuccessResponse
|
|
|
|
if user.IsMFAEnabled {
|
|
successResponse = models.SuccessResponse{
|
|
Code: http.StatusOK,
|
|
Message: "W1R3: TOTP required",
|
|
Response: models.PartialUserLoginResponse{
|
|
UserName: username,
|
|
PreAuthToken: jwt,
|
|
},
|
|
}
|
|
} else {
|
|
successResponse = models.SuccessResponse{
|
|
Code: http.StatusOK,
|
|
Message: "W1R3: Device " + username + " Authorized",
|
|
Response: models.SuccessfulUserLoginResponse{
|
|
UserName: username,
|
|
AuthToken: jwt,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Send back the JWT
|
|
successJSONResponse, jsonError := json.Marshal(successResponse)
|
|
if jsonError != nil {
|
|
logger.Log(0, username,
|
|
"error marshalling resp: ", jsonError.Error())
|
|
logic.ReturnErrorResponse(response, request, errorResponse)
|
|
return
|
|
}
|
|
logger.Log(2, username, "was authenticated")
|
|
|
|
// log user activity
|
|
if !user.IsMFAEnabled {
|
|
if val := request.Header.Get("From-Ui"); val == "true" {
|
|
logic.LogEvent(request.Context(), &models.Event{
|
|
Action: schema.Login,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: schema.DashboardSub.String(),
|
|
Name: schema.DashboardSub.String(),
|
|
Type: schema.DashboardSub,
|
|
},
|
|
Origin: schema.Dashboard,
|
|
})
|
|
} else {
|
|
logic.LogEvent(request.Context(), &models.Event{
|
|
Action: schema.Login,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: schema.ClientAppSub.String(),
|
|
Name: schema.ClientAppSub.String(),
|
|
Type: schema.ClientAppSub,
|
|
},
|
|
Origin: schema.ClientApp,
|
|
})
|
|
}
|
|
}
|
|
|
|
response.Header().Set("Content-Type", "application/json")
|
|
response.Write(successJSONResponse)
|
|
|
|
}
|
|
|
|
// @Summary Discover available login methods for a user email
|
|
// @Router /api/v1/auth/discover [post]
|
|
// @Tags Auth
|
|
// @Produce json
|
|
// @Success 200 {object} models.LoginMethodsResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func discoverLoginMethods(w http.ResponseWriter, r *http.Request) {
|
|
var req models.LoginMethodRequest
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to parse request: %v", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
|
|
options, err := logic.GetLoginMethodsForUser(r.Context(), req.Username)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
|
return
|
|
}
|
|
logic.ReturnSuccessResponseWithJson(w, r, models.LoginMethodsResponse{Options: options}, "login methods retrieved")
|
|
}
|
|
|
|
// @Summary Get the login methods available for a tenant or organization
|
|
// @Router /api/v1/auth/methods [get]
|
|
// @Tags Auth
|
|
// @Produce json
|
|
// @Param X-Tenant-ID header string false "Tenant ID -- returns the tenant's login methods"
|
|
// @Param X-Organization-ID header string false "Organization ID -- returns the organization's login methods"
|
|
// @Success 200 {object} models.LoginMethodsAvailable
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func getAuthMethods(w http.ResponseWriter, r *http.Request) {
|
|
methods, err := logic.GetScopeLoginMethods(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
|
return
|
|
}
|
|
logic.ReturnSuccessResponseWithJson(w, r, methods, "fetched auth methods")
|
|
}
|
|
|
|
// @Summary Validate a user's identity
|
|
// @Router /api/users/{username}/validate-identity [post]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param username path string true "Username"
|
|
// @Param body body models.UserIdentityValidationRequest true "User Identity Validation Request"
|
|
// @Success 200 {object} models.UserIdentityValidationResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
func validateUserIdentity(w http.ResponseWriter, r *http.Request) {
|
|
username := r.Header.Get("user")
|
|
|
|
var req models.UserIdentityValidationRequest
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
logger.Log(0, "failed to decode request body: ", err.Error())
|
|
err = fmt.Errorf("invalid request body: %v", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
user := &schema.User{Username: username}
|
|
err = user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, "failed to get user: ", err.Error())
|
|
err = fmt.Errorf("user not found: %v", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
err = logic.ResolveInheritedAuth(r.Context(), user)
|
|
if err != nil {
|
|
logger.Log(0, "failed to resolve inherited auth: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
var resp models.UserIdentityValidationResponse
|
|
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
|
|
if err != nil {
|
|
logic.ReturnSuccessResponseWithJson(w, r, resp, "user identity validation failed")
|
|
} else {
|
|
resp.IdentityValidated = true
|
|
logic.ReturnSuccessResponseWithJson(w, r, resp, "user identity validated")
|
|
}
|
|
}
|
|
|
|
// @Summary Initiate setting up TOTP 2FA for a user
|
|
// @Router /api/users/{username}/auth/init-totp [post]
|
|
// @Tags Auth
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param username path string true "Username"
|
|
// @Success 200 {object} models.TOTPInitiateResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func initiateTOTPSetup(w http.ResponseWriter, r *http.Request) {
|
|
username := r.Header.Get("user")
|
|
|
|
user := &schema.User{Username: username}
|
|
err := user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, "failed to get user: ", err.Error())
|
|
err = fmt.Errorf("user not found: %v", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
err = logic.ResolveInheritedAuth(r.Context(), user)
|
|
if err != nil {
|
|
logger.Log(0, "failed to resolve inherited auth: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
if user.AuthType == schema.OAuth {
|
|
err = fmt.Errorf("auth type is %s, cannot process totp setup", user.AuthType)
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
key, err := totp.Generate(totp.GenerateOpts{
|
|
Issuer: "Netmaker",
|
|
AccountName: username,
|
|
})
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to generate totp key: %v", err)
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
qrCodeImg, err := key.Image(200, 200)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to generate totp key: %v", err)
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
var qrCodePng bytes.Buffer
|
|
err = png.Encode(&qrCodePng, qrCodeImg)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to generate totp key: %v", err)
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
qrCode := "data:image/png;base64," + base64.StdEncoding.EncodeToString(qrCodePng.Bytes())
|
|
|
|
logic.ReturnSuccessResponseWithJson(w, r, models.TOTPInitiateResponse{
|
|
OTPAuthURL: key.URL(),
|
|
OTPAuthURLSignature: logic.GenerateOTPAuthURLSignature(key.URL()),
|
|
QRCode: qrCode,
|
|
}, "totp setup initiated")
|
|
}
|
|
|
|
// @Summary Verify and complete setting up TOTP 2FA for a user
|
|
// @Router /api/users/{username}/auth/complete-totp [post]
|
|
// @Tags Auth
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param username path string true "Username"
|
|
// @Param body body models.UserTOTPVerificationParams true "TOTP verification parameters"
|
|
// @Success 200 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func completeTOTPSetup(w http.ResponseWriter, r *http.Request) {
|
|
username := r.Header.Get("user")
|
|
|
|
var req models.UserTOTPVerificationParams
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
logger.Log(0, "failed to decode request body: ", err.Error())
|
|
err = fmt.Errorf("invalid request body: %v", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
if !logic.VerifyOTPAuthURL(req.OTPAuthURL, req.OTPAuthURLSignature) {
|
|
err = fmt.Errorf("otp auth url signature mismatch")
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
user := &schema.User{Username: username}
|
|
err = user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, "failed to get user: ", err.Error())
|
|
err = fmt.Errorf("user not found: %v", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
err = logic.ResolveInheritedAuth(r.Context(), user)
|
|
if err != nil {
|
|
logger.Log(0, "failed to resolve inherited auth: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
if user.AuthType == schema.OAuth {
|
|
err = fmt.Errorf("auth type is %s, cannot process totp setup", user.AuthType)
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
otpAuthURL, err := otp.NewKeyFromURL(req.OTPAuthURL)
|
|
if err != nil {
|
|
err = fmt.Errorf("error parsing otp auth url: %v", err)
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
totpSecret := otpAuthURL.Secret()
|
|
|
|
if totp.Validate(req.TOTP, totpSecret) {
|
|
user.IsMFAEnabled = true
|
|
user.TOTPSecret = totpSecret
|
|
err = user.UpdateMFA(r.Context())
|
|
if err != nil {
|
|
err = fmt.Errorf("error upserting user: %v", err)
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.EnableMFA,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
Origin: schema.Dashboard,
|
|
})
|
|
|
|
logic.ReturnSuccessResponse(w, r, fmt.Sprintf("totp setup complete for user %s", username))
|
|
} else {
|
|
err = fmt.Errorf("cannot setup totp for user %s: invalid otp", username)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
}
|
|
}
|
|
|
|
// @Summary Verify a user's TOTP token
|
|
// @Router /api/users/{username}/auth/verify-totp [post]
|
|
// @Tags Auth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param username path string true "Username"
|
|
// @Param body body models.UserTOTPVerificationParams true "TOTP verification parameters"
|
|
// @Success 200 {object} models.SuccessfulUserLoginResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func verifyTOTP(w http.ResponseWriter, r *http.Request) {
|
|
username := r.Header.Get("user")
|
|
|
|
appName := r.Header.Get("X-Application-Name")
|
|
if appName == "" {
|
|
appName = logic.NetmakerDesktopApp
|
|
}
|
|
|
|
var req models.UserTOTPVerificationParams
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
logger.Log(0, "failed to decode request body: ", err.Error())
|
|
err = fmt.Errorf("invalid request body: %v", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
user := &schema.User{Username: username}
|
|
err = user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, "failed to get user: ", err.Error())
|
|
err = fmt.Errorf("user not found: %v", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
err = logic.ResolveInheritedAuth(r.Context(), user)
|
|
if err != nil {
|
|
logger.Log(0, "failed to resolve inherited auth: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
if !user.IsMFAEnabled {
|
|
err = fmt.Errorf("mfa is disabled for user(%s), cannot process totp verification", username)
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
if totp.Validate(req.TOTP, user.TOTPSecret) {
|
|
jwt, err := logic.CreateUserJWT(r.Context(), user.Username, appName)
|
|
if err != nil {
|
|
err = fmt.Errorf("error creating token: %v", err)
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
// update last login time
|
|
user.LastLoginAt = time.Now().UTC()
|
|
err = logic.UpsertUser(*user)
|
|
if err != nil {
|
|
err = fmt.Errorf("error upserting user: %v", err)
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.Login,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: schema.DashboardSub.String(),
|
|
Name: schema.DashboardSub.String(),
|
|
Type: schema.DashboardSub,
|
|
},
|
|
Origin: schema.Dashboard,
|
|
})
|
|
|
|
logic.ReturnSuccessResponseWithJson(w, r, models.SuccessfulUserLoginResponse{
|
|
UserName: username,
|
|
AuthToken: jwt,
|
|
}, "W1R3: User "+username+" Authorized")
|
|
} else {
|
|
err = fmt.Errorf("invalid otp")
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
|
|
}
|
|
}
|
|
|
|
// @Summary Check if the server has a super admin
|
|
// @Router /api/users/adm/hassuperadmin [get]
|
|
// @Tags Users
|
|
// @Produce json
|
|
// @Success 200 {object} bool
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func hasSuperAdmin(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
exists, err := (&schema.User{}).SuperAdminExists(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, "failed to check for superadmin: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
// todo: this is not a json response.
|
|
_ = json.NewEncoder(w).Encode(exists)
|
|
}
|
|
|
|
// @Summary Get an individual user
|
|
// @Router /api/users/{username} [get]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param username path string true "Username of the user to fetch"
|
|
// @Success 200 {object} models.ReturnUser
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func getUser(w http.ResponseWriter, r *http.Request) {
|
|
// set header.
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
var params = mux.Vars(r)
|
|
username := params["username"]
|
|
_user := &schema.User{
|
|
Username: username,
|
|
}
|
|
err := _user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, username, "failed to fetch user: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
logger.Log(2, r.Header.Get("user"), "fetched user", username)
|
|
json.NewEncoder(w).Encode(logic.ToReturnUser(_user))
|
|
}
|
|
|
|
// @Summary Enable a user's account
|
|
// @Router /api/users/{username}/enable [post]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param username path string true "Username of the user to enable"
|
|
// @Param force_enable_configs query string false "Force enable configs"
|
|
// @Success 200 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func enableUserAccount(w http.ResponseWriter, r *http.Request) {
|
|
force := r.URL.Query().Get("force_enable_configs") == "true"
|
|
updateUserAccountStatus(w, r, false, force)
|
|
}
|
|
|
|
// @Summary Disable a user's account
|
|
// @Router /api/users/{username}/disable [post]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param username path string true "Username of the user to disable"
|
|
// @Param force_disable_configs query string false "Force disable configs"
|
|
// @Success 200 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func disableUserAccount(w http.ResponseWriter, r *http.Request) {
|
|
force := r.URL.Query().Get("force_disable_configs") == "true"
|
|
updateUserAccountStatus(w, r, true, force)
|
|
}
|
|
|
|
func updateUserAccountStatus(w http.ResponseWriter, r *http.Request, disableAccount, force bool) {
|
|
action := "enable"
|
|
if disableAccount {
|
|
action = "disable"
|
|
}
|
|
|
|
username := mux.Vars(r)["username"]
|
|
_user := &schema.User{Username: username}
|
|
err := _user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, "failed to fetch user: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
var _caller *schema.User
|
|
var isMaster bool
|
|
if r.Header.Get("user") == logic.MasterUser {
|
|
isMaster = true
|
|
} else {
|
|
_caller = &schema.User{Username: r.Header.Get("user")}
|
|
err = _caller.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
}
|
|
|
|
if !isMaster && _caller.Username == _user.Username {
|
|
// This implies that a user is trying to enable/disable themselves.
|
|
// An enabled user shouldn't be able to do that.
|
|
// And, a disabled user cannot be authenticated, so this will never happen.
|
|
err := fmt.Errorf("cannot %s self", action)
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
|
|
if !isMaster {
|
|
switch _user.PlatformRoleID {
|
|
case schema.SuperAdminRole:
|
|
if disableAccount {
|
|
err = errors.New("cannot disable a super-admin")
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
case schema.AdminRole:
|
|
if _caller.PlatformRoleID != schema.SuperAdminRole {
|
|
err = fmt.Errorf("%s cannot %s an admin", _caller.PlatformRoleID, action)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
case schema.PlatformUser:
|
|
if _caller.PlatformRoleID != schema.SuperAdminRole && _caller.PlatformRoleID != schema.AdminRole {
|
|
err = fmt.Errorf("%s cannot %s a platform-user", _caller.PlatformRoleID, action)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
case schema.ServiceUser:
|
|
if _caller.PlatformRoleID != schema.SuperAdminRole && _caller.PlatformRoleID != schema.AdminRole {
|
|
err = fmt.Errorf("%s cannot %s a service-user", _caller.PlatformRoleID, action)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
_user.AccountDisabled = disableAccount
|
|
err = _user.UpdateAccountStatus(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, fmt.Sprintf("failed to %s user account: %v", action, err))
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go func(ctx context.Context) {
|
|
if !force {
|
|
return
|
|
}
|
|
extclients, err := logic.GetAllExtClients(ctx)
|
|
if err != nil {
|
|
logger.Log(0, "failed to get user extclients:", err.Error())
|
|
return
|
|
}
|
|
extclientStatus := !disableAccount
|
|
for _, extclient := range extclients {
|
|
if extclient.OwnerID == _user.Username && extclient.Enabled != extclientStatus {
|
|
_, err = logic.ToggleExtClientConnectivity(ctx, &extclient, extclientStatus)
|
|
if err != nil {
|
|
logger.Log(1, "failed to delete user extclient:", err.Error())
|
|
}
|
|
}
|
|
}
|
|
mq.PublishPeerUpdate(ctx, false)
|
|
}(ctx)
|
|
|
|
src := logic.MasterUser
|
|
if !isMaster {
|
|
src = _caller.Username
|
|
}
|
|
|
|
event := schema.EnableUser
|
|
if disableAccount {
|
|
event = schema.DisableUser
|
|
}
|
|
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: event,
|
|
Source: models.Subject{
|
|
ID: src,
|
|
Name: src,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: src,
|
|
Target: models.Subject{
|
|
ID: _user.Username,
|
|
Name: _user.Username,
|
|
Type: schema.UserSub,
|
|
Info: logic.ToReturnUser(_user),
|
|
},
|
|
Origin: schema.Dashboard,
|
|
})
|
|
|
|
logic.ReturnSuccessResponse(w, r, fmt.Sprintf("user account %sd", action))
|
|
}
|
|
|
|
// @Summary Get a user's preferences and settings
|
|
// @Router /api/users/{username}/settings [get]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param username path string true "Username of the user"
|
|
// @Success 200 {object} models.UserSettings
|
|
func getUserSettings(w http.ResponseWriter, r *http.Request) {
|
|
userID := r.Header.Get("user")
|
|
userSettings := logic.GetUserSettings(userID)
|
|
logic.ReturnSuccessResponseWithJson(w, r, userSettings, "fetched user settings")
|
|
}
|
|
|
|
// @Summary Update a user's preferences and settings
|
|
// @Router /api/users/{username}/settings [put]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param username path string true "Username of the user"
|
|
// @Param body body models.UserSettings true "User settings"
|
|
// @Success 200 {object} models.UserSettings
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func updateUserSettings(w http.ResponseWriter, r *http.Request) {
|
|
userID := r.Header.Get("user")
|
|
var req models.UserSettings
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
logger.Log(0, "failed to decode request body: ", err.Error())
|
|
err = fmt.Errorf("invalid request body: %v", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
err = logic.UpsertUserSettings(userID, req)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to update user settings: %v", err.Error())
|
|
logger.Log(0, err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
logic.ReturnSuccessResponseWithJson(w, r, req, "updated user settings")
|
|
}
|
|
|
|
// @Summary Get an individual user with role info
|
|
// @Router /api/v1/users [get]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param username query string true "Username"
|
|
// @Success 200 {object} models.ReturnUser
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func getUserV1(w http.ResponseWriter, r *http.Request) {
|
|
// set header.
|
|
w.Header().Set("Content-Type", "application/json")
|
|
usernameFetched := r.URL.Query().Get("username")
|
|
if usernameFetched == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), "badrequest"))
|
|
return
|
|
}
|
|
|
|
_user := &schema.User{
|
|
Username: usernameFetched,
|
|
}
|
|
err := _user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, usernameFetched, "failed to fetch user: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
user := logic.ToReturnUser(_user)
|
|
user.NumAccessTokens, _ = (&schema.UserAccessToken{
|
|
UserName: user.UserName,
|
|
}).CountByUser(r.Context())
|
|
userRoleTemplate := &schema.UserRole{ID: user.PlatformRoleID}
|
|
err = userRoleTemplate.GetPlatformRole(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
type ReturnUserWithRolesAndGroups struct {
|
|
models.ReturnUser
|
|
PlatformRole *schema.UserRole `json:"platform_role"`
|
|
UserGroups map[schema.UserGroupID]schema.UserGroup `json:"user_group_ids"`
|
|
TenantRoles map[string]schema.UserRole `json:"tenant_roles,omitempty"`
|
|
}
|
|
resp := ReturnUserWithRolesAndGroups{
|
|
ReturnUser: user,
|
|
PlatformRole: userRoleTemplate,
|
|
UserGroups: map[schema.UserGroupID]schema.UserGroup{},
|
|
}
|
|
for gId := range user.UserGroups {
|
|
grp, err := logic.GetUserGroup(r.Context(), gId)
|
|
if err == nil {
|
|
resp.UserGroups[gId] = grp
|
|
}
|
|
}
|
|
if scope.Level(r.Context()) == scope.OrgScope {
|
|
roles, _ := (&schema.UserRole{}).ListPlatformRoles(r.Context())
|
|
rolesMap := make(map[schema.UserRoleID]schema.UserRole)
|
|
for _, role := range roles {
|
|
rolesMap[role.ID] = role
|
|
}
|
|
|
|
tenantMemberships, _ := (&schema.TenantMembership{UserID: _user.ID}).ListByUserID(r.Context())
|
|
if len(tenantMemberships) > 0 {
|
|
resp.TenantRoles = make(map[string]schema.UserRole)
|
|
}
|
|
for _, membership := range tenantMemberships {
|
|
resp.TenantRoles[membership.TenantID] = rolesMap[membership.RoleID]
|
|
}
|
|
}
|
|
logger.Log(2, r.Header.Get("user"), "fetched user", usernameFetched)
|
|
logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched user with role info")
|
|
}
|
|
|
|
// @Summary Get all users
|
|
// @Router /api/users [get]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Success 200 {array} models.ReturnUser
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func getUsers(w http.ResponseWriter, r *http.Request) {
|
|
// set header.
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
_users, err := (&schema.User{}).ListAllWithMembership(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, "failed to fetch users: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
users := make([]models.ReturnUser, 0, len(_users))
|
|
for i := range _users {
|
|
users = append(users, logic.ToReturnUser(&_users[i]))
|
|
users[i].NumAccessTokens, _ = (&schema.UserAccessToken{
|
|
UserName: users[i].UserName,
|
|
}).CountByUser(r.Context())
|
|
}
|
|
|
|
logic.SortUsers(users[:])
|
|
logger.Log(2, r.Header.Get("user"), "fetched users")
|
|
json.NewEncoder(w).Encode(users)
|
|
}
|
|
|
|
// @Summary List all users
|
|
// @Router /api/v2/users [get]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param account_status query string false "Filter by Account Status" Enums(enabled, disabled)
|
|
// @Param mfa_status query string false "Filter by MFA Status" Enums(enabled, disabled)
|
|
// @Param role query []string false "Filter by Role" Enums(super-admin, admin, platform-user, service-user, auditor)
|
|
// @Param auth_type query string false "Filter by Auth Type" Enums(basic, oauth)
|
|
// @Param q query string false "Search across fields"
|
|
// @Param page query int false "Page number"
|
|
// @Param per_page query int false "Items per page"
|
|
// @Success 200 {array} models.ReturnUser
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func listUsers(w http.ResponseWriter, r *http.Request) {
|
|
var accountStatusFilter, mfaStatusFilter, roleFilter, authTypeFilter []interface{}
|
|
for _, filter := range r.URL.Query()["account_status"] {
|
|
switch filter {
|
|
case "enabled":
|
|
accountStatusFilter = append(accountStatusFilter, false)
|
|
case "disabled":
|
|
accountStatusFilter = append(accountStatusFilter, true)
|
|
}
|
|
}
|
|
|
|
for _, filter := range r.URL.Query()["mfa_status"] {
|
|
switch filter {
|
|
case "enabled":
|
|
mfaStatusFilter = append(mfaStatusFilter, true)
|
|
case "disabled":
|
|
mfaStatusFilter = append(mfaStatusFilter, false)
|
|
}
|
|
}
|
|
|
|
for _, filter := range r.URL.Query()["role"] {
|
|
roleFilter = append(roleFilter, filter)
|
|
}
|
|
|
|
for _, filter := range r.URL.Query()["auth_type"] {
|
|
if filter == "basic" {
|
|
filter = string(schema.BasicAuth)
|
|
}
|
|
|
|
authTypeFilter = append(authTypeFilter, filter)
|
|
}
|
|
|
|
q := r.URL.Query().Get("q")
|
|
|
|
var page, pageSize int
|
|
page, _ = strconv.Atoi(r.URL.Query().Get("page"))
|
|
if page == 0 {
|
|
page = 1
|
|
}
|
|
|
|
pageSize, _ = strconv.Atoi(r.URL.Query().Get("per_page"))
|
|
if pageSize < 1 || pageSize > 100 {
|
|
pageSize = 10
|
|
}
|
|
|
|
// account_disabled, is_mfa_enabled, role_id and auth_type all live on the membership
|
|
// tables now, not users_v1. Qualify them with the join alias used by
|
|
// ListAllWithMembership/CountWithMembership so they don't collide with the identically
|
|
// named legacy columns still present on users_v1.
|
|
membershipAlias := "tm"
|
|
if scope.Level(r.Context()) == scope.OrgScope {
|
|
membershipAlias = "om"
|
|
}
|
|
|
|
_users, err := (&schema.User{}).ListAllWithMembership(
|
|
r.Context(),
|
|
dbtypes.WithFilter(membershipAlias+".account_disabled", accountStatusFilter...),
|
|
dbtypes.WithFilter(membershipAlias+".is_mfa_enabled", mfaStatusFilter...),
|
|
dbtypes.WithFilter(membershipAlias+".role_id", roleFilter...),
|
|
dbtypes.WithFilter(membershipAlias+".auth_type", authTypeFilter...),
|
|
dbtypes.WithSearchQuery(q, "username"),
|
|
dbtypes.InAscOrder("username"),
|
|
dbtypes.WithPagination(page, pageSize),
|
|
)
|
|
if err != nil {
|
|
logger.Log(0, "failed to fetch users: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
users := make([]models.ReturnUser, 0, len(_users))
|
|
for i := range _users {
|
|
users = append(users, logic.ToReturnUser(&_users[i]))
|
|
users[i].NumAccessTokens, _ = (&schema.UserAccessToken{
|
|
UserName: users[i].UserName,
|
|
}).CountByUser(r.Context())
|
|
}
|
|
|
|
total, err := (&schema.User{}).CountWithMembership(
|
|
r.Context(),
|
|
dbtypes.WithFilter(membershipAlias+".account_disabled", accountStatusFilter...),
|
|
dbtypes.WithFilter(membershipAlias+".is_mfa_enabled", mfaStatusFilter...),
|
|
dbtypes.WithFilter(membershipAlias+".role_id", roleFilter...),
|
|
dbtypes.WithFilter(membershipAlias+".auth_type", authTypeFilter...),
|
|
dbtypes.WithSearchQuery(q, "username"),
|
|
)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
|
return
|
|
}
|
|
|
|
totalPages := (total + pageSize - 1) / pageSize
|
|
if totalPages == 0 {
|
|
totalPages = 1
|
|
}
|
|
|
|
logger.Log(2, r.Header.Get("user"), "fetched users")
|
|
|
|
response := models.PaginatedResponse{
|
|
Data: users,
|
|
Page: page,
|
|
PerPage: pageSize,
|
|
Total: total,
|
|
TotalPages: totalPages,
|
|
}
|
|
|
|
logic.ReturnSuccessResponseWithJson(w, r, response, "fetched users")
|
|
}
|
|
|
|
// @Summary Create a super admin
|
|
// @Router /api/users/adm/createsuperadmin [post]
|
|
// @Tags Users
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body schema.User true "User details"
|
|
// @Success 200 {object} models.ReturnUser
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func createSuperAdmin(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
exists, err := (&schema.User{}).SuperAdminExists(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, "failed to check for superadmin: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
if exists {
|
|
err = errors.New("superadmin user already exists")
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
|
|
var user schema.User
|
|
err = json.NewDecoder(r.Body).Decode(&user)
|
|
if err != nil {
|
|
slog.Error("error decoding request body", "error", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
|
|
if !logic.IsBasicAuthEnabled(r.Context()) {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(fmt.Errorf("basic auth is disabled"), logic.BadReq),
|
|
)
|
|
return
|
|
}
|
|
|
|
user.PlatformRoleID = schema.SuperAdminRole
|
|
err = orchestrator.GetRepository().UserOrchestrator().ValidateCreateUser(r.Context(), &user)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
|
|
dbctx := db.BeginTx(r.Context())
|
|
commit := false
|
|
defer func() {
|
|
if commit {
|
|
db.FromContext(dbctx).Commit()
|
|
} else {
|
|
db.FromContext(dbctx).Rollback()
|
|
}
|
|
}()
|
|
|
|
err = orchestrator.GetRepository().UserOrchestrator().CreateUser(dbctx, &user)
|
|
if err != nil {
|
|
slog.Error("failed to create superadmin", "error", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
|
|
// on MSP the org owner is created via createOrgOwner instead, which also
|
|
// grants super admin on every tenant of the org.
|
|
if !logic.IsMSP(dbctx) {
|
|
err = createOrgOwnerMembership(dbctx, &user)
|
|
if err != nil {
|
|
slog.Error("failed to create org owner for superadmin", "error", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
|
return
|
|
}
|
|
}
|
|
|
|
commit = true
|
|
|
|
logger.Log(1, user.Username, "was made a super admin")
|
|
_ = json.NewEncoder(w).Encode(logic.ToReturnUser(&user))
|
|
}
|
|
|
|
// @Summary Transfer super admin role to another admin user
|
|
// @Router /api/users/adm/transfersuperadmin/{username} [post]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param username path string true "Username of the user to transfer super admin role"
|
|
// @Success 200 {object} models.ReturnUser
|
|
// @Failure 403 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func transferSuperAdmin(w http.ResponseWriter, r *http.Request) {
|
|
dbctx := db.BeginTx(r.Context())
|
|
commit := false
|
|
defer func() {
|
|
if commit {
|
|
db.FromContext(dbctx).Commit()
|
|
} else {
|
|
db.FromContext(dbctx).Rollback()
|
|
}
|
|
}()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
caller := &schema.User{Username: r.Header.Get("user")}
|
|
if err := caller.Get(dbctx); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
if caller.PlatformRoleID != schema.SuperAdminRole {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only superadmin can assign the superadmin role to another user"), "forbidden"))
|
|
return
|
|
}
|
|
var params = mux.Vars(r)
|
|
username := params["username"]
|
|
u := &schema.User{Username: username}
|
|
if err := u.Get(dbctx); err != nil {
|
|
slog.Error("error getting user", "user", u.Username, "error", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
if u.PlatformRoleID != schema.AdminRole {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only admins can be promoted to superadmin role"), "forbidden"))
|
|
return
|
|
}
|
|
if !logic.IsBasicAuthEnabled(dbctx) {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"),
|
|
)
|
|
return
|
|
}
|
|
|
|
u.PlatformRoleID = schema.SuperAdminRole
|
|
err := u.Update(dbctx)
|
|
if err != nil {
|
|
slog.Error("error updating user to superadmin: ", "user", u.Username, "error", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
if err = u.UpsertMembership(dbctx); err != nil {
|
|
slog.Error("error upserting membership for new superadmin: ", "user", u.Username, "error", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
caller.PlatformRoleID = schema.AdminRole
|
|
err = caller.Update(dbctx)
|
|
if err != nil {
|
|
slog.Error("error demoting user to admin: ", "user", caller.Username, "error", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
if err = caller.UpsertMembership(dbctx); err != nil {
|
|
slog.Error("error upserting membership for demoted admin: ", "user", caller.Username, "error", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
commit = true
|
|
|
|
slog.Info("user was made a super admin", "user", u.Username)
|
|
json.NewEncoder(w).Encode(logic.ToReturnUser(u))
|
|
}
|
|
|
|
// @Summary Create a user
|
|
// @Router /api/users/{username} [post]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param username path string true "Username of the user to create"
|
|
// @Param body body schema.User true "User details"
|
|
// @Success 200 {object} models.ReturnUser
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 403 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func createUser(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
caller := &schema.User{Username: r.Header.Get("user")}
|
|
err := caller.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
var user schema.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 !servercfg.IsPro {
|
|
user.PlatformRoleID = schema.AdminRole
|
|
}
|
|
if user.Username == logic.MasterUser {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username not allowed"), "badrequest"))
|
|
return
|
|
}
|
|
|
|
if user.PlatformRoleID == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("platform role is missing"), "badrequest"))
|
|
return
|
|
}
|
|
userRole := &schema.UserRole{ID: user.PlatformRoleID}
|
|
err = userRole.GetPlatformRole(r.Context())
|
|
if err != nil {
|
|
err = errors.New("error fetching role " + user.PlatformRoleID.String() + " " + err.Error())
|
|
slog.Error("error creating new user: ", "user", user.Username, "error", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
if userRole.ID == schema.SuperAdminRole {
|
|
err = errors.New("additional superadmins cannot be created")
|
|
slog.Error("error creating new user: ", "user", user.Username, "error", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
|
|
if caller.PlatformRoleID != schema.SuperAdminRole && user.PlatformRoleID == schema.AdminRole {
|
|
err = errors.New("only superadmin can create admin users")
|
|
slog.Error("error creating new user: ", "user", user.Username, "error", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
|
|
if !servercfg.IsPro && user.PlatformRoleID != schema.AdminRole {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("non-admins users can only be created on Pro version"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
err = orchestrator.GetRepository().UserOrchestrator().ValidateCreateUser(r.Context(), &user)
|
|
if err != nil {
|
|
err = fmt.Errorf("invalid user: %v", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
|
|
err = orchestrator.GetRepository().UserOrchestrator().CreateUser(r.Context(), &user)
|
|
if err != nil {
|
|
slog.Error("error creating new user: ", "user", user.Username, "error", err.Error())
|
|
errType := logic.BadReq
|
|
if errors.Is(err, logic.ErrUserLimitExceeded) {
|
|
errType = logic.Forbidden
|
|
}
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, errType))
|
|
return
|
|
}
|
|
if err = user.UpsertMembership(r.Context()); err != nil {
|
|
slog.Error("error upserting membership: ", "user", user.Username, "error", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.Create,
|
|
Source: models.Subject{
|
|
ID: caller.Username,
|
|
Name: caller.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: caller.Username,
|
|
Target: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
Info: logic.ToReturnUser(&user),
|
|
},
|
|
Origin: schema.Dashboard,
|
|
})
|
|
|
|
_ = (&schema.UserInvite{
|
|
Email: user.Username,
|
|
}).DeleteByEmail(r.Context())
|
|
|
|
_ = (&schema.PendingUser{
|
|
Username: user.Username,
|
|
}).Delete(r.Context())
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go mq.PublishPeerUpdate(ctx, false)
|
|
slog.Info("user was created", "username", user.Username)
|
|
json.NewEncoder(w).Encode(logic.ToReturnUser(&user))
|
|
}
|
|
|
|
// @Summary Update a user
|
|
// @Router /api/users/{username} [put]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param username path string true "Username of the user to update"
|
|
// @Param body body schema.User true "User details"
|
|
// @Success 200 {object} models.ReturnUser
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 403 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func updateUser(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
var params = mux.Vars(r)
|
|
// start here
|
|
var caller *schema.User
|
|
var err error
|
|
var ismaster bool
|
|
if r.Header.Get("user") == logic.MasterUser {
|
|
ismaster = true
|
|
} else {
|
|
caller = &schema.User{Username: r.Header.Get("user")}
|
|
err = caller.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
}
|
|
|
|
username := params["username"]
|
|
user := &schema.User{Username: username}
|
|
err = user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, username,
|
|
"failed to update user info: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
var userchange schema.User
|
|
// we decode our body request params
|
|
err = json.NewDecoder(r.Body).Decode(&userchange)
|
|
if err != nil {
|
|
slog.Error("failed to decode body", "error ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
if user.Username != userchange.Username {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(
|
|
errors.New("user in param and request body not matching"),
|
|
"badrequest",
|
|
),
|
|
)
|
|
return
|
|
}
|
|
if user.PlatformRoleID != userchange.PlatformRoleID && userchange.PlatformRoleID == schema.SuperAdminRole {
|
|
err = errors.New("only a super-admin can assign the super-admin role to another user via transferSuperAdmin method")
|
|
slog.Error(
|
|
"failed to update user",
|
|
"caller",
|
|
caller.Username,
|
|
"attempted to assign super-admin role to user",
|
|
username,
|
|
"error",
|
|
err,
|
|
)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
if user.PlatformRoleID != userchange.PlatformRoleID && userchange.PlatformRoleID == schema.OrgOwner {
|
|
err = errors.New("org-owner role cannot be assigned via this method, use the transfer ownership API instead")
|
|
slog.Error(
|
|
"failed to update user",
|
|
"caller",
|
|
caller.Username,
|
|
"attempted to assign org-owner role to user",
|
|
username,
|
|
"error",
|
|
err,
|
|
)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
selfUpdate := false
|
|
if !ismaster && caller.Username == user.Username {
|
|
selfUpdate = true
|
|
}
|
|
|
|
if !ismaster && !selfUpdate {
|
|
switch scope.Level(r.Context()) {
|
|
case scope.OrgScope:
|
|
if caller.PlatformRoleID == schema.OrgAdmin && user.PlatformRoleID == schema.OrgOwner {
|
|
slog.Error("non-org-owner user", "caller", caller.Username, "attempted to update org-owner user", username)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update org-owner user"), "forbidden"))
|
|
return
|
|
}
|
|
if caller.PlatformRoleID != schema.OrgAdmin && caller.PlatformRoleID != schema.OrgOwner {
|
|
slog.Error("operation not allowed", "caller", caller.Username, "attempted to update user", username)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update org-owner user"), "forbidden"))
|
|
return
|
|
}
|
|
if caller.PlatformRoleID == schema.OrgAdmin && user.PlatformRoleID == schema.OrgAdmin {
|
|
slog.Error("an org-admin user does not have permissions to update another org-admin user", "caller", caller.Username, "attempted to update org-admin user", username)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("an org-admin user does not have permissions to update another org-admin user"), "forbidden"))
|
|
return
|
|
}
|
|
if caller.PlatformRoleID == schema.OrgAdmin && userchange.PlatformRoleID == schema.OrgAdmin {
|
|
err = errors.New("an org-admin user does not have permissions to assign the org-admin role to another user")
|
|
slog.Error(
|
|
"failed to update user",
|
|
"caller",
|
|
caller.Username,
|
|
"attempted to update user",
|
|
username,
|
|
"error",
|
|
err,
|
|
)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
case scope.TenantScope:
|
|
if caller.PlatformRoleID == schema.AdminRole && user.PlatformRoleID == schema.SuperAdminRole {
|
|
slog.Error("non-superadmin user", "caller", caller.Username, "attempted to update superadmin user", username)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update superadmin user"), "forbidden"))
|
|
return
|
|
}
|
|
if caller.PlatformRoleID != schema.AdminRole && caller.PlatformRoleID != schema.SuperAdminRole {
|
|
slog.Error("operation not allowed", "caller", caller.Username, "attempted to update user", username)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update superadmin user"), "forbidden"))
|
|
return
|
|
}
|
|
if caller.PlatformRoleID == schema.AdminRole && user.PlatformRoleID == schema.AdminRole {
|
|
slog.Error("an admin user does not have permissions to update another admin user", "caller", caller.Username, "attempted to update admin user", username)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("an admin user does not have permissions to update another admin user"), "forbidden"))
|
|
return
|
|
}
|
|
if caller.PlatformRoleID == schema.AdminRole && userchange.PlatformRoleID == schema.AdminRole {
|
|
err = errors.New("an admin user does not have permissions to assign the admin role to another user")
|
|
slog.Error(
|
|
"failed to update user",
|
|
"caller",
|
|
caller.Username,
|
|
"attempted to update user",
|
|
username,
|
|
"error",
|
|
err,
|
|
)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if scope.Level(r.Context()) == scope.TenantScope {
|
|
if !ismaster && selfUpdate {
|
|
if user.PlatformRoleID != userchange.PlatformRoleID {
|
|
slog.Error("user cannot change his own role", "caller", caller.Username, "attempted to update user role", username)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not allowed to self assign role"), "forbidden"))
|
|
return
|
|
|
|
}
|
|
|
|
if logic.IsMFAEnforced(r.Context()) && user.IsMFAEnabled && !userchange.IsMFAEnabled {
|
|
err = errors.New("mfa is enforced, user cannot unset their own mfa")
|
|
slog.Error("failed to update user", "caller", caller.Username, "attempted to update user", username, "error", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
|
|
if servercfg.IsPro && caller.PlatformRoleID != schema.SuperAdminRole {
|
|
// users cannot update their own groups; superadmin is exempt
|
|
if len(user.UserGroups.Data()) != len(userchange.UserGroups.Data()) || !reflect.DeepEqual(user.UserGroups.Data(), userchange.UserGroups.Data()) {
|
|
err = errors.New("user cannot self update their groups")
|
|
slog.Error("failed to update user", "caller", caller.Username, "attempted to update user", username, "error", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if ismaster {
|
|
if user.PlatformRoleID != schema.SuperAdminRole && userchange.PlatformRoleID == schema.SuperAdminRole {
|
|
slog.Error("operation not allowed", "caller", logic.MasterUser, "attempted to update user role to superadmin", username)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("attempted to update user role to superadmin"), "forbidden"))
|
|
return
|
|
}
|
|
}
|
|
|
|
if user.AuthType == schema.Inherited && userchange.Password != "" {
|
|
err = fmt.Errorf("cannot update password for user with inherited auth")
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
}
|
|
|
|
if logic.IsOauthUser(r.Context(), user) == nil && userchange.Password != "" {
|
|
err := fmt.Errorf("cannot update password for an oauth user %s", username)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
|
|
return
|
|
}
|
|
|
|
var deleteAccessTokens bool
|
|
if scope.Level(r.Context()) == scope.TenantScope &&
|
|
(userchange.PlatformRoleID != user.PlatformRoleID || !logic.CompareMaps(user.UserGroups.Data(), userchange.UserGroups.Data())) {
|
|
deleteAccessTokens = true
|
|
}
|
|
|
|
oldUser := *user
|
|
if ismaster {
|
|
caller = &schema.User{
|
|
Username: logic.MasterUser,
|
|
}
|
|
}
|
|
action := schema.Update
|
|
// TODO: here we are relying on the dashboard to only
|
|
// make singular updates, but it's possible that the
|
|
// API can be called to make multiple changes to the
|
|
// user. We should update it to log multiple events
|
|
// or create singular update APIs.
|
|
if userchange.IsMFAEnabled != user.IsMFAEnabled {
|
|
if userchange.IsMFAEnabled {
|
|
// the update API won't be used to enable MFA.
|
|
action = schema.EnableMFA
|
|
} else {
|
|
action = schema.DisableMFA
|
|
}
|
|
}
|
|
e := models.Event{
|
|
Action: action,
|
|
Source: models.Subject{
|
|
ID: caller.Username,
|
|
Name: caller.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: caller.Username,
|
|
Target: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
Diff: models.Diff{
|
|
Old: logic.ToUserEventLog(r.Context(), &oldUser),
|
|
New: logic.ToUserEventLog(r.Context(), &userchange),
|
|
},
|
|
Origin: schema.Dashboard,
|
|
}
|
|
user, err = logic.UpdateUser(r.Context(), &userchange, user)
|
|
if err != nil {
|
|
logger.Log(0, username,
|
|
"failed to update user info: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
if deleteAccessTokens {
|
|
_ = (&schema.UserAccessToken{UserName: user.Username}).DeleteAllUserTokens(r.Context())
|
|
}
|
|
|
|
logic.LogEvent(r.Context(), &e)
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go mq.PublishPeerUpdate(ctx, false)
|
|
go func(ctx context.Context) {
|
|
if scope.Level(ctx) != scope.TenantScope {
|
|
return
|
|
}
|
|
|
|
extclients, err := logic.GetAllExtClients(ctx)
|
|
if err != nil {
|
|
slog.Error("failed to fetch extclients", "error", err)
|
|
return
|
|
}
|
|
|
|
for _, extclient := range extclients {
|
|
if extclient.OwnerID != user.Username {
|
|
continue
|
|
}
|
|
|
|
if !logic.UserHasNetworkGroupAccess(ctx, user, extclient.Network) {
|
|
err = logic.DeleteExtClientAndCleanup(r.Context(), extclient)
|
|
if err != nil {
|
|
slog.Error("failed to delete extclient",
|
|
"id", extclient.ClientID, "owner", user.Username, "error", err)
|
|
} else {
|
|
if err := mq.PublishDeletedClientPeerUpdate(ctx, &extclient); err != nil {
|
|
slog.Error("error setting ext peers: " + err.Error())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}(ctx)
|
|
logger.Log(1, username, "was updated")
|
|
json.NewEncoder(w).Encode(logic.ToReturnUser(user))
|
|
}
|
|
|
|
// @Summary Delete a user
|
|
// @Router /api/users/{username} [delete]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param username path string true "Username of the user to delete"
|
|
// @Param force_delete_configs query string false "Force delete configs"
|
|
// @Success 200 {string} string
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func deleteUser(w http.ResponseWriter, r *http.Request) {
|
|
// Set header
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// get params
|
|
var params = mux.Vars(r)
|
|
caller := &schema.User{Username: r.Header.Get("user")}
|
|
err := caller.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
callerUserRole := &schema.UserRole{ID: caller.PlatformRoleID}
|
|
err = callerUserRole.GetPlatformRole(r.Context())
|
|
if err != nil {
|
|
slog.Error("failed to get role ", "role", callerUserRole.ID, "error", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
username := params["username"]
|
|
user := &schema.User{Username: username}
|
|
err = user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logger.Log(0, username,
|
|
"failed to update user info: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
userRole := &schema.UserRole{ID: user.PlatformRoleID}
|
|
err = userRole.GetPlatformRole(r.Context())
|
|
if err != nil {
|
|
slog.Error("failed to get role ", "role", userRole.ID, "error", err)
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
switch scope.Level(r.Context()) {
|
|
case scope.TenantScope:
|
|
if userRole.ID == schema.SuperAdminRole {
|
|
slog.Error(
|
|
"failed to delete user: ", "user", username, "error", "superadmin cannot be deleted")
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(fmt.Errorf("superadmin cannot be deleted"), "internal"),
|
|
)
|
|
return
|
|
}
|
|
if callerUserRole.ID != schema.SuperAdminRole {
|
|
if callerUserRole.ID == schema.AdminRole && userRole.ID == schema.AdminRole {
|
|
slog.Error(
|
|
"failed to delete user: ",
|
|
"user",
|
|
username,
|
|
"error",
|
|
"admin cannot delete another admin user, including oneself",
|
|
)
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(
|
|
fmt.Errorf("admin cannot delete another admin user, including oneself"),
|
|
"internal",
|
|
),
|
|
)
|
|
return
|
|
}
|
|
}
|
|
case scope.OrgScope:
|
|
if userRole.ID == schema.OrgOwner {
|
|
slog.Error(
|
|
"failed to delete user: ", "user", username, "error", "org-owner cannot be deleted")
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(fmt.Errorf("org-owner cannot be deleted"), "internal"),
|
|
)
|
|
return
|
|
}
|
|
if userRole.ID == schema.OrgAdmin && callerUserRole.ID != schema.OrgOwner {
|
|
slog.Error(
|
|
"failed to delete user: ",
|
|
"user",
|
|
username,
|
|
"error",
|
|
"only the org-owner can delete an org-admin",
|
|
)
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(
|
|
fmt.Errorf("only the org-owner can delete an org-admin"),
|
|
"internal",
|
|
),
|
|
)
|
|
return
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
|
|
if logic.IsIDPUser(r.Context(), user) {
|
|
err = fmt.Errorf("cannot delete idp user %s", username)
|
|
logger.Log(0, username, "failed to delete user: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
forceDeleteConfigs := r.URL.Query().Get("force_delete_configs") == "true"
|
|
if scope.Level(r.Context()) == scope.OrgScope {
|
|
err = logic.DeleteOrgUser(r.Context(), user, forceDeleteConfigs, cleanupUserRefs)
|
|
} else {
|
|
err = logic.DeleteTenantUser(r.Context(), user, forceDeleteConfigs, cleanupUserRefs)
|
|
}
|
|
if err != nil {
|
|
logger.Log(0, username,
|
|
"failed to delete user membership: ", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.Delete,
|
|
Source: models.Subject{
|
|
ID: caller.Username,
|
|
Name: caller.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: caller.Username,
|
|
Target: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
Origin: schema.Dashboard,
|
|
Diff: models.Diff{
|
|
Old: logic.ToUserEventLog(r.Context(), user),
|
|
New: nil,
|
|
},
|
|
})
|
|
logger.Log(1, username, "was deleted")
|
|
json.NewEncoder(w).Encode(params["username"] + " deleted.")
|
|
}
|
|
|
|
func cleanupUserRefs(ctx context.Context, username string, forceDeleteConfigs bool) {
|
|
extclients, err := logic.GetAllExtClients(ctx)
|
|
if err != nil {
|
|
slog.Error("failed to get extclients", "error", err)
|
|
} else {
|
|
for _, extclient := range extclients {
|
|
if extclient.OwnerID != username {
|
|
continue
|
|
}
|
|
if extclient.DeviceID == "" && extclient.RemoteAccessClientID == "" && !forceDeleteConfigs {
|
|
// only delete wireguard configs on force
|
|
continue
|
|
}
|
|
if err := logic.DeleteExtClientAndCleanup(ctx, extclient); err != nil {
|
|
slog.Error("failed to delete extclient",
|
|
"id", extclient.ClientID, "owner", username, "error", err)
|
|
continue
|
|
}
|
|
if err := mq.PublishDeletedClientPeerUpdate(ctx, &extclient); err != nil {
|
|
slog.Error("error setting ext peers: " + err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
_ = (&schema.UserInvite{
|
|
Email: username,
|
|
}).DeleteByEmail(ctx)
|
|
|
|
if err := mq.PublishPeerUpdate(ctx, false); err != nil {
|
|
slog.Error("error publishing peer update", "error", err)
|
|
}
|
|
}
|
|
|
|
// @Summary Bulk delete users
|
|
// @Router /api/v1/users/bulk [delete]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body models.BulkDeleteRequest true "List of usernames to delete"
|
|
// @Param force_delete_configs query bool false "Force delete associated ext-client configs"
|
|
// @Success 202 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
func bulkDeleteUsers(w http.ResponseWriter, r *http.Request) {
|
|
var req models.BulkDeleteRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("invalid request body: %w", err), logic.BadReq))
|
|
return
|
|
}
|
|
if len(req.IDs) == 0 {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("no usernames provided"), logic.BadReq))
|
|
return
|
|
}
|
|
|
|
callerName := r.Header.Get("user")
|
|
var caller *schema.User
|
|
var callerRole *schema.UserRole
|
|
var isMaster bool
|
|
if callerName == logic.MasterUser {
|
|
isMaster = true
|
|
} else {
|
|
caller = &schema.User{Username: callerName}
|
|
if err := caller.GetWithMembership(r.Context()); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
callerRole = &schema.UserRole{ID: caller.PlatformRoleID}
|
|
if err := callerRole.GetPlatformRole(r.Context()); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
}
|
|
forceDeleteConfigs := r.URL.Query().Get("force_delete_configs") == "true"
|
|
logic.ReturnAcceptedResponse(w, r, fmt.Sprintf("bulk delete of %d user(s) accepted", len(req.IDs)))
|
|
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go func(ctx context.Context) {
|
|
deleted := 0
|
|
for _, username := range req.IDs {
|
|
user := &schema.User{Username: username}
|
|
if err := user.GetWithMembership(ctx); err != nil {
|
|
slog.Error("bulk user delete: user not found", "username", username, "error", err)
|
|
continue
|
|
}
|
|
if !isMaster && username == caller.Username {
|
|
slog.Error("bulk user delete: cannot delete own account", "username", username)
|
|
continue
|
|
}
|
|
if user.PlatformRoleID == schema.SuperAdminRole {
|
|
slog.Error("bulk user delete: cannot delete superadmin", "username", username)
|
|
continue
|
|
}
|
|
if !isMaster {
|
|
if callerRole.ID != schema.SuperAdminRole {
|
|
if callerRole.ID == schema.AdminRole && user.PlatformRoleID == schema.AdminRole {
|
|
slog.Error("bulk user delete: admin cannot delete another admin", "username", username)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
if user.AuthType == schema.OAuth || user.ExternalIdentityProviderID != "" {
|
|
slog.Error("bulk user delete: cannot delete idp user", "username", username)
|
|
continue
|
|
}
|
|
|
|
var deleteErr error
|
|
if scope.Level(ctx) == scope.OrgScope {
|
|
deleteErr = logic.DeleteOrgUser(ctx, user, forceDeleteConfigs, cleanupUserRefs)
|
|
} else {
|
|
deleteErr = logic.DeleteTenantUser(ctx, user, forceDeleteConfigs, cleanupUserRefs)
|
|
}
|
|
if deleteErr != nil {
|
|
slog.Error("bulk user delete: failed to delete user membership", "username", username, "error", deleteErr)
|
|
continue
|
|
}
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.Delete,
|
|
Source: models.Subject{
|
|
ID: callerName,
|
|
Name: callerName,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: callerName,
|
|
Target: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
Origin: schema.Dashboard,
|
|
Diff: models.Diff{Old: logic.ToUserEventLog(r.Context(), user), New: nil},
|
|
})
|
|
logger.Log(1, username, "was deleted")
|
|
deleted++
|
|
}
|
|
if deleted > 0 {
|
|
mq.PublishPeerUpdate(ctx, false)
|
|
}
|
|
slog.Info("bulk user delete completed", "deleted", deleted, "total", len(req.IDs))
|
|
}(ctx)
|
|
}
|
|
|
|
// @Summary Bulk disable/enable user accounts
|
|
// @Router /api/v1/users/bulk/status [post]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body models.BulkUserStatusUpdate true "List of usernames and desired status"
|
|
// @Param force_toggle_configs query bool false "Also toggle associated ext-client connectivity"
|
|
// @Success 202 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
func bulkUpdateUserStatus(w http.ResponseWriter, r *http.Request) {
|
|
var req models.BulkUserStatusUpdate
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("invalid request body: %w", err), logic.BadReq))
|
|
return
|
|
}
|
|
if len(req.IDs) == 0 {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("no usernames provided"), logic.BadReq))
|
|
return
|
|
}
|
|
|
|
callerName := r.Header.Get("user")
|
|
var caller *schema.User
|
|
var isMaster bool
|
|
if callerName == logic.MasterUser {
|
|
isMaster = true
|
|
} else {
|
|
caller = &schema.User{Username: callerName}
|
|
if err := caller.GetWithMembership(r.Context()); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
}
|
|
|
|
forceToggle := r.URL.Query().Get("force_toggle_configs") == "true"
|
|
action := "enable"
|
|
if req.Disable {
|
|
action = "disable"
|
|
}
|
|
logic.ReturnAcceptedResponse(w, r, fmt.Sprintf("bulk %s of %d user(s) accepted", action, len(req.IDs)))
|
|
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go func(ctx context.Context) {
|
|
var ownerExtClients map[string][]models.ExtClient
|
|
if forceToggle {
|
|
extclients, err := logic.GetAllExtClients(ctx)
|
|
if err != nil {
|
|
slog.Error("bulk user status: failed to get extclients", "error", err)
|
|
} else {
|
|
ownerExtClients = make(map[string][]models.ExtClient, len(req.IDs))
|
|
for _, ec := range extclients {
|
|
ownerExtClients[ec.OwnerID] = append(ownerExtClients[ec.OwnerID], ec)
|
|
}
|
|
}
|
|
}
|
|
|
|
updated := 0
|
|
for _, username := range req.IDs {
|
|
user := &schema.User{Username: username}
|
|
if err := user.GetWithMembership(ctx); err != nil {
|
|
slog.Error("bulk user status: user not found", "username", username, "error", err)
|
|
continue
|
|
}
|
|
if !isMaster && caller.Username == username {
|
|
slog.Error("bulk user status: cannot change own account status", "username", username)
|
|
continue
|
|
}
|
|
if !isMaster {
|
|
skip := false
|
|
switch user.PlatformRoleID {
|
|
case schema.SuperAdminRole:
|
|
if req.Disable {
|
|
slog.Error("bulk user status: cannot disable superadmin", "username", username)
|
|
skip = true
|
|
}
|
|
case schema.AdminRole:
|
|
if caller.PlatformRoleID != schema.SuperAdminRole {
|
|
slog.Error("bulk user status: insufficient role to change admin status", "username", username, "caller_role", caller.PlatformRoleID)
|
|
skip = true
|
|
}
|
|
case schema.PlatformUser:
|
|
if caller.PlatformRoleID != schema.SuperAdminRole && caller.PlatformRoleID != schema.AdminRole {
|
|
slog.Error("bulk user status: insufficient role to change platform-user status", "username", username, "caller_role", caller.PlatformRoleID)
|
|
skip = true
|
|
}
|
|
case schema.ServiceUser:
|
|
if caller.PlatformRoleID != schema.SuperAdminRole && caller.PlatformRoleID != schema.AdminRole {
|
|
slog.Error("bulk user status: insufficient role to change service-user status", "username", username, "caller_role", caller.PlatformRoleID)
|
|
skip = true
|
|
}
|
|
}
|
|
if skip {
|
|
continue
|
|
}
|
|
}
|
|
oldUser := *user
|
|
user.AccountDisabled = req.Disable
|
|
if err := user.UpdateAccountStatus(ctx); err != nil {
|
|
slog.Error("bulk user status: failed to update status", "username", username, "error", err)
|
|
continue
|
|
}
|
|
logic.LogEvent(ctx, &models.Event{
|
|
Action: schema.Update,
|
|
Source: models.Subject{
|
|
ID: callerName,
|
|
Name: callerName,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: callerName,
|
|
Target: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
Diff: models.Diff{
|
|
Old: logic.ToUserEventLog(ctx, &oldUser),
|
|
New: logic.ToUserEventLog(ctx, user),
|
|
},
|
|
Origin: schema.Dashboard,
|
|
})
|
|
logger.Log(1, username, "was", action+"d")
|
|
updated++
|
|
|
|
if forceToggle && ownerExtClients != nil {
|
|
extclientStatus := !req.Disable
|
|
for _, extclient := range ownerExtClients[user.Username] {
|
|
if extclient.Enabled != extclientStatus {
|
|
if _, err := logic.ToggleExtClientConnectivity(ctx, &extclient, extclientStatus); err != nil {
|
|
slog.Error("bulk user status: failed to toggle extclient", "id", extclient.ClientID, "owner", user.Username, "error", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if updated > 0 && forceToggle {
|
|
mq.PublishPeerUpdate(ctx, false)
|
|
}
|
|
slog.Info("bulk user status update completed", "action", action, "updated", updated, "total", len(req.IDs))
|
|
}(ctx)
|
|
}
|
|
|
|
// Called when vpn client dials in to start the auth flow and first stage is to get register URL itself
|
|
func socketHandler(w http.ResponseWriter, r *http.Request) {
|
|
// Upgrade our raw HTTP connection to a websocket based one
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
logger.Log(0, "error during connection upgrade for node sign-in:", err.Error())
|
|
return
|
|
}
|
|
if conn == nil {
|
|
logger.Log(0, "failed to establish web-socket connection during node sign-in")
|
|
return
|
|
}
|
|
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
// Start handling the session
|
|
go auth.SessionHandler(ctx, conn)
|
|
}
|
|
|
|
func listRoles(w http.ResponseWriter, r *http.Request) {
|
|
var roles []schema.UserRole
|
|
var err error
|
|
roles, err = (&schema.UserRole{}).ListPlatformRoles(r.Context())
|
|
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 Log out a user
|
|
// @Router /api/v1/users/logout [post]
|
|
// @Tags Users
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param username query string true "Username"
|
|
// @Success 200 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
func logout(w http.ResponseWriter, r *http.Request) {
|
|
// set header.
|
|
w.Header().Set("Content-Type", "application/json")
|
|
userName := r.URL.Query().Get("username")
|
|
user := &schema.User{Username: userName}
|
|
err := user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
var target schema.SubjectType
|
|
if val := r.Header.Get("From-Ui"); val == "true" {
|
|
target = schema.DashboardSub
|
|
} else {
|
|
target = schema.ClientAppSub
|
|
}
|
|
if target != "" {
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.LogOut,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: target.String(),
|
|
Name: target.String(),
|
|
Type: target,
|
|
},
|
|
Origin: schema.Origin(target),
|
|
})
|
|
}
|
|
|
|
logic.ReturnSuccessResponse(w, r, "user logged out")
|
|
}
|