From 7770a4525349d735ba9598dd46de296499104324 Mon Sep 17 00:00:00 2001 From: GareArc Date: Fri, 18 Apr 2025 05:02:26 -0400 Subject: [PATCH 01/12] fix: add password security update --- .../console/auth/forgot_password.py | 26 ++++++++++++++++--- api/controllers/console/auth/login.py | 4 ++- api/controllers/console/wraps.py | 16 +++++++++++- api/services/account_service.py | 22 +++++++++++++--- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index a9c4300b9a..2ee9c7c468 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -6,9 +6,13 @@ from flask_restful import Resource, reqparse # type: ignore from constants.languages import languages from controllers.console import api -from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError -from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError -from controllers.console.wraps import setup_required +from controllers.console.auth.error import (EmailCodeError, InvalidEmailError, + InvalidTokenError, + PasswordMismatchError) +from controllers.console.error import (AccountInFreezeError, AccountNotFound, + EmailSendIpLimitError) +from controllers.console.wraps import (email_password_login_enabled, + setup_required) from events.tenant_event import tenant_was_created from extensions.ext_database import db from libs.helper import email, extract_remote_ip @@ -22,6 +26,7 @@ from services.feature_service import FeatureService class ForgotPasswordSendEmailApi(Resource): @setup_required + @email_password_login_enabled def post(self): parser = reqparse.RequestParser() parser.add_argument("email", type=email, required=True, location="json") @@ -53,6 +58,7 @@ class ForgotPasswordSendEmailApi(Resource): class ForgotPasswordCheckApi(Resource): @setup_required + @email_password_login_enabled def post(self): parser = reqparse.RequestParser() parser.add_argument("email", type=str, required=True, location="json") @@ -72,11 +78,20 @@ class ForgotPasswordCheckApi(Resource): if args["code"] != token_data.get("code"): raise EmailCodeError() - return {"is_valid": True, "email": token_data.get("email")} + # Verified, revoke the first token + AccountService.revoke_reset_password_token(args["token"]) + + # Refresh token data by generating a new token + _, new_token = AccountService.generate_reset_password_token( + user_email, code=args["code"], additional_data={"phase": "reset"} + ) + + return {"is_valid": True, "email": token_data.get("email"), "token": new_token} class ForgotPasswordResetApi(Resource): @setup_required + @email_password_login_enabled def post(self): parser = reqparse.RequestParser() parser.add_argument("token", type=str, required=True, nullable=False, location="json") @@ -95,6 +110,9 @@ class ForgotPasswordResetApi(Resource): if reset_data is None: raise InvalidTokenError() + # Must use token in reset phase + if reset_data.get("phase", "") != "reset": + raise InvalidTokenError() AccountService.revoke_reset_password_token(token) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 41362e9fa2..16c1dcc441 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -22,7 +22,7 @@ from controllers.console.error import ( EmailSendIpLimitError, NotAllowedCreateWorkspace, ) -from controllers.console.wraps import setup_required +from controllers.console.wraps import email_password_login_enabled, setup_required from events.tenant_event import tenant_was_created from libs.helper import email, extract_remote_ip from libs.password import valid_password @@ -38,6 +38,7 @@ class LoginApi(Resource): """Resource for user login.""" @setup_required + @email_password_login_enabled def post(self): """Authenticate user and login.""" parser = reqparse.RequestParser() @@ -110,6 +111,7 @@ class LogoutApi(Resource): class ResetPasswordSendEmailApi(Resource): @setup_required + @email_password_login_enabled def post(self): parser = reqparse.RequestParser() parser.add_argument("email", type=email, required=True, location="json") diff --git a/api/controllers/console/wraps.py b/api/controllers/console/wraps.py index 510ea242a9..11b7140b1b 100644 --- a/api/controllers/console/wraps.py +++ b/api/controllers/console/wraps.py @@ -11,7 +11,8 @@ from models.model import DifySetup from services.feature_service import FeatureService, LicenseStatus from services.operation_service import OperationService -from .error import NotInitValidateError, NotSetupError, UnauthorizedAndForceLogout +from .error import (NotInitValidateError, NotSetupError, + UnauthorizedAndForceLogout) def account_initialization_required(view): @@ -165,3 +166,16 @@ def enterprise_license_required(view): return view(*args, **kwargs) return decorated + + +def email_password_login_enabled(view): + @wraps(view) + def decorated(*args, **kwargs): + features = FeatureService.get_system_features() + if features.enable_email_password_login: + return view(*args, **kwargs) + + # otherwise, return 403 + abort(403) + + return decorated diff --git a/api/services/account_service.py b/api/services/account_service.py index dd1cc5f94f..2fbc446984 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -406,10 +406,8 @@ class AccountService: raise PasswordResetRateLimitExceededError() - code = "".join([str(random.randint(0, 9)) for _ in range(6)]) - token = TokenManager.generate_token( - account=account, email=email, token_type="reset_password", additional_data={"code": code} - ) + code, token = cls.generate_reset_password_token(account_email, account) + send_reset_password_mail_task.delay( language=language, to=account_email, @@ -418,6 +416,22 @@ class AccountService: cls.reset_password_rate_limiter.increment_rate_limit(account_email) return token + @classmethod + def generate_reset_password_token( + cls, + email: str, + account: Optional[Account] = None, + code: Optional[str] = None, + additional_data: dict[str, Any] = {}, + ): + if not code: + code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + additional_data["code"] = code + token = TokenManager.generate_token( + account=account, email=email, token_type="reset_password", additional_data=additional_data + ) + return code, token + @classmethod def revoke_reset_password_token(cls, token: str): TokenManager.revoke_token(token, "reset_password") From 509733fbf0b8efa19e2168771409f21f33ec94dd Mon Sep 17 00:00:00 2001 From: NFish Date: Fri, 18 Apr 2025 17:15:02 +0800 Subject: [PATCH 02/12] fix: update reset password token when email code verify success (#18367) --- web/app/reset-password/check-code/page.tsx | 6 +++++- web/service/common.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/web/app/reset-password/check-code/page.tsx b/web/app/reset-password/check-code/page.tsx index ca53b68750..b8cb87629b 100644 --- a/web/app/reset-password/check-code/page.tsx +++ b/web/app/reset-password/check-code/page.tsx @@ -39,7 +39,11 @@ export default function CheckCode() { } setIsLoading(true) const ret = await verifyResetPasswordCode({ email, code, token }) - ret.is_valid && router.push(`/reset-password/set-password?${searchParams.toString()}`) + if (ret.is_valid) { + const params = new URLSearchParams(searchParams) + params.set('token', encodeURIComponent(ret.token)) + router.push(`/reset-password/set-password?${params.toString()}`) + } } catch (error) { console.error(error) } finally { diff --git a/web/service/common.ts b/web/service/common.ts index 7ccc6eb5c3..06562ece0c 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -338,7 +338,7 @@ export const sendResetPasswordCode = (email: string, language = 'en-US') => post('/forgot-password', { body: { email, language } }) export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }) => - post('/forgot-password/validity', { body }) + post('/forgot-password/validity', { body }) export const sendDeleteAccountCode = () => get('/account/delete/verify') From 2af1dd6de38b51500e0fae85160a58d9e545d760 Mon Sep 17 00:00:00 2001 From: GareArc Date: Sun, 20 Apr 2025 23:30:59 -0400 Subject: [PATCH 03/12] feat: add webapp auth apis --- api/controllers/web/app.py | 58 ++++++++++++++++++- api/services/app_service.py | 14 ++++- api/services/enterprise/enterprise_service.py | 4 +- 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/api/controllers/web/app.py b/api/controllers/web/app.py index 20e071c834..11db386b2e 100644 --- a/api/controllers/web/app.py +++ b/api/controllers/web/app.py @@ -1,12 +1,18 @@ -from flask_restful import marshal_with # type: ignore +import logging + +from flask import request +from flask_login import current_user +from flask_restful import Resource, marshal_with, reqparse # type: ignore from controllers.common import fields from controllers.common import helpers as controller_helpers from controllers.web import api from controllers.web.error import AppUnavailableError from controllers.web.wraps import WebApiResource +from libs.passport import PassportService from models.model import App, AppMode from services.app_service import AppService +from services.enterprise.enterprise_service import EnterpriseService class AppParameterApi(WebApiResource): @@ -42,5 +48,55 @@ class AppMeta(WebApiResource): return AppService().get_app_meta(app_model) +class AppAccessMode(Resource): + def get(self): + parser = reqparse.RequestParser() + parser.add_argument("appId", type=str, required=True, location="args") + args = parser.parse_args() + + app_id = args["appId"] + res = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id) + + return {"accessMode": res.access_mode} + + +class AppWebAuthPermission(Resource): + def get(self): + user_id = "visitor" + try: + auth_header = request.headers.get("Authorization") + if auth_header is None: + raise + if " " not in auth_header: + raise + + auth_scheme, tk = auth_header.split(None, 1) + auth_scheme = auth_scheme.lower() + if auth_scheme != "bearer": + raise + + decoded = PassportService().verify(tk) + user_id = decoded.get("user_id", "visitor") + except Exception as e: + pass + + parser = reqparse.RequestParser() + parser.add_argument("appId", type=str, required=True, location="args") + args = parser.parse_args() + + app_id = args["appId"] + user_id = current_user.id + logging.info(f"App ID: {app_id}, User ID: {user_id}") + + app_code = AppService.get_app_code_by_id(app_id) + logging.info(f"App code: {app_code}") + + res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_code) + return {"result": res} + + api.add_resource(AppParameterApi, "/parameters") api.add_resource(AppMeta, "/meta") +# webapp auth apis +api.add_resource(AppAccessMode, "/webapp/access-mode") +api.add_resource(AppWebAuthPermission, "/webapp/permission") diff --git a/api/services/app_service.py b/api/services/app_service.py index 9359bb2844..e6a1ae32a9 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -19,7 +19,7 @@ from core.tools.utils.configuration import ToolParameterConfigurationManager from events.app_event import app_was_created from extensions.ext_database import db from models.account import Account -from models.model import App, AppMode, AppModelConfig +from models.model import App, AppMode, AppModelConfig, Site from models.tools import ApiToolProvider from services.enterprise.enterprise_service import EnterpriseService from services.feature_service import FeatureService @@ -384,3 +384,15 @@ class AppService: meta["tool_icons"][tool_name] = {"background": "#252525", "content": "\ud83d\ude01"} return meta + + @staticmethod + def get_app_code_by_id(app_id: str) -> str: + """ + Get app code by app id + :param app_id: app id + :return: app code + """ + site = db.session.query(Site).filter(Site.app_id == app_id).first() + if not site: + raise ValueError(f"App with id {app_id} not found") + return str(site.code) diff --git a/api/services/enterprise/enterprise_service.py b/api/services/enterprise/enterprise_service.py index e44e7f6658..dd0857745e 100644 --- a/api/services/enterprise/enterprise_service.py +++ b/api/services/enterprise/enterprise_service.py @@ -45,12 +45,12 @@ class EnterpriseService: if not data: raise ValueError("No data found.") - if not isinstance(data['accessModes'], dict): + if not isinstance(data["accessModes"], dict): logging.info(f"Batch get app access mode by id returns data: {data}") raise ValueError("Invalid data format.") ret = {} - for key, value in data['accessModes'].items(): + for key, value in data["accessModes"].items(): curr = WebAppSettings() curr.access_mode = value ret[key] = curr From e52a9fbfb7918c7a6d2261d03348b663c073af05 Mon Sep 17 00:00:00 2001 From: GareArc Date: Sun, 20 Apr 2025 23:33:51 -0400 Subject: [PATCH 04/12] fix: remove curr user in webapp permission api --- api/controllers/web/app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/controllers/web/app.py b/api/controllers/web/app.py index 11db386b2e..823b80cfce 100644 --- a/api/controllers/web/app.py +++ b/api/controllers/web/app.py @@ -1,7 +1,6 @@ import logging from flask import request -from flask_login import current_user from flask_restful import Resource, marshal_with, reqparse # type: ignore from controllers.common import fields @@ -85,7 +84,6 @@ class AppWebAuthPermission(Resource): args = parser.parse_args() app_id = args["appId"] - user_id = current_user.id logging.info(f"App ID: {app_id}, User ID: {user_id}") app_code = AppService.get_app_code_by_id(app_id) From a27db51b8347c76009da670c462251614f3415e4 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 21 Apr 2025 02:06:07 -0400 Subject: [PATCH 05/12] fix: update webapp auth api path --- api/controllers/web/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/web/app.py b/api/controllers/web/app.py index 823b80cfce..967017b83c 100644 --- a/api/controllers/web/app.py +++ b/api/controllers/web/app.py @@ -96,5 +96,5 @@ class AppWebAuthPermission(Resource): api.add_resource(AppParameterApi, "/parameters") api.add_resource(AppMeta, "/meta") # webapp auth apis -api.add_resource(AppAccessMode, "/webapp/access-mode") -api.add_resource(AppWebAuthPermission, "/webapp/permission") +api.add_resource(AppAccessMode, "/enterprise/webapp/app/access-mode") +api.add_resource(AppWebAuthPermission, "/enterprise/webapp/permission") From c6e63ac816065e3d6cb544aaa61b9a9739b59f78 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 21 Apr 2025 02:07:43 -0400 Subject: [PATCH 06/12] Revert "fix: update webapp auth api path" This reverts commit a27db51b8347c76009da670c462251614f3415e4. --- api/controllers/web/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/web/app.py b/api/controllers/web/app.py index 967017b83c..823b80cfce 100644 --- a/api/controllers/web/app.py +++ b/api/controllers/web/app.py @@ -96,5 +96,5 @@ class AppWebAuthPermission(Resource): api.add_resource(AppParameterApi, "/parameters") api.add_resource(AppMeta, "/meta") # webapp auth apis -api.add_resource(AppAccessMode, "/enterprise/webapp/app/access-mode") -api.add_resource(AppWebAuthPermission, "/enterprise/webapp/permission") +api.add_resource(AppAccessMode, "/webapp/access-mode") +api.add_resource(AppWebAuthPermission, "/webapp/permission") From 9462ed7bbf7d25015151bd792f2ed69a136e84f3 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 21 Apr 2025 18:47:24 -0400 Subject: [PATCH 07/12] fix: add auth constraint to explore apps --- api/controllers/console/explore/wraps.py | 31 ++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py index b7ba81fba2..a455855866 100644 --- a/api/controllers/console/explore/wraps.py +++ b/api/controllers/console/explore/wraps.py @@ -2,12 +2,15 @@ from functools import wraps from flask_login import current_user # type: ignore from flask_restful import Resource # type: ignore -from werkzeug.exceptions import NotFound +from werkzeug.exceptions import NotFound, Unauthorized from controllers.console.wraps import account_initialization_required from extensions.ext_database import db from libs.login import login_required from models import InstalledApp +from services.app_service import AppService +from services.enterprise.enterprise_service import EnterpriseService +from services.feature_service import FeatureService def installed_app_required(view=None): @@ -48,6 +51,30 @@ def installed_app_required(view=None): return decorator +def user_allowed_to_access_app(view=None): + def decorator(view): + @wraps(view) + def decorated(*args, **kwargs): + feature = FeatureService.get_system_features() + if feature.webapp_auth.enabled: + app_id = kwargs.get("installed_app_id") + app_code = AppService.get_app_code_by_id(app_id) + res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp( + user_id=str(current_user.id), + app_code=app_code, + ) + if not res: + raise Unauthorized("User not allowed to access this app") + + return view(*args, **kwargs) + + return decorated + if view: + return decorator(view) + return decorator + + class InstalledAppResource(Resource): # must be reversed if there are multiple decorators - method_decorators = [installed_app_required, account_initialization_required, login_required] + + method_decorators = [user_allowed_to_access_app, installed_app_required, account_initialization_required, login_required] From d1a25e54e547c0d02be25cb3b6427ec958e9f76f Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 21 Apr 2025 18:48:24 -0400 Subject: [PATCH 08/12] fix: add logging --- api/controllers/console/explore/wraps.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py index a455855866..f91a0c98ed 100644 --- a/api/controllers/console/explore/wraps.py +++ b/api/controllers/console/explore/wraps.py @@ -1,3 +1,4 @@ +import logging from functools import wraps from flask_login import current_user # type: ignore @@ -59,10 +60,12 @@ def user_allowed_to_access_app(view=None): if feature.webapp_auth.enabled: app_id = kwargs.get("installed_app_id") app_code = AppService.get_app_code_by_id(app_id) + logging.info(f"app_id: {app_id}, app_code: {app_code}") res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp( user_id=str(current_user.id), app_code=app_code, ) + logging.info(f"res: {res}") if not res: raise Unauthorized("User not allowed to access this app") From 455d14296f5f8d5e15b410dfd407c877f48d1f0d Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 21 Apr 2025 19:03:10 -0400 Subject: [PATCH 09/12] fix: get app id from upstream decorator --- api/controllers/console/explore/wraps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py index f91a0c98ed..e27ed5aa7b 100644 --- a/api/controllers/console/explore/wraps.py +++ b/api/controllers/console/explore/wraps.py @@ -55,10 +55,10 @@ def installed_app_required(view=None): def user_allowed_to_access_app(view=None): def decorator(view): @wraps(view) - def decorated(*args, **kwargs): + def decorated(installed_app: InstalledApp, *args, **kwargs): feature = FeatureService.get_system_features() if feature.webapp_auth.enabled: - app_id = kwargs.get("installed_app_id") + app_id = installed_app.app_id app_code = AppService.get_app_code_by_id(app_id) logging.info(f"app_id: {app_id}, app_code: {app_code}") res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp( @@ -69,7 +69,7 @@ def user_allowed_to_access_app(view=None): if not res: raise Unauthorized("User not allowed to access this app") - return view(*args, **kwargs) + return view(installed_app, *args, **kwargs) return decorated if view: From bfa5828259a5b8e59de1d345b1805bf764f7a839 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 21 Apr 2025 19:40:51 -0400 Subject: [PATCH 10/12] fix: temp fix for unauthorized user in explore page --- api/controllers/console/explore/wraps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py index e27ed5aa7b..4fbf4092fd 100644 --- a/api/controllers/console/explore/wraps.py +++ b/api/controllers/console/explore/wraps.py @@ -3,7 +3,7 @@ from functools import wraps from flask_login import current_user # type: ignore from flask_restful import Resource # type: ignore -from werkzeug.exceptions import NotFound, Unauthorized +from werkzeug.exceptions import NotFound from controllers.console.wraps import account_initialization_required from extensions.ext_database import db @@ -67,7 +67,7 @@ def user_allowed_to_access_app(view=None): ) logging.info(f"res: {res}") if not res: - raise Unauthorized("User not allowed to access this app") + raise ValueError("User not allowed to access this app") return view(installed_app, *args, **kwargs) From 724ffe55c98c16408af265f972b3f33d4980bf66 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 21 Apr 2025 22:02:50 -0400 Subject: [PATCH 11/12] fix: add back sso system feature --- api/services/feature_service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/services/feature_service.py b/api/services/feature_service.py index e62a94cc9d..cef945a8a7 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -177,6 +177,9 @@ class FeatureService: if "SSOEnforcedForSignin" in enterprise_info: features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"] + if "sso_enforced_for_signin_protocol" in enterprise_info: + features.sso_enforced_for_signin_protocol = enterprise_info["SSOEnforcedForSigninProtocol"] + if "EnableEmailCodeLogin" in enterprise_info: features.enable_email_code_login = enterprise_info["EnableEmailCodeLogin"] From 669fb6be0f0af76781155e0d512b4af446700cda Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 21 Apr 2025 22:18:16 -0400 Subject: [PATCH 12/12] fix: wrong field name --- api/services/feature_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/services/feature_service.py b/api/services/feature_service.py index cef945a8a7..105af59322 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -177,7 +177,7 @@ class FeatureService: if "SSOEnforcedForSignin" in enterprise_info: features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"] - if "sso_enforced_for_signin_protocol" in enterprise_info: + if "SSOEnforcedForSigninProtocol" in enterprise_info: features.sso_enforced_for_signin_protocol = enterprise_info["SSOEnforcedForSigninProtocol"] if "EnableEmailCodeLogin" in enterprise_info: