mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-11-10 21:32:43 +08:00
Revert "fix: remove sso related code" (#21657)
This commit is contained in:
16 files changed
+135
-47
No files matched your search
@@ -496,6 +496,9 @@ DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
# Used for ThreadPoolExecutor
|
||||
THREAD_POOL_MAX_WORKERS=128
|
||||
|
||||
#Option to disable login form for SSO
|
||||
DISABLE_PASSWORD_LOGIN=false
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Knowledge compilation
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -215,6 +215,7 @@ user_default_llm:
|
||||
# http_secret_key:
|
||||
# site:
|
||||
# switch: false
|
||||
# disable_password_login: false
|
||||
# permission:
|
||||
# switch: false
|
||||
# component: false
|
||||
|
||||
@@ -62,7 +62,7 @@ func putVariable(t *testing.T, h *Handler, body string) setVariableResponse {
|
||||
}
|
||||
|
||||
// TestSetVariableFlattensNativeJSONScalars covers the reported regression: the
|
||||
// admin console sends a JSON boolean for a toggle, and a string-typed request
|
||||
// admin console toggles SSO with a JSON boolean, and a string-typed request
|
||||
// field used to reject it as a missing var_name.
|
||||
func TestSetVariableFlattensNativeJSONScalars(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
@@ -74,15 +74,15 @@ func TestSetVariableFlattensNativeJSONScalars(t *testing.T) {
|
||||
wantValue string
|
||||
}{
|
||||
{
|
||||
name: "bool true keeps its literal",
|
||||
varName: "mail.enabled",
|
||||
body: `{"var_name":"mail.enabled","var_value":true}`,
|
||||
name: "sso enable sends true",
|
||||
varName: "feishu|sso.enabled",
|
||||
body: `{"var_name":"feishu|sso.enabled","var_value":true}`,
|
||||
wantValue: "true",
|
||||
},
|
||||
{
|
||||
name: "bool false keeps its literal",
|
||||
varName: "mail.enabled",
|
||||
body: `{"var_name":"mail.enabled","var_value":false}`,
|
||||
name: "sso disable sends false",
|
||||
varName: "feishu|sso.enabled",
|
||||
body: `{"var_name":"feishu|sso.enabled","var_value":false}`,
|
||||
wantValue: "false",
|
||||
},
|
||||
{
|
||||
@@ -137,15 +137,15 @@ func TestSetVariableUpdatesExistingSetting(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
handler, db := setupSetVariableHandler(t)
|
||||
|
||||
if response := putVariable(t, handler, `{"var_name":"mail.enabled","var_value":true}`); response.Code != int(common.CodeSuccess) {
|
||||
if response := putVariable(t, handler, `{"var_name":"feishu|sso.enabled","var_value":true}`); response.Code != int(common.CodeSuccess) {
|
||||
t.Fatalf("enable: code = %d, message = %s", response.Code, response.Message)
|
||||
}
|
||||
if response := putVariable(t, handler, `{"var_name":"mail.enabled","var_value":false}`); response.Code != int(common.CodeSuccess) {
|
||||
if response := putVariable(t, handler, `{"var_name":"feishu|sso.enabled","var_value":false}`); response.Code != int(common.CodeSuccess) {
|
||||
t.Fatalf("disable: code = %d, message = %s", response.Code, response.Message)
|
||||
}
|
||||
|
||||
var stored entity.SystemSettings
|
||||
if err := db.Where("name = ?", "mail.enabled").First(&stored).Error; err != nil {
|
||||
if err := db.Where("name = ?", "feishu|sso.enabled").First(&stored).Error; err != nil {
|
||||
t.Fatalf("load stored setting: %v", err)
|
||||
}
|
||||
if stored.Value != "false" {
|
||||
@@ -190,9 +190,9 @@ func TestSetVariableRejectsUnusableRequests(t *testing.T) {
|
||||
// A value rejected by ValidateSystemSettingValue surfaces as a
|
||||
// server error, which is the pre-existing contract of this endpoint.
|
||||
name: "bool-typed setting rejects non bool",
|
||||
body: `{"var_name":"mail.enabled","var_value":"yes"}`,
|
||||
body: `{"var_name":"feishu|sso.enabled","var_value":"yes"}`,
|
||||
wantCode: common.CodeServerError,
|
||||
wantMessage: "invalid bool value for mail.enabled: expected true or false",
|
||||
wantMessage: "invalid bool value for feishu|sso.enabled: expected true or false",
|
||||
},
|
||||
{
|
||||
// Falsy scalars are legitimate values, not missing fields.
|
||||
|
||||
@@ -262,6 +262,7 @@ const (
|
||||
EnvFirecrawlRelayTimeout = "FIRECRAWL_RELAY_TIMEOUT"
|
||||
EnvRAGFlowSecretKey = "RAGFLOW_SECRET_KEY"
|
||||
EnvEnableRegister = "ENABLE_REGISTER"
|
||||
EnvDisablePasswordLogin = "DISABLE_PASSWORD_LOGIN"
|
||||
EnvMinioHost = "MINIO_HOST"
|
||||
EnvMinioRegion = "MINIO_REGION"
|
||||
EnvLang = "LANG"
|
||||
|
||||
@@ -105,7 +105,7 @@ func (h *AuthHandler) BetaAuthMiddleware() gin.HandlerFunc {
|
||||
ctx := c.Request.Context()
|
||||
auth := c.GetHeader("Authorization")
|
||||
if auth == "" {
|
||||
if cookie, err := c.Cookie(authCookie); err == nil {
|
||||
if cookie, err := c.Cookie(oauthAuthCookie); err == nil {
|
||||
auth = cookie
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +41,14 @@ func jsonInternalError(c *gin.Context, err error) {
|
||||
|
||||
// HandleNoRoute handles requests to undefined routes
|
||||
func HandleNoRoute(c *gin.Context) {
|
||||
// Python parity: these malformed paths resolved to a Werkzeug
|
||||
// MethodNotAllowed in the Python API, which server_error_response renders
|
||||
// as HTTP 200 / code 100 with the exception's repr() as the message. gin
|
||||
// instead falls through to NoRoute, so emit the same body here to keep the
|
||||
// error paths byte-for-byte aligned.
|
||||
if (c.Request.Method == http.MethodPost && c.Request.URL.Path == "/api/v1/chats//sessions") ||
|
||||
// Python parity: GET /api/v1/auth/login/ (an empty OAuth channel) resolves
|
||||
// to a Werkzeug MethodNotAllowed in the Python API, which
|
||||
// server_error_response renders as HTTP 200 / code 100 with the
|
||||
// exception's repr() as the message. gin instead falls through to
|
||||
// NoRoute, so emit the same body here to keep the auth error paths
|
||||
// byte-for-byte aligned.
|
||||
if (c.Request.Method == http.MethodGet && c.Request.URL.Path == "/api/v1/auth/login/") ||
|
||||
(c.Request.Method == http.MethodPost && c.Request.URL.Path == "/api/v1/chats//sessions") ||
|
||||
(c.Request.Method == http.MethodPatch && strings.HasSuffix(c.Request.URL.Path, "/sessions/")) {
|
||||
common.ResponseWithCodeData(c, common.CodeExceptionError, false, "<MethodNotAllowed '405: Method Not Allowed'>")
|
||||
return
|
||||
|
||||
+37
-17
@@ -45,21 +45,25 @@ func NewUserHandler(userService *service.UserService) *UserHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// authCookie carries the signed access token to the SPA after login so its
|
||||
// JavaScript can send it back as an Authorization header on later requests.
|
||||
// Not HttpOnly because the SPA has to read it; the server validates the token
|
||||
// signature and expiry on every request.
|
||||
const authCookie = "ragflow_auth"
|
||||
// oauthAuthCookie is the cookie the callback writes on success, so the SPA
|
||||
// can pick up the signed access token after the redirect. The frontend
|
||||
// reads it and either re-issues the value as an Authorization header on
|
||||
// subsequent API calls or hands it off to its own token store. Not
|
||||
// HttpOnly so the SPA's JS can read it.
|
||||
const oauthAuthCookie = "ragflow_auth"
|
||||
|
||||
// setAuthCookie writes the signed access token so the SPA can send it back as
|
||||
// a bearer token. Not HttpOnly so the SPA can copy it into its Authorization
|
||||
// header. The server checks the token signature and expiry on each request.
|
||||
func setAuthCookie(c *gin.Context, token string) {
|
||||
// codeql[go/cookie-httponly-not-set] Intentional: the SPA reads the cookie
|
||||
// via document.cookie, and the API validates the token signature and
|
||||
// expiry on each request.
|
||||
// setOAuthAuthCookie writes the signed access token so the SPA can pick it
|
||||
// up after the redirect. Not HttpOnly so the SPA can copy it into its
|
||||
// Authorization header on subsequent fetches. The server checks the token
|
||||
// signature and expiry on each request.
|
||||
func setOAuthAuthCookie(c *gin.Context, token string) {
|
||||
// the SPA's bootstrap credential after the OAuth redirect. The
|
||||
// SPA reads it via document.cookie and copies it into the
|
||||
// Authorization header. Setting HttpOnly would break the login
|
||||
// flow. The API validates the token signature and expiry on each request.
|
||||
// codeql[go/cookie-httponly-not-set] Intentional: this cookie is
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
Name: authCookie,
|
||||
Name: oauthAuthCookie,
|
||||
Value: token,
|
||||
Path: "/",
|
||||
MaxAge: 60 * 60 * 24 * 7,
|
||||
@@ -106,7 +110,7 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Header("Authorization", authToken)
|
||||
setAuthCookie(c, authToken)
|
||||
setOAuthAuthCookie(c, authToken)
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "*")
|
||||
c.Header("Access-Control-Allow-Headers", "*")
|
||||
@@ -184,7 +188,7 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
|
||||
// Set Authorization header with signed token
|
||||
c.Header("Authorization", authToken)
|
||||
setAuthCookie(c, authToken)
|
||||
setOAuthAuthCookie(c, authToken)
|
||||
// Set CORS headers
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "*")
|
||||
@@ -275,7 +279,7 @@ func (h *UserHandler) LoginByEmail(c *gin.Context) {
|
||||
operationLog.DurationMS = time.Since(startAt).Milliseconds()
|
||||
return
|
||||
}
|
||||
setAuthCookie(c, authToken)
|
||||
setOAuthAuthCookie(c, authToken)
|
||||
c.Header("Authorization", authToken)
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "*")
|
||||
@@ -339,7 +343,7 @@ func (h *UserHandler) Logout(c *gin.Context) {
|
||||
}()
|
||||
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
Name: authCookie,
|
||||
Name: oauthAuthCookie,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
@@ -476,6 +480,22 @@ func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
common.SuccessWithData(c, true, "password changed successfully")
|
||||
}
|
||||
|
||||
// GetLoginChannels get all supported authentication channels
|
||||
// @Summary Get Login Channels
|
||||
// @Description Get all supported OAuth authentication channels
|
||||
// @Tags users
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /v1/user/login/channels [get]
|
||||
func (h *UserHandler) GetLoginChannelsDeprecated(c *gin.Context) {
|
||||
channels, code, err := h.userService.GetLoginChannels()
|
||||
if err != nil {
|
||||
common.ResponseWithCodeData(c, code, []interface{}{}, "Load channels failure, error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithData(c, channels, "success")
|
||||
}
|
||||
|
||||
// SetTenantInfo update tenant information
|
||||
// @Summary Set Tenant Info
|
||||
// @Description Update tenant model configuration
|
||||
|
||||
@@ -156,6 +156,11 @@ func Init(configPath string) error {
|
||||
return fmt.Errorf("parse default models config error: %w", err)
|
||||
}
|
||||
|
||||
err = globalConfig.ParseOAuthConfig(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse OAuth config error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ const (
|
||||
)
|
||||
|
||||
type AuthenticationConfig struct {
|
||||
EnableRegister bool `mapstructure:"enable_register"`
|
||||
DisablePasswordLogin bool `mapstructure:"disable_password_login"`
|
||||
EnableRegister bool `mapstructure:"enable_register"`
|
||||
}
|
||||
|
||||
type APIServerConfig struct {
|
||||
@@ -93,6 +94,7 @@ func (c *Config) ParseAPIServerConfig(v *viper.Viper) error {
|
||||
|
||||
func (c *Config) parseAuthenticationConfig(v *viper.Viper) {
|
||||
apiServerConfig := &c.apiServer
|
||||
apiServerConfig.Authentication.DisablePasswordLogin = false
|
||||
apiServerConfig.Authentication.EnableRegister = true
|
||||
|
||||
if !v.IsSet("authentication") {
|
||||
@@ -103,11 +105,22 @@ func (c *Config) parseAuthenticationConfig(v *viper.Viper) {
|
||||
return
|
||||
}
|
||||
|
||||
if sub.IsSet("disable_password_login") {
|
||||
apiServerConfig.Authentication.DisablePasswordLogin = sub.GetBool("disable_password_login")
|
||||
}
|
||||
|
||||
if sub.IsSet("enable_register") {
|
||||
apiServerConfig.Authentication.EnableRegister = sub.GetBool("enable_register")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) DisablePasswordLogin() bool {
|
||||
if c.environments.DisablePasswordLogin != nil {
|
||||
return *c.environments.DisablePasswordLogin
|
||||
}
|
||||
return c.apiServer.Authentication.DisablePasswordLogin
|
||||
}
|
||||
|
||||
func (c *Config) EnableRegister() bool {
|
||||
if c.environments.EnableRegister != nil {
|
||||
return *c.environments.EnableRegister
|
||||
|
||||
@@ -43,4 +43,5 @@ type Config struct {
|
||||
// For EE
|
||||
defaultModels DefaultModelsConfig
|
||||
billing BillingConfig
|
||||
oAuth OAuthConfig
|
||||
}
|
||||
@@ -30,7 +30,8 @@ type Environments struct {
|
||||
|
||||
SecretKey string `mapstructure:"secret_key"`
|
||||
|
||||
EnableRegister *bool `mapstructure:"enable_register"`
|
||||
EnableRegister *bool `mapstructure:"enable_register"`
|
||||
DisablePasswordLogin *bool `mapstructure:"disable_password_login"`
|
||||
|
||||
DocumentEngineType string `mapstructure:"document_engine_type"`
|
||||
DatabaseType string `mapstructure:"database_type"`
|
||||
@@ -143,6 +144,18 @@ func (c *Config) GetEnvironments() error {
|
||||
}
|
||||
}
|
||||
|
||||
if envVal := common.GetEnv(common.EnvDisablePasswordLogin); envVal != "" {
|
||||
if c.environments.DisablePasswordLogin == nil {
|
||||
c.environments.DisablePasswordLogin = new(bool)
|
||||
}
|
||||
str := envVal
|
||||
if str == "true" || str == "1" || str == "yes" {
|
||||
*c.environments.DisablePasswordLogin = true
|
||||
} else {
|
||||
*c.environments.DisablePasswordLogin = false
|
||||
}
|
||||
}
|
||||
|
||||
// Doc engine
|
||||
docEngine := common.GetEnvSmall(common.EnvDocEngine)
|
||||
if docEngine != "" {
|
||||
@@ -587,6 +600,10 @@ func (c *Config) GetEnvEnableRegister() bool {
|
||||
return *c.environments.EnableRegister
|
||||
}
|
||||
|
||||
func (c *Config) GetEnvDisablePasswordLogin() bool {
|
||||
return *c.environments.DisablePasswordLogin
|
||||
}
|
||||
|
||||
func (c *Config) GetEnvDocumentEngineType() string {
|
||||
return c.environments.DocumentEngineType
|
||||
}
|
||||
|
||||
@@ -82,3 +82,11 @@ func (c *Config) GetDefaultOCRModel() ModelConfig { return c.defaultModels.OCRMo
|
||||
|
||||
// GetDefaultTTSModel returns the deployment TTS model, empty in the OSS build.
|
||||
func (c *Config) GetDefaultTTSModel() ModelConfig { return c.defaultModels.TTSModel }
|
||||
|
||||
// OAuthConfig carries the OAuth/OIDC login provider settings. The OSS build
|
||||
// has no OAuth login.
|
||||
type OAuthConfig struct{}
|
||||
|
||||
// ParseOAuthConfig ignores OAuth configuration: the OSS build has no OAuth
|
||||
// login.
|
||||
func (c *Config) ParseOAuthConfig(v *viper.Viper) error { return nil }
|
||||
@@ -46,7 +46,7 @@ backends:
|
||||
- password: ""
|
||||
- password: null
|
||||
password_policy: strict
|
||||
enable_register: true
|
||||
disable_password_login: true
|
||||
max_token: 64
|
||||
`)); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -80,10 +80,10 @@ max_token: 64
|
||||
map[string]any{"password": "******"},
|
||||
map[string]any{"password": "******"},
|
||||
},
|
||||
"credentials": wantCredentials,
|
||||
"password_policy": "strict",
|
||||
"enable_register": true,
|
||||
"max_token": int64(64),
|
||||
"credentials": wantCredentials,
|
||||
"password_policy": "strict",
|
||||
"disable_password_login": true,
|
||||
"max_token": int64(64),
|
||||
}
|
||||
if !reflect.DeepEqual(settings, want) {
|
||||
t.Fatalf("logged settings = %#v, want %#v", settings, want)
|
||||
|
||||
@@ -78,3 +78,16 @@ func resolveDefaultTenantIDEE(context.Context, string) (string, bool, error) {
|
||||
func resolveUserAuthTenant(_ context.Context, _ *entity.User, tenantID string) (string, error) {
|
||||
return tenantID, nil
|
||||
}
|
||||
|
||||
// LoginChannel is one authentication channel advertised to the login page.
|
||||
type LoginChannel struct {
|
||||
Channel string `json:"channel"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
// GetLoginChannels returns no channel: the community edition only offers
|
||||
// password login.
|
||||
func (s *UserService) GetLoginChannels() ([]*LoginChannel, common.ErrorCode, error) {
|
||||
return make([]*LoginChannel, 0), common.CodeSuccess, nil
|
||||
}
|
||||
@@ -41,14 +41,16 @@ func NewSystemService() *SystemService {
|
||||
|
||||
// ConfigResponse system configuration response
|
||||
type ConfigResponse struct {
|
||||
EnableRegister bool `json:"registerEnabled"`
|
||||
EnableRegister bool `json:"registerEnabled"`
|
||||
DisablePasswordLogin bool `json:"disablePasswordLogin"`
|
||||
}
|
||||
|
||||
// GetConfig get system configuration
|
||||
func (s *SystemService) GetConfig() (*ConfigResponse, error) {
|
||||
cfg := server.GetConfig()
|
||||
return &ConfigResponse{
|
||||
EnableRegister: cfg.EnableRegister(),
|
||||
EnableRegister: cfg.EnableRegister(),
|
||||
DisablePasswordLogin: cfg.DisablePasswordLogin(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,10 @@ func TestRESTSystemPublicContracts(t *testing.T) {
|
||||
}
|
||||
systemConfig := restOK(t, restCall(t, &config, http.MethodGet, "/system/config", nil, nil))
|
||||
configData := object(t, systemConfig["data"])
|
||||
if _, ok := configData["registerEnabled"]; !ok {
|
||||
t.Fatalf("system config missing %q: %v", "registerEnabled", configData)
|
||||
for _, key := range []string{"registerEnabled", "disablePasswordLogin"} {
|
||||
if _, ok := configData[key]; !ok {
|
||||
t.Fatalf("system config missing %q: %v", key, configData)
|
||||
}
|
||||
}
|
||||
health := restCall(t, &config, http.MethodGet, "/system/healthz", nil, nil)
|
||||
if health.Status != http.StatusOK && health.Status != http.StatusInternalServerError {
|
||||
|
||||
Reference in new issue
Block a user