forked from casdoor/casdoor
Compare commits
3 Commits
copilot/im
...
v2.320.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1731b74fa0 | ||
|
|
6e1e5dd569 | ||
|
|
b183359daf |
56
mcp/auth.go
56
mcp/auth.go
@@ -15,6 +15,7 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -120,3 +121,58 @@ func (c *McpController) GetAcceptLanguage() string {
|
||||
}
|
||||
return language
|
||||
}
|
||||
|
||||
// GetTokenFromRequest extracts the Bearer token from the Authorization header
|
||||
func (c *McpController) GetTokenFromRequest() string {
|
||||
authHeader := c.Ctx.Request.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Extract Bearer token
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
|
||||
return ""
|
||||
}
|
||||
|
||||
return parts[1]
|
||||
}
|
||||
|
||||
// GetClaimsFromToken parses and validates the JWT token and returns the claims
|
||||
// Returns nil if no token is present or if token is invalid
|
||||
func (c *McpController) GetClaimsFromToken() *object.Claims {
|
||||
tokenString := c.GetTokenFromRequest()
|
||||
if tokenString == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to find the application for this token
|
||||
// For MCP, we'll try to parse using the first available application's certificate
|
||||
// In a production scenario, you might want to use a specific MCP application
|
||||
token, err := object.GetTokenByAccessToken(tokenString)
|
||||
if err != nil || token == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
application, err := object.GetApplication(token.Application)
|
||||
if err != nil || application == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
claims, err := object.ParseJwtTokenByApplication(tokenString, application)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return claims
|
||||
}
|
||||
|
||||
// GetScopesFromClaims extracts the scopes from JWT claims and returns them as a slice
|
||||
func GetScopesFromClaims(claims *object.Claims) []string {
|
||||
if claims == nil || claims.Scope == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Scopes are space-separated in OAuth 2.0
|
||||
return strings.Split(claims.Scope, " ")
|
||||
}
|
||||
|
||||
211
mcp/base.go
211
mcp/base.go
@@ -268,7 +268,160 @@ func (c *McpController) handlePing(req McpRequest) {
|
||||
}
|
||||
|
||||
func (c *McpController) handleToolsList(req McpRequest) {
|
||||
tools := []McpTool{
|
||||
allTools := c.getAllTools()
|
||||
|
||||
// Get JWT claims from the request
|
||||
claims := c.GetClaimsFromToken()
|
||||
|
||||
// If no token is present, check session authentication
|
||||
if claims == nil {
|
||||
username := c.GetSessionUsername()
|
||||
// If user is authenticated via session, return all tools (backward compatibility)
|
||||
if username != "" {
|
||||
result := McpListToolsResult{
|
||||
Tools: allTools,
|
||||
}
|
||||
c.McpResponseOk(req.ID, result)
|
||||
return
|
||||
}
|
||||
|
||||
// Unauthenticated request - return all tools for discovery
|
||||
// This allows clients to see what tools are available before authenticating
|
||||
result := McpListToolsResult{
|
||||
Tools: allTools,
|
||||
}
|
||||
c.McpResponseOk(req.ID, result)
|
||||
return
|
||||
}
|
||||
|
||||
// Token-based authentication - filter tools by scopes
|
||||
grantedScopes := GetScopesFromClaims(claims)
|
||||
allowedTools := GetToolsForScopes(grantedScopes, BuiltinScopes)
|
||||
|
||||
// Filter tools based on allowed scopes
|
||||
var filteredTools []McpTool
|
||||
for _, tool := range allTools {
|
||||
if allowedTools[tool.Name] {
|
||||
filteredTools = append(filteredTools, tool)
|
||||
}
|
||||
}
|
||||
|
||||
result := McpListToolsResult{
|
||||
Tools: filteredTools,
|
||||
}
|
||||
|
||||
c.McpResponseOk(req.ID, result)
|
||||
}
|
||||
|
||||
func (c *McpController) handleToolsCall(req McpRequest) {
|
||||
var params McpCallToolParams
|
||||
err := json.Unmarshal(req.Params, ¶ms)
|
||||
if err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check scope-tool permission
|
||||
if !c.checkToolPermission(req.ID, params.Name) {
|
||||
return // Error already sent by checkToolPermission
|
||||
}
|
||||
|
||||
// Route to the appropriate tool handler
|
||||
switch params.Name {
|
||||
case "get_applications":
|
||||
var args GetApplicationsArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleGetApplicationsTool(req.ID, args)
|
||||
case "get_application":
|
||||
var args GetApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleGetApplicationTool(req.ID, args)
|
||||
case "add_application":
|
||||
var args AddApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleAddApplicationTool(req.ID, args)
|
||||
case "update_application":
|
||||
var args UpdateApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleUpdateApplicationTool(req.ID, args)
|
||||
case "delete_application":
|
||||
var args DeleteApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleDeleteApplicationTool(req.ID, args)
|
||||
default:
|
||||
c.McpResponseError(req.ID, -32602, "Invalid tool name", fmt.Sprintf("Tool '%s' not found", params.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// checkToolPermission validates that the current token has the required scope for the tool
|
||||
// Returns false and sends an error response if permission is denied
|
||||
func (c *McpController) checkToolPermission(id interface{}, toolName string) bool {
|
||||
// Get JWT claims from the request
|
||||
claims := c.GetClaimsFromToken()
|
||||
|
||||
// If no token is present, check if the user is authenticated via session
|
||||
if claims == nil {
|
||||
username := c.GetSessionUsername()
|
||||
// If user is authenticated via session (e.g., session cookie), allow access
|
||||
// This maintains backward compatibility with existing session-based auth
|
||||
if username != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// No authentication present - deny access
|
||||
c.sendInsufficientScopeError(id, toolName, []string{})
|
||||
return false
|
||||
}
|
||||
|
||||
// Extract scopes from claims
|
||||
grantedScopes := GetScopesFromClaims(claims)
|
||||
|
||||
// Get allowed tools for the granted scopes
|
||||
allowedTools := GetToolsForScopes(grantedScopes, BuiltinScopes)
|
||||
|
||||
// Check if the requested tool is allowed
|
||||
if !allowedTools[toolName] {
|
||||
c.sendInsufficientScopeError(id, toolName, grantedScopes)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// sendInsufficientScopeError sends an error response for insufficient scope
|
||||
func (c *McpController) sendInsufficientScopeError(id interface{}, toolName string, grantedScopes []string) {
|
||||
// Find required scope for this tool
|
||||
requiredScope := GetRequiredScopeForTool(toolName, BuiltinScopes)
|
||||
|
||||
errorData := map[string]interface{}{
|
||||
"tool": toolName,
|
||||
"granted_scopes": grantedScopes,
|
||||
}
|
||||
if requiredScope != "" {
|
||||
errorData["required_scope"] = requiredScope
|
||||
}
|
||||
|
||||
c.McpResponseError(id, -32001, "insufficient_scope", errorData)
|
||||
}
|
||||
|
||||
// getAllTools returns all available MCP tools
|
||||
func (c *McpController) getAllTools() []McpTool {
|
||||
return []McpTool{
|
||||
{
|
||||
Name: "get_applications",
|
||||
Description: "Get all applications for a specific owner",
|
||||
@@ -344,60 +497,4 @@ func (c *McpController) handleToolsList(req McpRequest) {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := McpListToolsResult{
|
||||
Tools: tools,
|
||||
}
|
||||
|
||||
c.McpResponseOk(req.ID, result)
|
||||
}
|
||||
|
||||
func (c *McpController) handleToolsCall(req McpRequest) {
|
||||
var params McpCallToolParams
|
||||
err := json.Unmarshal(req.Params, ¶ms)
|
||||
if err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Route to the appropriate tool handler
|
||||
switch params.Name {
|
||||
case "get_applications":
|
||||
var args GetApplicationsArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleGetApplicationsTool(req.ID, args)
|
||||
case "get_application":
|
||||
var args GetApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleGetApplicationTool(req.ID, args)
|
||||
case "add_application":
|
||||
var args AddApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleAddApplicationTool(req.ID, args)
|
||||
case "update_application":
|
||||
var args UpdateApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleUpdateApplicationTool(req.ID, args)
|
||||
case "delete_application":
|
||||
var args DeleteApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleDeleteApplicationTool(req.ID, args)
|
||||
default:
|
||||
c.McpResponseError(req.ID, -32602, "Invalid tool name", fmt.Sprintf("Tool '%s' not found", params.Name))
|
||||
}
|
||||
}
|
||||
|
||||
158
mcp/permission.go
Normal file
158
mcp/permission.go
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
// BuiltinScopes defines the default scope-to-tool mappings for Casdoor's MCP server
|
||||
var BuiltinScopes = []*object.ScopeItem{
|
||||
{
|
||||
Name: "application:read",
|
||||
DisplayName: "Read Applications",
|
||||
Description: "View application list and details",
|
||||
Tools: []string{"get_applications", "get_application"},
|
||||
},
|
||||
{
|
||||
Name: "application:write",
|
||||
DisplayName: "Manage Applications",
|
||||
Description: "Create, update, and delete applications",
|
||||
Tools: []string{"add_application", "update_application", "delete_application"},
|
||||
},
|
||||
{
|
||||
Name: "user:read",
|
||||
DisplayName: "Read Users",
|
||||
Description: "View user list and details",
|
||||
Tools: []string{"get_users", "get_user"},
|
||||
},
|
||||
{
|
||||
Name: "user:write",
|
||||
DisplayName: "Manage Users",
|
||||
Description: "Create, update, and delete users",
|
||||
Tools: []string{"add_user", "update_user", "delete_user"},
|
||||
},
|
||||
{
|
||||
Name: "organization:read",
|
||||
DisplayName: "Read Organizations",
|
||||
Description: "View organization list and details",
|
||||
Tools: []string{"get_organizations", "get_organization"},
|
||||
},
|
||||
{
|
||||
Name: "organization:write",
|
||||
DisplayName: "Manage Organizations",
|
||||
Description: "Create, update, and delete organizations",
|
||||
Tools: []string{"add_organization", "update_organization", "delete_organization"},
|
||||
},
|
||||
{
|
||||
Name: "permission:read",
|
||||
DisplayName: "Read Permissions",
|
||||
Description: "View permission list and details",
|
||||
Tools: []string{"get_permissions", "get_permission"},
|
||||
},
|
||||
{
|
||||
Name: "permission:write",
|
||||
DisplayName: "Manage Permissions",
|
||||
Description: "Create, update, and delete permissions",
|
||||
Tools: []string{"add_permission", "update_permission", "delete_permission"},
|
||||
},
|
||||
{
|
||||
Name: "role:read",
|
||||
DisplayName: "Read Roles",
|
||||
Description: "View role list and details",
|
||||
Tools: []string{"get_roles", "get_role"},
|
||||
},
|
||||
{
|
||||
Name: "role:write",
|
||||
DisplayName: "Manage Roles",
|
||||
Description: "Create, update, and delete roles",
|
||||
Tools: []string{"add_role", "update_role", "delete_role"},
|
||||
},
|
||||
{
|
||||
Name: "provider:read",
|
||||
DisplayName: "Read Providers",
|
||||
Description: "View provider list and details",
|
||||
Tools: []string{"get_providers", "get_provider"},
|
||||
},
|
||||
{
|
||||
Name: "provider:write",
|
||||
DisplayName: "Manage Providers",
|
||||
Description: "Create, update, and delete providers",
|
||||
Tools: []string{"add_provider", "update_provider", "delete_provider"},
|
||||
},
|
||||
{
|
||||
Name: "token:read",
|
||||
DisplayName: "Read Tokens",
|
||||
Description: "View token list and details",
|
||||
Tools: []string{"get_tokens", "get_token"},
|
||||
},
|
||||
{
|
||||
Name: "token:write",
|
||||
DisplayName: "Manage Tokens",
|
||||
Description: "Delete tokens",
|
||||
Tools: []string{"delete_token"},
|
||||
},
|
||||
}
|
||||
|
||||
// ConvenienceScopes defines alias scopes that expand to multiple resource scopes
|
||||
var ConvenienceScopes = map[string][]string{
|
||||
"read": {"application:read", "user:read", "organization:read", "permission:read", "role:read", "provider:read", "token:read"},
|
||||
"write": {"application:write", "user:write", "organization:write", "permission:write", "role:write", "provider:write", "token:write"},
|
||||
"admin": {"application:read", "application:write", "user:read", "user:write", "organization:read", "organization:write", "permission:read", "permission:write", "role:read", "role:write", "provider:read", "provider:write", "token:read", "token:write"},
|
||||
}
|
||||
|
||||
// GetToolsForScopes returns a map of tools allowed by the given scopes
|
||||
// The grantedScopes are the scopes present in the token
|
||||
// The registry contains the scope-to-tool mappings (either BuiltinScopes or Application.Scopes)
|
||||
func GetToolsForScopes(grantedScopes []string, registry []*object.ScopeItem) map[string]bool {
|
||||
allowed := make(map[string]bool)
|
||||
|
||||
// Expand convenience scopes first
|
||||
expandedScopes := make([]string, 0)
|
||||
for _, scopeName := range grantedScopes {
|
||||
if expansion, isConvenience := ConvenienceScopes[scopeName]; isConvenience {
|
||||
expandedScopes = append(expandedScopes, expansion...)
|
||||
} else {
|
||||
expandedScopes = append(expandedScopes, scopeName)
|
||||
}
|
||||
}
|
||||
|
||||
// Map scopes to tools
|
||||
for _, scopeName := range expandedScopes {
|
||||
for _, item := range registry {
|
||||
if item.Name == scopeName {
|
||||
for _, tool := range item.Tools {
|
||||
allowed[tool] = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allowed
|
||||
}
|
||||
|
||||
// GetRequiredScopeForTool returns the first scope that provides access to the given tool
|
||||
// Returns an empty string if no scope is found for the tool
|
||||
func GetRequiredScopeForTool(toolName string, registry []*object.ScopeItem) string {
|
||||
for _, scopeItem := range registry {
|
||||
for _, tool := range scopeItem.Tools {
|
||||
if tool == toolName {
|
||||
return scopeItem.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -68,9 +68,10 @@ type JwtItem struct {
|
||||
}
|
||||
|
||||
type ScopeItem struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Description string `json:"description"`
|
||||
Tools []string `json:"tools"` // MCP tools allowed by this scope
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
|
||||
@@ -303,7 +303,7 @@ func NotifyPayment(body []byte, owner string, paymentName string, lang string) (
|
||||
order.Message = "Payment successful"
|
||||
order.UpdateTime = util.GetCurrentTime()
|
||||
} else if payment.State == pp.PaymentStateError {
|
||||
order.State = "PaymentFailed"
|
||||
order.State = "Failed"
|
||||
order.Message = payment.Message
|
||||
order.UpdateTime = util.GetCurrentTime()
|
||||
} else if payment.State == pp.PaymentStateCanceled {
|
||||
|
||||
@@ -39,7 +39,7 @@ func (pp *DummyPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
|
||||
orderInfo := DummyOrderInfo{
|
||||
Price: r.Price,
|
||||
Currency: r.Currency,
|
||||
ProductDisplayName: r.ProductDisplayName,
|
||||
ProductDisplayName: "",
|
||||
}
|
||||
orderInfoBytes, err := json.Marshal(orderInfo)
|
||||
if err != nil {
|
||||
|
||||
@@ -236,6 +236,13 @@ class OrderListPage extends BaseListPage {
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("state"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Tooltip title={record.message || ""}>
|
||||
<span>{text}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
@@ -248,7 +255,7 @@ class OrderListPage extends BaseListPage {
|
||||
return (
|
||||
<div style={{display: "flex", flexWrap: "wrap", gap: "8px"}}>
|
||||
<Button onClick={() => this.props.history.push(`/orders/${record.owner}/${record.name}/pay`)}>
|
||||
{record.state === "Created" ? i18next.t("order:Pay") : i18next.t("general:Detail")}
|
||||
{(record.state === "Created" || record.state === "Failed") ? i18next.t("order:Pay") : i18next.t("general:Detail")}
|
||||
</Button>
|
||||
<Button danger onClick={() => this.cancelOrder(record)} disabled={record.state !== "Created" || !isAdmin}>
|
||||
{i18next.t("general:Cancel")}
|
||||
|
||||
@@ -272,7 +272,7 @@ class OrderPayPage extends React.Component {
|
||||
const updateTimeMap = {
|
||||
Paid: i18next.t("order:Payment time"),
|
||||
Canceled: i18next.t("order:Cancel time"),
|
||||
PaymentFailed: i18next.t("order:Payment failed time"),
|
||||
Failed: i18next.t("order:Payment failed time"),
|
||||
Timeout: i18next.t("order:Timeout time"),
|
||||
};
|
||||
const updateTimeLabel = updateTimeMap[state] || i18next.t("general:Updated time");
|
||||
|
||||
Reference in New Issue
Block a user