add api to transfer owner

pull/22346/head
Yansong Zhang 11 months ago
parent 490c6423d2
commit ab28394975

@ -596,7 +596,7 @@ class AuthConfig(BaseSettings):
default=86400, default=86400,
) )
OWNER_TRANSFER_LOCKOUT_DURATION : PositiveInt = Field( OWNER_TRANSFER_LOCKOUT_DURATION: PositiveInt = Field(
description="Time (in seconds) a user must wait before retrying owner transfer after exceeding the rate limit.", description="Time (in seconds) a user must wait before retrying owner transfer after exceeding the rate limit.",
default=86400, default=86400,
) )

@ -36,11 +36,13 @@ class EmailChangeRateLimitExceededError(BaseHTTPException):
description = "Too many email change emails have been sent. Please try again in 1 minutes." description = "Too many email change emails have been sent. Please try again in 1 minutes."
code = 429 code = 429
class OwnerTransferRateLimitExceededError(BaseHTTPException): class OwnerTransferRateLimitExceededError(BaseHTTPException):
error_code = "owner_transfer_rate_limit_exceeded" error_code = "owner_transfer_rate_limit_exceeded"
description = "Too many owner tansfer emails have been sent. Please try again in 1 minutes." description = "Too many owner tansfer emails have been sent. Please try again in 1 minutes."
code = 429 code = 429
class EmailCodeError(BaseHTTPException): class EmailCodeError(BaseHTTPException):
error_code = "email_code_error" error_code = "email_code_error"
description = "Email code is invalid or expired." description = "Email code is invalid or expired."
@ -88,7 +90,8 @@ class EmailAlreadyInUseError(BaseHTTPException):
description = "A user with this email already exists." description = "A user with this email already exists."
code = 400 code = 400
class OwnerTransferLimitError(BaseHTTPException): class OwnerTransferLimitError(BaseHTTPException):
error_code = "owner_transfer_limit" error_code = "owner_transfer_limit"
description = "Too many failed owner transfer attempts. Please try again in 24 hours." description = "Too many failed owner transfer attempts. Please try again in 24 hours."
code = 429 code = 429

