working on some code
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
-- Add user groups feature
|
||||
-- Groups table
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
is_system BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- User groups junction table
|
||||
CREATE TABLE IF NOT EXISTS user_groups (
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
assigned_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
assigned_by UUID REFERENCES users(id),
|
||||
PRIMARY KEY (user_id, group_id)
|
||||
);
|
||||
|
||||
-- Group roles junction table (groups can have roles)
|
||||
CREATE TABLE IF NOT EXISTS group_roles (
|
||||
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
granted_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (group_id, role_id)
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_name ON groups(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_groups_user_id ON user_groups(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_groups_group_id ON user_groups(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_roles_group_id ON group_roles(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_roles_role_id ON group_roles(role_id);
|
||||
|
||||
-- Insert default system groups
|
||||
INSERT INTO groups (name, description, is_system) VALUES
|
||||
('wheel', 'System administrators group', true),
|
||||
('operators', 'System operators group', true),
|
||||
('backup', 'Backup operators group', true),
|
||||
('auditors', 'Auditors group', true),
|
||||
('storage_admins', 'Storage administrators group', true),
|
||||
('services', 'Service accounts group', true)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
@@ -258,16 +258,32 @@ func NewRouter(cfg *config.Config, db *database.DB, log *logger.Logger) *gin.Eng
|
||||
systemGroup.GET("/interfaces", systemHandler.ListNetworkInterfaces)
|
||||
}
|
||||
|
||||
// IAM (admin only)
|
||||
// IAM routes - GetUser can be accessed by user viewing own profile or admin
|
||||
iamHandler := iam.NewHandler(db, cfg, log)
|
||||
protected.GET("/iam/users/:id", iamHandler.GetUser)
|
||||
|
||||
// IAM admin routes
|
||||
iamGroup := protected.Group("/iam")
|
||||
iamGroup.Use(requireRole("admin"))
|
||||
{
|
||||
iamGroup.GET("/users", iamHandler.ListUsers)
|
||||
iamGroup.GET("/users/:id", iamHandler.GetUser)
|
||||
iamGroup.POST("/users", iamHandler.CreateUser)
|
||||
iamGroup.PUT("/users/:id", iamHandler.UpdateUser)
|
||||
iamGroup.DELETE("/users/:id", iamHandler.DeleteUser)
|
||||
iamGroup.GET("/roles", iamHandler.ListRoles)
|
||||
iamGroup.POST("/users/:id/roles", iamHandler.AssignRoleToUser)
|
||||
iamGroup.DELETE("/users/:id/roles", iamHandler.RemoveRoleFromUser)
|
||||
iamGroup.POST("/users/:id/groups", iamHandler.AssignGroupToUser)
|
||||
iamGroup.DELETE("/users/:id/groups", iamHandler.RemoveGroupFromUser)
|
||||
|
||||
// Groups routes
|
||||
iamGroup.GET("/groups", iamHandler.ListGroups)
|
||||
iamGroup.GET("/groups/:id", iamHandler.GetGroup)
|
||||
iamGroup.POST("/groups", iamHandler.CreateGroup)
|
||||
iamGroup.PUT("/groups/:id", iamHandler.UpdateGroup)
|
||||
iamGroup.DELETE("/groups/:id", iamHandler.DeleteGroup)
|
||||
iamGroup.POST("/groups/:id/users", iamHandler.AddUserToGroup)
|
||||
iamGroup.DELETE("/groups/:id/users/:user_id", iamHandler.RemoveUserFromGroup)
|
||||
}
|
||||
|
||||
// Monitoring
|
||||
|
||||
218
backend/internal/iam/group.go
Normal file
218
backend/internal/iam/group.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/atlasos/calypso/internal/common/database"
|
||||
)
|
||||
|
||||
// Group represents a user group
|
||||
type Group struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
IsSystem bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
UserCount int
|
||||
RoleCount int
|
||||
}
|
||||
|
||||
// GetGroupByID retrieves a group by ID
|
||||
func GetGroupByID(db *database.DB, groupID string) (*Group, error) {
|
||||
query := `
|
||||
SELECT id, name, description, is_system, created_at, updated_at
|
||||
FROM groups
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
var group Group
|
||||
err := db.QueryRow(query, groupID).Scan(
|
||||
&group.ID, &group.Name, &group.Description, &group.IsSystem,
|
||||
&group.CreatedAt, &group.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get user count
|
||||
var userCount int
|
||||
db.QueryRow("SELECT COUNT(*) FROM user_groups WHERE group_id = $1", groupID).Scan(&userCount)
|
||||
group.UserCount = userCount
|
||||
|
||||
// Get role count
|
||||
var roleCount int
|
||||
db.QueryRow("SELECT COUNT(*) FROM group_roles WHERE group_id = $1", groupID).Scan(&roleCount)
|
||||
group.RoleCount = roleCount
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
// GetGroupByName retrieves a group by name
|
||||
func GetGroupByName(db *database.DB, name string) (*Group, error) {
|
||||
query := `
|
||||
SELECT id, name, description, is_system, created_at, updated_at
|
||||
FROM groups
|
||||
WHERE name = $1
|
||||
`
|
||||
|
||||
var group Group
|
||||
err := db.QueryRow(query, name).Scan(
|
||||
&group.ID, &group.Name, &group.Description, &group.IsSystem,
|
||||
&group.CreatedAt, &group.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
// GetUserGroups retrieves all groups for a user
|
||||
func GetUserGroups(db *database.DB, userID string) ([]string, error) {
|
||||
query := `
|
||||
SELECT g.name
|
||||
FROM groups g
|
||||
INNER JOIN user_groups ug ON g.id = ug.group_id
|
||||
WHERE ug.user_id = $1
|
||||
ORDER BY g.name
|
||||
`
|
||||
|
||||
rows, err := db.Query(query, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var groups []string
|
||||
for rows.Next() {
|
||||
var groupName string
|
||||
if err := rows.Scan(&groupName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groups = append(groups, groupName)
|
||||
}
|
||||
|
||||
return groups, rows.Err()
|
||||
}
|
||||
|
||||
// GetGroupUsers retrieves all users in a group
|
||||
func GetGroupUsers(db *database.DB, groupID string) ([]string, error) {
|
||||
query := `
|
||||
SELECT u.id
|
||||
FROM users u
|
||||
INNER JOIN user_groups ug ON u.id = ug.user_id
|
||||
WHERE ug.group_id = $1
|
||||
ORDER BY u.username
|
||||
`
|
||||
|
||||
rows, err := db.Query(query, groupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var userIDs []string
|
||||
for rows.Next() {
|
||||
var userID string
|
||||
if err := rows.Scan(&userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
|
||||
return userIDs, rows.Err()
|
||||
}
|
||||
|
||||
// GetGroupRoles retrieves all roles for a group
|
||||
func GetGroupRoles(db *database.DB, groupID string) ([]string, error) {
|
||||
query := `
|
||||
SELECT r.name
|
||||
FROM roles r
|
||||
INNER JOIN group_roles gr ON r.id = gr.role_id
|
||||
WHERE gr.group_id = $1
|
||||
ORDER BY r.name
|
||||
`
|
||||
|
||||
rows, err := db.Query(query, groupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var roles []string
|
||||
for rows.Next() {
|
||||
var role string
|
||||
if err := rows.Scan(&role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roles = append(roles, role)
|
||||
}
|
||||
|
||||
return roles, rows.Err()
|
||||
}
|
||||
|
||||
// AddUserToGroup adds a user to a group
|
||||
func AddUserToGroup(db *database.DB, userID, groupID, assignedBy string) error {
|
||||
query := `
|
||||
INSERT INTO user_groups (user_id, group_id, assigned_by)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, group_id) DO NOTHING
|
||||
`
|
||||
_, err := db.Exec(query, userID, groupID, assignedBy)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveUserFromGroup removes a user from a group
|
||||
func RemoveUserFromGroup(db *database.DB, userID, groupID string) error {
|
||||
query := `DELETE FROM user_groups WHERE user_id = $1 AND group_id = $2`
|
||||
_, err := db.Exec(query, userID, groupID)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddRoleToGroup adds a role to a group
|
||||
func AddRoleToGroup(db *database.DB, groupID, roleID string) error {
|
||||
query := `
|
||||
INSERT INTO group_roles (group_id, role_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (group_id, role_id) DO NOTHING
|
||||
`
|
||||
_, err := db.Exec(query, groupID, roleID)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveRoleFromGroup removes a role from a group
|
||||
func RemoveRoleFromGroup(db *database.DB, groupID, roleID string) error {
|
||||
query := `DELETE FROM group_roles WHERE group_id = $1 AND role_id = $2`
|
||||
_, err := db.Exec(query, groupID, roleID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUserRolesFromGroups retrieves all roles for a user via groups
|
||||
func GetUserRolesFromGroups(db *database.DB, userID string) ([]string, error) {
|
||||
query := `
|
||||
SELECT DISTINCT r.name
|
||||
FROM roles r
|
||||
INNER JOIN group_roles gr ON r.id = gr.role_id
|
||||
INNER JOIN user_groups ug ON gr.group_id = ug.group_id
|
||||
WHERE ug.user_id = $1
|
||||
ORDER BY r.name
|
||||
`
|
||||
|
||||
rows, err := db.Query(query, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var roles []string
|
||||
for rows.Next() {
|
||||
var role string
|
||||
if err := rows.Scan(&role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roles = append(roles, role)
|
||||
}
|
||||
|
||||
return roles, rows.Err()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -64,15 +65,22 @@ func (h *Handler) ListUsers(c *gin.Context) {
|
||||
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,
|
||||
"created_at": u.CreatedAt,
|
||||
"updated_at": u.UpdatedAt,
|
||||
"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,
|
||||
})
|
||||
}
|
||||
@@ -81,9 +89,45 @@ func (h *Handler) ListUsers(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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"})
|
||||
@@ -92,18 +136,21 @@ func (h *Handler) GetUser(c *gin.Context) {
|
||||
|
||||
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,
|
||||
"created_at": user.CreatedAt,
|
||||
"updated_at": user.UpdatedAt,
|
||||
"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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -230,3 +277,432 @@ func (h *Handler) DeleteUser(c *gin.Context) {
|
||||
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"})
|
||||
}
|
||||
|
||||
@@ -126,3 +126,27 @@ func GetUserPermissions(db *database.DB, userID string) ([]string, error) {
|
||||
return permissions, rows.Err()
|
||||
}
|
||||
|
||||
// AddUserRole assigns a role to a user
|
||||
func AddUserRole(db *database.DB, userID, roleID, assignedBy string) error {
|
||||
query := `
|
||||
INSERT INTO user_roles (user_id, role_id, assigned_by)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING
|
||||
`
|
||||
_, err := db.Exec(query, userID, roleID, assignedBy)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveUserRole removes a role from a user
|
||||
func RemoveUserRole(db *database.DB, userID, roleID string) error {
|
||||
query := `DELETE FROM user_roles WHERE user_id = $1 AND role_id = $2`
|
||||
_, err := db.Exec(query, userID, roleID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetRoleIDByName retrieves a role ID by name
|
||||
func GetRoleIDByName(db *database.DB, roleName string) (string, error) {
|
||||
var roleID string
|
||||
err := db.QueryRow("SELECT id FROM roles WHERE name = $1", roleName).Scan(&roleID)
|
||||
return roleID, err
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func (m *MHVTLMonitor) Stop() {
|
||||
|
||||
// syncMHVTL parses mhvtl configuration and syncs to database
|
||||
func (m *MHVTLMonitor) syncMHVTL(ctx context.Context) {
|
||||
m.logger.Debug("Running MHVTL configuration sync")
|
||||
m.logger.Info("Running MHVTL configuration sync")
|
||||
|
||||
deviceConfPath := filepath.Join(m.configPath, "device.conf")
|
||||
if _, err := os.Stat(deviceConfPath); os.IsNotExist(err) {
|
||||
@@ -84,6 +84,11 @@ func (m *MHVTLMonitor) syncMHVTL(ctx context.Context) {
|
||||
|
||||
m.logger.Info("Parsed MHVTL configuration", "libraries", len(libraries), "drives", len(drives))
|
||||
|
||||
// Log parsed drives for debugging
|
||||
for _, drive := range drives {
|
||||
m.logger.Debug("Parsed drive", "drive_id", drive.DriveID, "library_id", drive.LibraryID, "slot", drive.Slot)
|
||||
}
|
||||
|
||||
// Sync libraries to database
|
||||
for _, lib := range libraries {
|
||||
if err := m.syncLibrary(ctx, lib); err != nil {
|
||||
@@ -94,7 +99,9 @@ func (m *MHVTLMonitor) syncMHVTL(ctx context.Context) {
|
||||
// Sync drives to database
|
||||
for _, drive := range drives {
|
||||
if err := m.syncDrive(ctx, drive); err != nil {
|
||||
m.logger.Error("Failed to sync drive", "drive_id", drive.DriveID, "error", err)
|
||||
m.logger.Error("Failed to sync drive", "drive_id", drive.DriveID, "library_id", drive.LibraryID, "slot", drive.Slot, "error", err)
|
||||
} else {
|
||||
m.logger.Debug("Synced drive", "drive_id", drive.DriveID, "library_id", drive.LibraryID, "slot", drive.Slot)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +113,7 @@ func (m *MHVTLMonitor) syncMHVTL(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Debug("MHVTL configuration sync completed")
|
||||
m.logger.Info("MHVTL configuration sync completed")
|
||||
}
|
||||
|
||||
// LibraryInfo represents a library from device.conf
|
||||
@@ -189,6 +196,7 @@ func (m *MHVTLMonitor) parseDeviceConf(ctx context.Context, path string) ([]Libr
|
||||
Target: matches[3],
|
||||
LUN: matches[4],
|
||||
}
|
||||
// Library ID and Slot might be on the same line or next line
|
||||
if matches := libraryIDRegex.FindStringSubmatch(line); matches != nil {
|
||||
libID, _ := strconv.Atoi(matches[1])
|
||||
slot, _ := strconv.Atoi(matches[2])
|
||||
@@ -198,34 +206,63 @@ func (m *MHVTLMonitor) parseDeviceConf(ctx context.Context, path string) ([]Libr
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse library fields
|
||||
if currentLibrary != nil {
|
||||
if strings.HasPrefix(line, "Vendor identification:") {
|
||||
currentLibrary.Vendor = strings.TrimSpace(strings.TrimPrefix(line, "Vendor identification:"))
|
||||
} else if strings.HasPrefix(line, "Product identification:") {
|
||||
currentLibrary.Product = strings.TrimSpace(strings.TrimPrefix(line, "Product identification:"))
|
||||
} else if strings.HasPrefix(line, "Unit serial number:") {
|
||||
currentLibrary.SerialNumber = strings.TrimSpace(strings.TrimPrefix(line, "Unit serial number:"))
|
||||
} else if strings.HasPrefix(line, "Home directory:") {
|
||||
currentLibrary.HomeDirectory = strings.TrimSpace(strings.TrimPrefix(line, "Home directory:"))
|
||||
// Parse library fields (only if we're in a library section and not in a drive section)
|
||||
if currentLibrary != nil && currentDrive == nil {
|
||||
// Handle both "Vendor identification:" and " Vendor identification:" (with leading space)
|
||||
if strings.Contains(line, "Vendor identification:") {
|
||||
parts := strings.Split(line, "Vendor identification:")
|
||||
if len(parts) > 1 {
|
||||
currentLibrary.Vendor = strings.TrimSpace(parts[1])
|
||||
m.logger.Debug("Parsed vendor", "vendor", currentLibrary.Vendor, "library_id", currentLibrary.LibraryID)
|
||||
}
|
||||
} else if strings.Contains(line, "Product identification:") {
|
||||
parts := strings.Split(line, "Product identification:")
|
||||
if len(parts) > 1 {
|
||||
currentLibrary.Product = strings.TrimSpace(parts[1])
|
||||
m.logger.Info("Parsed library product", "product", currentLibrary.Product, "library_id", currentLibrary.LibraryID)
|
||||
}
|
||||
} else if strings.Contains(line, "Unit serial number:") {
|
||||
parts := strings.Split(line, "Unit serial number:")
|
||||
if len(parts) > 1 {
|
||||
currentLibrary.SerialNumber = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.Contains(line, "Home directory:") {
|
||||
parts := strings.Split(line, "Home directory:")
|
||||
if len(parts) > 1 {
|
||||
currentLibrary.HomeDirectory = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse drive fields
|
||||
if currentDrive != nil {
|
||||
if strings.HasPrefix(line, "Vendor identification:") {
|
||||
currentDrive.Vendor = strings.TrimSpace(strings.TrimPrefix(line, "Vendor identification:"))
|
||||
} else if strings.HasPrefix(line, "Product identification:") {
|
||||
currentDrive.Product = strings.TrimSpace(strings.TrimPrefix(line, "Product identification:"))
|
||||
} else if strings.HasPrefix(line, "Unit serial number:") {
|
||||
currentDrive.SerialNumber = strings.TrimSpace(strings.TrimPrefix(line, "Unit serial number:"))
|
||||
} else if strings.HasPrefix(line, "Library ID:") && strings.Contains(line, "Slot:") {
|
||||
// Check for Library ID and Slot first (can be on separate line)
|
||||
if strings.Contains(line, "Library ID:") && strings.Contains(line, "Slot:") {
|
||||
matches := libraryIDRegex.FindStringSubmatch(line)
|
||||
if matches != nil {
|
||||
libID, _ := strconv.Atoi(matches[1])
|
||||
slot, _ := strconv.Atoi(matches[2])
|
||||
currentDrive.LibraryID = libID
|
||||
currentDrive.Slot = slot
|
||||
m.logger.Debug("Parsed drive Library ID and Slot", "drive_id", currentDrive.DriveID, "library_id", libID, "slot", slot)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Handle both "Vendor identification:" and " Vendor identification:" (with leading space)
|
||||
if strings.Contains(line, "Vendor identification:") {
|
||||
parts := strings.Split(line, "Vendor identification:")
|
||||
if len(parts) > 1 {
|
||||
currentDrive.Vendor = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.Contains(line, "Product identification:") {
|
||||
parts := strings.Split(line, "Product identification:")
|
||||
if len(parts) > 1 {
|
||||
currentDrive.Product = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.Contains(line, "Unit serial number:") {
|
||||
parts := strings.Split(line, "Unit serial number:")
|
||||
if len(parts) > 1 {
|
||||
currentDrive.SerialNumber = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,9 +292,17 @@ func (m *MHVTLMonitor) syncLibrary(ctx context.Context, libInfo LibraryInfo) err
|
||||
libInfo.LibraryID,
|
||||
).Scan(&existingID)
|
||||
|
||||
m.logger.Debug("Syncing library", "library_id", libInfo.LibraryID, "vendor", libInfo.Vendor, "product", libInfo.Product)
|
||||
|
||||
// Use product identification for library name (without library ID)
|
||||
libraryName := fmt.Sprintf("VTL-%d", libInfo.LibraryID)
|
||||
if libInfo.Product != "" {
|
||||
libraryName = fmt.Sprintf("%s-%d", libInfo.Product, libInfo.LibraryID)
|
||||
// Use only product name, without library ID
|
||||
libraryName = libInfo.Product
|
||||
m.logger.Info("Using product for library name", "product", libInfo.Product, "library_id", libInfo.LibraryID, "name", libraryName)
|
||||
} else if libInfo.Vendor != "" {
|
||||
libraryName = libInfo.Vendor
|
||||
m.logger.Info("Using vendor for library name (product not available)", "vendor", libInfo.Vendor, "library_id", libInfo.LibraryID)
|
||||
}
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -284,13 +329,31 @@ func (m *MHVTLMonitor) syncLibrary(ctx context.Context, libInfo LibraryInfo) err
|
||||
}
|
||||
m.logger.Info("Created virtual library from MHVTL", "library_id", libInfo.LibraryID, "name", libraryName)
|
||||
} else if err == nil {
|
||||
// Update existing library
|
||||
// Update existing library - also update name if product is available
|
||||
updateName := libraryName
|
||||
// If product exists and current name doesn't match, update it
|
||||
if libInfo.Product != "" {
|
||||
var currentName string
|
||||
err := m.service.db.QueryRowContext(ctx,
|
||||
"SELECT name FROM virtual_tape_libraries WHERE id = $1", existingID,
|
||||
).Scan(¤tName)
|
||||
if err == nil {
|
||||
// Use only product name, without library ID
|
||||
expectedName := libInfo.Product
|
||||
if currentName != expectedName {
|
||||
updateName = expectedName
|
||||
m.logger.Info("Updating library name", "old", currentName, "new", updateName, "product", libInfo.Product)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Info("Updating existing library", "library_id", libInfo.LibraryID, "product", libInfo.Product, "vendor", libInfo.Vendor, "old_name", libraryName, "new_name", updateName)
|
||||
_, err = m.service.db.ExecContext(ctx, `
|
||||
UPDATE virtual_tape_libraries SET
|
||||
name = $1, description = $2, backing_store_path = $3,
|
||||
vendor = $4, is_active = $5, updated_at = NOW()
|
||||
WHERE id = $6
|
||||
`, libraryName, fmt.Sprintf("MHVTL Library %d (%s)", libInfo.LibraryID, libInfo.Product),
|
||||
`, updateName, fmt.Sprintf("MHVTL Library %d (%s)", libInfo.LibraryID, libInfo.Product),
|
||||
libInfo.HomeDirectory, libInfo.Vendor, true, existingID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update library: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user