mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
feat: Add disable_password_login configuration to support SSO-only authentication (#13151)
### What problem does this PR solve? Enterprise deployments that use an external Identity Provider (e.g., Microsoft Entra ID, Okta, Keycloak) need the ability to enforce SSO-only authentication by hiding the email/password login form. Currently, the login page always shows the password form alongside OAuth buttons, with no way to disable it. This PR adds a `disable_password_login` configuration option under the existing `authentication` section in `service_conf.yaml`. When set to `true`, the login page only displays configured OAuth/SSO buttons and hides the email/password form, "Remember me" checkbox, and "Sign up" link. The flag can be set via: - `service_conf.yaml` (`authentication.disable_password_login: true`) - Environment variable (`DISABLE_PASSWORD_LOGIN=true`) Default behavior is unchanged (`false`). ### Behavior | `disable_password_login` | OAuth configured | Result | |---|---|---| | `false` (default) | No | Standard email/password form | | `false` | Yes | Email/password form + SSO buttons below | | `true` | Yes | **SSO buttons only** (no form, no sign up link) | | `true` | No | Empty card (admin should configure OAuth first) | ### Type of change - [x] New Feature (non-breaking change which adds functionality) ### Files changed (5) 1. `docker/service_conf.yaml.template` — added `disable_password_login: false` under authentication 2. `common/settings.py` — added `DISABLE_PASSWORD_LOGIN` global variable and loader in `init_settings()` 3. `common/config_utils.py` — fixed `TypeError` in `show_configs()` when authentication section contains non-dict values (e.g., booleans) 4. `api/apps/system_app.py` — exposed `disablePasswordLogin` flag in `/config` endpoint 5. `web/src/pages/login/index.tsx` — conditionally render password form based on config flag; OAuth buttons always render when channels exist --------- Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
This commit is contained in:
@@ -371,4 +371,7 @@ def get_config():
|
||||
type: integer 0 means disabled, 1 means enabled
|
||||
description: Whether user registration is enabled
|
||||
"""
|
||||
return get_json_result(data={"registerEnabled": settings.REGISTER_ENABLED})
|
||||
return get_json_result(data={
|
||||
"registerEnabled": settings.REGISTER_ENABLED,
|
||||
"disablePasswordLogin": settings.DISABLE_PASSWORD_LOGIN,
|
||||
})
|
||||
|
||||
@@ -102,7 +102,7 @@ def show_configs():
|
||||
if "authentication" in k:
|
||||
v = copy.deepcopy(v)
|
||||
for key, val in v.items():
|
||||
if "http_secret_key" in val:
|
||||
if isinstance(val, dict) and "http_secret_key" in val:
|
||||
val["http_secret_key"] = "*" * 8
|
||||
msg += f"\n\t{k}: {v}"
|
||||
logging.info(msg)
|
||||
@@ -152,4 +152,4 @@ def update_config(key, value, conf_name=SERVICE_CONF):
|
||||
with FileLock(os.path.join(os.path.dirname(conf_path), ".lock")):
|
||||
config = load_yaml_conf(conf_path=conf_path) or {}
|
||||
config[key] = value
|
||||
rewrite_yaml_conf(conf_path=conf_path, config=config)
|
||||
rewrite_yaml_conf(conf_path=conf_path, config=config)
|
||||
@@ -92,6 +92,8 @@ kg_retriever = None
|
||||
# user registration switch
|
||||
REGISTER_ENABLED = 1
|
||||
|
||||
# SSO-only mode: hide password login form
|
||||
DISABLE_PASSWORD_LOGIN = False
|
||||
|
||||
# sandbox-executor-manager
|
||||
SANDBOX_HOST = None
|
||||
@@ -186,6 +188,17 @@ def init_settings():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
global DISABLE_PASSWORD_LOGIN
|
||||
try:
|
||||
env_val = os.environ.get("DISABLE_PASSWORD_LOGIN", "").lower()
|
||||
if env_val in ("1", "true", "yes"):
|
||||
DISABLE_PASSWORD_LOGIN = True
|
||||
else:
|
||||
authentication_conf = get_base_config("authentication", {})
|
||||
DISABLE_PASSWORD_LOGIN = bool(authentication_conf.get("disable_password_login", False))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
global FACTORY_LLM_INFOS
|
||||
try:
|
||||
with open(os.path.join(get_project_base_directory(), "conf", "llm_factories.json"), "r") as f:
|
||||
|
||||
@@ -276,4 +276,7 @@ DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
|
||||
|
||||
# Used for ThreadPoolExecutor
|
||||
THREAD_POOL_MAX_WORKERS=128
|
||||
THREAD_POOL_MAX_WORKERS=128
|
||||
|
||||
#Option to disable login form for SSO
|
||||
DISABLE_PASSWORD_LOGIN=false
|
||||
@@ -143,12 +143,13 @@ user_default_llm:
|
||||
# client_secret: "your_client_secret"
|
||||
# redirect_uri: "https://your-app.com/v1/user/oauth/callback/github"
|
||||
# authentication:
|
||||
# client:
|
||||
# switch: false
|
||||
# http_app_key:
|
||||
# http_secret_key:
|
||||
# site:
|
||||
# switch: false
|
||||
# client:
|
||||
# switch: false
|
||||
# http_app_key:
|
||||
# http_secret_key:
|
||||
# site:
|
||||
# switch: false
|
||||
# disable_password_login: false
|
||||
# permission:
|
||||
# switch: false
|
||||
# component: false
|
||||
|
||||
@@ -76,6 +76,7 @@ def _load_system_module(monkeypatch):
|
||||
settings_mod.STORAGE_IMPL_TYPE = "MINIO"
|
||||
settings_mod.DATABASE_TYPE = "MYSQL"
|
||||
settings_mod.REGISTER_ENABLED = True
|
||||
settings_mod.DISABLE_PASSWORD_LOGIN = False
|
||||
common_pkg.settings = settings_mod
|
||||
monkeypatch.setitem(sys.modules, "common.settings", settings_mod)
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ type LoginFormContentProps = {
|
||||
channels: { channel: string; icon?: string; display_name: string }[];
|
||||
handleLoginWithChannel: (channel: string) => void;
|
||||
t: ReturnType<typeof useTranslation>['t'];
|
||||
disablePasswordLogin?: boolean;
|
||||
};
|
||||
|
||||
function LoginFormContent({
|
||||
@@ -56,6 +57,7 @@ function LoginFormContent({
|
||||
channels,
|
||||
handleLoginWithChannel,
|
||||
t,
|
||||
disablePasswordLogin,
|
||||
}: LoginFormContentProps) {
|
||||
const face = useContext(FlipFaceContext);
|
||||
const isActiveFace = isLoginPage ? face === 'front' : face === 'back';
|
||||
@@ -68,6 +70,7 @@ function LoginFormContent({
|
||||
</h2>
|
||||
</div>
|
||||
<div className=" w-full max-w-[540px] bg-bg-component backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-10 pr-10 pb-2 border border-border-button ">
|
||||
{!disablePasswordLogin && (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-8 text-text-primary "
|
||||
@@ -177,32 +180,35 @@ function LoginFormContent({
|
||||
>
|
||||
{title === 'login' ? t('login') : t('continue')}
|
||||
</ButtonLoading>
|
||||
{title === 'login' && channels && channels.length > 0 && (
|
||||
<div className="mt-3 border">
|
||||
{channels.map((item) => (
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
key={item.channel}
|
||||
onClick={() => handleLoginWithChannel(item.channel)}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<SvgIcon
|
||||
name={item.icon || 'sso'}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 5 }}
|
||||
/>
|
||||
Sign in with {item.display_name}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{title === 'login' && registerEnabled && (
|
||||
{title === 'login' && channels && channels.length > 0 && (
|
||||
<div className={disablePasswordLogin ? 'py-8' : 'mt-3 border'}>
|
||||
{channels.map((item) => (
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
key={item.channel}
|
||||
onClick={() => handleLoginWithChannel(item.channel)}
|
||||
style={{ marginTop: 10 }}
|
||||
className={disablePasswordLogin ? 'w-full' : ''}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<SvgIcon
|
||||
name={item.icon || 'sso'}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 5 }}
|
||||
/>
|
||||
Sign in with {item.display_name}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!disablePasswordLogin && title === 'login' && registerEnabled && (
|
||||
<div className="mt-10 text-right">
|
||||
<p className="text-text-disabled text-sm">
|
||||
{t('signInTip')}
|
||||
@@ -217,7 +223,7 @@ function LoginFormContent({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{title === 'register' && (
|
||||
{!disablePasswordLogin && title === 'register' && (
|
||||
<div className="mt-10 text-right">
|
||||
<p className="text-text-disabled text-sm">
|
||||
{t('signUpTip')}
|
||||
@@ -369,14 +375,8 @@ const Login = () => {
|
||||
<h1 className="text-[36px] font-medium text-center mb-2">
|
||||
{t('title')}
|
||||
</h1>
|
||||
{/* border border-accent-primary rounded-full */}
|
||||
{/* <div className="mt-4 px-6 py-1 text-sm font-medium text-cyan-600 hover:bg-cyan-50 transition-colors duration-200 border-glow relative overflow-hidden">
|
||||
{t('start')}
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="relative z-10 flex flex-col items-center justify-center min-h-[1050px] px-4 sm:px-6 lg:px-8">
|
||||
{/* Logo and Header */}
|
||||
|
||||
{/* Login Form */}
|
||||
<FlipCard3D isLoginPage={isLoginPage}>
|
||||
<LoginFormContent
|
||||
@@ -390,6 +390,7 @@ const Login = () => {
|
||||
channels={channels || []}
|
||||
handleLoginWithChannel={handleLoginWithChannel}
|
||||
t={t}
|
||||
disablePasswordLogin={!!config?.disablePasswordLogin}
|
||||
/>
|
||||
</FlipCard3D>
|
||||
</div>
|
||||
@@ -398,4 +399,4 @@ const Login = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
export default Login;
|
||||
Reference in New Issue
Block a user