Files
1Panel/agent/utils/alert_config/secret.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
}
}