[MM-55028] Added OAuthOutgoingConnection store (#25221)

* OAuthOutgoingConnection model

* 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

* renamed migrations

* model change suggestions

* refactor test functionsn

* migration typo

* refactor store table names

* updated sanitize test

* oauthoutgoingconnection -> outgoingoauthconnection

* signature change

* i18n update

* granttype typo

* uppercase typo

* lowercase store name
This commit is contained in:
Felipe Martin
2023-11-20 15:42:07 +01:00
committed by GitHub
parent 8158c0e614
commit b40366dbdf
20 changed files with 1419 additions and 15 deletions

View File

@@ -228,6 +228,8 @@ channels/db/migrations/mysql/000114_sharedchannelremotes_drop_nextsyncat_descrip
channels/db/migrations/mysql/000114_sharedchannelremotes_drop_nextsyncat_description.up.sql
channels/db/migrations/mysql/000115_user_reporting_changes.down.sql
channels/db/migrations/mysql/000115_user_reporting_changes.up.sql
channels/db/migrations/mysql/000116_create_outgoing_oauth_connections.down.sql
channels/db/migrations/mysql/000116_create_outgoing_oauth_connections.up.sql
channels/db/migrations/postgres/000001_create_teams.down.sql
channels/db/migrations/postgres/000001_create_teams.up.sql
channels/db/migrations/postgres/000002_create_team_members.down.sql
@@ -456,3 +458,5 @@ channels/db/migrations/postgres/000114_sharedchannelremotes_drop_nextsyncat_desc
channels/db/migrations/postgres/000114_sharedchannelremotes_drop_nextsyncat_description.up.sql
channels/db/migrations/postgres/000115_user_reporting_changes.down.sql
channels/db/migrations/postgres/000115_user_reporting_changes.up.sql
channels/db/migrations/postgres/000116_create_outgoing_oauth_connections.down.sql
channels/db/migrations/postgres/000116_create_outgoing_oauth_connections.up.sql

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS OutgoingOAuthConnections;

View File

@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS OutgoingOAuthConnections (
Id varchar(26),
Name varchar(64),
CreatorId varchar(26) DEFAULT NULL,
CreateAt bigint(20) DEFAULT NULL,
UpdateAt bigint(20) DEFAULT NULL,
ClientId varchar(255),
ClientSecret varchar(255),
CredentialsUsername varchar(255),
CredentialsPassword varchar(255),
OAuthTokenURL text,
GrantType varchar(32) DEFAULT 'client_credentials',
Audiences TEXT,
PRIMARY KEY (Id),
KEY idx_OutgoingOAuthConnections_name (Name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS outgoingoauthconnections;
DROP TYPE IF EXISTS outgoingoauthconnections_granttype;

View File

@@ -0,0 +1,18 @@
CREATE TYPE outgoingoauthconnections_granttype AS ENUM ('client_credentials', 'password');
CREATE TABLE IF NOT EXISTS outgoingoauthconnections (
id varchar(26) PRIMARY KEY,
name varchar(64),
creatorid VARCHAR(26),
createat bigint,
updateat bigint,
clientid varchar(255),
clientsecret varchar(255),
credentialsusername varchar(255),
credentialspassword varchar(255),
oauthtokenurl text,
granttype outgoingoauthconnections_granttype DEFAULT 'client_credentials',
audiences VARCHAR(1024)
);
CREATE INDEX IF NOT EXISTS idx_outgoingoauthconnections_name ON outgoingoauthconnections (name);

View File

@@ -37,6 +37,7 @@ type OpenTracingLayer struct {
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
OutgoingOAuthConnectionStore store.OutgoingOAuthConnectionStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
@@ -137,6 +138,10 @@ func (s *OpenTracingLayer) OAuth() store.OAuthStore {
return s.OAuthStore
}
func (s *OpenTracingLayer) OutgoingOAuthConnection() store.OutgoingOAuthConnectionStore {
return s.OutgoingOAuthConnectionStore
}
func (s *OpenTracingLayer) Plugin() store.PluginStore {
return s.PluginStore
}
@@ -331,6 +336,11 @@ type OpenTracingLayerOAuthStore struct {
Root *OpenTracingLayer
}
type OpenTracingLayerOutgoingOAuthConnectionStore struct {
store.OutgoingOAuthConnectionStore
Root *OpenTracingLayer
}
type OpenTracingLayerPluginStore struct {
store.PluginStore
Root *OpenTracingLayer
@@ -5773,6 +5783,96 @@ func (s *OpenTracingLayerOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAut
return result, err
}
func (s *OpenTracingLayerOutgoingOAuthConnectionStore) DeleteConnection(c request.CTX, id string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OutgoingOAuthConnectionStore.DeleteConnection")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OutgoingOAuthConnectionStore.DeleteConnection(c, id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOutgoingOAuthConnectionStore) GetConnection(c request.CTX, id string) (*model.OutgoingOAuthConnection, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OutgoingOAuthConnectionStore.GetConnection")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OutgoingOAuthConnectionStore.GetConnection(c, id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOutgoingOAuthConnectionStore) GetConnections(c request.CTX, filters model.OutgoingOAuthConnectionGetConnectionsFilter) ([]*model.OutgoingOAuthConnection, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OutgoingOAuthConnectionStore.GetConnections")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OutgoingOAuthConnectionStore.GetConnections(c, filters)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOutgoingOAuthConnectionStore) SaveConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OutgoingOAuthConnectionStore.SaveConnection")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OutgoingOAuthConnectionStore.SaveConnection(c, conn)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOutgoingOAuthConnectionStore) UpdateConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OutgoingOAuthConnectionStore.UpdateConnection")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OutgoingOAuthConnectionStore.UpdateConnection(c, conn)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.CompareAndDelete")
@@ -13086,6 +13186,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
newStore.LinkMetadataStore = &OpenTracingLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
newStore.NotifyAdminStore = &OpenTracingLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
newStore.OAuthStore = &OpenTracingLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.OutgoingOAuthConnectionStore = &OpenTracingLayerOutgoingOAuthConnectionStore{OutgoingOAuthConnectionStore: childStore.OutgoingOAuthConnection(), Root: &newStore}
newStore.PluginStore = &OpenTracingLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &OpenTracingLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &OpenTracingLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}

View File

@@ -41,6 +41,7 @@ type RetryLayer struct {
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
OutgoingOAuthConnectionStore store.OutgoingOAuthConnectionStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
@@ -141,6 +142,10 @@ func (s *RetryLayer) OAuth() store.OAuthStore {
return s.OAuthStore
}
func (s *RetryLayer) OutgoingOAuthConnection() store.OutgoingOAuthConnectionStore {
return s.OutgoingOAuthConnectionStore
}
func (s *RetryLayer) Plugin() store.PluginStore {
return s.PluginStore
}
@@ -335,6 +340,11 @@ type RetryLayerOAuthStore struct {
Root *RetryLayer
}
type RetryLayerOutgoingOAuthConnectionStore struct {
store.OutgoingOAuthConnectionStore
Root *RetryLayer
}
type RetryLayerPluginStore struct {
store.PluginStore
Root *RetryLayer
@@ -6550,6 +6560,111 @@ func (s *RetryLayerOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp,
}
func (s *RetryLayerOutgoingOAuthConnectionStore) DeleteConnection(c request.CTX, id string) error {
tries := 0
for {
err := s.OutgoingOAuthConnectionStore.DeleteConnection(c, id)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOutgoingOAuthConnectionStore) GetConnection(c request.CTX, id string) (*model.OutgoingOAuthConnection, error) {
tries := 0
for {
result, err := s.OutgoingOAuthConnectionStore.GetConnection(c, id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOutgoingOAuthConnectionStore) GetConnections(c request.CTX, filters model.OutgoingOAuthConnectionGetConnectionsFilter) ([]*model.OutgoingOAuthConnection, error) {
tries := 0
for {
result, err := s.OutgoingOAuthConnectionStore.GetConnections(c, filters)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOutgoingOAuthConnectionStore) SaveConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error) {
tries := 0
for {
result, err := s.OutgoingOAuthConnectionStore.SaveConnection(c, conn)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOutgoingOAuthConnectionStore) UpdateConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error) {
tries := 0
for {
result, err := s.OutgoingOAuthConnectionStore.UpdateConnection(c, conn)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
tries := 0
@@ -14916,6 +15031,7 @@ func New(childStore store.Store) *RetryLayer {
newStore.LinkMetadataStore = &RetryLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
newStore.NotifyAdminStore = &RetryLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
newStore.OAuthStore = &RetryLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.OutgoingOAuthConnectionStore = &RetryLayerOutgoingOAuthConnectionStore{OutgoingOAuthConnectionStore: childStore.OutgoingOAuthConnection(), Root: &newStore}
newStore.PluginStore = &RetryLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &RetryLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &RetryLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}

View File

@@ -34,6 +34,7 @@ func genStore() *mocks.Store {
mock.On("LinkMetadata").Return(&mocks.LinkMetadataStore{})
mock.On("SharedChannel").Return(&mocks.SharedChannelStore{})
mock.On("OAuth").Return(&mocks.OAuthStore{})
mock.On("OutgoingOAuthConnection").Return(&mocks.OutgoingOAuthConnectionStore{})
mock.On("Plugin").Return(&mocks.PluginStore{})
mock.On("Post").Return(&mocks.PostStore{})
mock.On("Thread").Return(&mocks.ThreadStore{})

View File

@@ -0,0 +1,97 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"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/pkg/errors"
)
type SqlOutgoingOAuthConnectionStore struct {
*SqlStore
}
func newSqlOutgoingOAuthConnectionStore(sqlStore *SqlStore) store.OutgoingOAuthConnectionStore {
return &SqlOutgoingOAuthConnectionStore{sqlStore}
}
func (s *SqlOutgoingOAuthConnectionStore) SaveConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error) {
if conn.Id != "" {
return nil, store.NewErrInvalidInput("OutgoingOAuthConnection", "Id", conn.Id)
}
conn.PreSave()
if err := conn.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`INSERT INTO OutgoingOAuthConnections
(Id, Name, ClientId, ClientSecret, CreateAt, UpdateAt, CreatorId, OAuthTokenURL, GrantType, Audiences)
VALUES
(:Id, :Name, :ClientId, :ClientSecret, :CreateAt, :UpdateAt, :CreatorId, :OAuthTokenURL, :GrantType, :Audiences)`, conn); err != nil {
return nil, errors.Wrap(err, "failed to save OutgoingOAuthConnection")
}
return conn, nil
}
func (s *SqlOutgoingOAuthConnectionStore) UpdateConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error) {
if conn.Id == "" {
return nil, store.NewErrInvalidInput("OutgoingOAuthConnection", "Id", conn.Id)
}
conn.PreUpdate()
if err := conn.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`UPDATE OutgoingOAuthConnections SET
Name=:Name, ClientId=:ClientId, ClientSecret=:ClientSecret, UpdateAt=:UpdateAt, OAuthTokenURL=:OAuthTokenURL, GrantType=:GrantType, Audiences=:Audiences
WHERE Id=:Id`, conn); err != nil {
return nil, errors.Wrap(err, "failed to update OutgoingOAuthConnection")
}
return conn, nil
}
func (s *SqlOutgoingOAuthConnectionStore) GetConnection(c request.CTX, id string) (*model.OutgoingOAuthConnection, error) {
conn := &model.OutgoingOAuthConnection{}
if err := s.GetReplicaX().Get(conn, `SELECT * FROM OutgoingOAuthConnections WHERE Id=?`, id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("OutgoingOAuthConnection", id)
}
return nil, errors.Wrap(err, "failed to get OutgoingOAuthConnection")
}
return conn, nil
}
func (s *SqlOutgoingOAuthConnectionStore) GetConnections(c request.CTX, filters model.OutgoingOAuthConnectionGetConnectionsFilter) ([]*model.OutgoingOAuthConnection, error) {
filters.SetDefaults()
conns := []*model.OutgoingOAuthConnection{}
query := s.getQueryBuilder().
Select("*").
From("OutgoingOAuthConnections").
OrderBy("Id").
Limit(uint64(filters.Limit))
if filters.OffsetId != "" {
query = query.Where("Id > ?", filters.OffsetId)
}
if err := s.GetReplicaX().SelectBuilder(&conns, query); err != nil {
return nil, errors.Wrap(err, "failed to get OutgoingOAuthConnections")
}
return conns, nil
}
func (s *SqlOutgoingOAuthConnectionStore) DeleteConnection(c request.CTX, id string) error {
if _, err := s.GetMasterX().Exec(`DELETE FROM OutgoingOAuthConnections WHERE Id=?`, id); err != nil {
return errors.Wrap(err, "failed to delete OutgoingOAuthConnection")
}
return nil
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"testing"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
)
func TestOutgoingOAuthConnectionStore(t *testing.T) {
StoreTest(t, storetest.TestOutgoingOAuthConnectionStore)
}

View File

@@ -78,6 +78,7 @@ type SqlStoreStores struct {
compliance store.ComplianceStore
session store.SessionStore
oauth store.OAuthStore
outgoingOAuthConnection store.OutgoingOAuthConnectionStore
system store.SystemStore
webhook store.WebhookStore
command store.CommandStore
@@ -199,6 +200,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) (*Sql
store.stores.compliance = newSqlComplianceStore(store)
store.stores.session = newSqlSessionStore(store)
store.stores.oauth = newSqlOAuthStore(store)
store.stores.outgoingOAuthConnection = newSqlOutgoingOAuthConnectionStore(store)
store.stores.system = newSqlSystemStore(store)
store.stores.webhook = newSqlWebhookStore(store, metrics)
store.stores.command = newSqlCommandStore(store)
@@ -948,6 +950,10 @@ func (ss *SqlStore) OAuth() store.OAuthStore {
return ss.stores.oauth
}
func (ss *SqlStore) OutgoingOAuthConnection() store.OutgoingOAuthConnectionStore {
return ss.stores.outgoingOAuthConnection
}
func (ss *SqlStore) System() store.SystemStore {
return ss.stores.system
}

View File

@@ -46,6 +46,7 @@ type Store interface {
Compliance() ComplianceStore
Session() SessionStore
OAuth() OAuthStore
OutgoingOAuthConnection() OutgoingOAuthConnectionStore
System() SystemStore
Webhook() WebhookStore
Command() CommandStore
@@ -577,6 +578,14 @@ type OAuthStore interface {
RemoveAllAccessData() error
}
type OutgoingOAuthConnectionStore interface {
SaveConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error)
UpdateConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error)
GetConnection(c request.CTX, id string) (*model.OutgoingOAuthConnection, error)
GetConnections(c request.CTX, filters model.OutgoingOAuthConnectionGetConnectionsFilter) ([]*model.OutgoingOAuthConnection, error)
DeleteConnection(c request.CTX, id string) error
}
type SystemStore interface {
Save(system *model.System) error
SaveOrUpdate(system *model.System) error

View File

@@ -0,0 +1,149 @@
// Code generated by mockery v2.23.2. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost/server/public/model"
request "github.com/mattermost/mattermost/server/public/shared/request"
mock "github.com/stretchr/testify/mock"
)
// OutgoingOAuthConnectionStore is an autogenerated mock type for the OutgoingOAuthConnectionStore type
type OutgoingOAuthConnectionStore struct {
mock.Mock
}
// DeleteConnection provides a mock function with given fields: c, id
func (_m *OutgoingOAuthConnectionStore) DeleteConnection(c request.CTX, id string) error {
ret := _m.Called(c, id)
var r0 error
if rf, ok := ret.Get(0).(func(request.CTX, string) error); ok {
r0 = rf(c, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetConnection provides a mock function with given fields: c, id
func (_m *OutgoingOAuthConnectionStore) GetConnection(c request.CTX, id string) (*model.OutgoingOAuthConnection, error) {
ret := _m.Called(c, id)
var r0 *model.OutgoingOAuthConnection
var r1 error
if rf, ok := ret.Get(0).(func(request.CTX, string) (*model.OutgoingOAuthConnection, error)); ok {
return rf(c, id)
}
if rf, ok := ret.Get(0).(func(request.CTX, string) *model.OutgoingOAuthConnection); ok {
r0 = rf(c, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OutgoingOAuthConnection)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string) error); ok {
r1 = rf(c, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetConnections provides a mock function with given fields: c, filters
func (_m *OutgoingOAuthConnectionStore) GetConnections(c request.CTX, filters model.OutgoingOAuthConnectionGetConnectionsFilter) ([]*model.OutgoingOAuthConnection, error) {
ret := _m.Called(c, filters)
var r0 []*model.OutgoingOAuthConnection
var r1 error
if rf, ok := ret.Get(0).(func(request.CTX, model.OutgoingOAuthConnectionGetConnectionsFilter) ([]*model.OutgoingOAuthConnection, error)); ok {
return rf(c, filters)
}
if rf, ok := ret.Get(0).(func(request.CTX, model.OutgoingOAuthConnectionGetConnectionsFilter) []*model.OutgoingOAuthConnection); ok {
r0 = rf(c, filters)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingOAuthConnection)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, model.OutgoingOAuthConnectionGetConnectionsFilter) error); ok {
r1 = rf(c, filters)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveConnection provides a mock function with given fields: c, conn
func (_m *OutgoingOAuthConnectionStore) SaveConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error) {
ret := _m.Called(c, conn)
var r0 *model.OutgoingOAuthConnection
var r1 error
if rf, ok := ret.Get(0).(func(request.CTX, *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error)); ok {
return rf(c, conn)
}
if rf, ok := ret.Get(0).(func(request.CTX, *model.OutgoingOAuthConnection) *model.OutgoingOAuthConnection); ok {
r0 = rf(c, conn)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OutgoingOAuthConnection)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, *model.OutgoingOAuthConnection) error); ok {
r1 = rf(c, conn)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateConnection provides a mock function with given fields: c, conn
func (_m *OutgoingOAuthConnectionStore) UpdateConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error) {
ret := _m.Called(c, conn)
var r0 *model.OutgoingOAuthConnection
var r1 error
if rf, ok := ret.Get(0).(func(request.CTX, *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error)); ok {
return rf(c, conn)
}
if rf, ok := ret.Get(0).(func(request.CTX, *model.OutgoingOAuthConnection) *model.OutgoingOAuthConnection); ok {
r0 = rf(c, conn)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OutgoingOAuthConnection)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, *model.OutgoingOAuthConnection) error); ok {
r1 = rf(c, conn)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewOutgoingOAuthConnectionStore interface {
mock.TestingT
Cleanup(func())
}
// NewOutgoingOAuthConnectionStore creates a new instance of OutgoingOAuthConnectionStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewOutgoingOAuthConnectionStore(t mockConstructorTestingTNewOutgoingOAuthConnectionStore) *OutgoingOAuthConnectionStore {
mock := &OutgoingOAuthConnectionStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -492,6 +492,22 @@ func (_m *Store) OAuth() store.OAuthStore {
return r0
}
// OutgoingOAuthConnection provides a mock function with given fields:
func (_m *Store) OutgoingOAuthConnection() store.OutgoingOAuthConnectionStore {
ret := _m.Called()
var r0 store.OutgoingOAuthConnectionStore
if rf, ok := ret.Get(0).(func() store.OutgoingOAuthConnectionStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.OutgoingOAuthConnectionStore)
}
}
return r0
}
// Plugin provides a mock function with given fields:
func (_m *Store) Plugin() store.PluginStore {
ret := _m.Called()

View File

@@ -0,0 +1,239 @@
// 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("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)
})
}
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 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)
})
}

