From 804be01c2f93d2ae71f56f7a75457ae53251d3e7 Mon Sep 17 00:00:00 2001 From: Vijay Joshi Date: Thu, 8 Aug 2024 19:20:04 +0530 Subject: [PATCH] INFRA-3467 : Private Houston Incidents (#445) * INFRA-3467 : Private Houston Incidents * INFRA-3627 : Minor self review * INFRA-3627 : PR Review changes INFRA-3627 : Minor changes INFRA-3627 : UT fix INFRA-3637 : Message changes INFRA-3627 : Minor changes INFRA-3627 : Constant fix INFRA-3627 : Do not post SLA breach in public channels for private incidents --- Makefile | 1 + appcontext/app.go | 12 ++++ cmd/app/handler/incident_user_handler.go | 42 +++++++++++++ cmd/app/handler/slack_handler.go | 4 ++ cmd/app/server.go | 15 ++++- common/util/constant.go | 8 +++ db/migration/000028_private_incidents.up.sql | 12 ++++ .../processor/action/member_join_action.go | 38 +++++++++++- .../action/member_left_channel_action.go | 55 +++++++++++++++++ .../processor/events_api_event_processor.go | 33 ++++++++++ model/incident/entity.go | 2 + model/incident/incident.go | 52 +++++++++++++++- .../incident/incident_repository_interface.go | 4 +- model/incident/model.go | 2 + model/incidentUser/entity.go | 30 ++++++++++ model/incidentUser/model.go | 14 +++++ .../incident_jira_repository_impl.go | 2 +- .../incident_user_repository_impl.go | 32 ++++++++++ .../incident_user_repository_interface.go | 16 +++++ service/auth_service.go | 47 +++++++++++++++ .../incident/impl/incident_service_test.go | 4 +- service/incident/impl/incident_service_v2.go | 21 +++++-- .../incident/impl/incident_update_status.go | 4 ++ .../incident/incident_service_v2_interface.go | 3 +- .../incident_user_service_impl.go | 38 ++++++++++++ .../incident_user_service_interface.go | 17 ++++++ .../incident_user_service_test.go | 60 +++++++++++++++++++ service/incident_service.go | 28 +++++++-- .../incident_orchestrator_impl.go | 6 +- service/reminder/sla_breach_reminder.go | 2 +- service/reminder/team_incidents_reminder.go | 3 +- service/request/incident/create_incident.go | 1 + .../request/incidentUser/add_incident_user.go | 6 ++ service/slack/slack_service.go | 8 +-- service/slack/slack_service_interface.go | 2 +- service/slack/slack_service_test.go | 2 +- 36 files changed, 597 insertions(+), 29 deletions(-) create mode 100644 cmd/app/handler/incident_user_handler.go create mode 100644 db/migration/000028_private_incidents.up.sql create mode 100644 internal/processor/action/member_left_channel_action.go create mode 100644 model/incidentUser/entity.go create mode 100644 model/incidentUser/model.go create mode 100644 repository/incidentUser/incident_user_repository_impl.go create mode 100644 repository/incidentUser/incident_user_repository_interface.go create mode 100644 service/incidentUser/incident_user_service_impl.go create mode 100644 service/incidentUser/incident_user_service_interface.go create mode 100644 service/incidentUser/incident_user_service_test.go create mode 100644 service/request/incidentUser/add_incident_user.go diff --git a/Makefile b/Makefile index 19631c9..6cd96a4 100644 --- a/Makefile +++ b/Makefile @@ -84,3 +84,4 @@ generatemocks: cd $(CURDIR)/model/incident_channel && minimock -i IncidentChannelRepositoryInterface -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/repository/incidentStatus && minimock -i IncidentStatusRepository -s _mock.go -o $(CURDIR)/mocks/incidentStatus cd $(CURDIR)/service/incidentStatus && minimock -i IncidentStatusService -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/repository/incidentUser && minimock -i IncidentUserRepository -s _mock.go -o $(CURDIR)/mocks diff --git a/appcontext/app.go b/appcontext/app.go index 502650b..1e86999 100644 --- a/appcontext/app.go +++ b/appcontext/app.go @@ -18,6 +18,7 @@ import ( "houston/pkg/rest" "houston/repository/externalTeamRepo" incidentStatusRepo "houston/repository/incidentStatus" + incidentUserRepo "houston/repository/incidentUser" rcaRepository "houston/repository/rca/impl" "houston/repository/rcaInput" "houston/repository/severity" @@ -30,6 +31,7 @@ import ( incidentService "houston/service/incident/impl" "houston/service/incidentProducts" "houston/service/incidentStatus" + "houston/service/incidentUser" "houston/service/products" "houston/service/productsTeams" rcaService "houston/service/rca/impl" @@ -79,6 +81,7 @@ type houstonServices struct { teamService teamService.ITeamServiceV2 reminderService reminder.ReminderService incidentStatusService incidentStatus.IncidentStatusService + incidentUserService incidentUser.IncidentUserService } var appContext *applicationContext @@ -120,6 +123,7 @@ func InitializeServices() { teamUserService: initTeamUserService(), teamUserSeverityService: initTeamUserSeverityService(), incidentStatusService: incidentStatusService, + incidentUserService: initIncidentUserService(), } services.userService = initUserService() services.teamService = initTeamService() @@ -376,3 +380,11 @@ func initReminderService() reminder.ReminderService { func GetReminderService() reminder.ReminderService { return services.reminderService } + +func initIncidentUserService() incidentUser.IncidentUserService { + return incidentUser.NewIncidentUserService(incidentUserRepo.NewIncidentUserRepository(GetDB())) +} + +func GetIncidentUserService() incidentUser.IncidentUserService { + return services.incidentUserService +} diff --git a/cmd/app/handler/incident_user_handler.go b/cmd/app/handler/incident_user_handler.go new file mode 100644 index 0000000..0b34e0b --- /dev/null +++ b/cmd/app/handler/incident_user_handler.go @@ -0,0 +1,42 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "houston/common/metrics" + "houston/common/util" + "houston/service/incidentUser" + incidentUserRequest "houston/service/request/incidentUser" + common "houston/service/response/common" + "net/http" +) + +type IncidentUserHandler struct { + gin *gin.Engine + service incidentUser.IncidentUserService +} + +func NewIncidentUserHandler( + gin *gin.Engine, + incidentUserService incidentUser.IncidentUserService, +) *IncidentUserHandler { + return &IncidentUserHandler{ + gin: gin, + service: incidentUserService, + } +} + +func (handler *IncidentUserHandler) HandleAddIncidentUser(c *gin.Context) { + var addIncidentUserRequest incidentUserRequest.AddIncidentUserRequest + if err := c.ShouldBindJSON(&addIncidentUserRequest); err != nil { + c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil)) + return + } + + err := handler.service.AddIncidentUser(addIncidentUserRequest) + if err != nil { + c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil)) + metrics.PublishHoustonFlowFailureMetrics(util.ADD_INCIDENT_USER_MAPPING, err.Error()) + return + } + c.JSON(http.StatusOK, common.SuccessResponse("User added to incident successfully", http.StatusOK)) +} diff --git a/cmd/app/handler/slack_handler.go b/cmd/app/handler/slack_handler.go index 4b648cf..b82801f 100644 --- a/cmd/app/handler/slack_handler.go +++ b/cmd/app/handler/slack_handler.go @@ -42,6 +42,7 @@ type slackHandler struct { viewSubmissionProcessor *processor.ViewSubmissionProcessor userChangeEventProcessor *processor.UserChangeEventProcessor houstonCommandResolver *resolver.HoustonCommandResolver + memberLeftChannelEventProcessor *processor.MemberLeftChannelCallbackEventProcessor } func NewSlackHandler( @@ -113,6 +114,7 @@ func NewSlackHandler( houstonCommandResolver: resolver.NewHoustonCommandResolver( socketModeClient, slackbotClient, rcaService, productsService, orchestrator, ), + memberLeftChannelEventProcessor: processor.NewMemberLeftChannelCallbackEventProcessor(socketModeClient, *incidentServiceV2, appcontext.GetUserService()), } } @@ -163,6 +165,8 @@ func (sh *slackHandler) HoustonConnect() { switch innerEventData := innerEvent.Data.(type) { case *slackevents.MemberJoinedChannelEvent: sh.memberJoinCallbackProcessor.ProcessCommand(innerEventData, evt.Request) + case *slackevents.MemberLeftChannelEvent: + sh.memberLeftChannelEventProcessor.ProcessCommand(innerEventData, evt.Request) } } } diff --git a/cmd/app/server.go b/cmd/app/server.go index 0ac16e5..887294d 100644 --- a/cmd/app/server.go +++ b/cmd/app/server.go @@ -70,6 +70,7 @@ func (s *Server) Handler(houstonGroup *gin.RouterGroup) { s.usersHandler(houstonGroup) s.tagHandler(houstonGroup) s.filtersHandler(houstonGroup) + s.incidentUserHandler(houstonGroup) //this should always be at the end since it opens websocket to connect to slackbot s.houstonHandler() @@ -235,8 +236,10 @@ func (s *Server) incidentHandler(houstonGroup *gin.RouterGroup) { s.gin.GET("/incidents/:id", incidentHandler.GetIncidents) s.gin.GET("/incidents/header", incidentHandler.GetIncidentHeader) - houstonGroup.GET("/incidents", incidentHandler.GetIncidents) - houstonGroup.GET("/incidents/:id", incidentHandler.GetIncidents) + houstonGroup.GET("/incidents", s.authService.IfValidHoustonUser(incidentHandler.GetIncidents)) + houstonGroup.GET("/incidents/:id", s.authService.CanUserAccessIncident( + s.authService.IfValidHoustonUser(incidentHandler.GetIncidents), + )) houstonGroup.GET("/incidents/header", incidentHandler.GetIncidentHeader) houstonGroup.GET("/teamIncidents/:teamId", incidentHandler.GetTeamIncidents) } @@ -247,9 +250,15 @@ func (s *Server) incidentChannelHandler(houstonGroup *gin.RouterGroup) { houstonGroup.POST("/incident-channel/archive", incidentChannelHandler.HandleArchiveIncidentChannels) } +func (s *Server) incidentUserHandler(houstonGroup *gin.RouterGroup) { + incidentUserHandler := handler.NewIncidentUserHandler(s.gin, appcontext.GetIncidentUserService()) + + houstonGroup.POST("/incident-user", s.authService.IfAdmin(incidentUserHandler.HandleAddIncidentUser)) +} + func (s *Server) logHandler(houstonGroup *gin.RouterGroup) { logHandler := service.NewLogService(s.gin, s.db) - houstonGroup.GET("/logs/:log_type/:id", logHandler.GetLogs) + houstonGroup.GET("/logs/:log_type/:id", s.authService.CanUserAccessIncidentLogs(logHandler.GetLogs)) } func (s *Server) rcaHandler(houstonGroup *gin.RouterGroup) { diff --git a/common/util/constant.go b/common/util/constant.go index 3729aaf..568d0cb 100644 --- a/common/util/constant.go +++ b/common/util/constant.go @@ -134,3 +134,11 @@ const ( const ( ARCHIVAL_BATCH_SIZE = 20 ) + +const ( + IncidentLogType = "incident" +) + +const ( + ADD_INCIDENT_USER_MAPPING = "ADD_INCIDENT_USER_MAPPING" +) diff --git a/db/migration/000028_private_incidents.up.sql b/db/migration/000028_private_incidents.up.sql new file mode 100644 index 0000000..0fc4b77 --- /dev/null +++ b/db/migration/000028_private_incidents.up.sql @@ -0,0 +1,12 @@ +BEGIN; + +ALTER TABLE incident ADD COLUMN is_private boolean DEFAULT false; + +CREATE TABLE incident_user ( + id SERIAL PRIMARY KEY, + incident_id INTEGER NOT NULL REFERENCES incident(id), + user_id INTEGER NOT NULL REFERENCES houston_user(id), + UNIQUE (incident_id, user_id) +); + +COMMIT; \ No newline at end of file diff --git a/internal/processor/action/member_join_action.go b/internal/processor/action/member_join_action.go index 590491d..e79cfd6 100644 --- a/internal/processor/action/member_join_action.go +++ b/internal/processor/action/member_join_action.go @@ -6,12 +6,17 @@ import ( "github.com/slack-go/slack/socketmode" "go.uber.org/zap" "houston/appcontext" + "houston/common/metrics" + "houston/common/util" "houston/internal/processor/action/view" "houston/logger" "houston/model/incident" "houston/model/team" "houston/repository/severity" incidentStatusService "houston/service/incidentStatus" + "houston/service/incidentUser" + incidentUserRequest "houston/service/request/incidentUser" + userService "houston/service/user" ) type MemberJoinAction struct { @@ -20,6 +25,8 @@ type MemberJoinAction struct { teamService *team.Repository severityService *severity.Repository incidentStatusService incidentStatusService.IncidentStatusService + userService userService.UserService + incidentUserService incidentUser.IncidentUserService } func NewMemberJoinAction(socketModeClient *socketmode.Client, incidentService *incident.Repository, teamService *team.Repository, severityService *severity.Repository) *MemberJoinAction { @@ -29,11 +36,13 @@ func NewMemberJoinAction(socketModeClient *socketmode.Client, incidentService *i teamService: teamService, severityService: severityService, incidentStatusService: appcontext.GetIncidentStatusService(), + userService: appcontext.GetUserService(), + incidentUserService: appcontext.GetIncidentUserService(), } } func (mp *MemberJoinAction) PerformAction(memberJoinedChannelEvent *slackevents.MemberJoinedChannelEvent) { - logger.Info("processing member join action", zap.String("channel", memberJoinedChannelEvent.Channel)) + logger.Info(fmt.Sprintf("processing member joined channel event: %v", memberJoinedChannelEvent)) incidentEntity, err := mp.incidentService.FindIncidentByChannelId(memberJoinedChannelEvent.Channel) if err != nil { @@ -46,6 +55,14 @@ func (mp *MemberJoinAction) PerformAction(memberJoinedChannelEvent *slackevents. return } + go func() { + err := mp.addIncidentUserMapping(incidentEntity.ID, memberJoinedChannelEvent.User) + if err != nil { + logger.Error(fmt.Sprintf("failed to add incident-user mapping"), zap.Error(err)) + metrics.PublishHoustonFlowFailureMetrics(util.ADD_INCIDENT_USER_MAPPING, err.Error()) + } + }() + teamEntity, err := mp.teamService.FindTeamById(incidentEntity.TeamId) if err != nil { logger.Error("error in fetching team", zap.String("channel", memberJoinedChannelEvent.Channel), @@ -98,3 +115,22 @@ func (mp *MemberJoinAction) PerformAction(memberJoinedChannelEvent *slackevents. logger.Error("post response failed", zap.Error(err)) } } + +func (mp *MemberJoinAction) addIncidentUserMapping(incidentId uint, slackUserId string) error { + user, err := mp.userService.GetHoustonUserBySlackUserId(slackUserId) + if err != nil { + logger.Error(fmt.Sprintf("failed to get user with id %s : %v", slackUserId, err)) + return err + } else if user == nil { + errMsg := fmt.Sprintf("user with id %s not found", slackUserId) + logger.Error(errMsg) + return fmt.Errorf(errMsg) + } + + return mp.incidentUserService.AddIncidentUser( + incidentUserRequest.AddIncidentUserRequest{ + IncidentID: incidentId, + UserID: user.ID, + }, + ) +} diff --git a/internal/processor/action/member_left_channel_action.go b/internal/processor/action/member_left_channel_action.go new file mode 100644 index 0000000..2706a6d --- /dev/null +++ b/internal/processor/action/member_left_channel_action.go @@ -0,0 +1,55 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack/slackevents" + "houston/appcontext" + "houston/logger" + incidentServiceV2 "houston/service/incident/impl" + "houston/service/incidentUser" + userService "houston/service/user" +) + +type MemberLeftChannelAction struct { + incidentService incidentServiceV2.IncidentServiceV2 + userService userService.UserService + incidentUserService incidentUser.IncidentUserService +} + +const mlcaLogTag = "[member-left-channel-action]" + +func NewMemberLeftChannelAction(incidentService incidentServiceV2.IncidentServiceV2, userService userService.UserService) *MemberLeftChannelAction { + return &MemberLeftChannelAction{ + incidentService: incidentService, + userService: userService, + incidentUserService: appcontext.GetIncidentUserService(), + } +} + +func (mlca *MemberLeftChannelAction) PerformAction(memberLeftChannelEvent *slackevents.MemberLeftChannelEvent) error { + logger.Info(fmt.Sprintf("%s processing member left channel event: %v", mlcaLogTag, memberLeftChannelEvent)) + + incidentData, err := mlca.incidentService.GetIncidentByChannelID(memberLeftChannelEvent.Channel) + if err != nil { + errMsg := fmt.Sprintf("%s error in searching incident %v", mlcaLogTag, err) + logger.Error(errMsg) + return fmt.Errorf(errMsg) + } else if incidentData == nil { + errMsg := fmt.Sprintf("%s incident not found", mlcaLogTag) + logger.Info(errMsg) + return fmt.Errorf(errMsg) + } + + user, err := mlca.userService.GetHoustonUserBySlackUserId(memberLeftChannelEvent.User) + if err != nil { + errMsg := fmt.Sprintf("%s failed to get user with id %s : %v", mlcaLogTag, memberLeftChannelEvent.User, err) + logger.Error(errMsg) + return fmt.Errorf(errMsg) + } else if user == nil { + errMsg := fmt.Sprintf("%s user with id %s not found", mlcaLogTag, memberLeftChannelEvent.User) + logger.Info(errMsg) + return fmt.Errorf(errMsg) + } + + return mlca.incidentUserService.RemoveIncidentUser(incidentData.ID, user.ID) +} diff --git a/internal/processor/events_api_event_processor.go b/internal/processor/events_api_event_processor.go index 836fd1e..ecdbab7 100644 --- a/internal/processor/events_api_event_processor.go +++ b/internal/processor/events_api_event_processor.go @@ -4,12 +4,15 @@ import ( "fmt" "github.com/slack-go/slack/slackevents" "github.com/slack-go/slack/socketmode" + "houston/common/metrics" "houston/internal/processor/action" "houston/logger" "houston/model/incident" "houston/model/team" "houston/model/user" "houston/repository/severity" + incidentServiceV2 "houston/service/incident/impl" + userService "houston/service/user" ) type eventsApiEventProcessor interface { @@ -64,3 +67,33 @@ func (ucep *UserChangeEventProcessor) ProcessCommand(event slackevents.EventsAPI var payload interface{} ucep.socketModeClient.Ack(*request, payload) } + +type MemberLeftChannelCallbackEventProcessor struct { + socketModeClient *socketmode.Client + memberLeftChannelAction *action.MemberLeftChannelAction +} + +func NewMemberLeftChannelCallbackEventProcessor(socketModeClient *socketmode.Client, incidentService incidentServiceV2.IncidentServiceV2, userService userService.UserService) *MemberLeftChannelCallbackEventProcessor { + return &MemberLeftChannelCallbackEventProcessor{ + socketModeClient: socketModeClient, + memberLeftChannelAction: action.NewMemberLeftChannelAction(incidentService, userService), + } +} + +func (mlcc *MemberLeftChannelCallbackEventProcessor) ProcessCommand(event *slackevents.MemberLeftChannelEvent, request *socketmode.Request) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("[MLCC] Exception occurred: %v", r.(error))) + } + }() + + err := mlcc.memberLeftChannelAction.PerformAction(event) + if err != nil { + logger.Error(fmt.Sprintf("failed to perform action: %v", err)) + metrics.PublishHoustonFlowFailureMetrics(FLOW_NAME, err.Error()) + } + var payload interface{} + mlcc.socketModeClient.Ack(*request, payload) +} + +const FLOW_NAME = "REMOVE_INCIDENT_USER_MAPPING" diff --git a/model/incident/entity.go b/model/incident/entity.go index 4a56961..9994f71 100644 --- a/model/incident/entity.go +++ b/model/incident/entity.go @@ -72,6 +72,7 @@ type IncidentEntity struct { Team team.TeamEntity `gorm:"foreignKey:TeamId"` Severity severity.SeverityEntity `gorm:"foreignKey:SeverityId"` ReportingTeam team.TeamEntity `gorm:"foreignKey:ReportingTeamId"` + IsPrivate bool `gorm:"column:is_private"` } func (IncidentEntity) TableName() string { @@ -104,6 +105,7 @@ func (i IncidentEntity) ToDTO() IncidentDTO { Team: *(i.Team).ToDTO(), Severity: i.Severity.ToDTO(), ReportingTeam: *(i.ReportingTeam).ToDTO(), + IsPrivate: i.IsPrivate, } } diff --git a/model/incident/incident.go b/model/incident/incident.go index 054634c..429dde7 100644 --- a/model/incident/incident.go +++ b/model/incident/incident.go @@ -84,6 +84,7 @@ func (r *Repository) CreateIncidentEntity(request *CreateIncidentDTO, tx *gorm.D MetaData: request.MetaData, ReportingTeamId: request.ReportingTeamID, Products: products, + IsPrivate: request.IsPrivate, } tx.Create(incidentEntity) @@ -288,7 +289,7 @@ func (r *Repository) FindIncidentById(Id uint) (*IncidentEntity, error) { return &incidentEntity, nil } -func (r *Repository) GetAllIncidents(teamsIds, severityIds, statusIds []uint) (*[]IncidentEntity, int, error) { +func (r *Repository) GetAllIncidents(teamsIds, severityIds, statusIds []uint, isPrivate *bool) (*[]IncidentEntity, int, error) { var query = r.gormClient.Model([]IncidentEntity{}) var incidentEntity []IncidentEntity if len(teamsIds) != 0 { @@ -300,6 +301,9 @@ func (r *Repository) GetAllIncidents(teamsIds, severityIds, statusIds []uint) (* if len(statusIds) != 0 { query = query.Where("status IN ?", statusIds) } + if isPrivate != nil { + query = query.Where("is_private = ?", isPrivate) + } var totalElements int64 result := query.Count(&totalElements) @@ -334,6 +338,7 @@ func (r *Repository) FetchAllIncidentsPaginated( incidentName string, from string, to string, + userId *uint, ) ([]IncidentEntity, int, error) { var query = r.gormClient.Model([]IncidentEntity{}).Preload("Products") var incidentEntity []IncidentEntity @@ -364,6 +369,14 @@ func (r *Repository) FetchAllIncidentsPaginated( Where("incident_products.product_entity_id IN ?", productIds). Group("incident.id") } + if userId != nil { + query = query.Where( + fmt.Sprintf("%s OR %s or %s", isPrivateCondition, incidentTeamsAccessCondition, userInIncidentCondition), + false, *userId, *userId, + ) + } else { + query = query.Where("is_private = ?", false) + } var totalElements int64 result := query.Count(&totalElements) @@ -596,7 +609,7 @@ func (r *Repository) GetOpenIncidentsByCreatorIdForGivenTeamAndStatuses(created_ func (r *Repository) FindOpenIncidentsByTeamOrderedByCreationTimeAndSeverity(team string) (*[]IncidentEntity, error) { var incidentEntity []IncidentEntity - query := fmt.Sprintf("SELECT * FROM incident WHERE team_id = %v AND status NOT IN (4, 5) AND deleted_at IS NULL ORDER BY severity_id, id", team) + query := fmt.Sprintf("SELECT * FROM incident WHERE team_id = %v AND status NOT IN (4, 5) AND is_private = false AND deleted_at IS NULL ORDER BY severity_id, id", team) result := r.gormClient.Raw(query).Scan(&incidentEntity) if result.Error != nil { @@ -625,5 +638,40 @@ func (r *Repository) UpdateIncidentChannelEntity(incidentChannelEntity *Incident return nil } +func (r *Repository) CanUserWithEmailAccessIncidentWithId(userEmail string, incidentId uint) (bool, error) { + var count int64 + query := r.gormClient.Table("incident").Joins("JOIN houston_user ON houston_user.email = ?", userEmail) + + query = query.Where("incident.id = ?", incidentId) + + query = query.Where( + fmt.Sprintf("%s OR %s or %s", isPrivateCondition, incidentTeamsAccessCondition, userInIncidentCondition), + false, gorm.Expr("houston_user.id"), gorm.Expr("houston_user.id"), + ) + + if err := query.Count(&count).Error; err != nil { + return false, err + } + + return count > 0, nil +} + var statusesForEscalation = []uint{1, 2} var severitiesForSLABreach = []uint{2, 3, 4} + +const isPrivateCondition = "is_private = ?" +const incidentTeamsAccessCondition = `EXISTS ( + SELECT 1 + FROM team_user + WHERE team_user.user_id = ? + AND ( + team_user.team_id = incident.team_id + OR team_user.team_id = incident.reporting_team_id + ) +)` +const userInIncidentCondition = `EXISTS ( + SELECT 1 + FROM incident_user + WHERE incident_user.incident_id = incident.id + AND incident_user.user_id = ? +)` diff --git a/model/incident/incident_repository_interface.go b/model/incident/incident_repository_interface.go index 0b9e907..2e17668 100644 --- a/model/incident/incident_repository_interface.go +++ b/model/incident/incident_repository_interface.go @@ -13,7 +13,7 @@ type IIncidentRepository interface { CreateIncidentTagsInBatchesForAnIncident(incidentId uint, tagIds []uint) (*[]IncidentTagEntity, error) FindIncidentByChannelId(channelId string) (*IncidentEntity, error) FindIncidentById(Id uint) (*IncidentEntity, error) - GetAllIncidents(teamsIds, severityIds, statusIds []uint) (*[]IncidentEntity, int, error) + GetAllIncidents(teamsIds, severityIds, statusIds []uint, isPrivate *bool) (*[]IncidentEntity, int, error) FetchAllIncidentsPaginated( productIds []uint, reportingTeamIds []uint, @@ -25,6 +25,7 @@ type IIncidentRepository interface { incidentName string, from string, to string, + userId *uint, ) ([]IncidentEntity, int, error) UpsertIncidentRole(addIncidentRoleRequest *AddIncidentRoleRequest) error CreateIncidentChannelEntry(request *CreateIncidentChannelEntry) error @@ -42,4 +43,5 @@ type IIncidentRepository interface { GetOpenIncidentsByCreatorIdForGivenTeamAndStatuses(created_by string, teamId uint, statusIds []uint) (*[]IncidentEntity, error) GetIncidentTagsByTagIds(incidentId uint, tagIds []uint) (*IncidentTagEntity, error) FetchIncidentsWithSeverityTatBetweenGivenRange(slaStart, slaEnd string) (*[]IncidentEntity, error) + CanUserWithEmailAccessIncidentWithId(userEmail string, incidentId uint) (bool, error) } diff --git a/model/incident/model.go b/model/incident/model.go index 679b9e2..f2074b1 100644 --- a/model/incident/model.go +++ b/model/incident/model.go @@ -57,6 +57,7 @@ type CreateIncidentDTO struct { MetaData JSON `json:"meta_data,omitempty"` ProductIds []uint `json:"product_ids,omitempty"` ReportingTeamID *uint `json:"reporting_team_id,omitempty"` + IsPrivate bool `json:"is_private,omitempty"` } type IncidentSeverityTeamDTO struct { @@ -115,6 +116,7 @@ type IncidentDTO struct { Team team.TeamDTO `json:"team,omitempty"` Severity severity.SeverityDTO `json:"severity,omitempty"` ReportingTeam team.TeamDTO `json:"reporting_team,omitempty"` + IsPrivate bool `json:"is_private,omitempty"` } type IncidentRoleDTO struct { diff --git a/model/incidentUser/entity.go b/model/incidentUser/entity.go new file mode 100644 index 0000000..6429c7e --- /dev/null +++ b/model/incidentUser/entity.go @@ -0,0 +1,30 @@ +package incidentUser + +import ( + "houston/model/incident" + "houston/model/user" +) + +type IncidentUserEntity struct { + ID uint `gorm:"primaryKey"` + IncidentID uint `gorm:"column:incident_id;not null;uniqueIndex:incident_user_incident_id_user_id_key"` + UserID uint `gorm:"column:user_id;not null;uniqueIndex:incident_user_incident_id_user_id_key"` + + // Add foreign key constraints + Incident incident.IncidentEntity `gorm:"foreignKey:IncidentID"` + User user.UserEntity `gorm:"foreignKey:UserID"` +} + +func (IncidentUserEntity) TableName() string { + return "incident_user" +} + +func (entity IncidentUserEntity) ToDTO() IncidentUserDTO { + return IncidentUserDTO{ + ID: entity.ID, + IncidentID: entity.IncidentID, + UserID: entity.UserID, + Incident: entity.Incident.ToDTO(), + User: *entity.User.ToDTO(), + } +} diff --git a/model/incidentUser/model.go b/model/incidentUser/model.go new file mode 100644 index 0000000..2204088 --- /dev/null +++ b/model/incidentUser/model.go @@ -0,0 +1,14 @@ +package incidentUser + +import ( + "houston/model/incident" + "houston/model/user" +) + +type IncidentUserDTO struct { + ID uint `json:"id"` + IncidentID uint `json:"incident_id"` + UserID uint `json:"user_id"` + Incident incident.IncidentDTO + User user.UserDTO +} diff --git a/model/incident_jira/incident_jira_repository_impl.go b/model/incident_jira/incident_jira_repository_impl.go index 3ba5b6e..706a1c2 100644 --- a/model/incident_jira/incident_jira_repository_impl.go +++ b/model/incident_jira/incident_jira_repository_impl.go @@ -78,7 +78,7 @@ func (repo *IncidentJiraRepositoryImpl) GetAllJiraIdsByPage(incidentName string, var allJiraLinksFromDB []IncidentJiraLinksDTO query := repo.db.Table("incident_jira"). - Joins("JOIN incident ON incident_jira.incident_id = incident.id"). + Joins("JOIN incident ON incident_jira.incident_id = incident.id AND incident.is_private = false"). Select("incident.id as incident_id, incident.incident_name as incident_name, incident_jira.jira_link as jira_link"). Group("incident_jira.jira_link, incident.id") diff --git a/repository/incidentUser/incident_user_repository_impl.go b/repository/incidentUser/incident_user_repository_impl.go new file mode 100644 index 0000000..1ec178e --- /dev/null +++ b/repository/incidentUser/incident_user_repository_impl.go @@ -0,0 +1,32 @@ +package incidentUser + +import ( + "gorm.io/gorm" + "houston/model/incidentUser" +) + +type incidentUserRepositoryImpl struct { + gormClient *gorm.DB +} + +func (repo *incidentUserRepositoryImpl) AddIncidentUser(incidentId uint, userId uint) error { + result := repo.gormClient.Create(&incidentUser.IncidentUserEntity{ + IncidentID: incidentId, + UserID: userId, + }) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (repo *incidentUserRepositoryImpl) RemoveIncidentUser(incidentId uint, userId uint) error { + result := repo.gormClient.Where("incident_id = ? AND user_id = ?", incidentId, userId).Delete(&incidentUser.IncidentUserEntity{}) + if result.Error != nil { + return result.Error + } + + return nil +} diff --git a/repository/incidentUser/incident_user_repository_interface.go b/repository/incidentUser/incident_user_repository_interface.go new file mode 100644 index 0000000..93b12a1 --- /dev/null +++ b/repository/incidentUser/incident_user_repository_interface.go @@ -0,0 +1,16 @@ +package incidentUser + +import ( + "gorm.io/gorm" +) + +type IncidentUserRepository interface { + AddIncidentUser(incidentId uint, userId uint) error + RemoveIncidentUser(incidentId uint, userId uint) error +} + +func NewIncidentUserRepository(gormClient *gorm.DB) IncidentUserRepository { + return &incidentUserRepositoryImpl{ + gormClient: gormClient, + } +} diff --git a/service/auth_service.go b/service/auth_service.go index c5edcf9..05779fe 100644 --- a/service/auth_service.go +++ b/service/auth_service.go @@ -10,6 +10,7 @@ import ( "houston/logger" clientsResponse "houston/model/clients" "houston/pkg/slackbot" + "houston/service/incident" "houston/service/request/team" common "houston/service/response/common" "houston/service/teamUser" @@ -24,6 +25,7 @@ type AuthService struct { slackClient *slackbot.Client userService user.UserService teamUserService teamUser.ITeamUserService + incidentService incident.IIncidentService } func NewAuthService(mjolnirClient *clients.MjolnirClient, slackClient *slackbot.Client) *AuthService { @@ -32,6 +34,7 @@ func NewAuthService(mjolnirClient *clients.MjolnirClient, slackClient *slackbot. slackClient: slackClient, userService: appcontext.GetUserService(), teamUserService: appcontext.GetTeamUserService(), + incidentService: appcontext.GetIncidentService(), } } @@ -241,6 +244,50 @@ func (authService *AuthService) checkIfAdminOrTeamMember(context *gin.Context, t strings.Contains(strings.Join(teamMembers, ","), userInfo.ID), nil } +func (authService *AuthService) CanUserAccessIncident(fn gin.HandlerFunc) gin.HandlerFunc { + return func(c *gin.Context) { + incidentId, err := strconv.Atoi(c.Param(util.IdParam)) + if err != nil { + logger.Error("error occurred while parsing incident id", zap.Error(err)) + c.AbortWithStatus(http.StatusBadRequest) + return + } + + if !authService.incidentService.CanUserWithEmailAccessIncidentWithId( + c.Request.Header.Get(util.UserEmailHeader), uint(incidentId), + ) { + logger.Error("user is not allowed to access incident data") + c.JSON(http.StatusNotFound, common.ErrorResponse(fmt.Errorf("could not find incident with id %d", incidentId), http.StatusNotFound, nil)) + return + } + fn(c) + } +} + +func (authService *AuthService) CanUserAccessIncidentLogs(fn gin.HandlerFunc) gin.HandlerFunc { + return func(c *gin.Context) { + logType := c.Param("log_type") + + if logType == util.IncidentLogType { + incidentId, err := strconv.Atoi(c.Param(util.IdParam)) + if err != nil { + logger.Error("error occurred while parsing incident id", zap.Error(err)) + c.AbortWithStatus(http.StatusBadRequest) + return + } + + if !authService.incidentService.CanUserWithEmailAccessIncidentWithId( + c.Request.Header.Get(util.UserEmailHeader), uint(incidentId), + ) { + logger.Error("user is not allowed to access incident logs") + c.JSON(http.StatusNotFound, common.ErrorResponse(fmt.Errorf("could not find incident with id %d", incidentId), http.StatusNotFound, nil)) + return + } + } + fn(c) + } +} + func (authService *AuthService) CheckValidUser(sessionToken, userEmail string) (bool, error) { sessionResponse, err := authService.mjolnirClient.GetSessionResponse(sessionToken) if err != nil || sessionResponse.StatusCode == http.StatusUnauthorized { diff --git a/service/incident/impl/incident_service_test.go b/service/incident/impl/incident_service_test.go index 9e929ab..f404600 100644 --- a/service/incident/impl/incident_service_test.go +++ b/service/incident/impl/incident_service_test.go @@ -391,7 +391,7 @@ func (suite *IncidentServiceSuite) Test_UpdateIncident_ChannelRenameError() { func (suite *IncidentServiceSuite) Test_GetAllIncidents_FailureCase() { suite.incidentRepository.GetAllIncidentsMock.Return(nil, 0, errors.New("error while fetching incidents")) - incidents, count, err := suite.incidentService.GetAllIncidents(nil, nil, nil) + incidents, count, err := suite.incidentService.GetAllIncidents(nil, nil, nil, nil) suite.Nil(incidents, "Incidents should be nil") suite.Equal(0, count, "Count should be 0") suite.Error(err, "Error should not be nil") @@ -400,7 +400,7 @@ func (suite *IncidentServiceSuite) Test_GetAllIncidents_FailureCase() { func (suite *IncidentServiceSuite) Test_GetAllIncidents_SuccessCase() { mockIncidents := &[]incident.IncidentEntity{*GetMockIncident()} suite.incidentRepository.GetAllIncidentsMock.Return(mockIncidents, 1, nil) - incidents, count, err := suite.incidentService.GetAllIncidents(nil, nil, nil) + incidents, count, err := suite.incidentService.GetAllIncidents(nil, nil, nil, nil) suite.NotNil(incidents, "Incidents should not be nil") suite.Equal(1, count, "Count should be 1") suite.Nil(err, "Error should be nil") diff --git a/service/incident/impl/incident_service_v2.go b/service/incident/impl/incident_service_v2.go index 3fc678b..05d74de 100644 --- a/service/incident/impl/incident_service_v2.go +++ b/service/incident/impl/incident_service_v2.go @@ -218,7 +218,10 @@ func (i *IncidentServiceV2) PostIncidentCreationWorkflow( } i.HandleKrakatoaWorkflow(incidentEntity) - go postInWebhookSlackChannel(i, responderTeam, incidentEntity, severityEntity) + if !incidentEntity.IsPrivate { + go postInWebhookSlackChannel(i, responderTeam, incidentEntity, severityEntity) + } + go i.SendAlert(incidentEntity) } @@ -442,7 +445,7 @@ func (i *IncidentServiceV2) GetAllOpenIncidents() ([]incident.IncidentDTO, int, return nil, 0, err } - incidents, resultLength, err := i.incidentRepository.GetAllIncidents(nil, nil, nonTerminalStatusIds) + incidents, resultLength, err := i.incidentRepository.GetAllIncidents(nil, nil, nonTerminalStatusIds, nil) if err != nil { logger.Error(fmt.Sprintf("%s failed to get all incidents", logTag), zap.Error(err)) return nil, 0, err @@ -451,8 +454,8 @@ func (i *IncidentServiceV2) GetAllOpenIncidents() ([]incident.IncidentDTO, int, return dto.ToDtoArray[incident.IncidentEntity, incident.IncidentDTO](*incidents), resultLength, err } -func (i *IncidentServiceV2) GetAllIncidents(teamIds, severityIds, statusIds []uint) ([]incident.IncidentDTO, int, error) { - incidents, resultLength, err := i.incidentRepository.GetAllIncidents(teamIds, severityIds, statusIds) +func (i *IncidentServiceV2) GetAllIncidents(teamIds, severityIds, statusIds []uint, isPrivate *bool) ([]incident.IncidentDTO, int, error) { + incidents, resultLength, err := i.incidentRepository.GetAllIncidents(teamIds, severityIds, statusIds, isPrivate) if err != nil { return nil, 0, err } @@ -2354,6 +2357,16 @@ func (i *IncidentServiceV2) DeleteConferenceEvent(incidentEntity *incident.Incid } } +func (i *IncidentServiceV2) CanUserWithEmailAccessIncidentWithId(userEmail string, incidentId uint) bool { + isAllowed, err := i.incidentRepository.CanUserWithEmailAccessIncidentWithId(userEmail, incidentId) + if err != nil { + logger.Error(fmt.Sprintf("%s error in checking user access to incident", logTag), zap.Error(err)) + return false + } + + return isAllowed +} + func (i *IncidentServiceV2) addBookMarkAndPostMessageInSlack(incidentName string, conferenceLink string, slackChannel string) { i.slackService.AddBookmark(conferenceLink, slackChannel, "Google Meet") _, err := i.slackService.PostMessageByChannelID(fmt.Sprintf(util.ConferenceMessage, conferenceLink), false, slackChannel) diff --git a/service/incident/impl/incident_update_status.go b/service/incident/impl/incident_update_status.go index 5ed34ae..624e1cc 100644 --- a/service/incident/impl/incident_update_status.go +++ b/service/incident/impl/incident_update_status.go @@ -891,6 +891,10 @@ func (i *IncidentServiceV2) processArchivalTime(severityID uint, channelID strin } func (i *IncidentServiceV2) processIncidentRCAFlow(incidentEntity *incident.IncidentEntity) { + if incidentEntity.IsPrivate { + return + } + err := i.rcaService.SendConversationDataForGeneratingRCA( incidentEntity.ID, incidentEntity.IncidentName, incidentEntity.SlackChannel, ) diff --git a/service/incident/incident_service_v2_interface.go b/service/incident/incident_service_v2_interface.go index 76c8d27..f43ed56 100644 --- a/service/incident/incident_service_v2_interface.go +++ b/service/incident/incident_service_v2_interface.go @@ -20,7 +20,7 @@ type IIncidentService interface { LinkJiraToIncident(incidentId uint, linkedBy string, jiraLinks ...string) error UnLinkJiraFromIncident(incidentId uint, unLinkedBy, jiraLink string) error GetAllOpenIncidents() ([]incident.IncidentDTO, int, error) - GetAllIncidents(teamIds, severityIds, statusIds []uint) ([]incident.IncidentDTO, int, error) + GetAllIncidents(teamIds, severityIds, statusIds []uint, isPrivate *bool) ([]incident.IncidentDTO, int, error) GetIncidentRoleByIncidentIdAndRole(incidentId uint, role string) (*incident.IncidentRoleEntity, error) GetIncidentRolesByIncidentIdsAndRole(incidentIds []uint, role string) ([]incident.IncidentRoleDTO, error) UpdateIncidentJiraLinksEntity(incidentEntity *incident.IncidentEntity, updatedBy string, jiraLinks []string) error @@ -38,4 +38,5 @@ type IIncidentService interface { ) error FetchIncidentsApproachingSlaBreach() ([]incident.IncidentDTO, error) AddBotUponChannelUnarchival(channelID string) error + CanUserWithEmailAccessIncidentWithId(userEmail string, incidentId uint) bool } diff --git a/service/incidentUser/incident_user_service_impl.go b/service/incidentUser/incident_user_service_impl.go new file mode 100644 index 0000000..daffc97 --- /dev/null +++ b/service/incidentUser/incident_user_service_impl.go @@ -0,0 +1,38 @@ +package incidentUser + +import ( + "fmt" + "houston/logger" + "houston/repository/incidentUser" + incidentUserRequest "houston/service/request/incidentUser" +) + +type incidentUserServiceImpl struct { + incidentUserRepository incidentUser.IncidentUserRepository +} + +const logTag = "[incident-user-service]" + +func (i *incidentUserServiceImpl) AddIncidentUser(request incidentUserRequest.AddIncidentUserRequest) error { + err := i.incidentUserRepository.AddIncidentUser(request.IncidentID, request.UserID) + if err != nil { + logger.Error( + fmt.Sprintf("%s failed to add incident-user mapping with incidentId %d and userId %d: %v", + logTag, request.IncidentID, request.UserID, err, + ), + ) + } + return err +} + +func (i *incidentUserServiceImpl) RemoveIncidentUser(incidentId uint, userId uint) error { + err := i.incidentUserRepository.RemoveIncidentUser(incidentId, userId) + if err != nil { + logger.Error( + fmt.Sprintf("%s failed to remove incident-user mapping with incidentId %d and userId %d: %v", + logTag, incidentId, userId, err, + ), + ) + } + return err +} diff --git a/service/incidentUser/incident_user_service_interface.go b/service/incidentUser/incident_user_service_interface.go new file mode 100644 index 0000000..73bdf0d --- /dev/null +++ b/service/incidentUser/incident_user_service_interface.go @@ -0,0 +1,17 @@ +package incidentUser + +import ( + "houston/repository/incidentUser" + incidentUserRequest "houston/service/request/incidentUser" +) + +type IncidentUserService interface { + AddIncidentUser(request incidentUserRequest.AddIncidentUserRequest) error + RemoveIncidentUser(incidentId uint, userId uint) error +} + +func NewIncidentUserService(incidentUserRepository incidentUser.IncidentUserRepository) IncidentUserService { + return &incidentUserServiceImpl{ + incidentUserRepository: incidentUserRepository, + } +} diff --git a/service/incidentUser/incident_user_service_test.go b/service/incidentUser/incident_user_service_test.go new file mode 100644 index 0000000..0274c20 --- /dev/null +++ b/service/incidentUser/incident_user_service_test.go @@ -0,0 +1,60 @@ +package incidentUser + +import ( + "errors" + "github.com/stretchr/testify/suite" + "houston/logger" + "houston/mocks" + "houston/service/request/incidentUser" + "testing" +) + +type IncidentUserServiceSuite struct { + suite.Suite + incidentUserRepository *mocks.IncidentUserRepositoryMock + incidentUserService *incidentUserServiceImpl +} + +func (suite *IncidentUserServiceSuite) Test_AddIncidentUser_FailureCase() { + suite.incidentUserRepository.AddIncidentUserMock.Return(errors.New("DB error")) + + err := suite.incidentUserService.AddIncidentUser(getMockIncidentUserRequest()) + suite.NotNil(err, "error should not be nil") + suite.EqualError(err, "DB error", "error should be DB error") +} + +func (suite *IncidentUserServiceSuite) Test_AddIncidentUser_SuccessCase() { + suite.incidentUserRepository.AddIncidentUserMock.Return(nil) + + err := suite.incidentUserService.AddIncidentUser(getMockIncidentUserRequest()) + suite.Nil(err, "error should be nil") +} + +func (suite *IncidentUserServiceSuite) Test_RemoveIncidentUser_FailureCase() { + suite.incidentUserRepository.RemoveIncidentUserMock.Return(errors.New("DB error")) + + err := suite.incidentUserService.RemoveIncidentUser(1, 1) + suite.NotNil(err, "error should not be nil") + suite.EqualError(err, "DB error", "error should be DB error") +} + +func (suite *IncidentUserServiceSuite) Test_RemoveIncidentUser_SuccessCase() { + suite.incidentUserRepository.RemoveIncidentUserMock.Return(nil) + + err := suite.incidentUserService.RemoveIncidentUser(1, 1) + suite.Nil(err, "error should be nil") +} + +func (suite *IncidentUserServiceSuite) SetupSuite() { + logger.InitLogger() + suite.incidentUserRepository = mocks.NewIncidentUserRepositoryMock(suite.T()) + suite.incidentUserService = &incidentUserServiceImpl{incidentUserRepository: suite.incidentUserRepository} +} + +func getMockIncidentUserRequest() incidentUser.AddIncidentUserRequest { + return incidentUser.AddIncidentUserRequest{IncidentID: 1, UserID: 1} +} + +func TestIncidentUserService(t *testing.T) { + suite.Run(t, new(IncidentUserServiceSuite)) +} diff --git a/service/incident_service.go b/service/incident_service.go index 2fd9bc9..405955a 100644 --- a/service/incident_service.go +++ b/service/incident_service.go @@ -3,6 +3,7 @@ package service import ( "fmt" "houston/appcontext" + "houston/common/util" "houston/internal/processor/action" "houston/logger" "houston/model/incident" @@ -22,6 +23,8 @@ import ( severityServiceImpl "houston/service/severity/impl" slack2 "houston/service/slack" "houston/service/teamService" + "houston/service/teamUser" + userService "houston/service/user" utils "houston/service/utils" "math" "net/http" @@ -53,6 +56,8 @@ type incidentService struct { incidentServiceV2 *impl.IncidentServiceV2 rcaService *rcaService.RcaService incidentStatusService incidentStatusService.IncidentStatusService + userService userService.UserService + teamUserService teamUser.ITeamUserService } func NewIncidentService( @@ -89,6 +94,8 @@ func NewIncidentService( incidentServiceV2: incidentServiceV2, rcaService: rcaService, incidentStatusService: incidentStatusService, + userService: userService.NewUserService(userRepository, appcontext.GetSlackService()), + teamUserService: appcontext.GetTeamUserService(), } } @@ -102,6 +109,7 @@ func (i *incidentService) GetIncidents(c *gin.Context) { IncidentName := c.Query("incident_name") From := c.Query("from") To := c.Query("to") + userId := i.getRequesterUserIdFromEmail(c.Request.Header.Get(util.UserEmailHeader)) if IncidentId != "" { numIncidentId, err := strconv.Atoi(IncidentId) @@ -139,7 +147,7 @@ func (i *incidentService) GetIncidents(c *gin.Context) { IncidentName: IncidentName, From: From, To: To, - }, i.incidentRepository) + }, i.incidentRepository, userId) if err != nil { logger.Info("error in fetching incidents", zap.Error(err)) c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil)) @@ -168,6 +176,14 @@ func (i *incidentService) GetIncidents(c *gin.Context) { }, http.StatusOK)) } +func (i *incidentService) getRequesterUserIdFromEmail(email string) *uint { + user, _ := i.userService.GetHoustonUserByEmailId(email) + if user == nil { + return nil + } + return &user.ID +} + func (i *incidentService) GetIncidentResponseFromIncidentEntity( incidents []incident.IncidentEntity, incidentRepository *incident.Repository, @@ -266,7 +282,9 @@ func (i *incidentService) GetUserIdAndIdentityMappingOfAllIncidents(incidents [] return userIdAndIdentityMapping, nil } -func (i *incidentService) GetAllIncidents(incidentFilters request.IncidentFilters, incidentRepository *incident.Repository) ([]incident.IncidentEntity, int, error) { +func (i *incidentService) GetAllIncidents( + incidentFilters request.IncidentFilters, incidentRepository *incident.Repository, userId *uint, +) ([]incident.IncidentEntity, int, error) { var productIds []uint var reporterTeamIds []uint var TeamIds []uint @@ -288,7 +306,9 @@ func (i *incidentService) GetAllIncidents(incidentFilters request.IncidentFilter StatusIds = i.SplitStringAndGetUintArray(incidentFilters.StatusIds) } return incidentRepository.FetchAllIncidentsPaginated( - productIds, reporterTeamIds, TeamIds, SeverityIds, StatusIds, incidentFilters.PageNumber, incidentFilters.PageSize, incidentFilters.IncidentName, incidentFilters.From, incidentFilters.To) + productIds, reporterTeamIds, TeamIds, SeverityIds, StatusIds, incidentFilters.PageNumber, + incidentFilters.PageSize, incidentFilters.IncidentName, incidentFilters.From, incidentFilters.To, userId, + ) } func (i *incidentService) SplitStringAndGetUintArray(str string) []uint { @@ -380,7 +400,7 @@ func (i *incidentService) GetTeamIncidents(c *gin.Context) { StatusIds: Statuses, PageNumber: 0, PageSize: viper.GetInt64("CRM_TEAM_INCIDENT_COUNT"), - }, i.incidentRepository) + }, i.incidentRepository, nil) if err != nil { logger.Error("error in fetching incidents", zap.Error(err)) c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil)) diff --git a/service/orchestration/incident_orchestrator_impl.go b/service/orchestration/incident_orchestrator_impl.go index 36b8b16..9bf20c0 100644 --- a/service/orchestration/incident_orchestrator_impl.go +++ b/service/orchestration/incident_orchestrator_impl.go @@ -218,6 +218,7 @@ func (i *incidentOrchestratorImpl) CreateIncident( utils.ConstructIncidentChannelName( incidentEntity.ID, request.SeverityID, string(incident.Investigating), incidentEntity.Title, ), + request.IsPrivate, ) if err != nil { tx.Rollback() @@ -414,6 +415,7 @@ func (i *incidentOrchestratorImpl) buildCreateIncidentDTO( createIncidentDTO.UpdatedBy = createIncidentDTO.CreatedBy createIncidentDTO.StartTime = time.Now() createIncidentDTO.EnableReminder = false + createIncidentDTO.IsPrivate = createIncRequest.IsPrivate if createIncRequest.Metadata != nil { metadata, _ := json.Marshal([]incidentRequest.CreateIncidentMetadata{*createIncRequest.Metadata}) @@ -439,8 +441,8 @@ func (i *incidentOrchestratorImpl) createIncidentEntityTransactionally( return entity, channelTopic, nil } -func (i *incidentOrchestratorImpl) createSlackChannelTransactionally(incidentID uint, incidentName string, channelName string) (*slack2.Channel, error) { - channel, err := i.slackService.CreateSlackChannel(incidentID, channelName) +func (i *incidentOrchestratorImpl) createSlackChannelTransactionally(incidentID uint, incidentName string, channelName string, isPrivate bool) (*slack2.Channel, error) { + channel, err := i.slackService.CreateSlackChannel(incidentID, channelName, isPrivate) if err != nil { logger.Error( fmt.Sprintf("%s [%s] Error while crating slack channel", logTag, incidentName), diff --git a/service/reminder/sla_breach_reminder.go b/service/reminder/sla_breach_reminder.go index ba7af13..8c602a4 100644 --- a/service/reminder/sla_breach_reminder.go +++ b/service/reminder/sla_breach_reminder.go @@ -60,7 +60,7 @@ func (service *reminderServiceImpl) postSlaBreachMessageForIncidents(incidents [ } }) - if !util.IsBlank(teamSlackChannel) { + if !util.IsBlank(teamSlackChannel) && !incidentData.IsPrivate { wg.Add(1) go util.ExecuteConcurrentAction(&wg, func() { err := service.postSlaBreachMessageToChannel( diff --git a/service/reminder/team_incidents_reminder.go b/service/reminder/team_incidents_reminder.go index 58f3184..0dfc783 100644 --- a/service/reminder/team_incidents_reminder.go +++ b/service/reminder/team_incidents_reminder.go @@ -17,11 +17,12 @@ const postTeamMetricsLogTag = "POST_TEAM_INCIDENTS" var teamMetricsSeverities = []uint{1, 2, 3} var teamMetricsStatuses = []uint{1, 2, 3} +var isPrivate = false func (service *reminderServiceImpl) PostTeamIncidents() error { logger.Info(fmt.Sprintf("%s received request to post team metrics", postTeamMetricsLogTag)) - openIncidents, _, err := service.incidentService.GetAllIncidents(nil, teamMetricsSeverities, teamMetricsStatuses) + openIncidents, _, err := service.incidentService.GetAllIncidents(nil, teamMetricsSeverities, teamMetricsStatuses, &isPrivate) if err != nil { logger.Error(fmt.Sprintf("error while fetching open incidents. %+v", err)) return err diff --git a/service/request/incident/create_incident.go b/service/request/incident/create_incident.go index a865c44..79a8101 100644 --- a/service/request/incident/create_incident.go +++ b/service/request/incident/create_incident.go @@ -14,6 +14,7 @@ type CreateIncidentRequestV3 struct { ProductIds []uint `json:"productIds"` CreatedBy string `json:"createdBy"` Metadata *CreateIncidentMetadata `json:"metaData,omitempty"` + IsPrivate bool `json:"isPrivate"` } type CreateIncidentMetadata struct { diff --git a/service/request/incidentUser/add_incident_user.go b/service/request/incidentUser/add_incident_user.go new file mode 100644 index 0000000..151020b --- /dev/null +++ b/service/request/incidentUser/add_incident_user.go @@ -0,0 +1,6 @@ +package incidentUser + +type AddIncidentUserRequest struct { + IncidentID uint `json:"incident_id"` + UserID uint `json:"user_id"` +} diff --git a/service/slack/slack_service.go b/service/slack/slack_service.go index f262808..92db66d 100644 --- a/service/slack/slack_service.go +++ b/service/slack/slack_service.go @@ -273,8 +273,8 @@ func (s *SlackService) IsASlackUser(slackIdOrEmail string) (bool, *slack.User) { return true, user } -func (s *SlackService) CreateSlackChannel(incidentId uint, channelName string) (*slack.Channel, error) { - channel, err := createChannel(channelName, s.SocketModeClientWrapper) +func (s *SlackService) CreateSlackChannel(incidentId uint, channelName string, isPrivate bool) (*slack.Channel, error) { + channel, err := createChannel(channelName, s.SocketModeClientWrapper, isPrivate) if err != nil { return nil, fmt.Errorf("%s failed to create Slack Channel for incident %d. error: %+v", logTag, incidentId, err) } @@ -313,10 +313,10 @@ func (s *SlackService) InviteUsersToConversation(channelId string, userIds ...st return nil } -func createChannel(channelName string, socketModeWrapper socketModeClient.ISocketModeClientWrapper) (*slack.Channel, error) { +func createChannel(channelName string, socketModeWrapper socketModeClient.ISocketModeClientWrapper, isPrivate bool) (*slack.Channel, error) { request := slack.CreateConversationParams{ ChannelName: channelName, - IsPrivate: false, + IsPrivate: isPrivate, } channel, err := socketModeWrapper.CreateConversation(request) diff --git a/service/slack/slack_service_interface.go b/service/slack/slack_service_interface.go index 37fcf57..4caedb2 100644 --- a/service/slack/slack_service_interface.go +++ b/service/slack/slack_service_interface.go @@ -25,7 +25,7 @@ type ISlackService interface { GetUserBySlackID(slackUserID string) (*slack.User, error) GetSlackUserIdOrEmail(email string) string IsASlackUser(slackIdOrEmail string) (bool, *slack.User) - CreateSlackChannel(incidentId uint, channelName string) (*slack.Channel, error) + CreateSlackChannel(incidentId uint, channelName string, isPrivate bool) (*slack.Channel, error) InviteUsersToConversation(channelId string, userIds ...string) error GetSlackConversationHistoryWithReplies(channelId string) ([]service.ConversationResponse, error) GetChannelConversationHistory(channelId string) ([]slack.Message, error) diff --git a/service/slack/slack_service_test.go b/service/slack/slack_service_test.go index 23618cc..daae049 100644 --- a/service/slack/slack_service_test.go +++ b/service/slack/slack_service_test.go @@ -314,7 +314,7 @@ func (suite *SlackServiceSuite) Test_GetUsersInfo_UsersMoreThanSplitSize() { func (suite *SlackServiceSuite) Test_CreateSlackChannel() { suite.SocketModeClientWrapper.CreateConversationMock.Return(GetMockChannel(), nil) - channel, err := suite.SlackService.CreateSlackChannel(1, "TestSlackChannel") + channel, err := suite.SlackService.CreateSlackChannel(1, "TestSlackChannel", false) suite.NoError(err, "service must not throw error") suite.NotNil(channel, "channel must not be nil") }