forked from casdoor/casdoor
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15646b23ff | ||
|
|
4b663a437f | ||
|
|
9fb90fbb95 | ||
|
|
65eeaef8a7 | ||
|
|
ecf8e2eb32 | ||
|
|
e49e678d16 | ||
|
|
623ee23285 | ||
|
|
0901a1d5a0 | ||
|
|
58ff2fe69c | ||
|
|
737f44a059 | ||
|
|
32cef8e828 | ||
|
|
9e854abc77 | ||
|
|
9b3343d3db | ||
|
|
5b71725c94 | ||
|
|
59b6854ccc | ||
|
|
0daf67c52c | ||
|
|
4b612269ea |
16
Dockerfile
16
Dockerfile
@@ -1,12 +1,24 @@
|
||||
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
|
||||
WORKDIR /web
|
||||
COPY ./web .
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000 && NODE_OPTIONS="--max-old-space-size=4096" yarn run build
|
||||
|
||||
# Copy only dependency files first for better caching
|
||||
COPY ./web/package.json ./web/yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000
|
||||
|
||||
# Copy source files and build
|
||||
COPY ./web .
|
||||
RUN NODE_OPTIONS="--max-old-space-size=4096" yarn run build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23.12 AS BACK
|
||||
WORKDIR /go/src/casdoor
|
||||
|
||||
# Copy only go.mod and go.sum first for dependency caching
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source files
|
||||
COPY . .
|
||||
|
||||
RUN ./build.sh
|
||||
RUN go test -v -run TestGetVersionInfo ./util/system_test.go ./util/system.go > version_info.txt
|
||||
|
||||
|
||||
@@ -67,6 +67,9 @@ p, *, *, POST, /api/upload-users, *, *
|
||||
p, *, *, GET, /api/get-resources, *, *
|
||||
p, *, *, GET, /api/get-records, *, *
|
||||
p, *, *, GET, /api/get-product, *, *
|
||||
p, *, *, GET, /api/get-order, *, *
|
||||
p, *, *, GET, /api/get-orders, *, *
|
||||
p, *, *, GET, /api/get-user-orders, *, *
|
||||
p, *, *, GET, /api/get-payment, *, *
|
||||
p, *, *, POST, /api/update-payment, *, *
|
||||
p, *, *, POST, /api/invoice-payment, *, *
|
||||
|
||||
@@ -467,15 +467,17 @@ func (c *ApiController) SsoLogout() {
|
||||
var tokens []*object.Token
|
||||
var sessionIds []string
|
||||
|
||||
// Get tokens for notification (needed for both session-level and full logout)
|
||||
// This enables subsystems to identify and invalidate corresponding access tokens
|
||||
// Note: Tokens must be retrieved BEFORE expiration to include their hashes in the notification
|
||||
tokens, err = object.GetTokensByUser(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if logoutAllSessions {
|
||||
// Logout from all sessions: expire all tokens and delete all sessions
|
||||
// Get tokens before expiring them (for session-level logout notification)
|
||||
tokens, err = object.GetTokensByUser(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = object.ExpireTokenByUser(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -105,6 +105,13 @@ func (c *ApiController) getCurrentUser() *object.User {
|
||||
|
||||
// GetSessionUsername ...
|
||||
func (c *ApiController) GetSessionUsername() string {
|
||||
// prefer username stored in Beego context by ApiFilter
|
||||
if ctxUser := c.Ctx.Input.GetData("currentUserId"); ctxUser != nil {
|
||||
if username, ok := ctxUser.(string); ok {
|
||||
return username
|
||||
}
|
||||
}
|
||||
|
||||
// check if user session expired
|
||||
sessionData := c.GetSessionData()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ package controllers
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
@@ -62,6 +63,7 @@ func (c *ApiController) HandleSamlRedirect() {
|
||||
username := c.Ctx.Input.Query("username")
|
||||
loginHint := c.Ctx.Input.Query("login_hint")
|
||||
|
||||
relayState = url.QueryEscape(relayState)
|
||||
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host, username, loginHint)
|
||||
|
||||
c.Redirect(targetURL, http.StatusSeeOther)
|
||||
|
||||
@@ -778,6 +778,78 @@ func (c *ApiController) RemoveUserFromGroup() {
|
||||
c.ResponseOk(affected)
|
||||
}
|
||||
|
||||
// ImpersonateUser
|
||||
// @Title ImpersonateUser
|
||||
// @Tag User API
|
||||
// @Description set impersonation user for current admin session
|
||||
// @Param username formData string true "The username to impersonate (owner/name)"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /impersonation-user [post]
|
||||
func (c *ApiController) ImpersonateUser() {
|
||||
org, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
username := c.Ctx.Request.Form.Get("username")
|
||||
if username == "" {
|
||||
c.ResponseError(c.T("general:Missing parameter"))
|
||||
return
|
||||
}
|
||||
|
||||
owner, _, err := util.GetOwnerAndNameFromIdWithError(username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !(owner == org || org == "") {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
targetUser, err := object.GetUser(username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if targetUser == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), username))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.SetSession("impersonateUser", username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Ctx.SetCookie("impersonateUser", username, 0, "/")
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
// ExitImpersonateUser
|
||||
// @Title ExitImpersonateUser
|
||||
// @Tag User API
|
||||
// @Description clear impersonation info for current session
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /exit-impersonation-user [post]
|
||||
func (c *ApiController) ExitImpersonateUser() {
|
||||
_, ok := c.Ctx.Input.GetData("impersonating").(bool)
|
||||
if !ok {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
err := c.SetSession("impersonateUser", "")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Ctx.SetCookie("impersonateUser", "", -1, "/")
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
// VerifyIdentification
|
||||
// @Title VerifyIdentification
|
||||
// @Tag User API
|
||||
|
||||
4
go.mod
4
go.mod
@@ -14,6 +14,8 @@ require (
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0
|
||||
github.com/alibabacloud-go/tea v1.3.2
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible
|
||||
github.com/aliyun/credentials-go v1.3.10
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
|
||||
github.com/beego/beego/v2 v2.3.8
|
||||
github.com/beevik/etree v1.1.0
|
||||
@@ -110,8 +112,6 @@ require (
|
||||
github.com/alibabacloud-go/tea-utils v1.3.6 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545 // indirect
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
|
||||
github.com/aliyun/credentials-go v1.3.10 // indirect
|
||||
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.45.5 // indirect
|
||||
|
||||
13
mcp/auth.go
13
mcp/auth.go
@@ -26,11 +26,17 @@ type SessionData struct {
|
||||
ExpireTime int64
|
||||
}
|
||||
|
||||
// GetSessionUsername returns the username from session
|
||||
// GetSessionUsername returns the username from session or ctx
|
||||
func (c *McpController) GetSessionUsername() string {
|
||||
// check if user session expired
|
||||
sessionData := c.GetSessionData()
|
||||
// prefer username stored in Beego context by ApiFilter
|
||||
if ctxUser := c.Ctx.Input.GetData("currentUserId"); ctxUser != nil {
|
||||
if username, ok := ctxUser.(string); ok {
|
||||
return username
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to previous session-based logic with expiry check
|
||||
sessionData := c.GetSessionData()
|
||||
if sessionData != nil &&
|
||||
sessionData.ExpireTime != 0 &&
|
||||
sessionData.ExpireTime < time.Now().Unix() {
|
||||
@@ -77,6 +83,7 @@ func (c *McpController) IsGlobalAdmin() bool {
|
||||
|
||||
func (c *McpController) isGlobalAdmin() (bool, *object.User) {
|
||||
username := c.GetSessionUsername()
|
||||
|
||||
if object.IsAppUser(username) {
|
||||
// e.g., "app/app-casnode"
|
||||
return true, nil
|
||||
|
||||
72
mcp/base.go
72
mcp/base.go
@@ -17,6 +17,7 @@ package mcp
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -120,41 +121,38 @@ func (c *McpController) Prepare() {
|
||||
c.EnableRender = false
|
||||
}
|
||||
|
||||
// SendMcpResponse sends a successful MCP response
|
||||
func (c *McpController) SendMcpResponse(id interface{}, result interface{}) {
|
||||
func (c *McpController) McpResponseOk(id interface{}, result interface{}) {
|
||||
resp := BuildMcpResponse(id, result, nil)
|
||||
c.Ctx.Output.Header("Content-Type", "application/json")
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *McpController) McpResponseError(id interface{}, code int, message string, data interface{}) {
|
||||
resp := BuildMcpResponse(id, nil, &McpError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
c.Ctx.Output.Header("Content-Type", "application/json")
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetMcpResponse returns a McpResponse object
|
||||
func BuildMcpResponse(id interface{}, result interface{}, err *McpError) McpResponse {
|
||||
resp := McpResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Result: result,
|
||||
Error: err,
|
||||
}
|
||||
|
||||
// Set proper HTTP headers for MCP responses
|
||||
c.Ctx.Output.Header("Content-Type", "application/json")
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// SendMcpError sends an MCP error response
|
||||
func (c *McpController) SendMcpError(id interface{}, code int, message string, data interface{}) {
|
||||
resp := McpResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Error: &McpError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
},
|
||||
}
|
||||
|
||||
// Set proper HTTP headers for MCP responses
|
||||
c.Ctx.Output.Header("Content-Type", "application/json")
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
return resp
|
||||
}
|
||||
|
||||
// sendInvalidParamsError sends an invalid params error
|
||||
func (c *McpController) sendInvalidParamsError(id interface{}, details string) {
|
||||
c.SendMcpError(id, -32602, "Invalid params", details)
|
||||
c.McpResponseError(id, -32602, "Invalid params", details)
|
||||
}
|
||||
|
||||
// SendToolResult sends a successful tool execution result
|
||||
@@ -167,7 +165,7 @@ func (c *McpController) SendToolResult(id interface{}, text string) {
|
||||
},
|
||||
},
|
||||
}
|
||||
c.SendMcpResponse(id, result)
|
||||
c.McpResponseOk(id, result)
|
||||
}
|
||||
|
||||
// SendToolErrorResult sends a tool execution error result
|
||||
@@ -181,7 +179,7 @@ func (c *McpController) SendToolErrorResult(id interface{}, errorMsg string) {
|
||||
},
|
||||
IsError: true,
|
||||
}
|
||||
c.SendMcpResponse(id, result)
|
||||
c.McpResponseOk(id, result)
|
||||
}
|
||||
|
||||
// FormatOperationResult formats the result of CRUD operations in a clear, descriptive way
|
||||
@@ -211,7 +209,7 @@ func (c *McpController) HandleMcp() {
|
||||
var req McpRequest
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
|
||||
if err != nil {
|
||||
c.SendMcpError(nil, -32700, "Parse error", err.Error())
|
||||
c.McpResponseError(nil, -32700, "Parse error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -228,7 +226,7 @@ func (c *McpController) HandleMcp() {
|
||||
case "tools/call":
|
||||
c.handleToolsCall(req)
|
||||
default:
|
||||
c.SendMcpError(req.ID, -32601, "Method not found", fmt.Sprintf("Method '%s' not found", req.Method))
|
||||
c.McpResponseError(req.ID, -32601, "Method not found", fmt.Sprintf("Method '%s' not found", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,20 +253,18 @@ func (c *McpController) handleInitialize(req McpRequest) {
|
||||
},
|
||||
}
|
||||
|
||||
c.SendMcpResponse(req.ID, result)
|
||||
c.McpResponseOk(req.ID, result)
|
||||
}
|
||||
|
||||
func (c *McpController) handleNotificationsInitialized(req McpRequest) {
|
||||
// notifications/initialized is a notification from client indicating
|
||||
// that the initialization process is complete and the client is ready
|
||||
// to start using the server. No response is expected for notifications.
|
||||
// We can log this event or perform any post-initialization setup here.
|
||||
c.Ctx.Output.SetStatus(http.StatusAccepted)
|
||||
c.Ctx.Output.Body([]byte{})
|
||||
}
|
||||
|
||||
func (c *McpController) handlePing(req McpRequest) {
|
||||
// ping method is used to check if the server is alive and responsive
|
||||
// Return an empty object as result to indicate server is active
|
||||
c.SendMcpResponse(req.ID, map[string]interface{}{})
|
||||
c.McpResponseOk(req.ID, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func (c *McpController) handleToolsList(req McpRequest) {
|
||||
@@ -353,7 +349,7 @@ func (c *McpController) handleToolsList(req McpRequest) {
|
||||
Tools: tools,
|
||||
}
|
||||
|
||||
c.SendMcpResponse(req.ID, result)
|
||||
c.McpResponseOk(req.ID, result)
|
||||
}
|
||||
|
||||
func (c *McpController) handleToolsCall(req McpRequest) {
|
||||
@@ -402,6 +398,6 @@ func (c *McpController) handleToolsCall(req McpRequest) {
|
||||
}
|
||||
c.handleDeleteApplicationTool(req.ID, args)
|
||||
default:
|
||||
c.SendMcpError(req.ID, -32602, "Invalid tool name", fmt.Sprintf("Tool '%s' not found", params.Name))
|
||||
c.McpResponseError(req.ID, -32602, "Invalid tool name", fmt.Sprintf("Tool '%s' not found", params.Name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,16 +35,6 @@ func PlaceOrder(productId string, user *User, pricingName string, planName strin
|
||||
return nil, fmt.Errorf("the product: %s is out of stock", product.Name)
|
||||
}
|
||||
|
||||
userBalanceCurrency := user.BalanceCurrency
|
||||
if userBalanceCurrency == "" {
|
||||
org, err := getOrganization("admin", user.Owner)
|
||||
if err == nil && org != nil && org.BalanceCurrency != "" {
|
||||
userBalanceCurrency = org.BalanceCurrency
|
||||
} else {
|
||||
userBalanceCurrency = "USD"
|
||||
}
|
||||
}
|
||||
|
||||
productCurrency := product.Currency
|
||||
if productCurrency == "" {
|
||||
productCurrency = "USD"
|
||||
@@ -59,7 +49,6 @@ func PlaceOrder(productId string, user *User, pricingName string, planName strin
|
||||
} else {
|
||||
productPrice = product.Price
|
||||
}
|
||||
price := ConvertCurrency(productPrice, productCurrency, userBalanceCurrency)
|
||||
|
||||
orderName := fmt.Sprintf("order_%v", util.GenerateTimeId())
|
||||
order := &Order{
|
||||
@@ -73,8 +62,8 @@ func PlaceOrder(productId string, user *User, pricingName string, planName strin
|
||||
PlanName: planName,
|
||||
User: user.Name,
|
||||
Payment: "", // Payment will be set when user pays
|
||||
Price: price,
|
||||
Currency: userBalanceCurrency,
|
||||
Price: productPrice,
|
||||
Currency: productCurrency,
|
||||
State: "Created",
|
||||
Message: "",
|
||||
StartTime: util.GetCurrentTime(),
|
||||
|
||||
@@ -137,6 +137,12 @@ func UpdateProduct(id string, product *Product) (bool, error) {
|
||||
} else if p == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err = checkProduct(product)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(product)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -146,6 +152,11 @@ func UpdateProduct(id string, product *Product) (bool, error) {
|
||||
}
|
||||
|
||||
func AddProduct(product *Product) (bool, error) {
|
||||
err := checkProduct(product)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.Insert(product)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -154,6 +165,23 @@ func AddProduct(product *Product) (bool, error) {
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func checkProduct(product *Product) error {
|
||||
if product == nil {
|
||||
return fmt.Errorf("the product not exist")
|
||||
}
|
||||
|
||||
for _, providerName := range product.Providers {
|
||||
provider, err := getProvider(product.Owner, providerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if provider != nil && provider.Type == "Alipay" && product.Currency != "CNY" {
|
||||
return fmt.Errorf("alipay provider only supports CNY, got: %s", product.Currency)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteProduct(product *Product) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{product.Owner, product.Name}).Delete(&Product{})
|
||||
if err != nil {
|
||||
@@ -167,13 +195,23 @@ func (product *Product) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", product.Owner, product.Name)
|
||||
}
|
||||
|
||||
func (product *Product) isValidProvider(provider *Provider) bool {
|
||||
func (product *Product) isValidProvider(provider *Provider) error {
|
||||
if provider.Type == "Alipay" && product.Currency != "CNY" {
|
||||
return fmt.Errorf("alipay provider only supports CNY, got: %s", product.Currency)
|
||||
}
|
||||
|
||||
providerMatched := false
|
||||
for _, providerName := range product.Providers {
|
||||
if providerName == provider.Name {
|
||||
return true
|
||||
providerMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
if !providerMatched {
|
||||
return fmt.Errorf("the payment provider: %s is not valid for the product: %s", provider.Name, product.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (product *Product) getProvider(providerName string) (*Provider, error) {
|
||||
@@ -186,8 +224,8 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
|
||||
return nil, fmt.Errorf("the payment provider: %s does not exist", providerName)
|
||||
}
|
||||
|
||||
if !product.isValidProvider(provider) {
|
||||
return nil, fmt.Errorf("the payment provider: %s is not valid for the product: %s", providerName, product.Name)
|
||||
if err := product.isValidProvider(provider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
|
||||
@@ -312,8 +312,8 @@ func SendWebhooks(record *casvisorsdk.Record) error {
|
||||
errs := []error{}
|
||||
webhooks = getFilteredWebhooks(webhooks, record.Organization, record.Action)
|
||||
|
||||
record2 := *record
|
||||
for _, webhook := range webhooks {
|
||||
record2 := *record
|
||||
|
||||
if len(webhook.ObjectFields) != 0 && webhook.ObjectFields[0] != "All" {
|
||||
record2.Object = filterRecordObject(record.Object, webhook.ObjectFields)
|
||||
|
||||
@@ -18,11 +18,58 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
// convertGUIDToString converts a binary GUID byte array to a standard UUID string format
|
||||
// Active Directory GUIDs are 16 bytes in a specific byte order
|
||||
func convertGUIDToString(guidBytes []byte) string {
|
||||
if len(guidBytes) != 16 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Active Directory GUID format is:
|
||||
// Data1 (4 bytes, little-endian) - Data2 (2 bytes, little-endian) - Data3 (2 bytes, little-endian) - Data4 (2 bytes, big-endian) - Data5 (6 bytes, big-endian)
|
||||
// Convert to standard UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
return fmt.Sprintf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
||||
guidBytes[3], guidBytes[2], guidBytes[1], guidBytes[0], // Data1 (reverse byte order)
|
||||
guidBytes[5], guidBytes[4], // Data2 (reverse byte order)
|
||||
guidBytes[7], guidBytes[6], // Data3 (reverse byte order)
|
||||
guidBytes[8], guidBytes[9], // Data4 (big-endian)
|
||||
guidBytes[10], guidBytes[11], guidBytes[12], guidBytes[13], guidBytes[14], guidBytes[15]) // Data5 (big-endian)
|
||||
}
|
||||
|
||||
// sanitizeUTF8 ensures the string contains only valid UTF-8 characters
|
||||
// Invalid UTF-8 sequences are replaced with the Unicode replacement character
|
||||
func sanitizeUTF8(s string) string {
|
||||
if utf8.ValidString(s) {
|
||||
return s
|
||||
}
|
||||
|
||||
// Build a new string with only valid UTF-8
|
||||
var builder strings.Builder
|
||||
builder.Grow(len(s))
|
||||
|
||||
for _, r := range s {
|
||||
if r == utf8.RuneError {
|
||||
// Skip invalid runes
|
||||
continue
|
||||
}
|
||||
builder.WriteRune(r)
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// getAttributeValueSafe safely retrieves an LDAP attribute value and ensures it's valid UTF-8
|
||||
func getAttributeValueSafe(entry *goldap.Entry, attributeName string) string {
|
||||
value := entry.GetAttributeValue(attributeName)
|
||||
return sanitizeUTF8(value)
|
||||
}
|
||||
|
||||
// ActiveDirectorySyncerProvider implements SyncerProvider for Active Directory LDAP-based syncers
|
||||
type ActiveDirectorySyncerProvider struct {
|
||||
Syncer *Syncer
|
||||
@@ -197,26 +244,32 @@ func (p *ActiveDirectorySyncerProvider) adEntryToOriginalUser(entry *goldap.Entr
|
||||
Groups: []string{},
|
||||
}
|
||||
|
||||
// Get basic attributes
|
||||
sAMAccountName := entry.GetAttributeValue("sAMAccountName")
|
||||
userPrincipalName := entry.GetAttributeValue("userPrincipalName")
|
||||
displayName := entry.GetAttributeValue("displayName")
|
||||
givenName := entry.GetAttributeValue("givenName")
|
||||
sn := entry.GetAttributeValue("sn")
|
||||
mail := entry.GetAttributeValue("mail")
|
||||
telephoneNumber := entry.GetAttributeValue("telephoneNumber")
|
||||
mobile := entry.GetAttributeValue("mobile")
|
||||
title := entry.GetAttributeValue("title")
|
||||
department := entry.GetAttributeValue("department")
|
||||
company := entry.GetAttributeValue("company")
|
||||
streetAddress := entry.GetAttributeValue("streetAddress")
|
||||
city := entry.GetAttributeValue("l")
|
||||
state := entry.GetAttributeValue("st")
|
||||
postalCode := entry.GetAttributeValue("postalCode")
|
||||
country := entry.GetAttributeValue("co")
|
||||
objectGUID := entry.GetAttributeValue("objectGUID")
|
||||
whenCreated := entry.GetAttributeValue("whenCreated")
|
||||
userAccountControlStr := entry.GetAttributeValue("userAccountControl")
|
||||
// Get basic attributes with UTF-8 sanitization
|
||||
sAMAccountName := getAttributeValueSafe(entry, "sAMAccountName")
|
||||
userPrincipalName := getAttributeValueSafe(entry, "userPrincipalName")
|
||||
displayName := getAttributeValueSafe(entry, "displayName")
|
||||
givenName := getAttributeValueSafe(entry, "givenName")
|
||||
sn := getAttributeValueSafe(entry, "sn")
|
||||
mail := getAttributeValueSafe(entry, "mail")
|
||||
telephoneNumber := getAttributeValueSafe(entry, "telephoneNumber")
|
||||
mobile := getAttributeValueSafe(entry, "mobile")
|
||||
title := getAttributeValueSafe(entry, "title")
|
||||
department := getAttributeValueSafe(entry, "department")
|
||||
company := getAttributeValueSafe(entry, "company")
|
||||
streetAddress := getAttributeValueSafe(entry, "streetAddress")
|
||||
city := getAttributeValueSafe(entry, "l")
|
||||
state := getAttributeValueSafe(entry, "st")
|
||||
postalCode := getAttributeValueSafe(entry, "postalCode")
|
||||
country := getAttributeValueSafe(entry, "co")
|
||||
whenCreated := getAttributeValueSafe(entry, "whenCreated")
|
||||
userAccountControlStr := getAttributeValueSafe(entry, "userAccountControl")
|
||||
|
||||
// Handle objectGUID specially - it's a binary attribute
|
||||
var objectGUID string
|
||||
objectGUIDBytes := entry.GetRawAttributeValue("objectGUID")
|
||||
if len(objectGUIDBytes) == 16 {
|
||||
objectGUID = convertGUIDToString(objectGUIDBytes)
|
||||
}
|
||||
|
||||
// Set user fields
|
||||
// Use sAMAccountName as the primary username
|
||||
|
||||
@@ -43,22 +43,24 @@ type UserShort struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
|
||||
Id string `xorm:"varchar(100) index" json:"id"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Avatar string `xorm:"varchar(500)" json:"avatar"`
|
||||
Email string `xorm:"varchar(100) index" json:"email"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||
Id string `xorm:"varchar(100) index" json:"id"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Avatar string `xorm:"varchar(500)" json:"avatar"`
|
||||
Email string `xorm:"varchar(100) index" json:"email"`
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||
}
|
||||
|
||||
type UserStandard struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"preferred_username,omitempty"`
|
||||
|
||||
Id string `xorm:"varchar(100) index" json:"id"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"name,omitempty"`
|
||||
Avatar string `xorm:"varchar(500)" json:"picture,omitempty"`
|
||||
Email string `xorm:"varchar(100) index" json:"email,omitempty"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone,omitempty"`
|
||||
Id string `xorm:"varchar(100) index" json:"id"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"name,omitempty"`
|
||||
Avatar string `xorm:"varchar(500)" json:"picture,omitempty"`
|
||||
Email string `xorm:"varchar(100) index" json:"email,omitempty"`
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone,omitempty"`
|
||||
}
|
||||
|
||||
type UserWithoutThirdIdp struct {
|
||||
@@ -80,7 +82,7 @@ type UserWithoutThirdIdp struct {
|
||||
AvatarType string `xorm:"varchar(100)" json:"avatarType"`
|
||||
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
|
||||
Email string `xorm:"varchar(100) index" json:"email"`
|
||||
EmailVerified bool `json:"emailVerified"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||
CountryCode string `xorm:"varchar(6)" json:"countryCode"`
|
||||
Region string `xorm:"varchar(100)" json:"region"`
|
||||
@@ -190,11 +192,12 @@ func getShortUser(user *User) *UserShort {
|
||||
Owner: user.Owner,
|
||||
Name: user.Name,
|
||||
|
||||
Id: user.Id,
|
||||
DisplayName: user.DisplayName,
|
||||
Avatar: user.Avatar,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
Id: user.Id,
|
||||
DisplayName: user.DisplayName,
|
||||
Avatar: user.Avatar,
|
||||
Email: user.Email,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -204,11 +207,12 @@ func getStandardUser(user *User) *UserStandard {
|
||||
Owner: user.Owner,
|
||||
Name: user.Name,
|
||||
|
||||
Id: user.Id,
|
||||
DisplayName: user.DisplayName,
|
||||
Avatar: user.Avatar,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
Id: user.Id,
|
||||
DisplayName: user.DisplayName,
|
||||
Avatar: user.Avatar,
|
||||
Email: user.Email,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -319,7 +319,15 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
||||
if err != nil || token == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "refresh token is invalid, expired or revoked",
|
||||
ErrorDescription: "refresh token is invalid or revoked",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// check if the token has been invalidated (e.g., by SSO logout)
|
||||
if token.ExpiresIn <= 0 {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "refresh token is expired",
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -89,16 +89,25 @@ func getStandardClaims(claims Claims) ClaimsStandard {
|
||||
|
||||
func ParseStandardJwtToken(token string, cert *Cert) (*ClaimsStandard, error) {
|
||||
t, err := jwt.ParseWithClaims(token, &ClaimsStandard{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
var (
|
||||
certificate interface{}
|
||||
err error
|
||||
)
|
||||
|
||||
if cert.Certificate == "" {
|
||||
return nil, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
|
||||
}
|
||||
|
||||
// RSA certificate
|
||||
certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); ok {
|
||||
// RSA certificate
|
||||
certificate, err = jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
|
||||
} else if _, ok := token.Method.(*jwt.SigningMethodECDSA); ok {
|
||||
// ES certificate
|
||||
certificate, err = jwt.ParseECPublicKeyFromPEM([]byte(cert.Certificate))
|
||||
} else {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -258,8 +258,41 @@ func getExtraInfo(ctx *context.Context, urlPath string) map[string]interface{} {
|
||||
return extra
|
||||
}
|
||||
|
||||
func getImpersonateUser(ctx *context.Context, subOwner, subName, username string) (string, string, string) {
|
||||
impersonateUser, ok := ctx.Input.Session("impersonateUser").(string)
|
||||
impersonateUserCookie := ctx.GetCookie("impersonateUser")
|
||||
if ok && impersonateUser != "" && impersonateUserCookie != "" {
|
||||
user, err := object.GetUser(util.GetId(subOwner, subName))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
impUserOwner, impUserName, err := util.GetOwnerAndNameFromIdWithError(impersonateUser)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if user.IsAdmin && impUserOwner == user.Owner {
|
||||
ctx.Input.SetData("impersonating", true)
|
||||
return impUserOwner, impUserName, impersonateUser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return subOwner, subName, username
|
||||
}
|
||||
|
||||
func ApiFilter(ctx *context.Context) {
|
||||
subOwner, subName := getSubject(ctx)
|
||||
// stash current user info into request context for controllers
|
||||
username := ""
|
||||
if !(subOwner == "anonymous" && subName == "anonymous") {
|
||||
username = fmt.Sprintf("%s/%s", subOwner, subName)
|
||||
subOwner, subName, username = getImpersonateUser(ctx, subOwner, subName, username)
|
||||
}
|
||||
ctx.Input.SetData("currentUserId", username)
|
||||
|
||||
method := ctx.Request.Method
|
||||
urlPath := getUrlPath(ctx)
|
||||
extraInfo := getExtraInfo(ctx, urlPath)
|
||||
@@ -297,7 +330,11 @@ func ApiFilter(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if !isAllowed {
|
||||
denyRequest(ctx)
|
||||
if urlPath == "/api/mcp" {
|
||||
denyMcpRequest(ctx)
|
||||
} else {
|
||||
denyRequest(ctx)
|
||||
}
|
||||
record, err := object.NewRecord(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
"github.com/casdoor/casdoor/mcp"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -28,6 +30,14 @@ func AutoSigninFilter(ctx *context.Context) {
|
||||
if strings.HasPrefix(urlPath, "/api/login/oauth/access_token") {
|
||||
return
|
||||
}
|
||||
if urlPath == "/api/mcp" {
|
||||
var req mcp.McpRequest
|
||||
if err := json.Unmarshal(ctx.Input.RequestBody, &req); err == nil {
|
||||
if req.Method == "initialize" || req.Method == "notifications/initialized" || req.Method == "ping" || req.Method == "tools/list" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
//if getSessionUser(ctx) != "" {
|
||||
// return
|
||||
//}
|
||||
|
||||
@@ -16,14 +16,17 @@ package routers
|
||||
|
||||
import (
|
||||
stdcontext "context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/mcp"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -37,6 +40,11 @@ type Response struct {
|
||||
|
||||
func responseError(ctx *context.Context, error string, data ...interface{}) {
|
||||
// ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
|
||||
urlPath := ctx.Request.URL.Path
|
||||
if urlPath == "/api/mcp" {
|
||||
denyMcpRequest(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
resp := Response{Status: "error", Msg: error}
|
||||
switch len(data) {
|
||||
@@ -66,6 +74,30 @@ func denyRequest(ctx *context.Context) {
|
||||
responseError(ctx, T(ctx, "auth:Unauthorized operation"))
|
||||
}
|
||||
|
||||
func denyMcpRequest(ctx *context.Context) {
|
||||
req := mcp.McpRequest{}
|
||||
err := json.Unmarshal(ctx.Input.RequestBody, &req)
|
||||
if err != nil {
|
||||
ctx.Output.SetStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.ID == nil {
|
||||
ctx.Output.SetStatus(http.StatusAccepted)
|
||||
ctx.Output.Body([]byte{})
|
||||
return
|
||||
}
|
||||
|
||||
resp := mcp.BuildMcpResponse(req.ID, nil, &mcp.McpError{
|
||||
Code: -32001,
|
||||
Message: "Unauthorized",
|
||||
Data: T(ctx, "auth:Unauthorized operation"),
|
||||
})
|
||||
|
||||
ctx.Output.SetStatus(http.StatusUnauthorized)
|
||||
_ = ctx.Output.JSON(resp, true, false)
|
||||
}
|
||||
|
||||
func getUsernameByClientIdSecret(ctx *context.Context) (string, error) {
|
||||
clientId, clientSecret, ok := ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
|
||||
@@ -92,6 +92,8 @@ func InitAPI() {
|
||||
web.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers")
|
||||
web.Router("/api/remove-user-from-group", &controllers.ApiController{}, "POST:RemoveUserFromGroup")
|
||||
web.Router("/api/verify-identification", &controllers.ApiController{}, "POST:VerifyIdentification")
|
||||
web.Router("/api/impersonate-user", &controllers.ApiController{}, "POST:ImpersonateUser")
|
||||
web.Router("/api/exit-impersonate-user", &controllers.ApiController{}, "POST:ExitImpersonateUser")
|
||||
|
||||
web.Router("/api/get-invitations", &controllers.ApiController{}, "GET:GetInvitations")
|
||||
web.Router("/api/get-invitation", &controllers.ApiController{}, "GET:GetInvitation")
|
||||
|
||||
@@ -15,11 +15,55 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"github.com/casdoor/oss"
|
||||
"os"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"github.com/aliyun/credentials-go/credentials"
|
||||
casdoorOss "github.com/casdoor/oss"
|
||||
"github.com/casdoor/oss/aliyun"
|
||||
)
|
||||
|
||||
func NewAliyunOssStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
|
||||
func NewAliyunOssStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) casdoorOss.StorageInterface {
|
||||
// Check if RRSA is available (empty credentials + environment variables set)
|
||||
if (clientId == "" || clientId == "rrsa") &&
|
||||
(clientSecret == "" || clientSecret == "rrsa") &&
|
||||
os.Getenv("ALIBABA_CLOUD_ROLE_ARN") != "" {
|
||||
// Use RRSA to get temporary credentials
|
||||
config := &credentials.Config{}
|
||||
config.SetType("oidc_role_arn")
|
||||
config.SetRoleArn(os.Getenv("ALIBABA_CLOUD_ROLE_ARN"))
|
||||
config.SetOIDCProviderArn(os.Getenv("ALIBABA_CLOUD_OIDC_PROVIDER_ARN"))
|
||||
config.SetOIDCTokenFilePath(os.Getenv("ALIBABA_CLOUD_OIDC_TOKEN_FILE"))
|
||||
config.SetRoleSessionName("casdoor-oss")
|
||||
|
||||
// Set STS endpoint if provided
|
||||
if stsEndpoint := os.Getenv("ALIBABA_CLOUD_STS_ENDPOINT"); stsEndpoint != "" {
|
||||
config.SetSTSEndpoint(stsEndpoint)
|
||||
}
|
||||
|
||||
credential, err := credentials.NewCredential(config)
|
||||
if err == nil {
|
||||
accessKeyId, errId := credential.GetAccessKeyId()
|
||||
accessKeySecret, errSecret := credential.GetAccessKeySecret()
|
||||
securityToken, errToken := credential.GetSecurityToken()
|
||||
|
||||
if errId == nil && errSecret == nil && errToken == nil &&
|
||||
accessKeyId != nil && accessKeySecret != nil && securityToken != nil {
|
||||
// Successfully obtained RRSA credentials
|
||||
sp := aliyun.New(&aliyun.Config{
|
||||
AccessID: *accessKeyId,
|
||||
AccessKey: *accessKeySecret,
|
||||
Bucket: bucket,
|
||||
Endpoint: endpoint,
|
||||
ClientOptions: []oss.ClientOption{oss.SecurityToken(*securityToken)},
|
||||
})
|
||||
return sp
|
||||
}
|
||||
}
|
||||
// If RRSA fails, fall through to static credentials (which will fail if empty)
|
||||
}
|
||||
|
||||
// Use static credentials (existing behavior)
|
||||
sp := aliyun.New(&aliyun.Config{
|
||||
AccessID: clientId,
|
||||
AccessKey: clientSecret,
|
||||
|
||||
@@ -101,6 +101,8 @@ import TransactionEditPage from "./TransactionEditPage";
|
||||
import VerificationListPage from "./VerificationListPage";
|
||||
import TicketListPage from "./TicketListPage";
|
||||
import TicketEditPage from "./TicketEditPage";
|
||||
import * as Cookie from "cookie";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
|
||||
function ManagementPage(props) {
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
@@ -155,8 +157,14 @@ function ManagementPage(props) {
|
||||
"/account"
|
||||
));
|
||||
}
|
||||
items.push(Setting.getItem(<><LogoutOutlined /> {i18next.t("account:Logout")}</>,
|
||||
"/logout"));
|
||||
const curCookie = Cookie.parse(document.cookie);
|
||||
if (curCookie["impersonateUser"]) {
|
||||
items.push(Setting.getItem(<><LogoutOutlined /> {i18next.t("account:Exit impersonation")}</>,
|
||||
"/exit-impersonation"));
|
||||
} else {
|
||||
items.push(Setting.getItem(<><LogoutOutlined /> {i18next.t("account:Logout")}</>,
|
||||
"/logout"));
|
||||
}
|
||||
|
||||
const onClick = (e) => {
|
||||
if (e.key === "/account") {
|
||||
@@ -165,6 +173,16 @@ function ManagementPage(props) {
|
||||
props.history.push("/subscription");
|
||||
} else if (e.key === "/logout") {
|
||||
logout();
|
||||
} else if (e.key === "/exit-impersonation") {
|
||||
UserBackend.exitImpersonateUser().then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("account:Exit impersonation"));
|
||||
Setting.goToLinkSoft({props}, "/");
|
||||
window.location.reload();
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -289,7 +289,7 @@ class OrderListPage extends BaseListPage {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
this.setState({loading: true});
|
||||
OrderBackend.getOrders(Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
OrderBackend.getOrders(Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
|
||||
@@ -188,6 +188,18 @@ class UserListPage extends BaseListPage {
|
||||
});
|
||||
}
|
||||
|
||||
impersonateUser(user) {
|
||||
UserBackend.impersonateUser(user).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Success"));
|
||||
Setting.goToLinkSoft(this, "/");
|
||||
window.location.reload();
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderUpload() {
|
||||
const uploadThis = this;
|
||||
const props = {
|
||||
@@ -533,6 +545,10 @@ class UserListPage extends BaseListPage {
|
||||
const disabled = (record.owner === this.props.account.owner && record.name === this.props.account.name) || (record.owner === "built-in" && record.name === "admin");
|
||||
return (
|
||||
<Space>
|
||||
<Button size={isTreePage ? "small" : "middle"} type="primary" onClick={() => {
|
||||
this.impersonateUser(`${record.owner}/${record.name}`);
|
||||
}}>{i18next.t("general:Impersonation")}
|
||||
</Button>
|
||||
<Button size={isTreePage ? "small" : "middle"} type="primary" onClick={() => {
|
||||
sessionStorage.setItem("userListUrl", window.location.pathname);
|
||||
this.props.history.push(`/users/${record.owner}/${record.name}`);
|
||||
|
||||
@@ -577,7 +577,7 @@ class LoginPage extends React.Component {
|
||||
} else {
|
||||
const SAMLResponse = res.data;
|
||||
const redirectUri = res.data2.redirectUrl;
|
||||
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${encodeURIComponent(oAuthParams.relayState)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1563,7 +1563,7 @@ class LoginPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
const visibleOAuthProviderItems = (application.providers === null) ? [] : application.providers.filter(providerItem => this.isProviderVisible(providerItem));
|
||||
const visibleOAuthProviderItems = (application.providers === null) ? [] : application.providers.filter(providerItem => this.isProviderVisible(providerItem) && providerItem.provider?.category !== "SAML");
|
||||
if (this.props.preview !== "auto" && !Setting.isPasswordEnabled(application) && !Setting.isCodeSigninEnabled(application) && !Setting.isWebAuthnEnabled(application) && !Setting.isLdapEnabled(application) && visibleOAuthProviderItems.length === 1) {
|
||||
Setting.goToLink(Provider.getAuthUrl(application, visibleOAuthProviderItems[0].provider, "signup"));
|
||||
return (
|
||||
|
||||
@@ -200,6 +200,29 @@ export function resetEmailOrPhone(dest, type, code) {
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function impersonateUser(username) {
|
||||
const formData = new FormData();
|
||||
formData.append("username", username);
|
||||
return fetch(`${Setting.ServerUrl}/api/impersonate-user`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function exitImpersonateUser() {
|
||||
return fetch(`${Setting.ServerUrl}/api/exit-impersonate-user`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getCaptcha(owner, name, isCurrentProvider) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-captcha?applicationId=${owner}/${encodeURIComponent(name)}&isCurrentProvider=${isCurrentProvider}`, {
|
||||
method: "GET",
|
||||
|
||||
Reference in New Issue
Block a user