github.com/teloshs/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  }