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 <me@fmartingr.com>
Этот коммит содержится в:
Michael Kochell
2024-02-09 14:49:49 -05:00
коммит произвёл GitHub
родитель 3f6c94cfc3
Коммит 4e071e861c
91 изменённых файлов: 10116 добавлений и 201 удалений

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

@@ -277,7 +277,7 @@ func Init(srv *app.Server) (*API, error) {
api.BaseRoutes.Limits = api.BaseRoutes.APIRoot.PathPrefix("/limits").Subrouter()
api.BaseRoutes.OutgoingOAuthConnections = api.BaseRoutes.APIRoot.PathPrefix("/oauth/outgoing_connections").Subrouter()
api.BaseRoutes.OutgoingOAuthConnection = api.BaseRoutes.APIRoot.PathPrefix("/oauth/outgoing_connections/{outgoing_oauth_connection_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.OutgoingOAuthConnection = api.BaseRoutes.OutgoingOAuthConnections.PathPrefix("/{outgoing_oauth_connection_id:[A-Za-z0-9]+}").Subrouter()
api.InitUser()
api.InitBot()

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

@@ -6,11 +6,14 @@ package api4
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"github.com/mattermost/logr/v2"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/audit"
"github.com/mattermost/mattermost/server/v8/einterfaces"
)
@@ -20,12 +23,44 @@ const (
func (api *API) InitOutgoingOAuthConnection() {
api.BaseRoutes.OutgoingOAuthConnections.Handle("", api.APISessionRequired(listOutgoingOAuthConnections)).Methods("GET")
api.BaseRoutes.OutgoingOAuthConnections.Handle("", api.APISessionRequired(createOutgoingOAuthConnection)).Methods("POST")
api.BaseRoutes.OutgoingOAuthConnection.Handle("", api.APISessionRequired(getOutgoingOAuthConnection)).Methods("GET")
api.BaseRoutes.OutgoingOAuthConnection.Handle("", api.APISessionRequired(updateOutgoingOAuthConnection)).Methods("PUT")
api.BaseRoutes.OutgoingOAuthConnection.Handle("", api.APISessionRequired(deleteOutgoingOAuthConnection)).Methods("DELETE")
api.BaseRoutes.OutgoingOAuthConnections.Handle("/validate", api.APISessionRequired(validateOutgoingOAuthConnectionCredentials)).Methods("POST")
}
// checkOutgoingOAuthConnectionReadPermissions checks if the user has the permissions to read outgoing oauth connections.
// An user with the permissions to manage outgoing oauth connections can read outgoing oauth connections.
// Otherwise the user needs to have the permissions to manage outgoing webhooks or slash commands in order to read outgoing
// oauth connections so that they can use them.
// This is made in this way so only users with the management permission can setup the outgoing oauth connections and then
// other users can use them in their outgoing webhooks and slash commands if they have permissions to manage those.
func checkOutgoingOAuthConnectionReadPermissions(c *Context, teamId string) bool {
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOutgoingOAuthConnections) ||
c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageOutgoingWebhooks) ||
c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) {
return true
}
c.SetPermissionError(model.PermissionManageOutgoingWebhooks, model.PermissionManageSlashCommands)
return false
}
// checkOutgoingOAuthConnectionWritePermissions checks if the user has the permissions to write outgoing oauth connections.
// This is a more granular permissions intended for system admins to manage (setup) outgoing oauth connections.
func checkOutgoingOAuthConnectionWritePermissions(c *Context) bool {
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOutgoingOAuthConnections) {
return true
}
c.SetPermissionError(model.PermissionManageOutgoingOAuthConnections)
return false
}
func ensureOutgoingOAuthConnectionInterface(c *Context, where string) (einterfaces.OutgoingOAuthConnectionInterface, bool) {
if !c.App.Config().FeatureFlags.OutgoingOAuthConnections {
c.Err = model.NewAppError(where, "api.context.outgoing_oauth_connection.not_available.feature_flag", nil, "", http.StatusNotImplemented)
if c.App.Config().ServiceSettings.EnableOutgoingOAuthConnections != nil && !*c.App.Config().ServiceSettings.EnableOutgoingOAuthConnections {
c.Err = model.NewAppError(where, "api.context.outgoing_oauth_connection.not_available.configuration_disabled", nil, "", http.StatusNotImplemented)
return nil, false
}
@@ -37,8 +72,9 @@ func ensureOutgoingOAuthConnectionInterface(c *Context, where string) (einterfac
}
type listOutgoingOAuthConnectionsQuery struct {
FromID string
Limit int
FromID string
Limit int
Audience string
}
// SetDefaults sets the default values for the query.
@@ -62,6 +98,7 @@ func (q *listOutgoingOAuthConnectionsQuery) ToFilter() model.OutgoingOAuthConnec
return model.OutgoingOAuthConnectionGetConnectionsFilter{
OffsetId: q.FromID,
Limit: q.Limit,
Audience: q.Audience,
}
}
@@ -77,16 +114,26 @@ func NewListOutgoingOAuthConnectionsQueryFromURLQuery(values url.Values) (*listO
limit := values.Get("limit")
if limit != "" {
limitInt, err := strconv.Atoi(limit)
if err == nil {
if err != nil {
return nil, err
}
query.Limit = limitInt
}
audience := values.Get("audience")
if audience != "" {
query.Audience = audience
}
return query, nil
}
func listOutgoingOAuthConnections(c *Context, w http.ResponseWriter, r *http.Request) {
teamId := r.URL.Query().Get("team_id")
if !checkOutgoingOAuthConnectionReadPermissions(c, teamId) {
return
}
service, ok := ensureOutgoingOAuthConnectionInterface(c, whereOutgoingOAuthConnection)
if !ok {
return
@@ -103,10 +150,25 @@ func listOutgoingOAuthConnections(c *Context, w http.ResponseWriter, r *http.Req
return
}
connections, errList := service.GetConnections(c.AppContext, query.ToFilter())
if errList != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.list_connections.app_error", nil, errList.Error(), http.StatusInternalServerError)
return
var connections []*model.OutgoingOAuthConnection
if query.Audience != "" {
// If the consumer expects an audience match, use the `GetConnectionByAudience` method to
// retrieve a single connection.
connection, err := service.GetConnectionForAudience(c.AppContext, query.Audience)
if err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.list_connections.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
connections = append(connections, connection)
} else {
// If the consumer does not expect an audience match, use the `GetConnections` method to
// retrieve a list of connections that potentially matches the provided audience.
var errList *model.AppError
connections, errList = service.GetConnections(c.AppContext, query.ToFilter())
if errList != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.list_connections.app_error", nil, errList.Error(), http.StatusInternalServerError)
return
}
}
service.SanitizeConnections(connections)
@@ -118,6 +180,10 @@ func listOutgoingOAuthConnections(c *Context, w http.ResponseWriter, r *http.Req
}
func getOutgoingOAuthConnection(c *Context, w http.ResponseWriter, r *http.Request) {
if !checkOutgoingOAuthConnectionWritePermissions(c) {
return
}
service, ok := ensureOutgoingOAuthConnectionInterface(c, whereOutgoingOAuthConnection)
if !ok {
return
@@ -138,3 +204,216 @@ func getOutgoingOAuthConnection(c *Context, w http.ResponseWriter, r *http.Reque
return
}
}
func createOutgoingOAuthConnection(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("createOutgoingOauthConnection", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !checkOutgoingOAuthConnectionWritePermissions(c) {
return
}
service, ok := ensureOutgoingOAuthConnectionInterface(c, whereOutgoingOAuthConnection)
if !ok {
return
}
var inputConnection model.OutgoingOAuthConnection
bodyReader := io.LimitReader(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
if err := json.NewDecoder(bodyReader).Decode(&inputConnection); err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.create_connection.input_error", nil, err.Error(), http.StatusBadRequest)
return
}
audit.AddEventParameterAuditable(auditRec, "outgoing_oauth_connection", &inputConnection)
inputConnection.CreatorId = c.AppContext.Session().UserId
connection, err := service.SaveConnection(c.AppContext, &inputConnection)
if err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.create_connection.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.Success()
auditRec.AddEventResultState(connection)
auditRec.AddEventObjectType("outgoing_oauth_connection")
c.LogAudit("client_id=" + connection.ClientId)
service.SanitizeConnection(connection)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(connection); err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.create_connection.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func updateOutgoingOAuthConnection(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("updateOutgoingOAuthConnection", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "outgoing_oauth_connection_id", c.Params.OutgoingOAuthConnectionID)
c.LogAudit("attempt")
if !checkOutgoingOAuthConnectionWritePermissions(c) {
return
}
service, ok := ensureOutgoingOAuthConnectionInterface(c, whereOutgoingOAuthConnection)
if !ok {
return
}
c.RequireOutgoingOAuthConnectionId()
if c.Err != nil {
return
}
var inputConnection model.OutgoingOAuthConnection
bodyReader := io.LimitReader(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
if err := json.NewDecoder(bodyReader).Decode(&inputConnection); err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.update_connection.input_error", nil, err.Error(), http.StatusBadRequest)
return
}
if inputConnection.Id != c.Params.OutgoingOAuthConnectionID {
c.SetInvalidParam("id")
return
}
currentConnection, err := service.GetConnection(c.AppContext, c.Params.OutgoingOAuthConnectionID)
if err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.update_connection.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.AddEventPriorState(currentConnection)
currentConnection.Patch(&inputConnection)
connection, err := service.UpdateConnection(c.AppContext, currentConnection)
if err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.update_connection.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.AddEventObjectType("outgoing_oauth_connection")
auditRec.AddEventResultState(connection)
auditRec.Success()
auditLogExtraInfo := "success"
// Audit log changes to clientID/Client Secret
if connection.ClientId != currentConnection.ClientId {
auditLogExtraInfo += " new_client_id=" + connection.ClientId
}
if connection.ClientSecret != currentConnection.ClientSecret {
auditLogExtraInfo += " new_client_secret"
}
c.LogAudit(auditLogExtraInfo)
service.SanitizeConnection(connection)
if err := json.NewEncoder(w).Encode(connection); err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.update_connection.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func deleteOutgoingOAuthConnection(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("deleteOutgoingOAuthConnection", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "outgoing_oauth_connection_id", c.Params.OutgoingOAuthConnectionID)
c.LogAudit("attempt")
if !checkOutgoingOAuthConnectionWritePermissions(c) {
return
}
service, ok := ensureOutgoingOAuthConnectionInterface(c, whereOutgoingOAuthConnection)
if !ok {
return
}
c.RequireOutgoingOAuthConnectionId()
if c.Err != nil {
return
}
connection, err := service.GetConnection(c.AppContext, c.Params.OutgoingOAuthConnectionID)
if err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.delete_connection.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.AddEventPriorState(connection)
if err := service.DeleteConnection(c.AppContext, c.Params.OutgoingOAuthConnectionID); err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.delete_connection.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.AddEventObjectType("outgoing_oauth_connection")
auditRec.Success()
ReturnStatusOK(w)
}
// validateOutgoingOAuthConnectionCredentials validates the credentials of an outgoing oauth connection by requesting a token
// with the provided connection configuration. If the credentials are valid, the request will return a 200 status code and
// if the credentials are invalid, the request will return a 400 status code.
func validateOutgoingOAuthConnectionCredentials(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("validateOutgoingOAuthConnectionCredentials", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !checkOutgoingOAuthConnectionWritePermissions(c) {
return
}
service, ok := ensureOutgoingOAuthConnectionInterface(c, whereOutgoingOAuthConnection)
if !ok {
return
}
// Allow checking connections sent in the body or by id if coming from an already existing
// connection url.
var inputConnection *model.OutgoingOAuthConnection
bodyReader := io.LimitReader(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
if err := json.NewDecoder(bodyReader).Decode(&inputConnection); err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.validate_connection_credentials.input_error", nil, err.Error(), http.StatusBadRequest)
w.WriteHeader(c.Err.StatusCode)
return
}
if inputConnection.Id != "" && inputConnection.ClientSecret == "" {
var err *model.AppError
var storedConnection *model.OutgoingOAuthConnection
storedConnection, err = service.GetConnection(c.AppContext, inputConnection.Id)
if err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.validate_connection_credentials.app_error", nil, err.Error(), http.StatusInternalServerError)
w.WriteHeader(c.Err.StatusCode)
return
}
inputConnection.ClientSecret = storedConnection.ClientSecret
}
audit.AddEventParameterAuditable(auditRec, "outgoing_oauth_connection", inputConnection)
resultStatusCode := http.StatusOK
// Try to retrieve a token with the provided credentials
// do not store the token, just check if the credentials are valid and the request can be made
_, err := service.RetrieveTokenForConnection(c.AppContext, inputConnection)
if err != nil {
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.validate_connection_credentials.app_error", nil, err.Error(), err.StatusCode)
c.Logger.Error("Failed to retrieve token while validating outgoing oauth connection", logr.Err(err))
resultStatusCode = err.StatusCode
} else {
ReturnStatusOK(w)
}
auditRec.Success()
auditRec.AddEventResultState(inputConnection)
auditRec.AddEventObjectType("outgoing_oauth_connection")
w.WriteHeader(resultStatusCode)
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@@ -60,14 +60,13 @@ type Channels struct {
// previously fetched notices
cachedNotices model.ProductNotices
AccountMigration einterfaces.AccountMigrationInterface
Compliance einterfaces.ComplianceInterface
DataRetention einterfaces.DataRetentionInterface
MessageExport einterfaces.MessageExportInterface
Saml einterfaces.SamlInterface
Notification einterfaces.NotificationInterface
OutgoingOAuthConnection einterfaces.OutgoingOAuthConnectionInterface
Ldap einterfaces.LdapInterface
AccountMigration einterfaces.AccountMigrationInterface
Compliance einterfaces.ComplianceInterface
DataRetention einterfaces.DataRetentionInterface
MessageExport einterfaces.MessageExportInterface
Saml einterfaces.SamlInterface
Notification einterfaces.NotificationInterface
Ldap einterfaces.LdapInterface
// These are used to prevent concurrent upload requests
// for a given upload session which could cause inconsistencies
@@ -177,9 +176,6 @@ func NewChannels(services map[product.ServiceKey]any) (*Channels, error) {
if notificationInterface != nil {
ch.Notification = notificationInterface(New(ServerConnector(ch)))
}
if outgoingOauthConnectionInterface != nil {
ch.OutgoingOAuthConnection = outgoingOauthConnectionInterface(New(ServerConnector(ch)))
}
if samlInterfaceNew != nil {
ch.Saml = samlInterfaceNew(New(ServerConnector(ch)))
if err := ch.Saml.ConfigureSP(request.EmptyContext(s.Log())); err != nil {

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

@@ -485,6 +485,23 @@ func (a *App) DoCommandRequest(rctx request.CTX, cmd *model.Command, p url.Value
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*a.Config().ServiceSettings.OutgoingIntegrationRequestsTimeout)*time.Second)
defer cancel()
var accessToken *model.OutgoingOAuthConnectionToken
// Retrieve an access token from a connection if one exists to use for the webhook request
if a.Config().ServiceSettings.EnableOutgoingOAuthConnections != nil && *a.Config().ServiceSettings.EnableOutgoingOAuthConnections && a.OutgoingOAuthConnections() != nil {
connection, err := a.OutgoingOAuthConnections().GetConnectionForAudience(rctx, cmd.URL)
if err != nil {
a.Log().Error("Failed to find an outgoing oauth connection for the webhook", mlog.Err(err))
}
if connection != nil {
accessToken, err = a.OutgoingOAuthConnections().RetrieveTokenForConnection(rctx, connection)
if err != nil {
a.Log().Error("Failed to retrieve token for outgoing oauth connection", mlog.Err(err))
}
}
}
// Prepare the request
var req *http.Request
var err error
@@ -506,7 +523,14 @@ func (a *App) DoCommandRequest(rctx request.CTX, cmd *model.Command, p url.Value
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Token "+cmd.Token)
if cmd.Token != "" {
req.Header.Set("Authorization", "Token "+cmd.Token)
}
if accessToken != nil {
req.Header.Set("Authorization", accessToken.AsHeaderValue())
}
if cmd.Method == model.CommandMethodPost {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}

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

@@ -1141,6 +1141,21 @@ func (a *App) getAddIPFilterPermissionsMigration() (permissionsMap, error) {
return t, nil
}
func (a *App) getAddOutgoingOAuthConnectionsPermissions() (permissionsMap, error) {
t := []permissionTransformation{}
permissionManageOutgoingOAuthConnections := []string{
model.PermissionManageOutgoingOAuthConnections.Id,
}
t = append(t, permissionTransformation{
On: permissionOr(isExactRole(model.SystemAdminRoleId)),
Add: permissionManageOutgoingOAuthConnections,
})
return t, nil
}
// DoPermissionsMigrations execute all the permissions migrations need by the current version.
func (a *App) DoPermissionsMigrations() error {
return a.Srv().doPermissionsMigrations()
@@ -1186,6 +1201,7 @@ func (s *Server) doPermissionsMigrations() error {
{Key: model.MigrationKeyAddCustomUserGroupsPermissionRestore, Migration: a.getAddCustomUserGroupsPermissionRestore},
{Key: model.MigrationKeyAddReadChannelContentPermissions, Migration: a.getAddChannelReadContentPermissions},
{Key: model.MigrationKeyAddIPFilteringPermissions, Migration: a.getAddIPFilterPermissionsMigration},
{Key: model.MigrationKeyAddOutgoingOAuthConnectionsPermissions, Migration: a.getAddOutgoingOAuthConnectionsPermissions},
}
roles, err := s.Store().Role().GetAll()

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

@@ -402,6 +402,10 @@ func NewServer(options ...Option) (*Server, error) {
s.IPFiltering = ipFilteringInterface(app)
}
if outgoingOauthConnectionInterface != nil {
s.OutgoingOAuthConnection = outgoingOauthConnectionInterface(app)
}
s.clusterLeaderListenerId = s.AddClusterLeaderChangedListener(func() {
mlog.Info("Cluster leader changed. Determining if job schedulers should be running:", mlog.Bool("isLeader", s.IsLeader()))
if s.Jobs != nil {

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

@@ -17,6 +17,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/plugintest/mock"
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
)
type InfiniteReader struct {
@@ -459,6 +461,51 @@ func TestDoCommandRequest(t *testing.T) {
require.NotNil(t, resp)
assert.Equal(t, "Hello, World!", resp.Text)
})
t.Run("with a url that matches an outgoing oauth connection", func(t *testing.T) {
outgoingOauthIface := &mocks.OutgoingOAuthConnectionInterface{}
outgoingOauthImpl := th.App.Srv().OutgoingOAuthConnection
outgoingOAuthConnectionConfig := th.App.Config().ServiceSettings.EnableOutgoingOAuthConnections
th.App.Config().ServiceSettings.EnableOutgoingOAuthConnections = model.NewBool(true)
t.Cleanup(func() {
th.App.Srv().OutgoingOAuthConnection = outgoingOauthImpl
th.App.Config().ServiceSettings.EnableOutgoingOAuthConnections = outgoingOAuthConnectionConfig
})
th.App.Srv().OutgoingOAuthConnection = outgoingOauthIface
serverCommand := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, strings.NewReader(r.Header.Get("Authorization")))
}))
defer serverCommand.Close()
connection := &model.OutgoingOAuthConnection{
Id: model.NewId(),
Name: "test",
ClientId: "test",
ClientSecret: "test",
CreatorId: model.NewId(),
OAuthTokenURL: "fake",
GrantType: model.OutgoingOAuthConnectionGrantTypeClientCredentials,
Audiences: model.StringArray{
serverCommand.URL,
},
}
outgoingOauthIface.Mock.On("GetConnectionForAudience", mock.Anything, serverCommand.URL).Return(connection, nil)
outgoingOauthIface.Mock.On("SanitizeConnections", mock.Anything)
outgoingOauthIface.Mock.On("RetrieveTokenForConnection", mock.Anything, connection).Return(&model.OutgoingOAuthConnectionToken{
AccessToken: "token",
TokenType: "type",
}, nil)
_, resp, err := th.App.DoCommandRequest(th.Context, &model.Command{URL: serverCommand.URL}, url.Values{})
require.Nil(t, err)
require.NotNil(t, resp)
// Ensure that the Authorization header was set correctly by reading the body from the command response
// which was set to the Authorization header by the command handler.
assert.Equal(t, "type token", resp.Text)
})
}
func TestMentionsToTeamMembers(t *testing.T) {

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

@@ -117,7 +117,27 @@ func (a *App) TriggerWebhook(c request.CTX, payload *model.OutgoingWebhookPayloa
go func() {
defer wg.Done()
webhookResp, err := a.doOutgoingWebhookRequest(url, body, contentType)
var accessToken *model.OutgoingOAuthConnectionToken
// Retrieve an access token from a connection if one exists to use for the webhook request
if a.Config().ServiceSettings.EnableOutgoingOAuthConnections != nil && *a.Config().ServiceSettings.EnableOutgoingOAuthConnections && a.OutgoingOAuthConnections() != nil {
connection, err := a.OutgoingOAuthConnections().GetConnectionForAudience(c, url)
if err != nil {
c.Logger().Error("Failed to find an outgoing oauth connection for the webhook", mlog.Err(err))
return
}
if connection != nil {
accessToken, err = a.OutgoingOAuthConnections().RetrieveTokenForConnection(c, connection)
if err != nil {
c.Logger().Error("Failed to retrieve token for outgoing oauth connection", mlog.Err(err))
return
}
}
}
webhookResp, err := a.doOutgoingWebhookRequest(url, body, contentType, accessToken)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
c.Logger().Error("Outgoing Webhook POST timed out. Consider increasing ServiceSettings.OutgoingIntegrationRequestsTimeout.", mlog.Err(err))
@@ -162,7 +182,7 @@ func (a *App) TriggerWebhook(c request.CTX, payload *model.OutgoingWebhookPayloa
wg.Wait()
}
func (a *App) doOutgoingWebhookRequest(url string, body io.Reader, contentType string) (*model.OutgoingWebhookResponse, error) {
func (a *App) doOutgoingWebhookRequest(url string, body io.Reader, contentType string, accessToken *model.OutgoingOAuthConnectionToken) (*model.OutgoingWebhookResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*a.Config().ServiceSettings.OutgoingIntegrationRequestsTimeout)*time.Second)
defer cancel()
@@ -174,6 +194,10 @@ func (a *App) doOutgoingWebhookRequest(url string, body io.Reader, contentType s
req.Header.Set("Content-Type", contentType)
req.Header.Set("Accept", "application/json")
if accessToken != nil {
req.Header.Add("Authorization", accessToken.AsHeaderValue())
}
resp, err := a.Srv().outgoingWebhookClient.Do(req)
if err != nil {
return nil, err

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

@@ -6,6 +6,7 @@ package app
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
@@ -783,7 +784,7 @@ func TestDoOutgoingWebhookRequest(t *testing.T) {
}))
defer server.Close()
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
require.NoError(t, err)
require.NotNil(t, resp)
@@ -797,7 +798,7 @@ func TestDoOutgoingWebhookRequest(t *testing.T) {
}))
defer server.Close()
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
require.Error(t, err)
require.Equal(t, "api.unmarshal_error", err.(*model.AppError).Id)
})
@@ -808,7 +809,7 @@ func TestDoOutgoingWebhookRequest(t *testing.T) {
}))
defer server.Close()
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
require.Error(t, err)
require.Equal(t, "api.unmarshal_error", err.(*model.AppError).Id)
})
@@ -819,7 +820,7 @@ func TestDoOutgoingWebhookRequest(t *testing.T) {
}))
defer server.Close()
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
require.Error(t, err)
require.Equal(t, "api.unmarshal_error", err.(*model.AppError).Id)
})
@@ -838,7 +839,7 @@ func TestDoOutgoingWebhookRequest(t *testing.T) {
cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = model.NewInt64(1)
})
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
require.Error(t, err)
require.IsType(t, &url.Error{}, err)
})
@@ -855,7 +856,7 @@ func TestDoOutgoingWebhookRequest(t *testing.T) {
cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = model.NewInt64(2)
})
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
require.NoError(t, err)
require.NotNil(t, resp)
assert.NotNil(t, resp.Text)
@@ -867,8 +868,22 @@ func TestDoOutgoingWebhookRequest(t *testing.T) {
}))
defer server.Close()
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", nil)
require.NoError(t, err)
require.Nil(t, resp)
})
t.Run("with auth token", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, strings.NewReader(fmt.Sprintf(`{"text":"%s"}`, r.Header.Get("Authorization"))))
}))
defer server.Close()
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json", &model.OutgoingOAuthConnectionToken{
AccessToken: "test",
TokenType: "Bearer",
})
require.NoError(t, err)
require.Equal(t, `Bearer test`, *resp.Text)
})
}

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

