Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
415 lines
16 KiB
Go
415 lines
16 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package api4
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
|
|
"github.com/mattermost/logr/v2"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
|
)
|
|
|
|
const (
|
|
whereOutgoingOAuthConnection = "outgoingOAuthConnections"
|
|
)
|
|
|
|
func (api *API) InitOutgoingOAuthConnection() {
|
|
api.BaseRoutes.OutgoingOAuthConnections.Handle("", api.APISessionRequired(listOutgoingOAuthConnections)).Methods(http.MethodGet)
|
|
api.BaseRoutes.OutgoingOAuthConnections.Handle("", api.APISessionRequired(createOutgoingOAuthConnection)).Methods(http.MethodPost)
|
|
api.BaseRoutes.OutgoingOAuthConnection.Handle("", api.APISessionRequired(getOutgoingOAuthConnection)).Methods(http.MethodGet)
|
|
api.BaseRoutes.OutgoingOAuthConnection.Handle("", api.APISessionRequired(updateOutgoingOAuthConnection)).Methods(http.MethodPut)
|
|
api.BaseRoutes.OutgoingOAuthConnection.Handle("", api.APISessionRequired(deleteOutgoingOAuthConnection)).Methods(http.MethodDelete)
|
|
api.BaseRoutes.OutgoingOAuthConnections.Handle("/validate", api.APISessionRequired(validateOutgoingOAuthConnectionCredentials)).Methods(http.MethodPost)
|
|
}
|
|
|
|
// 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.PermissionManageOwnOutgoingWebhooks) ||
|
|
c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageOwnSlashCommands) {
|
|
return true
|
|
}
|
|
|
|
c.SetPermissionError(model.PermissionManageOwnOutgoingWebhooks, model.PermissionManageOwnSlashCommands)
|
|
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().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
|
|
}
|
|
|
|
if c.App.OutgoingOAuthConnections() == nil || !model.MinimumEnterpriseLicense(c.App.License()) {
|
|
c.Err = model.NewAppError(where, "api.license.upgrade_needed.app_error", nil, "", http.StatusNotImplemented)
|
|
return nil, false
|
|
}
|
|
return c.App.OutgoingOAuthConnections(), true
|
|
}
|
|
|
|
type listOutgoingOAuthConnectionsQuery struct {
|
|
FromID string
|
|
Limit int
|
|
Audience string
|
|
}
|
|
|
|
// SetDefaults sets the default values for the query.
|
|
func (q *listOutgoingOAuthConnectionsQuery) SetDefaults() {
|
|
// Set default values
|
|
if q.Limit == 0 {
|
|
q.Limit = 10
|
|
}
|
|
}
|
|
|
|
// IsValid validates the query.
|
|
func (q *listOutgoingOAuthConnectionsQuery) IsValid() error {
|
|
if q.Limit < 1 || q.Limit > 100 {
|
|
return fmt.Errorf("limit must be between 1 and 100")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ToFilter converts the query to a filter that can be used to query the database.
|
|
func (q *listOutgoingOAuthConnectionsQuery) ToFilter() model.OutgoingOAuthConnectionGetConnectionsFilter {
|
|
return model.OutgoingOAuthConnectionGetConnectionsFilter{
|
|
OffsetId: q.FromID,
|
|
Limit: q.Limit,
|
|
Audience: q.Audience,
|
|
}
|
|
}
|
|
|
|
func NewListOutgoingOAuthConnectionsQueryFromURLQuery(values url.Values) (*listOutgoingOAuthConnectionsQuery, error) {
|
|
query := &listOutgoingOAuthConnectionsQuery{}
|
|
query.SetDefaults()
|
|
|
|
fromID := values.Get("from_id")
|
|
if fromID != "" {
|
|
query.FromID = fromID
|
|
}
|
|
|
|
limit := values.Get("limit")
|
|
if limit != "" {
|
|
limitInt, err := strconv.Atoi(limit)
|
|
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
|
|
}
|
|
|
|
query, err := NewListOutgoingOAuthConnectionsQueryFromURLQuery(r.URL.Query())
|
|
if err != nil {
|
|
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.list_connections.input_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
return
|
|
}
|
|
|
|
if errValid := query.IsValid(); errValid != nil {
|
|
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.list_connections.input_error", nil, "", http.StatusBadRequest).Wrap(errValid)
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
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, "", http.StatusInternalServerError).Wrap(errList)
|
|
return
|
|
}
|
|
}
|
|
|
|
service.SanitizeConnections(connections)
|
|
|
|
if errJSON := json.NewEncoder(w).Encode(connections); errJSON != nil {
|
|
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.list_connections.app_error", nil, "", http.StatusInternalServerError).Wrap(errJSON)
|
|
return
|
|
}
|
|
}
|
|
|
|
func getOutgoingOAuthConnection(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if !checkOutgoingOAuthConnectionWritePermissions(c) {
|
|
return
|
|
}
|
|
|
|
service, ok := ensureOutgoingOAuthConnectionInterface(c, whereOutgoingOAuthConnection)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
c.RequireOutgoingOAuthConnectionId()
|
|
|
|
connection, err := service.GetConnection(c.AppContext, c.Params.OutgoingOAuthConnectionID)
|
|
if err != nil {
|
|
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.list_connections.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
|
|
service.SanitizeConnection(connection)
|
|
|
|
if err := json.NewEncoder(w).Encode(connection); err != nil {
|
|
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.list_connections.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func createOutgoingOAuthConnection(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
auditRec := c.MakeAuditRecord(model.AuditEventCreateOutgoingOauthConnection, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
c.LogAudit("attempt")
|
|
|
|
if !checkOutgoingOAuthConnectionWritePermissions(c) {
|
|
return
|
|
}
|
|
|
|
service, ok := ensureOutgoingOAuthConnectionInterface(c, whereOutgoingOAuthConnection)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var inputConnection model.OutgoingOAuthConnection
|
|
if err := json.NewDecoder(r.Body).Decode(&inputConnection); err != nil {
|
|
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.create_connection.input_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
return
|
|
}
|
|
|
|
model.AddEventParameterAuditableToAuditRec(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, "", http.StatusInternalServerError).Wrap(err)
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func updateOutgoingOAuthConnection(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
auditRec := c.MakeAuditRecord(model.AuditEventUpdateOutgoingOAuthConnection, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
model.AddEventParameterToAuditRec(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
|
|
if err := json.NewDecoder(r.Body).Decode(&inputConnection); err != nil {
|
|
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.update_connection.input_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func deleteOutgoingOAuthConnection(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
auditRec := c.MakeAuditRecord(model.AuditEventDeleteOutgoingOAuthConnection, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
model.AddEventParameterToAuditRec(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, "", http.StatusInternalServerError).Wrap(err)
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
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(model.AuditEventValidateOutgoingOAuthConnectionCredentials, model.AuditStatusFail)
|
|
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
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&inputConnection); err != nil {
|
|
c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.validate_connection_credentials.input_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
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, "", http.StatusInternalServerError).Wrap(err)
|
|
w.WriteHeader(c.Err.StatusCode)
|
|
return
|
|
}
|
|
|
|
inputConnection.ClientSecret = storedConnection.ClientSecret
|
|
}
|
|
|
|
model.AddEventParameterAuditableToAuditRec(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.StatusCode).Wrap(err)
|
|
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)
|
|
}
|