Add files via upload

This commit is contained in:
公明
2026-07-13 15:57:17 +08:00
committed by GitHub
parent f8481024ed
commit 6f39720669
4 changed files with 221 additions and 18 deletions
+17 -12
View File
@@ -28,18 +28,19 @@ type AuditLog struct {
// ListAuditLogsFilter query parameters. // ListAuditLogsFilter query parameters.
type ListAuditLogsFilter struct { type ListAuditLogsFilter struct {
Actor string Actor string
Level string Level string
Category string Category string
Action string Action string
Result string Result string
Query string Query string
ResourceType string ResourceType string
ResourceID string ResourceID string
Since *time.Time RelatedUserID string
Until *time.Time Since *time.Time
Limit int Until *time.Time
Offset int Limit int
Offset int
} }
func buildAuditLogsWhere(filter ListAuditLogsFilter) (string, []interface{}) { func buildAuditLogsWhere(filter ListAuditLogsFilter) (string, []interface{}) {
@@ -73,6 +74,10 @@ func buildAuditLogsWhere(filter ListAuditLogsFilter) (string, []interface{}) {
conditions = append(conditions, "resource_id = ?") conditions = append(conditions, "resource_id = ?")
args = append(args, filter.ResourceID) args = append(args, filter.ResourceID)
} }
if relatedUserID := strings.TrimSpace(filter.RelatedUserID); relatedUserID != "" {
conditions = append(conditions, `(resource_id = ? OR detail_json LIKE ? OR detail_json LIKE ?)`)
args = append(args, relatedUserID, `%"user_id":"`+relatedUserID+`"%`, `%"userId":"`+relatedUserID+`"%`)
}
if filter.Since != nil { if filter.Since != nil {
conditions = append(conditions, sqliteEpochGE("created_at", ">=")) conditions = append(conditions, sqliteEpochGE("created_at", ">="))
args = append(args, formatSQLiteUTC(*filter.Since)) args = append(args, formatSQLiteUTC(*filter.Since))
+13
View File
@@ -31,6 +31,19 @@ func TestBuildAuditLogsWhere_timeFilterSQL(t *testing.T) {
} }
} }
func TestBuildAuditLogsWhere_relatedUserID(t *testing.T) {
where, args := buildAuditLogsWhere(ListAuditLogsFilter{Category: "rbac", RelatedUserID: "user-123"})
if !strings.Contains(where, "resource_id = ?") || !strings.Contains(where, "detail_json LIKE ?") {
t.Fatalf("expected related-user predicates, got %q", where)
}
if len(args) != 4 {
t.Fatalf("expected category plus 3 related-user args, got %#v", args)
}
if args[1] != "user-123" || args[2] != `%"user_id":"user-123"%` || args[3] != `%"userId":"user-123"%` {
t.Fatalf("unexpected related-user args: %#v", args)
}
}
func TestListAuditLogs_timeFilterMixedStorageFormats(t *testing.T) { func TestListAuditLogs_timeFilterMixedStorageFormats(t *testing.T) {
root, err := os.Getwd() root, err := os.Getwd()
if err != nil { if err != nil {
+151 -6
View File
@@ -764,6 +764,37 @@ func (db *DB) ListAssignableRBACResourcesPage(resourceType, search string, limit
return options, rows.Err() return options, rows.Err()
} }
// CountAssignableRBACResources returns the total rows matching the resource picker filter.
func (db *DB) CountAssignableRBACResources(resourceType, search string) (int, error) {
resourceType = strings.TrimSpace(resourceType)
if _, ok := rbacAssignableResourceTables[resourceType]; !ok {
return 0, fmt.Errorf("不支持的资源类型: %s", resourceType)
}
pattern := "%" + strings.ToLower(strings.NewReplacer(
`\`, `\\`, `%`, `\%`, `_`, `\_`,
).Replace(strings.TrimSpace(search))) + "%"
var query string
switch resourceType {
case "project":
query = `SELECT COUNT(*) FROM projects WHERE LOWER(name) LIKE ? ESCAPE '\' OR LOWER(id) LIKE ? ESCAPE '\'`
case "conversation":
query = `SELECT COUNT(*) FROM conversations WHERE LOWER(COALESCE(NULLIF(TRIM(title), ''), id)) LIKE ? ESCAPE '\' OR LOWER(id) LIKE ? ESCAPE '\'`
case "vulnerability":
query = `SELECT COUNT(*) FROM vulnerabilities WHERE LOWER(title) LIKE ? ESCAPE '\' OR LOWER(id) LIKE ? ESCAPE '\'`
case "webshell":
query = `SELECT COUNT(*) FROM webshell_connections WHERE LOWER(COALESCE(NULLIF(remark, ''), url)) LIKE ? ESCAPE '\' OR LOWER(id) LIKE ? ESCAPE '\'`
case "batch_task":
query = `SELECT COUNT(*) FROM batch_task_queues WHERE LOWER(COALESCE(NULLIF(title, ''), id)) LIKE ? ESCAPE '\' OR LOWER(id) LIKE ? ESCAPE '\'`
case "c2_listener":
query = `SELECT COUNT(*) FROM c2_listeners WHERE LOWER(name) LIKE ? ESCAPE '\' OR LOWER(id) LIKE ? ESCAPE '\'`
}
var total int
if err := db.QueryRow(query, pattern, pattern).Scan(&total); err != nil {
return 0, err
}
return total, nil
}
func normalizeRBACResourceLabel(label, id string) string { func normalizeRBACResourceLabel(label, id string) string {
label = strings.TrimSpace(label) label = strings.TrimSpace(label)
if label == "" { if label == "" {
@@ -968,6 +999,86 @@ func (db *DB) AssignResourcesToUser(userID, resourceType string, resourceIDs []s
return created, nil return created, nil
} }
// AssignResourcesToUserAuto detects each resource's actual type before writing.
// The whole batch is validated first and committed atomically.
func (db *DB) AssignResourcesToUserAuto(userID string, resourceIDs []string) (int64, map[string]string, error) {
userID = strings.TrimSpace(userID)
if userID == "" || len(resourceIDs) == 0 {
return 0, nil, errors.New("user_id and resource_ids are required")
}
if len(resourceIDs) > RBACMaxBatchResourceAssignments {
return 0, nil, fmt.Errorf("一次最多授权 %d 个资源", RBACMaxBatchResourceAssignments)
}
uniqueIDs := make([]string, 0, len(resourceIDs))
seen := make(map[string]struct{}, len(resourceIDs))
for _, rawID := range resourceIDs {
id := strings.TrimSpace(rawID)
if id == "" {
return 0, nil, errors.New("资源 ID 不能为空")
}
if _, exists := seen[id]; exists {
continue
}
seen[id] = struct{}{}
uniqueIDs = append(uniqueIDs, id)
}
tx, err := db.Begin()
if err != nil {
return 0, nil, err
}
defer func() { _ = tx.Rollback() }()
var userExists int
if err := tx.QueryRow(`SELECT COUNT(*) FROM rbac_users WHERE id = ?`, userID).Scan(&userExists); err != nil {
return 0, nil, err
}
if userExists == 0 {
return 0, nil, errors.New("用户不存在")
}
typeTablePairs := []struct{ resourceType, table string }{
{"project", "projects"}, {"conversation", "conversations"},
{"vulnerability", "vulnerabilities"}, {"webshell", "webshell_connections"},
{"batch_task", "batch_task_queues"}, {"c2_listener", "c2_listeners"},
}
detected := make(map[string]string, len(uniqueIDs))
for _, resourceID := range uniqueIDs {
for _, pair := range typeTablePairs {
var exists int
if err := tx.QueryRow(`SELECT COUNT(*) FROM `+pair.table+` WHERE id = ?`, resourceID).Scan(&exists); err != nil {
return 0, nil, err
}
if exists > 0 {
if previous := detected[resourceID]; previous != "" {
return 0, nil, fmt.Errorf("资源 ID 同时匹配多个类型: %s (%s, %s)", resourceID, previous, pair.resourceType)
}
detected[resourceID] = pair.resourceType
}
}
if detected[resourceID] == "" {
return 0, nil, fmt.Errorf("资源不存在: %s", resourceID)
}
}
var created int64
for _, resourceID := range uniqueIDs {
result, err := tx.Exec(`
INSERT OR IGNORE INTO rbac_resource_assignments (id, user_id, resource_type, resource_id, created_at)
VALUES (?, ?, ?, ?, ?)
`, uuid.NewString(), userID, detected[resourceID], resourceID, time.Now())
if err != nil {
return 0, nil, err
}
if n, err := result.RowsAffected(); err == nil {
created += n
}
}
if err := tx.Commit(); err != nil {
return 0, nil, err
}
return created, detected, nil
}
func (db *DB) ListRBACUsers() ([]RBACUser, error) { func (db *DB) ListRBACUsers() ([]RBACUser, error) {
rows, err := db.Query(`SELECT id, username, display_name, password_hash, enabled, is_builtin, created_at, updated_at FROM rbac_users ORDER BY username ASC`) rows, err := db.Query(`SELECT id, username, display_name, password_hash, enabled, is_builtin, created_at, updated_at FROM rbac_users ORDER BY username ASC`)
if err != nil { if err != nil {
@@ -1257,16 +1368,50 @@ func (db *DB) ListRBACResourceAssignments(userID string) ([]RBACResourceAssignme
} }
func (db *DB) DeleteRBACResourceAssignment(id string) error { func (db *DB) DeleteRBACResourceAssignment(id string) error {
_, err := db.DeleteRBACResourceAssignmentWithDetails(id)
return err
}
// DeleteRBACResourceAssignmentWithDetails atomically removes an assignment and
// returns the deleted row so callers can write a complete, attributable audit
// event without racing a separate lookup against another delete.
func (db *DB) DeleteRBACResourceAssignmentWithDetails(id string) (*RBACResourceAssignment, error) {
id = strings.TrimSpace(id) id = strings.TrimSpace(id)
if id == "" { if id == "" {
return errors.New("assignment id is required") return nil, errors.New("assignment id is required")
} }
result, err := db.Exec(`DELETE FROM rbac_resource_assignments WHERE id = ?`, id) tx, err := db.Begin()
if err != nil { if err != nil {
return err return nil, err
} }
if affected, err := result.RowsAffected(); err == nil && affected == 0 { defer tx.Rollback()
return errors.New("资源授权不存在或已撤销")
var row RBACResourceAssignment
var createdAt string
err = tx.QueryRow(`
SELECT id, user_id, resource_type, resource_id, created_at
FROM rbac_resource_assignments
WHERE id = ?
`, id).Scan(&row.ID, &row.UserID, &row.ResourceType, &row.ResourceID, &createdAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.New("资源授权不存在或已撤销")
} }
return nil if err != nil {
return nil, err
}
row.CreatedAt = parseDBTime(createdAt)
result, err := tx.Exec(`DELETE FROM rbac_resource_assignments WHERE id = ?`, id)
if err != nil {
return nil, err
}
if affected, rowsErr := result.RowsAffected(); rowsErr != nil {
return nil, rowsErr
} else if affected != 1 {
return nil, errors.New("资源授权不存在或已撤销")
}
if err := tx.Commit(); err != nil {
return nil, err
}
return &row, nil
} }
+40
View File
@@ -579,3 +579,43 @@ func TestRBACAssignmentLabelsAndWeakTitles(t *testing.T) {
t.Fatalf("assignment label = %q, want Alpha Project", rows[0].ResourceLabel) t.Fatalf("assignment label = %q, want Alpha Project", rows[0].ResourceLabel)
} }
} }
func TestDeleteRBACResourceAssignmentWithDetails(t *testing.T) {
db := newRBACTestDB(t)
user, err := db.CreateRBACUser("revoke-member", "Revoke Member", "hash", true, nil)
if err != nil {
t.Fatal(err)
}
project, err := db.CreateProject(&Project{Name: "Revoked Project"})
if err != nil {
t.Fatal(err)
}
if _, err := db.AssignResourcesToUser(user.ID, "project", []string{project.ID}); err != nil {
t.Fatal(err)
}
rows, err := db.ListRBACResourceAssignments(user.ID)
if err != nil {
t.Fatal(err)
}
if len(rows) != 1 {
t.Fatalf("assignments = %#v, want 1", rows)
}
deleted, err := db.DeleteRBACResourceAssignmentWithDetails(rows[0].ID)
if err != nil {
t.Fatal(err)
}
if deleted.ID != rows[0].ID || deleted.UserID != user.ID || deleted.ResourceType != "project" || deleted.ResourceID != project.ID {
t.Fatalf("deleted assignment = %#v", deleted)
}
remaining, err := db.ListRBACResourceAssignments(user.ID)
if err != nil {
t.Fatal(err)
}
if len(remaining) != 0 {
t.Fatalf("remaining assignments = %#v, want none", remaining)
}
if _, err := db.DeleteRBACResourceAssignmentWithDetails(rows[0].ID); err == nil {
t.Fatal("second delete unexpectedly succeeded")
}
}