Files
team/server/channels/store/storetest/outgoing_oauth_connection.go
b9794d5844 Webapp - Outgoing OAuth Connections (#25507)
* added store

* make generated

* add missing license headers

* fix receiver name

* i18n

* i18n sorting

* update migrations from master

* make migrations-extract

* update retrylayer tests

* replaced sql query with id pagination

* fixed flaky tests

* missing columns

* missing columns on save/update

* typo

* improved tests

* remove enum from mysql colum

* add password credentials to store

* license changes

* OAuthOutgoingConnectionInterface

* Oauth -> OAuth

* make generated

* copied over installed_oauth_apps component and renamed things to installed_outgoing_oauth_connections

* merge migrations

* renamed migrations

* model change suggestions

* refactor test functionsn

* migration typo

* refactor store table names

* updated sanitize test

* cleanup merge

* refactor symbol

* "installed outgoing oauth connections" page works

* move things into a nested folder

* add and edit page stubs work

* list endpoint

* oauthoutgoingconnection -> outgoingoauthconnection

* signature change

* i18n update

* granttype typo

* naming

* api list

* uppercase typo

* i18n

* missing license header

* fixed path in comments

* updated openapi definitions

* changes to support selecting command request url

* sanitize connections

* make generated

* test license and no feature flag

* removed t.fatal

* updated testhelper calls

* yaml schema fixes

* switched interface name

* suggested translation

* missing i18n translation

* management permission

* moved permission initalization to proper place

* endpoints

* put tests

* error check typo

* fixed specific enttity urls

* tests

* read permission check

* updated openapi definitions

* i18n

* GetConnectionByAudience method

* notes

* replaced GetConnectionsByAudience with a filter

* added custom oauth token object

* updated interface and usage

* properly set enterprise interface

* move retrieval logic to impl

* webhook tests

* translations

* i18n: updates

* address comments

* endpoint and tests

* i18n

* api docs

* fixed endpoint path

* sq.like

* use filter object instead of parameters

* set url values if not empty

* typos

* converted some components to function components, and move around files

* correctly check token url

* restore flag to previous value

* added command oauth handler

* update enterprise imports

* migrate last component to function component

* Added enterprise import

* refactor permissions and add necessary webapp code

* Check correct flag in permission tree

* allow partial updates

* sort i18n webapp

* missing test modification

* fixed webapp i18n sorting

* allow validating stored connections

* added missing translation

* fix finished adding connection link and text on result page

* added missing permission to smoke tests

* missing role in smoke test

* updated translations

* updated translations

* support editing client secret on existing connection

* fix some i18n strings

* updated translations

* better error messages

* progress on using react select for command request url while maintaining typed in value

* remove writeheader, test

* HasValidGrantType

* end early to avoid nil pointer errors

* move slash command request url input box into its own component

* wrap components related to oauth connections in config check

* fix tests

* i18n-extract

* change some i18n strings to say "Outgoing OAuth 2.0 Connections"

* remove debug code

* fixed i18n

* updated i18n file

* feature configuration backend

* typo

* add system console setting

* Revert "typo"

This reverts commit 669da23e8ee47525ccaa6f59cbbd20bf8a121191.

* Revert "updated i18n file"

This reverts commit d0882c0dd7587533f0d0f7a7b7b190684186158a.

* Revert "fixed i18n"

This reverts commit 3108866bc19139182dfd094921c56cdefc4695ea.

* fixed i18n

* updated i18n file

* typo

* updated i18n

* updated i18n

* updated i18n

* updated version to 9.6

* replace feature flag with system console configuration

* i18n

* updated tests

* pr feedback

* fix styling of disabled text box

* fix styling of action links in integration console

* server changes for validation feature

* webapp changes for validation feature

* pencil icon styling

* styling fixes for oauth audience correct configuration message

* fix sanitize test

* remove max lengths from outgoing oauth connection form

* use config var in webapp instead of feature flag

* change asterisks to bullets

* update api docs for validate endpoint

* feedback from ux review

* fix lint, types, tests

* fix stylelint

* implement validation button under the token url input

* support wildcard for matching audience urls

* updates for styling

* update snapshots

* add doc links for the outgoing oauth connections feature

* change doc links to use permalink

* add docs link to system console

* fix: use limitedreader in json decoding

* fix: form error in validation

* management permission can read now

* updated api documentation

* doc typo

* require one permission to read only

* fix api connection list audience filter

* fix audience matching and add loading indicator

* fix team permissions on outgoing oauth connection api calls

* fix api doc and test, for adding team id to query params

* handle read permissions by adding a team in the payload

* missing teamid query parameter in test

* change validate button logic to not require audience urls to be filled out

* fix redux type

---------

Co-authored-by: Felipe Martin <[email protected]>
2024-02-09 14:49:49 -05:00

412 lines
14 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"sort"
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/stretchr/testify/require"
)
func newValidOutgoingOAuthConnection() *model.OutgoingOAuthConnection {
return &model.OutgoingOAuthConnection{
CreatorId: model.NewId(),
Name: "Test Connection",
ClientId: model.NewId(),
ClientSecret: model.NewId(),
OAuthTokenURL: "https://nowhere.com/oauth/token",
GrantType: model.OutgoingOAuthConnectionGrantTypeClientCredentials,
Audiences: []string{"https://nowhere.com"},
}
}
func cleanupOutgoingOAuthConnections(t *testing.T, ss store.Store) func() {
return func() {
// Delete all outgoing connections
connections, err := ss.OutgoingOAuthConnection().GetConnections(request.TestContext(t), model.OutgoingOAuthConnectionGetConnectionsFilter{
Limit: 100,
})
require.NoError(t, err)
for _, conn := range connections {
err := ss.OutgoingOAuthConnection().DeleteConnection(request.TestContext(t), conn.Id)
require.NoError(t, err)
}
}
}
func TestOutgoingOAuthConnectionStore(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("SaveConnection", func(t *testing.T) {
t.Cleanup(cleanupOutgoingOAuthConnections(t, ss))
testSaveOutgoingOAuthConnection(t, ss)
})
t.Run("UpdateConnection", func(t *testing.T) {
t.Cleanup(cleanupOutgoingOAuthConnections(t, ss))
testUpdateOutgoingOAuthConnection(t, ss)
})
t.Run("GetConnection", func(t *testing.T) {
t.Cleanup(cleanupOutgoingOAuthConnections(t, ss))
testGetOutgoingOAuthConnection(t, ss)
})
t.Run("GetConnectionsByAudience", func(t *testing.T) {
t.Cleanup(cleanupOutgoingOAuthConnections(t, ss))
testGetOutgoingOAuthConnectionByAudience(t, ss)
})
t.Run("GetConnections", func(t *testing.T) {
t.Cleanup(cleanupOutgoingOAuthConnections(t, ss))
testGetOutgoingOAuthConnections(t, ss)
})
t.Run("DeleteConnection", func(t *testing.T) {
t.Cleanup(cleanupOutgoingOAuthConnections(t, ss))
testDeleteOutgoingOAuthConnection(t, ss)
})
}
func testSaveOutgoingOAuthConnection(t *testing.T, ss store.Store) {
c := request.TestContext(t)
t.Run("save/get", func(t *testing.T) {
// Define test data
connection := newValidOutgoingOAuthConnection()
// Save the connection
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
// Retrieve the connection
storeConn, err := ss.OutgoingOAuthConnection().GetConnection(c, connection.Id)
require.NoError(t, err)
require.Equal(t, connection, storeConn)
})
t.Run("save without id should fail", func(t *testing.T) {
connection := &model.OutgoingOAuthConnection{
Id: model.NewId(),
}
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.Error(t, err)
})
t.Run("save with incorrect grant type should fail", func(t *testing.T) {
connection := newValidOutgoingOAuthConnection()
connection.GrantType = "incorrect"
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.Error(t, err)
})
}
func testUpdateOutgoingOAuthConnection(t *testing.T, ss store.Store) {
c := request.TestContext(t)
t.Run("update/get", func(t *testing.T) {
// Define test data
connection := newValidOutgoingOAuthConnection()
// Save the connection
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
// Update the connection
connection.Name = "Updated Name"
_, err = ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.NoError(t, err)
// Retrieve the connection
storeConn, err := ss.OutgoingOAuthConnection().GetConnection(c, connection.Id)
require.NoError(t, err)
require.Equal(t, connection, storeConn)
})
t.Run("update non-existing", func(t *testing.T) {
connection := newValidOutgoingOAuthConnection()
connection.Id = model.NewId()
_, err := ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.Error(t, err)
})
t.Run("update without id should fail", func(t *testing.T) {
connection := &model.OutgoingOAuthConnection{
Id: model.NewId(),
}
_, err := ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.Error(t, err)
})
t.Run("update should update all fields", func(t *testing.T) {
// Define test data
connection := newValidOutgoingOAuthConnection()
// Save the connection
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
// Update the connection
connection.Name = "Updated Name"
connection.ClientId = "Updated ClientId"
connection.ClientSecret = "Updated ClientSecret"
connection.OAuthTokenURL = "https://nowhere.com/updated"
// connection.GrantType = "client_credentials" // ignoring since we only allow one for now
connection.Audiences = []string{"https://nowhere.com/updated"}
_, err = ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.NoError(t, err)
// Retrieve the connection
storeConn, err := ss.OutgoingOAuthConnection().GetConnection(c, connection.Id)
require.NoError(t, err)
require.Equal(t, connection, storeConn)
})
t.Run("patch", func(t *testing.T) {
t.Run("name", func(t *testing.T) {
connection := newValidOutgoingOAuthConnection()
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
connection.Name = "Updated Name"
updated, err := ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.NoError(t, err)
require.Equal(t, connection, updated)
})
t.Run("client id", func(t *testing.T) {
connection := newValidOutgoingOAuthConnection()
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
connection.ClientId = "Updated ClientId"
updated, err := ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.NoError(t, err)
require.Equal(t, connection, updated)
})
t.Run("client secret", func(t *testing.T) {
connection := newValidOutgoingOAuthConnection()
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
connection.ClientSecret = "Updated ClientSecret"
updated, err := ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.NoError(t, err)
require.Equal(t, connection, updated)
})
t.Run("oauth token url", func(t *testing.T) {
connection := newValidOutgoingOAuthConnection()
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
connection.OAuthTokenURL = "https://nowhere.com/updated"
updated, err := ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.NoError(t, err)
require.Equal(t, connection, updated)
})
t.Run("grant type", func(t *testing.T) {
connection := newValidOutgoingOAuthConnection()
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
connection.GrantType = model.OutgoingOAuthConnectionGrantTypeClientCredentials
updated, err := ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.NoError(t, err)
require.Equal(t, connection, updated)
})
t.Run("audiences", func(t *testing.T) {
connection := newValidOutgoingOAuthConnection()
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
connection.Audiences = model.StringArray{"https://nowhere.com/updated"}
updated, err := ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.NoError(t, err)
require.Equal(t, connection, updated)
})
t.Run("credentials username", func(t *testing.T) {
connection := newValidOutgoingOAuthConnection()
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
username := "updated username"
connection.CredentialsUsername = &username
updated, err := ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.NoError(t, err)
require.Equal(t, connection, updated)
})
t.Run("credentials password", func(t *testing.T) {
connection := newValidOutgoingOAuthConnection()
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
password := "updated password"
connection.CredentialsPassword = &password
updated, err := ss.OutgoingOAuthConnection().UpdateConnection(c, connection)
require.NoError(t, err)
require.Equal(t, connection, updated)
})
})
}
func testGetOutgoingOAuthConnection(t *testing.T, ss store.Store) {
c := request.TestContext(t)
t.Run("get non-existing", func(t *testing.T) {
nonExistingId := model.NewId()
var expected *store.ErrNotFound
_, err := ss.OutgoingOAuthConnection().GetConnection(c, nonExistingId)
require.ErrorAs(t, err, &expected)
})
}
func runAudienceTests(t *testing.T, ss store.Store, connection *model.OutgoingOAuthConnection) {
c := request.TestContext(t)
t.Run("find by host only", func(t *testing.T) {
conn, err := ss.OutgoingOAuthConnection().GetConnections(c, model.OutgoingOAuthConnectionGetConnectionsFilter{Audience: "knowhere.com"})
require.NoError(t, err)
require.Len(t, conn, 1)
require.Equal(t, []*model.OutgoingOAuthConnection{connection}, conn)
})
t.Run("find by host and path", func(t *testing.T) {
conn, err := ss.OutgoingOAuthConnection().GetConnections(c, model.OutgoingOAuthConnectionGetConnectionsFilter{Audience: "knowhere.com/audience"})
require.NoError(t, err)
require.Len(t, conn, 1)
require.Equal(t, []*model.OutgoingOAuthConnection{connection}, conn)
})
t.Run("find by full url", func(t *testing.T) {
conn, err := ss.OutgoingOAuthConnection().GetConnections(c, model.OutgoingOAuthConnectionGetConnectionsFilter{Audience: "https://knowhere.com/audience"})
require.NoError(t, err)
require.Len(t, conn, 1)
require.Equal(t, []*model.OutgoingOAuthConnection{connection}, conn)
})
t.Run("non-existent", func(t *testing.T) {
conn, err := ss.OutgoingOAuthConnection().GetConnections(c, model.OutgoingOAuthConnectionGetConnectionsFilter{Audience: "https://mattermost.com"})
require.NoError(t, err)
require.Empty(t, conn)
})
}
func testGetOutgoingOAuthConnectionByAudience(t *testing.T, ss store.Store) {
t.Run("get non-existing", func(t *testing.T) {
c := request.TestContext(t)
nonExistingId := model.NewId()
var expected *store.ErrNotFound
_, err := ss.OutgoingOAuthConnection().GetConnection(c, nonExistingId)
require.ErrorAs(t, err, &expected)
})
t.Run("get existing (single audience)", func(t *testing.T) {
t.Cleanup(cleanupOutgoingOAuthConnections(t, ss))
c := request.TestContext(t)
connection := newValidOutgoingOAuthConnection()
connection.Audiences = []string{"https://knowhere.com/audience"}
var err error
connection, err = ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
runAudienceTests(t, ss, connection)
})
t.Run("get existing (multiple audiences)", func(t *testing.T) {
t.Cleanup(cleanupOutgoingOAuthConnections(t, ss))
c := request.TestContext(t)
connection := newValidOutgoingOAuthConnection()
connection.Audiences = []string{"https://knowhere.com/audience", "https://example.com"}
var err error
connection, err = ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
runAudienceTests(t, ss, connection)
})
}
func testGetOutgoingOAuthConnections(t *testing.T, ss store.Store) {
c := request.TestContext(t)
// Define test data
connection1 := newValidOutgoingOAuthConnection()
connection2 := newValidOutgoingOAuthConnection()
connection3 := newValidOutgoingOAuthConnection()
// Save the connections
connection1, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection1)
require.NoError(t, err)
connection2, err = ss.OutgoingOAuthConnection().SaveConnection(c, connection2)
require.NoError(t, err)
connection3, err = ss.OutgoingOAuthConnection().SaveConnection(c, connection3)
require.NoError(t, err)
connections := []*model.OutgoingOAuthConnection{connection1, connection2, connection3}
sort.Slice(connections, func(i, j int) bool {
return connections[i].Id < connections[j].Id
})
t.Run("get all", func(t *testing.T) {
// Retrieve the connections
conns, err := ss.OutgoingOAuthConnection().GetConnections(c, model.OutgoingOAuthConnectionGetConnectionsFilter{Limit: 3})
require.NoError(t, err)
require.Len(t, conns, 3)
})
t.Run("get connections using pagination", func(t *testing.T) {
// Retrieve the first page
conns, err := ss.OutgoingOAuthConnection().GetConnections(c, model.OutgoingOAuthConnectionGetConnectionsFilter{Limit: 1})
require.NoError(t, err)
require.Len(t, conns, 1)
require.Equal(t, connections[0].Id, conns[0].Id, "should return the first connection")
// Retrieve the second page
conns, err = ss.OutgoingOAuthConnection().GetConnections(c, model.OutgoingOAuthConnectionGetConnectionsFilter{OffsetId: connections[0].Id})
require.NoError(t, err)
require.Len(t, conns, 2)
require.Equal(t, connections[1].Id, conns[0].Id, "should return the second connection")
require.Equal(t, connections[2].Id, conns[1].Id, "should return the third connection")
})
}
func testDeleteOutgoingOAuthConnection(t *testing.T, ss store.Store) {
c := request.TestContext(t)
t.Run("delete", func(t *testing.T) {
// Define test data
connection := newValidOutgoingOAuthConnection()
// Save the connection
_, err := ss.OutgoingOAuthConnection().SaveConnection(c, connection)
require.NoError(t, err)
// Delete the connection
err = ss.OutgoingOAuthConnection().DeleteConnection(c, connection.Id)
require.NoError(t, err)
// Retrieve the connection
_, err = ss.OutgoingOAuthConnection().GetConnection(c, connection.Id)
var expected *store.ErrNotFound
require.ErrorAs(t, err, &expected)
})
}