@@ -5,6 +5,9 @@ package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
@@ -49,9 +52,33 @@ func (s *SqlOutgoingOAuthConnectionStore) UpdateConnection(c request.CTX, conn *
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 {
query := s.getQueryBuilder().Update("OutgoingOAuthConnections").Where(sq.Eq{"Id": conn.Id}).Set("UpdateAt", conn.UpdateAt)
if conn.Name != "" {
query = query.Set("Name", conn.Name)
}
if conn.ClientId != "" {
query = query.Set("ClientId", conn.ClientId)
}
if conn.ClientSecret != "" {
query = query.Set("ClientSecret", conn.ClientSecret)
}
if conn.OAuthTokenURL != "" {
query = query.Set("OAuthTokenURL", conn.OAuthTokenURL)
}
if conn.GrantType != "" {
query = query.Set("GrantType", conn.GrantType)
}
if len(conn.Audiences) > 0 {
query = query.Set("Audiences", conn.Audiences)
}
if conn.CredentialsUsername != nil {
query = query.Set("CredentialsUsername", conn.CredentialsUsername)
}
if conn.CredentialsPassword != nil {
query = query.Set("CredentialsPassword", conn.CredentialsPassword)
}
if _, err := s.GetMasterX().ExecBuilder(query); err != nil {
return nil, errors.Wrap(err, "failed to update OutgoingOAuthConnection")
}
return conn, nil
@@ -82,6 +109,10 @@ func (s *SqlOutgoingOAuthConnectionStore) GetConnections(c request.CTX, filters
query = query.Where("Id > ?", filters.OffsetId)
}
if filters.Audience != "" {
query = query.Where(sq.Like{"Audiences": fmt.Sprint("%", filters.Audience, "%")})
}
if err := s.GetReplicaX().SelectBuilder(&conns, query); err != nil {
return nil, errors.Wrap(err, "failed to get OutgoingOAuthConnections")
}

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

@@ -52,6 +52,10 @@ func TestOutgoingOAuthConnectionStore(t *testing.T, rctx request.CTX, ss store.S
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)
@@ -159,6 +163,106 @@ func testUpdateOutgoingOAuthConnection(t *testing.T, ss store.Store) {
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) {
@@ -172,6 +276,74 @@ func testGetOutgoingOAuthConnection(t *testing.T, ss store.Store) {
})
}
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)

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

