working on some code

This commit is contained in:
Warp Agent
2025-12-27 16:58:19 +00:00
parent 8677820864
commit 97659421b5
16 changed files with 3318 additions and 151 deletions

View File

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

View File

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

View 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()
}

View File

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

View File

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

View File

@@ -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(&currentName)
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)