mattermost-community-enterp.../channels/api4/outgoing_oauth_connection.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
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>
2025-12-17 23:59:07 +09:00

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)
}