diff --git a/api/.env.example b/api/.env.example index cd05b124e3..7b08c032ed 100644 --- a/api/.env.example +++ b/api/.env.example @@ -483,6 +483,7 @@ ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id} # Reset password token expiry minutes RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5 +OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5 CREATE_TIDB_SERVICE_JOB_ENABLED=false diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index e0f7f75421..2fd9f94e06 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -36,6 +36,11 @@ class SecurityConfig(BaseSettings): default=5, ) + OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES: PositiveInt = Field( + description="Duration in minutes for which a owner transfer token remains valid", + default=5, + ) + LOGIN_DISABLED: bool = Field( description="Whether to disable login checks", default=False, diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index 829fc797bd..4bca2516e8 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -95,3 +95,14 @@ class OwnerTransferLimitError(BaseHTTPException): error_code = "owner_transfer_limit" description = "Too many failed owner transfer attempts. Please try again in 24 hours." code = 429 + + +class NotOwnerError(BaseHTTPException): + error_code = "not_owner" + description = "You are not the owner of the workspace." + code = 400 + +class CannotTransferOwnerToSelfError(BaseHTTPException): + error_code = "cannot_transfer_owner_to_self" + description = "You cannot transfer ownership to yourself." + code = 400 \ No newline at end of file diff --git a/api/controllers/console/workspace/members.py b/api/controllers/console/workspace/members.py index 5bb509dc2f..df95596917 100644 --- a/api/controllers/console/workspace/members.py +++ b/api/controllers/console/workspace/members.py @@ -7,7 +7,7 @@ from flask_restful import Resource, abort, marshal_with, reqparse import services from configs import dify_config from controllers.console import api -from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, OwnerTransferLimitError +from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, OwnerTransferLimitError,NotOwnerError,CannotTransferOwnerToSelfError from controllers.console.error import EmailSendIpLimitError, WorkspaceMembersLimitExceeded from controllers.console.wraps import ( account_initialization_required, @@ -172,10 +172,17 @@ class SendOwnerTransferEmailApi(Resource): parser.add_argument("language", type=str, required=False, location="json") args = parser.parse_args() + # check if the current user is the owner of the workspace + if not TenantService.is_owner(current_user, current_user.current_tenant): + raise NotOwnerError() + + if current_user.id == str(member_id): + raise CannotTransferOwnerToSelfError() + ip_address = extract_remote_ip(request) if AccountService.is_email_send_ip_limit(ip_address): raise EmailSendIpLimitError() - + if args["language"] is not None and args["language"] == "zh-Hans": language = "zh-Hans" else: @@ -199,7 +206,7 @@ class SendOwnerTransferEmailApi(Resource): return {"result": "success", "data": token} -class OwnerTransferCheckEApi(Resource): +class OwnerTransferCheckApi(Resource): @setup_required @login_required @account_initialization_required @@ -209,10 +216,13 @@ class OwnerTransferCheckEApi(Resource): parser.add_argument("code", type=str, required=True, location="json") parser.add_argument("token", type=str, required=True, nullable=False, location="json") args = parser.parse_args() + # check if the current user is the owner of the workspace + if not TenantService.is_owner(current_user, current_user.current_tenant): + raise NotOwnerError() user_email = current_user.email - is_owner_transfer_error_rate_limit = AccountService.is_owner_transfer_error_rate_limit(args["email"]) + is_owner_transfer_error_rate_limit = AccountService.is_owner_transfer_error_rate_limit(user_email) if is_owner_transfer_error_rate_limit: raise OwnerTransferLimitError() @@ -224,7 +234,7 @@ class OwnerTransferCheckEApi(Resource): raise InvalidEmailError() if args["code"] != token_data.get("code"): - AccountService.add_owner_transfer_error_rate_limit(args["email"]) + AccountService.add_owner_transfer_error_rate_limit(user_email) raise EmailCodeError() # Verified, revoke the first token @@ -233,7 +243,7 @@ class OwnerTransferCheckEApi(Resource): # Refresh token data by generating a new token _, new_token = AccountService.generate_owner_transfer_token(user_email, code=args["code"], additional_data={}) - AccountService.reset_owner_transfer_error_rate_limit(args["email"]) + AccountService.reset_owner_transfer_error_rate_limit(user_email) return {"is_valid": True, "email": token_data.get("email"), "token": new_token} @@ -246,15 +256,21 @@ class OwnerTransfer(Resource): parser = reqparse.RequestParser() parser.add_argument("token", type=str, required=True, nullable=False, location="json") args = parser.parse_args() + + # check if the current user is the owner of the workspace + if not TenantService.is_owner(current_user, current_user.current_tenant): + raise NotOwnerError() + + if current_user.id == str(member_id): + raise CannotTransferOwnerToSelfError() transfer_token_data = AccountService.get_owner_transfer_data(args["token"]) if not transfer_token_data: - raise InvalidTokenError() - - if transfer_token_data.get("phase", "") != "owner_transfer": + print(transfer_token_data, "transfer_token_data") raise InvalidTokenError() if transfer_token_data.get("email") != current_user.email: + print(transfer_token_data.get("email"), current_user.email) raise InvalidEmailError() AccountService.revoke_owner_transfer_token(args["token"]) @@ -295,5 +311,5 @@ api.add_resource(DatasetOperatorMemberListApi, "/workspaces/current/dataset-oper api.add_resource( SendOwnerTransferEmailApi, "/workspaces/current/members//send-owner-transfer-confirm-email" ) -api.add_resource(OwnerTransferCheckEApi, "/workspaces/current/members/owner-transfer-check") +api.add_resource(OwnerTransferCheckApi, "/workspaces/current/members/owner-transfer-check") api.add_resource(OwnerTransfer, "/workspaces/current/members//owner-transfer") diff --git a/api/services/account_service.py b/api/services/account_service.py index fc3e4f4d4c..688a9d0fbb 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -1083,6 +1083,10 @@ class TenantService: return cast(dict, tenant.custom_config_dict) + @staticmethod + def is_owner(account: Account, tenant: Tenant) -> bool: + return TenantService.get_user_role(account, tenant) == TenantAccountRole.OWNER + class RegisterService: @classmethod diff --git a/api/tasks/mail_owner_transfer_task.py b/api/tasks/mail_owner_transfer_task.py index fab776898e..d9184248bd 100644 --- a/api/tasks/mail_owner_transfer_task.py +++ b/api/tasks/mail_owner_transfer_task.py @@ -15,7 +15,6 @@ def send_owner_transfer_confirm_task(language: str, to: str, code: str, workspac Async Send owner transfer confirm mail :param language: Language in which the email should be sent (e.g., 'en', 'zh') :param to: Recipient email address - :param code: Change email code :param workspace: Workspace name :param member: Member name """ @@ -59,12 +58,11 @@ def send_owner_transfer_confirm_task(language: str, to: str, code: str, workspac @shared_task(queue="mail") -def send_old_owner_transfer_notify_email_task(language: str, to: str, code: str, workspace: str, new_owner_email: str): +def send_old_owner_transfer_notify_email_task(language: str, to: str, workspace: str, new_owner_email: str): """ Async Send owner transfer confirm mail :param language: Language in which the email should be sent (e.g., 'en', 'zh') :param to: Recipient email address - :param code: Change email code :param workspace: Workspace name :param new_owner_email: New owner email """ @@ -116,7 +114,7 @@ def send_old_owner_transfer_notify_email_task(language: str, to: str, code: str, @shared_task(queue="mail") -def send_new_owner_transfer_notify_email_task(language: str, to: str, code: str, workspace: str): +def send_new_owner_transfer_notify_email_task(language: str, to: str, workspace: str): """ Async Send owner transfer confirm mail :param language: Language in which the email should be sent (e.g., 'en', 'zh') diff --git a/api/tests/integration_tests/.env.example b/api/tests/integration_tests/.env.example index 49e01140b5..2e98dec964 100644 --- a/api/tests/integration_tests/.env.example +++ b/api/tests/integration_tests/.env.example @@ -204,6 +204,7 @@ ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id} # Reset password token expiry minutes RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5 +OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5 CREATE_TIDB_SERVICE_JOB_ENABLED=false diff --git a/docker/.env.example b/docker/.env.example index 376fd573c3..e2d7436067 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -769,6 +769,7 @@ INVITE_EXPIRY_HOURS=72 # Reset password token valid time (minutes), RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5 +OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5 # The sandbox service endpoint. CODE_EXECUTION_ENDPOINT=http://sandbox:8194 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index a7b8935e8c..3803c26a33 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -334,6 +334,7 @@ x-shared-env: &shared-api-worker-env INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72} RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5} CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: ${CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES:-5} + OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES: ${OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES:-5} CODE_EXECUTION_ENDPOINT: ${CODE_EXECUTION_ENDPOINT:-http://sandbox:8194} CODE_EXECUTION_API_KEY: ${CODE_EXECUTION_API_KEY:-dify-sandbox} CODE_MAX_NUMBER: ${CODE_MAX_NUMBER:-9223372036854775807}