mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-26 02:30:02 +00:00
Merge branch '11.0'
This commit is contained in:
commit
ce2eb2b407
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import AsyncSelect from 'react-select/async';
|
import AsyncSelect from 'react-select/async';
|
||||||
import { seafileAPI } from '../utils/seafile-api';
|
import { seafileAPI } from '../utils/seafile-api';
|
||||||
import { gettext, enableShowContactEmailWhenSearchUser } from '../utils/constants';
|
import { gettext, enableShowContactEmailWhenSearchUser, enableShowLoginIDWhenSearchUser } from '../utils/constants';
|
||||||
import { Utils } from '../utils/utils';
|
import { Utils } from '../utils/utils';
|
||||||
import toaster from './toast';
|
import toaster from './toast';
|
||||||
import { UserSelectStyle } from './common/select';
|
import { UserSelectStyle } from './common/select';
|
||||||
@ -54,24 +54,21 @@ class UserSelect extends React.Component {
|
|||||||
let obj = {};
|
let obj = {};
|
||||||
obj.value = item.name;
|
obj.value = item.name;
|
||||||
obj.email = item.email;
|
obj.email = item.email;
|
||||||
if (enableShowContactEmailWhenSearchUser) {
|
obj.label = (enableShowContactEmailWhenSearchUser || enableShowLoginIDWhenSearchUser) ? (
|
||||||
obj.label = (
|
<div className="d-flex">
|
||||||
<div className="d-flex">
|
<img src={item.avatar_url} className="avatar" width="24" alt="" />
|
||||||
<img src={item.avatar_url} className="avatar" width="24" alt="" />
|
<div className="ml-2">
|
||||||
<div className="ml-2">
|
<span className="user-option-name">{item.name}</span><br />
|
||||||
<span className="user-option-name">{item.name}</span><br />
|
{enableShowContactEmailWhenSearchUser && <span className="user-option-email">{item.contact_email}</span>}
|
||||||
<span className="user-option-email">{item.contact_email}</span>
|
{enableShowLoginIDWhenSearchUser && <span className="user-option-email">{item.login_id}</span>}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
} else {
|
) : (
|
||||||
obj.label = (
|
<React.Fragment>
|
||||||
<>
|
<img src={item.avatar_url} className="select-module select-module-icon avatar" alt=""/>
|
||||||
<img src={item.avatar_url} className="select-module select-module-icon avatar" alt=""/>
|
<span className='select-module select-module-name'>{item.name}</span>
|
||||||
<span className='select-module select-module-name'>{item.name}</span>
|
</React.Fragment>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
this.options.push(obj);
|
this.options.push(obj);
|
||||||
}
|
}
|
||||||
callback(this.options);
|
callback(this.options);
|
||||||
|
@ -5,7 +5,8 @@ import { gettext } from '../../utils/constants';
|
|||||||
const {
|
const {
|
||||||
nameLabel,
|
nameLabel,
|
||||||
enableUpdateUserInfo,
|
enableUpdateUserInfo,
|
||||||
enableUserSetContactEmail
|
enableUserSetContactEmail,
|
||||||
|
enableUserSetName
|
||||||
} = window.app.pageOptions;
|
} = window.app.pageOptions;
|
||||||
|
|
||||||
class UserBasicInfoForm extends React.Component {
|
class UserBasicInfoForm extends React.Component {
|
||||||
@ -38,9 +39,10 @@ class UserBasicInfoForm extends React.Component {
|
|||||||
|
|
||||||
handleSubmit = (e) => {
|
handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let data = {
|
let data = {};
|
||||||
name: this.state.name
|
if (enableUserSetName) {
|
||||||
};
|
data.name = this.state.name;
|
||||||
|
}
|
||||||
if (enableUserSetContactEmail) {
|
if (enableUserSetContactEmail) {
|
||||||
data.contact_email = this.state.contactEmail;
|
data.contact_email = this.state.contactEmail;
|
||||||
}
|
}
|
||||||
@ -60,7 +62,7 @@ class UserBasicInfoForm extends React.Component {
|
|||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<label className="col-sm-1 col-form-label" htmlFor="name">{nameLabel}</label>
|
<label className="col-sm-1 col-form-label" htmlFor="name">{nameLabel}</label>
|
||||||
<div className="col-sm-5">
|
<div className="col-sm-5">
|
||||||
<input className="form-control" id="name" type="text" name="nickname" value={name} disabled={!enableUpdateUserInfo} onChange={this.handleNameInputChange} />
|
<input className="form-control" id="name" type="text" name="nickname" value={name} disabled={!enableUpdateUserInfo || !enableUserSetName} onChange={this.handleNameInputChange} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -104,6 +104,7 @@ class Item extends Component {
|
|||||||
case 'group_delete': return gettext('Delete Group');
|
case 'group_delete': return gettext('Delete Group');
|
||||||
case 'user_add': return gettext('Add User');
|
case 'user_add': return gettext('Add User');
|
||||||
case 'user_delete': return gettext('Delete User');
|
case 'user_delete': return gettext('Delete User');
|
||||||
|
case 'user_migrate': return gettext('Migrate User');
|
||||||
default: return '';
|
default: return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -183,6 +184,12 @@ class Item extends Component {
|
|||||||
.replace('{user}', '<span class="font-weight-bold">' + detail.email + '</span>');
|
.replace('{user}', '<span class="font-weight-bold">' + detail.email + '</span>');
|
||||||
return detailText;
|
return detailText;
|
||||||
|
|
||||||
|
case 'user_migrate':
|
||||||
|
detailText = gettext('User migrate from {user_from} to {user_to}')
|
||||||
|
.replace('{user_from}', '<span class="font-weight-bold">' + detail.from + '</span>')
|
||||||
|
.replace('{user_to}', '<span class="font-weight-bold">' + detail.to+ '</span>');
|
||||||
|
return detailText;
|
||||||
|
|
||||||
default: return '';
|
default: return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -81,6 +81,7 @@ export const canInvitePeople = window.app.pageOptions.canInvitePeople;
|
|||||||
export const canLockUnlockFile = window.app.pageOptions.canLockUnlockFile;
|
export const canLockUnlockFile = window.app.pageOptions.canLockUnlockFile;
|
||||||
export const customNavItems = window.app.pageOptions.customNavItems;
|
export const customNavItems = window.app.pageOptions.customNavItems;
|
||||||
export const enableShowContactEmailWhenSearchUser = window.app.pageOptions.enableShowContactEmailWhenSearchUser;
|
export const enableShowContactEmailWhenSearchUser = window.app.pageOptions.enableShowContactEmailWhenSearchUser;
|
||||||
|
export const enableShowLoginIDWhenSearchUser = window.app.pageOptions.enableShowLoginIDWhenSearchUser;
|
||||||
export const maxUploadFileSize = window.app.pageOptions.maxUploadFileSize;
|
export const maxUploadFileSize = window.app.pageOptions.maxUploadFileSize;
|
||||||
export const maxNumberOfFilesForFileupload = window.app.pageOptions.maxNumberOfFilesForFileupload;
|
export const maxNumberOfFilesForFileupload = window.app.pageOptions.maxNumberOfFilesForFileupload;
|
||||||
export const enableOCM = window.app.pageOptions.enableOCM;
|
export const enableOCM = window.app.pageOptions.enableOCM;
|
||||||
|
@ -1237,6 +1237,8 @@ export const Utils = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
errorMsg = gettext('Permission denied');
|
errorMsg = gettext('Permission denied');
|
||||||
|
} else if (error.response.status == 429) {
|
||||||
|
errorMsg = gettext('Too many requests');
|
||||||
} else if (error.response.data &&
|
} else if (error.response.data &&
|
||||||
error.response.data['error_msg']) {
|
error.response.data['error_msg']) {
|
||||||
errorMsg = error.response.data['error_msg'];
|
errorMsg = error.response.data['error_msg'];
|
||||||
|
@ -28,9 +28,12 @@ USER_ADD = 'user_add'
|
|||||||
# 'user_delete': {'email': deleted_user}
|
# 'user_delete': {'email': deleted_user}
|
||||||
USER_DELETE = 'user_delete'
|
USER_DELETE = 'user_delete'
|
||||||
|
|
||||||
|
# 'user_migrate': {'from': from_user, 'to': to_user}
|
||||||
|
USER_MIGRATE = 'user_migrate'
|
||||||
|
|
||||||
ADMIN_LOG_OPERATION_TYPE = (REPO_TRANSFER, REPO_DELETE,
|
ADMIN_LOG_OPERATION_TYPE = (REPO_TRANSFER, REPO_DELETE,
|
||||||
GROUP_CREATE, GROUP_TRANSFER, GROUP_DELETE,
|
GROUP_CREATE, GROUP_TRANSFER, GROUP_DELETE,
|
||||||
USER_ADD, USER_DELETE)
|
USER_ADD, USER_DELETE, USER_MIGRATE)
|
||||||
|
|
||||||
|
|
||||||
class AdminLogManager(models.Manager):
|
class AdminLogManager(models.Manager):
|
||||||
|
@ -13,6 +13,8 @@ from rest_framework.views import APIView
|
|||||||
import seaserv
|
import seaserv
|
||||||
from seaserv import seafile_api, ccnet_threaded_rpc, ccnet_api
|
from seaserv import seafile_api, ccnet_threaded_rpc, ccnet_api
|
||||||
|
|
||||||
|
from seahub.admin_log.models import USER_MIGRATE
|
||||||
|
from seahub.admin_log.signals import admin_operation
|
||||||
from seahub.api2.authentication import TokenAuthentication
|
from seahub.api2.authentication import TokenAuthentication
|
||||||
from seahub.api2.serializers import AccountSerializer
|
from seahub.api2.serializers import AccountSerializer
|
||||||
from seahub.api2.throttling import UserRateThrottle
|
from seahub.api2.throttling import UserRateThrottle
|
||||||
@ -21,6 +23,7 @@ from seahub.base.accounts import User
|
|||||||
from seahub.base.templatetags.seahub_tags import email2nickname
|
from seahub.base.templatetags.seahub_tags import email2nickname
|
||||||
from seahub.profile.models import Profile, DetailedProfile
|
from seahub.profile.models import Profile, DetailedProfile
|
||||||
from seahub.institutions.models import Institution
|
from seahub.institutions.models import Institution
|
||||||
|
from seahub.share.models import UploadLinkShare, FileShare
|
||||||
from seahub.utils import is_valid_username, is_org_context
|
from seahub.utils import is_valid_username, is_org_context
|
||||||
from seahub.utils.file_size import get_file_size_unit
|
from seahub.utils.file_size import get_file_size_unit
|
||||||
from seahub.group.utils import is_group_member
|
from seahub.group.utils import is_group_member
|
||||||
@ -107,6 +110,21 @@ class Account(APIView):
|
|||||||
if from_user == g.creator_name:
|
if from_user == g.creator_name:
|
||||||
ccnet_threaded_rpc.set_group_creator(g.id, to_user)
|
ccnet_threaded_rpc.set_group_creator(g.id, to_user)
|
||||||
|
|
||||||
|
# reshare repo to links
|
||||||
|
try:
|
||||||
|
UploadLinkShare.objects.filter(username=from_user).update(username=to_user)
|
||||||
|
FileShare.objects.filter(username=from_user).update(username=to_user)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
admin_op_detail = {
|
||||||
|
"from": from_user,
|
||||||
|
"to": to_user
|
||||||
|
}
|
||||||
|
admin_operation.send(sender=None, admin_name=request.user.username,
|
||||||
|
operation=USER_MIGRATE, detail=admin_op_detail)
|
||||||
return Response({'success': True})
|
return Response({'success': True})
|
||||||
else:
|
else:
|
||||||
return api_error(status.HTTP_400_BAD_REQUEST, 'op can only be migrate.')
|
return api_error(status.HTTP_400_BAD_REQUEST, 'op can only be migrate.')
|
||||||
|
@ -12,6 +12,7 @@ from rest_framework import status
|
|||||||
from seaserv import ccnet_api, seafile_api
|
from seaserv import ccnet_api, seafile_api
|
||||||
|
|
||||||
from seahub.auth.utils import get_virtual_id_by_email
|
from seahub.auth.utils import get_virtual_id_by_email
|
||||||
|
from seahub.organizations.settings import ORG_MEMBER_QUOTA_DEFAULT
|
||||||
from seahub.utils import is_valid_email
|
from seahub.utils import is_valid_email
|
||||||
from seahub.utils.file_size import get_file_size_unit
|
from seahub.utils.file_size import get_file_size_unit
|
||||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||||
@ -233,6 +234,20 @@ class AdminOrganizations(APIView):
|
|||||||
logger.error(e)
|
logger.error(e)
|
||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
quota = request.data.get('quota', None)
|
||||||
|
if quota:
|
||||||
|
try:
|
||||||
|
quota_mb = int(quota)
|
||||||
|
quota = quota_mb * get_file_size_unit('MB')
|
||||||
|
seafile_api.set_org_quota(org_id, quota)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(e)
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, "Quota is not valid")
|
||||||
|
|
||||||
|
if ORG_MEMBER_QUOTA_ENABLED:
|
||||||
|
member_limit = request.data.get('member_limit', ORG_MEMBER_QUOTA_DEFAULT)
|
||||||
|
OrgMemberQuota.objects.set_quota(org_id, member_limit)
|
||||||
|
|
||||||
org = ccnet_api.get_org_by_id(org_id)
|
org = ccnet_api.get_org_by_id(org_id)
|
||||||
try:
|
try:
|
||||||
|
@ -27,7 +27,7 @@ from seahub.contacts.models import Contact
|
|||||||
from seahub.avatar.templatetags.avatar_tags import api_avatar_url
|
from seahub.avatar.templatetags.avatar_tags import api_avatar_url
|
||||||
|
|
||||||
from seahub.settings import ENABLE_GLOBAL_ADDRESSBOOK, \
|
from seahub.settings import ENABLE_GLOBAL_ADDRESSBOOK, \
|
||||||
ENABLE_SEARCH_FROM_LDAP_DIRECTLY
|
ENABLE_SEARCH_FROM_LDAP_DIRECTLY, ENABLE_SHOW_LOGIN_ID_WHEN_SEARCH_USER
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -175,15 +175,21 @@ class SearchUser(APIView):
|
|||||||
|
|
||||||
def format_searched_user_result(request, users, size):
|
def format_searched_user_result(request, users, size):
|
||||||
results = []
|
results = []
|
||||||
|
if ENABLE_SHOW_LOGIN_ID_WHEN_SEARCH_USER:
|
||||||
|
profile_queryset = Profile.objects.filter(user__in=users)
|
||||||
|
profile_dict = { p.user: p.login_id for p in profile_queryset if p.login_id }
|
||||||
|
|
||||||
for email in users:
|
for email in users:
|
||||||
url, is_default, date_uploaded = api_avatar_url(email, size)
|
url, is_default, date_uploaded = api_avatar_url(email, size)
|
||||||
results.append({
|
user_info = {
|
||||||
"email": email,
|
"email": email,
|
||||||
"avatar_url": url,
|
"avatar_url": url,
|
||||||
"name": email2nickname(email),
|
"name": email2nickname(email),
|
||||||
"contact_email": email2contact_email(email),
|
"contact_email": email2contact_email(email),
|
||||||
})
|
}
|
||||||
|
if ENABLE_SHOW_LOGIN_ID_WHEN_SEARCH_USER:
|
||||||
|
user_info['login_id'] = profile_dict.get(email, '')
|
||||||
|
results.append(user_info)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from rest_framework import status
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from seahub.api2.throttling import UserRateThrottle
|
from seahub.api2.throttling import ShareLinkZipTaskThrottle
|
||||||
from seahub.api2.utils import api_error
|
from seahub.api2.utils import api_error
|
||||||
|
|
||||||
from seahub.views.file import send_file_access_msg
|
from seahub.views.file import send_file_access_msg
|
||||||
@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class ShareLinkZipTaskView(APIView):
|
class ShareLinkZipTaskView(APIView):
|
||||||
|
|
||||||
throttle_classes = (UserRateThrottle,)
|
throttle_classes = (ShareLinkZipTaskThrottle, )
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
|
|
||||||
|
@ -23,7 +23,8 @@ from seahub.api2.utils import api_error
|
|||||||
from seahub.base.templatetags.seahub_tags import email2nickname, \
|
from seahub.base.templatetags.seahub_tags import email2nickname, \
|
||||||
email2contact_email
|
email2contact_email
|
||||||
from seahub.profile.models import Profile, DetailedProfile
|
from seahub.profile.models import Profile, DetailedProfile
|
||||||
from seahub.settings import ENABLE_UPDATE_USER_INFO, ENABLE_USER_SET_CONTACT_EMAIL, ENABLE_CONVERT_TO_TEAM_ACCOUNT
|
from seahub.settings import ENABLE_UPDATE_USER_INFO, ENABLE_USER_SET_CONTACT_EMAIL, ENABLE_CONVERT_TO_TEAM_ACCOUNT, \
|
||||||
|
ENABLE_USER_SET_NAME
|
||||||
|
|
||||||
import seaserv
|
import seaserv
|
||||||
from seaserv import ccnet_api, seafile_api
|
from seaserv import ccnet_api, seafile_api
|
||||||
@ -91,6 +92,9 @@ class User(APIView):
|
|||||||
# argument check for name
|
# argument check for name
|
||||||
name = request.data.get("name", None)
|
name = request.data.get("name", None)
|
||||||
if name:
|
if name:
|
||||||
|
if not ENABLE_USER_SET_NAME:
|
||||||
|
error_msg = _('Feature disabled.')
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
name = name.strip()
|
name = name.strip()
|
||||||
if len(name) > 64:
|
if len(name) > 64:
|
||||||
error_msg = _('Name is too long (maximum is 64 characters)')
|
error_msg = _('Name is too long (maximum is 64 characters)')
|
||||||
|
@ -7,12 +7,15 @@ from seaserv import ccnet_api
|
|||||||
from seahub.auth import authenticate
|
from seahub.auth import authenticate
|
||||||
from seahub.api2.models import DESKTOP_PLATFORMS
|
from seahub.api2.models import DESKTOP_PLATFORMS
|
||||||
from seahub.api2.utils import get_token_v1, get_token_v2
|
from seahub.api2.utils import get_token_v1, get_token_v2
|
||||||
|
from seahub.auth.utils import incr_login_failed_attempts
|
||||||
|
from seahub.base.accounts import User
|
||||||
from seahub.profile.models import Profile
|
from seahub.profile.models import Profile
|
||||||
from seahub.two_factor.models import default_device
|
from seahub.two_factor.models import default_device
|
||||||
from seahub.two_factor.views.login import is_device_remembered
|
from seahub.two_factor.views.login import is_device_remembered
|
||||||
from seahub.utils.two_factor_auth import has_two_factor_auth, \
|
from seahub.utils.two_factor_auth import has_two_factor_auth, \
|
||||||
two_factor_auth_enabled, verify_two_factor_token
|
two_factor_auth_enabled, verify_two_factor_token
|
||||||
from seahub.settings import ENABLE_LDAP
|
from seahub.settings import ENABLE_LDAP
|
||||||
|
from constance import config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -71,20 +74,29 @@ class AuthTokenSerializer(serializers.Serializer):
|
|||||||
raise serializers.ValidationError('invalid params')
|
raise serializers.ValidationError('invalid params')
|
||||||
|
|
||||||
if login_id and password:
|
if login_id and password:
|
||||||
user = authenticate(username=login_id, password=password)
|
# First check the user is active or not
|
||||||
if user:
|
username = Profile.objects.convert_login_str_to_username(login_id)
|
||||||
|
if username is None:
|
||||||
|
username = login_id
|
||||||
|
try:
|
||||||
|
user = User.objects.get(username)
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
raise serializers.ValidationError('User account is disabled.')
|
raise serializers.ValidationError('User account is disabled.')
|
||||||
else:
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Second check the password correct or not
|
||||||
|
user = authenticate(username=login_id, password=password)
|
||||||
|
if not user:
|
||||||
"""try login id/contact email"""
|
"""try login id/contact email"""
|
||||||
# convert login id or contact email to username if any
|
# convert login id or contact email to username if any
|
||||||
username = Profile.objects.convert_login_str_to_username(login_id)
|
|
||||||
user = authenticate(username=username, password=password)
|
user = authenticate(username=username, password=password)
|
||||||
# After local user authentication process is completed, authenticate LDAP user
|
# After local user authentication process is completed, authenticate LDAP user
|
||||||
if user is None and ENABLE_LDAP:
|
if user is None and ENABLE_LDAP:
|
||||||
user = authenticate(ldap_user=username, password=password)
|
user = authenticate(ldap_user=username, password=password)
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
|
self._handle_failed_login(username)
|
||||||
raise serializers.ValidationError('Unable to login with provided credentials.')
|
raise serializers.ValidationError('Unable to login with provided credentials.')
|
||||||
elif not user.is_active:
|
elif not user.is_active:
|
||||||
raise serializers.ValidationError('User account is disabled.')
|
raise serializers.ValidationError('User account is disabled.')
|
||||||
@ -134,6 +146,23 @@ class AuthTokenSerializer(serializers.Serializer):
|
|||||||
self.two_factor_auth_failed = True
|
self.two_factor_auth_failed = True
|
||||||
msg = 'Two factor auth token is invalid.'
|
msg = 'Two factor auth token is invalid.'
|
||||||
raise serializers.ValidationError(msg)
|
raise serializers.ValidationError(msg)
|
||||||
|
|
||||||
|
def _handle_failed_login(self, login_id):
|
||||||
|
failed_attempt = incr_login_failed_attempts(username=login_id)
|
||||||
|
if failed_attempt >= config.LOGIN_ATTEMPT_LIMIT:
|
||||||
|
if bool(config.FREEZE_USER_ON_LOGIN_FAILED) is True:
|
||||||
|
email = Profile.objects.get_username_by_login_id(login_id)
|
||||||
|
if email is None:
|
||||||
|
email = Profile.objects.get_username_by_contact_email(login_id)
|
||||||
|
if email is None:
|
||||||
|
email = login_id
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email)
|
||||||
|
if user.is_active:
|
||||||
|
user.freeze_user(notify_admins=True, notify_org_admins=True)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
raise serializers.ValidationError('This account has been frozen due to too many failed login attempts.')
|
||||||
|
|
||||||
|
|
||||||
class AccountSerializer(serializers.Serializer):
|
class AccountSerializer(serializers.Serializer):
|
||||||
|
@ -7,6 +7,7 @@ from django.conf import settings
|
|||||||
from django.core.cache import cache as default_cache
|
from django.core.cache import cache as default_cache
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
from rest_framework.throttling import BaseThrottle
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from seahub.utils.ip import get_remote_ip
|
from seahub.utils.ip import get_remote_ip
|
||||||
@ -207,6 +208,22 @@ class UserRateThrottle(SimpleRateThrottle):
|
|||||||
'ident': ident
|
'ident': ident
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ShareLinkZipTaskThrottle(SimpleRateThrottle):
|
||||||
|
|
||||||
|
scope = 'share_link_zip_task'
|
||||||
|
|
||||||
|
def get_cache_key(self, request, view):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
ident = request.user.id
|
||||||
|
else:
|
||||||
|
ident = self.get_ident(request)
|
||||||
|
|
||||||
|
return self.cache_format % {
|
||||||
|
'scope': self.scope,
|
||||||
|
'ident': ident
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ScopedRateThrottle(SimpleRateThrottle):
|
class ScopedRateThrottle(SimpleRateThrottle):
|
||||||
"""
|
"""
|
||||||
|
@ -29,7 +29,7 @@ from django.http import HttpResponse
|
|||||||
from django.template.defaultfilters import filesizeformat
|
from django.template.defaultfilters import filesizeformat
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from seahub.auth.utils import get_virtual_id_by_email
|
||||||
from .throttling import ScopedRateThrottle, AnonRateThrottle, UserRateThrottle
|
from .throttling import ScopedRateThrottle, AnonRateThrottle, UserRateThrottle
|
||||||
from .authentication import TokenAuthentication
|
from .authentication import TokenAuthentication
|
||||||
from .serializers import AuthTokenSerializer
|
from .serializers import AuthTokenSerializer
|
||||||
@ -131,6 +131,11 @@ try:
|
|||||||
from seahub.settings import ENABLE_OFFICE_WEB_APP
|
from seahub.settings import ENABLE_OFFICE_WEB_APP
|
||||||
except ImportError:
|
except ImportError:
|
||||||
ENABLE_OFFICE_WEB_APP = False
|
ENABLE_OFFICE_WEB_APP = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from seahub.settings import ORG_MEMBER_QUOTA_ENABLED
|
||||||
|
except ImportError:
|
||||||
|
ORG_MEMBER_QUOTA_ENABLED = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from seahub.settings import OFFICE_WEB_APP_FILE_EXTENSION
|
from seahub.settings import OFFICE_WEB_APP_FILE_EXTENSION
|
||||||
@ -1756,6 +1761,9 @@ class DownloadRepo(APIView):
|
|||||||
forbidden_path = resp_json.get('forbidden_path', '')
|
forbidden_path = resp_json.get('forbidden_path', '')
|
||||||
|
|
||||||
if 'seadrive' in request.META.get('HTTP_USER_AGENT', '').lower():
|
if 'seadrive' in request.META.get('HTTP_USER_AGENT', '').lower():
|
||||||
|
# This is to help the desktop client to show error to the user.
|
||||||
|
# The actual permission check will be done at the file download time.
|
||||||
|
|
||||||
if not is_syncable and forbidden_path == '/':
|
if not is_syncable and forbidden_path == '/':
|
||||||
error_msg = 'unsyncable share permission'
|
error_msg = 'unsyncable share permission'
|
||||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
@ -5338,7 +5346,7 @@ class OrganizationView(APIView):
|
|||||||
member_limit = request.POST.get('member_limit', ORG_MEMBER_QUOTA_DEFAULT)
|
member_limit = request.POST.get('member_limit', ORG_MEMBER_QUOTA_DEFAULT)
|
||||||
|
|
||||||
if not org_name or not username or not password or \
|
if not org_name or not username or not password or \
|
||||||
not prefix or not quota or not member_limit:
|
not prefix or not quota:
|
||||||
return api_error(status.HTTP_400_BAD_REQUEST, "Missing argument")
|
return api_error(status.HTTP_400_BAD_REQUEST, "Missing argument")
|
||||||
|
|
||||||
if not is_valid_username(username):
|
if not is_valid_username(username):
|
||||||
@ -5350,8 +5358,9 @@ class OrganizationView(APIView):
|
|||||||
logger.error(e)
|
logger.error(e)
|
||||||
return api_error(status.HTTP_400_BAD_REQUEST, "Quota is not valid")
|
return api_error(status.HTTP_400_BAD_REQUEST, "Quota is not valid")
|
||||||
|
|
||||||
|
vid = get_virtual_id_by_email(username)
|
||||||
try:
|
try:
|
||||||
User.objects.get(email = username)
|
User.objects.get(email = vid)
|
||||||
user_exist = True
|
user_exist = True
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
user_exist = False
|
user_exist = False
|
||||||
@ -5367,15 +5376,16 @@ class OrganizationView(APIView):
|
|||||||
return api_error(status.HTTP_400_BAD_REQUEST, "An organization with this prefix already exists")
|
return api_error(status.HTTP_400_BAD_REQUEST, "An organization with this prefix already exists")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
User.objects.create_user(username, password, is_staff=False, is_active=True)
|
new_user = User.objects.create_user(username, password, is_staff=False, is_active=True)
|
||||||
create_org(org_name, prefix, username)
|
create_org(org_name, prefix, new_user.username)
|
||||||
|
|
||||||
org = ccnet_threaded_rpc.get_org_by_url_prefix(prefix)
|
org = ccnet_threaded_rpc.get_org_by_url_prefix(prefix)
|
||||||
org_id = org.org_id
|
org_id = org.org_id
|
||||||
|
|
||||||
# set member limit
|
# set member limit
|
||||||
from seahub.organizations.models import OrgMemberQuota
|
if ORG_MEMBER_QUOTA_ENABLED:
|
||||||
OrgMemberQuota.objects.set_quota(org_id, member_limit)
|
from seahub.organizations.models import OrgMemberQuota
|
||||||
|
OrgMemberQuota.objects.set_quota(org_id, member_limit)
|
||||||
|
|
||||||
# set quota
|
# set quota
|
||||||
quota = quota_mb * get_file_size_unit('MB')
|
quota = quota_mb * get_file_size_unit('MB')
|
||||||
|
@ -29,7 +29,7 @@ class AuthenticationForm(forms.Form):
|
|||||||
Base class for authenticating users. Extend this to get a form that accepts
|
Base class for authenticating users. Extend this to get a form that accepts
|
||||||
username/password logins.
|
username/password logins.
|
||||||
"""
|
"""
|
||||||
login = forms.CharField(label=_("Email or Username"), max_length=255)
|
login = forms.CharField(label=_("Email or Username") )
|
||||||
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
||||||
|
|
||||||
def __init__(self, request=None, *args, **kwargs):
|
def __init__(self, request=None, *args, **kwargs):
|
||||||
@ -41,6 +41,7 @@ class AuthenticationForm(forms.Form):
|
|||||||
"""
|
"""
|
||||||
self.request = request
|
self.request = request
|
||||||
self.user_cache = None
|
self.user_cache = None
|
||||||
|
self.db_record = True
|
||||||
super(AuthenticationForm, self).__init__(*args, **kwargs)
|
super(AuthenticationForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def clean_login(self):
|
def clean_login(self):
|
||||||
@ -49,8 +50,25 @@ class AuthenticationForm(forms.Form):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
username = self.cleaned_data.get('login')
|
username = self.cleaned_data.get('login')
|
||||||
password = self.cleaned_data.get('password')
|
password = self.cleaned_data.get('password')
|
||||||
|
|
||||||
if username and password:
|
if username and password:
|
||||||
|
# First check the account length for validation.
|
||||||
|
if len(username) > 255:
|
||||||
|
self.errors['invalid_input'] = "The login name is too long."
|
||||||
|
self.db_record = False
|
||||||
|
raise forms.ValidationError("The login name is too long.")
|
||||||
|
|
||||||
|
# Check user account active or not
|
||||||
|
email = Profile.objects.convert_login_str_to_username(username)
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email=email)
|
||||||
|
if not user.is_active:
|
||||||
|
self.errors['inactive'] = _("This account is inactive.")
|
||||||
|
raise forms.ValidationError(_("This account is inactive."))
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Second check the password correct or not
|
||||||
self.user_cache = authenticate(username=username, password=password)
|
self.user_cache = authenticate(username=username, password=password)
|
||||||
if self.user_cache is None:
|
if self.user_cache is None:
|
||||||
"""then try login id/contact email/primary id"""
|
"""then try login id/contact email/primary id"""
|
||||||
|
@ -97,7 +97,6 @@ def login(request, template_name='registration/login.html',
|
|||||||
redirect_field_name=REDIRECT_FIELD_NAME,
|
redirect_field_name=REDIRECT_FIELD_NAME,
|
||||||
authentication_form=AuthenticationForm):
|
authentication_form=AuthenticationForm):
|
||||||
"""Displays the login form and handles the login action."""
|
"""Displays the login form and handles the login action."""
|
||||||
|
|
||||||
redirect_to = request.GET.get(redirect_field_name, '')
|
redirect_to = request.GET.get(redirect_field_name, '')
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
if redirect_to and url_has_allowed_host_and_scheme(redirect_to, allowed_hosts=request.get_host()):
|
if redirect_to and url_has_allowed_host_and_scheme(redirect_to, allowed_hosts=request.get_host()):
|
||||||
@ -130,7 +129,8 @@ def login(request, template_name='registration/login.html',
|
|||||||
redirect_to, remember_me)
|
redirect_to, remember_me)
|
||||||
|
|
||||||
# form is invalid
|
# form is invalid
|
||||||
user_logged_in_failed.send(sender=None, request=request)
|
form.db_record and user_logged_in_failed.send(sender=None, request=request)
|
||||||
|
|
||||||
failed_attempt = incr_login_failed_attempts(username=login,
|
failed_attempt = incr_login_failed_attempts(username=login,
|
||||||
ip=ip)
|
ip=ip)
|
||||||
|
|
||||||
|
@ -839,7 +839,7 @@ class OrgAdminInviteUser(APIView):
|
|||||||
new_user = User.objects.create_user(email, '!',
|
new_user = User.objects.create_user(email, '!',
|
||||||
is_staff=False,
|
is_staff=False,
|
||||||
is_active=False)
|
is_active=False)
|
||||||
set_org_user(org_id, email)
|
set_org_user(org_id, new_user.username)
|
||||||
|
|
||||||
# send invitation link
|
# send invitation link
|
||||||
i = Invitation.objects.add(inviter=username,
|
i = Invitation.objects.add(inviter=username,
|
||||||
|
@ -19,6 +19,7 @@ window.app.pageOptions = {
|
|||||||
enableUpdateUserInfo: {% if ENABLE_UPDATE_USER_INFO %} true {% else %} false {% endif %},
|
enableUpdateUserInfo: {% if ENABLE_UPDATE_USER_INFO %} true {% else %} false {% endif %},
|
||||||
nameLabel: "{% trans "Name:" context "true name" %}",
|
nameLabel: "{% trans "Name:" context "true name" %}",
|
||||||
enableUserSetContactEmail: {% if ENABLE_USER_SET_CONTACT_EMAIL %} true {% else %} false {% endif %},
|
enableUserSetContactEmail: {% if ENABLE_USER_SET_CONTACT_EMAIL %} true {% else %} false {% endif %},
|
||||||
|
enableUserSetName: {% if ENABLE_USER_SET_NAME %} true {% else %} false {% endif %},
|
||||||
|
|
||||||
canUpdatePassword: {% if not is_ldap_user and ENABLE_CHANGE_PASSWORD %} true {% else %} false {% endif %},
|
canUpdatePassword: {% if not is_ldap_user and ENABLE_CHANGE_PASSWORD %} true {% else %} false {% endif %},
|
||||||
{% if not is_ldap_user and ENABLE_CHANGE_PASSWORD %}
|
{% if not is_ldap_user and ENABLE_CHANGE_PASSWORD %}
|
||||||
|
@ -165,6 +165,7 @@ def edit_profile(request):
|
|||||||
'enable_dingtalk': enable_dingtalk,
|
'enable_dingtalk': enable_dingtalk,
|
||||||
'social_connected_dingtalk': social_connected_dingtalk,
|
'social_connected_dingtalk': social_connected_dingtalk,
|
||||||
'ENABLE_USER_SET_CONTACT_EMAIL': settings.ENABLE_USER_SET_CONTACT_EMAIL,
|
'ENABLE_USER_SET_CONTACT_EMAIL': settings.ENABLE_USER_SET_CONTACT_EMAIL,
|
||||||
|
'ENABLE_USER_SET_NAME': settings.ENABLE_USER_SET_NAME,
|
||||||
'user_unusable_password': request.user.enc_password == UNUSABLE_PASSWORD,
|
'user_unusable_password': request.user.enc_password == UNUSABLE_PASSWORD,
|
||||||
'enable_adfs': enable_adfs,
|
'enable_adfs': enable_adfs,
|
||||||
'saml_connected': saml_connected,
|
'saml_connected': saml_connected,
|
||||||
|
@ -319,6 +319,7 @@ ENABLE_OAUTH = False
|
|||||||
ENABLE_WATERMARK = False
|
ENABLE_WATERMARK = False
|
||||||
|
|
||||||
ENABLE_SHOW_CONTACT_EMAIL_WHEN_SEARCH_USER = False
|
ENABLE_SHOW_CONTACT_EMAIL_WHEN_SEARCH_USER = False
|
||||||
|
ENABLE_SHOW_LOGIN_ID_WHEN_SEARCH_USER = False
|
||||||
|
|
||||||
# enable work weixin
|
# enable work weixin
|
||||||
ENABLE_WORK_WEIXIN = False
|
ENABLE_WORK_WEIXIN = False
|
||||||
@ -540,6 +541,7 @@ REST_FRAMEWORK = {
|
|||||||
'ping': '3000/minute',
|
'ping': '3000/minute',
|
||||||
'anon': '60/minute',
|
'anon': '60/minute',
|
||||||
'user': '3000/minute',
|
'user': '3000/minute',
|
||||||
|
'share_link_zip_task': '10/minute'
|
||||||
},
|
},
|
||||||
# https://github.com/tomchristie/django-rest-framework/issues/2891
|
# https://github.com/tomchristie/django-rest-framework/issues/2891
|
||||||
'UNICODE_JSON': False,
|
'UNICODE_JSON': False,
|
||||||
@ -758,6 +760,7 @@ WEBDAV_SECRET_MIN_LENGTH = 1
|
|||||||
WEBDAV_SECRET_STRENGTH_LEVEL = 1
|
WEBDAV_SECRET_STRENGTH_LEVEL = 1
|
||||||
|
|
||||||
ENABLE_USER_SET_CONTACT_EMAIL = False
|
ENABLE_USER_SET_CONTACT_EMAIL = False
|
||||||
|
ENABLE_USER_SET_NAME = True
|
||||||
|
|
||||||
# SSO to thirdparty website
|
# SSO to thirdparty website
|
||||||
ENABLE_SSO_TO_THIRDPART_WEBSITE = False
|
ENABLE_SSO_TO_THIRDPART_WEBSITE = False
|
||||||
|
@ -131,6 +131,7 @@
|
|||||||
canInvitePeople: {% if enable_guest_invitation and user.permissions.can_invite_guest %} true {% else %} false {% endif %},
|
canInvitePeople: {% if enable_guest_invitation and user.permissions.can_invite_guest %} true {% else %} false {% endif %},
|
||||||
customNavItems: {% if custom_nav_items %} JSON.parse('{{ custom_nav_items | escapejs }}') {% else %} {{'[]'}} {% endif %},
|
customNavItems: {% if custom_nav_items %} JSON.parse('{{ custom_nav_items | escapejs }}') {% else %} {{'[]'}} {% endif %},
|
||||||
enableShowContactEmailWhenSearchUser: {% if enable_show_contact_email_when_search_user %} true {% else %} false {% endif %},
|
enableShowContactEmailWhenSearchUser: {% if enable_show_contact_email_when_search_user %} true {% else %} false {% endif %},
|
||||||
|
enableShowLoginIDWhenSearchUser: {% if enable_show_login_id_when_search_user %} true {% else %} false {% endif %},
|
||||||
{% if max_upload_file_size > 0 %}
|
{% if max_upload_file_size > 0 %}
|
||||||
maxUploadFileSize: {{ max_upload_file_size }},
|
maxUploadFileSize: {{ max_upload_file_size }},
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -53,6 +53,8 @@ html, body, #wrapper { height:100%; }
|
|||||||
<p class="error mt-2">{{ form.errors.freeze_account }}</p>
|
<p class="error mt-2">{{ form.errors.freeze_account }}</p>
|
||||||
{% elif form.errors.inactive %}
|
{% elif form.errors.inactive %}
|
||||||
<p class="error mt-2">{{ form.errors.inactive }}</p>
|
<p class="error mt-2">{{ form.errors.inactive }}</p>
|
||||||
|
{% elif form.errors.invalid_input %}
|
||||||
|
<p class="error mt-2">{{ form.errors.invalid_input }}</p>
|
||||||
{% elif form.errors.not_found %}
|
{% elif form.errors.not_found %}
|
||||||
<p class="error mt-2">{{ form.errors.not_found }}</p>
|
<p class="error mt-2">{{ form.errors.not_found }}</p>
|
||||||
{% elif form.errors.disable_pwd_login %}
|
{% elif form.errors.disable_pwd_login %}
|
||||||
|
@ -13,6 +13,7 @@ REST_FRAMEWORK = {
|
|||||||
'ping': '90000/minute',
|
'ping': '90000/minute',
|
||||||
'anon': '90000/minute',
|
'anon': '90000/minute',
|
||||||
'user': '90000/minute',
|
'user': '90000/minute',
|
||||||
|
'share_link_zip_task': '90000/minute'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1134,6 +1134,7 @@ def react_fake_view(request, **kwargs):
|
|||||||
'file_audit_enabled': FILE_AUDIT_ENABLED,
|
'file_audit_enabled': FILE_AUDIT_ENABLED,
|
||||||
'custom_nav_items': json.dumps(CUSTOM_NAV_ITEMS),
|
'custom_nav_items': json.dumps(CUSTOM_NAV_ITEMS),
|
||||||
'enable_show_contact_email_when_search_user': settings.ENABLE_SHOW_CONTACT_EMAIL_WHEN_SEARCH_USER,
|
'enable_show_contact_email_when_search_user': settings.ENABLE_SHOW_CONTACT_EMAIL_WHEN_SEARCH_USER,
|
||||||
|
'enable_show_login_id_when_search_user': settings.ENABLE_SHOW_LOGIN_ID_WHEN_SEARCH_USER,
|
||||||
'additional_share_dialog_note': ADDITIONAL_SHARE_DIALOG_NOTE,
|
'additional_share_dialog_note': ADDITIONAL_SHARE_DIALOG_NOTE,
|
||||||
'additional_app_bottom_links': ADDITIONAL_APP_BOTTOM_LINKS,
|
'additional_app_bottom_links': ADDITIONAL_APP_BOTTOM_LINKS,
|
||||||
'additional_about_dialog_links': ADDITIONAL_ABOUT_DIALOG_LINKS,
|
'additional_about_dialog_links': ADDITIONAL_ABOUT_DIALOG_LINKS,
|
||||||
|
@ -90,7 +90,8 @@ class AuthTokenSerializerTest(BaseTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
s = AuthTokenSerializer(data=d, context={'request': self.fake_request})
|
s = AuthTokenSerializer(data=d, context={'request': self.fake_request})
|
||||||
self.assertFailed(s)
|
assert s.is_valid() is False
|
||||||
|
assert 'User account is disabled.' in s.errors['non_field_errors']
|
||||||
|
|
||||||
def test_login_failed(self):
|
def test_login_failed(self):
|
||||||
d = {
|
d = {
|
||||||
|
@ -48,7 +48,8 @@ class AuthenticationFormTest(BaseTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
form = AuthenticationForm(None, data)
|
form = AuthenticationForm(None, data)
|
||||||
self.assertFailed(form)
|
assert form.is_valid() is False
|
||||||
|
assert 'This account is inactive.' in form.non_field_errors()
|
||||||
|
|
||||||
def test_login_success(self):
|
def test_login_success(self):
|
||||||
data = {
|
data = {
|
||||||
|
Loading…
Reference in New Issue
Block a user