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  }