github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/model/integration_action.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package model 5 6 import ( 7 "crypto" 8 "crypto/aes" 9 "crypto/cipher" 10 "crypto/ecdsa" 11 "crypto/rand" 12 "encoding/asn1" 13 "encoding/base64" 14 "encoding/json" 15 "fmt" 16 "io" 17 "math/big" 18 "net/http" 19 "strconv" 20 "strings" 21 ) 22 23 const ( 24 POST_ACTION_TYPE_BUTTON = "button" 25 POST_ACTION_TYPE_SELECT = "select" 26 INTERACTIVE_DIALOG_TRIGGER_TIMEOUT_MILLISECONDS = 3000 27 ) 28 29 var PostActionRetainPropKeys = []string{"from_webhook", "override_username", "override_icon_url"} 30 31 type DoPostActionRequest struct { 32 SelectedOption string `json:"selected_option,omitempty"` 33 Cookie string `json:"cookie,omitempty"` 34 } 35 36 type PostAction struct { 37 // A unique Action ID. If not set, generated automatically. 38 Id string `json:"id,omitempty"` 39 40 // The type of the interactive element. Currently supported are 41 // "select" and "button". 42 Type string `json:"type,omitempty"` 43 44 // The text on the button, or in the select placeholder. 45 Name string `json:"name,omitempty"` 46 47 // If the action is disabled. 48 Disabled bool `json:"disabled,omitempty"` 49 50 // Style defines a text and border style. 51 // Supported values are "default", "primary", "success", "good", "warning", "danger" 52 // and any hex color. 53 Style string `json:"style,omitempty"` 54 55 // DataSource indicates the data source for the select action. If left 56 // empty, the select is populated from Options. Other supported values 57 // are "users" and "channels". 58 DataSource string `json:"data_source,omitempty"` 59 60 // Options contains the values listed in a select dropdown on the post. 61 Options []*PostActionOptions `json:"options,omitempty"` 62 63 // DefaultOption contains the option, if any, that will appear as the 64 // default selection in a select box. It has no effect when used with 65 // other types of actions. 66 DefaultOption string `json:"default_option,omitempty"` 67 68 // Defines the interaction with the backend upon a user action. 69 // Integration contains Context, which is private plugin data; 70 // Integrations are stripped from Posts when they are sent to the 71 // client, or are encrypted in a Cookie. 72 Integration *PostActionIntegration `json:"integration,omitempty"` 73 Cookie string `json:"cookie,omitempty" db:"-"` 74 } 75 76 func (p *PostAction) Equals(input *PostAction) bool { 77 if p.Id != input.Id { 78 return false 79 } 80 81 if p.Type != input.Type { 82 return false 83 } 84 85 if p.Name != input.Name { 86 return false 87 } 88 89 if p.DataSource != input.DataSource { 90 return false 91 } 92 93 if p.DefaultOption != input.DefaultOption { 94 return false 95 } 96 97 if p.Cookie != input.Cookie { 98 return false 99 } 100 101 // Compare PostActionOptions 102 if len(p.Options) != len(input.Options) { 103 return false 104 } 105 106 for k := range p.Options { 107 if p.Options[k].Text != input.Options[k].Text { 108 return false 109 } 110 111 if p.Options[k].Value != input.Options[k].Value { 112 return false 113 } 114 } 115 116 // Compare PostActionIntegration 117 if p.Integration.URL != input.Integration.URL { 118 return false 119 } 120 121 if len(p.Integration.Context) != len(input.Integration.Context) { 122 return false 123 } 124 125 for key, value := range p.Integration.Context { 126 inputValue, ok := input.Integration.Context[key] 127 128 if !ok { 129 return false 130 } 131 132 if value != inputValue { 133 return false 134 } 135 } 136 137 return true 138 } 139 140 // PostActionCookie is set by the server, serialized and encrypted into 141 // PostAction.Cookie. The clients should hold on to it, and include it with 142 // subsequent DoPostAction requests. This allows the server to access the 143 // action metadata even when it's not available in the database, for ephemeral 144 // posts. 145 type PostActionCookie struct { 146 Type string `json:"type,omitempty"` 147 PostId string `json:"post_id,omitempty"` 148 RootPostId string `json:"root_post_id,omitempty"` 149 ChannelId string `json:"channel_id,omitempty"` 150 DataSource string `json:"data_source,omitempty"` 151 Integration *PostActionIntegration `json:"integration,omitempty"` 152 RetainProps map[string]interface{} `json:"retain_props,omitempty"` 153 RemoveProps []string `json:"remove_props,omitempty"` 154 } 155 156 type PostActionOptions struct { 157 Text string `json:"text"` 158 Value string `json:"value"` 159 } 160 161 type PostActionIntegration struct { 162 URL string `json:"url,omitempty"` 163 Context map[string]interface{} `json:"context,omitempty"` 164 } 165 166 type PostActionIntegrationRequest struct { 167 UserId string `json:"user_id"` 168 UserName string `json:"user_name"` 169 ChannelId string `json:"channel_id"` 170 ChannelName string `json:"channel_name"` 171 TeamId string `json:"team_id"` 172 TeamName string `json:"team_domain"` 173 PostId string `json:"post_id"` 174 TriggerId string `json:"trigger_id"` 175 Type string `json:"type"` 176 DataSource string `json:"data_source"` 177 Context map[string]interface{} `json:"context,omitempty"` 178 } 179 180 type PostActionIntegrationResponse struct { 181 Update *Post `json:"update"` 182 EphemeralText string `json:"ephemeral_text"` 183 SkipSlackParsing bool `json:"skip_slack_parsing"` // Set to `true` to skip the Slack-compatibility handling of Text. 184 } 185 186 type PostActionAPIResponse struct { 187 Status string `json:"status"` // needed to maintain backwards compatibility 188 TriggerId string `json:"trigger_id"` 189 } 190 191 type Dialog struct { 192 CallbackId string `json:"callback_id"` 193 Title string `json:"title"` 194 IntroductionText string `json:"introduction_text"` 195 IconURL string `json:"icon_url"` 196 Elements []DialogElement `json:"elements"` 197 SubmitLabel string `json:"submit_label"` 198 NotifyOnCancel bool `json:"notify_on_cancel"` 199 State string `json:"state"` 200 } 201 202 type DialogElement struct { 203 DisplayName string `json:"display_name"` 204 Name string `json:"name"` 205 Type string `json:"type"` 206 SubType string `json:"subtype"` 207 Default string `json:"default"` 208 Placeholder string `json:"placeholder"` 209 HelpText string `json:"help_text"` 210 Optional bool `json:"optional"` 211 MinLength int `json:"min_length"` 212 MaxLength int `json:"max_length"` 213 DataSource string `json:"data_source"` 214 Options []*PostActionOptions `json:"options"` 215 } 216 217 type OpenDialogRequest struct { 218 TriggerId string `json:"trigger_id"` 219 URL string `json:"url"` 220 Dialog Dialog `json:"dialog"` 221 } 222 223 type SubmitDialogRequest struct { 224 Type string `json:"type"` 225 URL string `json:"url,omitempty"` 226 CallbackId string `json:"callback_id"` 227 State string `json:"state"` 228 UserId string `json:"user_id"` 229 ChannelId string `json:"channel_id"` 230 TeamId string `json:"team_id"` 231 Submission map[string]interface{} `json:"submission"` 232 Cancelled bool `json:"cancelled"` 233 } 234 235 type SubmitDialogResponse struct { 236 Error string `json:"error,omitempty"` 237 Errors map[string]string `json:"errors,omitempty"` 238 } 239 240 func GenerateTriggerId(userId string, s crypto.Signer) (string, string, *AppError) { 241 clientTriggerId := NewId() 242 triggerData := strings.Join([]string{clientTriggerId, userId, strconv.FormatInt(GetMillis(), 10)}, ":") + ":" 243 244 h := crypto.SHA256 245 sum := h.New() 246 sum.Write([]byte(triggerData)) 247 signature, err := s.Sign(rand.Reader, sum.Sum(nil), h) 248 if err != nil { 249 return "", "", NewAppError("GenerateTriggerId", "interactive_message.generate_trigger_id.signing_failed", nil, err.Error(), http.StatusInternalServerError) 250 } 251 252 base64Sig := base64.StdEncoding.EncodeToString(signature) 253 254 triggerId := base64.StdEncoding.EncodeToString([]byte(triggerData + base64Sig)) 255 return clientTriggerId, triggerId, nil 256 } 257 258 func (r *PostActionIntegrationRequest) GenerateTriggerId(s crypto.Signer) (string, string, *AppError) { 259 clientTriggerId, triggerId, err := GenerateTriggerId(r.UserId, s) 260 if err != nil { 261 return "", "", err 262 } 263 264 r.TriggerId = triggerId 265 return clientTriggerId, triggerId, nil 266 } 267 268 func DecodeAndVerifyTriggerId(triggerId string, s *ecdsa.PrivateKey) (string, string, *AppError) { 269 triggerIdBytes, err := base64.StdEncoding.DecodeString(triggerId) 270 if err != nil { 271 return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.base64_decode_failed", nil, err.Error(), http.StatusBadRequest) 272 } 273 274 split := strings.Split(string(triggerIdBytes), ":") 275 if len(split) != 4 { 276 return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.missing_data", nil, "", http.StatusBadRequest) 277 } 278 279 clientTriggerId := split[0] 280 userId := split[1] 281 timestampStr := split[2] 282 timestamp, _ := strconv.ParseInt(timestampStr, 10, 64) 283 284 now := GetMillis() 285 if now-timestamp > INTERACTIVE_DIALOG_TRIGGER_TIMEOUT_MILLISECONDS { 286 return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.expired", map[string]interface{}{"Seconds": INTERACTIVE_DIALOG_TRIGGER_TIMEOUT_MILLISECONDS / 1000}, "", http.StatusBadRequest) 287 } 288 289 signature, err := base64.StdEncoding.DecodeString(split[3]) 290 if err != nil { 291 return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.base64_decode_failed_signature", nil, err.Error(), http.StatusBadRequest) 292 } 293 294 var esig struct { 295 R, S *big.Int 296 } 297 298 if _, err := asn1.Unmarshal(signature, &esig); err != nil { 299 return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.signature_decode_failed", nil, err.Error(), http.StatusBadRequest) 300 } 301 302 triggerData := strings.Join([]string{clientTriggerId, userId, timestampStr}, ":") + ":" 303 304 h := crypto.SHA256 305 sum := h.New() 306 sum.Write([]byte(triggerData)) 307 308 if !ecdsa.Verify(&s.PublicKey, sum.Sum(nil), esig.R, esig.S) { 309 return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.verify_signature_failed", nil, "", http.StatusBadRequest) 310 } 311 312 return clientTriggerId, userId, nil 313 } 314 315 func (r *OpenDialogRequest) DecodeAndVerifyTriggerId(s *ecdsa.PrivateKey) (string, string, *AppError) { 316 return DecodeAndVerifyTriggerId(r.TriggerId, s) 317 } 318 319 func (r *PostActionIntegrationRequest) ToJson() []byte { 320 b, _ := json.Marshal(r) 321 return b 322 } 323 324 func PostActionIntegrationRequestFromJson(data io.Reader) *PostActionIntegrationRequest { 325 var o *PostActionIntegrationRequest 326 err := json.NewDecoder(data).Decode(&o) 327 if err != nil { 328 return nil 329 } 330 return o 331 } 332 333 func (r *PostActionIntegrationResponse) ToJson() []byte { 334 b, _ := json.Marshal(r) 335 return b 336 } 337 338 func PostActionIntegrationResponseFromJson(data io.Reader) *PostActionIntegrationResponse { 339 var o *PostActionIntegrationResponse 340 err := json.NewDecoder(data).Decode(&o) 341 if err != nil { 342 return nil 343 } 344 return o 345 } 346 347 func SubmitDialogRequestFromJson(data io.Reader) *SubmitDialogRequest { 348 var o *SubmitDialogRequest 349 err := json.NewDecoder(data).Decode(&o) 350 if err != nil { 351 return nil 352 } 353 return o 354 } 355 356 func (r *SubmitDialogRequest) ToJson() []byte { 357 b, _ := json.Marshal(r) 358 return b 359 } 360 361 func SubmitDialogResponseFromJson(data io.Reader) *SubmitDialogResponse { 362 var o *SubmitDialogResponse 363 err := json.NewDecoder(data).Decode(&o) 364 if err != nil { 365 return nil 366 } 367 return o 368 } 369 370 func (r *SubmitDialogResponse) ToJson() []byte { 371 b, _ := json.Marshal(r) 372 return b 373 } 374 375 func (o *Post) StripActionIntegrations() { 376 attachments := o.Attachments() 377 if o.GetProp("attachments") != nil { 378 o.AddProp("attachments", attachments) 379 } 380 for _, attachment := range attachments { 381 for _, action := range attachment.Actions { 382 action.Integration = nil 383 } 384 } 385 } 386 387 func (o *Post) GetAction(id string) *PostAction { 388 for _, attachment := range o.Attachments() { 389 for _, action := range attachment.Actions { 390 if action.Id == id { 391 return action 392 } 393 } 394 } 395 return nil 396 } 397 398 func (o *Post) GenerateActionIds() { 399 if o.GetProp("attachments") != nil { 400 o.AddProp("attachments", o.Attachments()) 401 } 402 if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok { 403 for _, attachment := range attachments { 404 for _, action := range attachment.Actions { 405 if action.Id == "" { 406 action.Id = NewId() 407 } 408 } 409 } 410 } 411 } 412 413 func AddPostActionCookies(o *Post, secret []byte) *Post { 414 p := o.Clone() 415 416 // retainedProps carry over their value from the old post, including no value 417 retainProps := map[string]interface{}{} 418 removeProps := []string{} 419 for _, key := range PostActionRetainPropKeys { 420 value, ok := p.GetProps()[key] 421 if ok { 422 retainProps[key] = value 423 } else { 424 removeProps = append(removeProps, key) 425 } 426 } 427 428 attachments := p.Attachments() 429 for _, attachment := range attachments { 430 for _, action := range attachment.Actions { 431 c := &PostActionCookie{ 432 Type: action.Type, 433 ChannelId: p.ChannelId, 434 DataSource: action.DataSource, 435 Integration: action.Integration, 436 RetainProps: retainProps, 437 RemoveProps: removeProps, 438 } 439 440 c.PostId = p.Id 441 if p.RootId == "" { 442 c.RootPostId = p.Id 443 } else { 444 c.RootPostId = p.RootId 445 } 446 447 b, _ := json.Marshal(c) 448 action.Cookie, _ = encryptPostActionCookie(string(b), secret) 449 } 450 } 451 452 return p 453 } 454 455 func encryptPostActionCookie(plain string, secret []byte) (string, error) { 456 if len(secret) == 0 { 457 return plain, nil 458 } 459 460 block, err := aes.NewCipher(secret) 461 if err != nil { 462 return "", err 463 } 464 465 aesgcm, err := cipher.NewGCM(block) 466 if err != nil { 467 return "", err 468 } 469 470 nonce := make([]byte, aesgcm.NonceSize()) 471 _, err = io.ReadFull(rand.Reader, nonce) 472 if err != nil { 473 return "", err 474 } 475 476 sealed := aesgcm.Seal(nil, nonce, []byte(plain), nil) 477 478 combined := append(nonce, sealed...) 479 encoded := make([]byte, base64.StdEncoding.EncodedLen(len(combined))) 480 base64.StdEncoding.Encode(encoded, combined) 481 482 return string(encoded), nil 483 } 484 485 func DecryptPostActionCookie(encoded string, secret []byte) (string, error) { 486 if len(secret) == 0 { 487 return encoded, nil 488 } 489 490 block, err := aes.NewCipher(secret) 491 if err != nil { 492 return "", err 493 } 494 495 aesgcm, err := cipher.NewGCM(block) 496 if err != nil { 497 return "", err 498 } 499 500 decoded := make([]byte, base64.StdEncoding.DecodedLen(len(encoded))) 501 n, err := base64.StdEncoding.Decode(decoded, []byte(encoded)) 502 if err != nil { 503 return "", err 504 } 505 decoded = decoded[:n] 506 507 nonceSize := aesgcm.NonceSize() 508 if len(decoded) < nonceSize { 509 return "", fmt.Errorf("cookie too short") 510 } 511 512 nonce, decoded := decoded[:nonceSize], decoded[nonceSize:] 513 plain, err := aesgcm.Open(nil, nonce, decoded, nil) 514 if err != nil { 515 return "", err 516 } 517 518 return string(plain), nil 519 } 520 521 func DoPostActionRequestFromJson(data io.Reader) *DoPostActionRequest { 522 var o *DoPostActionRequest 523 json.NewDecoder(data).Decode(&o) 524 return o 525 }