View File

@@ -29,6 +29,7 @@ type Store struct {
ComplianceStore mocks.ComplianceStore
SessionStore mocks.SessionStore
OAuthStore mocks.OAuthStore
OutgoingOAuthConnectionStore mocks.OutgoingOAuthConnectionStore
SystemStore mocks.SystemStore
WebhookStore mocks.WebhookStore
CommandStore mocks.CommandStore
@@ -64,21 +65,24 @@ type Store struct {
DesktopTokensStore mocks.DesktopTokensStore
}
func (s *Store) SetContext(context context.Context) { s.context = context }
func (s *Store) Context() context.Context { return s.context }
func (s *Store) Team() store.TeamStore { return &s.TeamStore }
func (s *Store) Channel() store.ChannelStore { return &s.ChannelStore }
func (s *Store) Post() store.PostStore { return &s.PostStore }
func (s *Store) User() store.UserStore { return &s.UserStore }
func (s *Store) RetentionPolicy() store.RetentionPolicyStore { return &s.RetentionPolicyStore }
func (s *Store) Bot() store.BotStore { return &s.BotStore }
func (s *Store) ProductNotices() store.ProductNoticesStore { return &s.ProductNoticesStore }
func (s *Store) Audit() store.AuditStore { return &s.AuditStore }
func (s *Store) ClusterDiscovery() store.ClusterDiscoveryStore { return &s.ClusterDiscoveryStore }
func (s *Store) RemoteCluster() store.RemoteClusterStore { return &s.RemoteClusterStore }
func (s *Store) Compliance() store.ComplianceStore { return &s.ComplianceStore }
func (s *Store) Session() store.SessionStore { return &s.SessionStore }
func (s *Store) OAuth() store.OAuthStore { return &s.OAuthStore }
func (s *Store) SetContext(context context.Context) { s.context = context }
func (s *Store) Context() context.Context { return s.context }
func (s *Store) Team() store.TeamStore { return &s.TeamStore }
func (s *Store) Channel() store.ChannelStore { return &s.ChannelStore }
func (s *Store) Post() store.PostStore { return &s.PostStore }
func (s *Store) User() store.UserStore { return &s.UserStore }
func (s *Store) RetentionPolicy() store.RetentionPolicyStore { return &s.RetentionPolicyStore }
func (s *Store) Bot() store.BotStore { return &s.BotStore }
func (s *Store) ProductNotices() store.ProductNoticesStore { return &s.ProductNoticesStore }
func (s *Store) Audit() store.AuditStore { return &s.AuditStore }
func (s *Store) ClusterDiscovery() store.ClusterDiscoveryStore { return &s.ClusterDiscoveryStore }
func (s *Store) RemoteCluster() store.RemoteClusterStore { return &s.RemoteClusterStore }
func (s *Store) Compliance() store.ComplianceStore { return &s.ComplianceStore }
func (s *Store) Session() store.SessionStore { return &s.SessionStore }
func (s *Store) OAuth() store.OAuthStore { return &s.OAuthStore }
func (s *Store) OutgoingOAuthConnection() store.OutgoingOAuthConnectionStore {
return &s.OutgoingOAuthConnectionStore
}
func (s *Store) System() store.SystemStore { return &s.SystemStore }
func (s *Store) Webhook() store.WebhookStore { return &s.WebhookStore }
func (s *Store) Command() store.CommandStore { return &s.CommandStore }

View File

@@ -37,6 +37,7 @@ type TimerLayer struct {
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
OutgoingOAuthConnectionStore store.OutgoingOAuthConnectionStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
@@ -137,6 +138,10 @@ func (s *TimerLayer) OAuth() store.OAuthStore {
return s.OAuthStore
}
func (s *TimerLayer) OutgoingOAuthConnection() store.OutgoingOAuthConnectionStore {
return s.OutgoingOAuthConnectionStore
}
func (s *TimerLayer) Plugin() store.PluginStore {
return s.PluginStore
}
@@ -331,6 +336,11 @@ type TimerLayerOAuthStore struct {
Root *TimerLayer
}
type TimerLayerOutgoingOAuthConnectionStore struct {
store.OutgoingOAuthConnectionStore
Root *TimerLayer
}
type TimerLayerPluginStore struct {
store.PluginStore
Root *TimerLayer
@@ -5233,6 +5243,86 @@ func (s *TimerLayerOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp,
return result, err
}
func (s *TimerLayerOutgoingOAuthConnectionStore) DeleteConnection(c request.CTX, id string) error {
start := time.Now()
err := s.OutgoingOAuthConnectionStore.DeleteConnection(c, id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OutgoingOAuthConnectionStore.DeleteConnection", success, elapsed)
}
return err
}
func (s *TimerLayerOutgoingOAuthConnectionStore) GetConnection(c request.CTX, id string) (*model.OutgoingOAuthConnection, error) {
start := time.Now()
result, err := s.OutgoingOAuthConnectionStore.GetConnection(c, id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OutgoingOAuthConnectionStore.GetConnection", success, elapsed)
}
return result, err
}
func (s *TimerLayerOutgoingOAuthConnectionStore) GetConnections(c request.CTX, filters model.OutgoingOAuthConnectionGetConnectionsFilter) ([]*model.OutgoingOAuthConnection, error) {
start := time.Now()
result, err := s.OutgoingOAuthConnectionStore.GetConnections(c, filters)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OutgoingOAuthConnectionStore.GetConnections", success, elapsed)
}
return result, err
}
func (s *TimerLayerOutgoingOAuthConnectionStore) SaveConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error) {
start := time.Now()
result, err := s.OutgoingOAuthConnectionStore.SaveConnection(c, conn)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OutgoingOAuthConnectionStore.SaveConnection", success, elapsed)
}
return result, err
}
func (s *TimerLayerOutgoingOAuthConnectionStore) UpdateConnection(c request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnection, error) {
start := time.Now()
result, err := s.OutgoingOAuthConnectionStore.UpdateConnection(c, conn)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OutgoingOAuthConnectionStore.UpdateConnection", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
start := time.Now()
@@ -11794,6 +11884,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
newStore.LinkMetadataStore = &TimerLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
newStore.NotifyAdminStore = &TimerLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
newStore.OAuthStore = &TimerLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.OutgoingOAuthConnectionStore = &TimerLayerOutgoingOAuthConnectionStore{OutgoingOAuthConnectionStore: childStore.OutgoingOAuthConnection(), Root: &newStore}
newStore.PluginStore = &TimerLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &TimerLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &TimerLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}

