mirror of
https://github.com/haiwen/seahub.git
synced 2025-05-11 09:24:38 +00:00
Merge branch 'master' into 12.0
This commit is contained in:
commit
8bcc05a6cd
.github/workflows
frontend/src
components
pages
utils
scripts
seahub
adfs_auth
api2/endpoints
auth
base
invitations/migrations
organizations/migrations
role_permissions
settings.pyshare/migrations
templates
urls.pyutils
wopi
tests/seahub
thirdpart/shibboleth
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -25,8 +25,8 @@ jobs:
|
||||
|
||||
- name: clone and build
|
||||
run: |
|
||||
git clone --depth=1 --branch=master https://github.com/haiwen/seafile-test-deploy /tmp/seafile-test-deploy
|
||||
cd /tmp/seafile-test-deploy && git fetch origin master:master && git checkout master
|
||||
git clone --depth=1 --branch=11.0 https://github.com/haiwen/seafile-test-deploy /tmp/seafile-test-deploy
|
||||
cd /tmp/seafile-test-deploy && git fetch origin 11.0:11.0 && git checkout 11.0
|
||||
./bootstrap.sh
|
||||
|
||||
- name: pip install
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap';
|
||||
import { gettext, username, canGenerateShareLink, canGenerateUploadLink, canInvitePeople, additionalShareDialogNote, enableOCM, isPro } from '../../utils/constants';
|
||||
import { gettext, username, canGenerateShareLink, canGenerateUploadLink, canInvitePeople, additionalShareDialogNote, enableOCM, isPro, canShareRepo } from '../../utils/constants';
|
||||
import ShareLinkPanel from '../share-link-panel';
|
||||
import GenerateUploadLink from './generate-upload-link';
|
||||
import ShareToUser from './share-to-user';
|
||||
@ -131,17 +131,21 @@ class ShareDialog extends React.Component {
|
||||
}
|
||||
{enableDirPrivateShare &&
|
||||
<Fragment>
|
||||
<NavItem role="tab" aria-selected={activeTab === 'shareToUser'} aria-controls="share-to-user-panel">
|
||||
<NavLink className={activeTab === 'shareToUser' ? 'active' : ''} onClick={this.toggle.bind(this, 'shareToUser')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
|
||||
{gettext('Share to user')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem role="tab" aria-selected={activeTab === 'shareToGroup'} aria-controls="share-to-group-panel">
|
||||
<NavLink className={activeTab === 'shareToGroup' ? 'active' : ''} onClick={this.toggle.bind(this, 'shareToGroup')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
|
||||
{gettext('Share to group')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
{isPro && !isCustomPermission && (
|
||||
{ canShareRepo && (
|
||||
<NavItem role="tab" aria-selected={activeTab === 'shareToUser'} aria-controls="share-to-user-panel">
|
||||
<NavLink className={activeTab === 'shareToUser' ? 'active' : ''} onClick={this.toggle.bind(this, 'shareToUser')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
|
||||
{gettext('Share to user')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
)}
|
||||
{ canShareRepo && (
|
||||
<NavItem role="tab" aria-selected={activeTab === 'shareToGroup'} aria-controls="share-to-group-panel">
|
||||
<NavLink className={activeTab === 'shareToGroup' ? 'active' : ''} onClick={this.toggle.bind(this, 'shareToGroup')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
|
||||
{gettext('Share to group')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
)}
|
||||
{isPro && !isCustomPermission && canShareRepo && (
|
||||
<NavItem role="tab" aria-selected={activeTab === 'customSharePermission'} aria-controls="custom-share-perm-panel">
|
||||
<NavLink className={activeTab === 'customSharePermission' ? 'active' : ''} onClick={this.toggle.bind(this, 'customSharePermission')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
|
||||
{gettext('Custom sharing permissions')}
|
||||
@ -307,7 +311,7 @@ class ShareDialog extends React.Component {
|
||||
return (
|
||||
<div className="external-share-message mt-2">
|
||||
<h6>{additionalShareDialogNote.title}</h6>
|
||||
<div style={{fontSize: '14px', color: '#666'}}>{additionalShareDialogNote.content}</div>
|
||||
<p style={{fontSize: '14px', color: '#666'}} className="text-wrap m-0">{additionalShareDialogNote.content}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -319,8 +323,8 @@ class ShareDialog extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<Modal isOpen={true} style={{maxWidth: '760px'}} className="share-dialog" toggle={this.props.toggleDialog}>
|
||||
<ModalHeader toggle={this.props.toggleDialog}>
|
||||
{gettext('Share')} <span className="op-target" title={itemName}>{itemName}</span>
|
||||
<ModalHeader toggle={this.props.toggleDialog} tag="div">
|
||||
<h5 className="text-truncate">{gettext('Share')} <span className="op-target" title={itemName}>{itemName}</span></h5>
|
||||
{this.renderExternalShareMessage()}
|
||||
</ModalHeader>
|
||||
<ModalBody className="share-dialog-content" role="tablist">
|
||||
|
@ -2,8 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import {
|
||||
gettext, siteRoot, canAddGroup,
|
||||
canAddRepo, canGenerateShareLink, canGenerateUploadLink, canInvitePeople,
|
||||
gettext, siteRoot, canAddGroup, canAddRepo, canShareRepo,
|
||||
canGenerateShareLink, canGenerateUploadLink, canInvitePeople,
|
||||
enableTC, sideNavFooterCustomHtml, additionalAppBottomLinks,
|
||||
canViewOrg, isDocs, isPro, isDBSqlite3, customNavItems
|
||||
} from '../utils/constants';
|
||||
@ -166,7 +166,7 @@ class MainSideNav extends React.Component {
|
||||
className={`nav sub-nav nav-pills flex-column ${this.state.sharedExtended ? 'side-panel-slide-share-admin' : 'side-panel-slide-up-share-admin'}`}
|
||||
style={style}
|
||||
>
|
||||
{canAddRepo && (
|
||||
{canAddRepo && canShareRepo && (
|
||||
<li className={`nav-item ${this.getActiveClass('share-admin-libs')}`}>
|
||||
<Link to={siteRoot + 'share-admin-libs/'} className={`nav-link ellipsis ${this.getActiveClass('share-admin-libs')}`} title={gettext('Libraries')} onClick={(e) => this.tabItemClick(e, 'share-admin-libs')}>
|
||||
<span aria-hidden="true" className="sharp">#</span>
|
||||
@ -174,12 +174,14 @@ class MainSideNav extends React.Component {
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
<li className={`nav-item ${this.getActiveClass('share-admin-folders')}`}>
|
||||
<Link to={siteRoot + 'share-admin-folders/'} className={`nav-link ellipsis ${this.getActiveClass('share-admin-folders')}`} title={gettext('Folders')} onClick={(e) => this.tabItemClick(e, 'share-admin-folders')}>
|
||||
<span aria-hidden="true" className="sharp">#</span>
|
||||
<span className="nav-text">{gettext('Folders')}</span>
|
||||
</Link>
|
||||
</li>
|
||||
{canShareRepo && (
|
||||
<li className={`nav-item ${this.getActiveClass('share-admin-folders')}`}>
|
||||
<Link to={siteRoot + 'share-admin-folders/'} className={`nav-link ellipsis ${this.getActiveClass('share-admin-folders')}`} title={gettext('Folders')} onClick={(e) => this.tabItemClick(e, 'share-admin-folders')}>
|
||||
<span aria-hidden="true" className="sharp">#</span>
|
||||
<span className="nav-text">{gettext('Folders')}</span>
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{linksNavItem}
|
||||
</ul>
|
||||
);
|
||||
|
@ -16,7 +16,7 @@ class TrafficTableBody extends React.Component {
|
||||
case 'user':
|
||||
if (userTrafficItem.name) {
|
||||
return (
|
||||
<a href={siteRoot + 'useradmin/info/' + userTrafficItem.email + '/'}>{userTrafficItem.name}</a>
|
||||
<a href={siteRoot + 'org/useradmin/info/' + userTrafficItem.email + '/'}>{userTrafficItem.name}</a>
|
||||
);
|
||||
}
|
||||
return(<span>{'--'}</span>);
|
||||
|
@ -16,7 +16,7 @@ class TrafficTableBody extends React.Component {
|
||||
case 'user':
|
||||
if (userTrafficItem.name) {
|
||||
return (
|
||||
<a href={siteRoot + 'useradmin/info/' + userTrafficItem.email + '/'}>{userTrafficItem.name}</a>
|
||||
<a href={siteRoot + 'sys/users/' + userTrafficItem.email + '/'}>{userTrafficItem.name}</a>
|
||||
);
|
||||
}
|
||||
return(<span>{'--'}</span>);
|
||||
|
@ -37,6 +37,7 @@ export const name = window.app.pageOptions.name;
|
||||
export const contactEmail = window.app.pageOptions.contactEmail;
|
||||
export const username = window.app.pageOptions.username;
|
||||
export const canAddRepo = window.app.pageOptions.canAddRepo;
|
||||
export const canShareRepo = window.app.pageOptions.canShareRepo;
|
||||
export const canAddGroup = window.app.pageOptions.canAddGroup;
|
||||
export const groupImportMembersExtraMsg = window.app.pageOptions.groupImportMembersExtraMsg;
|
||||
export const canGenerateShareLink = window.app.pageOptions.canGenerateShareLink;
|
||||
|
@ -112,7 +112,11 @@ log "Start Monitor"
|
||||
|
||||
while [ 1 ]; do
|
||||
|
||||
monitor_seafevents
|
||||
if [ $CLUSTER_MODE ] && [ $CLUSTER_MODE = "backend" ]; then
|
||||
:
|
||||
else
|
||||
monitor_seafevents
|
||||
fi
|
||||
|
||||
if [ $ENABLE_NOTIFICATION_SERVER ] && [ $ENABLE_NOTIFICATION_SERVER = "true" ]; then
|
||||
monitor_notification_server
|
||||
|
@ -20,6 +20,7 @@ from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.core.cache import cache
|
||||
|
||||
from seaserv import ccnet_api, seafile_api
|
||||
|
||||
@ -34,6 +35,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
SAML_PROVIDER_IDENTIFIER = getattr(settings, 'SAML_PROVIDER_IDENTIFIER', 'saml')
|
||||
SHIBBOLETH_AFFILIATION_ROLE_MAP = getattr(settings, 'SHIBBOLETH_AFFILIATION_ROLE_MAP', {})
|
||||
CACHE_KEY_GROUPS = "all_groups_cache"
|
||||
|
||||
|
||||
class Saml2Backend(ModelBackend):
|
||||
@ -196,9 +198,27 @@ class Saml2Backend(ModelBackend):
|
||||
|
||||
# support a list of comma-separated IDs as seafile_groups claim
|
||||
if len(seafile_groups) == 1 and ',' in seafile_groups[0]:
|
||||
seafile_groups = seafile_groups[0].split(',')
|
||||
seafile_groups = [group.strip() for group in seafile_groups[0].split(',')]
|
||||
|
||||
saml_group_ids = [int(group_id) for group_id in seafile_groups]
|
||||
if all(str(group_id).isdigit() for group_id in seafile_groups):
|
||||
# all groups are provided as numeric IDs
|
||||
saml_group_ids = [int(group_id) for group_id in seafile_groups]
|
||||
else:
|
||||
# groups are provided as names, try to get current group information from cache
|
||||
all_groups = cache.get(CACHE_KEY_GROUPS)
|
||||
if not all_groups or any(group not in all_groups for group in seafile_groups):
|
||||
# groups not yet cached or missing entry, reload groups from API
|
||||
all_groups = {group.group_name: group.id for group in ccnet_api.get_all_groups(-1, -1)}
|
||||
cache.set(CACHE_KEY_GROUPS, all_groups, 3600) # cache for 1 hour
|
||||
# create groups which are not yet existing
|
||||
for group in [group_name for group_name in seafile_groups if group_name not in all_groups]:
|
||||
new_group = ccnet_api.create_group(group, 'system admin') # we are not operating in user context here
|
||||
if new_group < 0:
|
||||
logger.error('failed to create group %s' % group)
|
||||
return
|
||||
all_groups[group] = new_group
|
||||
# generate numeric IDs from group names
|
||||
saml_group_ids = [id for group, id in all_groups.items() if group in seafile_groups]
|
||||
|
||||
joined_groups = ccnet_api.get_groups(user.username)
|
||||
joined_group_ids = [g.id for g in joined_groups]
|
||||
|
@ -22,9 +22,12 @@ from ldap.controls.libldap import SimplePagedResultsControl
|
||||
from seaserv import seafile_api, ccnet_api
|
||||
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.endpoints.utils import is_org_user
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.utils import api_error, to_python_boolean, get_user_common_info
|
||||
from seahub.api2.models import TokenV2
|
||||
from seahub.organizations.models import OrgSettings
|
||||
from seahub.organizations.views import gen_org_url_prefix
|
||||
from seahub.utils.ccnet_db import get_ccnet_db_name
|
||||
import seahub.settings as settings
|
||||
from seahub.settings import SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER, INIT_PASSWD, \
|
||||
@ -40,6 +43,7 @@ from seahub.utils import is_valid_username2, is_org_context, \
|
||||
is_pro_version, normalize_cache_key, is_valid_email, \
|
||||
IS_EMAIL_CONFIGURED, send_html_email, get_site_name, \
|
||||
gen_shared_link, gen_shared_upload_link
|
||||
from seahub.utils.db_api import SeafileDB
|
||||
|
||||
from seahub.utils.file_size import get_file_size_unit, byte_to_kb
|
||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr, \
|
||||
@ -48,6 +52,7 @@ from seahub.utils.user_permissions import get_user_role
|
||||
from seahub.utils.repo import normalize_repo_status_code
|
||||
from seahub.utils.ccnet_db import CcnetDB
|
||||
from seahub.constants import DEFAULT_ADMIN
|
||||
from seahub.constants import DEFAULT_ADMIN, DEFAULT_ORG
|
||||
from seahub.role_permissions.models import AdminRole
|
||||
from seahub.role_permissions.utils import get_available_roles
|
||||
from seahub.utils.licenseparse import user_number_over_limit
|
||||
@ -2139,3 +2144,61 @@ class AdminUserList(APIView):
|
||||
user_list.append(user_info)
|
||||
|
||||
return Response({'user_list': user_list})
|
||||
|
||||
|
||||
class AdminUserConvertToTeamView(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAdminUser,)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def post(self, request):
|
||||
username = request.data.get('email')
|
||||
if not username:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'email invalid.')
|
||||
|
||||
# resource check
|
||||
try:
|
||||
user = User.objects.get(email=username)
|
||||
except User.DoesNotExist:
|
||||
error_msg = 'User %s not found.' % username
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
if is_org_user(username):
|
||||
error_msg = 'User is already in team.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
org_role = DEFAULT_ORG
|
||||
|
||||
url_prefix = gen_org_url_prefix(3)
|
||||
if url_prefix is None:
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
# Use the nickname as the org_name, but the org_name does not support emoji
|
||||
nickname = email2nickname(username)
|
||||
nickname_characters = []
|
||||
for character in nickname:
|
||||
if len(character.encode('utf-8')) > 3:
|
||||
nickname_characters.append('_')
|
||||
else:
|
||||
nickname_characters.append(character)
|
||||
org_name = ''.join(nickname_characters)
|
||||
|
||||
try:
|
||||
# 1. Create a new org, and add the user(username) to org as a team admin
|
||||
# by ccnet_api.create_org
|
||||
org_id = ccnet_api.create_org(org_name, url_prefix, username)
|
||||
# 2. Update org-settings
|
||||
new_org = ccnet_api.get_org_by_id(org_id)
|
||||
OrgSettings.objects.add_or_update(new_org, org_role)
|
||||
# 3. Add user's repo to OrgRepo
|
||||
owned_repos = seafile_api.get_owned_repo_list(username, ret_corrupted=True)
|
||||
owned_repo_ids = [item.repo_id for item in owned_repos]
|
||||
seafile_db = SeafileDB()
|
||||
seafile_db.add_repos_to_org_user(org_id, username, owned_repo_ids)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
return Response({'success': True})
|
||||
|
@ -29,6 +29,9 @@ class CustomSharePermissionsView(APIView):
|
||||
"""List custom share permissions
|
||||
"""
|
||||
# permission check
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
if not check_folder_permission(request, repo_id, '/'):
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
@ -53,6 +56,9 @@ class CustomSharePermissionsView(APIView):
|
||||
def post(self, request, repo_id):
|
||||
"""Add a custom share permission
|
||||
"""
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
username = request.user.username
|
||||
# argument check
|
||||
permission = request.data.get('permission', None)
|
||||
@ -98,6 +104,9 @@ class CustomSharePermissionView(APIView):
|
||||
"""get a custom share permission
|
||||
"""
|
||||
# permission check
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
if not check_folder_permission(request, repo_id, '/'):
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
@ -122,6 +131,9 @@ class CustomSharePermissionView(APIView):
|
||||
"""Update a custom share permission
|
||||
"""
|
||||
# argument check
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
permission = request.data.get('permission', None)
|
||||
if not permission:
|
||||
error_msg = 'permission invalid.'
|
||||
@ -170,6 +182,9 @@ class CustomSharePermissionView(APIView):
|
||||
def delete(self, request, repo_id, permission_id):
|
||||
"""Delete a custom share permission
|
||||
"""
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
username = request.user.username
|
||||
|
||||
# permission check
|
||||
|
@ -61,10 +61,13 @@ class DirSharedItemsEndpoint(APIView):
|
||||
org_id = request.user.org.org_id
|
||||
if path == '/':
|
||||
share_items = seafile_api.list_org_repo_shared_to(org_id,
|
||||
repo_owner, repo_id)
|
||||
repo_owner,
|
||||
repo_id)
|
||||
else:
|
||||
share_items = seafile_api.get_org_shared_users_for_subdir(org_id,
|
||||
repo_id, path, repo_owner)
|
||||
repo_id,
|
||||
path,
|
||||
repo_owner)
|
||||
else:
|
||||
repo_owner = seafile_api.get_repo_owner(repo_id)
|
||||
if path == '/':
|
||||
@ -98,10 +101,13 @@ class DirSharedItemsEndpoint(APIView):
|
||||
org_id = request.user.org.org_id
|
||||
if path == '/':
|
||||
share_items = seafile_api.list_org_repo_shared_group(org_id,
|
||||
repo_owner, repo_id)
|
||||
repo_owner,
|
||||
repo_id)
|
||||
else:
|
||||
share_items = seafile_api.get_org_shared_groups_for_subdir(org_id,
|
||||
repo_id, path, repo_owner)
|
||||
repo_id,
|
||||
path,
|
||||
repo_owner)
|
||||
else:
|
||||
repo_owner = seafile_api.get_repo_owner(repo_id)
|
||||
if path == '/':
|
||||
@ -129,11 +135,10 @@ class DirSharedItemsEndpoint(APIView):
|
||||
org_id, repo_id, path, repo_owner, group_id)
|
||||
else:
|
||||
if path == '/':
|
||||
seafile_api.unset_group_repo(repo_id, group_id,
|
||||
repo_owner)
|
||||
seafile_api.unset_group_repo(repo_id, group_id, repo_owner)
|
||||
else:
|
||||
seafile_api.unshare_subdir_for_group(
|
||||
repo_id, path, repo_owner, group_id)
|
||||
seafile_api.unshare_subdir_for_group(repo_id, path,
|
||||
repo_owner, group_id)
|
||||
continue
|
||||
|
||||
ret.append({
|
||||
@ -197,6 +202,9 @@ class DirSharedItemsEndpoint(APIView):
|
||||
def get(self, request, repo_id, format=None):
|
||||
"""List shared items(shared to users/groups) for a folder/library.
|
||||
"""
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id)
|
||||
@ -220,6 +228,9 @@ class DirSharedItemsEndpoint(APIView):
|
||||
def post(self, request, repo_id, format=None):
|
||||
"""Update shared item permission.
|
||||
"""
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
username = request.user.username
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
@ -305,6 +316,10 @@ class DirSharedItemsEndpoint(APIView):
|
||||
content_type=json_content_type)
|
||||
|
||||
def put(self, request, repo_id, format=None):
|
||||
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
username = request.user.username
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
@ -368,8 +383,7 @@ class DirSharedItemsEndpoint(APIView):
|
||||
|
||||
if not is_org_user(to_user, int(org_id)):
|
||||
org_name = request.user.org.org_name
|
||||
error_msg = 'User %s is not member of organization %s.' \
|
||||
% (to_user, org_name)
|
||||
error_msg = f'User {to_user} is not member of organization {org_name}.'
|
||||
|
||||
result['failed'].append({
|
||||
'email': to_user,
|
||||
@ -377,7 +391,8 @@ class DirSharedItemsEndpoint(APIView):
|
||||
})
|
||||
continue
|
||||
|
||||
# when calling seafile API to share authority related functions, change the uesrname to repo owner.
|
||||
# when calling seafile API to share authority related functions,
|
||||
# change the uesrname to repo owner.
|
||||
repo_owner = seafile_api.get_org_repo_owner(repo_id)
|
||||
# can't share to owner
|
||||
if to_user == repo_owner:
|
||||
@ -477,7 +492,8 @@ class DirSharedItemsEndpoint(APIView):
|
||||
try:
|
||||
org_id = None
|
||||
if is_org_context(request):
|
||||
# when calling seafile API to share authority related functions, change the uesrname to repo owner.
|
||||
# when calling seafile API to share authority related functions,
|
||||
# change the uesrname to repo owner.
|
||||
repo_owner = seafile_api.get_org_repo_owner(repo_id)
|
||||
org_id = request.user.org.org_id
|
||||
|
||||
@ -513,9 +529,14 @@ class DirSharedItemsEndpoint(APIView):
|
||||
continue
|
||||
|
||||
return HttpResponse(json.dumps(result),
|
||||
status=200, content_type=json_content_type)
|
||||
status=200,
|
||||
content_type=json_content_type)
|
||||
|
||||
def delete(self, request, repo_id, format=None):
|
||||
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
username = request.user.username
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
@ -556,7 +577,7 @@ class DirSharedItemsEndpoint(APIView):
|
||||
|
||||
# Delete share permission at ExtraSharePermission table.
|
||||
if path == '/':
|
||||
ExtraSharePermission.objects.delete_share_permission(repo_id,
|
||||
ExtraSharePermission.objects.delete_share_permission(repo_id,
|
||||
shared_to)
|
||||
|
||||
org_id = None
|
||||
@ -601,8 +622,8 @@ class DirSharedItemsEndpoint(APIView):
|
||||
|
||||
# delete share permission if repo is deleted
|
||||
if path == '/':
|
||||
ExtraGroupsSharePermission.objects.delete_share_permission(repo_id,
|
||||
group_id)
|
||||
ExtraGroupsSharePermission.objects.delete_share_permission(repo_id,
|
||||
group_id)
|
||||
send_perm_audit_msg('delete-repo-perm', username, group_id,
|
||||
repo_id, path, permission)
|
||||
|
||||
|
@ -33,6 +33,8 @@ class SharedFolders(APIView):
|
||||
Permission checking:
|
||||
1. all authenticated user can perform this action.
|
||||
"""
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
shared_repos = []
|
||||
username = request.user.username
|
||||
|
@ -38,6 +38,8 @@ class SharedRepos(APIView):
|
||||
Permission checking:
|
||||
1. all authenticated user can perform this action.
|
||||
"""
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
shared_repos = []
|
||||
username = request.user.username
|
||||
@ -128,6 +130,9 @@ class SharedRepo(APIView):
|
||||
Permission checking:
|
||||
1. Only repo owner can update.
|
||||
"""
|
||||
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
# argument check
|
||||
permission = request.data.get('permission', None)
|
||||
@ -244,7 +249,9 @@ class SharedRepo(APIView):
|
||||
Permission checking:
|
||||
1. Only repo owner and system admin can unshare a publib library.
|
||||
"""
|
||||
|
||||
if not request.user.permissions.can_share_repo():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
# argument check
|
||||
share_type = request.GET.get('share_type', None)
|
||||
if not share_type:
|
||||
|
@ -8,6 +8,10 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from seahub.constants import DEFAULT_ORG
|
||||
from seahub.organizations.models import OrgSettings
|
||||
from seahub.organizations.settings import ORG_AUTO_URL_PREFIX
|
||||
from seahub.organizations.views import gen_org_url_prefix
|
||||
from seahub.utils import is_valid_email
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
@ -15,7 +19,12 @@ from seahub.api2.utils import api_error
|
||||
from seahub.base.templatetags.seahub_tags import email2nickname, \
|
||||
email2contact_email
|
||||
from seahub.profile.models import Profile, DetailedProfile
|
||||
from seahub.settings import ENABLE_UPDATE_USER_INFO, ENABLE_USER_SET_CONTACT_EMAIL
|
||||
from seahub.settings import ENABLE_UPDATE_USER_INFO, ENABLE_USER_SET_CONTACT_EMAIL, ENABLE_CONVERT_TO_TEAM_ACCOUNT
|
||||
|
||||
import seaserv
|
||||
from seaserv import ccnet_api, seafile_api
|
||||
|
||||
from seahub.utils.db_api import SeafileDB
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
json_content_type = 'application/json; charset=utf-8'
|
||||
@ -138,3 +147,54 @@ class User(APIView):
|
||||
# get user info and return
|
||||
info = self._get_user_info(email)
|
||||
return Response(info)
|
||||
|
||||
class UserConvertToTeamView(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def post(self, request):
|
||||
if not ENABLE_CONVERT_TO_TEAM_ACCOUNT:
|
||||
error_msg = 'Feature is not enabled.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
org_role = DEFAULT_ORG
|
||||
|
||||
if request.user.org:
|
||||
error_msg = 'User is already in team.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
url_prefix = gen_org_url_prefix(3)
|
||||
if url_prefix is None:
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
username = request.user.username
|
||||
# Use the nickname as the org_name, but the org_name does not support emoji
|
||||
nickname = email2nickname(username)
|
||||
nickname_characters = []
|
||||
for character in nickname:
|
||||
if len(character.encode('utf-8')) > 3:
|
||||
nickname_characters.append('_')
|
||||
else:
|
||||
nickname_characters.append(character)
|
||||
org_name = ''.join(nickname_characters)
|
||||
|
||||
try:
|
||||
# 1. Create a new org, and add the current to org as a team admin
|
||||
# by ccnet_api.create_org
|
||||
org_id = ccnet_api.create_org(org_name, url_prefix, username)
|
||||
# 2. Update org-settings
|
||||
new_org = ccnet_api.get_org_by_id(org_id)
|
||||
OrgSettings.objects.add_or_update(new_org, org_role)
|
||||
# 3. Add user's repo to OrgRepo
|
||||
owned_repos = seafile_api.get_owned_repo_list(username, ret_corrupted=True)
|
||||
owned_repo_ids = [item.repo_id for item in owned_repos]
|
||||
seafile_db = SeafileDB()
|
||||
seafile_db.add_repos_to_org_user(org_id, username, owned_repo_ids)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
return Response({'success': True})
|
||||
|
@ -1,17 +1,12 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
import datetime
|
||||
import hashlib
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import logging
|
||||
|
||||
# import auth
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from registration.signals import user_deleted
|
||||
from django.db import models
|
||||
from django.db.models.manager import EmptyManager
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
from django.utils.encoding import smart_str
|
||||
from django.db.models.manager import EmptyManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
UNUSABLE_PASSWORD = '!' # This will never be a valid hash
|
||||
@ -130,15 +125,26 @@ class AnonymousUser(object):
|
||||
|
||||
|
||||
class SocialAuthUserManager(models.Manager):
|
||||
|
||||
def add(self, username, provider, uid, extra_data=''):
|
||||
try:
|
||||
social_auth_user = self.model(username=username, provider=provider, uid=uid, extra_data=extra_data)
|
||||
social_auth_user = self.model(username=username, provider=provider,
|
||||
uid=uid, extra_data=extra_data)
|
||||
social_auth_user.save()
|
||||
return social_auth_user
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return None
|
||||
|
||||
def add_if_not_exists(self, username, provider, uid, extra_data=''):
|
||||
|
||||
social_auth_user = self.get_by_provider_and_uid(provider, uid)
|
||||
if not social_auth_user:
|
||||
social_auth_user = self.add(username, provider,
|
||||
uid, extra_data=extra_data)
|
||||
|
||||
return social_auth_user
|
||||
|
||||
def get_by_provider_and_uid(self, provider, uid):
|
||||
try:
|
||||
social_auth_user = self.get(provider=provider, uid=uid)
|
||||
@ -186,11 +192,7 @@ class ExternalDepartment(models.Model):
|
||||
db_table = 'external_department'
|
||||
|
||||
|
||||
# # handle signals
|
||||
from django.dispatch import receiver
|
||||
from registration.signals import user_deleted
|
||||
|
||||
|
||||
# handle signals
|
||||
@receiver(user_deleted)
|
||||
def user_deleted_cb(sender, **kwargs):
|
||||
username = kwargs['username']
|
||||
|
@ -35,6 +35,7 @@ from seahub.options.models import UserOptions
|
||||
from seahub.profile.models import Profile
|
||||
from seahub.two_factor.views.login import is_device_remembered
|
||||
from seahub.utils import render_error, get_site_name, is_valid_email
|
||||
from seahub.utils.http import rate_limit
|
||||
from seahub.utils.ip import get_remote_ip
|
||||
from seahub.utils.file_size import get_quota_from_string
|
||||
from seahub.utils.two_factor_auth import two_factor_auth_enabled, handle_two_factor_auth
|
||||
@ -195,10 +196,8 @@ def login(request, template_name='registration/login.html',
|
||||
getattr(settings, 'ENABLE_KRB5_LOGIN', False) or \
|
||||
getattr(settings, 'ENABLE_ADFS_LOGIN', False) or \
|
||||
getattr(settings, 'ENABLE_OAUTH', False) or \
|
||||
getattr(settings, 'ENABLE_DINGTALK', False) or \
|
||||
getattr(settings, 'ENABLE_CAS', False) or \
|
||||
getattr(settings, 'ENABLE_REMOTE_USER_AUTHENTICATION', False) or \
|
||||
getattr(settings, 'ENABLE_WORK_WEIXIN', False)
|
||||
getattr(settings, 'ENABLE_REMOTE_USER_AUTHENTICATION', False)
|
||||
|
||||
login_bg_image_path = get_login_bg_image_path()
|
||||
|
||||
@ -348,11 +347,15 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
|
||||
# prompts for a new password
|
||||
# - password_reset_complete shows a success message for the above
|
||||
|
||||
|
||||
@csrf_protect
|
||||
def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html',
|
||||
email_template_name='registration/password_reset_email.html',
|
||||
password_reset_form=PasswordResetForm, token_generator=default_token_generator,
|
||||
post_reset_redirect=None):
|
||||
@rate_limit()
|
||||
def password_reset(request, is_admin_site=False,
|
||||
template_name='registration/password_reset_form.html',
|
||||
email_template_name='registration/password_reset_email.html',
|
||||
password_reset_form=PasswordResetForm,
|
||||
token_generator=default_token_generator,
|
||||
post_reset_redirect=None):
|
||||
|
||||
has_bind_social_auth = False
|
||||
if SocialAuthUser.objects.filter(username=request.user.username).exists():
|
||||
|
@ -359,6 +359,9 @@ class UserPermissions(object):
|
||||
def can_add_repo(self):
|
||||
return self._get_perm_by_roles('can_add_repo')
|
||||
|
||||
def can_share_repo(self):
|
||||
return self._get_perm_by_roles('can_share_repo')
|
||||
|
||||
def can_add_group(self):
|
||||
return self._get_perm_by_roles('can_add_group')
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
# Generated by Django 4.2.2 on 2024-06-12 10:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import seahub.base.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0004_externaldepartment_socialauthuser_usermonitoredrepos_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ClientSSOToken',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('token', models.CharField(max_length=100, unique=True)),
|
||||
('username', seahub.base.fields.LowerCaseCharField(blank=True, db_index=True, max_length=255, null=True)),
|
||||
('status', models.CharField(choices=[('waiting', 'waiting'), ('success', 'success'), ('error', 'error')], default='waiting', max_length=10)),
|
||||
('api_key', models.CharField(blank=True, max_length=40, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(blank=True, db_index=True, null=True)),
|
||||
('accessed_at', models.DateTimeField(blank=True, db_index=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='socialauthuser',
|
||||
name='extra_data',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.2 on 2024-06-12 10:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('invitations', '0006_reposhareinvitation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='invitation',
|
||||
name='invite_type',
|
||||
field=models.CharField(choices=[('guest', 'guest'), ('default', 'default')], default='guest', max_length=20),
|
||||
),
|
||||
]
|
@ -0,0 +1,33 @@
|
||||
# Generated by Django 4.2.2 on 2024-06-12 10:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('organizations', '0004_orgsamlconfig_orgadminsettings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='orgsamlconfig',
|
||||
name='dns_txt',
|
||||
field=models.CharField(blank=True, max_length=64, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orgsamlconfig',
|
||||
name='domain_verified',
|
||||
field=models.BooleanField(db_index=True, default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orgsamlconfig',
|
||||
name='idp_certificate',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orgsamlconfig',
|
||||
name='domain',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, unique=True),
|
||||
),
|
||||
]
|
@ -27,6 +27,7 @@ def merge_roles(default, custom):
|
||||
DEFAULT_ENABLED_ROLE_PERMISSIONS = {
|
||||
DEFAULT_USER: {
|
||||
'can_add_repo': True,
|
||||
'can_share_repo': True,
|
||||
'can_add_group': True,
|
||||
'can_view_org': True,
|
||||
'can_add_public_repo': False,
|
||||
@ -48,6 +49,7 @@ DEFAULT_ENABLED_ROLE_PERMISSIONS = {
|
||||
},
|
||||
GUEST_USER: {
|
||||
'can_add_repo': False,
|
||||
'can_share_repo': False,
|
||||
'can_add_group': False,
|
||||
'can_view_org': False,
|
||||
'can_add_public_repo': False,
|
||||
|
@ -349,6 +349,9 @@ LOGOUT_REDIRECT_URL = None
|
||||
|
||||
ACCOUNT_ACTIVATION_DAYS = 7
|
||||
|
||||
REQUEST_RATE_LIMIT_NUMBER = 3
|
||||
REQUEST_RATE_LIMIT_PERIOD = 60 # seconds
|
||||
|
||||
# allow seafile admin view user's repo
|
||||
ENABLE_SYS_ADMIN_VIEW_REPO = False
|
||||
|
||||
@ -489,6 +492,8 @@ ENABLE_SEAFILE_DOCS = False
|
||||
# enable integration seatbale
|
||||
ENABLE_SEATABLE_INTEGRATION = False
|
||||
|
||||
ENABLE_CONVERT_TO_TEAM_ACCOUNT = False
|
||||
|
||||
# File preview
|
||||
FILE_PREVIEW_MAX_SIZE = 30 * 1024 * 1024
|
||||
FILE_ENCODING_LIST = ['auto', 'utf-8', 'gbk', 'ISO-8859-1', 'ISO-8859-5']
|
||||
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.2 on 2024-06-12 10:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('share', '0002_customsharepermissions_alter_fileshare_permission_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='uploadlinkshare',
|
||||
name='expire_date',
|
||||
field=models.DateTimeField(db_index=True, null=True),
|
||||
),
|
||||
]
|
@ -66,6 +66,7 @@
|
||||
guideEnabled: {% if guide_enabled %} true {% else %} false {% endif %},
|
||||
trashReposExpireDays: {% if trash_repos_expire_days >= 0 %} {{ trash_repos_expire_days }} {% else %} null {% endif %},
|
||||
canAddRepo: {% if user.permissions.can_add_repo %} true {% else %} false {% endif %},
|
||||
canShareRepo: {% if user.permissions.can_share_repo %} true {% else %} false {% endif %},
|
||||
canAddGroup: {% if user.permissions.can_add_group %} true {% else %} false {% endif %},
|
||||
groupImportMembersExtraMsg: "{{group_import_members_extra_msg}}",
|
||||
canGenerateShareLink: {% if user.permissions.can_generate_share_link %} true {% else %} false {% endif %},
|
||||
|
@ -93,7 +93,7 @@ from seahub.api2.endpoints.repo_draft_info import RepoDraftInfo, RepoDraftCounts
|
||||
from seahub.api2.endpoints.activities import ActivitiesView
|
||||
from seahub.api2.endpoints.wiki_pages import WikiPagesDirView, WikiPageContentView
|
||||
from seahub.api2.endpoints.revision_tag import TaggedItemsView, TagNamesView
|
||||
from seahub.api2.endpoints.user import User
|
||||
from seahub.api2.endpoints.user import User, UserConvertToTeamView
|
||||
from seahub.api2.endpoints.auth_token_by_session import AuthTokenBySession
|
||||
from seahub.api2.endpoints.repo_tags import RepoTagsView, RepoTagView
|
||||
from seahub.api2.endpoints.file_tag import RepoFileTagsView, RepoFileTagView
|
||||
@ -141,7 +141,7 @@ from seahub.api2.endpoints.admin.devices import AdminDevices
|
||||
from seahub.api2.endpoints.admin.device_errors import AdminDeviceErrors
|
||||
from seahub.api2.endpoints.admin.users import AdminUsers, AdminUser, AdminUserResetPassword, AdminAdminUsers, \
|
||||
AdminUserGroups, AdminUserShareLinks, AdminUserUploadLinks, AdminUserBeSharedRepos, \
|
||||
AdminLDAPUsers, AdminSearchUser, AdminUpdateUserCcnetEmail, AdminUserList
|
||||
AdminLDAPUsers, AdminSearchUser, AdminUpdateUserCcnetEmail, AdminUserList, AdminUserConvertToTeamView
|
||||
from seahub.api2.endpoints.admin.device_trusted_ip import AdminDeviceTrustedIP
|
||||
from seahub.api2.endpoints.admin.libraries import AdminLibraries, AdminLibrary, \
|
||||
AdminSearchLibrary
|
||||
@ -316,6 +316,9 @@ urlpatterns = [
|
||||
|
||||
## user
|
||||
re_path(r'^api/v2.1/user/$', User.as_view(), name="api-v2.1-user"),
|
||||
|
||||
# user:convert to team account
|
||||
re_path(r'^api/v2.1/user/convert-to-team/$', UserConvertToTeamView.as_view(), name="api-v2.1-user-convert-to-team"),
|
||||
|
||||
## obtain auth token by login session
|
||||
re_path(r'^api/v2.1/auth-token-by-session/$', AuthTokenBySession.as_view(), name="api-v2.1-auth-token-by-session"),
|
||||
@ -596,6 +599,7 @@ urlpatterns = [
|
||||
re_path(r'^api/v2.1/admin/search-user/$', AdminSearchUser.as_view(), name='api-v2.1-admin-search-user'),
|
||||
re_path(r'^api/v2.1/admin/update-user-ccnet-email/$', AdminUpdateUserCcnetEmail.as_view(), name='api-v2.1-admin-update-user-ccnet-email'),
|
||||
re_path(r'^api/v2.1/admin/user-list/$', AdminUserList.as_view(), name='api-v2.1-admin-user-list'),
|
||||
re_path(r'^api/v2.1/admin/user-convert-to-team/$', AdminUserConvertToTeamView.as_view(), name='api-v2.1-admin-user-list'),
|
||||
|
||||
# [^...] Matches any single character not in brackets
|
||||
# + Matches between one and unlimited times, as many times as possible
|
||||
|
@ -348,3 +348,12 @@ class SeafileDB:
|
||||
repo_ids.append(repo_id)
|
||||
del_repo_trash(cursor, repo_ids)
|
||||
cursor.close()
|
||||
|
||||
def add_repos_to_org_user(self, org_id, username, repo_ids):
|
||||
for repo_id in repo_ids:
|
||||
sql = f"""
|
||||
INSERT INTO `{self.db_name}`.`OrgRepo` (org_id, repo_id, user)
|
||||
VALUES ({org_id}, "{repo_id}", "{username}");
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql)
|
||||
|
@ -1,10 +1,18 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
|
||||
|
||||
import time
|
||||
import json
|
||||
|
||||
from functools import wraps
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import gettext as _
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, \
|
||||
HttpResponseForbidden
|
||||
|
||||
from seahub.settings import REQUEST_RATE_LIMIT_NUMBER, \
|
||||
REQUEST_RATE_LIMIT_PERIOD
|
||||
|
||||
JSON_CONTENT_TYPE = 'application/json; charset=utf-8'
|
||||
|
||||
|
||||
class _HTTPException(Exception):
|
||||
def __init__(self, message=''):
|
||||
@ -13,13 +21,15 @@ class _HTTPException(Exception):
|
||||
def __str__(self):
|
||||
return '%s: %s' % (self.__class__.__name__, self.message)
|
||||
|
||||
|
||||
class BadRequestException(_HTTPException):
|
||||
pass
|
||||
|
||||
|
||||
class RequestForbbiddenException(_HTTPException):
|
||||
pass
|
||||
|
||||
JSON_CONTENT_TYPE = 'application/json; charset=utf-8'
|
||||
|
||||
def json_response(func):
|
||||
@wraps(func)
|
||||
def wrapped(*a, **kw):
|
||||
@ -36,6 +46,7 @@ def json_response(func):
|
||||
content_type=JSON_CONTENT_TYPE)
|
||||
return wrapped
|
||||
|
||||
|
||||
def int_param(request, key):
|
||||
v = request.GET.get(key, None)
|
||||
if not v:
|
||||
@ -44,3 +55,37 @@ def int_param(request, key):
|
||||
return int(v)
|
||||
except ValueError:
|
||||
raise BadRequestException()
|
||||
|
||||
|
||||
def rate_limit(number=REQUEST_RATE_LIMIT_NUMBER,
|
||||
period=REQUEST_RATE_LIMIT_PERIOD):
|
||||
"""
|
||||
:param number: number of requests
|
||||
:param period: second
|
||||
"""
|
||||
def decorator(func):
|
||||
|
||||
@wraps(func)
|
||||
def wrapped(request, *args, **kwargs):
|
||||
|
||||
if REQUEST_RATE_LIMIT_NUMBER > 0 and \
|
||||
REQUEST_RATE_LIMIT_PERIOD > 0:
|
||||
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
cache_key = f"rate_limit:{ip}"
|
||||
|
||||
current_time = time.time()
|
||||
data = cache.get(cache_key, {'count': 0, 'start_time': current_time})
|
||||
if current_time - data['start_time'] > period:
|
||||
data = {'count': 1, 'start_time': current_time}
|
||||
else:
|
||||
data['count'] += 1
|
||||
|
||||
cache.set(cache_key, data, timeout=period)
|
||||
|
||||
if data['count'] > number:
|
||||
return HttpResponse(_("Too many requests"), status=429)
|
||||
|
||||
return func(request, *args, **kwargs)
|
||||
return wrapped
|
||||
return decorator
|
||||
|
@ -2,8 +2,9 @@
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
import requests
|
||||
import hashlib
|
||||
import logging
|
||||
@ -30,12 +31,14 @@ from seahub.settings import ENABLE_WATERMARK
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_access_token_cache_key(token):
|
||||
""" Generate cache key for WOPI access token
|
||||
"""
|
||||
|
||||
return 'wopi_access_token_' + str(token)
|
||||
|
||||
|
||||
def get_file_info_by_token(token):
|
||||
""" Get file info from cache by access token
|
||||
|
||||
@ -50,6 +53,7 @@ def get_file_info_by_token(token):
|
||||
|
||||
return value if value else None
|
||||
|
||||
|
||||
def generate_discovery_cache_key(name, ext):
|
||||
""" Generate cache key for office web app hosting discovery
|
||||
|
||||
@ -59,9 +63,10 @@ def generate_discovery_cache_key(name, ext):
|
||||
|
||||
return 'wopi_' + name + '_' + ext
|
||||
|
||||
|
||||
def get_wopi_dict(request_user, repo_id, file_path,
|
||||
action_name='view', can_download=True,
|
||||
language_code='en', obj_id=''):
|
||||
action_name='view', can_download=True,
|
||||
language_code='en', obj_id=''):
|
||||
""" Prepare dict data for WOPI host page
|
||||
"""
|
||||
|
||||
@ -82,6 +87,14 @@ def get_wopi_dict(request_user, repo_id, file_path,
|
||||
file_ext = 'xlsx'
|
||||
|
||||
wopi_key = generate_discovery_cache_key(action_name, file_ext)
|
||||
|
||||
if OFFICE_SERVER_TYPE.lower() == 'collaboraoffice':
|
||||
# Since the hosting discover page of Collabora Online does not provide
|
||||
# a URL with the action set to "view" for some common file formats,
|
||||
# we always use "edit" here.
|
||||
# Preview file is achieved by setting `UserCanWrite` to `False` in `CheckFileInfo`.
|
||||
wopi_key = generate_discovery_cache_key('edit', file_ext)
|
||||
|
||||
action_url = cache.get(wopi_key)
|
||||
|
||||
if not action_url:
|
||||
@ -90,12 +103,13 @@ def get_wopi_dict(request_user, repo_id, file_path,
|
||||
try:
|
||||
if OFFICE_WEB_APP_CLIENT_CERT and OFFICE_WEB_APP_CLIENT_KEY:
|
||||
xml = requests.get(OFFICE_WEB_APP_BASE_URL,
|
||||
cert=(OFFICE_WEB_APP_CLIENT_CERT, OFFICE_WEB_APP_CLIENT_KEY),
|
||||
verify=OFFICE_WEB_APP_SERVER_CA)
|
||||
cert=(OFFICE_WEB_APP_CLIENT_CERT,
|
||||
OFFICE_WEB_APP_CLIENT_KEY),
|
||||
verify=OFFICE_WEB_APP_SERVER_CA)
|
||||
elif OFFICE_WEB_APP_CLIENT_PEM:
|
||||
xml = requests.get(OFFICE_WEB_APP_BASE_URL,
|
||||
cert=OFFICE_WEB_APP_CLIENT_PEM,
|
||||
verify=OFFICE_WEB_APP_SERVER_CA)
|
||||
cert=OFFICE_WEB_APP_CLIENT_PEM,
|
||||
verify=OFFICE_WEB_APP_SERVER_CA)
|
||||
else:
|
||||
xml = requests.get(OFFICE_WEB_APP_BASE_URL, verify=OFFICE_WEB_APP_SERVER_CA)
|
||||
except Exception as e:
|
||||
@ -119,7 +133,7 @@ def get_wopi_dict(request_user, repo_id, file_path,
|
||||
tmp_action_url = re.sub(r'<.*>', '', urlsrc)
|
||||
tmp_wopi_key = generate_discovery_cache_key(name, ext)
|
||||
cache.set(tmp_wopi_key, tmp_action_url,
|
||||
OFFICE_WEB_APP_DISCOVERY_EXPIRATION)
|
||||
OFFICE_WEB_APP_DISCOVERY_EXPIRATION)
|
||||
|
||||
if wopi_key == tmp_wopi_key:
|
||||
action_url = tmp_action_url
|
||||
|
@ -261,7 +261,7 @@ class WOPIFilesView(APIView):
|
||||
result['ReadOnly'] = True if not can_edit else False
|
||||
|
||||
avatar_url, _, _ = api_avatar_url(request_user, int(72))
|
||||
result['UserExtraInfo'] = { 'avatar': avatar_url, 'mail': request_user }
|
||||
result['UserExtraInfo'] = {'avatar': avatar_url, 'mail': request_user}
|
||||
|
||||
# new file creation feature is not implemented on wopi host(seahub)
|
||||
# hide save as button on view/edit file page
|
||||
|
@ -11,4 +11,4 @@ class UtilsTest(BaseTestCase):
|
||||
assert DEFAULT_USER in get_available_roles()
|
||||
|
||||
def test_get_enabled_role_permissions_by_role(self):
|
||||
assert len(list(get_enabled_role_permissions_by_role(DEFAULT_USER).keys())) == 19
|
||||
assert len(list(get_enabled_role_permissions_by_role(DEFAULT_USER).keys())) == 20
|
||||
|
@ -48,7 +48,7 @@ class ShibbolethRemoteUserMiddlewareTest(BaseTestCase):
|
||||
|
||||
self.request.META = {}
|
||||
self.request.META['Shibboleth-eppn'] = 'sampledeveloper@school.edu'
|
||||
self.request.META['REMOTE_USER'] = 'sampledeveloper@school.edu'
|
||||
self.request.META['HTTP_REMOTE_USER'] = 'sampledeveloper@school.edu'
|
||||
self.request.META['givenname'] = 'test_gname'
|
||||
self.request.META['surname'] = 'test_sname'
|
||||
self.request.META['Shibboleth-displayName'] = 'Sample Developer'
|
||||
@ -68,6 +68,12 @@ class ShibbolethRemoteUserMiddlewareTest(BaseTestCase):
|
||||
def test_can_process(self):
|
||||
assert len(Profile.objects.all()) == 0
|
||||
|
||||
# logout first
|
||||
from seahub.auth.models import AnonymousUser
|
||||
self.request.session.flush()
|
||||
self.request.user = AnonymousUser()
|
||||
|
||||
# then login user via thibboleth
|
||||
self.middleware.process_request(self.request)
|
||||
shib_user = SocialAuthUser.objects.get_by_provider_and_uid(
|
||||
SHIBBOLETH_PROVIDER_IDENTIFIER, 'sampledeveloper@school.edu')
|
||||
@ -95,6 +101,12 @@ class ShibbolethRemoteUserMiddlewareTest(BaseTestCase):
|
||||
def test_can_process_user_role(self):
|
||||
assert len(Profile.objects.all()) == 0
|
||||
|
||||
# logout first
|
||||
from seahub.auth.models import AnonymousUser
|
||||
self.request.session.flush()
|
||||
self.request.user = AnonymousUser()
|
||||
|
||||
# then login user via thibboleth
|
||||
self.middleware.process_request(self.request)
|
||||
shib_user = SocialAuthUser.objects.get_by_provider_and_uid(
|
||||
SHIBBOLETH_PROVIDER_IDENTIFIER, 'sampledeveloper@school.edu')
|
||||
@ -191,4 +203,3 @@ class ShibbolethRemoteUserMiddlewareTest(BaseTestCase):
|
||||
assert obj._get_role_by_affiliation('student1@school.edu') == 'student'
|
||||
assert obj._get_role_by_affiliation('a@x.edu') == 'aaa'
|
||||
assert obj._get_role_by_affiliation('a@x.com') == 'guest'
|
||||
|
||||
|
@ -2,30 +2,31 @@
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
#At a minimum you will need username,
|
||||
# At a minimum you will need username,
|
||||
default_shib_attributes = {
|
||||
"Shibboleth-eppn": (True, "username"),
|
||||
}
|
||||
}
|
||||
|
||||
SHIB_ATTRIBUTE_MAP = getattr(settings, 'SHIBBOLETH_ATTRIBUTE_MAP', default_shib_attributes)
|
||||
#Set to true if you are testing and want to insert sample headers.
|
||||
# Set to true if you are testing and want to insert sample headers.
|
||||
SHIB_MOCK_HEADERS = getattr(settings, 'SHIBBOLETH_MOCK_HEADERS', False)
|
||||
|
||||
SHIB_USER_HEADER = getattr(settings, 'SHIBBOLETH_USER_HEADER', "REMOTE_USER")
|
||||
SHIB_USER_HEADER = getattr(settings, 'SHIBBOLETH_USER_HEADER', "HTTP_REMOTE_USER")
|
||||
SHIB_USER_HEADER_SECOND_UID = getattr(settings,
|
||||
'SHIBBOLETH_USER_HEADER_SECOND_UID',
|
||||
'HTTP_REMOTE_USER_SUBJECT_ID')
|
||||
|
||||
LOGIN_URL = getattr(settings, 'LOGIN_URL', None)
|
||||
|
||||
if not LOGIN_URL:
|
||||
raise ImproperlyConfigured("A LOGIN_URL is required. Specify in settings.py")
|
||||
|
||||
#Optional logout parameters
|
||||
#This should look like: https://sso.school.edu/idp/logout.jsp?return=%s
|
||||
#The return url variable will be replaced in the LogoutView.
|
||||
# Optional logout parameters
|
||||
# This should look like: https://sso.school.edu/idp/logout.jsp?return=%s
|
||||
# The return url variable will be replaced in the LogoutView.
|
||||
LOGOUT_URL = getattr(settings, 'SHIBBOLETH_LOGOUT_URL', None)
|
||||
#LOGOUT_REDIRECT_URL specifies a default logout page that will always be used when
|
||||
#users logout from Shibboleth.
|
||||
# LOGOUT_REDIRECT_URL specifies a default logout page that will always be used when
|
||||
# users logout from Shibboleth.
|
||||
LOGOUT_REDIRECT_URL = getattr(settings, 'SHIBBOLETH_LOGOUT_REDIRECT_URL', None)
|
||||
#Name of key. Probably no need to change this.
|
||||
# Name of key. Probably no need to change this.
|
||||
LOGOUT_SESSION_KEY = getattr(settings, 'SHIBBOLETH_FORCE_REAUTH_SESSION_KEY', 'shib_force_reauth')
|
||||
|
||||
|
||||
|
@ -38,7 +38,7 @@ class ShibbolethRemoteUserBackend(RemoteUserBackend):
|
||||
user = None
|
||||
return user
|
||||
|
||||
def authenticate(self, remote_user, shib_meta):
|
||||
def authenticate(self, remote_user, shib_meta, second_uid=''):
|
||||
"""
|
||||
The username passed as ``remote_user`` is considered trusted. This
|
||||
method simply returns the ``User`` object with the given username,
|
||||
@ -69,10 +69,21 @@ class ShibbolethRemoteUserBackend(RemoteUserBackend):
|
||||
except User.DoesNotExist:
|
||||
user = None
|
||||
|
||||
if user and second_uid:
|
||||
SocialAuthUser.objects.add_if_not_exists(user.username,
|
||||
SHIBBOLETH_PROVIDER_IDENTIFIER,
|
||||
second_uid)
|
||||
|
||||
if not user and self.create_unknown_user:
|
||||
try:
|
||||
user = User.objects.create_shib_user(is_active=self.activate_after_creation)
|
||||
SocialAuthUser.objects.add(user.username, SHIBBOLETH_PROVIDER_IDENTIFIER, remote_user)
|
||||
SocialAuthUser.objects.add_if_not_exists(user.username,
|
||||
SHIBBOLETH_PROVIDER_IDENTIFIER,
|
||||
remote_user)
|
||||
if second_uid:
|
||||
SocialAuthUser.objects.add_if_not_exists(user.username,
|
||||
SHIBBOLETH_PROVIDER_IDENTIFIER,
|
||||
second_uid)
|
||||
except Exception as e:
|
||||
logger.error('create shib user failed: %s' % e)
|
||||
return None
|
||||
|
@ -11,7 +11,8 @@ from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from seaserv import seafile_api, ccnet_api
|
||||
|
||||
from shibboleth.app_settings import SHIB_ATTRIBUTE_MAP, LOGOUT_SESSION_KEY, SHIB_USER_HEADER
|
||||
from shibboleth.app_settings import SHIB_ATTRIBUTE_MAP, LOGOUT_SESSION_KEY, \
|
||||
SHIB_USER_HEADER, SHIB_USER_HEADER_SECOND_UID
|
||||
|
||||
from seahub import auth
|
||||
from seahub.base.accounts import User
|
||||
@ -40,8 +41,7 @@ SHIBBOLETH_PROVIDER_IDENTIFIER = getattr(settings, 'SHIBBOLETH_PROVIDER_IDENTIFI
|
||||
|
||||
class ShibbolethRemoteUserMiddleware(RemoteUserMiddleware):
|
||||
"""
|
||||
Authentication Middleware for use with Shibboleth. Uses the recommended pattern
|
||||
for remote authentication from: http://code.djangoproject.com/svn/django/tags/releases/1.3/django/contrib/auth/middleware.py
|
||||
Authentication Middleware for use with Shibboleth.
|
||||
"""
|
||||
def process_request(self, request):
|
||||
if request.path.rstrip('/') != settings.SITE_ROOT + 'sso':
|
||||
@ -74,19 +74,13 @@ class ShibbolethRemoteUserMiddleware(RemoteUserMiddleware):
|
||||
# AuthenticationMiddleware).
|
||||
return
|
||||
|
||||
second_uid = request.META.get(SHIB_USER_HEADER_SECOND_UID, '')
|
||||
|
||||
# If the user is already authenticated and that user is the user we are
|
||||
# getting passed in the headers, then the correct user is already
|
||||
# persisted in the session and we don't need to continue.
|
||||
if request.user.is_authenticated:
|
||||
# If user is already authenticated, the value of request.user.username should be random ID of user,
|
||||
# not the SHIB_USER_HEADER in the request header
|
||||
shib_user = SocialAuthUser.objects.get_by_provider_and_uid(SHIBBOLETH_PROVIDER_IDENTIFIER, remote_user)
|
||||
if shib_user:
|
||||
remote_user = shib_user.username
|
||||
if request.user.username == remote_user:
|
||||
if request.user.is_staff:
|
||||
update_sudo_mode_ts(request)
|
||||
return
|
||||
return
|
||||
|
||||
# Make sure we have all required Shiboleth elements before proceeding.
|
||||
shib_meta, error = self.parse_attributes(request)
|
||||
@ -98,7 +92,9 @@ class ShibbolethRemoteUserMiddleware(RemoteUserMiddleware):
|
||||
|
||||
# We are seeing this user for the first time in this session, attempt
|
||||
# to authenticate the user.
|
||||
user = auth.authenticate(remote_user=remote_user, shib_meta=shib_meta)
|
||||
user = auth.authenticate(remote_user=remote_user,
|
||||
shib_meta=shib_meta,
|
||||
second_uid=second_uid)
|
||||
if user:
|
||||
if not user.is_active:
|
||||
return HttpResponseRedirect(reverse('shib_complete'))
|
||||
|
Loading…
Reference in New Issue
Block a user