Skip to content

Commit

Permalink
Release 1.6.1 (#275)
Browse files Browse the repository at this point in the history
* Cross-plugin task: Enable the CircleCI "test" job in each plugin repo that has a webapp plugin (#237)

Co-authored-by: maisnamrajusingh <raju.s@demansol.com>

* [MM-223] Fixing Post Permissions Access (#233)

* For non-cloud plugin settings, set hosting property to `on-prem` (#269)

* set hosting to on-prem for `Enable OAuth`, `APIKey`, and `APISecret` plugin settings

* use actual server commit

* force oauth to be used when using cloud version of the plugin

* bump go mod version

* test circleci config

* remove references to plugin-ci

* bump Go version in circleci

* use cimg

* add other cimg changes

* go mod tidy

* revert circleci changes

* revert build/go.mod changes so we are using the old server version

* extend manifest struct to support hosting field

* Revert "extend manifest struct to support hosting field"

This reverts commit b862b40.

* revert all go mod changes

* remove manifest field name restriction

* add comment explaining commented out code

* reorder conditions for readability

* Fix OAuth token refresh (#253)

* avoid refreshing token if an error occurs while fetching current token

* pass in firstConnect bool

* lint

* explicitly assign firstConnect boolean to local variable

* ensure oauth token is decrypted when fetched from the kv store

* rename functions

* wrap account level token refresh in firstConnect block as well

* Cherrypick "Correct docs and cleanup screenshots"

* Revert "Cherrypick "Correct docs and cleanup screenshots""

This reverts commit 7164075.

* preserve access token when saving encrypted version

* use refresh token for check

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
Co-authored-by: Daniel Espino García <larkox@gmail.com>

* bump to 1.6.1

* add GetLicense call in test

* check nil license features (#277)

Co-authored-by: sibasankarnayak <86650698+sibasankarnayak@users.noreply.github.com>
Co-authored-by: maisnamrajusingh <raju.s@demansol.com>
Co-authored-by: Jupri Abel <88376482+vicky-demansol@users.noreply.github.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
Co-authored-by: Daniel Espino García <larkox@gmail.com>
  • Loading branch information
7 people committed Aug 16, 2022
1 parent 1c3516f commit 9d5a119
Show file tree
Hide file tree
Showing 14 changed files with 166 additions and 60 deletions.
10 changes: 10 additions & 0 deletions .circleci/config.yml
Expand Up @@ -23,14 +23,22 @@ workflows:
filters:
tags:
only: /^v.*/
- plugin-ci/test:
filters:
tags:
only: /^v.*/
- plugin-ci/coverage:
filters:
tags:
only: /^v.*/
requires:
- plugin-ci/test
- plugin-ci/build:
filters:
tags:
only: /^v.*/
requires:
- plugin-ci/test
- plugin-ci/deploy-ci:
filters:
branches:
Expand All @@ -40,6 +48,7 @@ workflows:
- plugin-ci/lint
- plugin-ci/coverage
- plugin-ci/build
- plugin-ci/test
- plugin-ci/deploy-release-github:
filters:
tags:
Expand All @@ -51,3 +60,4 @@ workflows:
- plugin-ci/lint
- plugin-ci/coverage
- plugin-ci/build
- plugin-ci/test
2 changes: 1 addition & 1 deletion build/manifest/main.go
Expand Up @@ -82,7 +82,7 @@ func findManifest() (*model.Manifest, error) {
// we don't want to accidentally clobber anything we won't preserve.
var manifest model.Manifest
decoder := json.NewDecoder(manifestFile)
decoder.DisallowUnknownFields()
// decoder.DisallowUnknownFields() // Commented out until circleci image is updated: https://github.com/mattermost/mattermost-plugin-zoom/pull/269#discussion_r927075701
if err = decoder.Decode(&manifest); err != nil {
return nil, errors.Wrap(err, "failed to parse manifest")
}
Expand Down
13 changes: 8 additions & 5 deletions plugin.json
Expand Up @@ -4,9 +4,9 @@
"description": "Zoom audio and video conferencing plugin for Mattermost 5.2+.",
"homepage_url": "https://github.com/mattermost/mattermost-plugin-zoom",
"support_url": "https://github.com/mattermost/mattermost-plugin-zoom/issues",
"release_notes_url": "https://github.com/mattermost/mattermost-plugin-zoom/releases/tag/v1.6.0",
"release_notes_url": "https://github.com/mattermost/mattermost-plugin-zoom/releases/tag/v1.6.1",
"icon_path": "assets/profile.svg",
"version": "1.6.0",
"version": "1.6.1",
"min_server_version": "5.12.0",
"server": {
"executables": {
Expand Down Expand Up @@ -45,7 +45,8 @@
"type": "bool",
"help_text": "When true, OAuth will be used as the authentication means with Zoom. \n When false, JWT will be used as the authentication means with Zoom. \n If you're currently using a JWT Zoom application and switch to OAuth, all users will need to connect their Zoom account using OAuth the next time they try to start a meeting. [More information](https://mattermost.gitbook.io/plugin-zoom/installation/zoom-configuration).",
"placeholder": "",
"default": false
"default": true,
"hosting": "on-prem"
},
{
"key": "AccountLevelApp",
Expand Down Expand Up @@ -86,15 +87,17 @@
"type": "text",
"help_text": "The API key generated by Zoom, used to create meetings and pull user data.",
"placeholder": "",
"default": null
"default": null,
"hosting": "on-prem"
},
{
"key": "APISecret",
"display_name": "API Secret:",
"type": "text",
"help_text": "The API secret generated by Zoom for your API key.",
"placeholder": "",
"default": null
"default": null,
"hosting": "on-prem"
},
{
"key": "WebhookSecret",
Expand Down
6 changes: 3 additions & 3 deletions server/command.go
Expand Up @@ -80,7 +80,7 @@ func (p *Plugin) executeCommand(c *plugin.Context, args *model.CommandArgs) (str
}

func (p *Plugin) canConnect(user *model.User) bool {
return p.configuration.EnableOAuth && // we are not on JWT
return p.OAuthEnabled() && // we are not on JWT
(!p.configuration.AccountLevelApp || // we are on user managed app
user.IsSystemAdmin()) // admins can connect Account level apps
}
Expand Down Expand Up @@ -205,7 +205,7 @@ func (p *Plugin) runHelpCommand(user *model.User) (string, error) {
// getAutocompleteData retrieves auto-complete data for the "/zoom" command
func (p *Plugin) getAutocompleteData() *model.AutocompleteData {
available := "start, help"
if p.configuration.EnableOAuth && !p.configuration.AccountLevelApp {
if p.OAuthEnabled() && !p.configuration.AccountLevelApp {
available = "start, connect, disconnect, help"
}
zoom := model.NewAutocompleteData("zoom", "[command]", fmt.Sprintf("Available commands: %s", available))
Expand All @@ -214,7 +214,7 @@ func (p *Plugin) getAutocompleteData() *model.AutocompleteData {
zoom.AddCommand(start)

// no point in showing the 'disconnect' option if OAuth is not enabled
if p.configuration.EnableOAuth && !p.configuration.AccountLevelApp {
if p.OAuthEnabled() && !p.configuration.AccountLevelApp {
connect := model.NewAutocompleteData("connect", "", "Connect to Zoom")
disconnect := model.NewAutocompleteData("disconnect", "", "Disonnects from Zoom")
zoom.AddCommand(connect)
Expand Down
8 changes: 3 additions & 5 deletions server/configuration.go
Expand Up @@ -51,17 +51,17 @@ func (c *configuration) Clone() *configuration {
}

// IsValid checks if all needed fields are set.
func (c *configuration) IsValid() error {
func (c *configuration) IsValid(isCloud bool) error {
switch {
case !c.EnableOAuth:
case !c.EnableOAuth && !isCloud: // JWT for on-prem
switch {
case len(c.APIKey) == 0:
return errors.New("please configure APIKey")

case len(c.APISecret) == 0:
return errors.New("please configure APISecret")
}
case c.EnableOAuth:
case c.EnableOAuth || isCloud: // OAuth for either platform
switch {
case len(c.OAuthClientSecret) == 0:
return errors.New("please configure OAuthClientSecret")
Expand All @@ -72,8 +72,6 @@ func (c *configuration) IsValid() error {
case len(c.EncryptionKey) == 0:
return errors.New("please generate EncryptionKey from Zoom plugin settings")
}
default:
return errors.New("please select either OAuth or Password based authentication")
}

if len(c.WebhookSecret) == 0 {
Expand Down
9 changes: 7 additions & 2 deletions server/http.go
Expand Up @@ -37,7 +37,7 @@ type startMeetingRequest struct {

func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
config := p.getConfiguration()
if err := config.IsValid(); err != nil {
if err := config.IsValid(p.isCloudLicense()); err != nil {
http.Error(w, "This plugin is not configured.", http.StatusNotImplemented)
return
}
Expand Down Expand Up @@ -135,7 +135,8 @@ func (p *Plugin) completeUserOAuthToZoom(w http.ResponseWriter, r *http.Request)
return
}

zoomUser, authErr := client.GetUser(user)
firstConnect := true
zoomUser, authErr := client.GetUser(user, firstConnect)
if authErr != nil {
if p.configuration.AccountLevelApp && !justConnect {
http.Error(w, "Connection completed but there was an error creating the meeting. "+authErr.Message, http.StatusInternalServerError)
Expand Down Expand Up @@ -303,6 +304,10 @@ func (p *Plugin) postMeeting(creator *model.User, meetingID int, channelID strin
topic = defaultMeetingTopic
}

if !p.API.HasPermissionToChannel(creator.Id, channelID, model.PERMISSION_CREATE_POST) {
return errors.New("this channel is not accessible, you might not have permissions to write in this channel. Contact the administrator of this channel to find out if you have access permissions")
}

slackAttachment := model.SlackAttachment{
Fallback: fmt.Sprintf("Video Meeting started at [%d](%s).\n\n[Join Meeting](%s)", meetingID, meetingURL, meetingURL),
Title: topic,
Expand Down
2 changes: 1 addition & 1 deletion server/manifest.go
Expand Up @@ -7,5 +7,5 @@ var manifest = struct {
Version string
}{
ID: "zoom",
Version: "1.6.0",
Version: "1.6.1",
}
51 changes: 41 additions & 10 deletions server/plugin.go
Expand Up @@ -53,13 +53,13 @@ type Plugin struct {
// Client defines a common interface for the API and OAuth Zoom clients
type Client interface {
GetMeeting(meetingID int) (*zoom.Meeting, error)
GetUser(user *model.User) (*zoom.User, *zoom.AuthError)
GetUser(user *model.User, firstConnect bool) (*zoom.User, *zoom.AuthError)
}

// OnActivate checks if the configurations is valid and ensures the bot account exists
func (p *Plugin) OnActivate() error {
config := p.getConfiguration()
if err := config.IsValid(); err != nil {
if err := config.IsValid(p.isCloudLicense()); err != nil {
return err
}

Expand Down Expand Up @@ -138,7 +138,7 @@ func (p *Plugin) getActiveClient(user *model.User) (Client, string, error) {
config := p.getConfiguration()

// JWT
if !config.EnableOAuth {
if !p.OAuthEnabled() {
return p.jwtClient, "", nil
}

Expand All @@ -165,12 +165,6 @@ func (p *Plugin) getActiveClient(user *model.User) (Client, string, error) {
return nil, message, errors.Wrap(err, "could not fetch Zoom OAuth info")
}

plainToken, err := decrypt([]byte(config.EncryptionKey), info.OAuthToken.AccessToken)
if err != nil {
return nil, message, errors.New("could not decrypt OAuth access token")
}

info.OAuthToken.AccessToken = plainToken
conf := p.getOAuthConfig()
return zoom.NewOAuthClient(info.OAuthToken, conf, p.siteURL, p.getZoomAPIURL(), false, p), "", nil
}
Expand Down Expand Up @@ -201,7 +195,8 @@ func (p *Plugin) authenticateAndFetchZoomUser(user *model.User) (*zoom.User, *zo
}
}

return zoomClient.GetUser(user)
firstConnect := false
return zoomClient.GetUser(user, firstConnect)
}

func (p *Plugin) sendDirectMessage(userID string, message string) error {
Expand Down Expand Up @@ -240,3 +235,39 @@ func (p *Plugin) SetZoomSuperUserToken(token *oauth2.Token) error {
}
return nil
}

func (p *Plugin) GetZoomOAuthUserInfo(userID string) (*zoom.OAuthUserInfo, error) {
info, err := p.fetchOAuthUserInfo(zoomUserByMMID, userID)
if err != nil {
return nil, errors.Wrap(err, "could not get token")
}
if info == nil {
return nil, errors.New("zoom app not connected")
}

return info, nil
}

func (p *Plugin) UpdateZoomOAuthUserInfo(userID string, info *zoom.OAuthUserInfo) error {
if err := p.storeOAuthUserInfo(info); err != nil {
msg := "unable to update user token"
p.API.LogWarn(msg, "error", err.Error())
return errors.Wrap(err, msg)
}

return nil
}

func (p *Plugin) isCloudLicense() bool {
license := p.API.GetLicense()
return license != nil && license.Features != nil && license.Features.Cloud != nil && *license.Features.Cloud
}

func (p *Plugin) OAuthEnabled() bool {
config := p.getConfiguration()
if config.EnableOAuth {
return true
}

return p.isCloudLicense()
}
41 changes: 29 additions & 12 deletions server/plugin_test.go
Expand Up @@ -53,29 +53,43 @@ func TestPlugin(t *testing.T) {

noSecretWebhookRequest := httptest.NewRequest("POST", "/webhook", strings.NewReader(endedPayload))

unauthorizedUserRequest := httptest.NewRequest("POST", "/api/v1/meetings", strings.NewReader("{\"channel_id\": \"thechannelid\", \"personal\": true}"))
unauthorizedUserRequest.Header.Add("Mattermost-User-Id", "theuserid")

for name, tc := range map[string]struct {
Request *http.Request
ExpectedStatusCode int
Request *http.Request
ExpectedStatusCode int
HasPermissionToChannel bool
}{
"UnauthorizedMeetingRequest": {
Request: noAuthMeetingRequest,
ExpectedStatusCode: http.StatusUnauthorized,
Request: noAuthMeetingRequest,
ExpectedStatusCode: http.StatusUnauthorized,
HasPermissionToChannel: true,
},
"ValidPersonalMeetingRequest": {
Request: personalMeetingRequest,
ExpectedStatusCode: http.StatusOK,
Request: personalMeetingRequest,
ExpectedStatusCode: http.StatusOK,
HasPermissionToChannel: true,
},
"ValidStoppedWebhookRequest": {
Request: validStoppedWebhookRequest,
ExpectedStatusCode: http.StatusOK,
Request: validStoppedWebhookRequest,
ExpectedStatusCode: http.StatusOK,
HasPermissionToChannel: true,
},
"ValidStartedWebhookRequest": {
Request: validStartedWebhookRequest,
ExpectedStatusCode: http.StatusNotImplemented,
Request: validStartedWebhookRequest,
ExpectedStatusCode: http.StatusNotImplemented,
HasPermissionToChannel: true,
},
"NoSecretWebhookRequest": {
Request: noSecretWebhookRequest,
ExpectedStatusCode: http.StatusUnauthorized,
Request: noSecretWebhookRequest,
ExpectedStatusCode: http.StatusUnauthorized,
HasPermissionToChannel: true,
},
"UnauthorizedChannelPermissions": {
Request: unauthorizedUserRequest,
ExpectedStatusCode: http.StatusInternalServerError,
HasPermissionToChannel: false,
},
} {
t.Run(name, func(t *testing.T) {
Expand All @@ -88,12 +102,15 @@ func TestPlugin(t *testing.T) {
Email: "theuseremail",
}, nil)

api.On("HasPermissionToChannel", "theuserid", "thechannelid", model.PERMISSION_CREATE_POST).Return(tc.HasPermissionToChannel)

api.On("GetChannelMember", "thechannelid", "theuserid").Return(&model.ChannelMember{}, nil)

api.On("GetPost", "thepostid").Return(&model.Post{Props: map[string]interface{}{}}, nil)
api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(&model.Post{}, nil)
api.On("UpdatePost", mock.AnythingOfType("*model.Post")).Return(&model.Post{}, nil)
api.On("GetPostsSince", "thechannelid", mock.AnythingOfType("int64")).Return(&model.PostList{}, nil)
api.On("GetLicense").Return(&model.License{Features: &model.Features{Cloud: model.NewBool(false)}})

api.On("KVSetWithExpiry", fmt.Sprintf("%v%v", postMeetingKey, 234), mock.AnythingOfType("[]uint8"), mock.AnythingOfType("int64")).Return(nil)
api.On("KVSetWithExpiry", fmt.Sprintf("%v%v", postMeetingKey, 123), mock.AnythingOfType("[]uint8"), mock.AnythingOfType("int64")).Return(nil)
Expand Down
12 changes: 12 additions & 0 deletions server/store.go
Expand Up @@ -32,6 +32,8 @@ func (p *Plugin) storeOAuthUserInfo(info *zoom.OAuthUserInfo) error {
if err != nil {
return errors.Wrap(err, "could not encrypt OAuth token")
}

original := info.OAuthToken.AccessToken
info.OAuthToken.AccessToken = encryptedToken

encoded, err := json.Marshal(info)
Expand All @@ -47,10 +49,13 @@ func (p *Plugin) storeOAuthUserInfo(info *zoom.OAuthUserInfo) error {
return err
}

info.OAuthToken.AccessToken = original
return nil
}

func (p *Plugin) fetchOAuthUserInfo(tokenKey, userID string) (*zoom.OAuthUserInfo, error) {
config := p.getConfiguration()

encoded, appErr := p.API.KVGet(tokenKey + userID)
if appErr != nil || encoded == nil {
return nil, errors.New("must connect user account to Zoom first")
Expand All @@ -61,6 +66,13 @@ func (p *Plugin) fetchOAuthUserInfo(tokenKey, userID string) (*zoom.OAuthUserInf
return nil, errors.New("could not to parse OAauth access token")
}

plainToken, err := decrypt([]byte(config.EncryptionKey), info.OAuthToken.AccessToken)
if err != nil {
return nil, errors.New("could not decrypt OAuth access token")
}

info.OAuthToken.AccessToken = plainToken

return &info, nil
}

Expand Down
4 changes: 3 additions & 1 deletion server/zoom/client.go
Expand Up @@ -26,10 +26,12 @@ var errNotFound = errors.New("not found")

type Client interface {
GetMeeting(meetingID int) (*Meeting, error)
GetUser(user *model.User) (*User, *AuthError)
GetUser(user *model.User, firstConnect bool) (*User, *AuthError)
}

type PluginAPI interface {
GetZoomSuperUserToken() (*oauth2.Token, error)
SetZoomSuperUserToken(*oauth2.Token) error
GetZoomOAuthUserInfo(userID string) (*OAuthUserInfo, error)
UpdateZoomOAuthUserInfo(userID string, info *OAuthUserInfo) error
}

0 comments on commit 9d5a119

Please sign in to comment.