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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
3f6c94cfc3
Коммит
4e071e861c
@@ -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{
|
||||
|
||||
Ссылка в новой задаче
Block a user