-
Notifications
You must be signed in to change notification settings - Fork 117
Expand file tree
/
Copy pathmiddleware.go
More file actions
206 lines (186 loc) · 7.12 KB
/
middleware.go
File metadata and controls
206 lines (186 loc) · 7.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package api
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/mcpjungle/mcpjungle/internal/model"
"github.com/mcpjungle/mcpjungle/pkg/types"
)
// requireInitialized is middleware to reject requests to certain routes if the server is not initialized
func (s *Server) requireInitialized() gin.HandlerFunc {
return func(c *gin.Context) {
cfg, err := s.configService.GetConfig()
if err != nil || !cfg.Initialized {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "server is not initialized"})
return
}
// propagate the server mode in context for other middleware/handlers to use
c.Set("mode", cfg.Mode)
c.Next()
}
}
// verifyUserAuthForAPIAccess is middleware that checks for a valid user token if the server is in enterprise mode.
// this middleware doesn't care about the role of the user, it just verifies that they're authenticated.
func (s *Server) verifyUserAuthForAPIAccess() gin.HandlerFunc {
return func(c *gin.Context) {
mode, exists := c.Get("mode")
if !exists {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "server mode not found in context"})
return
}
m, ok := mode.(model.ServerMode)
if !ok {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "invalid server mode in context"})
return
}
if m == model.ModeDev {
// no auth is required in case of dev mode
c.Next()
return
}
authHeader := c.GetHeader("Authorization")
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing access token"})
return
}
// Verify that the token is valid and corresponds to a user
authenticatedUser, err := s.userService.GetUserByAccessToken(token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid access token: " + err.Error()})
return
}
// Store user in context for potential role checks in subsequent handlers
c.Set("user", authenticatedUser)
c.Next()
}
}
// requireAdminUser is middleware that ensures the authenticated user has an admin role when in enterprise mode.
// It assumes that verifyUserAuthForAPIAccess middleware has already run and set the user in context.
func (s *Server) requireAdminUser() gin.HandlerFunc {
return func(c *gin.Context) {
mode, exists := c.Get("mode")
if !exists {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "server mode not found in context"})
return
}
m, ok := mode.(model.ServerMode)
if !ok {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "invalid server mode in context"})
return
}
if m == model.ModeDev {
// no admin check is required in dev mode
c.Next()
return
}
authenticatedUser, exists := c.Get("user")
if !exists {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user is not authenticated"})
return
}
u, ok := authenticatedUser.(*model.User)
if ok && u.Role == types.UserRoleAdmin {
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "user is not authorized to perform this action"})
}
}
// requireServerMode is middleware that checks if the server is in a specific mode.
// If not, the request is rejected with a 403 Forbidden status.
// This is useful for routes that should only be accessible in certain modes (e.g., enterprise-only features).
// NOTE: ModeProd is supported for backwards compatibility, it is equivalent to ModeEnterprise.
func (s *Server) requireServerMode(m model.ServerMode) gin.HandlerFunc {
return func(c *gin.Context) {
mode, exists := c.Get("mode")
if !exists {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "server mode not found in context"})
return
}
currentMode, ok := mode.(model.ServerMode)
if !ok {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "invalid server mode in context"})
return
}
if currentMode == m {
// current mode matches the required mode, allow access
c.Next()
return
}
if model.IsEnterpriseMode(currentMode) && model.IsEnterpriseMode(m) {
// both current and required modes are enterprise modes, allow access
c.Next()
return
}
// current mode does not match the required mode, reject the request
c.AbortWithStatusJSON(
http.StatusForbidden,
gin.H{"error": fmt.Sprintf("this request is only allowed in %s mode", m)},
)
}
}
// checkAuthForMcpProxyAccess is middleware for MCP proxy that checks for a valid MCP client token
// if the server is in enterprise mode.
// In development mode, mcp clients do not require auth to access the MCP proxy.
func (s *Server) checkAuthForMcpProxyAccess() gin.HandlerFunc {
return func(c *gin.Context) {
mode, exists := c.Get("mode")
if !exists {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "server mode not found in context"})
return
}
m, ok := mode.(model.ServerMode)
if !ok {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "invalid server mode in context"})
return
}
// the gin context doesn't get passed down to the MCP proxy server, so we need to
// set values in the underlying request's context to be able to access them from proxy.
ctx := context.WithValue(c.Request.Context(), "mode", m)
c.Request = c.Request.WithContext(ctx)
if m == model.ModeDev {
// no auth is required in case of dev mode
c.Next()
return
}
authHeader := c.GetHeader("Authorization")
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing MCP client access token"})
return
}
// First, check if it's a static MCP client token (legacy authentication method)
// Static tokens are created via `mcpjungle create mcp-client` and are long-lived.
client, err := s.mcpClientService.GetClientByToken(token)
if err == nil {
// inject the authenticated MCP client in context for the proxy to use
ctx = context.WithValue(c.Request.Context(), "client", client)
c.Request = c.Request.WithContext(ctx)
c.Next()
return
}
// Second, check if it's an OAuth token issued by mcpjungle (OAuth 2.0 flow)
// OAuth tokens are issued via the /oauth/token endpoint after user authorization.
// They are tied to a specific MCPJungle user and have expiration times.
oauthToken, err := s.oauthService.GetTokenByValue(token)
if err == nil {
// Create a "virtual" McpClient for the OAuth-authenticated user.
// This allows us to reuse the existing proxy authorization logic without major changes.
// The virtual client represents the user's OAuth session.
// TODO: Implement proper User -> Server ACLs instead of wildcard access.
virtualClient := &model.McpClient{
Name: "oauth-user-" + fmt.Sprint(oauthToken.UserID),
AccessToken: oauthToken.Token,
AllowList: []byte("[\"*\"]"), // Wildcard access for authenticated users for now
}
ctx = context.WithValue(c.Request.Context(), "client", virtualClient)
c.Request = c.Request.WithContext(ctx)
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid access token"})
}
}