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