mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-16 08:57:30 +02:00
Add files via upload
This commit is contained in:
@@ -10,13 +10,15 @@ import (
|
|||||||
|
|
||||||
func auditFilterFromQuery(c *gin.Context) database.ListAuditLogsFilter {
|
func auditFilterFromQuery(c *gin.Context) database.ListAuditLogsFilter {
|
||||||
filter := database.ListAuditLogsFilter{
|
filter := database.ListAuditLogsFilter{
|
||||||
Level: c.Query("level"),
|
Actor: c.Query("actor"),
|
||||||
Category: c.Query("category"),
|
Level: c.Query("level"),
|
||||||
Action: c.Query("action"),
|
Category: c.Query("category"),
|
||||||
Result: c.Query("result"),
|
Action: c.Query("action"),
|
||||||
Query: c.Query("q"),
|
Result: c.Query("result"),
|
||||||
ResourceType: c.Query("resource_type"),
|
Query: c.Query("q"),
|
||||||
ResourceID: c.Query("resource_id"),
|
ResourceType: c.Query("resource_type"),
|
||||||
|
ResourceID: c.Query("resource_id"),
|
||||||
|
RelatedUserID: c.Query("related_user_id"),
|
||||||
}
|
}
|
||||||
if since := c.Query("since"); since != "" {
|
if since := c.Query("since"); since != "" {
|
||||||
if t, err := database.ParseRFC3339Time(since); err == nil {
|
if t, err := database.ParseRFC3339Time(since); err == nil {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuditFilterFromQueryIncludesActorAndMemberFilters(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
context, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||||
|
context.Request = httptest.NewRequest("GET", "/api/audit/logs?actor=operator-user&action=assign_resource&resource_type=conversation&related_user_id=user-1", nil)
|
||||||
|
|
||||||
|
filter := auditFilterFromQuery(context)
|
||||||
|
if filter.Actor != "operator-user" || filter.Action != "assign_resource" || filter.ResourceType != "conversation" || filter.RelatedUserID != "user-1" {
|
||||||
|
t.Fatalf("filter = %#v", filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -324,6 +324,7 @@ type assignResourceRequest struct {
|
|||||||
ResourceType string `json:"resource_type" binding:"required"`
|
ResourceType string `json:"resource_type" binding:"required"`
|
||||||
ResourceID string `json:"resource_id"`
|
ResourceID string `json:"resource_id"`
|
||||||
ResourceIDs []string `json:"resource_ids"`
|
ResourceIDs []string `json:"resource_ids"`
|
||||||
|
AutoDetect bool `json:"auto_detect"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RBACHandler) AssignResource(c *gin.Context) {
|
func (h *RBACHandler) AssignResource(c *gin.Context) {
|
||||||
@@ -340,21 +341,33 @@ func (h *RBACHandler) AssignResource(c *gin.Context) {
|
|||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "至少需要一个资源 ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "至少需要一个资源 ID"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
created, err := h.db.AssignResourcesToUser(req.UserID, req.ResourceType, resourceIDs)
|
var created int64
|
||||||
|
var detectedTypes map[string]string
|
||||||
|
var err error
|
||||||
|
if req.AutoDetect {
|
||||||
|
created, detectedTypes, err = h.db.AssignResourcesToUserAuto(req.UserID, resourceIDs)
|
||||||
|
} else {
|
||||||
|
created, err = h.db.AssignResourcesToUser(req.UserID, req.ResourceType, resourceIDs)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.audit != nil {
|
if h.audit != nil {
|
||||||
for _, resourceID := range resourceIDs {
|
for _, resourceID := range resourceIDs {
|
||||||
h.audit.RecordOK(c, "rbac", "assign_resource", "授权资源访问", req.ResourceType, strings.TrimSpace(resourceID), map[string]interface{}{"user_id": req.UserID})
|
resourceType := req.ResourceType
|
||||||
|
if detectedTypes != nil {
|
||||||
|
resourceType = detectedTypes[strings.TrimSpace(resourceID)]
|
||||||
|
}
|
||||||
|
h.audit.RecordOK(c, "rbac", "assign_resource", "授权资源访问", resourceType, strings.TrimSpace(resourceID), map[string]interface{}{"user_id": req.UserID})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"requested": len(resourceIDs),
|
"requested": len(resourceIDs),
|
||||||
"created": created,
|
"created": created,
|
||||||
"skipped": int64(len(resourceIDs)) - created,
|
"skipped": int64(len(resourceIDs)) - created,
|
||||||
|
"detected_types": detectedTypes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,22 +398,32 @@ func (h *RBACHandler) ListAssignableResources(c *gin.Context) {
|
|||||||
if hasMore {
|
if hasMore {
|
||||||
resources = resources[:limit]
|
resources = resources[:limit]
|
||||||
}
|
}
|
||||||
|
total, err := h.db.CountAssignableRBACResources(c.Query("type"), c.Query("q"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"resources": resources,
|
"resources": resources,
|
||||||
"has_more": hasMore,
|
"has_more": hasMore,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"offset": offset,
|
"offset": offset,
|
||||||
|
"total": total,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RBACHandler) DeleteResourceAssignment(c *gin.Context) {
|
func (h *RBACHandler) DeleteResourceAssignment(c *gin.Context) {
|
||||||
id := strings.TrimSpace(c.Param("id"))
|
id := strings.TrimSpace(c.Param("id"))
|
||||||
if err := h.db.DeleteRBACResourceAssignment(id); err != nil {
|
assignment, err := h.db.DeleteRBACResourceAssignmentWithDetails(id)
|
||||||
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.audit != nil {
|
if h.audit != nil {
|
||||||
h.audit.RecordOK(c, "rbac", "delete_resource_assignment", "撤销资源授权", "resource_assignment", id, nil)
|
h.audit.RecordOK(c, "rbac", "delete_resource_assignment", "撤销资源授权", assignment.ResourceType, assignment.ResourceID, map[string]interface{}{
|
||||||
|
"user_id": assignment.UserID,
|
||||||
|
"assignment_id": assignment.ID,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cyberstrike-ai/internal/audit"
|
||||||
|
"cyberstrike-ai/internal/config"
|
||||||
"cyberstrike-ai/internal/database"
|
"cyberstrike-ai/internal/database"
|
||||||
|
"cyberstrike-ai/internal/security"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -69,6 +72,40 @@ func TestRBACAssignResourceBatchIsAtomicAndLegacyCompatible(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRBACAssignResourceAutoDetectsActualType(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db, err := database.NewDB(filepath.Join(t.TempDir(), "rbac-auto-detect.db"), zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
user, err := db.CreateRBACUser("auto-member", "Auto Member", "hash", true, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
project, err := db.CreateProject(&database.Project{Name: "auto-project"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h := NewRBACHandler(db, zap.NewNop())
|
||||||
|
router := gin.New()
|
||||||
|
router.POST("/api/rbac/resource-assignments", h.AssignResource)
|
||||||
|
|
||||||
|
response := performRBACJSONRequest(t, router, map[string]interface{}{
|
||||||
|
"user_id": user.ID, "resource_type": "conversation", "resource_ids": []string{project.ID}, "auto_detect": true,
|
||||||
|
})
|
||||||
|
if response.Code != http.StatusOK {
|
||||||
|
t.Fatalf("auto-detect status = %d, body = %s", response.Code, response.Body.String())
|
||||||
|
}
|
||||||
|
rows, err := db.ListRBACResourceAssignments(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(rows) != 1 || rows[0].ResourceType != "project" || rows[0].ResourceID != project.ID {
|
||||||
|
t.Fatalf("auto-detected assignments = %#v, want project/%s", rows, project.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRBACAssignableResourcesArePaged(t *testing.T) {
|
func TestRBACAssignableResourcesArePaged(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
db, err := database.NewDB(filepath.Join(t.TempDir(), "rbac-picker.db"), zap.NewNop())
|
db, err := database.NewDB(filepath.Join(t.TempDir(), "rbac-picker.db"), zap.NewNop())
|
||||||
@@ -104,6 +141,60 @@ func TestRBACAssignableResourcesArePaged(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRBACDeleteResourceAssignmentAuditsTargetResource(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db, err := database.NewDB(filepath.Join(t.TempDir(), "rbac-revoke-audit.db"), zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
user, err := db.CreateRBACUser("audit-member", "Audit Member", "hash", true, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
project, err := db.CreateProject(&database.Project{Name: "audit-project"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.AssignResourcesToUser(user.ID, "project", []string{project.ID}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assignments, err := db.ListRBACResourceAssignments(user.ID)
|
||||||
|
if err != nil || len(assignments) != 1 {
|
||||||
|
t.Fatalf("assignments = %#v, err = %v", assignments, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewRBACHandler(db, zap.NewNop())
|
||||||
|
h.SetAudit(audit.NewService(db, &config.Config{}, zap.NewNop()))
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set(security.ContextUsernameKey, "operator-user")
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
router.DELETE("/api/rbac/resource-assignments/:id", h.DeleteResourceAssignment)
|
||||||
|
request := httptest.NewRequest(http.MethodDelete, "/api/rbac/resource-assignments/"+assignments[0].ID, nil)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := db.ListAuditLogs(database.ListAuditLogsFilter{Category: "rbac", RelatedUserID: user.ID, Limit: 10})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(logs) != 1 {
|
||||||
|
t.Fatalf("audit logs = %#v, want one member-related revoke", logs)
|
||||||
|
}
|
||||||
|
log := logs[0]
|
||||||
|
if log.Action != "delete_resource_assignment" || log.Actor != "operator-user" || log.ResourceType != "project" || log.ResourceID != project.ID {
|
||||||
|
t.Fatalf("audit log = %#v", log)
|
||||||
|
}
|
||||||
|
if log.Detail["user_id"] != user.ID || log.Detail["assignment_id"] != assignments[0].ID {
|
||||||
|
t.Fatalf("audit detail = %#v", log.Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func performRBACJSONRequest(t *testing.T, router http.Handler, payload map[string]interface{}) *httptest.ResponseRecorder {
|
func performRBACJSONRequest(t *testing.T, router http.Handler, payload map[string]interface{}) *httptest.ResponseRecorder {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
body, err := json.Marshal(payload)
|
body, err := json.Marshal(payload)
|
||||||
|
|||||||
Reference in New Issue
Block a user