@ -1,4 +1,5 @@
from urllib import parse from urllib import parse
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
from flask_restful import Resource, abort, marshal_with, reqparse from flask_restful import Resource, abort, marshal_with, reqparse
@ -6,23 +7,23 @@ from flask_restful import Resource, abort, marshal_with, reqparse
import services import services
from configs import dify_config from configs import dify_config
from controllers.console import api from controllers.console import api
from controllers.console.error import WorkspaceMembersLimitExceeded from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, OwnerTransferLimitError
from controllers.console.error import EmailSendIpLimitError, WorkspaceMembersLimitExceeded
from controllers.console.wraps import ( from controllers.console.wraps import (
account_initialization_required, account_initialization_required,
cloud_edition_billing_resource_check, cloud_edition_billing_resource_check,
setup_required,
is_allow_transfer_owner, is_allow_transfer_owner,
setup_required,
) )
from extensions.ext_database import db from extensions.ext_database import db
from fields.member_fields import account_with_role_list_fields from fields.member_fields import account_with_role_list_fields
from libs.helper import extract_remote_ip
from libs.login import login_required from libs.login import login_required
from models.account import Account, TenantAccountRole from models.account import Account, TenantAccountRole
from services.account_service import RegisterService, TenantService,AccountService from services.account_service import AccountService, RegisterService, TenantService
from services.errors.account import AccountAlreadyInTenantError from services.errors.account import AccountAlreadyInTenantError
from services.feature_service import FeatureService from services.feature_service import FeatureService
from libs.helper import email,extract_remote_ip
from controllers.console.error import EmailSendIpLimitError
from controllers.console.auth.error import OwnerTransferLimitError,InvalidTokenError,InvalidEmailError,EmailCodeError
class MemberListApi(Resource): class MemberListApi(Resource):
"""List all members of current tenant.""" """List all members of current tenant."""
@ -166,11 +167,11 @@ class SendOwnerTransferEmailApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@is_allow_transfer_owner @is_allow_transfer_owner
def post(self,member_id): def post(self, member_id):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("language", type=str, required=False, location="json") parser.add_argument("language", type=str, required=False, location="json")
args = parser.parse_args() args = parser.parse_args()
ip_address = extract_remote_ip(request) ip_address = extract_remote_ip(request)
if AccountService.is_email_send_ip_limit(ip_address): if AccountService.is_email_send_ip_limit(ip_address):
raise EmailSendIpLimitError() raise EmailSendIpLimitError()
@ -184,13 +185,20 @@ class SendOwnerTransferEmailApi(Resource):
member = db.session.get(Account, str(member_id)) member = db.session.get(Account, str(member_id))
if not member: if not member:
abort(404) abort(404)
else:
member_name = member.name
token = AccountService.send_owner_transfer_email( token = AccountService.send_owner_transfer_email(
account=current_user, email=email, language=language, workspace=current_user.current_tenant, member=member account=current_user,
email=email,
language=language,
workspace_name=current_user.current_tenant.name,
member_name=member_name,
) )
return {"result": "success", "data": token} return {"result": "success", "data": token}
class OwnerTransferCheckEApi(Resource): class OwnerTransferCheckEApi(Resource):
@setup_required @setup_required
@login_required @login_required
@ -223,13 +231,12 @@ class OwnerTransferCheckEApi(Resource):
AccountService.revoke_owner_transfer_token(args["token"]) AccountService.revoke_owner_transfer_token(args["token"])
# Refresh token data by generating a new token # Refresh token data by generating a new token
_, new_token = AccountService.generate_owner_transfer_token( _, new_token = AccountService.generate_owner_transfer_token(user_email, code=args["code"], additional_data={})
user_email, code=args["code"], additional_data={}
)
AccountService.reset_owner_transfer_error_rate_limit(args["email"]) AccountService.reset_owner_transfer_error_rate_limit(args["email"])
return {"is_valid": True, "email": token_data.get("email"), "token": new_token} return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
class OwnerTransfer(Resource): class OwnerTransfer(Resource):
@setup_required @setup_required
@login_required @login_required
@ -249,7 +256,7 @@ class OwnerTransfer(Resource):
if transfer_token_data.get("email") != current_user.email: if transfer_token_data.get("email") != current_user.email:
raise InvalidEmailError() raise InvalidEmailError()
AccountService.revoke_owner_transfer_token(args["token"]) AccountService.revoke_owner_transfer_token(args["token"])
member = db.session.get(Account, str(member_id)) member = db.session.get(Account, str(member_id))
@ -270,7 +277,9 @@ api.add_resource(MemberInviteEmailApi, "/workspaces/current/members/invite-email
api.add_resource(MemberCancelInviteApi, "/workspaces/current/members/<uuid:member_id>") api.add_resource(MemberCancelInviteApi, "/workspaces/current/members/<uuid:member_id>")
api.add_resource(MemberUpdateRoleApi, "/workspaces/current/members/<uuid:member_id>/update-role") api.add_resource(MemberUpdateRoleApi, "/workspaces/current/members/<uuid:member_id>/update-role")
api.add_resource(DatasetOperatorMemberListApi, "/workspaces/current/dataset-operators") api.add_resource(DatasetOperatorMemberListApi, "/workspaces/current/dataset-operators")
#owner transfer # owner transfer
api.add_resource(SendOwnerTransferEmailApi, "/workspaces/current/members/<uuid:member_id>/send-owner-transfer-confirm-email") api.add_resource(
SendOwnerTransferEmailApi, "/workspaces/current/members/<uuid:member_id>/send-owner-transfer-confirm-email"
)
api.add_resource(OwnerTransferCheckEApi, "/workspaces/current/members/owner-transfer-check") api.add_resource(OwnerTransferCheckEApi, "/workspaces/current/members/owner-transfer-check")
api.add_resource(OwnerTransfer, "/workspaces/current/members/<uuid:member_id>/owner-transfer") api.add_resource(OwnerTransfer, "/workspaces/current/members/<uuid:member_id>/owner-transfer")

@ -249,14 +249,15 @@ def enable_change_email(view):
return decorated return decorated
def is_allow_transfer_owner(view): def is_allow_transfer_owner(view):
@wraps(view) @wraps(view)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
features = FeatureService.get_features() features = FeatureService.get_features(current_user.current_tenant_id)
if features.is_allow_transfer_owner: if features.is_allow_transfer_workspace:
return view(*args, **kwargs) return view(*args, **kwargs)
# otherwise, return 403 # otherwise, return 403
abort(403) abort(403)
return decorated return decorated

@ -55,8 +55,8 @@ from tasks.mail_account_deletion_task import send_account_deletion_verification_
from tasks.mail_change_mail_task import send_change_mail_task from tasks.mail_change_mail_task import send_change_mail_task
from tasks.mail_email_code_login import send_email_code_login_mail_task from tasks.mail_email_code_login import send_email_code_login_mail_task
from tasks.mail_invite_member_task import send_invite_member_mail_task from tasks.mail_invite_member_task import send_invite_member_mail_task
from tasks.mail_reset_password_task import send_reset_password_mail_task
from tasks.mail_owner_transfer_task import send_owner_transfer_confirm_task from tasks.mail_owner_transfer_task import send_owner_transfer_confirm_task
from tasks.mail_reset_password_task import send_reset_password_mail_task
class TokenPair(BaseModel): class TokenPair(BaseModel):
@ -461,8 +461,8 @@ class AccountService:
account: Optional[Account] = None, account: Optional[Account] = None,
email: Optional[str] = None, email: Optional[str] = None,
language: Optional[str] = "en-US", language: Optional[str] = "en-US",
workspace: Optional[Tenant] = None, workspace_name: Optional[str] = "",
member: Optional[Account] = None, member_name: Optional[str] = "",
): ):
account_email = account.email if account else email account_email = account.email if account else email
if account_email is None: if account_email is None:
@ -479,8 +479,8 @@ class AccountService:
language=language, language=language,
to=account_email, to=account_email,
code=code, code=code,
workspace=workspace.name, workspace=workspace_name,
member=member.name, member=member_name,
) )
cls.owner_transfer_rate_limiter.increment_rate_limit(account_email) cls.owner_transfer_rate_limiter.increment_rate_limit(account_email)
return token return token
@ -518,7 +518,7 @@ class AccountService:
account=account, email=email, token_type="change_email", additional_data=additional_data account=account, email=email, token_type="change_email", additional_data=additional_data
) )
return code, token return code, token
@classmethod @classmethod
def generate_owner_transfer_token( def generate_owner_transfer_token(
cls, cls,
@ -695,7 +695,7 @@ class AccountService:
def reset_change_email_error_rate_limit(email: str): def reset_change_email_error_rate_limit(email: str):
key = f"change_email_error_rate_limit:{email}" key = f"change_email_error_rate_limit:{email}"
redis_client.delete(key) redis_client.delete(key)
@staticmethod @staticmethod
@redis_fallback(default_return=None) @redis_fallback(default_return=None)
def add_owner_transfer_error_rate_limit(email: str) -> None: def add_owner_transfer_error_rate_limit(email: str) -> None:
@ -705,7 +705,7 @@ class AccountService:
count = 0 count = 0
count = int(count) + 1 count = int(count) + 1
redis_client.setex(key, dify_config.OWNER_TRANSFER_LOCKOUT_DURATION, count) redis_client.setex(key, dify_config.OWNER_TRANSFER_LOCKOUT_DURATION, count)
@staticmethod @staticmethod
@redis_fallback(default_return=False) @redis_fallback(default_return=False)
def is_owner_transfer_error_rate_limit(email: str) -> bool: def is_owner_transfer_error_rate_limit(email: str) -> bool:
@ -717,18 +717,13 @@ class AccountService:
if count > AccountService.OWNER_TRANSFER_MAX_ERROR_LIMITS: if count > AccountService.OWNER_TRANSFER_MAX_ERROR_LIMITS:
return True return True
return False return False
@staticmethod @staticmethod
@redis_fallback(default_return=None) @redis_fallback(default_return=None)
def reset_owner_transfer_error_rate_limit(email: str): def reset_owner_transfer_error_rate_limit(email: str):
key = f"owner_transfer_error_rate_limit:{email}" key = f"owner_transfer_error_rate_limit:{email}"
redis_client.delete(key) redis_client.delete(key)
@staticmethod
@redis_fallback(default_return=None)
def reset_owner_transfer_error_rate_limit(email: str):
key = f"owner_transfer_error_rate_limit:{email}"
@staticmethod @staticmethod
@redis_fallback(default_return=False) @redis_fallback(default_return=False)
def is_email_send_ip_limit(ip_address: str): def is_email_send_ip_limit(ip_address: str):

@ -151,6 +151,7 @@ class SystemFeatureModel(BaseModel):
plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel() plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel()
enable_change_email: bool = True enable_change_email: bool = True
class FeatureService: class FeatureService:
@classmethod @classmethod
def get_features(cls, tenant_id: str) -> FeatureModel: def get_features(cls, tenant_id: str) -> FeatureModel:

@ -31,25 +31,28 @@ def send_owner_transfer_confirm_task(language: str, to: str, code: str, workspac
system_features = FeatureService.get_system_features() system_features = FeatureService.get_system_features()
if system_features.branding.enabled: if system_features.branding.enabled:
template = "without-brand/transfer_workspace_owner_confirm_template_zh-CN.html" template = "without-brand/transfer_workspace_owner_confirm_template_zh-CN.html"
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace,NewOwner=member) html_content = render_template(template, to=to, code=code, WorkspaceName=workspace, NewOwner=member)
mail.send(to=to, subject=f"验证您转移工作空间所有权的请求", html=html_content) mail.send(to=to, subject="验证您转移工作空间所有权的请求", html=html_content)
else: else:
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace,NewOwner=member) html_content = render_template(template, to=to, code=code, WorkspaceName=workspace, NewOwner=member)
mail.send(to=to, subject="验证您转移工作空间所有权的请求", html=html_content) mail.send(to=to, subject="验证您转移工作空间所有权的请求", html=html_content)
else: else:
template = "transfer_workspace_owner_confirm_template_en-US.html" template = "transfer_workspace_owner_confirm_template_en-US.html"
system_features = FeatureService.get_system_features() system_features = FeatureService.get_system_features()
if system_features.branding.enabled: if system_features.branding.enabled:
template = "without-brand/transfer_workspace_owner_confirm_template_en-US.html" template = "without-brand/transfer_workspace_owner_confirm_template_en-US.html"
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace,NewOwner=member) html_content = render_template(template, to=to, code=code, WorkspaceName=workspace, NewOwner=member)
mail.send(to=to, subject=f"Verify Your Request to Transfer Workspace Ownership", html=html_content) mail.send(to=to, subject="Verify Your Request to Transfer Workspace Ownership", html=html_content)
else: else:
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace,NewOwner=member) html_content = render_template(template, to=to, code=code, WorkspaceName=workspace, NewOwner=member)
mail.send(to=to, subject="Verify Your Request to Transfer Workspace Ownership", html=html_content) mail.send(to=to, subject="Verify Your Request to Transfer Workspace Ownership", html=html_content)
end_at = time.perf_counter() end_at = time.perf_counter()
logging.info( logging.info(
click.style("Send owner transfer confirm mail to {} succeeded: latency: {}".format(to, end_at - start_at), fg="green") click.style(
"Send owner transfer confirm mail to {} succeeded: latency: {}".format(to, end_at - start_at),
fg="green",
)
) )
except Exception: except Exception:
logging.exception("owner transfer confirm email mail to {} failed".format(to)) logging.exception("owner transfer confirm email mail to {} failed".format(to))

Loading…
Cancel
Save