View File

@@ -9382,6 +9382,54 @@
"id": "model.outgoing_hook.username.app_error",
"translation": "Invalid username."
},
{
"id": "model.outgoing_oauth_connection.is_valid.audience.empty",
"translation": "Audience must not be empty."
},
{
"id": "model.outgoing_oauth_connection.is_valid.audience.error",
"translation": "Some audience URL is incorrect."
},
{
"id": "model.outgoing_oauth_connection.is_valid.client_id.error",
"translation": "Invalid client id."
},
{
"id": "model.outgoing_oauth_connection.is_valid.client_secret.error",
"translation": "Invalid client secret."
},
{
"id": "model.outgoing_oauth_connection.is_valid.create_at.error",
"translation": "Create at must be a valid time."
},
{
"id": "model.outgoing_oauth_connection.is_valid.creator_id.error",
"translation": "Invalid creator id."
},
{
"id": "model.outgoing_oauth_connection.is_valid.grant_type.error",
"translation": "Invalid grant type."
},
{
"id": "model.outgoing_oauth_connection.is_valid.id.error",
"translation": "Invalid id."
},
{
"id": "model.outgoing_oauth_connection.is_valid.name.error",
"translation": "Invalid name."
},
{
"id": "model.outgoing_oauth_connection.is_valid.oauth_token_url.error",
"translation": "Invalid oauth token url."
},
{
"id": "model.outgoing_oauth_connection.is_valid.password_credentials.error",
"translation": "Invalid password credentials."
},
{
"id": "model.outgoing_oauth_connection.is_valid.update_at.error",
"translation": "Update at must be a valid time."
},
{
"id": "model.plugin_command.error.app_error",
"translation": "An error occurred while trying to execute this command."

View File

@@ -0,0 +1,158 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"unicode/utf8"
)
type OutgoingOAuthConnectionGrantType string
func (gt OutgoingOAuthConnectionGrantType) IsValid() bool {
return gt == OutgoingOAuthConnectionGrantTypeClientCredentials || gt == OutgoingOAuthConnectionGrantTypePassword
}
const (
OutgoingOAuthConnectionGrantTypeClientCredentials OutgoingOAuthConnectionGrantType = "client_credentials"
OutgoingOAuthConnectionGrantTypePassword OutgoingOAuthConnectionGrantType = "password"
defaultGetConnectionsLimit = 50
)
type OutgoingOAuthConnection struct {
Id string `json:"id"`
CreatorId string `json:"creator_id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
Name string `json:"name"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
CredentialsUsername *string `json:"credentials_username,omitempty"`
CredentialsPassword *string `json:"credentials_password,omitempty"`
OAuthTokenURL string `json:"oauth_token_url"`
GrantType OutgoingOAuthConnectionGrantType `json:"grant_type"`
Audiences StringArray `json:"audiences"`
}
func (oa *OutgoingOAuthConnection) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": oa.Id,
"creator_id": oa.CreatorId,
"create_at": oa.CreateAt,
"update_at": oa.UpdateAt,
"name": oa.Name,
"grant_type": oa.GrantType,
}
}
// IsValid validates the object and returns an error if it isn't properly configured
func (oa *OutgoingOAuthConnection) IsValid() *AppError {
if !IsValidId(oa.Id) {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.id.error", nil, "", http.StatusBadRequest)
}
if oa.CreateAt == 0 {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.create_at.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
if oa.UpdateAt == 0 {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.update_at.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
if !IsValidId(oa.CreatorId) {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.creator_id.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(oa.Name) > 64 {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.name.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
if len(oa.ClientId) == 0 || utf8.RuneCountInString(oa.ClientId) > 255 {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.client_id.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
if len(oa.ClientSecret) == 0 || utf8.RuneCountInString(oa.ClientSecret) > 255 {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.client_secret.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
if len(oa.OAuthTokenURL) == 0 || utf8.RuneCountInString(oa.OAuthTokenURL) > 256 {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.oauth_token_url.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
if err := oa.IsValidGrantType(); err != nil {
return err
}
if len(oa.Audiences) == 0 {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.audience.empty", nil, "id="+oa.Id, http.StatusBadRequest)
}
if len(oa.Audiences) > 0 {
for _, audience := range oa.Audiences {
if !IsValidHTTPURL(audience) {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.audience.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
}
}
return nil
}
// IsValidGrantType validates the grant type and its parameters returning an error if it isn't properly configured
func (oa *OutgoingOAuthConnection) IsValidGrantType() *AppError {
if !oa.GrantType.IsValid() {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.grant_type.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
if oa.GrantType == OutgoingOAuthConnectionGrantTypePassword && (oa.CredentialsUsername == nil || oa.CredentialsPassword == nil) {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.password_credentials.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
if oa.GrantType == OutgoingOAuthConnectionGrantTypePassword && (*oa.CredentialsUsername == "" || *oa.CredentialsPassword == "") {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.password_credentials.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
return nil
}
// PreSave will set the Id if empty, ensuring the object has one and the create/update times.
func (oa *OutgoingOAuthConnection) PreSave() {
if oa.Id == "" {
oa.Id = NewId()
}
oa.CreateAt = GetMillis()
oa.UpdateAt = oa.CreateAt
}
// PreUpdate will set the update time to now.
func (oa *OutgoingOAuthConnection) PreUpdate() {
oa.UpdateAt = GetMillis()
}
// Etag returns the ETag for the cache.
func (oa *OutgoingOAuthConnection) Etag() string {
return Etag(oa.Id, oa.UpdateAt)
}
// Sanitize removes any sensitive fields from the OutgoingOAuthConnection object.
func (oa *OutgoingOAuthConnection) Sanitize() {
oa.ClientSecret = ""
oa.CredentialsUsername = nil
oa.CredentialsPassword = nil
}
// OutgoingOAuthConnectionGetConnectionsFilter is used to filter outgoing connections
type OutgoingOAuthConnectionGetConnectionsFilter struct {
OffsetId string
Limit int
}
// SetDefaults sets the default values for the filter
func (oaf *OutgoingOAuthConnectionGetConnectionsFilter) SetDefaults() {
if oaf.Limit == 0 {
oaf.Limit = defaultGetConnectionsLimit
}
}

View File

@@ -0,0 +1,313 @@
package model
import (
"testing"
"github.com/stretchr/testify/require"
)
var (
emptyString = ""
someString = "userorpass"
)
func newValidOutgoingOAuthConnection() *OutgoingOAuthConnection {
return &OutgoingOAuthConnection{
Id: NewId(),
CreatorId: NewId(),
Name: "Test Connection",
ClientId: NewId(),
ClientSecret: NewId(),
OAuthTokenURL: "https://nowhere.com/oauth/token",
GrantType: "client_credentials",
CreateAt: GetMillis(),
UpdateAt: GetMillis(),
Audiences: []string{"https://nowhere.com"},
}
}
func TestOutgoingOAuthConnectionIsValid(t *testing.T) {
var cases = []struct {
name string
item func() *OutgoingOAuthConnection
assert func(t *testing.T, oa *OutgoingOAuthConnection)
}{
{
name: "valid",
item: func() *OutgoingOAuthConnection {
return newValidOutgoingOAuthConnection()
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Nil(t, oa.IsValid())
},
},
{
name: "invalid id",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.Id = ""
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "invalid create_at",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.CreateAt = 0
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "invalid update_at",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.UpdateAt = 0
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "invalid creator_id",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.CreatorId = ""
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "invalid name",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.Name = ""
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "invalid client_id",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.ClientId = ""
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "long client_id",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.ClientId = string(make([]byte, 257))
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "invalid client_secret",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.ClientSecret = ""
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "long client_secret",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.ClientSecret = string(make([]byte, 257))
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "empty oauth_token_url",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.OAuthTokenURL = ""
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "long oauth_token_url",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.OAuthTokenURL = string(make([]byte, 257))
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "invalid oauth_token_url",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.OAuthTokenURL = "invalid"
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "invalid grant_type",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.GrantType = ""
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "nil password credentials",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.GrantType = OutgoingOAuthConnectionGrantTypePassword
oa.CredentialsUsername = nil
oa.CredentialsPassword = nil
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "invalid password credentials username",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.GrantType = OutgoingOAuthConnectionGrantTypePassword
oa.CredentialsUsername = &emptyString
oa.CredentialsPassword = &someString
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "invalid password credentials password",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.GrantType = OutgoingOAuthConnectionGrantTypePassword
oa.CredentialsUsername = &someString
oa.CredentialsPassword = &emptyString
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "empty password credentials",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.GrantType = OutgoingOAuthConnectionGrantTypePassword
oa.CredentialsUsername = &emptyString
oa.CredentialsPassword = &emptyString
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "correct password credentials",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.GrantType = OutgoingOAuthConnectionGrantTypePassword
oa.CredentialsUsername = &someString
oa.CredentialsPassword = &someString
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Nil(t, oa.IsValid())
},
},
{
name: "empty audience",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.Audiences = []string{}
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
{
name: "invalid audience",
item: func() *OutgoingOAuthConnection {
oa := newValidOutgoingOAuthConnection()
oa.Audiences = []string{"https://nowhere.com", "invalid"}
return oa
},
assert: func(t *testing.T, oa *OutgoingOAuthConnection) {
require.Error(t, oa.IsValid())
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
tc.assert(t, tc.item())
})
}
}
func TestOutgoingOAuthConnectionPreSave(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.PreSave()
require.NotEmpty(t, oa.Id)
require.NotZero(t, oa.CreateAt)
require.NotZero(t, oa.UpdateAt)
}
func TestOutgoingOAuthConnectionPreUpdate(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.PreUpdate()
require.NotZero(t, oa.UpdateAt)
}
func TestOutgoingOAuthConnectionEtag(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.PreSave()
require.NotEmpty(t, oa.Etag())
}
func TestOutgoingOAuthConnectionSanitize(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.Sanitize()
require.Empty(t, oa.ClientSecret)
require.Empty(t, oa.CredentialsUsername)
require.Empty(t, oa.CredentialsPassword)
}