We have a role concept

This commit is contained in:
2025-12-30 08:00:04 +01:00
parent aa82b95d9e
commit bc948fcad4
13 changed files with 985 additions and 4 deletions
+194
View File
@@ -0,0 +1,194 @@
package user
import (
"bamort/database"
"bamort/logger"
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// ListUsers returns all users (admin only)
func ListUsers(c *gin.Context) {
logger.Debug("Listing all users...")
var users []User
if err := database.DB.Find(&users).Error; err != nil {
logger.Error("Failed to fetch users: %s", err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to fetch users")
return
}
// Remove password hashes from response
for i := range users {
users[i].PasswordHash = ""
users[i].ResetPwHash = nil
}
logger.Info("Successfully fetched %d users", len(users))
c.JSON(http.StatusOK, users)
}
// GetUser returns a specific user by ID (admin only, or own profile)
func GetUser(c *gin.Context) {
logger.Debug("Fetching user by ID...")
userIDParam := c.Param("id")
targetUserID, err := strconv.ParseUint(userIDParam, 10, 32)
if err != nil {
logger.Error("Invalid user ID: %s", userIDParam)
respondWithError(c, http.StatusBadRequest, "Invalid user ID")
return
}
// Get requesting user from context
requestingUserInterface, exists := c.Get("user")
if !exists {
logger.Error("User not found in context")
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
return
}
requestingUser, ok := requestingUserInterface.(*User)
if !ok {
logger.Error("Invalid user context")
respondWithError(c, http.StatusInternalServerError, "Invalid user context")
return
}
// Allow users to view their own profile, or admins to view any profile
if requestingUser.UserID != uint(targetUserID) && !requestingUser.IsAdmin() {
logger.Warn("User %s attempted to access user %d without permission", requestingUser.Username, targetUserID)
respondWithError(c, http.StatusForbidden, "Forbidden")
return
}
var user User
if err := user.FirstId(uint(targetUserID)); err != nil {
logger.Error("User not found: %d", targetUserID)
respondWithError(c, http.StatusNotFound, "User not found")
return
}
// Remove sensitive data
user.PasswordHash = ""
user.ResetPwHash = nil
logger.Info("Successfully fetched user: %s (ID: %d)", user.Username, user.UserID)
c.JSON(http.StatusOK, user)
}
// UpdateUserRole updates a user's role (admin only)
func UpdateUserRole(c *gin.Context) {
logger.Debug("Updating user role...")
userIDParam := c.Param("id")
targetUserID, err := strconv.ParseUint(userIDParam, 10, 32)
if err != nil {
logger.Error("Invalid user ID: %s", userIDParam)
respondWithError(c, http.StatusBadRequest, "Invalid user ID")
return
}
var input struct {
Role string `json:"role" binding:"required"`
}
if err := c.ShouldBindJSON(&input); err != nil {
logger.Error("Failed to parse role update data: %s", err.Error())
respondWithError(c, http.StatusBadRequest, "Role is required")
return
}
// Validate role
if !ValidateRole(input.Role) {
logger.Error("Invalid role: %s", input.Role)
respondWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid role. Must be one of: %s, %s, %s", RoleStandardUser, RoleMaintainer, RoleAdmin))
return
}
var user User
if err := user.FirstId(uint(targetUserID)); err != nil {
logger.Error("User not found: %d", targetUserID)
respondWithError(c, http.StatusNotFound, "User not found")
return
}
// Get requesting user for logging
requestingUserInterface, _ := c.Get("user")
requestingUser, _ := requestingUserInterface.(*User)
oldRole := user.Role
user.Role = input.Role
if err := user.Save(); err != nil {
logger.Error("Failed to update user role for user %s: %s", user.Username, err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to update user role")
return
}
logger.Info("User role updated: %s (ID: %d) from %s to %s by %s", user.Username, user.UserID, oldRole, user.Role, requestingUser.Username)
c.JSON(http.StatusOK, gin.H{
"message": "User role updated successfully",
"user": gin.H{
"id": user.UserID,
"username": user.Username,
"role": user.Role,
},
})
}
// DeleteUser deletes a user (admin only)
func DeleteUser(c *gin.Context) {
logger.Debug("Deleting user...")
userIDParam := c.Param("id")
targetUserID, err := strconv.ParseUint(userIDParam, 10, 32)
if err != nil {
logger.Error("Invalid user ID: %s", userIDParam)
respondWithError(c, http.StatusBadRequest, "Invalid user ID")
return
}
// Get requesting user
requestingUserInterface, exists := c.Get("user")
if !exists {
logger.Error("User not found in context")
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
return
}
requestingUser, ok := requestingUserInterface.(*User)
if !ok {
logger.Error("Invalid user context")
respondWithError(c, http.StatusInternalServerError, "Invalid user context")
return
}
// Prevent self-deletion
if requestingUser.UserID == uint(targetUserID) {
logger.Warn("User %s attempted to delete themselves", requestingUser.Username)
respondWithError(c, http.StatusBadRequest, "Cannot delete your own account")
return
}
var user User
if err := user.FirstId(uint(targetUserID)); err != nil {
logger.Error("User not found: %d", targetUserID)
respondWithError(c, http.StatusNotFound, "User not found")
return
}
if err := database.DB.Delete(&user).Error; err != nil {
logger.Error("Failed to delete user %s: %s", user.Username, err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to delete user")
return
}
logger.Info("User deleted: %s (ID: %d) by %s", user.Username, user.UserID, requestingUser.Username)
c.JSON(http.StatusOK, gin.H{
"message": "User deleted successfully",
})
}
+7
View File
@@ -60,6 +60,11 @@ func RegisterUser(c *gin.Context) {
user.PasswordHash = hex.EncodeToString(hashedPassword[:])
logger.Debug("Passwort-Hash erstellt für Benutzer: %s", user.Username)
// Set default role for new users
if user.Role == "" {
user.Role = RoleStandardUser
}
//fmt.Printf("pwdh: %s", user.PasswordHash)
if err := user.Create(); err != nil {
logger.Error("Fehler beim Erstellen des Benutzers %s: %s", user.Username, err.Error())
@@ -217,6 +222,7 @@ func AuthMiddleware() gin.HandlerFunc {
// Set user information in context
c.Set("userID", user.UserID)
c.Set("username", user.Username)
c.Set("user", user)
c.Next()
}
@@ -438,6 +444,7 @@ func GetUserProfile(c *gin.Context) {
"id": user.UserID,
"username": user.Username,
"email": user.Email,
"role": user.Role,
})
}
+65
View File
@@ -0,0 +1,65 @@
package user
import (
"net/http"
"github.com/gin-gonic/gin"
)
// RequireRole is a middleware that checks if the user has the required role
func RequireRole(requiredRole string) gin.HandlerFunc {
return func(c *gin.Context) {
// Get user from context (set by auth middleware)
userInterface, exists := c.Get("user")
if !exists {
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
c.Abort()
return
}
user, ok := userInterface.(*User)
if !ok {
respondWithError(c, http.StatusInternalServerError, "Invalid user context")
c.Abort()
return
}
// Check if user has required role
switch requiredRole {
case RoleAdmin:
if !user.IsAdmin() {
respondWithError(c, http.StatusForbidden, "Admin role required")
c.Abort()
return
}
case RoleMaintainer:
if !user.IsMaintainer() {
respondWithError(c, http.StatusForbidden, "Maintainer role required")
c.Abort()
return
}
case RoleStandardUser:
if !user.IsStandardUser() {
respondWithError(c, http.StatusForbidden, "Insufficient permissions")
c.Abort()
return
}
default:
respondWithError(c, http.StatusInternalServerError, "Invalid role requirement")
c.Abort()
return
}
c.Next()
}
}
// RequireAdmin is a convenience middleware for admin-only endpoints
func RequireAdmin() gin.HandlerFunc {
return RequireRole(RoleAdmin)
}
// RequireMaintainer is a convenience middleware for maintainer-or-higher endpoints
func RequireMaintainer() gin.HandlerFunc {
return RequireRole(RoleMaintainer)
}
+33
View File
@@ -8,11 +8,19 @@ import (
"gorm.io/gorm"
)
// Role constants
const (
RoleStandardUser = "standard"
RoleMaintainer = "maintainer"
RoleAdmin = "admin"
)
type User struct {
UserID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"unique" json:"username"`
PasswordHash string `json:"password"`
Email string `gorm:"unique" json:"email"`
Role string `gorm:"default:standard" json:"role"`
ResetPwHash *string `gorm:"index" json:"-"` // Hash für Password-Reset (wird nicht serialisiert)
ResetPwHashExpires *time.Time `json:"-"` // Ablaufzeit für Password-Reset-Hash
CreatedAt time.Time `json:"created_at"`
@@ -120,3 +128,28 @@ func (object *User) IsResetHashValid(resetHash string) bool {
}
return *object.ResetPwHash == resetHash && time.Now().Before(*object.ResetPwHashExpires)
}
// HasRole checks if the user has the specified role
func (u *User) HasRole(role string) bool {
return u.Role == role
}
// IsAdmin checks if the user is an admin
func (u *User) IsAdmin() bool {
return u.Role == RoleAdmin
}
// IsMaintainer checks if the user is a maintainer or higher
func (u *User) IsMaintainer() bool {
return u.Role == RoleMaintainer || u.Role == RoleAdmin
}
// IsStandardUser checks if the user is a standard user or higher
func (u *User) IsStandardUser() bool {
return u.Role == RoleStandardUser || u.IsMaintainer()
}
// ValidateRole checks if the given role is valid
func ValidateRole(role string) bool {
return role == RoleStandardUser || role == RoleMaintainer || role == RoleAdmin
}
+233
View File
@@ -0,0 +1,233 @@
package user
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"bamort/database"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// setupRoleTestEnvironment sets up the test environment for role tests
func setupRoleTestEnvironment(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
err := MigrateStructure()
require.NoError(t, err, "Should migrate user structure")
gin.SetMode(gin.TestMode)
}
// TestUserRoleDefaults tests that new users get standard role by default
func TestUserRoleDefaults(t *testing.T) {
setupRoleTestEnvironment(t)
user := &User{
Username: "roletest_user",
PasswordHash: "hashedpw",
Email: "roletest@example.com",
}
err := user.Create()
require.NoError(t, err, "Should create user")
assert.Equal(t, RoleStandardUser, user.Role, "New users should have standard role")
}
// TestRoleValidation tests role validation
func TestRoleValidation(t *testing.T) {
assert.True(t, ValidateRole(RoleStandardUser), "standard should be valid")
assert.True(t, ValidateRole(RoleMaintainer), "maintainer should be valid")
assert.True(t, ValidateRole(RoleAdmin), "admin should be valid")
assert.False(t, ValidateRole("invalid"), "invalid should not be valid")
assert.False(t, ValidateRole(""), "empty should not be valid")
}
// TestRoleHierarchy tests role hierarchy methods
func TestRoleHierarchy(t *testing.T) {
standardUser := &User{Role: RoleStandardUser}
maintainer := &User{Role: RoleMaintainer}
admin := &User{Role: RoleAdmin}
// Test IsStandardUser
assert.True(t, standardUser.IsStandardUser(), "standard user should pass IsStandardUser")
assert.True(t, maintainer.IsStandardUser(), "maintainer should pass IsStandardUser")
assert.True(t, admin.IsStandardUser(), "admin should pass IsStandardUser")
// Test IsMaintainer
assert.False(t, standardUser.IsMaintainer(), "standard user should not pass IsMaintainer")
assert.True(t, maintainer.IsMaintainer(), "maintainer should pass IsMaintainer")
assert.True(t, admin.IsMaintainer(), "admin should pass IsMaintainer")
// Test IsAdmin
assert.False(t, standardUser.IsAdmin(), "standard user should not pass IsAdmin")
assert.False(t, maintainer.IsAdmin(), "maintainer should not pass IsAdmin")
assert.True(t, admin.IsAdmin(), "admin should pass IsAdmin")
}
// TestListUsers tests listing all users (admin only)
func TestListUsers(t *testing.T) {
setupRoleTestEnvironment(t)
// Create test users
admin := &User{Username: "listadmin", PasswordHash: "hash", Email: "listadmin@test.com", Role: RoleAdmin}
require.NoError(t, admin.Create())
standardUser := &User{Username: "listuser", PasswordHash: "hash", Email: "listuser@test.com", Role: RoleStandardUser}
require.NoError(t, standardUser.Create())
// Test admin access
router := gin.Default()
router.GET("/users", func(c *gin.Context) {
c.Set("user", admin)
}, RequireAdmin(), ListUsers)
req, _ := http.NewRequest("GET", "/users", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, "Admin should access list")
// Test standard user access (should fail)
router2 := gin.Default()
router2.GET("/users", func(c *gin.Context) {
c.Set("user", standardUser)
}, RequireAdmin(), ListUsers)
req2, _ := http.NewRequest("GET", "/users", nil)
w2 := httptest.NewRecorder()
router2.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusForbidden, w2.Code, "Standard user should not access list")
}
// TestUpdateUserRole tests updating user role (admin only)
func TestUpdateUserRole(t *testing.T) {
setupRoleTestEnvironment(t)
// Create admin and target user
admin := &User{Username: "roleadmin", PasswordHash: "hash", Email: "roleadmin@test.com", Role: RoleAdmin}
require.NoError(t, admin.Create())
targetUser := &User{Username: "roletarget", PasswordHash: "hash", Email: "roletarget@test.com", Role: RoleStandardUser}
require.NoError(t, targetUser.Create())
// Test valid role update
router := gin.Default()
router.PUT("/users/:id/role", func(c *gin.Context) {
c.Set("user", admin)
}, RequireAdmin(), UpdateUserRole)
updateData := map[string]string{"role": RoleMaintainer}
jsonData, _ := json.Marshal(updateData)
req, _ := http.NewRequest("PUT", fmt.Sprintf("/users/%d/role", targetUser.UserID), bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, "Admin should update role")
// Verify role was updated
var updatedUser User
require.NoError(t, updatedUser.FirstId(targetUser.UserID))
assert.Equal(t, RoleMaintainer, updatedUser.Role, "Role should be updated to maintainer")
// Test invalid role update
invalidData := map[string]string{"role": "invalid"}
jsonData2, _ := json.Marshal(invalidData)
req2, _ := http.NewRequest("PUT", fmt.Sprintf("/users/%d/role", targetUser.UserID), bytes.NewBuffer(jsonData2))
req2.Header.Set("Content-Type", "application/json")
w2 := httptest.NewRecorder()
router.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusBadRequest, w2.Code, "Should reject invalid role")
}
// TestDeleteUser tests deleting a user (admin only)
func TestDeleteUser(t *testing.T) {
setupRoleTestEnvironment(t)
// Create admin and target user
admin := &User{Username: "deladmin", PasswordHash: "hash", Email: "deladmin@test.com", Role: RoleAdmin}
require.NoError(t, admin.Create())
targetUser := &User{Username: "deltarget", PasswordHash: "hash", Email: "deltarget@test.com", Role: RoleStandardUser}
require.NoError(t, targetUser.Create())
// Test deletion
router := gin.Default()
router.DELETE("/users/:id", func(c *gin.Context) {
c.Set("user", admin)
}, RequireAdmin(), DeleteUser)
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/users/%d", targetUser.UserID), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, "Admin should delete user")
// Verify user was deleted
var deletedUser User
err := deletedUser.FirstId(targetUser.UserID)
assert.Error(t, err, "User should not exist after deletion")
}
// TestMaintainerPermissions tests maintainer-specific permissions
func TestMaintainerPermissions(t *testing.T) {
setupRoleTestEnvironment(t)
standardUser := &User{Username: "permuser", PasswordHash: "hash", Email: "permuser@test.com", Role: RoleStandardUser}
require.NoError(t, standardUser.Create())
maintainer := &User{Username: "permmaintainer", PasswordHash: "hash", Email: "permmaintainer@test.com", Role: RoleMaintainer}
require.NoError(t, maintainer.Create())
admin := &User{Username: "permadmin", PasswordHash: "hash", Email: "permadmin@test.com", Role: RoleAdmin}
require.NoError(t, admin.Create())
router := gin.Default()
router.GET("/maintainer-only", func(c *gin.Context) {
c.Set("user", standardUser)
}, RequireMaintainer(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "success"})
})
// Standard user should fail
req, _ := http.NewRequest("GET", "/maintainer-only", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code, "Standard user should not access")
// Maintainer should succeed
router2 := gin.Default()
router2.GET("/maintainer-only", func(c *gin.Context) {
c.Set("user", maintainer)
}, RequireMaintainer(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "success"})
})
req2, _ := http.NewRequest("GET", "/maintainer-only", nil)
w2 := httptest.NewRecorder()
router2.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code, "Maintainer should access")
// Admin should also succeed
router3 := gin.Default()
router3.GET("/maintainer-only", func(c *gin.Context) {
c.Set("user", admin)
}, RequireMaintainer(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "success"})
})
req3, _ := http.NewRequest("GET", "/maintainer-only", nil)
w3 := httptest.NewRecorder()
router3.ServeHTTP(w3, req3)
assert.Equal(t, http.StatusOK, w3.Code, "Admin should access maintainer endpoints")
}
+10
View File
@@ -13,4 +13,14 @@ func RegisterRoutes(r *gin.RouterGroup) {
userGroup.PUT("/email", UpdateEmail)
userGroup.PUT("/password", UpdatePassword)
}
// Admin routes - require admin role
adminGroup := r.Group("/users")
adminGroup.Use(RequireAdmin())
{
adminGroup.GET("", ListUsers)
adminGroup.GET("/:id", GetUser)
adminGroup.PUT("/:id/role", UpdateUserRole)
adminGroup.DELETE("/:id", DeleteUser)
}
}