From 020fd0804e7ec75b7a2a9aec1d7686e422e4d70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Fri, 7 Aug 2026 12:25:13 -0600 Subject: [PATCH 1/2] fix[backend](soar): added scoped tenant on sql requests --- backend/modules/soar/connectors/repository.go | 40 ++++++++--------- backend/modules/soar/connectors/usecase.go | 44 +++++++++---------- backend/modules/soar/handler/action.go | 12 ++--- .../modules/soar/handler/action_command.go | 10 ++--- backend/modules/soar/handler/command_ws.go | 4 +- backend/modules/soar/handler/job.go | 10 ++--- backend/modules/soar/handler/variable.go | 10 ++--- .../soar/repository/action_command_pg.go | 18 +++++--- backend/modules/soar/repository/action_pg.go | 18 +++++--- .../modules/soar/repository/execution_pg.go | 9 ++-- backend/modules/soar/repository/job_pg.go | 22 ++++++---- backend/modules/soar/repository/tenant.go | 26 +++++++++++ .../modules/soar/repository/variable_pg.go | 30 +++++++------ backend/modules/soar/usecase/action.go | 23 +++++----- .../modules/soar/usecase/action_command.go | 23 +++++----- backend/modules/soar/usecase/dispatch.go | 4 +- backend/modules/soar/usecase/job.go | 21 ++++----- backend/modules/soar/usecase/variable.go | 31 ++++++------- 18 files changed, 202 insertions(+), 153 deletions(-) create mode 100644 backend/modules/soar/repository/tenant.go diff --git a/backend/modules/soar/connectors/repository.go b/backend/modules/soar/connectors/repository.go index 2a3144082..a533cb68a 100644 --- a/backend/modules/soar/connectors/repository.go +++ b/backend/modules/soar/connectors/repository.go @@ -58,33 +58,33 @@ type AgentRepository interface { } type VariableRepository interface { - Save(v *domain.UtmIncidentVariable) error - FindByID(id int64) (*domain.UtmIncidentVariable, error) - FindAll(f dto.VariableFilter) ([]domain.UtmIncidentVariable, int64, error) - FindAllPlain() ([]domain.UtmIncidentVariable, error) - FindByName(name string) (*domain.UtmIncidentVariable, error) - FindByNames(names []string) ([]domain.UtmIncidentVariable, error) - Delete(id int64) error + Save(ctx context.Context, v *domain.UtmIncidentVariable) error + FindByID(ctx context.Context, id int64) (*domain.UtmIncidentVariable, error) + FindAll(ctx context.Context, f dto.VariableFilter) ([]domain.UtmIncidentVariable, int64, error) + FindAllPlain(ctx context.Context) ([]domain.UtmIncidentVariable, error) + FindByName(ctx context.Context, name string) (*domain.UtmIncidentVariable, error) + FindByNames(ctx context.Context, names []string) ([]domain.UtmIncidentVariable, error) + Delete(ctx context.Context, id int64) error } type ActionRepository interface { - Save(action *domain.UtmIncidentAction) error - FindByID(id int64) (*domain.UtmIncidentAction, error) - FindAll(f dto.ActionFilter) ([]domain.UtmIncidentAction, int64, error) - Delete(id int64) error + Save(ctx context.Context, action *domain.UtmIncidentAction) error + FindByID(ctx context.Context, id int64) (*domain.UtmIncidentAction, error) + FindAll(ctx context.Context, f dto.ActionFilter) ([]domain.UtmIncidentAction, int64, error) + Delete(ctx context.Context, id int64) error } type ActionCommandRepository interface { - Save(cmd *domain.UtmIncidentActionCommand) error - FindByID(id int64) (*domain.UtmIncidentActionCommand, error) - FindAll(f dto.ActionCommandFilter) ([]domain.UtmIncidentActionCommand, int64, error) - Delete(id int64) error + Save(ctx context.Context, cmd *domain.UtmIncidentActionCommand) error + FindByID(ctx context.Context, id int64) (*domain.UtmIncidentActionCommand, error) + FindAll(ctx context.Context, f dto.ActionCommandFilter) ([]domain.UtmIncidentActionCommand, int64, error) + Delete(ctx context.Context, id int64) error } type JobRepository interface { - Save(job *domain.UtmIncidentJob) error - FindByID(id int64) (*domain.UtmIncidentJob, error) - FindAll(f dto.JobFilter) ([]domain.UtmIncidentJob, int64, error) - Count(f dto.JobFilter) (int64, error) - Delete(id int64) error + Save(ctx context.Context, job *domain.UtmIncidentJob) error + FindByID(ctx context.Context, id int64) (*domain.UtmIncidentJob, error) + FindAll(ctx context.Context, f dto.JobFilter) ([]domain.UtmIncidentJob, int64, error) + Count(ctx context.Context, f dto.JobFilter) (int64, error) + Delete(ctx context.Context, id int64) error } diff --git a/backend/modules/soar/connectors/usecase.go b/backend/modules/soar/connectors/usecase.go index 1f7387364..259ff0976 100644 --- a/backend/modules/soar/connectors/usecase.go +++ b/backend/modules/soar/connectors/usecase.go @@ -28,30 +28,30 @@ type ExecutionUsecase interface { } type VariableUsecase interface { - Create(req dto.CreateVariableRequest, user string) (*dto.VariableResponse, error) - Update(req dto.UpdateVariableRequest, user string) (*dto.VariableResponse, error) - FindByID(id int64) (*dto.VariableResponse, error) - FindAll(f dto.VariableFilter) ([]dto.VariableResponse, int64, error) - Delete(id int64) error + Create(ctx context.Context, req dto.CreateVariableRequest, user string) (*dto.VariableResponse, error) + Update(ctx context.Context, req dto.UpdateVariableRequest, user string) (*dto.VariableResponse, error) + FindByID(ctx context.Context, id int64) (*dto.VariableResponse, error) + FindAll(ctx context.Context, f dto.VariableFilter) ([]dto.VariableResponse, int64, error) + Delete(ctx context.Context, id int64) error - InterpolateCommand(cmd string) (string, error) - MaskSecrets(output string) (string, error) + InterpolateCommand(ctx context.Context, cmd string) (string, error) + MaskSecrets(ctx context.Context, output string) (string, error) } type ActionUsecase interface { - Create(req dto.CreateActionRequest, user string) (*domain.UtmIncidentAction, error) - Update(req dto.UpdateActionRequest, user string) (*domain.UtmIncidentAction, error) - FindByID(id int64) (*domain.UtmIncidentAction, error) - FindAll(f dto.ActionFilter) ([]domain.UtmIncidentAction, int64, error) - Delete(id int64) error + Create(ctx context.Context, req dto.CreateActionRequest, user string) (*domain.UtmIncidentAction, error) + Update(ctx context.Context, req dto.UpdateActionRequest, user string) (*domain.UtmIncidentAction, error) + FindByID(ctx context.Context, id int64) (*domain.UtmIncidentAction, error) + FindAll(ctx context.Context, f dto.ActionFilter) ([]domain.UtmIncidentAction, int64, error) + Delete(ctx context.Context, id int64) error } type ActionCommandUsecase interface { - Create(req dto.CreateActionCommandRequest) (*domain.UtmIncidentActionCommand, error) - Update(req dto.UpdateActionCommandRequest) (*domain.UtmIncidentActionCommand, error) - FindByID(id int64) (*domain.UtmIncidentActionCommand, error) - FindAll(f dto.ActionCommandFilter) ([]domain.UtmIncidentActionCommand, int64, error) - Delete(id int64) error + Create(ctx context.Context, req dto.CreateActionCommandRequest) (*domain.UtmIncidentActionCommand, error) + Update(ctx context.Context, req dto.UpdateActionCommandRequest) (*domain.UtmIncidentActionCommand, error) + FindByID(ctx context.Context, id int64) (*domain.UtmIncidentActionCommand, error) + FindAll(ctx context.Context, f dto.ActionCommandFilter) ([]domain.UtmIncidentActionCommand, int64, error) + Delete(ctx context.Context, id int64) error } type AgentUsecase interface { @@ -59,9 +59,9 @@ type AgentUsecase interface { } type JobUsecase interface { - Create(req dto.CreateJobRequest, user string) (*domain.UtmIncidentJob, error) - FindByID(id int64) (*domain.UtmIncidentJob, error) - FindAll(f dto.JobFilter) ([]domain.UtmIncidentJob, int64, error) - Count(f dto.JobFilter) (int64, error) - Delete(id int64) error + Create(ctx context.Context, req dto.CreateJobRequest, user string) (*domain.UtmIncidentJob, error) + FindByID(ctx context.Context, id int64) (*domain.UtmIncidentJob, error) + FindAll(ctx context.Context, f dto.JobFilter) ([]domain.UtmIncidentJob, int64, error) + Count(ctx context.Context, f dto.JobFilter) (int64, error) + Delete(ctx context.Context, id int64) error } diff --git a/backend/modules/soar/handler/action.go b/backend/modules/soar/handler/action.go index 14d4e90ad..3d53b9a5c 100644 --- a/backend/modules/soar/handler/action.go +++ b/backend/modules/soar/handler/action.go @@ -36,7 +36,7 @@ func (h *ActionHandler) Create(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - result, err := h.uc.Create(req, loginFromCtx(c)) + result, err := h.uc.Create(c.Request.Context(), req, loginFromCtx(c)) if err != nil { writeARRError(c, err) return @@ -63,7 +63,7 @@ func (h *ActionHandler) Update(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - result, err := h.uc.Update(req, loginFromCtx(c)) + result, err := h.uc.Update(c.Request.Context(), req, loginFromCtx(c)) if err != nil { writeARRError(c, err) return @@ -94,7 +94,7 @@ func (h *ActionHandler) List(c *gin.Context) { ActionType: queryIntPtr(c, "actionType"), ActionEditable: queryBoolPtr(c, "actionEditable"), } - items, total, err := h.uc.FindAll(f) + items, total, err := h.uc.FindAll(c.Request.Context(), f) if err != nil { writeARRError(c, err) return @@ -135,7 +135,7 @@ func (h *ActionHandler) Count(c *gin.Context) { ActionType: queryIntPtr(c, "actionType"), ActionEditable: queryBoolPtr(c, "actionEditable"), } - _, total, err := h.uc.FindAll(f) + _, total, err := h.uc.FindAll(c.Request.Context(), f) if err != nil { writeARRError(c, err) return @@ -160,7 +160,7 @@ func (h *ActionHandler) GetByID(c *gin.Context) { if !ok { return } - result, err := h.uc.FindByID(id) + result, err := h.uc.FindByID(c.Request.Context(), id) if err != nil { writeARRError(c, err) return @@ -184,7 +184,7 @@ func (h *ActionHandler) Delete(c *gin.Context) { if !ok { return } - if err := h.uc.Delete(id); err != nil { + if err := h.uc.Delete(c.Request.Context(), id); err != nil { writeARRError(c, err) return } diff --git a/backend/modules/soar/handler/action_command.go b/backend/modules/soar/handler/action_command.go index 4cb994dd4..fe74f7e45 100644 --- a/backend/modules/soar/handler/action_command.go +++ b/backend/modules/soar/handler/action_command.go @@ -36,7 +36,7 @@ func (h *ActionCommandHandler) Create(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - result, err := h.uc.Create(req) + result, err := h.uc.Create(c.Request.Context(), req) if err != nil { writeARRError(c, err) return @@ -64,7 +64,7 @@ func (h *ActionCommandHandler) Update(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - result, err := h.uc.Update(req) + result, err := h.uc.Update(c.Request.Context(), req) if err != nil { writeARRError(c, err) return @@ -95,7 +95,7 @@ func (h *ActionCommandHandler) List(c *gin.Context) { OsPlatform: queryString(c, "osPlatform"), Command: queryString(c, "command"), } - items, total, err := h.uc.FindAll(f) + items, total, err := h.uc.FindAll(c.Request.Context(), f) if err != nil { writeARRError(c, err) return @@ -120,7 +120,7 @@ func (h *ActionCommandHandler) GetByID(c *gin.Context) { if !ok { return } - result, err := h.uc.FindByID(id) + result, err := h.uc.FindByID(c.Request.Context(), id) if err != nil { writeARRError(c, err) return @@ -144,7 +144,7 @@ func (h *ActionCommandHandler) Delete(c *gin.Context) { if !ok { return } - if err := h.uc.Delete(id); err != nil { + if err := h.uc.Delete(c.Request.Context(), id); err != nil { writeARRError(c, err) return } diff --git a/backend/modules/soar/handler/command_ws.go b/backend/modules/soar/handler/command_ws.go index ec4e49529..94e80efea 100644 --- a/backend/modules/soar/handler/command_ws.go +++ b/backend/modules/soar/handler/command_ws.go @@ -154,7 +154,7 @@ func (h *CommandWSHandler) CommandStream(c *gin.Context) { } command := req.Command if h.variableUC != nil { - if interpolated, vErr := h.variableUC.InterpolateCommand(command); vErr != nil { + if interpolated, vErr := h.variableUC.InterpolateCommand(ctx, command); vErr != nil { _ = catcher.Error("CommandStream: variable interpolation", vErr, nil) } else { command = interpolated @@ -182,7 +182,7 @@ func (h *CommandWSHandler) CommandStream(c *gin.Context) { } output := result.GetResult() if h.variableUC != nil { - if masked, mErr := h.variableUC.MaskSecrets(output); mErr != nil { + if masked, mErr := h.variableUC.MaskSecrets(ctx, output); mErr != nil { _ = catcher.Error("CommandStream: mask secrets", mErr, nil) } else { output = masked diff --git a/backend/modules/soar/handler/job.go b/backend/modules/soar/handler/job.go index 3a6f052c3..49d1cfa45 100644 --- a/backend/modules/soar/handler/job.go +++ b/backend/modules/soar/handler/job.go @@ -36,7 +36,7 @@ func (h *JobHandler) Create(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - result, err := h.uc.Create(req, loginFromCtx(c)) + result, err := h.uc.Create(c.Request.Context(), req, loginFromCtx(c)) if err != nil { writeARRError(c, err) return @@ -71,7 +71,7 @@ func (h *JobHandler) List(c *gin.Context) { OriginID: queryIntPtr(c, "originId"), OriginType: queryString(c, "originType"), } - items, total, err := h.uc.FindAll(f) + items, total, err := h.uc.FindAll(c.Request.Context(), f) if err != nil { writeARRError(c, err) return @@ -102,7 +102,7 @@ func (h *JobHandler) Count(c *gin.Context) { OriginID: queryIntPtr(c, "originId"), OriginType: queryString(c, "originType"), } - total, err := h.uc.Count(f) + total, err := h.uc.Count(c.Request.Context(), f) if err != nil { writeARRError(c, err) return @@ -127,7 +127,7 @@ func (h *JobHandler) GetByID(c *gin.Context) { if !ok { return } - result, err := h.uc.FindByID(id) + result, err := h.uc.FindByID(c.Request.Context(), id) if err != nil { writeARRError(c, err) return @@ -151,7 +151,7 @@ func (h *JobHandler) Delete(c *gin.Context) { if !ok { return } - if err := h.uc.Delete(id); err != nil { + if err := h.uc.Delete(c.Request.Context(), id); err != nil { writeARRError(c, err) return } diff --git a/backend/modules/soar/handler/variable.go b/backend/modules/soar/handler/variable.go index 1f15bd99b..aeaef2940 100644 --- a/backend/modules/soar/handler/variable.go +++ b/backend/modules/soar/handler/variable.go @@ -36,7 +36,7 @@ func (h *VariableHandler) Create(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - result, err := h.uc.Create(req, loginFromCtx(c)) + result, err := h.uc.Create(c.Request.Context(), req, loginFromCtx(c)) if err != nil { writeARRError(c, err) return @@ -64,7 +64,7 @@ func (h *VariableHandler) Update(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - result, err := h.uc.Update(req, loginFromCtx(c)) + result, err := h.uc.Update(c.Request.Context(), req, loginFromCtx(c)) if err != nil { writeARRError(c, err) return @@ -91,7 +91,7 @@ func (h *VariableHandler) List(c *gin.Context) { Params: database.Params{Page: queryInt(c, "page", 0), Size: queryInt(c, "size", 20)}, VariableName: queryString(c, "variableName"), } - items, total, err := h.uc.FindAll(f) + items, total, err := h.uc.FindAll(c.Request.Context(), f) if err != nil { writeARRError(c, err) return @@ -116,7 +116,7 @@ func (h *VariableHandler) GetByID(c *gin.Context) { if !ok { return } - result, err := h.uc.FindByID(id) + result, err := h.uc.FindByID(c.Request.Context(), id) if err != nil { writeARRError(c, err) return @@ -140,7 +140,7 @@ func (h *VariableHandler) Delete(c *gin.Context) { if !ok { return } - if err := h.uc.Delete(id); err != nil { + if err := h.uc.Delete(c.Request.Context(), id); err != nil { writeARRError(c, err) return } diff --git a/backend/modules/soar/repository/action_command_pg.go b/backend/modules/soar/repository/action_command_pg.go index d5b2ba606..13754f3cc 100644 --- a/backend/modules/soar/repository/action_command_pg.go +++ b/backend/modules/soar/repository/action_command_pg.go @@ -1,6 +1,7 @@ package repository import ( + "context" "errors" "fmt" @@ -20,16 +21,19 @@ func NewActionCommandRepository(db *gorm.DB) *actionCommandRepository { return &actionCommandRepository{db: db} } -func (r *actionCommandRepository) Save(cmd *domain.UtmIncidentActionCommand) error { +func (r *actionCommandRepository) Save(ctx context.Context, cmd *domain.UtmIncidentActionCommand) error { + if cmd.TenantID == "" { + cmd.TenantID = tenantFromCtx(ctx) + } if err := r.db.Save(cmd).Error; err != nil { return fmt.Errorf("actionCommandRepository.Save: %w", err) } return nil } -func (r *actionCommandRepository) FindByID(id int64) (*domain.UtmIncidentActionCommand, error) { +func (r *actionCommandRepository) FindByID(ctx context.Context, id int64) (*domain.UtmIncidentActionCommand, error) { var c domain.UtmIncidentActionCommand - err := r.db.First(&c, id).Error + err := scopeTenant(ctx, r.db).First(&c, id).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, domain.ErrIncidentRecordNotFound } @@ -39,8 +43,8 @@ func (r *actionCommandRepository) FindByID(id int64) (*domain.UtmIncidentActionC return &c, nil } -func (r *actionCommandRepository) FindAll(f dto.ActionCommandFilter) ([]domain.UtmIncidentActionCommand, int64, error) { - q := r.db.Model(&domain.UtmIncidentActionCommand{}) +func (r *actionCommandRepository) FindAll(ctx context.Context, f dto.ActionCommandFilter) ([]domain.UtmIncidentActionCommand, int64, error) { + q := scopeTenant(ctx, r.db.Model(&domain.UtmIncidentActionCommand{})) if f.ActionID != nil { q = q.Where("action_id = ?", *f.ActionID) } @@ -63,8 +67,8 @@ func (r *actionCommandRepository) FindAll(f dto.ActionCommandFilter) ([]domain.U return items, total, nil } -func (r *actionCommandRepository) Delete(id int64) error { - if err := r.db.Delete(&domain.UtmIncidentActionCommand{}, id).Error; err != nil { +func (r *actionCommandRepository) Delete(ctx context.Context, id int64) error { + if err := scopeTenant(ctx, r.db).Delete(&domain.UtmIncidentActionCommand{}, id).Error; err != nil { return fmt.Errorf("actionCommandRepository.Delete: %w", err) } return nil diff --git a/backend/modules/soar/repository/action_pg.go b/backend/modules/soar/repository/action_pg.go index 6476db57f..68f80d403 100644 --- a/backend/modules/soar/repository/action_pg.go +++ b/backend/modules/soar/repository/action_pg.go @@ -1,6 +1,7 @@ package repository import ( + "context" "errors" "fmt" @@ -20,16 +21,19 @@ func NewActionRepository(db *gorm.DB) *actionRepository { return &actionRepository{db: db} } -func (r *actionRepository) Save(action *domain.UtmIncidentAction) error { +func (r *actionRepository) Save(ctx context.Context, action *domain.UtmIncidentAction) error { + if action.TenantID == "" { + action.TenantID = tenantFromCtx(ctx) + } if err := r.db.Save(action).Error; err != nil { return fmt.Errorf("actionRepository.Save: %w", err) } return nil } -func (r *actionRepository) FindByID(id int64) (*domain.UtmIncidentAction, error) { +func (r *actionRepository) FindByID(ctx context.Context, id int64) (*domain.UtmIncidentAction, error) { var a domain.UtmIncidentAction - err := r.db.First(&a, id).Error + err := scopeTenant(ctx, r.db).First(&a, id).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, domain.ErrIncidentRecordNotFound } @@ -39,8 +43,8 @@ func (r *actionRepository) FindByID(id int64) (*domain.UtmIncidentAction, error) return &a, nil } -func (r *actionRepository) FindAll(f dto.ActionFilter) ([]domain.UtmIncidentAction, int64, error) { - q := r.db.Model(&domain.UtmIncidentAction{}) +func (r *actionRepository) FindAll(ctx context.Context, f dto.ActionFilter) ([]domain.UtmIncidentAction, int64, error) { + q := scopeTenant(ctx, r.db.Model(&domain.UtmIncidentAction{})) if f.ActionCommand != nil { q = q.Where("action_command = ?", *f.ActionCommand) } @@ -63,8 +67,8 @@ func (r *actionRepository) FindAll(f dto.ActionFilter) ([]domain.UtmIncidentActi return items, total, nil } -func (r *actionRepository) Delete(id int64) error { - if err := r.db.Delete(&domain.UtmIncidentAction{}, id).Error; err != nil { +func (r *actionRepository) Delete(ctx context.Context, id int64) error { + if err := scopeTenant(ctx, r.db).Delete(&domain.UtmIncidentAction{}, id).Error; err != nil { return fmt.Errorf("actionRepository.Delete: %w", err) } return nil diff --git a/backend/modules/soar/repository/execution_pg.go b/backend/modules/soar/repository/execution_pg.go index d65adb376..06fb83228 100644 --- a/backend/modules/soar/repository/execution_pg.go +++ b/backend/modules/soar/repository/execution_pg.go @@ -9,6 +9,7 @@ import ( "gorm.io/gorm" ) + type pgExecutionRepository struct { db *gorm.DB } @@ -18,6 +19,9 @@ func NewExecutionRepository(db *gorm.DB) connectors.ExecutionRepository { } func (r *pgExecutionRepository) Create(ctx context.Context, e *domain.AlertResponseRuleExecution) (*domain.AlertResponseRuleExecution, error) { + if e.TenantID == "" { + e.TenantID = tenantFromCtx(ctx) + } if err := r.db.WithContext(ctx).Create(e).Error; err != nil { return nil, err } @@ -26,7 +30,7 @@ func (r *pgExecutionRepository) Create(ctx context.Context, e *domain.AlertRespo func (r *pgExecutionRepository) List(ctx context.Context, f connectors.ExecutionFilters) ([]domain.AlertResponseRuleExecution, int64, error) { - q := r.db.WithContext(ctx).Model(&domain.AlertResponseRuleExecution{}) + q := scopeTenant(ctx, r.db.WithContext(ctx).Model(&domain.AlertResponseRuleExecution{})) // id.equals if f.ID != 0 { @@ -95,8 +99,7 @@ func (r *pgExecutionRepository) UpdateStatus(ctx context.Context, id int64, u co return nil } - res := r.db.WithContext(ctx). - Model(&domain.AlertResponseRuleExecution{}). + res := scopeTenant(ctx, r.db.WithContext(ctx).Model(&domain.AlertResponseRuleExecution{})). Where("id = ?", id). Updates(updates) if res.Error != nil { diff --git a/backend/modules/soar/repository/job_pg.go b/backend/modules/soar/repository/job_pg.go index 80600d5b8..324ca10c0 100644 --- a/backend/modules/soar/repository/job_pg.go +++ b/backend/modules/soar/repository/job_pg.go @@ -1,6 +1,7 @@ package repository import ( + "context" "errors" "fmt" @@ -20,16 +21,19 @@ func NewJobRepository(db *gorm.DB) *jobRepository { return &jobRepository{db: db} } -func (r *jobRepository) Save(job *domain.UtmIncidentJob) error { +func (r *jobRepository) Save(ctx context.Context, job *domain.UtmIncidentJob) error { + if job.TenantID == "" { + job.TenantID = tenantFromCtx(ctx) + } if err := r.db.Save(job).Error; err != nil { return fmt.Errorf("jobRepository.Save: %w", err) } return nil } -func (r *jobRepository) FindByID(id int64) (*domain.UtmIncidentJob, error) { +func (r *jobRepository) FindByID(ctx context.Context, id int64) (*domain.UtmIncidentJob, error) { var j domain.UtmIncidentJob - err := r.db.First(&j, id).Error + err := scopeTenant(ctx, r.db).First(&j, id).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, domain.ErrIncidentRecordNotFound } @@ -58,8 +62,8 @@ func (r *jobRepository) applyFilters(q *gorm.DB, f dto.JobFilter) *gorm.DB { return q } -func (r *jobRepository) FindAll(f dto.JobFilter) ([]domain.UtmIncidentJob, int64, error) { - q := r.applyFilters(r.db.Model(&domain.UtmIncidentJob{}), f) +func (r *jobRepository) FindAll(ctx context.Context, f dto.JobFilter) ([]domain.UtmIncidentJob, int64, error) { + q := r.applyFilters(scopeTenant(ctx, r.db.Model(&domain.UtmIncidentJob{})), f) var total int64 if err := q.Count(&total).Error; err != nil { @@ -73,17 +77,17 @@ func (r *jobRepository) FindAll(f dto.JobFilter) ([]domain.UtmIncidentJob, int64 return items, total, nil } -func (r *jobRepository) Count(f dto.JobFilter) (int64, error) { +func (r *jobRepository) Count(ctx context.Context, f dto.JobFilter) (int64, error) { var total int64 - q := r.applyFilters(r.db.Model(&domain.UtmIncidentJob{}), f) + q := r.applyFilters(scopeTenant(ctx, r.db.Model(&domain.UtmIncidentJob{})), f) if err := q.Count(&total).Error; err != nil { return 0, fmt.Errorf("jobRepository.Count: %w", err) } return total, nil } -func (r *jobRepository) Delete(id int64) error { - if err := r.db.Delete(&domain.UtmIncidentJob{}, id).Error; err != nil { +func (r *jobRepository) Delete(ctx context.Context, id int64) error { + if err := scopeTenant(ctx, r.db).Delete(&domain.UtmIncidentJob{}, id).Error; err != nil { return fmt.Errorf("jobRepository.Delete: %w", err) } return nil diff --git a/backend/modules/soar/repository/tenant.go b/backend/modules/soar/repository/tenant.go new file mode 100644 index 000000000..1263bd25e --- /dev/null +++ b/backend/modules/soar/repository/tenant.go @@ -0,0 +1,26 @@ +package repository + +import ( + "context" + + "github.com/utmstack/utmstack/backend/pkg/authz" + "github.com/utmstack/utmstack/backend/pkg/tenancy" + "gorm.io/gorm" +) + +// tenantFromCtx returns the raw tenant-id string from ctx, or "" for on-prem/global actors. +func tenantFromCtx(ctx context.Context) string { + return authz.TenantIDFromContext(ctx) +} + +// scopeTenant narrows q to the acting tenant. Skipped when ctx carries +// tenancy.WithAllTenants (dispatcher cross-tenant scans). +func scopeTenant(ctx context.Context, q *gorm.DB) *gorm.DB { + if tenancy.SpansAllTenants(ctx) { + return q + } + if tid := tenantFromCtx(ctx); tid != "" { + return q.Where("tenant_id = ?", tid) + } + return q +} diff --git a/backend/modules/soar/repository/variable_pg.go b/backend/modules/soar/repository/variable_pg.go index 6d601e14c..f38bf78bc 100644 --- a/backend/modules/soar/repository/variable_pg.go +++ b/backend/modules/soar/repository/variable_pg.go @@ -1,6 +1,7 @@ package repository import ( + "context" "errors" "fmt" @@ -20,16 +21,19 @@ func NewVariableRepository(db *gorm.DB) *variableRepository { return &variableRepository{db: db} } -func (r *variableRepository) Save(v *domain.UtmIncidentVariable) error { +func (r *variableRepository) Save(ctx context.Context, v *domain.UtmIncidentVariable) error { + if v.TenantID == "" { + v.TenantID = tenantFromCtx(ctx) + } if err := r.db.Save(v).Error; err != nil { return fmt.Errorf("variableRepository.Save: %w", err) } return nil } -func (r *variableRepository) FindByID(id int64) (*domain.UtmIncidentVariable, error) { +func (r *variableRepository) FindByID(ctx context.Context, id int64) (*domain.UtmIncidentVariable, error) { var v domain.UtmIncidentVariable - err := r.db.First(&v, id).Error + err := scopeTenant(ctx, r.db).First(&v, id).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, domain.ErrVariableNotFound } @@ -39,8 +43,8 @@ func (r *variableRepository) FindByID(id int64) (*domain.UtmIncidentVariable, er return &v, nil } -func (r *variableRepository) FindAll(f dto.VariableFilter) ([]domain.UtmIncidentVariable, int64, error) { - q := r.db.Model(&domain.UtmIncidentVariable{}) +func (r *variableRepository) FindAll(ctx context.Context, f dto.VariableFilter) ([]domain.UtmIncidentVariable, int64, error) { + q := scopeTenant(ctx, r.db.Model(&domain.UtmIncidentVariable{})) if f.VariableName != nil { q = q.Where("variable_name ILIKE ?", "%"+*f.VariableName+"%") } @@ -57,17 +61,17 @@ func (r *variableRepository) FindAll(f dto.VariableFilter) ([]domain.UtmIncident return items, total, nil } -func (r *variableRepository) FindAllPlain() ([]domain.UtmIncidentVariable, error) { +func (r *variableRepository) FindAllPlain(ctx context.Context) ([]domain.UtmIncidentVariable, error) { var items []domain.UtmIncidentVariable - if err := r.db.Find(&items).Error; err != nil { + if err := scopeTenant(ctx, r.db).Find(&items).Error; err != nil { return nil, fmt.Errorf("variableRepository.FindAllPlain: %w", err) } return items, nil } -func (r *variableRepository) FindByName(name string) (*domain.UtmIncidentVariable, error) { +func (r *variableRepository) FindByName(ctx context.Context, name string) (*domain.UtmIncidentVariable, error) { var v domain.UtmIncidentVariable - err := r.db.Where("variable_name = ?", name).First(&v).Error + err := scopeTenant(ctx, r.db).Where("variable_name = ?", name).First(&v).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, domain.ErrVariableNotFound } @@ -77,19 +81,19 @@ func (r *variableRepository) FindByName(name string) (*domain.UtmIncidentVariabl return &v, nil } -func (r *variableRepository) FindByNames(names []string) ([]domain.UtmIncidentVariable, error) { +func (r *variableRepository) FindByNames(ctx context.Context, names []string) ([]domain.UtmIncidentVariable, error) { if len(names) == 0 { return nil, nil } var items []domain.UtmIncidentVariable - if err := r.db.Where("variable_name IN ?", names).Find(&items).Error; err != nil { + if err := scopeTenant(ctx, r.db).Where("variable_name IN ?", names).Find(&items).Error; err != nil { return nil, fmt.Errorf("variableRepository.FindByNames: %w", err) } return items, nil } -func (r *variableRepository) Delete(id int64) error { - if err := r.db.Delete(&domain.UtmIncidentVariable{}, id).Error; err != nil { +func (r *variableRepository) Delete(ctx context.Context, id int64) error { + if err := scopeTenant(ctx, r.db).Delete(&domain.UtmIncidentVariable{}, id).Error; err != nil { return fmt.Errorf("variableRepository.Delete: %w", err) } return nil diff --git a/backend/modules/soar/usecase/action.go b/backend/modules/soar/usecase/action.go index 10a127c29..5b2b459a6 100644 --- a/backend/modules/soar/usecase/action.go +++ b/backend/modules/soar/usecase/action.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "fmt" "time" @@ -17,7 +18,7 @@ func NewActionUsecase(repo connectors.ActionRepository) connectors.ActionUsecase return &actionUsecase{repo: repo} } -func (u *actionUsecase) Create(req dto.CreateActionRequest, user string) (*domain.UtmIncidentAction, error) { +func (u *actionUsecase) Create(ctx context.Context, req dto.CreateActionRequest, user string) (*domain.UtmIncidentAction, error) { a := &domain.UtmIncidentAction{ ActionCommand: req.ActionCommand, ActionDescription: req.ActionDescription, @@ -27,14 +28,14 @@ func (u *actionUsecase) Create(req dto.CreateActionRequest, user string) (*domai CreatedDate: time.Now().UTC(), CreatedUser: user, } - if err := u.repo.Save(a); err != nil { + if err := u.repo.Save(ctx, a); err != nil { return nil, fmt.Errorf("actionUsecase.Create: %w", err) } return a, nil } -func (u *actionUsecase) Update(req dto.UpdateActionRequest, user string) (*domain.UtmIncidentAction, error) { - a, err := u.repo.FindByID(req.ID) +func (u *actionUsecase) Update(ctx context.Context, req dto.UpdateActionRequest, user string) (*domain.UtmIncidentAction, error) { + a, err := u.repo.FindByID(ctx, req.ID) if err != nil { return nil, err } @@ -46,20 +47,20 @@ func (u *actionUsecase) Update(req dto.UpdateActionRequest, user string) (*domai a.ActionEditable = req.ActionEditable a.ModifiedDate = &now a.ModifiedUser = &user - if err := u.repo.Save(a); err != nil { + if err := u.repo.Save(ctx, a); err != nil { return nil, fmt.Errorf("actionUsecase.Update: %w", err) } return a, nil } -func (u *actionUsecase) FindByID(id int64) (*domain.UtmIncidentAction, error) { - return u.repo.FindByID(id) +func (u *actionUsecase) FindByID(ctx context.Context, id int64) (*domain.UtmIncidentAction, error) { + return u.repo.FindByID(ctx, id) } -func (u *actionUsecase) FindAll(f dto.ActionFilter) ([]domain.UtmIncidentAction, int64, error) { - return u.repo.FindAll(f) +func (u *actionUsecase) FindAll(ctx context.Context, f dto.ActionFilter) ([]domain.UtmIncidentAction, int64, error) { + return u.repo.FindAll(ctx, f) } -func (u *actionUsecase) Delete(id int64) error { - return u.repo.Delete(id) +func (u *actionUsecase) Delete(ctx context.Context, id int64) error { + return u.repo.Delete(ctx, id) } diff --git a/backend/modules/soar/usecase/action_command.go b/backend/modules/soar/usecase/action_command.go index 333fa1b01..e9046a0aa 100644 --- a/backend/modules/soar/usecase/action_command.go +++ b/backend/modules/soar/usecase/action_command.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "fmt" "github.com/utmstack/utmstack/backend/modules/soar/connectors" @@ -16,40 +17,40 @@ func NewActionCommandUsecase(repo connectors.ActionCommandRepository) connectors return &actionCommandUsecase{repo: repo} } -func (u *actionCommandUsecase) Create(req dto.CreateActionCommandRequest) (*domain.UtmIncidentActionCommand, error) { +func (u *actionCommandUsecase) Create(ctx context.Context, req dto.CreateActionCommandRequest) (*domain.UtmIncidentActionCommand, error) { c := &domain.UtmIncidentActionCommand{ ActionID: req.ActionID, OsPlatform: req.OsPlatform, Command: req.Command, } - if err := u.repo.Save(c); err != nil { + if err := u.repo.Save(ctx, c); err != nil { return nil, fmt.Errorf("actionCommandUsecase.Create: %w", err) } return c, nil } -func (u *actionCommandUsecase) Update(req dto.UpdateActionCommandRequest) (*domain.UtmIncidentActionCommand, error) { - c, err := u.repo.FindByID(req.ID) +func (u *actionCommandUsecase) Update(ctx context.Context, req dto.UpdateActionCommandRequest) (*domain.UtmIncidentActionCommand, error) { + c, err := u.repo.FindByID(ctx, req.ID) if err != nil { return nil, err } c.ActionID = req.ActionID c.OsPlatform = req.OsPlatform c.Command = req.Command - if err := u.repo.Save(c); err != nil { + if err := u.repo.Save(ctx, c); err != nil { return nil, fmt.Errorf("actionCommandUsecase.Update: %w", err) } return c, nil } -func (u *actionCommandUsecase) FindByID(id int64) (*domain.UtmIncidentActionCommand, error) { - return u.repo.FindByID(id) +func (u *actionCommandUsecase) FindByID(ctx context.Context, id int64) (*domain.UtmIncidentActionCommand, error) { + return u.repo.FindByID(ctx, id) } -func (u *actionCommandUsecase) FindAll(f dto.ActionCommandFilter) ([]domain.UtmIncidentActionCommand, int64, error) { - return u.repo.FindAll(f) +func (u *actionCommandUsecase) FindAll(ctx context.Context, f dto.ActionCommandFilter) ([]domain.UtmIncidentActionCommand, int64, error) { + return u.repo.FindAll(ctx, f) } -func (u *actionCommandUsecase) Delete(id int64) error { - return u.repo.Delete(id) +func (u *actionCommandUsecase) Delete(ctx context.Context, id int64) error { + return u.repo.Delete(ctx, id) } diff --git a/backend/modules/soar/usecase/dispatch.go b/backend/modules/soar/usecase/dispatch.go index 7e55ef68c..f9c5fb8c5 100644 --- a/backend/modules/soar/usecase/dispatch.go +++ b/backend/modules/soar/usecase/dispatch.go @@ -150,7 +150,7 @@ func (d *Dispatcher) process(parent context.Context, exec domain.AlertResponseRu command := exec.Command if d.vars != nil { - interpolated, ierr := d.vars.InterpolateCommand(exec.Command) + interpolated, ierr := d.vars.InterpolateCommand(ctx, exec.Command) if ierr != nil { _ = catcher.Error("soar dispatch: variable interpolation failed", ierr, map[string]any{"execution": exec.ID}) } @@ -182,7 +182,7 @@ func (d *Dispatcher) process(parent context.Context, exec domain.AlertResponseRu result := res.GetResult() if d.vars != nil { - masked, merr := d.vars.MaskSecrets(result) + masked, merr := d.vars.MaskSecrets(ctx, result) if merr != nil { _ = catcher.Error("soar dispatch: mask secrets failed", merr, map[string]any{"execution": exec.ID}) } diff --git a/backend/modules/soar/usecase/job.go b/backend/modules/soar/usecase/job.go index 173acfb98..4630ad7b6 100644 --- a/backend/modules/soar/usecase/job.go +++ b/backend/modules/soar/usecase/job.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "fmt" "time" @@ -17,7 +18,7 @@ func NewJobUsecase(repo connectors.JobRepository) connectors.JobUsecase { return &jobUsecase{repo: repo} } -func (u *jobUsecase) Create(req dto.CreateJobRequest, user string) (*domain.UtmIncidentJob, error) { +func (u *jobUsecase) Create(ctx context.Context, req dto.CreateJobRequest, user string) (*domain.UtmIncidentJob, error) { now := time.Now().UTC() j := &domain.UtmIncidentJob{ @@ -30,24 +31,24 @@ func (u *jobUsecase) Create(req dto.CreateJobRequest, user string) (*domain.UtmI CreatedDate: now, CreatedUser: user, } - if err := u.repo.Save(j); err != nil { + if err := u.repo.Save(ctx, j); err != nil { return nil, fmt.Errorf("jobUsecase.Create: %w", err) } return j, nil } -func (u *jobUsecase) FindByID(id int64) (*domain.UtmIncidentJob, error) { - return u.repo.FindByID(id) +func (u *jobUsecase) FindByID(ctx context.Context, id int64) (*domain.UtmIncidentJob, error) { + return u.repo.FindByID(ctx, id) } -func (u *jobUsecase) FindAll(f dto.JobFilter) ([]domain.UtmIncidentJob, int64, error) { - return u.repo.FindAll(f) +func (u *jobUsecase) FindAll(ctx context.Context, f dto.JobFilter) ([]domain.UtmIncidentJob, int64, error) { + return u.repo.FindAll(ctx, f) } -func (u *jobUsecase) Count(f dto.JobFilter) (int64, error) { - return u.repo.Count(f) +func (u *jobUsecase) Count(ctx context.Context, f dto.JobFilter) (int64, error) { + return u.repo.Count(ctx, f) } -func (u *jobUsecase) Delete(id int64) error { - return u.repo.Delete(id) +func (u *jobUsecase) Delete(ctx context.Context, id int64) error { + return u.repo.Delete(ctx, id) } diff --git a/backend/modules/soar/usecase/variable.go b/backend/modules/soar/usecase/variable.go index 3cc234782..3375e1ce1 100644 --- a/backend/modules/soar/usecase/variable.go +++ b/backend/modules/soar/usecase/variable.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "fmt" "regexp" "strings" @@ -22,7 +23,7 @@ func NewVariableUsecase(repo connectors.VariableRepository, cipher connectors.Va return &variableUsecase{repo: repo, cipher: cipher} } -func (u *variableUsecase) Create(req dto.CreateVariableRequest, user string) (*dto.VariableResponse, error) { +func (u *variableUsecase) Create(ctx context.Context, req dto.CreateVariableRequest, user string) (*dto.VariableResponse, error) { now := time.Now().UTC() v := &domain.UtmIncidentVariable{ VariableDescription: req.VariableDescription, @@ -43,14 +44,14 @@ func (u *variableUsecase) Create(req dto.CreateVariableRequest, user string) (*d v.VariableValue = req.VariableValue } - if err := u.repo.Save(v); err != nil { + if err := u.repo.Save(ctx, v); err != nil { return nil, fmt.Errorf("variableUsecase.Create: %w", err) } return u.toResponse(v), nil } -func (u *variableUsecase) Update(req dto.UpdateVariableRequest, user string) (*dto.VariableResponse, error) { - v, err := u.repo.FindByID(req.ID) +func (u *variableUsecase) Update(ctx context.Context, req dto.UpdateVariableRequest, user string) (*dto.VariableResponse, error) { + v, err := u.repo.FindByID(ctx, req.ID) if err != nil { return nil, err } @@ -73,22 +74,22 @@ func (u *variableUsecase) Update(req dto.UpdateVariableRequest, user string) (*d v.VariableValue = req.VariableValue } - if err := u.repo.Save(v); err != nil { + if err := u.repo.Save(ctx, v); err != nil { return nil, fmt.Errorf("variableUsecase.Update: %w", err) } return u.toResponse(v), nil } -func (u *variableUsecase) FindByID(id int64) (*dto.VariableResponse, error) { - v, err := u.repo.FindByID(id) +func (u *variableUsecase) FindByID(ctx context.Context, id int64) (*dto.VariableResponse, error) { + v, err := u.repo.FindByID(ctx, id) if err != nil { return nil, err } return u.toResponse(v), nil } -func (u *variableUsecase) FindAll(f dto.VariableFilter) ([]dto.VariableResponse, int64, error) { - items, total, err := u.repo.FindAll(f) +func (u *variableUsecase) FindAll(ctx context.Context, f dto.VariableFilter) ([]dto.VariableResponse, int64, error) { + items, total, err := u.repo.FindAll(ctx, f) if err != nil { return nil, 0, err } @@ -99,11 +100,11 @@ func (u *variableUsecase) FindAll(f dto.VariableFilter) ([]dto.VariableResponse, return resp, total, nil } -func (u *variableUsecase) Delete(id int64) error { - return u.repo.Delete(id) +func (u *variableUsecase) Delete(ctx context.Context, id int64) error { + return u.repo.Delete(ctx, id) } -func (u *variableUsecase) InterpolateCommand(cmd string) (string, error) { +func (u *variableUsecase) InterpolateCommand(ctx context.Context, cmd string) (string, error) { matches := variableInterpolationRegex.FindAllStringSubmatch(cmd, -1) if len(matches) == 0 { return cmd, nil @@ -117,7 +118,7 @@ func (u *variableUsecase) InterpolateCommand(cmd string) (string, error) { } } - vars, err := u.repo.FindByNames(names) + vars, err := u.repo.FindByNames(ctx, names) if err != nil { return cmd, fmt.Errorf("InterpolateCommand: %w", err) } @@ -143,8 +144,8 @@ func (u *variableUsecase) InterpolateCommand(cmd string) (string, error) { return cmd, nil } -func (u *variableUsecase) MaskSecrets(output string) (string, error) { - vars, err := u.repo.FindAllPlain() +func (u *variableUsecase) MaskSecrets(ctx context.Context, output string) (string, error) { + vars, err := u.repo.FindAllPlain(ctx) if err != nil { return output, fmt.Errorf("MaskSecrets: %w", err) } From 74c0e30155330f78da3e6246e804f98b09921244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Fri, 7 Aug 2026 12:39:41 -0600 Subject: [PATCH 2/2] fix[frontend](soar): fixed command concatenation drawer scroll and bottom margin --- .../features/soar/components/FlowEditor.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/soar/components/FlowEditor.tsx b/frontend/src/features/soar/components/FlowEditor.tsx index ebfff5baf..22ab1ddad 100644 --- a/frontend/src/features/soar/components/FlowEditor.tsx +++ b/frontend/src/features/soar/components/FlowEditor.tsx @@ -41,6 +41,7 @@ export function FlowEditor({ const [confirmDelete, setConfirmDelete] = useState(false) const [agents, setAgents] = useState([]) + const scrollRef = useRef(null) const set = (k: K, v: FlowFormState[K]) => setForm((f) => ({ ...f, [k]: v })) @@ -200,7 +201,7 @@ export function FlowEditor({ ) : ( -
+
{/* Flow identity */}
@@ -272,7 +273,7 @@ export function FlowEditor({ - set('commands', c)} t={t} /> + set('commands', c)} scrollRef={scrollRef} t={t} />
@@ -424,6 +425,7 @@ function CommandsEditor({ shell, readOnly, onChange, + scrollRef, t, }: { commands: FlowCommand[] @@ -431,6 +433,7 @@ function CommandsEditor({ shell: string readOnly?: boolean onChange: (c: FlowCommand[]) => void + scrollRef?: React.RefObject t: ReturnType['t'] }) { const refs = useRef<(HTMLInputElement | null)[]>([]) @@ -441,6 +444,15 @@ function CommandsEditor({ const fieldRef = useRef(null) const templatesRef = useRef(null) const shellKind = shellKindFor(agentPlatform, shell) + const prevLen = useRef(commands.length) + + useEffect(() => { + if (commands.length > prevLen.current) { + const el = scrollRef?.current + if (el) requestAnimationFrame(() => el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })) + } + prevLen.current = commands.length + }, [commands.length, scrollRef]) useEffect(() => { if (!fieldOpen) return @@ -528,7 +540,7 @@ function CommandsEditor({
{commands.map((cmd, i) => ( -
+
{i > 0 && (