4f3f07d4dd
- Vendor shuffle-shared v1.2.51 as backend/go-app/shuffle-shared - Add replace directive in go.mod to use the local moduled copy - In HandleCheckLicense, force org.Licensed=true and set every SyncFeatures limit to 1e9, skipping all license-key logic - Update Dockerfile to ADD the local shuffle-shared before go build - Verified: backend image builds successfully via docker
1316 lines
40 KiB
Go
1316 lines
40 KiB
Go
package shuffle
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
uuid "github.com/satori/go.uuid"
|
|
)
|
|
|
|
// Standalone to make it work many places
|
|
func markNotificationRead(ctx context.Context, notification *Notification) error {
|
|
notification.Read = true
|
|
err := SetNotification(ctx, *notification)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func HandleMarkAsRead(resp http.ResponseWriter, request *http.Request) {
|
|
cors := HandleCors(resp, request)
|
|
if cors {
|
|
return
|
|
}
|
|
|
|
var fileId string
|
|
location := strings.Split(request.URL.String(), "/")
|
|
if location[1] == "api" {
|
|
if len(location) <= 4 {
|
|
log.Printf("Path too short: %d", len(location))
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
fileId = location[4]
|
|
}
|
|
|
|
if len(fileId) != 36 {
|
|
log.Printf("[WARNING] Bad format for fileId in notification %s", fileId)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false, "reason": "Badly formatted ID"}`))
|
|
return
|
|
}
|
|
|
|
// 1. Check user directly
|
|
// 2. Check workflow execution authorization
|
|
user, err := HandleApiAuthentication(resp, request)
|
|
if err != nil {
|
|
log.Printf("[INFO] INITIAL Api authentication failed in notification mark: %s", err)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
ctx := GetContext(request)
|
|
notification, err := GetNotification(ctx, fileId)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed getting notification %s for user %s: %s", fileId, user.Id, err)
|
|
resp.WriteHeader(500)
|
|
resp.Write([]byte(`{"success": false, "reason": "Bad userId or notification doesn't exist"}`))
|
|
return
|
|
}
|
|
|
|
if notification.Personal && notification.UserId != user.Id {
|
|
log.Printf("[WARNING] Bad user for notification. %s (wanted) vs %s", notification.UserId, user.Id)
|
|
resp.WriteHeader(403)
|
|
resp.Write([]byte(`{"success": false, "reason": "Bad userId or notification doesn't exist"}`))
|
|
return
|
|
}
|
|
|
|
if notification.OrgId != user.ActiveOrg.Id {
|
|
log.Printf("[WARNING] Bad org for notification. %s (wanted) vs %s", notification.OrgId, user.ActiveOrg.Id)
|
|
resp.WriteHeader(403)
|
|
resp.Write([]byte(`{"success": false, "reason": "Bad userId or notification doesn't exist"}`))
|
|
return
|
|
}
|
|
|
|
notification.ModifiedBy = user.Username
|
|
|
|
// Look for the "disabled" query in the url
|
|
if request.URL.Query().Get("disabled") == "true" {
|
|
notification.Ignored = true
|
|
|
|
//log.Printf("[AUDIT] Marked %s as ignored by user %s (%s)", notification.Id, user.Username, user.Id)
|
|
} else if request.URL.Query().Get("disabled") == "false" {
|
|
notification.Ignored = false
|
|
}
|
|
|
|
err = markNotificationRead(ctx, notification)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed updating notification %s (%s) to read: %s", notification.Title, notification.Id, err)
|
|
resp.WriteHeader(500)
|
|
resp.Write([]byte(`{"success": false, "reason": "Failed to mark it as read"}`))
|
|
return
|
|
}
|
|
|
|
log.Printf("[AUDIT] Marked %s as read by user %s (%s)", notification.Id, user.Username, user.Id)
|
|
|
|
resp.WriteHeader(200)
|
|
resp.Write([]byte(`{"success": true}`))
|
|
|
|
return
|
|
}
|
|
|
|
func HandleClearNotifications(resp http.ResponseWriter, request *http.Request) {
|
|
cors := HandleCors(resp, request)
|
|
if cors {
|
|
return
|
|
}
|
|
|
|
// 1. Check user directly
|
|
// 2. Check workflow execution authorization
|
|
user, err := HandleApiAuthentication(resp, request)
|
|
if err != nil {
|
|
log.Printf("[INFO] INITIAL Api authentication failed in notification list: %s", err)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
/*
|
|
if user.Role != "admin" {
|
|
log.Printf("[AUTH] User isn't admin")
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Need to be admin to list files"}`)))
|
|
return
|
|
}
|
|
*/
|
|
|
|
ctx := GetContext(request)
|
|
//notifications, err := GetUserNotifications(ctx, user.Id)
|
|
notifications, err := GetOrgNotifications(ctx, user.ActiveOrg.Id)
|
|
if err != nil && len(notifications) == 0 {
|
|
log.Printf("[ERROR] Failed to get notifications (clear): %s", err)
|
|
resp.WriteHeader(500)
|
|
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error getting notifications."}`)))
|
|
return
|
|
}
|
|
|
|
for _, notification := range notifications {
|
|
// Not including this as we want to mark as read for all users in the org
|
|
// We stopped using personal vs org notifications
|
|
// Also added index to track by updated time
|
|
//if user.Id != notification.UserId {
|
|
// continue
|
|
//}
|
|
|
|
notification.ModifiedBy = user.Username
|
|
err = markNotificationRead(ctx, ¬ification)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed updating notification %s (%s) to read (clear): %s", notification.Title, notification.Id, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
log.Printf("[AUDIT] Cleared %d notifications for user %s (%s) in org %s (%s)", len(notifications), user.Username, user.Id, user.ActiveOrg.Name, user.ActiveOrg.Id)
|
|
cacheKey := fmt.Sprintf("notifications_%s", user.ActiveOrg.Id)
|
|
DeleteCache(ctx, cacheKey)
|
|
cacheKey = fmt.Sprintf("notifications_%s", user.Id)
|
|
DeleteCache(ctx, cacheKey)
|
|
|
|
resp.WriteHeader(200)
|
|
resp.Write([]byte(`{"success": true}`))
|
|
}
|
|
|
|
func HandleGetNotifications(resp http.ResponseWriter, request *http.Request) {
|
|
cors := HandleCors(resp, request)
|
|
if cors {
|
|
return
|
|
}
|
|
|
|
// 1. Check user directly
|
|
// 2. Check workflow execution authorization
|
|
user, err := HandleApiAuthentication(resp, request)
|
|
if err != nil {
|
|
log.Printf("[INFO] INITIAL Api authentication failed in notification list: %s", err)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
/*
|
|
if user.Role != "admin" {
|
|
log.Printf("[AUTH] User isn't admin")
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Need to be admin to list files"}`)))
|
|
return
|
|
}
|
|
*/
|
|
|
|
// Should be made org-wide instead? Right now, it's cross org
|
|
ctx := GetContext(request)
|
|
|
|
//notifications, err := GetUserNotifications(ctx, user.Id)
|
|
notifications, err := GetOrgNotifications(ctx, user.ActiveOrg.Id)
|
|
if err != nil && len(notifications) == 0 {
|
|
log.Printf("[ERROR] Failed to get notifications: %s", err)
|
|
resp.WriteHeader(500)
|
|
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error getting notifications."}`)))
|
|
return
|
|
}
|
|
|
|
curType := ""
|
|
typeList, typeOk := request.URL.Query()["origin"]
|
|
if typeOk && len(typeList) > 0 {
|
|
curType = typeList[0]
|
|
} else {
|
|
typeList, typeOk = request.URL.Query()["type"]
|
|
if typeOk && len(typeList) > 0 {
|
|
curType = typeList[0]
|
|
}
|
|
}
|
|
|
|
severity := ""
|
|
severityList, severityOk := request.URL.Query()["severity"]
|
|
if severityOk && len(severityList) > 0 {
|
|
severity = strings.ToLower(severityList[0])
|
|
}
|
|
|
|
status := ""
|
|
statusList, statusOk := request.URL.Query()["status"]
|
|
if statusOk && len(statusList) > 0 {
|
|
status = strings.ToLower(statusList[0])
|
|
}
|
|
|
|
//log.Printf("[AUDIT] Got %d notifications for org %s (%s)", len(notifications), user.ActiveOrg.Name, user.ActiveOrg.Id)
|
|
|
|
newNotifications := []Notification{}
|
|
for _, notification := range notifications {
|
|
// Check how long ago?
|
|
if notification.Read {
|
|
if status == "unread" || status == "open" {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if notification.Personal {
|
|
continue
|
|
}
|
|
|
|
if len(severity) > 0 && notification.Severity != severity {
|
|
continue
|
|
}
|
|
|
|
if len(curType) > 0 && curType != notification.Origin {
|
|
continue
|
|
}
|
|
|
|
//if notification.UserId != user.Id {
|
|
// continue
|
|
//}
|
|
|
|
notification.UserId = ""
|
|
//notification.OrgId = ""
|
|
newNotifications = append(newNotifications, notification)
|
|
}
|
|
|
|
sort.Slice(notifications[:], func(i, j int) bool {
|
|
return notifications[i].UpdatedAt > notifications[j].UpdatedAt
|
|
})
|
|
|
|
notificationResponse := NotificationResponse{
|
|
Success: true,
|
|
Notifications: newNotifications,
|
|
}
|
|
|
|
//log.Printf("[DEBUG] Got %d notifications for user %s", len(notifications), user.Id)
|
|
newBody, err := json.Marshal(notificationResponse)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed marshaling files: %s", err)
|
|
resp.WriteHeader(500)
|
|
resp.Write([]byte(`{"success": false, "reason": "Failed to marshal files"}`))
|
|
return
|
|
}
|
|
|
|
resp.WriteHeader(200)
|
|
resp.Write([]byte(newBody))
|
|
}
|
|
|
|
// how to make sure that the notification workflow bucket always empties itself:
|
|
// call sendToNotificationWorkflow with the first cached notification
|
|
func sendToNotificationWorkflow(ctx context.Context, notification Notification, userApikey, workflowId string, relieveNotifications bool, authOrg Org) error {
|
|
/*
|
|
// FIXME: Was used for disabling it before due to possible issues with infinite loops.
|
|
if project.Environment != "onprem" {
|
|
log.Printf("[DEBUG] Skipping notification workflow send for workflow %s as workflows are disabled for cloud for now.", workflowId)
|
|
return nil
|
|
}
|
|
*/
|
|
|
|
if len(workflowId) < 10 {
|
|
return nil
|
|
}
|
|
|
|
if notification.Ignored {
|
|
log.Printf("[DEBUG] Skipping notification workflow send for notification %s as it's ignored. WorkflowId: %#v", notification.Id, workflowId)
|
|
return nil
|
|
}
|
|
|
|
//log.Printf("[DEBUG] Sending notification to workflow with id: %#v", workflowId)
|
|
|
|
cachedNotifications := NotificationCached{}
|
|
// caclulate hash of notification title + workflow id
|
|
unHashed := fmt.Sprintf("%s_%s", notification.Description, workflowId)
|
|
|
|
// Calculate SHA-256 hash
|
|
hasher := sha256.New()
|
|
hasher.Write([]byte(unHashed))
|
|
hashBytes := hasher.Sum(nil)
|
|
|
|
// Convert the hash to a hexadecimal string
|
|
cacheKey := hex.EncodeToString(hashBytes)
|
|
|
|
cacheData := []byte{}
|
|
|
|
// check if cache exists
|
|
cache, err := GetCache(ctx, cacheKey)
|
|
if err != nil {
|
|
/*
|
|
log.Printf("[ERROR] Failed getting cached notifications %s for notification %s: %s. Assuming no notifications are found!",
|
|
cacheKey,
|
|
notification.Id,
|
|
err,
|
|
)
|
|
*/
|
|
cacheData = []byte{}
|
|
} else {
|
|
cacheData = []byte(cache.([]uint8))
|
|
}
|
|
|
|
bucketingMinutes := os.Getenv("SHUFFLE_NOTIFICATION_BUCKETING_MINUTES")
|
|
if len(bucketingMinutes) == 0 {
|
|
bucketingMinutes = "2"
|
|
}
|
|
|
|
// convert to int
|
|
bucketingMinutesInt, err := strconv.ParseInt(bucketingMinutes, 10, 32)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed converting bucketing minutes to int: %s. Defaulting to 10 minutes!", err)
|
|
bucketingMinutesInt = 2
|
|
}
|
|
|
|
// converting to int32
|
|
bucketingTime := int32(bucketingMinutesInt)
|
|
if !relieveNotifications {
|
|
// worry about the 1440 minutes as timeout later
|
|
if len(cacheData) == 0 {
|
|
timeNow := int64(time.Now().Unix())
|
|
// save to cache and send notification
|
|
cachedNotification := NotificationCached{
|
|
NotificationId: notification.Id,
|
|
OriginalNotification: notification.Id,
|
|
LastNotificationAttempted: notification.Id,
|
|
WorkflowId: workflowId,
|
|
LastUpdated: timeNow,
|
|
FirstUpdated: timeNow,
|
|
Amount: 1,
|
|
}
|
|
|
|
// marshal cachedNotifications
|
|
cacheData, err := json.Marshal(cachedNotification)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed marshaling cached notifications for notification %s: %s", notification.Id, err)
|
|
return err
|
|
}
|
|
|
|
err = SetCache(ctx, cacheKey, cacheData, 1440)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed saving cached notifications %s for notification %s: %s (0)",
|
|
cacheKey,
|
|
notification.Id,
|
|
err,
|
|
)
|
|
return err
|
|
}
|
|
|
|
notification.BucketDescription = fmt.Sprintf("First notification for %s workflow %s. If more notifications are sent within %d minutes, they will be added to the next notification in %d minutes",
|
|
notification.Id,
|
|
workflowId,
|
|
bucketingMinutesInt,
|
|
bucketingMinutesInt,
|
|
)
|
|
} else {
|
|
// unmarshal cached data
|
|
err := json.Unmarshal(cacheData, &cachedNotifications)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed unmarshaling cached notifications: %s", err)
|
|
return err
|
|
}
|
|
|
|
// check cachedNotifications.cachedNotifications
|
|
//log.Printf("[DEBUG] Found %d cached notifications for %s workflow %s",
|
|
// cachedNotifications.Amount,
|
|
// cachedNotifications.NotificationId,
|
|
// workflowId,
|
|
//)
|
|
|
|
cachedNotifications.Amount += 1
|
|
cachedNotifications.LastUpdated = int64(time.Now().Unix())
|
|
cachedNotifications.LastNotificationAttempted = notification.Id
|
|
|
|
// marshal cachedNotifications
|
|
cacheData, err := json.Marshal(cachedNotifications)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed marshaling cached notifications for notification %s: %s", notification.Id, err)
|
|
return err
|
|
}
|
|
|
|
totalTimeElapsed := int64((cachedNotifications.LastUpdated - cachedNotifications.FirstUpdated) / 60)
|
|
|
|
//log.Printf("[DEBUG] Time elapsed since first notification: %d for notification %s", totalTimeElapsed, notification.Id)
|
|
|
|
err = SetCache(ctx, cacheKey, cacheData, 1440)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed saving cached notifications %s for notification %s: %s (1)",
|
|
cacheKey,
|
|
notification.Id,
|
|
err,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// Literally only starts on the 2nd, not otherwise
|
|
if cachedNotifications.Amount == 2 {
|
|
//log.Printf("[DEBUG] Starting timer for %d minutes for relieving notificaions through %s notification", bucketingTime, notification.Id)
|
|
timeAfter := time.Duration(bucketingTime) * time.Minute
|
|
time.AfterFunc(timeAfter, func() {
|
|
// Read from cache again
|
|
cache, err := GetCache(ctx, cacheKey)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed getting cached notifications %s for notification %s: %s. Assuming no notifications are found. that shouldn't happen.",
|
|
cacheKey,
|
|
notification.Id,
|
|
err,
|
|
)
|
|
}
|
|
|
|
// Test if it's a string or uint8
|
|
var cacheData []byte
|
|
tmpString, ok := cache.(string)
|
|
if !ok {
|
|
tmpUint8, ok := cache.([]uint8)
|
|
if !ok {
|
|
log.Printf("[ERROR] Failed setting cache data for notification %s. Cache casting failed", notification.Id)
|
|
return
|
|
} else {
|
|
cacheData = []byte(tmpUint8)
|
|
}
|
|
} else {
|
|
cacheData = []byte(tmpString)
|
|
}
|
|
|
|
// unmarshal cached data
|
|
var newCachedNotifications NotificationCached
|
|
err = json.Unmarshal(cacheData, &newCachedNotifications)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed unmarshaling cached notifications for notification %s: %s", notification.Id, err)
|
|
return
|
|
}
|
|
notification.BucketDescription = fmt.Sprintf("Accumilated %d notifications in %d minutes. (Bucketing time: %d)",
|
|
newCachedNotifications.Amount-1,
|
|
totalTimeElapsed,
|
|
bucketingMinutesInt,
|
|
)
|
|
_ = sendToNotificationWorkflow(ctx, notification, userApikey, workflowId, true, authOrg)
|
|
err = DeleteCache(ctx, cacheKey)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed deleting cached notifications %s for notification %s: %s. Assuming everything is okay and moving on",
|
|
cacheKey,
|
|
notification.Id,
|
|
err,
|
|
)
|
|
}
|
|
})
|
|
return errors.New(
|
|
"Notification with id " + notification.Id + " was the second bucketed notification. " +
|
|
"It is responsible for relieving the bucket. " +
|
|
"We have its cache stored at: " + cacheKey,
|
|
)
|
|
}
|
|
return errors.New("Notification with id" + notification.Id + " won't be sent and is bucketed. We have its cache stored at: " + cacheKey)
|
|
}
|
|
}
|
|
|
|
if strings.Contains(strings.ToLower(notification.ReferenceUrl), strings.ToLower(workflowId)) {
|
|
return errors.New("Same workflow ID as notification ID. Stopped for infinite loop")
|
|
}
|
|
|
|
log.Printf("[DEBUG] Should send notifications to workflow %s", workflowId)
|
|
backendUrl := os.Getenv("BASE_URL")
|
|
if project.Environment == "cloud" {
|
|
// Doesn't work multi-region
|
|
backendUrl = "https://shuffler.io"
|
|
}
|
|
|
|
// Callback to itself onprem.
|
|
if len(backendUrl) == 0 {
|
|
backendUrl = "http://localhost:5001"
|
|
}
|
|
|
|
if len(os.Getenv("SHUFFLE_CLOUDRUN_URL")) > 0 {
|
|
backendUrl = os.Getenv("SHUFFLE_CLOUDRUN_URL")
|
|
}
|
|
|
|
// The /workflows/{id}/execute endpoint accepts ExecutionRequest in the body.
|
|
// If we send notification.ExecutionId, it can be interpreted as the execution
|
|
// ID for the new run and overwrite the original failing execution.
|
|
payloadNotification := notification
|
|
payloadNotification.ExecutionId = ""
|
|
|
|
b, err := json.Marshal(payloadNotification)
|
|
if err != nil {
|
|
log.Printf("[DEBUG] Failed marshaling notification: %s", err)
|
|
return err
|
|
}
|
|
|
|
executionUrl := fmt.Sprintf("%s/api/v1/workflows/%s/execute", backendUrl, workflowId)
|
|
client := GetExternalClient(executionUrl)
|
|
|
|
// Set timeout to 30 sec
|
|
client.Timeout = 10 * time.Second
|
|
req, err := http.NewRequest(
|
|
"POST",
|
|
executionUrl,
|
|
bytes.NewBuffer(b),
|
|
)
|
|
|
|
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, userApikey))
|
|
req.Header.Add("Org-Id", authOrg.Id)
|
|
newresp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer newresp.Body.Close()
|
|
respBody, err := ioutil.ReadAll(newresp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_ = respBody
|
|
|
|
//log.Printf("[DEBUG] Finished notification request to %s with status %d. Data: %s", executionUrl, newresp.StatusCode, string(respBody))
|
|
if newresp.StatusCode != 200 {
|
|
log.Printf("[DEBUG] Finished notification request to %s with status %d. If status is not 200, an error is created.", executionUrl, newresp.StatusCode)
|
|
return errors.New(fmt.Sprintf("Got status code %d when sending notification for org %s", newresp.StatusCode, notification.OrgId))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func forwardNotificationRequest(ctx context.Context, title, description, referenceUrl, orgId string) error {
|
|
if !strings.Contains(referenceUrl, "execution_id") && !strings.Contains(referenceUrl, "detection") {
|
|
log.Printf("[DEBUG] Notification doesn't contain execution ID and detection. Skipping (1)")
|
|
return nil
|
|
}
|
|
|
|
// Find execution id
|
|
executionId := ""
|
|
userApikey := ""
|
|
if strings.Contains(referenceUrl, "execution_id") {
|
|
executionId = strings.Split(referenceUrl, "execution_id=")[1]
|
|
if len(executionId) == 0 {
|
|
log.Printf("[DEBUG] Notification doesn't contain execution ID. Skipping (2)")
|
|
return nil
|
|
}
|
|
|
|
if strings.Contains(executionId, "&") {
|
|
executionId = strings.Split(executionId, "&")[0]
|
|
}
|
|
|
|
// Get the execution
|
|
exec, err := GetWorkflowExecution(ctx, executionId)
|
|
if err != nil {
|
|
log.Printf("[DEBUG] Failed getting execution from notification %s: %s", executionId, err)
|
|
return err
|
|
}
|
|
|
|
userApikey = exec.Authorization
|
|
}
|
|
|
|
if len(userApikey) == 0 {
|
|
auth := os.Getenv("AUTH")
|
|
if len(auth) > 0 {
|
|
userApikey = auth
|
|
}
|
|
}
|
|
|
|
notification := Notification{
|
|
Title: title,
|
|
Description: description,
|
|
ReferenceUrl: referenceUrl,
|
|
OrgId: orgId,
|
|
|
|
ExecutionId: executionId,
|
|
}
|
|
|
|
b, err := json.Marshal(notification)
|
|
if err != nil {
|
|
log.Printf("[DEBUG] Failed marshaling notification: %s", err)
|
|
return err
|
|
}
|
|
|
|
backendUrl := os.Getenv("BASE_URL")
|
|
if len(os.Getenv("SHUFFLE_CLOUDRUN_URL")) > 0 {
|
|
backendUrl = os.Getenv("SHUFFLE_CLOUDRUN_URL")
|
|
}
|
|
|
|
if len(backendUrl) == 0 {
|
|
log.Printf("[ERROR] No backend URL set for notification forwarding")
|
|
return errors.New("No backend URL set for notification")
|
|
}
|
|
|
|
notificationUrl := fmt.Sprintf("%s/api/v1/notifications", backendUrl)
|
|
client := GetExternalClient(notificationUrl)
|
|
//client := &http.Client{
|
|
// Timeout: 5 * time.Second,
|
|
//}
|
|
|
|
req, err := http.NewRequest(
|
|
"POST",
|
|
notificationUrl,
|
|
bytes.NewBuffer(b),
|
|
)
|
|
|
|
// Environment auth if possible.
|
|
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, userApikey))
|
|
envName := os.Getenv("ENVIRONMENT_NAME")
|
|
req.Header.Add("Org-Id", notification.OrgId)
|
|
if len(envName) > 0 {
|
|
req.Header.Add("ENVIRONMENT_NAME", envName)
|
|
}
|
|
|
|
newresp, err := client.Do(req)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed sending notification to backend: %s", err)
|
|
return err
|
|
}
|
|
|
|
defer newresp.Body.Close()
|
|
respBody, err := ioutil.ReadAll(newresp.Body)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed reading response body from backend: %s", err)
|
|
return err
|
|
}
|
|
|
|
log.Printf("[DEBUG] Finished notification request to %s with status %d. Data: %s", notificationUrl, newresp.StatusCode, string(respBody))
|
|
return nil
|
|
}
|
|
|
|
func getNotificationReferenceParam(referenceUrl, key string) string {
|
|
if len(referenceUrl) == 0 || len(key) == 0 {
|
|
return ""
|
|
}
|
|
|
|
parsedUrl, err := url.Parse(referenceUrl)
|
|
if err == nil {
|
|
value := parsedUrl.Query().Get(key)
|
|
if len(value) > 0 {
|
|
return value
|
|
}
|
|
}
|
|
|
|
prefix := fmt.Sprintf("%s=", key)
|
|
if !strings.Contains(referenceUrl, prefix) {
|
|
return ""
|
|
}
|
|
|
|
value := strings.Split(referenceUrl, prefix)[1]
|
|
if strings.Contains(value, "&") {
|
|
value = strings.Split(value, "&")[0]
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func getFailureReasonFromResult(result, description string) string {
|
|
if len(result) == 0 {
|
|
return description
|
|
}
|
|
|
|
resultCheck := ResultChecker{}
|
|
err := json.Unmarshal([]byte(result), &resultCheck)
|
|
if err == nil && len(resultCheck.Reason) > 0 {
|
|
return resultCheck.Reason
|
|
}
|
|
|
|
genericResult := map[string]interface{}{}
|
|
err = json.Unmarshal([]byte(result), &genericResult)
|
|
if err == nil {
|
|
reason, ok := genericResult["reason"].(string)
|
|
if ok && len(reason) > 0 {
|
|
return reason
|
|
}
|
|
|
|
errorValue, ok := genericResult["error"].(string)
|
|
if ok && len(errorValue) > 0 {
|
|
return errorValue
|
|
}
|
|
}
|
|
|
|
return description
|
|
}
|
|
|
|
func enrichNotificationFailureContext(ctx context.Context, referenceUrl, description string) NotificationFailureContext {
|
|
enriched := NotificationFailureContext{}
|
|
enriched.ExecutionId = getNotificationReferenceParam(referenceUrl, "execution_id")
|
|
enriched.NodeId = getNotificationReferenceParam(referenceUrl, "node")
|
|
|
|
if len(enriched.ExecutionId) == 0 {
|
|
return enriched
|
|
}
|
|
|
|
workflowExecution, err := GetWorkflowExecution(ctx, enriched.ExecutionId)
|
|
if err != nil {
|
|
log.Printf("[DEBUG] Failed loading execution %s for notification enrichment: %s", enriched.ExecutionId, err)
|
|
return enriched
|
|
}
|
|
|
|
enriched.WorkflowId = workflowExecution.WorkflowId
|
|
if len(workflowExecution.Workflow.ID) > 0 {
|
|
enriched.WorkflowId = workflowExecution.Workflow.ID
|
|
}
|
|
|
|
if len(enriched.NodeId) == 0 {
|
|
enriched.NodeId = workflowExecution.LastNode
|
|
}
|
|
|
|
if len(enriched.NodeId) == 0 {
|
|
enriched.FailureReason = description
|
|
return enriched
|
|
}
|
|
|
|
action := GetAction(*workflowExecution, enriched.NodeId, "")
|
|
if len(action.ID) > 0 {
|
|
enriched.NodeLabel = action.Label
|
|
enriched.ActionName = action.Name
|
|
enriched.AppName = action.AppName
|
|
}
|
|
|
|
_, actionResult := GetActionResult(ctx, *workflowExecution, enriched.NodeId)
|
|
if len(actionResult.Action.ID) > 0 {
|
|
enriched.NodeStatus = actionResult.Status
|
|
|
|
if len(enriched.NodeLabel) == 0 {
|
|
enriched.NodeLabel = actionResult.Action.Label
|
|
}
|
|
|
|
if len(enriched.ActionName) == 0 {
|
|
enriched.ActionName = actionResult.Action.Name
|
|
}
|
|
|
|
if len(enriched.AppName) == 0 {
|
|
enriched.AppName = actionResult.Action.AppName
|
|
}
|
|
|
|
enriched.FailureReason = getFailureReasonFromResult(actionResult.Result, description)
|
|
}
|
|
|
|
if len(enriched.FailureReason) == 0 {
|
|
enriched.FailureReason = description
|
|
}
|
|
|
|
if len(enriched.FailureReason) > 2000 {
|
|
enriched.FailureReason = enriched.FailureReason[:2000]
|
|
}
|
|
|
|
return enriched
|
|
}
|
|
|
|
// New fields:
|
|
// Severities = LOW/MEDIUM/HIGH/CRITICAL
|
|
// Origin = the source location
|
|
func CreateOrgNotification(ctx context.Context, title, description, referenceUrl, orgId string, adminsOnly bool, severity string, origin string) error {
|
|
if standalone {
|
|
return nil
|
|
}
|
|
|
|
if len(orgId) == 0 {
|
|
log.Printf("[ERROR] No org ID provided to create notification '%s'", title)
|
|
return errors.New("no org ID provided")
|
|
}
|
|
|
|
// Since we use a static workflow name, this should be effective.
|
|
if strings.Contains(title, "Ops Dashboard Workflow") {
|
|
log.Printf("[INFO] Skipping create notification for health check workflow")
|
|
return errors.New("health check workflow detected")
|
|
}
|
|
|
|
if project.Environment == "" {
|
|
|
|
auth := os.Getenv("AUTH")
|
|
org := os.Getenv("ORG")
|
|
environment := os.Getenv("ENVIRONMENT_NAME")
|
|
if len(auth) == 0 || len(org) == 0 || len(environment) == 0 {
|
|
log.Printf("[ERROR] Not generating notification, as no project.Environment has been detected: %#v. This should not happen in Orborus. ENV: %s, AUTH: %d, ORG: %d", project.Environment, environment, len(auth), len(org))
|
|
return nil
|
|
}
|
|
|
|
// Overriding it for Orborus to ensure we have a way to manage
|
|
project.Environment = "worker"
|
|
}
|
|
|
|
//log.Printf("[DEBUG] Creating org notification! %s. Env: %s", orgId, project.Environment)
|
|
|
|
// Check if the referenceUrl is already in cache or not
|
|
if len(referenceUrl) > 0 {
|
|
// Have a 0-0.5 sec timeout here?
|
|
|
|
cacheKey := fmt.Sprintf("notification-%s", referenceUrl)
|
|
_, err := GetCache(ctx, cacheKey)
|
|
if err == nil {
|
|
// Avoiding duplicates for the same workflow+execution
|
|
if project.Environment != "cloud" {
|
|
//log.Printf("[DEBUG] Found cached notification for %s", referenceUrl)
|
|
}
|
|
|
|
return nil
|
|
|
|
} else {
|
|
if project.Environment != "cloud" {
|
|
//log.Printf("[DEBUG] No cached notification for %s. Creating one", referenceUrl)
|
|
}
|
|
|
|
err := SetCache(ctx, cacheKey, []byte("1"), 1)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed saving cached notification %s: %s", cacheKey, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// FIXME: Send a request to the backend here from worker when optimized
|
|
if project.Environment == "worker" {
|
|
log.Printf("[DEBUG] Creating backend notification for org %s", orgId)
|
|
forwardNotificationRequest(ctx, title, description, referenceUrl, orgId)
|
|
return nil
|
|
}
|
|
|
|
//log.Printf("[DEBUG] Creating notification for org '%s'", orgId)
|
|
notifications, err := GetOrgNotifications(ctx, orgId)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed getting org notifications for %s: %s", orgId, err)
|
|
}
|
|
|
|
matchingNotifications := []Notification{}
|
|
for _, notification := range notifications {
|
|
if notification.Personal {
|
|
continue
|
|
}
|
|
|
|
// notification.Title == title &&
|
|
//log.Printf("%s vs %s", notification.ReferenceUrl, referenceUrl)
|
|
if notification.Title == title && notification.Description == description {
|
|
matchingNotifications = append(matchingNotifications, notification)
|
|
}
|
|
}
|
|
|
|
org, err := GetOrg(ctx, orgId)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Error getting org %s in createOrgNotification: %s", orgId, err)
|
|
return err
|
|
}
|
|
|
|
enrichedFailureContext := enrichNotificationFailureContext(ctx, referenceUrl, description)
|
|
|
|
generatedId := uuid.NewV4().String()
|
|
mainNotification := Notification{
|
|
Title: title,
|
|
Description: description,
|
|
Id: generatedId,
|
|
OrgId: orgId,
|
|
OrgName: org.Name,
|
|
UserId: "",
|
|
Tags: []string{},
|
|
Amount: 1,
|
|
ReferenceUrl: referenceUrl,
|
|
OrgNotificationId: "",
|
|
Dismissable: true,
|
|
Personal: false,
|
|
Read: false,
|
|
CreatedAt: int64(time.Now().Unix()),
|
|
UpdatedAt: int64(time.Now().Unix()),
|
|
ExecutionId: enrichedFailureContext.ExecutionId,
|
|
WorkflowId: enrichedFailureContext.WorkflowId,
|
|
NodeId: enrichedFailureContext.NodeId,
|
|
NodeLabel: enrichedFailureContext.NodeLabel,
|
|
ActionName: enrichedFailureContext.ActionName,
|
|
AppName: enrichedFailureContext.AppName,
|
|
NodeStatus: enrichedFailureContext.NodeStatus,
|
|
FailureReason: enrichedFailureContext.FailureReason,
|
|
Severity: severity,
|
|
Origin: origin,
|
|
}
|
|
|
|
selectedApikey := ""
|
|
|
|
authOrg := org
|
|
if org.Defaults.NotificationWorkflow == "parent" && org.CreatorOrg != "" {
|
|
parentOrg, err := GetOrg(ctx, org.CreatorOrg)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to get required parent org %s: %s", org.CreatorOrg, err)
|
|
return err
|
|
}
|
|
if parentOrg == nil {
|
|
log.Printf("[ERROR] Required parent org %s not found", org.CreatorOrg)
|
|
return errors.New("parent org not found")
|
|
}
|
|
authOrg = parentOrg
|
|
org.Defaults.NotificationWorkflow = parentOrg.Defaults.NotificationWorkflow
|
|
}
|
|
|
|
for _, user := range authOrg.Users {
|
|
if user.Role == "org-reader" || user.Id == "" {
|
|
continue
|
|
}
|
|
|
|
foundUser, err := GetUser(ctx, user.Id)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
apiKey := foundUser.ApiKey
|
|
|
|
// if user has no API key, generate one
|
|
if apiKey == "" {
|
|
generatedUser, genErr := GenerateApikey(ctx, *foundUser)
|
|
if genErr != nil {
|
|
log.Printf("[ERROR] Failed to auto-generate API key for user %s: %s", foundUser.Username, genErr)
|
|
continue
|
|
}
|
|
apiKey = generatedUser.ApiKey
|
|
}
|
|
|
|
selectedApikey = apiKey
|
|
break
|
|
}
|
|
|
|
if len(matchingNotifications) > 0 {
|
|
// FIXME: This may have bugs for old workflows with new users (not being rediscovered)
|
|
if project.Environment != "cloud" {
|
|
log.Printf("[INFO] Reopening notification with title %#v for users in org %s", title, orgId)
|
|
}
|
|
|
|
usersHandled := []string{}
|
|
// Make sure to only reopen one per user
|
|
for _, notification := range matchingNotifications {
|
|
if ArrayContains(usersHandled, notification.UserId) {
|
|
//log.Printf("[DEBUG] Skipping notification %s for user %s as it's already been handled", notification.Title, notification.UserId)
|
|
|
|
continue
|
|
}
|
|
|
|
//if notification.Read == false {
|
|
// log.Printf("[DEBUG] Incrementing notification %s for user %s as it's NOT been read", notification.Title, notification.UserId)
|
|
// notification.Amount += 1
|
|
// usersHandled = append(usersHandled, notification.UserId)
|
|
// continue
|
|
//}
|
|
|
|
notification.Amount += 1
|
|
notification.Read = false
|
|
notification.ReferenceUrl = referenceUrl
|
|
notification.ExecutionId = mainNotification.ExecutionId
|
|
notification.WorkflowId = mainNotification.WorkflowId
|
|
notification.NodeId = mainNotification.NodeId
|
|
notification.NodeLabel = mainNotification.NodeLabel
|
|
notification.ActionName = mainNotification.ActionName
|
|
notification.AppName = mainNotification.AppName
|
|
notification.NodeStatus = mainNotification.NodeStatus
|
|
notification.FailureReason = mainNotification.FailureReason
|
|
|
|
// Added ignore as someone could want to never see a specific alert again due to e.g. expecting a 404 on purpose
|
|
if notification.Ignored {
|
|
notification.Read = true
|
|
|
|
mainNotification.Ignored = true
|
|
}
|
|
|
|
err = SetNotification(ctx, notification)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed to reopen notification %s for user %s", notification.Title, notification.UserId)
|
|
} else {
|
|
//log.Printf("[INFO] Reopened and incremented notification %s for %s", notification.Title, notification.UserId)
|
|
usersHandled = append(usersHandled, notification.UserId)
|
|
}
|
|
}
|
|
|
|
if mainNotification.Ignored {
|
|
log.Printf("[INFO] Ignored notification %s for %s", mainNotification.Title, mainNotification.UserId)
|
|
} else {
|
|
authOrgValue := *authOrg
|
|
go func() {
|
|
err = sendToNotificationWorkflow(ctx, mainNotification, selectedApikey, org.Defaults.NotificationWorkflow, false, authOrgValue)
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "cache stored") && !strings.Contains(err.Error(), "Same workflow") {
|
|
log.Printf("[ERROR] Failed sending notification to workflowId %s for reference %s (2): %s", org.Defaults.NotificationWorkflow, mainNotification.Id, err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
return nil
|
|
} else {
|
|
//log.Printf("[INFO] New notification with title %#v is being made for users in org %s", title, orgId)
|
|
|
|
// Only gonna load this after
|
|
// All the other personal ones are kind of irrelevant
|
|
err = SetNotification(ctx, mainNotification)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed making org notification with title %#v for org %s", title, orgId)
|
|
return err
|
|
}
|
|
|
|
// 1. Find users in org
|
|
// 2. Make notification for each of them
|
|
// 3. Make reference to org notification
|
|
|
|
//NotificationWorkflow string `json:"notification_workflow" datastore:"notification_workflow"`
|
|
|
|
if len(org.Defaults.NotificationWorkflow) > 0 {
|
|
if len(selectedApikey) == 0 {
|
|
log.Printf("[ERROR] No API key available to trigger notification workflow for org %s to workflow %s", org.Id, org.Defaults.NotificationWorkflow)
|
|
return errors.New("no API key available for notification workflow")
|
|
}
|
|
|
|
workflow, err := GetWorkflow(ctx, org.Defaults.NotificationWorkflow)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed getting workflow with ID %s: %s", org.Defaults.NotificationWorkflow, err)
|
|
return err
|
|
}
|
|
|
|
if workflow.OrgId != mainNotification.OrgId {
|
|
log.Printf("[WARNING] Can't access workflow %s with org %s (%s): %#v", workflow.ID, mainNotification.OrgName, mainNotification.OrgId, workflow.Org)
|
|
|
|
// Get parent org if it exists and check too
|
|
if len(org.ManagerOrgs) > 0 {
|
|
parentOrg, err := GetOrg(ctx, org.ManagerOrgs[0].Id)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Error getting parent org %s in createOrgNotification (2): %s", orgId, err)
|
|
return err
|
|
}
|
|
|
|
if org.Defaults.NotificationWorkflow != parentOrg.Defaults.NotificationWorkflow {
|
|
return errors.New(fmt.Sprintf("Org %s does not have access to workflow with ID %s", mainNotification.OrgId, workflow.ID))
|
|
} else {
|
|
log.Printf("[DEBUG] Running with parent orgs' notification workflow")
|
|
}
|
|
} else {
|
|
return errors.New(fmt.Sprintf("Org %s does not have access to workflow with ID %s", mainNotification.OrgId, workflow.ID))
|
|
}
|
|
}
|
|
|
|
authOrgValue := *authOrg
|
|
go func() {
|
|
err = sendToNotificationWorkflow(ctx, mainNotification, selectedApikey, org.Defaults.NotificationWorkflow, false, authOrgValue)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed sending notification to workflowId %s for reference %s: %s", org.Defaults.NotificationWorkflow, mainNotification.Id, err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func HandleCreateNotification(resp http.ResponseWriter, request *http.Request) {
|
|
cors := HandleCors(resp, request)
|
|
if cors {
|
|
return
|
|
}
|
|
|
|
// Unmarshal body to the Notification struct
|
|
// Done first so we can use the data for auth
|
|
body, err := ioutil.ReadAll(request.Body)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed reading body in create notification api: %s", err)
|
|
resp.WriteHeader(500)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
//log.Printf("[DEBUG] Creating notification based on: %s", string(body))
|
|
|
|
notification := Notification{}
|
|
err = json.Unmarshal(body, ¬ification)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed unmarshaling body in create notification api: %s", err)
|
|
resp.WriteHeader(500)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
// 1. Check user directly
|
|
// 2. Check workflow execution authorization
|
|
skipUserCheck := false
|
|
orgId := ""
|
|
ctx := GetContext(request)
|
|
user, err := HandleApiAuthentication(resp, request)
|
|
if err != nil {
|
|
log.Printf("[AUDIT] INITIAL Api authentication failed in Create notification api: %s", err)
|
|
|
|
// Environmentauth
|
|
// Why don't we have a function for this?
|
|
newOrgId := request.Header.Get("Org-Id")
|
|
environment := request.Header.Get("ENVIRONMENT_NAME")
|
|
apikey := request.Header.Get("Authorization")
|
|
if len(newOrgId) > 0 {
|
|
orgId = newOrgId
|
|
}
|
|
|
|
// Doesn't work ENV never have the auth
|
|
if len(orgId) > 0 && len(environment) > 0 && len(apikey) > 0 && false {
|
|
authHeaderParts := strings.Split(apikey, " ")
|
|
if len(authHeaderParts) != 2 {
|
|
log.Printf("[WARNING] Invalid authorization header in create notification api")
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
if authHeaderParts[0] != "Bearer" {
|
|
log.Printf("[WARNING] Invalid authorization header in create notification api")
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
authKey := authHeaderParts[1]
|
|
environments, err := GetEnvironments(ctx, orgId)
|
|
if err != nil {
|
|
resp.WriteHeader(400)
|
|
resp.Write([]byte(`{"success": false, "reason": "Failed getting environments"}`))
|
|
return
|
|
}
|
|
|
|
found := false
|
|
for _, env := range environments {
|
|
if env.Name == environment && env.Auth == authKey {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
log.Printf("[AUDIT] Invalid authorization header in create notification api for Orborus request")
|
|
resp.WriteHeader(403)
|
|
resp.Write([]byte(`{"success": false, "reason": "Invalid authorization config for Environment auth"}`))
|
|
return
|
|
}
|
|
|
|
log.Printf("[AUDIT] Environment auth successful for environment %s", environment)
|
|
|
|
} else {
|
|
// Allows for execution authorization
|
|
if len(notification.ExecutionId) == 0 {
|
|
log.Printf("[INFO][%s] User tried to create notification without an execution ID present. OrgId: %s", notification.ExecutionId, orgId)
|
|
resp.WriteHeader(403)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
exec, err := GetWorkflowExecution(ctx, notification.ExecutionId)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed getting execution %s in create notification api: %s", notification.ExecutionId, err)
|
|
resp.WriteHeader(400)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
// Check if user has access. Parse out authorization header with "Bearer X"
|
|
authHeader := request.Header.Get("Authorization")
|
|
if len(authHeader) == 0 {
|
|
log.Printf("[INFO] No authorization header in create notification api")
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
authHeaderParts := strings.Split(authHeader, " ")
|
|
if len(authHeaderParts) != 2 {
|
|
log.Printf("[INFO] Invalid authorization header in create notification api")
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
if authHeaderParts[0] != "Bearer" {
|
|
log.Printf("[INFO] Invalid authorization header in create notification api")
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
// Check if user has access to execution
|
|
if authHeaderParts[1] != exec.Authorization {
|
|
log.Printf("[INFO] User tried to create notification for execution %s without authorization", exec.ExecutionId)
|
|
resp.WriteHeader(403)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
// Check if exec org id is same
|
|
if exec.OrgId != notification.OrgId {
|
|
log.Printf("[WARNING] User tried to create notification for execution %s with org id %s, but notification org id is %s", exec.ExecutionId, exec.OrgId, notification.OrgId)
|
|
}
|
|
|
|
skipUserCheck = true
|
|
user.Role = "admin"
|
|
user.Username = fmt.Sprintf("execution %s", exec.ExecutionId)
|
|
|
|
if len(exec.ExecutionOrg) > 0 {
|
|
orgId = exec.ExecutionOrg
|
|
}
|
|
|
|
if len(orgId) == 0 && len(exec.OrgId) > 0 {
|
|
orgId = exec.OrgId
|
|
}
|
|
|
|
if len(orgId) == 0 && len(exec.Workflow.OrgId) > 0 {
|
|
orgId = exec.Workflow.OrgId
|
|
}
|
|
|
|
if len(orgId) == 0 {
|
|
log.Printf("[ERROR] No org id found in create notification api from worker(?)")
|
|
resp.WriteHeader(400)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
}
|
|
|
|
user.ActiveOrg.Id = orgId
|
|
notification.OrgId = orgId
|
|
}
|
|
|
|
if user.Role == "org-reader" {
|
|
log.Printf("[INFO] User %s (%s) tried to create a notification without being admin", user.Username, user.Id)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
// Goes in here if it's a manual user making the request
|
|
if !skipUserCheck {
|
|
orgId = user.ActiveOrg.Id
|
|
|
|
if len(notification.OrgId) > 0 {
|
|
orgId = notification.OrgId
|
|
|
|
// Check if user has access
|
|
org, err := GetOrg(ctx, orgId)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed getting org %s in create notification api: %s", orgId, err)
|
|
resp.WriteHeader(500)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
found := false
|
|
for _, orgUser := range org.Users {
|
|
if orgUser.Id == user.Id {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
log.Printf("[ERROR] User %s does not have access to org %s in create notification api", user.Id, orgId)
|
|
resp.WriteHeader(403)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("[DEBUG] User '%s' (%s) in org '%s' (%s) is creating notification '%s'", user.Username, user.Id, user.ActiveOrg.Name, user.ActiveOrg.Id, notification.Title)
|
|
err = CreateOrgNotification(
|
|
ctx,
|
|
notification.Title,
|
|
notification.Description,
|
|
notification.ReferenceUrl,
|
|
orgId,
|
|
false,
|
|
notification.Severity,
|
|
notification.Origin,
|
|
)
|
|
|
|
DeleteCache(ctx, fmt.Sprintf("%s_%s", "notifications", user.ActiveOrg.Id))
|
|
DeleteCache(ctx, fmt.Sprintf("%s_%s", "notifications", user.Id))
|
|
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed creating notification in create notification api: %s", err)
|
|
resp.WriteHeader(500)
|
|
resp.Write([]byte(`{"success": false, "reason": "Failed creating notification"}`))
|
|
return
|
|
}
|
|
|
|
resp.WriteHeader(200)
|
|
resp.Write([]byte(`{"success": true}`))
|
|
}
|