feature: Adds user deletion support.

This commit is contained in:
greysoh 2024-12-22 12:36:55 -05:00
parent 37d0d41570
commit cee4e62f53
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
7 changed files with 169 additions and 7 deletions

View file

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

View file

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

View file

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

View file

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

View file

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