mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
313 lines
7.9 KiB
Go
313 lines
7.9 KiB
Go
package alert_config
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
)
|
|
|
|
const MaskedSecret = "******"
|
|
|
|
type secretMutation struct {
|
|
Action string `json:"action"`
|
|
Value string `json:"value,omitempty"`
|
|
}
|
|
|
|
func IsLegacySecretType(configType string) bool {
|
|
return secretField(configType) != ""
|
|
}
|
|
|
|
func UsesMutation(configType, rawMutation string) (bool, error) {
|
|
field := secretField(configType)
|
|
if field == "" {
|
|
return false, nil
|
|
}
|
|
root, err := decodeObject(rawMutation)
|
|
if err != nil {
|
|
return false, fmt.Errorf("decode alert config mutation: %w", err)
|
|
}
|
|
if raw, ok := root[field]; ok && rawIsObject(raw) {
|
|
return true, nil
|
|
}
|
|
if !isWebhookType(configType) {
|
|
return false, nil
|
|
}
|
|
raw, ok := root["webhooks"]
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
var items []map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &items); err != nil {
|
|
return false, fmt.Errorf("webhooks mutation must be an array")
|
|
}
|
|
for _, item := range items {
|
|
if rawURL, ok := item["url"]; ok && rawIsObject(rawURL) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func Prepare(configType, rawMutation, status string, existing *model.AlertConfig) (string, error) {
|
|
field := secretField(configType)
|
|
if field == "" {
|
|
return rawMutation, nil
|
|
}
|
|
root, err := decodeObject(rawMutation)
|
|
if err != nil {
|
|
return "", fmt.Errorf("decode alert config mutation: %w", err)
|
|
}
|
|
var existingRoot map[string]json.RawMessage
|
|
if existing != nil {
|
|
if existing.Type != configType {
|
|
return "", fmt.Errorf("alert config %d has type %s, not %s", existing.ID, existing.Type, configType)
|
|
}
|
|
existingRoot, err = decodeObject(existing.Config)
|
|
if err != nil {
|
|
return "", fmt.Errorf("decode stored alert config: %w", err)
|
|
}
|
|
}
|
|
|
|
existingSecret, err := storedSecret(existingRoot, field)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if raw, ok := root[field]; ok {
|
|
value, err := mergeSecret(field, raw, existingSecret, existing != nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("merge alert config %s: %w", field, err)
|
|
}
|
|
root[field] = mustJSON(value)
|
|
} else if existing != nil {
|
|
root[field] = mustJSON(existingSecret)
|
|
}
|
|
|
|
if isWebhookType(configType) {
|
|
if err := mergeWebhookArray(root, existingRoot, existing != nil); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
if status == constant.AlertEnable {
|
|
if configType == constant.SMSConfig {
|
|
phone, err := storedSecret(root, "phone")
|
|
if err != nil || phone == "" {
|
|
return "", fmt.Errorf("SMS phone is required while the config is enabled")
|
|
}
|
|
}
|
|
if isWebhookType(configType) && !hasWebhookURL(root) {
|
|
return "", fmt.Errorf("webhook URL is required while the config is enabled")
|
|
}
|
|
}
|
|
return encodeObject(root)
|
|
}
|
|
|
|
func secretField(configType string) string {
|
|
switch configType {
|
|
case constant.EmailConfig:
|
|
return "password"
|
|
case constant.SMSConfig:
|
|
return "phone"
|
|
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark:
|
|
return "url"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func isWebhookType(configType string) bool {
|
|
switch configType {
|
|
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func decodeObject(raw string) (map[string]json.RawMessage, error) {
|
|
decoder := json.NewDecoder(strings.NewReader(raw))
|
|
decoder.DisallowUnknownFields()
|
|
var result map[string]json.RawMessage
|
|
if err := decoder.Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
if result == nil {
|
|
return nil, fmt.Errorf("alert config must be a JSON object")
|
|
}
|
|
var trailing any
|
|
if err := decoder.Decode(&trailing); err != io.EOF {
|
|
if err == nil {
|
|
return nil, fmt.Errorf("multiple JSON values are not allowed")
|
|
}
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func encodeObject(value map[string]json.RawMessage) (string, error) {
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil {
|
|
return "", fmt.Errorf("encode alert config: %w", err)
|
|
}
|
|
return string(encoded), nil
|
|
}
|
|
|
|
func mustJSON(value any) json.RawMessage {
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return encoded
|
|
}
|
|
|
|
func decodeStoredSecret(raw json.RawMessage) (string, error) {
|
|
var value string
|
|
if err := json.Unmarshal(raw, &value); err != nil {
|
|
return "", fmt.Errorf("stored secret is not a string")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func storedSecret(root map[string]json.RawMessage, field string) (string, error) {
|
|
if root == nil {
|
|
return "", nil
|
|
}
|
|
raw, ok := root[field]
|
|
if !ok {
|
|
return "", nil
|
|
}
|
|
value, err := decodeStoredSecret(raw)
|
|
if err != nil {
|
|
return "", fmt.Errorf("stored alert config %s is invalid", field)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func mergeSecret(field string, raw json.RawMessage, existing string, hasExisting bool) (string, error) {
|
|
if value, err := decodeStoredSecret(raw); err == nil {
|
|
if hasExisting && isLegacyMaskValue(field, value) {
|
|
return existing, nil
|
|
}
|
|
return value, nil
|
|
}
|
|
var mutation secretMutation
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&mutation); err != nil {
|
|
return "", fmt.Errorf("secret mutation must be a string or keep/replace/clear object")
|
|
}
|
|
switch mutation.Action {
|
|
case "keep":
|
|
if !hasExisting {
|
|
return "", fmt.Errorf("secret cannot be kept because the config does not exist")
|
|
}
|
|
return existing, nil
|
|
case "replace":
|
|
return mutation.Value, nil
|
|
case "clear":
|
|
return "", nil
|
|
default:
|
|
return "", fmt.Errorf("secret action must be keep, replace, or clear")
|
|
}
|
|
}
|
|
|
|
func isLegacyMaskValue(field, value string) bool {
|
|
if field == "phone" {
|
|
return isLegacyMaskedPhone(value)
|
|
}
|
|
return value == MaskedSecret
|
|
}
|
|
|
|
func isLegacyMaskedPhone(value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
if !strings.Contains(value, "*") {
|
|
return false
|
|
}
|
|
return maskPhone(strings.ReplaceAll(value, "*", "0")) == value
|
|
}
|
|
|
|
func rawIsObject(raw json.RawMessage) bool {
|
|
trimmed := bytes.TrimSpace(raw)
|
|
return len(trimmed) > 0 && trimmed[0] == '{'
|
|
}
|
|
|
|
func mergeWebhookArray(root, existingRoot map[string]json.RawMessage, hasExisting bool) error {
|
|
raw, ok := root["webhooks"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
var items []map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &items); err != nil {
|
|
return fmt.Errorf("webhooks mutation must be an array")
|
|
}
|
|
var existingItems []map[string]json.RawMessage
|
|
if existingRoot != nil {
|
|
if existingRaw, ok := existingRoot["webhooks"]; ok {
|
|
if err := json.Unmarshal(existingRaw, &existingItems); err != nil {
|
|
return fmt.Errorf("stored webhooks must be an array")
|
|
}
|
|
}
|
|
}
|
|
for index := range items {
|
|
rawURL, ok := items[index]["url"]
|
|
if !ok {
|
|
continue
|
|
}
|
|
var existingURL string
|
|
itemExists := hasExisting && index < len(existingItems)
|
|
if itemExists {
|
|
var err error
|
|
existingURL, err = storedSecret(existingItems[index], "url")
|
|
if err != nil {
|
|
return fmt.Errorf("stored webhook URL %d is invalid", index)
|
|
}
|
|
}
|
|
value, err := mergeSecret("url", rawURL, existingURL, itemExists)
|
|
if err != nil {
|
|
return fmt.Errorf("merge webhook URL %d: %w", index, err)
|
|
}
|
|
items[index]["url"] = mustJSON(value)
|
|
}
|
|
root["webhooks"] = mustJSON(items)
|
|
return nil
|
|
}
|
|
|
|
func hasWebhookURL(root map[string]json.RawMessage) bool {
|
|
if value, err := storedSecret(root, "url"); err == nil && value != "" {
|
|
return true
|
|
}
|
|
raw, ok := root["webhooks"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
var items []map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &items); err != nil {
|
|
return false
|
|
}
|
|
for _, item := range items {
|
|
if value, err := storedSecret(item, "url"); err == nil && value != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func maskPhone(value string) string {
|
|
runes := []rune(strings.TrimSpace(value))
|
|
switch {
|
|
case len(runes) >= 11:
|
|
return string(runes[:3]) + strings.Repeat("*", len(runes)-7) + string(runes[len(runes)-4:])
|
|
case len(runes) >= 8:
|
|
return string(runes[:2]) + strings.Repeat("*", len(runes)-4) + string(runes[len(runes)-2:])
|
|
case utf8.RuneCountInString(value) == 0:
|
|
return ""
|
|
default:
|
|
return MaskedSecret
|
|
}
|
|
}
|