Files
shuffle-cracked/backend/go-app/shuffle-shared/notifications.go
T
Marat Kharitonov 4f3f07d4dd Crack: bypass license check - force all limits unlimited
- 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
2026-08-12 03:14:24 +03:00

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, &notification)
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, &notification)
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}`))
}