diff --git a/backend/api/controllers/v1/users/create.go b/backend/api/controllers/v1/users/create.go index 65da7ca..0530554 100644 --- a/backend/api/controllers/v1/users/create.go +++ b/backend/api/controllers/v1/users/create.go @@ -9,9 +9,9 @@ import ( "github.com/go-playground/validator/v10" - "git.terah.dev/imterah/hermes/api/constants" "git.terah.dev/imterah/hermes/api/dbcore" "git.terah.dev/imterah/hermes/api/jwtcore" + permissionHelper "git.terah.dev/imterah/hermes/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" @@ -92,7 +92,7 @@ func CreateUser(c *gin.Context) { permissions := []dbcore.Permission{} - for _, permission := range constants.DefaultPermissionNodes { + for _, permission := range permissionHelper.DefaultPermissionNodes { permissionEnabledState := false if unsafeSignup || strings.HasPrefix(permission, "routes.") || permission == "permissions.see" { diff --git a/backend/api/controllers/v1/users/remove.go b/backend/api/controllers/v1/users/remove.go new file mode 100644 index 0000000..0f508c8 --- /dev/null +++ b/backend/api/controllers/v1/users/remove.go @@ -0,0 +1,105 @@ +package users + +import ( + "fmt" + "net/http" + + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" + "github.com/charmbracelet/log" + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" +) + +type UserRemovalRequest struct { + Token string `validate:"required"` + UID *uint `json:"uid"` +} + +func RemoveUser(c *gin.Context) { + var req UserRemovalRequest + + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) + + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + user, err := jwtcore.GetUserFromJWT(req.Token) + + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + uid := user.ID + + if req.UID != nil { + uid = *req.UID + + if uid != user.ID && !permissions.UserHasPermission(user, "users.remove") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) + + return + } + } + + // Make sure the user exists first if we have a custom UserID + + if uid != user.ID { + var customUser *dbcore.User + userRequest := dbcore.DB.Where("id = ?", uid).Find(&customUser) + + if userRequest.Error != nil { + log.Warnf("failed to find if user exists or not: %s", userRequest.Error) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if user exists", + }) + + return + } + + userExists := userRequest.RowsAffected > 0 + + if !userExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "User doesn't exist", + }) + + return + } + } + + dbcore.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user) + + c.JSON(http.StatusOK, gin.H{ + "success": true, + }) +} diff --git a/backend/api/jwtcore/jwt.go b/backend/api/jwtcore/jwt.go index 01809c0..a40c6de 100644 --- a/backend/api/jwtcore/jwt.go +++ b/backend/api/jwtcore/jwt.go @@ -2,11 +2,13 @@ package jwtcore import ( "encoding/base64" + "errors" "fmt" "os" "strconv" "time" + "git.terah.dev/imterah/hermes/api/dbcore" "github.com/golang-jwt/jwt/v5" ) @@ -37,6 +39,49 @@ func Parse(tokenString string, options ...jwt.ParserOption) (*jwt.Token, error) return jwt.Parse(tokenString, JWTKeyCallback, options...) } +func GetUserFromJWT(token string) (*dbcore.User, error) { + parsedJWT, err := Parse(token) + + if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + return nil, fmt.Errorf("token is expired") + } else { + return nil, err + } + } + + audience, err := parsedJWT.Claims.GetAudience() + + if err != nil { + return nil, err + } + + if len(audience) < 1 { + return nil, fmt.Errorf("audience is too small") + } + + uid, err := strconv.Atoi(audience[0]) + + if err != nil { + return nil, err + } + + user := &dbcore.User{} + userRequest := dbcore.DB.Preload("Permissions").Where("id = ?", uint(uid)).Find(&user) + + if userRequest.Error != nil { + return user, fmt.Errorf("failed to find if user exists or not: %s", userRequest.Error) + } + + userExists := userRequest.RowsAffected > 0 + + if !userExists { + return user, fmt.Errorf("user does not exist") + } + + return user, nil +} + func Generate(uid uint) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(3 * time.Minute)), diff --git a/backend/api/main.go b/backend/api/main.go index c8b5ea4..ae1bcf7 100644 --- a/backend/api/main.go +++ b/backend/api/main.go @@ -94,6 +94,7 @@ func main() { engine.POST("/api/v1/users/create", users.CreateUser) engine.POST("/api/v1/users/login", users.LoginUser) engine.POST("/api/v1/users/refresh", users.RefreshUserToken) + engine.POST("/api/v1/users/remove", users.RemoveUser) log.Infof("Listening on: %s", listeningAddress) err = engine.Run(listeningAddress) diff --git a/backend/api/constants/permission_nodes.go b/backend/api/permissions/permission_nodes.go similarity index 57% rename from backend/api/constants/permission_nodes.go rename to backend/api/permissions/permission_nodes.go index a62a606..812fafa 100644 --- a/backend/api/constants/permission_nodes.go +++ b/backend/api/permissions/permission_nodes.go @@ -1,4 +1,6 @@ -package constants +package permissions + +import "git.terah.dev/imterah/hermes/api/dbcore" var DefaultPermissionNodes []string = []string{ "routes.add", @@ -24,3 +26,13 @@ var DefaultPermissionNodes []string = []string{ "users.lookup", "users.edit", } + +func UserHasPermission(user *dbcore.User, node string) bool { + for _, permission := range user.Permissions { + if permission.PermissionNode == node && permission.HasPermission { + return true + } + } + + return false +} diff --git a/routes/Hermes API/Users/Refresh JWT Token.bru b/routes/Hermes API/Users/Refresh JWT Token.bru index 9d2e922..ef15e23 100644 --- a/routes/Hermes API/Users/Refresh JWT Token.bru +++ b/routes/Hermes API/Users/Refresh JWT Token.bru @@ -12,6 +12,6 @@ post { body:json { { - "token": "HMtM7zKYyVJQqCayMe2G3dcjVU6rCd9Wc+nK6qKbgJ0OZZVBIDYOyBoKES9aVjBvJeM67xgHtJXgPUFDT1jn6JxuQ2Cd1YbhYZHEvt3dCh4=" + "token": "0AqAPZ6P7RTM1DsV18R3MzFmhyoHgn8xMCO35K1zLv6U1J3mUwOUADbT9ZaRsUZRtNLn87YRqyexZzHDgbaQAg9GGLV46g9+UWJKOUgCCSA=" } } diff --git a/routes/Hermes API/Users/Remove.bru b/routes/Hermes API/Users/Remove.bru index 0bf5723..55e4ec5 100644 --- a/routes/Hermes API/Users/Remove.bru +++ b/routes/Hermes API/Users/Remove.bru @@ -5,14 +5,13 @@ meta { } post { - url: http://127.0.0.1:3000/api/v1/users/remove + url: http://127.0.0.1:8000/api/v1/users/remove body: json auth: inherit } body:json { { - "token": "5e2cb92a338a832d385790861312eb85d69f46f82317bfa984ac5e3517368ab5a827897b0f9775a9181b02fa3b9cffed7e59e5b3111d5bdc37f729156caf5f", - "uid": "2" + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiMSJdLCJleHAiOjE3MzQ4ODg4NDMsIm5iZiI6MTczNDg4ODY2MywiaWF0IjoxNzM0ODg4NjYzfQ.diHYYJaQrVAPvWpeqsriH2sbJnuCaBCAzbAL4JoYzqc" } }