MM-10516: Added support for PostActions in ephemeral posts (#10258)

* Added support for PostActions in ephemeral posts

The general approach is that we take all the metadata that DoPostAction
needs to process client DoPostActionRequests, and store it in a
serialized, encrypted Cookie field, in the PostAction struct.

The client then must send it back, and it is then used to process
PostActions as a fallback top the metadata in the database.

This PR adds a new config setting, `ServiceSettings.ActionCookieSecret`.
In a cluster environment it must be the same for all instances.

- Added type PostActionCookie, and a Cookie string to PostAction.
- Added App.AddActionCookiesToPost.
- Use App.AddActionCookiesToPost in api4.createEphemeralPost,
  App.SendEphemeralPost, App.UpdateEphemeralPost.
- Added App.DoPostActionWithCookie to process incoming requests with
  cookies. For backward compatibility, it prefers the metadata in the
  database; falls back to cookie.
- Added plugin.API.UpdateEphemeralPost and plugin.API.DeleteEphemeralPost.
- Added App.encryptActionCookie/App.decryptActionCookie.

* Style

* Fixed an unfortunate typo, tested with matterpoll

* minor PR feedback

* Fixed uninitialized Context

* Fixed another test failure

* Fixed permission check

* Added api test for DoPostActionWithCookie

* Replaced config.ActionCookieSecret with Server.PostActionCookieSecret

Modeled after AsymetricSigningKey

* style

* Set DeleteAt in DeleteEphemeralPost

* PR feedback

* Removed deadwood comment

* Added EXPERIMENTAL comment to the 2 APIs in question
Этот коммит содержится в:
Lev
2019-03-01 10:15:31 -08:00
коммит произвёл GitHub
родитель dcf611b735
Коммит 3ad901b50b
17 изменённых файлов: 611 добавлений и 85 удалений

Просмотреть файл

@@ -23,26 +23,46 @@ func doPostAction(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if !c.App.SessionHasPermissionToChannelByPost(c.App.Session, c.Params.PostId, model.PERMISSION_READ_CHANNEL) {
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
actionRequest := model.DoPostActionRequestFromJson(r.Body)
if actionRequest == nil {
actionRequest = &model.DoPostActionRequest{}
}
var err *model.AppError
var cookie *model.PostActionCookie
if actionRequest.Cookie != "" {
cookie = &model.PostActionCookie{}
cookieStr, err := model.DecryptPostActionCookie(actionRequest.Cookie, c.App.PostActionCookieSecret())
if err != nil {
c.Err = model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
return
}
err = json.Unmarshal([]byte(cookieStr), &cookie)
if err != nil {
c.Err = model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionToChannel(c.App.Session, cookie.ChannelId, model.PERMISSION_READ_CHANNEL) {
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
} else {
if !c.App.SessionHasPermissionToChannelByPost(c.App.Session, c.Params.PostId, model.PERMISSION_READ_CHANNEL) {
c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
}
var appErr *model.AppError
resp := &model.PostActionAPIResponse{Status: "OK"}
if resp.TriggerId, err = c.App.DoPostAction(c.Params.PostId, c.Params.ActionId, c.App.Session.UserId, actionRequest.SelectedOption); err != nil {
c.Err = err
resp.TriggerId, appErr = c.App.DoPostActionWithCookie(c.Params.PostId, c.Params.ActionId, c.App.Session.UserId,
actionRequest.SelectedOption, cookie)
if appErr != nil {
c.Err = appErr
return
}
b, _ := json.Marshal(resp)
w.Write(b)
}

Просмотреть файл

@@ -4,7 +4,9 @@
package api4
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
@@ -15,6 +17,82 @@ import (
"github.com/stretchr/testify/require"
)
type testHandler struct {
t *testing.T
}
func (th *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
bb, err := ioutil.ReadAll(r.Body)
assert.Nil(th.t, err)
assert.NotEmpty(th.t, string(bb))
poir := model.PostActionIntegrationRequestFromJson(bytes.NewReader(bb))
assert.NotEmpty(th.t, poir.UserId)
assert.NotEmpty(th.t, poir.ChannelId)
assert.Empty(th.t, poir.TeamId)
assert.NotEmpty(th.t, poir.PostId)
assert.NotEmpty(th.t, poir.TriggerId)
assert.Equal(th.t, "button", poir.Type)
assert.Equal(th.t, "test-value", poir.Context["test-key"])
w.Write([]byte("{}"))
w.WriteHeader(200)
}
func TestPostActionCookies(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
Client := th.Client
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1"
})
handler := &testHandler{t}
server := httptest.NewServer(handler)
action := model.PostAction{
Id: model.NewId(),
Name: "Test-action",
Type: model.POST_ACTION_TYPE_BUTTON,
Integration: &model.PostActionIntegration{
URL: server.URL,
Context: map[string]interface{}{
"test-key": "test-value",
},
},
}
post := &model.Post{
Id: model.NewId(),
Type: model.POST_EPHEMERAL,
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
Props: map[string]interface{}{
"attachments": []*model.SlackAttachment{
{
Title: "some-title",
TitleLink: "https://some-url.com",
Text: "some-text",
ImageURL: "https://some-other-url.com",
Actions: []*model.PostAction{&action},
},
},
},
}
post.GenerateActionIds()
assert.Equal(t, 32, len(th.App.PostActionCookieSecret()))
post = model.AddPostActionCookies(post, th.App.PostActionCookieSecret())
ok, resp := Client.DoPostActionWithCookie(post.Id, action.Id, "", action.Cookie)
assert.True(t, ok)
assert.NotNil(t, resp)
assert.Equal(t, 200, resp.StatusCode)
assert.Nil(t, resp.Error)
assert.NotNil(t, resp.RequestId)
assert.NotNil(t, resp.ServerVersion)
}
func TestOpenDialog(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()

Просмотреть файл

@@ -97,7 +97,9 @@ func createEphemeralPost(c *Context, w http.ResponseWriter, r *http.Request) {
rp := c.App.SendEphemeralPost(ephRequest.UserID, c.App.PostWithProxyRemovedFromImageURLs(ephRequest.Post))
w.WriteHeader(http.StatusCreated)
w.Write([]byte(c.App.PreparePostForClient(rp, true).ToJson()))
rp = model.AddPostActionCookies(rp, c.App.PostActionCookieSecret())
rp = c.App.PreparePostForClient(rp, true)
w.Write([]byte(rp.ToJson()))
}
func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) {

Просмотреть файл

@@ -100,6 +100,64 @@ func (a *App) RemoveConfigListener(id string) {
a.Srv.RemoveConfigListener(id)
}
// ensurePostActionCookieSecret ensures that the key for encrypting PostActionCookie exists
// and future calls to PostAcrionCookieSecret will always return a valid key, same on all
// servers in the cluster
func (a *App) ensurePostActionCookieSecret() error {
if a.Srv.postActionCookieSecret != nil {
return nil
}
var secret *model.SystemPostActionCookieSecret
result := <-a.Srv.Store.System().GetByName(model.SYSTEM_POST_ACTION_COOKIE_SECRET)
if result.Err == nil {
if err := json.Unmarshal([]byte(result.Data.(*model.System).Value), &secret); err != nil {
return err
}
}
// If we don't already have a key, try to generate one.
if secret == nil {
newSecret := &model.SystemPostActionCookieSecret{
Secret: make([]byte, 32),
}
_, err := rand.Reader.Read(newSecret.Secret)
if err != nil {
return err
}
system := &model.System{
Name: model.SYSTEM_POST_ACTION_COOKIE_SECRET,
}
v, err := json.Marshal(newSecret)
if err != nil {
return err
}
system.Value = string(v)
if result = <-a.Srv.Store.System().Save(system); result.Err == nil {
// If we were able to save the key, use it, otherwise ignore the error.
secret = newSecret
}
}
// If we weren't able to save a new key above, another server must have beat us to it. Get the
// key from the database, and if that fails, error out.
if secret == nil {
result := <-a.Srv.Store.System().GetByName(model.SYSTEM_POST_ACTION_COOKIE_SECRET)
if result.Err != nil {
return result.Err
}
if err := json.Unmarshal([]byte(result.Data.(*model.System).Value), &secret); err != nil {
return err
}
}
a.Srv.postActionCookieSecret = secret.Secret
return nil
}
// EnsureAsymmetricSigningKey ensures that an asymmetric signing key exists and future calls to
// AsymmetricSigningKey will always return a valid signing key.
func (a *App) ensureAsymmetricSigningKey() error {
@@ -209,6 +267,14 @@ func (a *App) AsymmetricSigningKey() *ecdsa.PrivateKey {
return a.Srv.AsymmetricSigningKey()
}
func (s *Server) PostActionCookieSecret() []byte {
return s.postActionCookieSecret
}
func (a *App) PostActionCookieSecret() []byte {
return a.Srv.PostActionCookieSecret()
}
func (a *App) regenerateClientConfig() {
clientConfig := config.GenerateClientConfig(a.Config(), a.DiagnosticId(), a.License())
limitedClientConfig := config.GenerateLimitedClientConfig(a.Config(), a.DiagnosticId(), a.License())

Просмотреть файл

@@ -57,6 +57,12 @@ func TestAsymmetricSigningKey(t *testing.T) {
assert.NotEmpty(t, th.App.ClientConfig()["AsymmetricSigningPublicKey"])
}
func TestPostActionCookieSecret(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
assert.Equal(t, 32, len(th.App.PostActionCookieSecret()))
}
func TestClientConfigWithComputed(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()

Просмотреть файл

@@ -31,50 +31,107 @@ import (
)
func (a *App) DoPostAction(postId, actionId, userId, selectedOption string) (string, *model.AppError) {
return a.DoPostActionWithCookie(postId, actionId, userId, selectedOption, nil)
}
func (a *App) DoPostActionWithCookie(postId, actionId, userId, selectedOption string, cookie *model.PostActionCookie) (string, *model.AppError) {
// the prop values that we need to retain/clear in replacement message to match the original
remove := []string{"override_username", "override_icon_url"}
retain := map[string]interface{}{}
datasource := ""
upstreamURL := ""
rootPostId := ""
upstreamRequest := &model.PostActionIntegrationRequest{
UserId: userId,
PostId: postId,
}
// See if the post exists in the DB, if so ignore the cookie.
// Start all queries here for parallel execution
pchan := a.Srv.Store.Post().GetSingle(postId)
cchan := a.Srv.Store.Channel().GetForPost(postId)
result := <-pchan
if result.Err != nil {
return "", result.Err
}
post := result.Data.(*model.Post)
if cookie == nil {
return "", result.Err
}
if cookie.Integration == nil {
return "", model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "no Integration in action cookie", http.StatusBadRequest)
}
result = <-cchan
if result.Err != nil {
return "", result.Err
}
channel := result.Data.(*model.Channel)
if postId != cookie.PostId {
return "", model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "postId doesn't match", http.StatusBadRequest)
}
action := post.GetAction(actionId)
if action == nil || action.Integration == nil {
return "", model.NewAppError("DoPostAction", "api.post.do_action.action_id.app_error", nil, fmt.Sprintf("action=%v", action), http.StatusNotFound)
upstreamRequest.ChannelId = cookie.ChannelId
upstreamRequest.Type = cookie.Type
upstreamRequest.Context = cookie.Integration.Context
datasource = cookie.DataSource
retain = cookie.RetainProps
remove = cookie.RemoveProps
rootPostId = cookie.RootPostId
upstreamURL = cookie.Integration.URL
} else {
// Get action metadata from the database
post := result.Data.(*model.Post)
result = <-cchan
if result.Err != nil {
return "", result.Err
}
channel := result.Data.(*model.Channel)
action := post.GetAction(actionId)
if action == nil || action.Integration == nil {
return "", model.NewAppError("DoPostAction", "api.post.do_action.action_id.app_error", nil, fmt.Sprintf("action=%v", action), http.StatusNotFound)
}
upstreamRequest.ChannelId = post.ChannelId
upstreamRequest.TeamId = channel.TeamId
upstreamRequest.Type = action.Type
upstreamRequest.Context = action.Integration.Context
datasource = action.DataSource
retainPropKeys := []string{"override_username", "override_icon_url"}
for _, key := range retainPropKeys {
value, ok := post.Props[key]
if ok {
retain[key] = value
} else {
remove = append(remove, key)
}
}
if post.RootId == "" {
rootPostId = post.Id
} else {
rootPostId = post.RootId
}
upstreamURL = action.Integration.URL
}
request := &model.PostActionIntegrationRequest{
UserId: userId,
ChannelId: post.ChannelId,
TeamId: channel.TeamId,
PostId: postId,
Type: action.Type,
Context: action.Integration.Context,
if upstreamRequest.Type == model.POST_ACTION_TYPE_SELECT {
if selectedOption != "" {
if upstreamRequest.Context == nil {
upstreamRequest.Context = map[string]interface{}{}
}
upstreamRequest.DataSource = datasource
upstreamRequest.Context["selected_option"] = selectedOption
}
}
clientTriggerId, _, err := request.GenerateTriggerId(a.AsymmetricSigningKey())
if err != nil {
return "", err
clientTriggerId, _, appErr := upstreamRequest.GenerateTriggerId(a.AsymmetricSigningKey())
if appErr != nil {
return "", appErr
}
if action.Type == model.POST_ACTION_TYPE_SELECT {
request.DataSource = action.DataSource
request.Context["selected_option"] = selectedOption
resp, appErr := a.DoActionRequest(upstreamURL, upstreamRequest.ToJson())
if appErr != nil {
return "", appErr
}
resp, err := a.DoActionRequest(action.Integration.URL, request.ToJson())
if err != nil {
return "", err
}
defer resp.Body.Close()
var response model.PostActionIntegrationResponse
@@ -82,39 +139,30 @@ func (a *App) DoPostAction(postId, actionId, userId, selectedOption string) (str
return "", model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
}
retainedProps := []string{"override_username", "override_icon_url"}
if response.Update != nil {
response.Update.Id = postId
response.Update.AddProp("from_webhook", "true")
for _, prop := range retainedProps {
if value, ok := post.Props[prop]; ok {
response.Update.Props[prop] = value
} else {
delete(response.Update.Props, prop)
}
for key, value := range retain {
response.Update.AddProp(key, value)
}
if _, err := a.UpdatePost(response.Update, false); err != nil {
return "", err
for _, key := range remove {
delete(response.Update.Props, key)
}
if _, appErr = a.UpdatePost(response.Update, false); appErr != nil {
return "", appErr
}
}
if response.EphemeralText != "" {
ephemeralPost := &model.Post{}
ephemeralPost.Message = model.ParseSlackLinksToMarkdown(response.EphemeralText)
ephemeralPost.ChannelId = post.ChannelId
ephemeralPost.RootId = post.RootId
if ephemeralPost.RootId == "" {
ephemeralPost.RootId = post.Id
ephemeralPost := &model.Post{
Message: model.ParseSlackLinksToMarkdown(response.EphemeralText),
ChannelId: upstreamRequest.ChannelId,
RootId: rootPostId,
UserId: userId,
}
ephemeralPost.UserId = post.UserId
ephemeralPost.AddProp("from_webhook", "true")
for _, prop := range retainedProps {
if value, ok := post.Props[prop]; ok {
ephemeralPost.Props[prop] = value
} else {
delete(ephemeralPost.Props, prop)
}
for key, value := range retain {
ephemeralPost.AddProp(key, value)
}
a.SendEphemeralPost(userId, ephemeralPost)
}

Просмотреть файл

@@ -415,6 +415,14 @@ func (api *PluginAPI) SendEphemeralPost(userId string, post *model.Post) *model.
return api.app.SendEphemeralPost(userId, post)
}
func (api *PluginAPI) UpdateEphemeralPost(userId string, post *model.Post) *model.Post {
return api.app.UpdateEphemeralPost(userId, post)
}
func (api *PluginAPI) DeleteEphemeralPost(userId string, post *model.Post) {
api.app.DeleteEphemeralPost(userId, post)
}
func (api *PluginAPI) DeletePost(postId string) *model.AppError {
_, err := api.app.DeletePost(postId, api.id)
return err

Просмотреть файл

@@ -404,8 +404,41 @@ func (a *App) SendEphemeralPost(userId string, post *model.Post) *model.Post {
post.Props = model.StringInterface{}
}
post.GenerateActionIds()
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil)
message.Add("post", a.PreparePostForClient(post, true).ToJson())
post = a.PreparePostForClient(post, true)
post = model.AddPostActionCookies(post, a.PostActionCookieSecret())
message.Add("post", post.ToJson())
a.Publish(message)
return post
}
func (a *App) UpdateEphemeralPost(userId string, post *model.Post) *model.Post {
post.Type = model.POST_EPHEMERAL
post.UpdateAt = model.GetMillis()
if post.Props == nil {
post.Props = model.StringInterface{}
}
post.GenerateActionIds()
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", post.ChannelId, userId, nil)
post = a.PreparePostForClient(post, true)
post = model.AddPostActionCookies(post, a.PostActionCookieSecret())
message.Add("post", post.ToJson())
a.Publish(message)
return post
}
func (a *App) DeleteEphemeralPost(userId string, post *model.Post) *model.Post {
post.Type = model.POST_EPHEMERAL
post.DeleteAt = model.GetMillis()
post.UpdateAt = post.DeleteAt
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, userId, nil)
message.Add("post", post.ToJson())
a.Publish(message)
return post
@@ -506,7 +539,9 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model
rpost = a.PreparePostForClient(rpost, false)
a.sendUpdatedPostEvent(rpost)
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil)
message.Add("post", rpost.ToJson())
a.Publish(message)
a.InvalidateCacheForChannelPosts(rpost.ChannelId)
@@ -529,12 +564,6 @@ func (a *App) PatchPost(postId string, patch *model.PostPatch) (*model.Post, *mo
return updatedPost, nil
}
func (a *App) sendUpdatedPostEvent(post *model.Post) {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", post.ChannelId, "", nil)
message.Add("post", post.ToJson())
a.Publish(message)
}
func (a *App) GetPostsPage(channelId string, page int, perPage int) (*model.PostList, *model.AppError) {
result := <-a.Srv.Store.Post().GetPosts(channelId, page*perPage, perPage, true)
if result.Err != nil {

Просмотреть файл

@@ -93,6 +93,7 @@ type Server struct {
clusterLeaderListenerId string
configStore config.Store
asymmetricSigningKey *ecdsa.PrivateKey
postActionCookieSecret []byte
pluginCommands []*PluginCommand
pluginCommandsLock sync.RWMutex

Просмотреть файл

@@ -77,6 +77,10 @@ func (s *Server) RunOldAppInitalization() error {
return errors.Wrapf(err, "unable to ensure asymmetric signing key")
}
if err := s.FakeApp().ensurePostActionCookieSecret(); err != nil {
return errors.Wrapf(err, "unable to ensure PostAction cookie secret")
}
if err := s.FakeApp().ensureInstallationDate(); err != nil {
return errors.Wrapf(err, "unable to ensure installation date")
}

Просмотреть файл

@@ -2333,6 +2333,23 @@ func (c *Client4) DoPostAction(postId, actionId string) (bool, *Response) {
return CheckStatusOK(r), BuildResponse(r)
}
// DoPostActionWithCookie performs a post action with extra arguments
func (c *Client4) DoPostActionWithCookie(postId, actionId, selected, cookieStr string) (bool, *Response) {
var body []byte
if selected != "" || cookieStr != "" {
body, _ = json.Marshal(DoPostActionRequest{
SelectedOption: selected,
Cookie: cookieStr,
})
}
r, err := c.DoApiPost(c.GetPostRoute(postId)+"/actions/"+actionId, string(body))
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// OpenInteractiveDialog sends a WebSocket event to a user's clients to
// open interactive dialogs, based on the provided trigger ID and other
// provided data. Used with interactive message buttons, menus and

Просмотреть файл

@@ -5,11 +5,14 @@ package model
import (
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/rand"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
@@ -24,16 +27,49 @@ const (
)
type DoPostActionRequest struct {
SelectedOption string `json:"selected_option"`
SelectedOption string `json:"selected_option,omitempty"`
Cookie string `json:"cookie,omitempty"`
}
type PostAction struct {
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
DataSource string `json:"data_source"`
Options []*PostActionOptions `json:"options"`
// A unique Action ID. If not set, generated automatically.
Id string `json:"id,omitempty"`
// The type of the interactive element. Currently supported are
// "select" and "button".
Type string `json:"type,omitempty"`
// The text on the button, or in the select placeholder.
Name string `json:"name,omitempty"`
// DataSource indicates the data source for the select action. If left
// empty, the select is populated from Options. Other supported values
// are "users" and "channels".
DataSource string `json:"data_source,omitempty"`
Options []*PostActionOptions `json:"options,omitempty"`
// Defines the interaction with the backend upon a user action.
// Integration contains Context, which is private plugin data;
// Integrations are stripped from Posts when they are sent to the
// client, or are encrypted in a Cookie.
Integration *PostActionIntegration `json:"integration,omitempty"`
Cookie string `json:"cookie,omitempty" db:"-"`
}
// PostActionCookie is set by the server, serialized and encrypted into
// PostAction.Cookie. The clients should hold on to it, and include it with
// subsequent DoPostAction requests. This allows the server to access the
// action metadata even when it's not available in the database, for ephemeral
// posts.
type PostActionCookie struct {
Type string `json:"type,omitempty"`
PostId string `json:"post_id,omitempty"`
RootPostId string `json:"root_post_id,omitempty"`
ChannelId string `json:"channel_id,omitempty"`
DataSource string `json:"data_source,omitempty"`
Integration *PostActionIntegration `json:"integration,omitempty"`
RetainProps map[string]interface{} `json:"retain_props,omitempty"`
RemoveProps []string `json:"remove_props,omitempty"`
}
type PostActionOptions struct {
@@ -287,6 +323,115 @@ func (o *Post) GenerateActionIds() {
}
}
func AddPostActionCookies(o *Post, secret []byte) *Post {
p := o.Clone()
// retainedProps carry over their value from the old post, including no value
retainPropKeys := []string{"override_username", "override_icon_url"}
retainProps := map[string]interface{}{}
removeProps := []string{}
for _, key := range retainPropKeys {
value, ok := p.Props[key]
if ok {
retainProps[key] = value
} else {
removeProps = append(removeProps, key)
}
}
attachments := p.Attachments()
for _, attachment := range attachments {
for _, action := range attachment.Actions {
c := &PostActionCookie{
Type: action.Type,
ChannelId: p.ChannelId,
DataSource: action.DataSource,
Integration: action.Integration,
RetainProps: retainProps,
RemoveProps: removeProps,
}
c.PostId = p.Id
if p.RootId == "" {
c.RootPostId = p.Id
} else {
c.RootPostId = p.RootId
}
b, _ := json.Marshal(c)
action.Cookie, _ = encryptPostActionCookie(string(b), secret)
}
}
return p
}
func encryptPostActionCookie(plain string, secret []byte) (string, error) {
if len(secret) == 0 {
return plain, nil
}
block, err := aes.NewCipher(secret)
if err != nil {
return "", err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, aesgcm.NonceSize())
_, err = io.ReadFull(rand.Reader, nonce)
if err != nil {
return "", err
}
sealed := aesgcm.Seal(nil, nonce, []byte(plain), nil)
combined := append(nonce, sealed...)
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(combined)))
base64.StdEncoding.Encode(encoded, combined)
return string(encoded), nil
}
func DecryptPostActionCookie(encoded string, secret []byte) (string, error) {
if len(secret) == 0 {
return encoded, nil
}
block, err := aes.NewCipher(secret)
if err != nil {
return "", err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
decoded := make([]byte, base64.StdEncoding.DecodedLen(len(encoded)))
n, err := base64.StdEncoding.Decode(decoded, []byte(encoded))
if err != nil {
return "", err
}
decoded = decoded[:n]
nonceSize := aesgcm.NonceSize()
if len(decoded) < nonceSize {
return "", fmt.Errorf("cookie too short")
}
nonce, decoded := decoded[:nonceSize], decoded[nonceSize:]
plain, err := aesgcm.Open(nil, nonce, decoded, nil)
if err != nil {
return "", err
}
return string(plain), nil
}
func DoPostActionRequestFromJson(data io.Reader) *DoPostActionRequest {
var o *DoPostActionRequest
json.NewDecoder(data).Decode(&o)

Просмотреть файл

@@ -10,13 +10,14 @@ import (
)
const (
SYSTEM_DIAGNOSTIC_ID = "DiagnosticId"
SYSTEM_RAN_UNIT_TESTS = "RanUnitTests"
SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime"
SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId"
SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime"
SYSTEM_ASYMMETRIC_SIGNING_KEY = "AsymmetricSigningKey"
SYSTEM_INSTALLATION_DATE_KEY = "InstallationDate"
SYSTEM_DIAGNOSTIC_ID = "DiagnosticId"
SYSTEM_RAN_UNIT_TESTS = "RanUnitTests"
SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime"
SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId"
SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime"
SYSTEM_ASYMMETRIC_SIGNING_KEY = "AsymmetricSigningKey"
SYSTEM_POST_ACTION_COOKIE_SECRET = "PostActionCookieSecret"
SYSTEM_INSTALLATION_DATE_KEY = "InstallationDate"
)
type System struct {
@@ -35,6 +36,10 @@ func SystemFromJson(data io.Reader) *System {
return o
}
type SystemPostActionCookieSecret struct {
Secret []byte `json:"key,omitempty"`
}
type SystemAsymmetricSigningKey struct {
ECDSAKey *SystemECDSAKey `json:"ecdsa_key,omitempty"`
}

Просмотреть файл

@@ -287,6 +287,14 @@ type API interface {
// SendEphemeralPost creates an ephemeral post.
SendEphemeralPost(userId string, post *model.Post) *model.Post
// UpdateEphemeralPost updates an ephemeral message previously sent to the user.
// EXPERIMENTAL: This API is experimental and can be changed without advance notice.
UpdateEphemeralPost(userId string, post *model.Post) *model.Post
// DeleteEphemeralPost deletes an ephemeral message previously sent to the user.
// EXPERIMENTAL: This API is experimental and can be changed without advance notice.
DeleteEphemeralPost(userId string, post *model.Post)
// DeletePost deletes a post.
DeletePost(postId string) *model.AppError

Просмотреть файл

@@ -2502,6 +2502,63 @@ func (s *apiRPCServer) SendEphemeralPost(args *Z_SendEphemeralPostArgs, returns
return nil
}
type Z_UpdateEphemeralPostArgs struct {
A string
B *model.Post
}
type Z_UpdateEphemeralPostReturns struct {
A *model.Post
}
func (g *apiRPCClient) UpdateEphemeralPost(userId string, post *model.Post) *model.Post {
_args := &Z_UpdateEphemeralPostArgs{userId, post}
_returns := &Z_UpdateEphemeralPostReturns{}
if err := g.client.Call("Plugin.UpdateEphemeralPost", _args, _returns); err != nil {
log.Printf("RPC call to UpdateEphemeralPost API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) UpdateEphemeralPost(args *Z_UpdateEphemeralPostArgs, returns *Z_UpdateEphemeralPostReturns) error {
if hook, ok := s.impl.(interface {
UpdateEphemeralPost(userId string, post *model.Post) *model.Post
}); ok {
returns.A = hook.UpdateEphemeralPost(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API UpdateEphemeralPost called but not implemented."))
}
return nil
}
type Z_DeleteEphemeralPostArgs struct {
A string
B *model.Post
}
type Z_DeleteEphemeralPostReturns struct {
}
func (g *apiRPCClient) DeleteEphemeralPost(userId string, post *model.Post) {
_args := &Z_DeleteEphemeralPostArgs{userId, post}
_returns := &Z_DeleteEphemeralPostReturns{}
if err := g.client.Call("Plugin.DeleteEphemeralPost", _args, _returns); err != nil {
log.Printf("RPC call to DeleteEphemeralPost API failed: %s", err.Error())
}
}
func (s *apiRPCServer) DeleteEphemeralPost(args *Z_DeleteEphemeralPostArgs, returns *Z_DeleteEphemeralPostReturns) error {
if hook, ok := s.impl.(interface {
DeleteEphemeralPost(userId string, post *model.Post)
}); ok {
hook.DeleteEphemeralPost(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API DeleteEphemeralPost called but not implemented."))
}
return nil
}
type Z_DeletePostArgs struct {
A string
}

Просмотреть файл

@@ -2124,6 +2124,35 @@ func (_m *API) SendEphemeralPost(userId string, post *model.Post) *model.Post {
return r0
}
// UpdateEphemeralPost provides a mock function with given fields: userId, post
func (_m *API) UpdateEphemeralPost(userId string, post *model.Post) *model.Post {
ret := _m.Called(userId, post)
var r0 *model.Post
if rf, ok := ret.Get(0).(func(string, *model.Post) *model.Post); ok {
r0 = rf(userId, post)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
return r0
}
// DeleteEphemeralPost provides a mock function with given fields: userId, post
func (_m *API) DeleteEphemeralPost(userId string, post *model.Post) {
ret := _m.Called(userId, post)
if rf, ok := ret.Get(0).(func(string, *model.Post) *model.Post); ok {
_ = rf(userId, post)
} else {
if ret.Get(0) != nil {
_ = ret.Get(0).(*model.Post)
}
}
}
// SendMail provides a mock function with given fields: to, subject, htmlBody
func (_m *API) SendMail(to string, subject string, htmlBody string) *model.AppError {
ret := _m.Called(to, subject, htmlBody)

Просмотреть файл

@@ -28,3 +28,6 @@ func (StaticConfigService) RemoveConfigListener(string) {
func (StaticConfigService) AsymmetricSigningKey() *ecdsa.PrivateKey {
return &ecdsa.PrivateKey{}
}
func (StaticConfigService) PostActionCookieSecret() []byte {
return make([]byte, 32)
}