package iam import ( "database/sql" "fmt" "net/http" "strings" "github.com/atlasos/calypso/internal/common/config" "github.com/atlasos/calypso/internal/common/database" "github.com/atlasos/calypso/internal/common/logger" "github.com/atlasos/calypso/internal/common/password" "github.com/gin-gonic/gin" ) // Handler handles IAM-related requests type Handler struct { db *database.DB config *config.Config logger *logger.Logger } // NewHandler creates a new IAM handler func NewHandler(db *database.DB, cfg *config.Config, log *logger.Logger) *Handler { return &Handler{ db: db, config: cfg, logger: log, } } // ListUsers lists all users func (h *Handler) ListUsers(c *gin.Context) { query := ` SELECT id, username, email, full_name, is_active, is_system, created_at, updated_at, last_login_at FROM users ORDER BY username ` rows, err := h.db.Query(query) if err != nil { h.logger.Error("Failed to list users", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list users"}) return } defer rows.Close() var users []map[string]interface{} for rows.Next() { var u struct { ID string Username string Email string FullName string IsActive bool IsSystem bool CreatedAt string UpdatedAt string LastLoginAt *string } if err := rows.Scan(&u.ID, &u.Username, &u.Email, &u.FullName, &u.IsActive, &u.IsSystem, &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt); err != nil { h.logger.Error("Failed to scan user", "error", err) continue } roles, _ := GetUserRoles(h.db, u.ID) permissions, _ := GetUserPermissions(h.db, u.ID) groups, _ := GetUserGroups(h.db, u.ID) users = append(users, map[string]interface{}{ "id": u.ID, "username": u.Username, "email": u.Email, "full_name": u.FullName, "is_active": u.IsActive, "is_system": u.IsSystem, "roles": roles, "permissions": permissions, "groups": groups, "created_at": u.CreatedAt, "updated_at": u.UpdatedAt, "last_login_at": u.LastLoginAt, }) } c.JSON(http.StatusOK, gin.H{"users": users}) } // GetUser retrieves a single user // Permission: User can view their own profile, or admin can view any profile func (h *Handler) GetUser(c *gin.Context) { userID := c.Param("id") // Get current authenticated user from context authUser, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"}) return } currentUser, ok := authUser.(*User) if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user context"}) return } // Check permission: user can view own profile, or admin can view any profile canView := false if currentUser.ID == userID { canView = true } else { // Check if current user is admin roles, err := GetUserRoles(h.db, currentUser.ID) if err == nil { for _, role := range roles { if role == "admin" { canView = true break } } } } if !canView { c.JSON(http.StatusForbidden, gin.H{"error": "insufficient permissions"}) return } user, err := GetUserByID(h.db, userID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } roles, _ := GetUserRoles(h.db, userID) permissions, _ := GetUserPermissions(h.db, userID) groups, _ := GetUserGroups(h.db, userID) c.JSON(http.StatusOK, gin.H{ "id": user.ID, "username": user.Username, "email": user.Email, "full_name": user.FullName, "is_active": user.IsActive, "is_system": user.IsSystem, "roles": roles, "permissions": permissions, "groups": groups, "created_at": user.CreatedAt, "updated_at": user.UpdatedAt, "last_login_at": user.LastLoginAt, }) } // CreateUser creates a new user func (h *Handler) CreateUser(c *gin.Context) { var req struct { Username string `json:"username" binding:"required"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` FullName string `json:"full_name"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Hash password with Argon2id passwordHash, err := password.HashPassword(req.Password, h.config.Auth.Argon2Params) if err != nil { h.logger.Error("Failed to hash password", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"}) return } query := ` INSERT INTO users (username, email, password_hash, full_name) VALUES ($1, $2, $3, $4) RETURNING id ` var userID string err = h.db.QueryRow(query, req.Username, req.Email, passwordHash, req.FullName).Scan(&userID) if err != nil { h.logger.Error("Failed to create user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"}) return } h.logger.Info("User created", "user_id", userID, "username", req.Username) c.JSON(http.StatusCreated, gin.H{"id": userID, "username": req.Username}) } // UpdateUser updates an existing user func (h *Handler) UpdateUser(c *gin.Context) { userID := c.Param("id") var req struct { Email *string `json:"email"` FullName *string `json:"full_name"` IsActive *bool `json:"is_active"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Build update query dynamically updates := []string{"updated_at = NOW()"} args := []interface{}{} argPos := 1 if req.Email != nil { updates = append(updates, fmt.Sprintf("email = $%d", argPos)) args = append(args, *req.Email) argPos++ } if req.FullName != nil { updates = append(updates, fmt.Sprintf("full_name = $%d", argPos)) args = append(args, *req.FullName) argPos++ } if req.IsActive != nil { updates = append(updates, fmt.Sprintf("is_active = $%d", argPos)) args = append(args, *req.IsActive) argPos++ } if len(updates) == 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"}) return } args = append(args, userID) query := "UPDATE users SET " + strings.Join(updates, ", ") + fmt.Sprintf(" WHERE id = $%d", argPos) _, err := h.db.Exec(query, args...) if err != nil { h.logger.Error("Failed to update user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update user"}) return } h.logger.Info("User updated", "user_id", userID) c.JSON(http.StatusOK, gin.H{"message": "user updated successfully"}) } // DeleteUser deletes a user func (h *Handler) DeleteUser(c *gin.Context) { userID := c.Param("id") // Check if user is system user var isSystem bool err := h.db.QueryRow("SELECT is_system FROM users WHERE id = $1", userID).Scan(&isSystem) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } if isSystem { c.JSON(http.StatusForbidden, gin.H{"error": "cannot delete system user"}) return } _, err = h.db.Exec("DELETE FROM users WHERE id = $1", userID) if err != nil { h.logger.Error("Failed to delete user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete user"}) return } h.logger.Info("User deleted", "user_id", userID) c.JSON(http.StatusOK, gin.H{"message": "user deleted successfully"}) } // ListGroups lists all groups func (h *Handler) ListGroups(c *gin.Context) { query := ` SELECT g.id, g.name, g.description, g.is_system, g.created_at, g.updated_at, COUNT(DISTINCT ug.user_id) as user_count, COUNT(DISTINCT gr.role_id) as role_count FROM groups g LEFT JOIN user_groups ug ON g.id = ug.group_id LEFT JOIN group_roles gr ON g.id = gr.group_id GROUP BY g.id, g.name, g.description, g.is_system, g.created_at, g.updated_at ORDER BY g.name ` rows, err := h.db.Query(query) if err != nil { h.logger.Error("Failed to list groups", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list groups"}) return } defer rows.Close() var groups []map[string]interface{} for rows.Next() { var g struct { ID string Name string Description sql.NullString IsSystem bool CreatedAt string UpdatedAt string UserCount int RoleCount int } if err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.IsSystem, &g.CreatedAt, &g.UpdatedAt, &g.UserCount, &g.RoleCount); err != nil { h.logger.Error("Failed to scan group", "error", err) continue } groups = append(groups, map[string]interface{}{ "id": g.ID, "name": g.Name, "description": g.Description.String, "is_system": g.IsSystem, "user_count": g.UserCount, "role_count": g.RoleCount, "created_at": g.CreatedAt, "updated_at": g.UpdatedAt, }) } c.JSON(http.StatusOK, gin.H{"groups": groups}) } // GetGroup retrieves a single group func (h *Handler) GetGroup(c *gin.Context) { groupID := c.Param("id") group, err := GetGroupByID(h.db, groupID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } users, _ := GetGroupUsers(h.db, groupID) roles, _ := GetGroupRoles(h.db, groupID) c.JSON(http.StatusOK, gin.H{ "id": group.ID, "name": group.Name, "description": group.Description, "is_system": group.IsSystem, "user_count": group.UserCount, "role_count": group.RoleCount, "users": users, "roles": roles, "created_at": group.CreatedAt, "updated_at": group.UpdatedAt, }) } // CreateGroup creates a new group func (h *Handler) CreateGroup(c *gin.Context) { var req struct { Name string `json:"name" binding:"required"` Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid request to create group", "error", err) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: " + err.Error()}) return } // Trim whitespace req.Name = strings.TrimSpace(req.Name) if req.Name == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) return } // Handle empty description description := strings.TrimSpace(req.Description) if description == "" { description = "" } query := ` INSERT INTO groups (name, description) VALUES ($1, $2) RETURNING id ` var groupID string err := h.db.QueryRow(query, req.Name, description).Scan(&groupID) if err != nil { // Check if it's a unique constraint violation if strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "unique constraint") { h.logger.Error("Group name already exists", "name", req.Name, "error", err) c.JSON(http.StatusConflict, gin.H{"error": "group name already exists"}) return } h.logger.Error("Failed to create group", "error", err, "name", req.Name) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create group: " + err.Error()}) return } h.logger.Info("Group created successfully", "group_id", groupID, "name", req.Name) c.JSON(http.StatusCreated, gin.H{"id": groupID, "name": req.Name}) } // UpdateGroup updates an existing group func (h *Handler) UpdateGroup(c *gin.Context) { groupID := c.Param("id") // Check if group is system group var isSystem bool err := h.db.QueryRow("SELECT is_system FROM groups WHERE id = $1", groupID).Scan(&isSystem) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } var req struct { Name string `json:"name"` Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Build update query dynamically var updates []string var args []interface{} argIndex := 1 if req.Name != "" { updates = append(updates, fmt.Sprintf("name = $%d", argIndex)) args = append(args, req.Name) argIndex++ } if req.Description != "" { updates = append(updates, fmt.Sprintf("description = $%d", argIndex)) args = append(args, req.Description) argIndex++ } if len(updates) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"}) return } updates = append(updates, "updated_at = NOW()") args = append(args, groupID) query := fmt.Sprintf("UPDATE groups SET %s WHERE id = $%d", strings.Join(updates, ", "), argIndex) _, err = h.db.Exec(query, args...) if err != nil { h.logger.Error("Failed to update group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update group"}) return } h.logger.Info("Group updated", "group_id", groupID) c.JSON(http.StatusOK, gin.H{"message": "group updated successfully"}) } // DeleteGroup deletes a group func (h *Handler) DeleteGroup(c *gin.Context) { groupID := c.Param("id") // Check if group is system group var isSystem bool err := h.db.QueryRow("SELECT is_system FROM groups WHERE id = $1", groupID).Scan(&isSystem) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } if isSystem { c.JSON(http.StatusForbidden, gin.H{"error": "cannot delete system group"}) return } _, err = h.db.Exec("DELETE FROM groups WHERE id = $1", groupID) if err != nil { h.logger.Error("Failed to delete group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete group"}) return } h.logger.Info("Group deleted", "group_id", groupID) c.JSON(http.StatusOK, gin.H{"message": "group deleted successfully"}) } // AddUserToGroup adds a user to a group func (h *Handler) AddUserToGroup(c *gin.Context) { groupID := c.Param("id") var req struct { UserID string `json:"user_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get current user ID from context authUser, _ := c.Get("user") currentUser := authUser.(*User) err := AddUserToGroup(h.db, req.UserID, groupID, currentUser.ID) if err != nil { h.logger.Error("Failed to add user to group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add user to group"}) return } h.logger.Info("User added to group", "user_id", req.UserID, "group_id", groupID) c.JSON(http.StatusOK, gin.H{"message": "user added to group successfully"}) } // RemoveUserFromGroup removes a user from a group func (h *Handler) RemoveUserFromGroup(c *gin.Context) { groupID := c.Param("id") userID := c.Param("user_id") err := RemoveUserFromGroup(h.db, userID, groupID) if err != nil { h.logger.Error("Failed to remove user from group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove user from group"}) return } h.logger.Info("User removed from group", "user_id", userID, "group_id", groupID) c.JSON(http.StatusOK, gin.H{"message": "user removed from group successfully"}) } // AssignRoleToUser assigns a role to a user func (h *Handler) AssignRoleToUser(c *gin.Context) { userID := c.Param("id") var req struct { RoleName string `json:"role_name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get role ID by name roleID, err := GetRoleIDByName(h.db, req.RoleName) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "role not found"}) return } h.logger.Error("Failed to get role", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign role"}) return } // Get current user ID from context authUser, _ := c.Get("user") currentUser := authUser.(*User) err = AddUserRole(h.db, userID, roleID, currentUser.ID) if err != nil { h.logger.Error("Failed to assign role to user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign role"}) return } h.logger.Info("Role assigned to user", "user_id", userID, "role", req.RoleName) c.JSON(http.StatusOK, gin.H{"message": "role assigned successfully"}) } // RemoveRoleFromUser removes a role from a user func (h *Handler) RemoveRoleFromUser(c *gin.Context) { userID := c.Param("id") var req struct { RoleName string `json:"role_name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get role ID by name roleID, err := GetRoleIDByName(h.db, req.RoleName) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "role not found"}) return } h.logger.Error("Failed to get role", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove role"}) return } err = RemoveUserRole(h.db, userID, roleID) if err != nil { h.logger.Error("Failed to remove role from user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove role"}) return } h.logger.Info("Role removed from user", "user_id", userID, "role", req.RoleName) c.JSON(http.StatusOK, gin.H{"message": "role removed successfully"}) } // AssignGroupToUser assigns a group to a user func (h *Handler) AssignGroupToUser(c *gin.Context) { userID := c.Param("id") var req struct { GroupName string `json:"group_name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get group ID by name group, err := GetGroupByName(h.db, req.GroupName) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } h.logger.Error("Failed to get group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign group"}) return } groupID := group.ID if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } h.logger.Error("Failed to get group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign group"}) return } // Get current user ID from context authUser, _ := c.Get("user") currentUser := authUser.(*User) err = AddUserToGroup(h.db, userID, groupID, currentUser.ID) if err != nil { h.logger.Error("Failed to assign group to user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign group"}) return } h.logger.Info("Group assigned to user", "user_id", userID, "group", req.GroupName) c.JSON(http.StatusOK, gin.H{"message": "group assigned successfully"}) } // RemoveGroupFromUser removes a group from a user func (h *Handler) RemoveGroupFromUser(c *gin.Context) { userID := c.Param("id") var req struct { GroupName string `json:"group_name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get group ID by name group, err := GetGroupByName(h.db, req.GroupName) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } h.logger.Error("Failed to get group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign group"}) return } groupID := group.ID if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } h.logger.Error("Failed to get group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove group"}) return } err = RemoveUserFromGroup(h.db, userID, groupID) if err != nil { h.logger.Error("Failed to remove group from user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove group"}) return } h.logger.Info("Group removed from user", "user_id", userID, "group", req.GroupName) c.JSON(http.StatusOK, gin.H{"message": "group removed successfully"}) }