diff --git a/internal/api/auth/auth_service.go b/internal/api/auth/auth_service.go index 846e980..0431600 100644 --- a/internal/api/auth/auth_service.go +++ b/internal/api/auth/auth_service.go @@ -2,21 +2,19 @@ package auth import ( "fmt" - "slices" + "strings" "github.com/cpp-cyber/proclone/internal/ldap" - "github.com/cpp-cyber/proclone/internal/proxmox" ) -func NewAuthService(proxmoxService *proxmox.ProxmoxService) (*AuthService, error) { +func NewAuthService() (*AuthService, error) { ldapService, err := ldap.NewLDAPService() if err != nil { return nil, fmt.Errorf("failed to create LDAP service: %w", err) } return &AuthService{ - ldapService: ldapService, - proxmoxService: proxmoxService, + ldapService: ldapService, }, nil } @@ -53,36 +51,76 @@ func (s *AuthService) Authenticate(username string, password string) (bool, erro } func (s *AuthService) IsAdmin(username string) (bool, error) { - // Get user's groups from Proxmox - userGroups, err := s.proxmoxService.GetUserGroups(username) + // Input validation + if username == "" { + return false, fmt.Errorf("username cannot be empty") + } + + // Get user DN + userDN, err := s.ldapService.GetUserDN(username) + if err != nil { + return false, fmt.Errorf("failed to get user DN: %w", err) + } + + // Get user's groups + userGroups, err := s.ldapService.GetUserGroups(userDN) if err != nil { return false, fmt.Errorf("failed to get user groups: %w", err) } - // Get the admin group name from config - adminGroupName := s.proxmoxService.Config.AdminGroupName + // Load LDAP config to get admin group DN + config, err := ldap.LoadConfig() + if err != nil { + return false, fmt.Errorf("failed to load LDAP config: %w", err) + } + + if config.AdminGroupName == "" { + return false, fmt.Errorf("admin group DN not configured") + } // Check if user is in the admin group - if slices.Contains(userGroups, adminGroupName) { - return true, nil + for _, groupName := range userGroups { + if strings.EqualFold(groupName, config.AdminGroupName) { + return true, nil + } } return false, nil } func (s *AuthService) IsCreator(username string) (bool, error) { - // Get user's groups from Proxmox - userGroups, err := s.proxmoxService.GetUserGroups(username) + // Input validation + if username == "" { + return false, fmt.Errorf("username cannot be empty") + } + + // Get user DN + userDN, err := s.ldapService.GetUserDN(username) + if err != nil { + return false, fmt.Errorf("failed to get user DN: %w", err) + } + + // Get user's groups + userGroups, err := s.ldapService.GetUserGroups(userDN) if err != nil { return false, fmt.Errorf("failed to get user groups: %w", err) } - // Get the creator group name from config - creatorGroupName := s.proxmoxService.Config.CreatorGroupName + // Load LDAP config to get creator group DN + config, err := ldap.LoadConfig() + if err != nil { + return false, fmt.Errorf("failed to load LDAP config: %w", err) + } + + if config.CreatorGroupName == "" { + return false, fmt.Errorf("creator group DN not configured") + } // Check if user is in the creator group - if slices.Contains(userGroups, creatorGroupName) { - return true, nil + for _, groupName := range userGroups { + if strings.EqualFold(groupName, config.CreatorGroupName) { + return true, nil + } } return false, nil diff --git a/internal/api/auth/types.go b/internal/api/auth/types.go index 1ab6569..9ca2f36 100644 --- a/internal/api/auth/types.go +++ b/internal/api/auth/types.go @@ -2,7 +2,6 @@ package auth import ( "github.com/cpp-cyber/proclone/internal/ldap" - "github.com/cpp-cyber/proclone/internal/proxmox" ) // ================================================= @@ -21,14 +20,13 @@ type Service interface { } type AuthService struct { - ldapService ldap.Service - proxmoxService *proxmox.ProxmoxService + ldapService ldap.Service } // ================================================= // Types for Auth Service (re-exported from ldap) // ================================================= -type User = proxmox.User -type Group = proxmox.Group +type User = ldap.User +type Group = ldap.Group type UserRegistrationInfo = ldap.UserRegistrationInfo diff --git a/internal/api/handlers/auth_handler.go b/internal/api/handlers/auth_handler.go index 813d601..0df7a58 100644 --- a/internal/api/handlers/auth_handler.go +++ b/internal/api/handlers/auth_handler.go @@ -18,18 +18,7 @@ import ( // NewAuthHandler creates a new authentication handler func NewAuthHandler() (*AuthHandler, error) { - proxmoxServiceInterface, err := proxmox.NewService() - if err != nil { - return nil, fmt.Errorf("failed to create proxmox service: %w", err) - } - - // Type assert to get concrete type for auth service - proxmoxService, ok := proxmoxServiceInterface.(*proxmox.ProxmoxService) - if !ok { - return nil, fmt.Errorf("failed to convert proxmox service to concrete type") - } - - authService, err := auth.NewAuthService(proxmoxService) + authService, err := auth.NewAuthService() if err != nil { return nil, fmt.Errorf("failed to create auth service: %w", err) } @@ -39,12 +28,17 @@ func NewAuthHandler() (*AuthHandler, error) { return nil, fmt.Errorf("failed to create LDAP service: %w", err) } + proxmoxService, err := proxmox.NewService() + if err != nil { + return nil, fmt.Errorf("failed to create proxmox service: %w", err) + } + log.Println("Auth handler initialized") return &AuthHandler{ authService: authService, ldapService: ldapService, - proxmoxService: proxmoxServiceInterface, + proxmoxService: proxmoxService, }, nil } @@ -239,3 +233,272 @@ func (h *AuthHandler) DeleteUsersHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Users deleted successfully"}) } + +// ================================================= +// User Handlers +// ================================================= + +// ADMIN: GetUsersHandler returns a list of all users +func (h *AuthHandler) GetUsersHandler(c *gin.Context) { + users, err := h.ldapService.GetUsers() + if err != nil { + log.Printf("Failed to retrieve users: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve users"}) + return + } + + var adminCount = 0 + var disabledCount = 0 + var creatorCount = 0 + for _, user := range users { + if user.IsAdmin { + adminCount++ + } + if !user.Enabled { + disabledCount++ + } + if user.IsCreator { + creatorCount++ + } + } + + c.JSON(http.StatusOK, gin.H{ + "users": users, + "count": len(users), + "disabled_count": disabledCount, + "admin_count": adminCount, + "creator_count": creatorCount, + }) +} + +// ADMIN: EnableUsersHandler enables existing user(s) +func (h *AuthHandler) EnableUsersHandler(c *gin.Context) { + var req UsersRequest + if !validateAndBind(c, &req) { + return + } + + var errors []error + + for _, username := range req.Usernames { + if err := h.ldapService.EnableUserAccount(username); err != nil { + errors = append(errors, fmt.Errorf("failed to enable user %s: %v", username, err)) + } + } + + if len(errors) > 0 { + log.Printf("Failed to enable users: %v", errors) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to enable users", "details": errors}) + return + } + + // Sync users to Proxmox + if err := h.proxmoxService.SyncUsers(); err != nil { + log.Printf("Failed to sync users with Proxmox: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync users with Proxmox", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Users enabled successfully"}) +} + +// ADMIN: DisableUsersHandler disables existing user(s) +func (h *AuthHandler) DisableUsersHandler(c *gin.Context) { + var req UsersRequest + if !validateAndBind(c, &req) { + return + } + + var errors []error + + for _, username := range req.Usernames { + if err := h.ldapService.DisableUserAccount(username); err != nil { + errors = append(errors, fmt.Errorf("failed to disable user %s: %v", username, err)) + } + } + + if len(errors) > 0 { + log.Printf("Failed to disable users: %v", errors) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to disable users", "details": errors}) + return + } + + // Sync users to Proxmox + if err := h.proxmoxService.SyncUsers(); err != nil { + log.Printf("Failed to sync users with Proxmox: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync users with Proxmox", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Users disabled successfully"}) +} + +// ================================================= +// Group Handlers +// ================================================= + +// ADMIN: SetUserGroupsHandler sets the groups for an existing user +func (h *AuthHandler) SetUserGroupsHandler(c *gin.Context) { + var req SetUserGroupsRequest + if !validateAndBind(c, &req) { + return + } + + if err := h.ldapService.SetUserGroups(req.Username, req.Groups); err != nil { + log.Printf("Failed to set groups for user %s: %v", req.Username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to set user groups", "details": err.Error()}) + return + } + + // Sync groups to Proxmox + if err := h.proxmoxService.SyncGroups(); err != nil { + log.Printf("Failed to sync groups with Proxmox: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "User groups updated successfully"}) +} + +func (h *AuthHandler) GetGroupsHandler(c *gin.Context) { + groups, err := h.ldapService.GetGroups() + if err != nil { + log.Printf("Failed to retrieve groups: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve groups"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "groups": groups, + "count": len(groups), + }) +} + +// ADMIN: CreateGroupsHandler creates new group(s) +func (h *AuthHandler) CreateGroupsHandler(c *gin.Context) { + var req GroupsRequest + if !validateAndBind(c, &req) { + return + } + + var errors []error + + // Create groups in AD + for _, group := range req.Groups { + if err := h.ldapService.CreateGroup(group); err != nil { + errors = append(errors, fmt.Errorf("failed to create group %s: %v", group, err)) + } + } + + if len(errors) > 0 { + log.Printf("Failed to create groups: %v", errors) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create groups", "details": errors}) + return + } + + // Sync groups to Proxmox + if err := h.proxmoxService.SyncGroups(); err != nil { + log.Printf("Failed to sync groups with Proxmox: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{"message": "Groups created successfully"}) +} + +func (h *AuthHandler) RenameGroupHandler(c *gin.Context) { + var req RenameGroupRequest + if !validateAndBind(c, &req) { + return + } + + if err := h.ldapService.RenameGroup(req.OldName, req.NewName); err != nil { + log.Printf("Failed to rename group %s to %s: %v", req.OldName, req.NewName, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to rename group", "details": err.Error()}) + return + } + + // Sync groups to Proxmox + if err := h.proxmoxService.SyncGroups(); err != nil { + log.Printf("Failed to sync groups with Proxmox: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Group renamed successfully"}) +} + +func (h *AuthHandler) DeleteGroupsHandler(c *gin.Context) { + var req GroupsRequest + if !validateAndBind(c, &req) { + return + } + + var errors []error + + // Delete groups in AD + for _, group := range req.Groups { + if err := h.ldapService.DeleteGroup(group); err != nil { + errors = append(errors, fmt.Errorf("failed to delete group %s: %v", group, err)) + } + } + + if len(errors) > 0 { + log.Printf("Failed to delete groups: %v", errors) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete groups", "details": errors}) + return + } + + // Sync groups to Proxmox + if err := h.proxmoxService.SyncGroups(); err != nil { + log.Printf("Failed to sync groups with Proxmox: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Groups deleted successfully"}) +} + +func (h *AuthHandler) AddUsersHandler(c *gin.Context) { + var req ModifyGroupMembersRequest + if !validateAndBind(c, &req) { + return + } + + if err := h.ldapService.AddUsersToGroup(req.Group, req.Usernames); err != nil { + log.Printf("Failed to add users to group %s: %v", req.Group, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add users to group", "details": err.Error()}) + return + } + + // Sync groups to Proxmox + if err := h.proxmoxService.SyncGroups(); err != nil { + log.Printf("Failed to sync groups with Proxmox: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Users added to group successfully"}) +} + +func (h *AuthHandler) RemoveUsersHandler(c *gin.Context) { + var req ModifyGroupMembersRequest + if !validateAndBind(c, &req) { + return + } + + if err := h.ldapService.RemoveUsersFromGroup(req.Group, req.Usernames); err != nil { + log.Printf("Failed to remove users from group %s: %v", req.Group, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove users from group", "details": err.Error()}) + return + } + + // Sync groups to Proxmox + if err := h.proxmoxService.SyncGroups(); err != nil { + log.Printf("Failed to sync groups with Proxmox: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync groups with Proxmox", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Users removed from group successfully"}) +} diff --git a/internal/api/handlers/dashboard_handler.go b/internal/api/handlers/dashboard_handler.go index f80939b..62321b5 100644 --- a/internal/api/handlers/dashboard_handler.go +++ b/internal/api/handlers/dashboard_handler.go @@ -24,7 +24,7 @@ func (dh *DashboardHandler) GetAdminDashboardStatsHandler(c *gin.Context) { stats := DashboardStats{} // Get user count - users, err := dh.proxmoxHandler.service.GetUsers() + users, err := dh.authHandler.ldapService.GetUsers() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve user count", "details": err.Error()}) return @@ -32,7 +32,7 @@ func (dh *DashboardHandler) GetAdminDashboardStatsHandler(c *gin.Context) { stats.UserCount = len(users) // Get group count - groups, err := dh.proxmoxHandler.service.GetGroups() + groups, err := dh.authHandler.ldapService.GetGroups() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve group count", "details": err.Error()}) return @@ -102,7 +102,7 @@ func (dh *DashboardHandler) GetUserDashboardStatsHandler(c *gin.Context) { } // Get user's information - userInfo, err := dh.authHandler.proxmoxService.GetUser(username) + userInfo, err := dh.authHandler.ldapService.GetUser(username) if err != nil { log.Printf("Error retrieving user info for %s: %v", username, err) c.JSON(http.StatusInternalServerError, gin.H{ diff --git a/internal/api/handlers/proxmox_handler.go b/internal/api/handlers/proxmox_handler.go index 0c3e581..f18da35 100644 --- a/internal/api/handlers/proxmox_handler.go +++ b/internal/api/handlers/proxmox_handler.go @@ -99,161 +99,6 @@ func (ph *ProxmoxHandler) RebootVMHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "VM rebooted"}) } -// ADMIN: GetUsersHandler handles GET requests for retrieving all users from Proxmox -func (ph *ProxmoxHandler) GetUsersHandler(c *gin.Context) { - users, err := ph.service.GetUsers() - if err != nil { - log.Printf("Error retrieving users: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve users", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "count": len(users), - "users": users, - }) -} - -// ADMIN: GetGroupsHandler handles GET requests for retrieving all groups from Proxmox -func (ph *ProxmoxHandler) GetGroupsHandler(c *gin.Context) { - groups, err := ph.service.GetGroups() - if err != nil { - log.Printf("Error retrieving groups: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve groups", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "count": len(groups), - "groups": groups, - }) -} - -// ADMIN: SetUserGroupsHandler handles POST requests for setting user groups in Proxmox -func (ph *ProxmoxHandler) SetUserGroupsHandler(c *gin.Context) { - var req SetUserGroupsRequest - if !validateAndBind(c, &req) { - return - } - - if err := ph.service.SetUserGroups(req.Username, req.Groups); err != nil { - log.Printf("Error setting groups for user %s: %v", req.Username, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to set user groups", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"status": "User groups updated"}) -} - -// ADMIN: CreateGroupsHandler handles POST requests for creating groups in Proxmox -func (ph *ProxmoxHandler) CreateGroupsHandler(c *gin.Context) { - var req GroupsRequest - if !validateAndBind(c, &req) { - return - } - - var errors []string - var created []string - - for _, groupName := range req.Groups { - if err := ph.service.CreateGroup(groupName, ""); err != nil { - log.Printf("Error creating group %s: %v", groupName, err) - errors = append(errors, fmt.Sprintf("Failed to create group %s: %v", groupName, err)) - } else { - created = append(created, groupName) - } - } - - if len(errors) > 0 { - c.JSON(http.StatusPartialContent, gin.H{ - "status": "Partial success", - "created": created, - "errors": errors, - }) - return - } - - c.JSON(http.StatusOK, gin.H{"status": "Groups created", "created": created}) -} - -// ADMIN: AddUsersHandler handles POST requests for adding users to a group in Proxmox -func (ph *ProxmoxHandler) AddUsersHandler(c *gin.Context) { - var req ModifyGroupMembersRequest - if !validateAndBind(c, &req) { - return - } - - if err := ph.service.AddUsersToGroup(req.Group, req.Usernames); err != nil { - log.Printf("Error adding users to group %s: %v", req.Group, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add users to group", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"status": "Users added to group"}) -} - -// ADMIN: RemoveUsersHandler handles POST requests for removing users from a group in Proxmox -func (ph *ProxmoxHandler) RemoveUsersHandler(c *gin.Context) { - var req ModifyGroupMembersRequest - if !validateAndBind(c, &req) { - return - } - - if err := ph.service.RemoveUsersFromGroup(req.Group, req.Usernames); err != nil { - log.Printf("Error removing users from group %s: %v", req.Group, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove users from group", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"status": "Users removed from group"}) -} - -// ADMIN: DeleteGroupsHandler handles POST requests for deleting groups in Proxmox -func (ph *ProxmoxHandler) DeleteGroupsHandler(c *gin.Context) { - var req GroupsRequest - if !validateAndBind(c, &req) { - return - } - - var errors []string - var deleted []string - - for _, groupName := range req.Groups { - if err := ph.service.DeleteGroup(groupName); err != nil { - log.Printf("Error deleting group %s: %v", groupName, err) - errors = append(errors, fmt.Sprintf("Failed to delete group %s: %v", groupName, err)) - } else { - deleted = append(deleted, groupName) - } - } - - if len(errors) > 0 { - c.JSON(http.StatusPartialContent, gin.H{ - "status": "Partial success", - "deleted": deleted, - "errors": errors, - }) - return - } - - c.JSON(http.StatusOK, gin.H{"status": "Groups deleted", "deleted": deleted}) -} - -func (ph *ProxmoxHandler) EditGroupHandler(c *gin.Context) { - var req EditGroupRequest - if !validateAndBind(c, &req) { - return - } - - if err := ph.service.EditGroup(req.Group, req.Comment); err != nil { - log.Printf("Error editing group %s: %v", req.Group, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to edit group", "details": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{"status": "Group edited"}) -} - func (ph *ProxmoxHandler) GetVMTemplatesHandler(c *gin.Context) { vmTemplates, err := ph.service.GetVMTemplates() if err != nil { diff --git a/internal/api/handlers/types.go b/internal/api/handlers/types.go index fc7048c..7548f7f 100644 --- a/internal/api/handlers/types.go +++ b/internal/api/handlers/types.go @@ -108,6 +108,11 @@ type EditGroupRequest struct { Comment string `json:"comment"` } +type RenameGroupRequest struct { + OldName string `json:"old_name" binding:"required,min=1,max=100" validate:"alphanum,ascii"` + NewName string `json:"new_name" binding:"required,min=1,max=100" validate:"alphanum,ascii"` +} + type DashboardStats struct { UserCount int `json:"users"` GroupCount int `json:"groups"` diff --git a/internal/api/routes/admin_routes.go b/internal/api/routes/admin_routes.go index 07bf126..b7c3499 100644 --- a/internal/api/routes/admin_routes.go +++ b/internal/api/routes/admin_routes.go @@ -16,18 +16,20 @@ func registerAdminRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler, g.GET("/pods", cloningHandler.AdminGetPodsHandler) // User management (admin only) - g.GET("/users", proxmoxHandler.GetUsersHandler) + g.GET("/users", authHandler.GetUsersHandler) g.POST("/users/create", authHandler.CreateUsersHandler) g.POST("/users/delete", authHandler.DeleteUsersHandler) - g.POST("/user/groups", proxmoxHandler.SetUserGroupsHandler) + g.POST("/users/enable", authHandler.EnableUsersHandler) + g.POST("/users/disable", authHandler.DisableUsersHandler) + g.POST("/user/groups", authHandler.SetUserGroupsHandler) // Group management (admin only) - g.GET("/groups", proxmoxHandler.GetGroupsHandler) - g.POST("/groups/create", proxmoxHandler.CreateGroupsHandler) - g.POST("/group/members/add", proxmoxHandler.AddUsersHandler) - g.POST("/group/members/remove", proxmoxHandler.RemoveUsersHandler) - g.POST("/group/edit", proxmoxHandler.EditGroupHandler) - g.POST("/groups/delete", proxmoxHandler.DeleteGroupsHandler) + g.GET("/groups", authHandler.GetGroupsHandler) + g.POST("/groups/create", authHandler.CreateGroupsHandler) + g.POST("/group/members/add", authHandler.AddUsersHandler) + g.POST("/group/members/remove", authHandler.RemoveUsersHandler) + g.POST("/group/rename", authHandler.RenameGroupHandler) + g.POST("/groups/delete", authHandler.DeleteGroupsHandler) // VM management (admin only) g.POST("/vm/start", proxmoxHandler.StartVMHandler) diff --git a/internal/cloning/pods.go b/internal/cloning/pods.go index 369b84a..0ef5880 100644 --- a/internal/cloning/pods.go +++ b/internal/cloning/pods.go @@ -9,8 +9,14 @@ import ( ) func (cs *CloningService) GetPods(username string) ([]Pod, error) { + // Get User DN + userDN, err := cs.LDAPService.GetUserDN(username) + if err != nil { + return nil, fmt.Errorf("failed to get user DN: %w", err) + } + // Get user's groups - groups, err := cs.ProxmoxService.GetUserGroups(username) + groups, err := cs.LDAPService.GetUserGroups(userDN) if err != nil { return nil, fmt.Errorf("failed to get user groups: %w", err) } diff --git a/internal/ldap/groups.go b/internal/ldap/groups.go new file mode 100644 index 0000000..5f7359d --- /dev/null +++ b/internal/ldap/groups.go @@ -0,0 +1,326 @@ +package ldap + +import ( + "fmt" + "regexp" + "strings" + "time" + + ldapv3 "github.com/go-ldap/ldap/v3" +) + +// ================================================= +// Public Functions +// ================================================= + +func (s *LDAPService) GetGroups() ([]Group, error) { + // Search for all groups in the KaminoGroups OU + kaminoGroupsOU := "OU=KaminoGroups," + s.client.config.BaseDN + req := ldapv3.NewSearchRequest( + kaminoGroupsOU, + ldapv3.ScopeWholeSubtree, ldapv3.NeverDerefAliases, 0, 0, false, + "(objectClass=group)", + []string{"cn", "whenCreated", "member"}, + nil, + ) + + searchResult, err := s.client.Search(req) + if err != nil { + return nil, fmt.Errorf("failed to search for groups: %v", err) + } + + var groups []Group + for _, entry := range searchResult.Entries { + cn := entry.GetAttributeValue("cn") + + // Check if the group is protected + protectedGroup, err := isProtectedGroup(cn) + if err != nil { + return nil, fmt.Errorf("failed to determine if the group %s is protected: %v", cn, err) + } + + group := Group{ + Name: cn, + CanModify: !protectedGroup, + UserCount: len(entry.GetAttributeValues("member")), + } + + // Add creation date if available and convert it + whenCreated := entry.GetAttributeValue("whenCreated") + if whenCreated != "" { + // AD stores dates in GeneralizedTime format: YYYYMMDDHHMMSS.0Z + if parsedTime, err := time.Parse("20060102150405.0Z", whenCreated); err == nil { + group.CreatedAt = parsedTime.Format("2006-01-02 15:04:05") + } + } + + groups = append(groups, group) + } + + return groups, nil +} + +func (s *LDAPService) CreateGroup(groupName string) error { + // Validate group name + if err := validateGroupName(groupName); err != nil { + return fmt.Errorf("invalid group name: %v", err) + } + + // Check if group already exists + _, err := s.getGroupDN(groupName) + if err == nil { + return fmt.Errorf("group already exists: %s", groupName) + } + + // Construct the DN for the new group + groupDN := fmt.Sprintf("CN=%s,OU=KaminoGroups,%s", groupName, s.client.config.BaseDN) + + // Create the add request + addReq := ldapv3.NewAddRequest(groupDN, nil) + addReq.Attribute("objectClass", []string{"top", "group"}) + addReq.Attribute("cn", []string{groupName}) + addReq.Attribute("sAMAccountName", []string{groupName}) + addReq.Attribute("groupType", []string{"-2147483646"}) + + // Execute the add request + err = s.client.Add(addReq) + if err != nil { + return fmt.Errorf("failed to create group: %v", err) + } + + return nil +} + +func (s *LDAPService) RenameGroup(oldGroupName string, newGroupName string) error { + // Validate new group name + if err := validateGroupName(newGroupName); err != nil { + return fmt.Errorf("invalid new group name: %v", err) + } + + // Check if old group exists + oldGroupDN, err := s.getGroupDN(oldGroupName) + if err != nil { + return fmt.Errorf("old group not found: %v", err) + } + + // Check if new group already exists + _, err = s.getGroupDN(newGroupName) + if err == nil { + return fmt.Errorf("new group name already exists: %s", newGroupName) + } + + // Create modify DN request + newRDN := fmt.Sprintf("CN=%s", newGroupName) + modifyDNReq := ldapv3.NewModifyDNRequest(oldGroupDN, newRDN, true, "") + + // Execute the modify DN request + err = s.client.ModifyDN(modifyDNReq) + if err != nil { + return fmt.Errorf("failed to rename group: %v", err) + } + + return nil +} + +func (s *LDAPService) DeleteGroup(groupName string) error { + // Check if group is protected + protected, err := isProtectedGroup(groupName) + if err != nil { + return fmt.Errorf("failed to check if group is protected: %v", err) + } + if protected { + return fmt.Errorf("cannot delete protected group: %s", groupName) + } + + // Get group DN + groupDN, err := s.getGroupDN(groupName) + if err != nil { + return fmt.Errorf("group not found: %v", err) + } + + // Create delete request + delReq := ldapv3.NewDelRequest(groupDN, nil) + + // Execute the delete request + err = s.client.Del(delReq) + if err != nil { + return fmt.Errorf("failed to delete group: %v", err) + } + + return nil +} + +func (s *LDAPService) GetGroupMembers(groupName string) ([]User, error) { + groupDN, err := s.getGroupDN(groupName) + if err != nil { + return nil, fmt.Errorf("group not found: %v", err) + } + + // Search for the group and get its members + req := ldapv3.NewSearchRequest( + groupDN, + ldapv3.ScopeBaseObject, ldapv3.NeverDerefAliases, 0, 0, false, + "(objectClass=group)", + []string{"member"}, + nil, + ) + + searchResult, err := s.client.Search(req) + if err != nil { + return nil, fmt.Errorf("failed to search for group: %v", err) + } + + if len(searchResult.Entries) == 0 { + return []User{}, nil + } + + memberDNs := searchResult.Entries[0].GetAttributeValues("member") + var users []User + + for _, memberDN := range memberDNs { + // Get user details from DN + userReq := ldapv3.NewSearchRequest( + memberDN, + ldapv3.ScopeBaseObject, ldapv3.NeverDerefAliases, 0, 0, false, + "(objectClass=user)", + []string{"sAMAccountName", "cn", "whenCreated", "userAccountControl"}, + nil, + ) + + userResult, err := s.client.Search(userReq) + if err != nil { + continue // Skip this user if there's an error + } + + if len(userResult.Entries) > 0 { + entry := userResult.Entries[0] + user := User{ + Name: entry.GetAttributeValue("sAMAccountName"), + CreatedAt: entry.GetAttributeValue("whenCreated"), + Enabled: true, // Default, will be updated based on userAccountControl + } + + // Check if user is enabled + userAccountControl := entry.GetAttributeValue("userAccountControl") + if userAccountControl != "" { + // Parse userAccountControl to determine if account is enabled + // UF_ACCOUNTDISABLE = 0x02 + if strings.Contains(userAccountControl, "2") { + user.Enabled = false + } + } + + users = append(users, user) + } + } + + return users, nil +} + +func (s *LDAPService) AddUsersToGroup(groupName string, usernames []string) error { + groupDN, err := s.getGroupDN(groupName) + if err != nil { + return fmt.Errorf("group not found: %v", err) + } + + // Add users one by one to handle cases where some users might already be in the group + for _, username := range usernames { + userDN, err := s.GetUserDN(username) + if err != nil { + return fmt.Errorf("user %s not found: %v", username, err) + } + + if err := s.AddToGroup(userDN, groupDN); err != nil { + return fmt.Errorf("failed to add user %s to group: %v", username, err) + } + } + + return nil +} + +func (s *LDAPService) RemoveUsersFromGroup(groupName string, usernames []string) error { + groupDN, err := s.getGroupDN(groupName) + if err != nil { + return fmt.Errorf("group not found: %v", err) + } + + // Remove users one by one to handle cases where some users might not be in the group + for _, username := range usernames { + userDN, err := s.GetUserDN(username) + if err != nil { + return fmt.Errorf("user %s not found: %v", username, err) + } + + if err := s.RemoveFromGroup(userDN, groupDN); err != nil { + return fmt.Errorf("failed to remove user %s from group: %v", username, err) + } + } + + return nil +} + +// ================================================= +// Private Functions +// ================================================= + +func (s *LDAPService) getGroupDN(groupName string) (string, error) { + kaminoGroupsOU := "OU=KaminoGroups," + s.client.config.BaseDN + req := ldapv3.NewSearchRequest( + kaminoGroupsOU, + ldapv3.ScopeWholeSubtree, ldapv3.NeverDerefAliases, 1, 30, false, + fmt.Sprintf("(&(objectClass=group)(cn=%s))", ldapv3.EscapeFilter(groupName)), + []string{"dn"}, + nil, + ) + + searchResult, err := s.client.Search(req) + if err != nil { + return "", fmt.Errorf("failed to search for group: %v", err) + } + + if len(searchResult.Entries) == 0 { + return "", fmt.Errorf("group %s not found", groupName) + } + + return searchResult.Entries[0].DN, nil +} + +func validateGroupName(groupName string) error { + if groupName == "" { + return fmt.Errorf("group name cannot be empty") + } + + if len(groupName) >= 64 { + return fmt.Errorf("group name must be less than 64 characters") + } + + regex := regexp.MustCompile("^[a-zA-Z0-9-_]*$") + if !regex.MatchString(groupName) { + return fmt.Errorf("group name must only contain letters, numbers, hyphens, and underscores") + } + + return nil +} + +func isProtectedGroup(groupName string) (bool, error) { + protectedGroups := []string{ + "Domain Admins", + "Domain Users", + "Domain Guests", + "Schema Admins", + "Enterprise Admins", + "Administrators", + "Users", + "Guests", + "Proxmox-Admins", + "KaminoUsers", + } + + for _, protectedGroup := range protectedGroups { + if strings.EqualFold(groupName, protectedGroup) { + return true, nil + } + } + + return false, nil +} diff --git a/internal/ldap/types.go b/internal/ldap/types.go index 93e07ad..6f7b205 100644 --- a/internal/ldap/types.go +++ b/internal/ldap/types.go @@ -12,10 +12,27 @@ import ( type Service interface { // User Management + GetUsers() ([]User, error) + GetUser(username string) (*User, error) CreateAndRegisterUser(userInfo UserRegistrationInfo) error DeleteUser(username string) error + AddUserToGroup(username string, groupName string) error + SetUserGroups(username string, groups []string) error + EnableUserAccount(username string) error + DisableUserAccount(username string) error + GetUserGroups(userDN string) ([]string, error) GetUserDN(username string) (string, error) + // Group Management + CreateGroup(groupName string) error + GetGroups() ([]Group, error) + RenameGroup(oldGroupName string, newGroupName string) error + DeleteGroup(groupName string) error + GetGroupMembers(groupName string) ([]User, error) + RemoveUserFromGroup(username string, groupName string) error + AddUsersToGroup(groupName string, usernames []string) error + RemoveUsersFromGroup(groupName string, usernames []string) error + // Connection Management HealthCheck() error Reconnect() error @@ -31,11 +48,13 @@ type LDAPService struct { // ================================================= type Config struct { - URL string `envconfig:"LDAP_URL" default:"ldaps://localhost:636"` - BindUser string `envconfig:"LDAP_BIND_USER"` - BindPassword string `envconfig:"LDAP_BIND_PASSWORD"` - SkipTLSVerify bool `envconfig:"LDAP_SKIP_TLS_VERIFY" default:"false"` - BaseDN string `envconfig:"LDAP_BASE_DN"` + URL string `envconfig:"LDAP_URL" default:"ldaps://localhost:636"` + BindUser string `envconfig:"LDAP_BIND_USER"` + BindPassword string `envconfig:"LDAP_BIND_PASSWORD"` + SkipTLSVerify bool `envconfig:"LDAP_SKIP_TLS_VERIFY" default:"false"` + AdminGroupName string `envconfig:"LDAP_ADMIN_GROUP_NAME"` + CreatorGroupName string `envconfig:"LDAP_CREATOR_GROUP_NAME"` + BaseDN string `envconfig:"LDAP_BASE_DN"` } type Client struct { @@ -53,10 +72,26 @@ type CreateRequest struct { Group string `json:"group"` } +type Group struct { + Name string `json:"name"` + CanModify bool `json:"can_modify"` + CreatedAt string `json:"created_at,omitempty"` + UserCount int `json:"user_count,omitempty"` +} + // ================================================= // Users // ================================================= +type User struct { + Name string `json:"name"` + CreatedAt string `json:"created_at"` + Enabled bool `json:"enabled"` + IsAdmin bool `json:"is_admin"` + IsCreator bool `json:"is_creator"` + Groups []Group `json:"groups"` +} + type UserRegistrationInfo struct { Username string `json:"username" validate:"required,min=1,max=20"` Password string `json:"password" validate:"required,min=8,max=128"` diff --git a/internal/ldap/users.go b/internal/ldap/users.go index 16722d6..9cc3cea 100644 --- a/internal/ldap/users.go +++ b/internal/ldap/users.go @@ -4,7 +4,9 @@ import ( "encoding/binary" "fmt" "regexp" + "strconv" "strings" + "time" "unicode/utf16" ldapv3 "github.com/go-ldap/ldap/v3" @@ -14,6 +16,128 @@ import ( // Public Functions // ================================================= +func (s *LDAPService) GetUsers() ([]User, error) { + kaminoUsersGroupDN := "CN=KaminoUsers,OU=KaminoGroups," + s.client.config.BaseDN + searchRequest := ldapv3.NewSearchRequest( + s.client.config.BaseDN, ldapv3.ScopeWholeSubtree, ldapv3.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=user)(sAMAccountName=*)(memberOf=%s))", kaminoUsersGroupDN), // Filter for users in KaminoUsers group + []string{"sAMAccountName", "dn", "whenCreated", "memberOf", "userAccountControl"}, // Attributes to retrieve + nil, + ) + + searchResult, err := s.client.Search(searchRequest) + if err != nil { + return nil, fmt.Errorf("failed to search for users: %v", err) + } + + var users = []User{} + for _, entry := range searchResult.Entries { + user := User{ + Name: entry.GetAttributeValue("sAMAccountName"), + } + + whenCreated := entry.GetAttributeValue("whenCreated") + if whenCreated != "" { + // AD stores dates in GeneralizedTime format: YYYYMMDDHHMMSS.0Z + if parsedTime, err := time.Parse("20060102150405.0Z", whenCreated); err == nil { + user.CreatedAt = parsedTime.Format("2006-01-02 15:04:05") + } + } + + // Check if user is enabled + userAccountControl := entry.GetAttributeValue("userAccountControl") + if userAccountControl != "" { + uac, err := strconv.Atoi(userAccountControl) + if err == nil { + // UF_ACCOUNTDISABLE = 0x02 + user.Enabled = (uac & 0x02) == 0 + } + } + + // Check if user is admin or creator + memberOfValues := entry.GetAttributeValues("memberOf") + for _, memberOf := range memberOfValues { + if strings.Contains(memberOf, s.client.config.AdminGroupName) { + user.IsAdmin = true + } + if strings.Contains(memberOf, s.client.config.CreatorGroupName) { + user.IsCreator = true + } + } + + // Get user groups + groups, err := getUserGroupsFromMemberOf(memberOfValues) + if err == nil { + user.Groups = groups + } + + users = append(users, user) + } + + return users, nil +} + +func (s *LDAPService) GetUser(username string) (*User, error) { + kaminoUsersGroupDN := "CN=KaminoUsers,OU=KaminoGroups," + s.client.config.BaseDN + searchRequest := ldapv3.NewSearchRequest( + s.client.config.BaseDN, ldapv3.ScopeWholeSubtree, ldapv3.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=user)(sAMAccountName=%s)(memberOf=%s))", username, kaminoUsersGroupDN), // Filter for specific user in KaminoUsers group + []string{"sAMAccountName", "dn", "whenCreated", "memberOf", "userAccountControl"}, // Attributes to retrieve + nil, + ) + + searchResult, err := s.client.Search(searchRequest) + if err != nil { + return nil, fmt.Errorf("failed to search for user: %v", err) + } + + if len(searchResult.Entries) == 0 { + return nil, fmt.Errorf("user '%s' not found", username) + } + + entry := searchResult.Entries[0] + user := User{ + Name: entry.GetAttributeValue("sAMAccountName"), + } + + whenCreated := entry.GetAttributeValue("whenCreated") + if whenCreated != "" { + // AD stores dates in GeneralizedTime format: YYYYMMDDHHMMSS.0Z + if parsedTime, err := time.Parse("20060102150405.0Z", whenCreated); err == nil { + user.CreatedAt = parsedTime.Format("2006-01-02 15:04:05") + } + } + + // Check if user is enabled + userAccountControl := entry.GetAttributeValue("userAccountControl") + if userAccountControl != "" { + uac, err := strconv.Atoi(userAccountControl) + if err == nil { + // UF_ACCOUNTDISABLE = 0x02 + user.Enabled = (uac & 0x02) == 0 + } + } + + // Check if user is admin or creator + memberOfValues := entry.GetAttributeValues("memberOf") + for _, memberOf := range memberOfValues { + if strings.Contains(memberOf, s.client.config.AdminGroupName) { + user.IsAdmin = true + } + if strings.Contains(memberOf, s.client.config.CreatorGroupName) { + user.IsCreator = true + } + } + + // Get user groups + groups, err := getUserGroupsFromMemberOf(memberOfValues) + if err == nil { + user.Groups = groups + } + + return &user, nil +} + func (s *LDAPService) CreateUser(userInfo UserRegistrationInfo) (string, error) { // Create DN for new user in Users container // TODO: Static @@ -71,6 +195,54 @@ func (s *LDAPService) EnableUserAccountByDN(userDN string) error { return nil } +// DisableUserAccountByDN disables a user account by DN +func (s *LDAPService) DisableUserAccountByDN(userDN string) error { + modifyRequest := ldapv3.NewModifyRequest(userDN, nil) + modifyRequest.Replace("userAccountControl", []string{"514"}) // Disabled account + + err := s.client.Modify(modifyRequest) + if err != nil { + return fmt.Errorf("failed to disable user account: %v", err) + } + + return nil +} + +func (s *LDAPService) AddToGroup(userDN string, groupDN string) error { + modifyRequest := ldapv3.NewModifyRequest(groupDN, nil) + modifyRequest.Add("member", []string{userDN}) + + err := s.client.Modify(modifyRequest) + if err != nil { + // Check if the error is because the user is already in the group + if strings.Contains(strings.ToLower(err.Error()), "already exists") || + strings.Contains(strings.ToLower(err.Error()), "attribute or value exists") { + return nil // Not an error if user is already in group + } + return fmt.Errorf("failed to add user to group: %v", err) + } + + return nil +} + +func (s *LDAPService) RemoveFromGroup(userDN string, groupDN string) error { + modifyRequest := ldapv3.NewModifyRequest(groupDN, nil) + modifyRequest.Delete("member", []string{userDN}) + + err := s.client.Modify(modifyRequest) + if err != nil { + // Check if the error is because the user is not in the group + if strings.Contains(strings.ToLower(err.Error()), "no such attribute") || + strings.Contains(strings.ToLower(err.Error()), "unwilling to perform") || + strings.Contains(strings.ToLower(err.Error()), "no such object") { + return nil // Not an error if user is not in group + } + return fmt.Errorf("failed to remove user from group: %v", err) + } + + return nil +} + func (s *LDAPService) CreateAndRegisterUser(userInfo UserRegistrationInfo) error { // Validate username if !isValidUsername(userInfo.Username) { @@ -102,6 +274,49 @@ func (s *LDAPService) CreateAndRegisterUser(userInfo UserRegistrationInfo) error return fmt.Errorf("failed to enable user account: %v", err) } + // Add user to KaminoUsers group + kaminoUsersGroupDN := "CN=KaminoUsers,OU=KaminoGroups," + s.client.config.BaseDN + err = s.AddToGroup(userDN, kaminoUsersGroupDN) + if err != nil { + return fmt.Errorf("failed to add user to KaminoUsers group: %v", err) + } + + return nil +} + +func (s *LDAPService) AddUserToGroup(username string, groupName string) error { + userDN, err := s.GetUserDN(username) + if err != nil { + return fmt.Errorf("failed to get user DN: %v", err) + } + + groupDN, err := s.getGroupDN(groupName) + if err != nil { + return fmt.Errorf("failed to get group DN: %v", err) + } + + return s.AddToGroup(userDN, groupDN) +} + +func (s *LDAPService) RemoveUserFromGroup(username string, groupName string) error { + userDN, err := s.GetUserDN(username) + if err != nil { + return fmt.Errorf("failed to get user DN: %v", err) + } + + groupDN, err := s.getGroupDN(groupName) + if err != nil { + return fmt.Errorf("failed to get group DN: %v", err) + } + + modifyRequest := ldapv3.NewModifyRequest(groupDN, nil) + modifyRequest.Delete("member", []string{userDN}) + + err = s.client.Modify(modifyRequest) + if err != nil { + return fmt.Errorf("failed to remove user from group: %v", err) + } + return nil } @@ -131,10 +346,108 @@ func (s *LDAPService) DeleteUsers(usernames []string) []error { return errors } +func (s *LDAPService) GetUserGroups(userDN string) ([]string, error) { + searchRequest := ldapv3.NewSearchRequest( + userDN, + ldapv3.ScopeBaseObject, + ldapv3.NeverDerefAliases, + 1, + 30, + false, + "(objectClass=*)", + []string{"memberOf"}, + nil, + ) + + searchResult, err := s.client.Search(searchRequest) + if err != nil { + return nil, fmt.Errorf("failed to search for user groups: %v", err) + } + + if len(searchResult.Entries) == 0 { + return []string{}, nil + } + + memberOfValues := searchResult.Entries[0].GetAttributeValues("memberOf") + var groups []string + for _, memberOf := range memberOfValues { + // Extract CN from DN + parts := strings.Split(memberOf, ",") + if len(parts) > 0 && strings.HasPrefix(parts[0], "CN=") { + groupName := strings.TrimPrefix(parts[0], "CN=") + groups = append(groups, groupName) + } + } + + return groups, nil +} + +func (s *LDAPService) EnableUserAccount(username string) error { + userDN, err := s.GetUserDN(username) + if err != nil { + return fmt.Errorf("failed to get user DN: %v", err) + } + + return s.EnableUserAccountByDN(userDN) +} + +func (s *LDAPService) DisableUserAccount(username string) error { + userDN, err := s.GetUserDN(username) + if err != nil { + return fmt.Errorf("failed to get user DN: %v", err) + } + + return s.DisableUserAccountByDN(userDN) +} + +func (s *LDAPService) SetUserGroups(username string, groups []string) error { + userDN, err := s.GetUserDN(username) + if err != nil { + return fmt.Errorf("failed to get user DN: %v", err) + } + + // Get current groups + currentGroups, err := s.GetUserGroups(userDN) + if err != nil { + return fmt.Errorf("failed to get current user groups: %v", err) + } + + // Remove from current groups + for _, group := range currentGroups { + err = s.RemoveUserFromGroup(username, group) + if err != nil { + return fmt.Errorf("failed to remove user from group %s: %v", group, err) + } + } + + // Add to new groups + for _, group := range groups { + err = s.AddUserToGroup(username, group) + if err != nil { + return fmt.Errorf("failed to add user to group %s: %v", group, err) + } + } + + return nil +} + // ================================================= // Private Functions // ================================================= +func getUserGroupsFromMemberOf(memberOfValues []string) ([]Group, error) { + var groups []Group + for _, memberOf := range memberOfValues { + // Extract CN from DN + parts := strings.Split(memberOf, ",") + if len(parts) > 0 && strings.HasPrefix(parts[0], "CN=") { + groupName := strings.TrimPrefix(parts[0], "CN=") + groups = append(groups, Group{Name: groupName}) + } + } + return groups, nil +} + func isValidUsername(username string) bool { if len(username) < 1 || len(username) > 20 { return false @@ -150,8 +463,8 @@ func extractDomainFromDN(dn string) string { for _, part := range parts { part = strings.TrimSpace(part) - if domain, found := strings.CutPrefix(part, "dc="); found { - domainParts = append(domainParts, domain) + if strings.HasPrefix(part, "dc=") { + domainParts = append(domainParts, strings.TrimPrefix(part, "dc=")) } } diff --git a/internal/proxmox/cluster.go b/internal/proxmox/cluster.go index d58514c..9de7983 100644 --- a/internal/proxmox/cluster.go +++ b/internal/proxmox/cluster.go @@ -113,6 +113,10 @@ func (s *ProxmoxService) SyncUsers() error { return s.syncRealm("users") } +func (s *ProxmoxService) SyncGroups() error { + return s.syncRealm("groups") +} + // ================================================= // Private Functions // ================================================= diff --git a/internal/proxmox/groups.go b/internal/proxmox/groups.go deleted file mode 100644 index 7d3e446..0000000 --- a/internal/proxmox/groups.go +++ /dev/null @@ -1,231 +0,0 @@ -package proxmox - -import ( - "fmt" - "regexp" - "slices" - "strings" - - "github.com/cpp-cyber/proclone/internal/tools" -) - -// ================================================= -// Public Functions -// ================================================= - -func (s *ProxmoxService) GetGroups() ([]Group, error) { - groups := []Group{} - - req := tools.ProxmoxAPIRequest{ - Method: "GET", - Endpoint: "/access/groups", - } - - var groupsResponse []GroupsResponse - if err := s.RequestHelper.MakeRequestAndUnmarshal(req, &groupsResponse); err != nil { - return nil, fmt.Errorf("failed to get groups: %w", err) - } - - for _, group := range groupsResponse { - groups = append(groups, Group{ - Name: group.Name, - UserCount: len(strings.Split(group.Users, ",")), - Comment: group.Comment, - }) - } - - return groups, nil -} - -func (s *ProxmoxService) CreateGroup(groupName string, comment string) error { - // Validate group name - if err := validateGroupName(groupName); err != nil { - return fmt.Errorf("invalid group name: %v", err) - } - - req := tools.ProxmoxAPIRequest{ - Method: "POST", - Endpoint: "/access/groups", - RequestBody: map[string]string{ - "groupid": groupName, - "comment": comment, - }, - } - - _, err := s.RequestHelper.MakeRequest(req) - if err != nil { - return fmt.Errorf("failed to create group: %w", err) - } - - return nil -} - -func (s *ProxmoxService) DeleteGroup(groupName string) error { - req := tools.ProxmoxAPIRequest{ - Method: "DELETE", - Endpoint: fmt.Sprintf("/access/groups/%s", groupName), - } - - _, err := s.RequestHelper.MakeRequest(req) - if err != nil { - return fmt.Errorf("failed to delete group: %w", err) - } - - return nil -} - -func (s *ProxmoxService) GetGroupMembers(groupName string) ([]string, error) { - // Search for the group and get its members - req := tools.ProxmoxAPIRequest{ - Method: "GET", - Endpoint: fmt.Sprintf("/access/groups/%s", groupName), - } - - var groupMemebersResponse GroupMembersResponse - if err := s.RequestHelper.MakeRequestAndUnmarshal(req, &groupMemebersResponse); err != nil { - return nil, fmt.Errorf("failed to get groups members of group: %w", err) - } - - return groupMemebersResponse.Members, nil -} - -func (s *ProxmoxService) AddUsersToGroup(groupName string, usernames []string) error { - // Add users one by one to handle cases where some users might already be in the group - for _, username := range usernames { - // Check user's current groups - userGroups, err := s.getUserGroups(username) - if err != nil { - return fmt.Errorf("failed to get user %s groups: %w", username, err) - } - - // Check if user is already in the group, if not add them - if !contains(userGroups, groupName) { - userGroups = append(userGroups, groupName) - } - - userID := s.getUserID(username) - - req := tools.ProxmoxAPIRequest{ - Method: "PUT", - Endpoint: fmt.Sprintf("/access/users/%s", userID), - RequestBody: map[string]string{ - "groups": strings.Join(userGroups, ","), - }, - } - - _, err = s.RequestHelper.MakeRequest(req) - if err != nil { - return fmt.Errorf("failed to add user %s to group: %v", username, err) - } - } - - return nil -} - -func (s *ProxmoxService) RemoveUsersFromGroup(groupName string, usernames []string) error { - // Remove users one by one to handle cases where some users might not be in the group - for _, username := range usernames { - // Check user's current groups - userGroups, err := s.getUserGroups(username) - if err != nil { - return fmt.Errorf("failed to get user %s groups: %w", username, err) - } - - // Check if user is already in the group, if so, remove them - if contains(userGroups, groupName) { - userGroups = remove(userGroups, groupName) - } - - userID := s.getUserID(username) - - req := tools.ProxmoxAPIRequest{ - Method: "PUT", - Endpoint: fmt.Sprintf("/access/users/%s", userID), - RequestBody: map[string]string{ - "groups": strings.Join(userGroups, ","), - }, - } - - _, err = s.RequestHelper.MakeRequest(req) - if err != nil { - return fmt.Errorf("failed to remove user %s from group %s: %v", username, groupName, err) - } - } - - return nil -} - -func (s *ProxmoxService) EditGroup(groupName string, comment string) error { - if err := validateGroupName(groupName); err != nil { - return err - } - - req := tools.ProxmoxAPIRequest{ - Method: "PUT", - Endpoint: fmt.Sprintf("/access/groups/%s", groupName), - RequestBody: map[string]string{ - "comment": comment, - }, - } - - _, err := s.RequestHelper.MakeRequest(req) - if err != nil { - return fmt.Errorf("failed to edit group %s: %v", groupName, err) - } - - return nil -} - -// ================================================= -// Private Functions -// ================================================= - -func (s *ProxmoxService) getUserGroups(username string) ([]string, error) { - userID := s.getUserID(username) - - req := tools.ProxmoxAPIRequest{ - Method: "GET", - Endpoint: fmt.Sprintf("/access/users/%s", userID), - } - - var userResponse ProxmoxUserIDResponse - if err := s.RequestHelper.MakeRequestAndUnmarshal(req, &userResponse); err != nil { - return nil, fmt.Errorf("failed to get user information: %v", err) - } - - return userResponse.Groups, nil -} - -func validateGroupName(groupName string) error { - if groupName == "" { - return fmt.Errorf("group name cannot be empty") - } - - if len(groupName) >= 64 { - return fmt.Errorf("group name must be less than 64 characters") - } - - regex := regexp.MustCompile("^[a-zA-Z0-9-_]*$") - if !regex.MatchString(groupName) { - return fmt.Errorf("group name must only contain letters, numbers, hyphens, and underscores") - } - - return nil -} - -func contains(slice []string, item string) bool { - if slices.Contains(slice, item) { - return true - } - return false -} - -func remove(slice []string, item string) []string { - result := []string{} - for _, s := range slice { - if s != item { - result = append(result, s) - } - } - return result -} diff --git a/internal/proxmox/types.go b/internal/proxmox/types.go index fed3fc8..245e505 100644 --- a/internal/proxmox/types.go +++ b/internal/proxmox/types.go @@ -18,7 +18,6 @@ type ProxmoxConfig struct { Realm string `envconfig:"PROXMOX_REALM"` NodesStr string `envconfig:"PROXMOX_NODES"` StorageID string `envconfig:"PROXMOX_STORAGE_ID" default:"local-lvm"` - AdminGroupName string `envconfig:"PROXMOX_ADMIN_GROUP_NAME" default:"Admin"` CreatorGroupName string `envconfig:"PROXMOX_CREATOR_GROUP_NAME" default:"Creator"` VMTemplatePool string `envconfig:"PROXMOX_VM_TEMPLATE_POOL" default:"Templates"` RouterName string `envconfig:"PROXMOX_ROUTER_NAME" default:"1-1NAT-vyos"` @@ -41,6 +40,7 @@ type Service interface { GetNodeStatus(nodeName string) (*ProxmoxNodeStatus, error) FindBestNode() (string, error) SyncUsers() error + SyncGroups() error // Pod Management GetNextPodIDs(minPodID int, maxPodID int, num int) ([]string, []int, error) @@ -81,21 +81,6 @@ type Service interface { GetUsedVNets() ([]VNet, error) CreateTemplatePool(creator string, name string, addRouter bool, vms []VM) error - // User Management - GetUsers() ([]User, error) - GetUser(username string) (*User, error) - SetUserGroups(username string, groups []string) error - GetUserGroups(username string) ([]string, error) - - // Group Management - GetGroups() ([]Group, error) - CreateGroup(groupName string, comment string) error - DeleteGroup(groupName string) error - EditGroup(groupName string, comment string) error - GetGroupMembers(groupName string) ([]string, error) - AddUsersToGroup(groupName string, usernames []string) error - RemoveUsersFromGroup(groupName string, usernames []string) error - // Internal access for router functionality GetRequestHelper() *tools.ProxmoxRequestHelper } @@ -199,41 +184,6 @@ type PendingDiskResponse struct { Size int64 `json:"size"` } -type GroupsResponse struct { - Name string `json:"groupid"` - Users string `json:"users"` - Comment string `json:"comment"` -} - -type Group struct { - Name string `json:"name"` - UserCount int `json:"user_count"` - Comment string `json:"comment"` -} - -type GroupMembersResponse struct { - Members []string `json:"members"` -} - -type ProxmoxUserResponse struct { - ID string `json:"userid"` - Comment string `json:"comment"` - Expire int64 `json:"expire"` - Groups string `json:"groups"` -} - -type ProxmoxUserIDResponse struct { - ID string `json:"userid"` - Comment string `json:"comment"` - Expire int64 `json:"expire"` - Groups []string `json:"groups"` -} - -type User struct { - Name string `json:"name"` - Groups []string `json:"groups"` -} - type VNet struct { Name string `json:"vnet"` Tag int `json:"tag"` diff --git a/internal/proxmox/users.go b/internal/proxmox/users.go deleted file mode 100644 index 8909362..0000000 --- a/internal/proxmox/users.go +++ /dev/null @@ -1,98 +0,0 @@ -package proxmox - -import ( - "fmt" - "strings" - - "github.com/cpp-cyber/proclone/internal/tools" -) - -// ================================================= -// Public Functions -// ================================================= - -// GetUsers retrieves all users from Proxmox -func (s *ProxmoxService) GetUsers() ([]User, error) { - req := tools.ProxmoxAPIRequest{ - Method: "GET", - Endpoint: "/access/users?full=1", - } - - var usersResponse []ProxmoxUserResponse - if err := s.RequestHelper.MakeRequestAndUnmarshal(req, &usersResponse); err != nil { - return nil, fmt.Errorf("failed to get users: %w", err) - } - - users := []User{} - for _, userResp := range usersResponse { - // Extract username without realm suffix - username := strings.TrimSuffix(userResp.ID, "@"+s.Config.Realm) - - user := User{ - Name: username, - Groups: strings.Split(userResp.Groups, ","), - } - - users = append(users, user) - } - - return users, nil -} - -// GetUser retrieves a specific user from Proxmox -func (s *ProxmoxService) GetUser(username string) (*User, error) { - userID := s.getUserID(username) - - req := tools.ProxmoxAPIRequest{ - Method: "GET", - Endpoint: fmt.Sprintf("/access/users/%s", userID), - } - - var userResp ProxmoxUserIDResponse - if err := s.RequestHelper.MakeRequestAndUnmarshal(req, &userResp); err != nil { - return nil, fmt.Errorf("failed to get user: %w", err) - } - - user := User{ - Name: username, - Groups: userResp.Groups, - } - - return &user, nil -} - -// SetUserGroups sets the groups for a user in Proxmox -func (s *ProxmoxService) SetUserGroups(username string, groups []string) error { - userID := s.getUserID(username) - - req := tools.ProxmoxAPIRequest{ - Method: "PUT", - Endpoint: fmt.Sprintf("/access/users/%s", userID), - RequestBody: map[string]any{ - "groups": strings.Join(groups, ","), - }, - } - - _, err := s.RequestHelper.MakeRequest(req) - if err != nil { - return fmt.Errorf("failed to set user groups: %w", err) - } - - return nil -} - -// GetUserGroups retrieves the groups a user belongs to -func (s *ProxmoxService) GetUserGroups(username string) ([]string, error) { - return s.getUserGroups(username) -} - -// ================================================= -// Private Functions -// ================================================= - -func (s *ProxmoxService) getUserID(username string) string { - if strings.Contains(username, "@") { - return username - } - return fmt.Sprintf("%s@%s", username, s.Config.Realm) -}