@@ -74,6 +74,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
systemStore.On("GetByName", model.MigrationKeyDeleteEmptyDrafts).Return(&model.System{Name: model.MigrationKeyDeleteEmptyDrafts, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyDeleteOrphanDrafts).Return(&model.System{Name: model.MigrationKeyDeleteOrphanDrafts, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddIPFilteringPermissions).Return(&model.System{Name: model.MigrationKeyAddIPFilteringPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddOutgoingOAuthConnectionsPermissions).Return(&model.System{Name: model.MigrationKeyAddOutgoingOAuthConnectionsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", "CustomGroupAdminRoleCreationMigrationComplete").Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
systemStore.On("GetByName", "products_boards").Return(&model.System{Name: "products_boards", Value: "true"}, nil)
systemStore.On("GetByName", "elasticsearch_fix_channel_index_migration").Return(&model.System{Name: "elasticsearch_fix_channel_index_migration", Value: "true"}, nil)

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

@@ -103,6 +103,7 @@ func setupClientTests(cfg *model.Config) {
*cfg.ServiceSettings.EnableCustomEmoji = true
*cfg.ServiceSettings.EnableIncomingWebhooks = false
*cfg.ServiceSettings.EnableOutgoingWebhooks = false
*cfg.ServiceSettings.EnableOutgoingOAuthConnections = false
}
func executeTestCommand(command *exec.Cmd) {

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

@@ -29,6 +29,7 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
props["GoogleDeveloperKey"] = *c.ServiceSettings.GoogleDeveloperKey
props["EnableIncomingWebhooks"] = strconv.FormatBool(*c.ServiceSettings.EnableIncomingWebhooks)
props["EnableOutgoingWebhooks"] = strconv.FormatBool(*c.ServiceSettings.EnableOutgoingWebhooks)
props["EnableOutgoingOAuthConnections"] = strconv.FormatBool(*c.ServiceSettings.EnableOutgoingOAuthConnections)
props["EnableCommands"] = strconv.FormatBool(*c.ServiceSettings.EnableCommands)
props["EnablePostUsernameOverride"] = strconv.FormatBool(*c.ServiceSettings.EnablePostUsernameOverride)
props["EnablePostIconOverride"] = strconv.FormatBool(*c.ServiceSettings.EnablePostIconOverride)

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

@@ -59,6 +59,34 @@ func (_m *OutgoingOAuthConnectionInterface) GetConnection(rctx request.CTX, id s
return r0, r1
}
// GetConnectionForAudience provides a mock function with given fields: rctx, url
func (_m *OutgoingOAuthConnectionInterface) GetConnectionForAudience(rctx request.CTX, url string) (*model.OutgoingOAuthConnection, *model.AppError) {
ret := _m.Called(rctx, url)
var r0 *model.OutgoingOAuthConnection
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, string) (*model.OutgoingOAuthConnection, *model.AppError)); ok {
return rf(rctx, url)
}
if rf, ok := ret.Get(0).(func(request.CTX, string) *model.OutgoingOAuthConnection); ok {
r0 = rf(rctx, url)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OutgoingOAuthConnection)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, string) *model.AppError); ok {
r1 = rf(rctx, url)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetConnections provides a mock function with given fields: rctx, filters
func (_m *OutgoingOAuthConnectionInterface) GetConnections(rctx request.CTX, filters model.OutgoingOAuthConnectionGetConnectionsFilter) ([]*model.OutgoingOAuthConnection, *model.AppError) {
ret := _m.Called(rctx, filters)
@@ -87,6 +115,34 @@ func (_m *OutgoingOAuthConnectionInterface) GetConnections(rctx request.CTX, fil
return r0, r1
}
// RetrieveTokenForConnection provides a mock function with given fields: rctx, conn
func (_m *OutgoingOAuthConnectionInterface) RetrieveTokenForConnection(rctx request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnectionToken, *model.AppError) {
ret := _m.Called(rctx, conn)
var r0 *model.OutgoingOAuthConnectionToken
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(request.CTX, *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnectionToken, *model.AppError)); ok {
return rf(rctx, conn)
}
if rf, ok := ret.Get(0).(func(request.CTX, *model.OutgoingOAuthConnection) *model.OutgoingOAuthConnectionToken); ok {
r0 = rf(rctx, conn)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OutgoingOAuthConnectionToken)
}
}
if rf, ok := ret.Get(1).(func(request.CTX, *model.OutgoingOAuthConnection) *model.AppError); ok {
r1 = rf(rctx, conn)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// SanitizeConnection provides a mock function with given fields: conn
func (_m *OutgoingOAuthConnectionInterface) SanitizeConnection(conn *model.OutgoingOAuthConnection) {
_m.Called(conn)

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

@@ -17,4 +17,7 @@ type OutgoingOAuthConnectionInterface interface {
SanitizeConnection(conn *model.OutgoingOAuthConnection)
SanitizeConnections(conns []*model.OutgoingOAuthConnection)
GetConnectionForAudience(rctx request.CTX, url string) (*model.OutgoingOAuthConnection, *model.AppError)
RetrieveTokenForConnection(rctx request.CTX, conn *model.OutgoingOAuthConnection) (*model.OutgoingOAuthConnectionToken, *model.AppError)
}

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

@@ -42,4 +42,6 @@ import (
_ "github.com/mattermost/enterprise/license"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/ip_filtering"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/outgoing_oauth_connections"
)

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

@@ -1661,6 +1661,18 @@
"id": "api.context.mfa_required.app_error",
"translation": "Multi-factor authentication is required on this server."
},
{
"id": "api.context.outgoing_oauth_connection.create_connection.app_error",
"translation": "There was an error while creating the outgoing OAuth connection."
},
{
"id": "api.context.outgoing_oauth_connection.create_connection.input_error",
"translation": "Invalid input parameters."
},
{
"id": "api.context.outgoing_oauth_connection.delete_connection.app_error",
"translation": "There was an error while deleting the outgoing OAuth connection."
},
{
"id": "api.context.outgoing_oauth_connection.list_connections.app_error",
"translation": "There was an error while listing outgoing OAuth connections."
@@ -1670,8 +1682,24 @@
"translation": "Invalid input parameters."
},
{
"id": "api.context.outgoing_oauth_connection.not_available.feature_flag",
"translation": "This feature is restricted by a feature flag."
"id": "api.context.outgoing_oauth_connection.not_available.configuration_disabled",
"translation": "Outgoing OAuth connections are not available on this server."
},
{
"id": "api.context.outgoing_oauth_connection.update_connection.app_error",
"translation": "There was an error while updating the outgoing OAuth connection."
},
{
"id": "api.context.outgoing_oauth_connection.update_connection.input_error",
"translation": "Invalid input parameters."
},
{
"id": "api.context.outgoing_oauth_connection.validate_connection_credentials.app_error",
"translation": "There was an error while validating the outgoing OAuth connection credentials."
},
{
"id": "api.context.outgoing_oauth_connection.validate_connection_credentials.input_error",
"translation": "Couldn't retrieve credentials with the specified connection configuration."
},
{
"id": "api.context.permissions.app_error",
@@ -8302,10 +8330,26 @@
"id": "ent.migration.migratetosaml.username_already_used_by_other_user",
"translation": "Username already used by another Mattermost user."
},
{
"id": "ent.outgoing_oauth_connections.authenticate.app_error",
"translation": "There was an error while authenticating the outgoing oauth connection: {{ .Error }}"
},
{
"id": "ent.outgoing_oauth_connections.connection_matching_audience_exists.app_error",
"translation": "There is already an outgoing oauth connection for the provided audience."
},
{
"id": "ent.outgoing_oauth_connections.connection_matching_audience_exists.not_found",
"translation": "There is no outgoing oauth connection for the provided audience."
},
{
"id": "ent.outgoing_oauth_connections.delete_connection.app_error",
"translation": "There was an error while deleting the outgoing oauth connection."
},
{
"id": "ent.outgoing_oauth_connections.feature_disabled",
"translation": "Outgoing OAuth connections are not available on this server."
},
{
"id": "ent.outgoing_oauth_connections.get_connection.app_error",
"translation": "There was an error retrieving the outgoing oauth connection."
@@ -8314,17 +8358,45 @@
"id": "ent.outgoing_oauth_connections.get_connection.not_found.app_error",
"translation": "The outgoing oauth connection was not found."
},
{
"id": "ent.outgoing_oauth_connections.get_connection_for_audience.app_error",
"translation": "There was an error retrieving the outgoing oauth connection for the audience."
},
{
"id": "ent.outgoing_oauth_connections.get_connection_for_audience.not_found.app_error",
"translation": "The outgoing oauth connection for the provided audience was not found."
},
{
"id": "ent.outgoing_oauth_connections.get_connections.app_error",
"translation": "There was an error retrieving the outgoing oauth connections."
},
{
"id": "ent.outgoing_oauth_connections.license_disable.app_error",
"translation": "Your license does not support outgoing oauth connections."
},
{
"id": "ent.outgoing_oauth_connections.save_connection.app_error",
"translation": "There was an error saving the outgoing oauth connection."
"translation": "There was an error saving the outgoing oauth connection: {{ .Error }}"
},
{
"id": "ent.outgoing_oauth_connections.save_connection.audience_duplicated",
"translation": "There is already an outgoing oauth connection for the provided audience: {{ .Audience }}"
},
{
"id": "ent.outgoing_oauth_connections.save_connection.audience_invalid",
"translation": "The provided audience is invalid: {{ .Error }}"
},
{
"id": "ent.outgoing_oauth_connections.update_connection.app_error",
"translation": "There was an error updating the outgoing oauth connection."
"translation": "There was an error updating the outgoing oauth connection: {{ .Error }}"
},
{
"id": "ent.outgoing_oauth_connections.update_connection.audience_duplicated",
"translation": "There is already an outgoing oauth connection for the provided audience: {{ .Audience }}"
},
{
"id": "ent.outgoing_oauth_connections.update_connection.audience_invalid",
"translation": "The provided audience is invalid: {{ .Error }}"
},
{
"id": "ent.saml.attribute.app_error",
@@ -9668,7 +9740,7 @@
},
{
"id": "model.outgoing_oauth_connection.is_valid.audience.error",
"translation": "Some audience URL is incorrect."
"translation": "Audience URL is invalid: {{ .Url }}"
},
{
"id": "model.outgoing_oauth_connection.is_valid.client_id.error",

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

@@ -410,6 +410,7 @@ func (ts *TelemetryService) trackConfig() {
"enable_insecure_outgoing_connections": *cfg.ServiceSettings.EnableInsecureOutgoingConnections,
"enable_incoming_webhooks": cfg.ServiceSettings.EnableIncomingWebhooks,
"enable_outgoing_webhooks": cfg.ServiceSettings.EnableOutgoingWebhooks,
"enable_outgoing_oauth_connections": cfg.ServiceSettings.EnableOutgoingOAuthConnections,
"enable_commands": *cfg.ServiceSettings.EnableCommands,
"outgoing_integrations_requests_timeout": cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout,
"enable_post_username_override": cfg.ServiceSettings.EnablePostUsernameOverride,

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

@@ -485,7 +485,7 @@ func (c *Client4) outgoingOAuthConnectionsRoute() string {
}
func (c *Client4) outgoingOAuthConnectionRoute(id string) string {
return fmt.Sprintf("/oauth/outgoing_connections/%s", id)
return fmt.Sprintf("%s/%s", c.outgoingOAuthConnectionsRoute(), id)
}
func (c *Client4) jobsRoute() string {
@@ -6023,8 +6023,8 @@ func (c *Client4) GetOAuthAccessToken(ctx context.Context, data url.Values) (*Ac
// OutgoingOAuthConnection section
// GetOutgoingOAuthConnections retrieves the outgoing OAuth connections.
func (c *Client4) GetOutgoingOAuthConnections(ctx context.Context, fromID string, limit int) ([]*OutgoingOAuthConnection, *Response, error) {
r, err := c.DoAPIGet(ctx, c.outgoingOAuthConnectionsRoute(), "")
func (c *Client4) GetOutgoingOAuthConnections(ctx context.Context, filters OutgoingOAuthConnectionGetConnectionsFilter) ([]*OutgoingOAuthConnection, *Response, error) {
r, err := c.DoAPIGet(ctx, c.outgoingOAuthConnectionsRoute()+"?"+filters.ToURLValues().Encode(), "")
if err != nil {
return nil, BuildResponse(r), err
}
@@ -6050,6 +6050,53 @@ func (c *Client4) GetOutgoingOAuthConnection(ctx context.Context, id string) (*O
return connection, BuildResponse(r), nil
}
// DeleteOutgoingOAuthConnection deletes the outgoing OAuth connection with the given ID.
func (c *Client4) DeleteOutgoingOAuthConnection(ctx context.Context, id string) (*Response, error) {
r, err := c.DoAPIDelete(ctx, c.outgoingOAuthConnectionRoute(id))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UpdateOutgoingOAuthConnection updates the outgoing OAuth connection with the given ID.
func (c *Client4) UpdateOutgoingOAuthConnection(ctx context.Context, connection *OutgoingOAuthConnection) (*OutgoingOAuthConnection, *Response, error) {
buf, err := json.Marshal(connection)
if err != nil {
return nil, nil, NewAppError("UpdateOutgoingOAuthConnection", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(ctx, c.outgoingOAuthConnectionRoute(connection.Id), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var resultConnection OutgoingOAuthConnection
if err := json.NewDecoder(r.Body).Decode(&resultConnection); err != nil {
return nil, nil, NewAppError("UpdateOutgoingOAuthConnection", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &resultConnection, BuildResponse(r), nil
}
// CreateOutgoingOAuthConnection creates a new outgoing OAuth connection.
func (c *Client4) CreateOutgoingOAuthConnection(ctx context.Context, connection *OutgoingOAuthConnection) (*OutgoingOAuthConnection, *Response, error) {
buf, err := json.Marshal(connection)
if err != nil {
return nil, nil, NewAppError("CreateOutgoingOAuthConnection", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(ctx, c.outgoingOAuthConnectionsRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var resultConnection OutgoingOAuthConnection
if err := json.NewDecoder(r.Body).Decode(&resultConnection); err != nil {
return nil, nil, NewAppError("CreateOutgoingOAuthConnection", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &resultConnection, BuildResponse(r), nil
}
// Elasticsearch Section
// TestElasticsearch will attempt to connect to the configured Elasticsearch server and return OK if configured.

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

@@ -312,6 +312,7 @@ type ServiceSettings struct {
EnableOAuthServiceProvider *bool `access:"integrations_integration_management"`
EnableIncomingWebhooks *bool `access:"integrations_integration_management"`
EnableOutgoingWebhooks *bool `access:"integrations_integration_management"`
EnableOutgoingOAuthConnections *bool `access:"integrations_integration_management"`
EnableCommands *bool `access:"integrations_integration_management"`
OutgoingIntegrationRequestsTimeout *int64 `access:"integrations_integration_management"` // In seconds.
EnablePostUsernameOverride *bool `access:"integrations_integration_management"`
@@ -515,6 +516,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
s.EnableOutgoingWebhooks = NewBool(true)
}
if s.EnableOutgoingOAuthConnections == nil {
s.EnableOutgoingOAuthConnections = NewBool(false)
}
if s.OutgoingIntegrationRequestsTimeout == nil {
s.OutgoingIntegrationRequestsTimeout = NewInt64(OutgoingIntegrationRequestsDefaultTimeout)
}

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

@@ -50,8 +50,6 @@ type FeatureFlags struct {
ConsumePostHook bool
CloudAnnualRenewals bool
OutgoingOAuthConnections bool
}
func (f *FeatureFlags) SetDefaults() {
@@ -71,7 +69,6 @@ func (f *FeatureFlags) SetDefaults() {
f.CloudIPFiltering = false
f.ConsumePostHook = false
f.CloudAnnualRenewals = false
f.OutgoingOAuthConnections = false
}
// ToMap returns the feature flags as a map[string]string

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

@@ -46,4 +46,5 @@ const (
MigrationKeyDeleteEmptyDrafts = "delete_empty_drafts_migration"
MigrationKeyDeleteOrphanDrafts = "delete_orphan_drafts_migration"
MigrationKeyAddIPFilteringPermissions = "add_ip_filtering_permissions"
MigrationKeyAddOutgoingOAuthConnectionsPermissions = "add_outgoing_oauth_connections_permissions"
)

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

@@ -4,7 +4,9 @@
package model
import (
"fmt"
"net/http"
"net/url"
"unicode/utf8"
)
@@ -49,12 +51,42 @@ func (oa *OutgoingOAuthConnection) Auditable() map[string]interface{} {
// Sanitize removes any sensitive fields from the OutgoingOAuthConnection object.
func (oa *OutgoingOAuthConnection) Sanitize() {
oa.ClientId = ""
oa.ClientSecret = ""
oa.CredentialsUsername = nil
oa.CredentialsPassword = nil
}
// Patch updates the OutgoingOAuthConnection object with the non-empty fields from the given connection.
func (oa *OutgoingOAuthConnection) Patch(conn *OutgoingOAuthConnection) {
if conn == nil {
return
}
if conn.Name != "" {
oa.Name = conn.Name
}
if conn.ClientId != "" {
oa.ClientId = conn.ClientId
}
if conn.ClientSecret != "" {
oa.ClientSecret = conn.ClientSecret
}
if conn.OAuthTokenURL != "" {
oa.OAuthTokenURL = conn.OAuthTokenURL
}
if conn.GrantType != "" {
oa.GrantType = conn.GrantType
}
if len(conn.Audiences) > 0 {
oa.Audiences = conn.Audiences
}
if conn.CredentialsUsername != nil {
oa.CredentialsUsername = conn.CredentialsUsername
}
if conn.CredentialsPassword != nil {
oa.CredentialsPassword = conn.CredentialsPassword
}
}
// IsValid validates the object and returns an error if it isn't properly configured
func (oa *OutgoingOAuthConnection) IsValid() *AppError {
if !IsValidId(oa.Id) {
@@ -85,11 +117,11 @@ func (oa *OutgoingOAuthConnection) IsValid() *AppError {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.client_secret.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
if oa.OAuthTokenURL == "" || utf8.RuneCountInString(oa.OAuthTokenURL) > 256 {
if !IsValidHTTPURL(oa.OAuthTokenURL) || 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 {
if err := oa.HasValidGrantType(); err != nil {
return err
}
@@ -100,7 +132,7 @@ func (oa *OutgoingOAuthConnection) IsValid() *AppError {
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 NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.audience.error", map[string]any{"Url": audience}, "id="+oa.Id, http.StatusBadRequest)
}
}
}
@@ -108,8 +140,8 @@ func (oa *OutgoingOAuthConnection) IsValid() *AppError {
return nil
}
// IsValidGrantType validates the grant type and its parameters returning an error if it isn't properly configured
func (oa *OutgoingOAuthConnection) IsValidGrantType() *AppError {
// HasValidGrantType validates the grant type and its parameters returning an error if it isn't properly configured
func (oa *OutgoingOAuthConnection) HasValidGrantType() *AppError {
if !oa.GrantType.IsValid() {
return NewAppError("OutgoingOAuthConnection.IsValid", "model.outgoing_oauth_connection.is_valid.grant_type.error", nil, "id="+oa.Id, http.StatusBadRequest)
}
@@ -149,6 +181,12 @@ func (oa *OutgoingOAuthConnection) Etag() string {
type OutgoingOAuthConnectionGetConnectionsFilter struct {
OffsetId string
Limit int
Audience string
// TeamId is not used as a filter but as a way to check if the current user has permission to
// access the outgoing oauth connection for the given team in order to use them in the slash
// commands and outgoing webhooks.
TeamId string
}
// SetDefaults sets the default values for the filter
@@ -157,3 +195,36 @@ func (oaf *OutgoingOAuthConnectionGetConnectionsFilter) SetDefaults() {
oaf.Limit = defaultGetConnectionsLimit
}
}
// ToURLValues converts the filter to url.Values
func (oaf *OutgoingOAuthConnectionGetConnectionsFilter) ToURLValues() url.Values {
v := url.Values{}
if oaf.Limit > 0 {
v.Set("limit", fmt.Sprintf("%d", oaf.Limit))
}
if oaf.OffsetId != "" {
v.Set("offset_id", oaf.OffsetId)
}
if oaf.Audience != "" {
v.Set("audience", oaf.Audience)
}
if oaf.TeamId != "" {
v.Set("team_id", oaf.TeamId)
}
return v
}
// OutgoingOAuthConnectionToken is used to return the token for an outgoing connection oauth
// authentication request
type OutgoingOAuthConnectionToken struct {
AccessToken string
TokenType string
}
func (ooct *OutgoingOAuthConnectionToken) AsHeaderValue() string {
return ooct.TokenType + " " + ooct.AccessToken
}

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

@@ -13,16 +13,18 @@ var (
func newValidOutgoingOAuthConnection() *OutgoingOAuthConnection {
return &OutgoingOAuthConnection{
Id: NewId(),
CreatorId: NewId(),
Name: "Test Connection",
ClientId: NewId(),
ClientSecret: NewId(),
OAuthTokenURL: "https://nowhere.com/oauth/token",
GrantType: OutgoingOAuthConnectionGrantTypeClientCredentials,
CreateAt: GetMillis(),
UpdateAt: GetMillis(),
Audiences: []string{"https://nowhere.com"},
Id: NewId(),
CreatorId: NewId(),
Name: "Test Connection",
ClientId: NewId(),
ClientSecret: NewId(),
CredentialsUsername: NewString(NewId()),
CredentialsPassword: NewString(NewId()),
OAuthTokenURL: "https://nowhere.com/oauth/token",
GrantType: OutgoingOAuthConnectionGrantTypeClientCredentials,
CreateAt: GetMillis(),
UpdateAt: GetMillis(),
Audiences: []string{"https://nowhere.com"},
}
}
@@ -318,8 +320,66 @@ func TestOutgoingOAuthConnectionSanitize(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.Sanitize()
require.Empty(t, oa.ClientId)
require.NotEmpty(t, oa.ClientId)
require.Empty(t, oa.ClientSecret)
require.Empty(t, oa.CredentialsUsername)
require.NotEmpty(t, oa.CredentialsUsername)
require.Empty(t, oa.CredentialsPassword)
}
func TestOutgoingOAuthConnectionPatch(t *testing.T) {
t.Run("name", func(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.Patch(&OutgoingOAuthConnection{Name: "new name"})
require.Equal(t, "new name", oa.Name)
})
t.Run("client_id", func(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.Patch(&OutgoingOAuthConnection{ClientId: "new client id"})
require.Equal(t, "new client id", oa.ClientId)
})
t.Run("client_secret", func(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.Patch(&OutgoingOAuthConnection{ClientSecret: "new client secret"})
require.Equal(t, "new client secret", oa.ClientSecret)
})
t.Run("oauth_token_url", func(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.Patch(&OutgoingOAuthConnection{OAuthTokenURL: "new oauth token url"})
require.Equal(t, "new oauth token url", oa.OAuthTokenURL)
})
t.Run("grant_type", func(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.Patch(&OutgoingOAuthConnection{GrantType: OutgoingOAuthConnectionGrantTypePassword})
require.Equal(t, OutgoingOAuthConnectionGrantTypePassword, oa.GrantType)
})
t.Run("audiences", func(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.Patch(&OutgoingOAuthConnection{Audiences: StringArray{"new audience"}})
require.Equal(t, StringArray{"new audience"}, oa.Audiences)
})
t.Run("credentials_username", func(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.Patch(&OutgoingOAuthConnection{CredentialsUsername: &someString})
require.Equal(t, &someString, oa.CredentialsUsername)
})
t.Run("credentials_password", func(t *testing.T) {
oa := newValidOutgoingOAuthConnection()
oa.Patch(&OutgoingOAuthConnection{CredentialsPassword: &someString})
require.Equal(t, &someString, oa.CredentialsPassword)
})
}

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

@@ -388,6 +388,8 @@ var ChannelModeratedPermissionsMap map[string]string
var SysconsoleReadPermissions []*Permission
var SysconsoleWritePermissions []*Permission
var PermissionManageOutgoingOAuthConnections *Permission
func initializePermissions() {
PermissionInviteUser = &Permission{
"invite_user",
@@ -2125,6 +2127,13 @@ func initializePermissions() {
PermissionScopeSystem,
}
PermissionManageOutgoingOAuthConnections = &Permission{
"manage_outgoing_oauth_connections",
"authentication.permissions.manage_outgoing_oauth_connections.name",
"authentication.permissions.manage_outgoing_oauth_connections.description",
PermissionScopeSystem,
}
SysconsoleReadPermissions = []*Permission{
PermissionSysconsoleReadAboutEditionAndLicense,
PermissionSysconsoleReadBilling,
@@ -2317,6 +2326,7 @@ func initializePermissions() {
PermissionReadLicenseInformation,
PermissionManageLicenseInformation,
PermissionCreateCustomGroup,
PermissionManageOutgoingOAuthConnections,
}
TeamScopedPermissions := []*Permission{

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

@@ -342,6 +342,7 @@ func init() {
PermissionSysconsoleWriteIntegrationsCors.Id,
PermissionSysconsoleReadProductsBoards.Id,
PermissionSysconsoleWriteProductsBoards.Id,
PermissionManageOutgoingOAuthConnections.Id,
}
SystemCustomGroupAdminDefaultPermissions = []string{