1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-27 11:10:10 +00:00

Merge branch 'master' into 12.0

This commit is contained in:
lian 2024-06-24 12:00:15 +08:00
commit 8bcc05a6cd
34 changed files with 523 additions and 116 deletions

View File

@ -25,8 +25,8 @@ jobs:
- name: clone and build - name: clone and build
run: | run: |
git clone --depth=1 --branch=master https://github.com/haiwen/seafile-test-deploy /tmp/seafile-test-deploy 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 master:master && git checkout master cd /tmp/seafile-test-deploy && git fetch origin 11.0:11.0 && git checkout 11.0
./bootstrap.sh ./bootstrap.sh
- name: pip install - name: pip install

View File

@ -1,7 +1,7 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap'; 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 ShareLinkPanel from '../share-link-panel';
import GenerateUploadLink from './generate-upload-link'; import GenerateUploadLink from './generate-upload-link';
import ShareToUser from './share-to-user'; import ShareToUser from './share-to-user';
@ -131,17 +131,21 @@ class ShareDialog extends React.Component {
} }
{enableDirPrivateShare && {enableDirPrivateShare &&
<Fragment> <Fragment>
{ canShareRepo && (
<NavItem role="tab" aria-selected={activeTab === 'shareToUser'} aria-controls="share-to-user-panel"> <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}> <NavLink className={activeTab === 'shareToUser' ? 'active' : ''} onClick={this.toggle.bind(this, 'shareToUser')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
{gettext('Share to user')} {gettext('Share to user')}
</NavLink> </NavLink>
</NavItem> </NavItem>
)}
{ canShareRepo && (
<NavItem role="tab" aria-selected={activeTab === 'shareToGroup'} aria-controls="share-to-group-panel"> <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}> <NavLink className={activeTab === 'shareToGroup' ? 'active' : ''} onClick={this.toggle.bind(this, 'shareToGroup')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
{gettext('Share to group')} {gettext('Share to group')}
</NavLink> </NavLink>
</NavItem> </NavItem>
{isPro && !isCustomPermission && ( )}
{isPro && !isCustomPermission && canShareRepo && (
<NavItem role="tab" aria-selected={activeTab === 'customSharePermission'} aria-controls="custom-share-perm-panel"> <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}> <NavLink className={activeTab === 'customSharePermission' ? 'active' : ''} onClick={this.toggle.bind(this, 'customSharePermission')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
{gettext('Custom sharing permissions')} {gettext('Custom sharing permissions')}
@ -307,7 +311,7 @@ class ShareDialog extends React.Component {
return ( return (
<div className="external-share-message mt-2"> <div className="external-share-message mt-2">
<h6>{additionalShareDialogNote.title}</h6> <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> </div>
); );
} }
@ -319,8 +323,8 @@ class ShareDialog extends React.Component {
return ( return (
<div> <div>
<Modal isOpen={true} style={{maxWidth: '760px'}} className="share-dialog" toggle={this.props.toggleDialog}> <Modal isOpen={true} style={{maxWidth: '760px'}} className="share-dialog" toggle={this.props.toggleDialog}>
<ModalHeader toggle={this.props.toggleDialog}> <ModalHeader toggle={this.props.toggleDialog} tag="div">
{gettext('Share')} <span className="op-target" title={itemName}>{itemName}</span> <h5 className="text-truncate">{gettext('Share')} <span className="op-target" title={itemName}>{itemName}</span></h5>
{this.renderExternalShareMessage()} {this.renderExternalShareMessage()}
</ModalHeader> </ModalHeader>
<ModalBody className="share-dialog-content" role="tablist"> <ModalBody className="share-dialog-content" role="tablist">

View File

@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Link } from '@gatsbyjs/reach-router'; import { Link } from '@gatsbyjs/reach-router';
import { import {
gettext, siteRoot, canAddGroup, gettext, siteRoot, canAddGroup, canAddRepo, canShareRepo,
canAddRepo, canGenerateShareLink, canGenerateUploadLink, canInvitePeople, canGenerateShareLink, canGenerateUploadLink, canInvitePeople,
enableTC, sideNavFooterCustomHtml, additionalAppBottomLinks, enableTC, sideNavFooterCustomHtml, additionalAppBottomLinks,
canViewOrg, isDocs, isPro, isDBSqlite3, customNavItems canViewOrg, isDocs, isPro, isDBSqlite3, customNavItems
} from '../utils/constants'; } 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'}`} className={`nav sub-nav nav-pills flex-column ${this.state.sharedExtended ? 'side-panel-slide-share-admin' : 'side-panel-slide-up-share-admin'}`}
style={style} style={style}
> >
{canAddRepo && ( {canAddRepo && canShareRepo && (
<li className={`nav-item ${this.getActiveClass('share-admin-libs')}`}> <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')}> <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> <span aria-hidden="true" className="sharp">#</span>
@ -174,12 +174,14 @@ class MainSideNav extends React.Component {
</Link> </Link>
</li> </li>
)} )}
{canShareRepo && (
<li className={`nav-item ${this.getActiveClass('share-admin-folders')}`}> <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')}> <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 aria-hidden="true" className="sharp">#</span>
<span className="nav-text">{gettext('Folders')}</span> <span className="nav-text">{gettext('Folders')}</span>
</Link> </Link>
</li> </li>
)}
{linksNavItem} {linksNavItem}
</ul> </ul>
); );

View File

@ -16,7 +16,7 @@ class TrafficTableBody extends React.Component {
case 'user': case 'user':
if (userTrafficItem.name) { if (userTrafficItem.name) {
return ( 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>); return(<span>{'--'}</span>);

View File

@ -16,7 +16,7 @@ class TrafficTableBody extends React.Component {
case 'user': case 'user':
if (userTrafficItem.name) { if (userTrafficItem.name) {
return ( return (
<a href={siteRoot + 'useradmin/info/' + userTrafficItem.email + '/'}>{userTrafficItem.name}</a> <a href={siteRoot + 'sys/users/' + userTrafficItem.email + '/'}>{userTrafficItem.name}</a>
); );
} }
return(<span>{'--'}</span>); return(<span>{'--'}</span>);

View File

@ -37,6 +37,7 @@ export const name = window.app.pageOptions.name;
export const contactEmail = window.app.pageOptions.contactEmail; export const contactEmail = window.app.pageOptions.contactEmail;
export const username = window.app.pageOptions.username; export const username = window.app.pageOptions.username;
export const canAddRepo = window.app.pageOptions.canAddRepo; export const canAddRepo = window.app.pageOptions.canAddRepo;
export const canShareRepo = window.app.pageOptions.canShareRepo;
export const canAddGroup = window.app.pageOptions.canAddGroup; export const canAddGroup = window.app.pageOptions.canAddGroup;
export const groupImportMembersExtraMsg = window.app.pageOptions.groupImportMembersExtraMsg; export const groupImportMembersExtraMsg = window.app.pageOptions.groupImportMembersExtraMsg;
export const canGenerateShareLink = window.app.pageOptions.canGenerateShareLink; export const canGenerateShareLink = window.app.pageOptions.canGenerateShareLink;

View File

@ -112,7 +112,11 @@ log "Start Monitor"
while [ 1 ]; do while [ 1 ]; do
if [ $CLUSTER_MODE ] && [ $CLUSTER_MODE = "backend" ]; then
:
else
monitor_seafevents monitor_seafevents
fi
if [ $ENABLE_NOTIFICATION_SERVER ] && [ $ENABLE_NOTIFICATION_SERVER = "true" ]; then if [ $ENABLE_NOTIFICATION_SERVER ] && [ $ENABLE_NOTIFICATION_SERVER = "true" ]; then
monitor_notification_server monitor_notification_server

View File

@ -20,6 +20,7 @@ from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
from django.core.cache import cache
from seaserv import ccnet_api, seafile_api from seaserv import ccnet_api, seafile_api
@ -34,6 +35,7 @@ logger = logging.getLogger(__name__)
SAML_PROVIDER_IDENTIFIER = getattr(settings, 'SAML_PROVIDER_IDENTIFIER', 'saml') SAML_PROVIDER_IDENTIFIER = getattr(settings, 'SAML_PROVIDER_IDENTIFIER', 'saml')
SHIBBOLETH_AFFILIATION_ROLE_MAP = getattr(settings, 'SHIBBOLETH_AFFILIATION_ROLE_MAP', {}) SHIBBOLETH_AFFILIATION_ROLE_MAP = getattr(settings, 'SHIBBOLETH_AFFILIATION_ROLE_MAP', {})
CACHE_KEY_GROUPS = "all_groups_cache"
class Saml2Backend(ModelBackend): class Saml2Backend(ModelBackend):
@ -196,9 +198,27 @@ class Saml2Backend(ModelBackend):
# support a list of comma-separated IDs as seafile_groups claim # support a list of comma-separated IDs as seafile_groups claim
if len(seafile_groups) == 1 and ',' in seafile_groups[0]: 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(',')]
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] 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_groups = ccnet_api.get_groups(user.username)
joined_group_ids = [g.id for g in joined_groups] joined_group_ids = [g.id for g in joined_groups]

View File

@ -22,9 +22,12 @@ from ldap.controls.libldap import SimplePagedResultsControl
from seaserv import seafile_api, ccnet_api from seaserv import seafile_api, ccnet_api
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.endpoints.utils import is_org_user
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error, to_python_boolean, get_user_common_info from seahub.api2.utils import api_error, to_python_boolean, get_user_common_info
from seahub.api2.models import TokenV2 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 from seahub.utils.ccnet_db import get_ccnet_db_name
import seahub.settings as settings import seahub.settings as settings
from seahub.settings import SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER, INIT_PASSWD, \ 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_pro_version, normalize_cache_key, is_valid_email, \
IS_EMAIL_CONFIGURED, send_html_email, get_site_name, \ IS_EMAIL_CONFIGURED, send_html_email, get_site_name, \
gen_shared_link, gen_shared_upload_link 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.file_size import get_file_size_unit, byte_to_kb
from seahub.utils.timeutils import timestamp_to_isoformat_timestr, \ 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.repo import normalize_repo_status_code
from seahub.utils.ccnet_db import CcnetDB from seahub.utils.ccnet_db import CcnetDB
from seahub.constants import DEFAULT_ADMIN 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.models import AdminRole
from seahub.role_permissions.utils import get_available_roles from seahub.role_permissions.utils import get_available_roles
from seahub.utils.licenseparse import user_number_over_limit from seahub.utils.licenseparse import user_number_over_limit
@ -2139,3 +2144,61 @@ class AdminUserList(APIView):
user_list.append(user_info) user_list.append(user_info)
return Response({'user_list': user_list}) 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})

View File

@ -29,6 +29,9 @@ class CustomSharePermissionsView(APIView):
"""List custom share permissions """List custom share permissions
""" """
# permission check # 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, '/'): if not check_folder_permission(request, repo_id, '/'):
error_msg = 'Permission denied.' error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg) return api_error(status.HTTP_403_FORBIDDEN, error_msg)
@ -53,6 +56,9 @@ class CustomSharePermissionsView(APIView):
def post(self, request, repo_id): def post(self, request, repo_id):
"""Add a custom share permission """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 username = request.user.username
# argument check # argument check
permission = request.data.get('permission', None) permission = request.data.get('permission', None)
@ -98,6 +104,9 @@ class CustomSharePermissionView(APIView):
"""get a custom share permission """get a custom share permission
""" """
# permission check # 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, '/'): if not check_folder_permission(request, repo_id, '/'):
error_msg = 'Permission denied.' error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg) return api_error(status.HTTP_403_FORBIDDEN, error_msg)
@ -122,6 +131,9 @@ class CustomSharePermissionView(APIView):
"""Update a custom share permission """Update a custom share permission
""" """
# argument check # 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) permission = request.data.get('permission', None)
if not permission: if not permission:
error_msg = 'permission invalid.' error_msg = 'permission invalid.'
@ -170,6 +182,9 @@ class CustomSharePermissionView(APIView):
def delete(self, request, repo_id, permission_id): def delete(self, request, repo_id, permission_id):
"""Delete a custom share permission """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 username = request.user.username
# permission check # permission check

View File

@ -61,10 +61,13 @@ class DirSharedItemsEndpoint(APIView):
org_id = request.user.org.org_id org_id = request.user.org.org_id
if path == '/': if path == '/':
share_items = seafile_api.list_org_repo_shared_to(org_id, share_items = seafile_api.list_org_repo_shared_to(org_id,
repo_owner, repo_id) repo_owner,
repo_id)
else: else:
share_items = seafile_api.get_org_shared_users_for_subdir(org_id, share_items = seafile_api.get_org_shared_users_for_subdir(org_id,
repo_id, path, repo_owner) repo_id,
path,
repo_owner)
else: else:
repo_owner = seafile_api.get_repo_owner(repo_id) repo_owner = seafile_api.get_repo_owner(repo_id)
if path == '/': if path == '/':
@ -98,10 +101,13 @@ class DirSharedItemsEndpoint(APIView):
org_id = request.user.org.org_id org_id = request.user.org.org_id
if path == '/': if path == '/':
share_items = seafile_api.list_org_repo_shared_group(org_id, share_items = seafile_api.list_org_repo_shared_group(org_id,
repo_owner, repo_id) repo_owner,
repo_id)
else: else:
share_items = seafile_api.get_org_shared_groups_for_subdir(org_id, share_items = seafile_api.get_org_shared_groups_for_subdir(org_id,
repo_id, path, repo_owner) repo_id,
path,
repo_owner)
else: else:
repo_owner = seafile_api.get_repo_owner(repo_id) repo_owner = seafile_api.get_repo_owner(repo_id)
if path == '/': if path == '/':
@ -129,11 +135,10 @@ class DirSharedItemsEndpoint(APIView):
org_id, repo_id, path, repo_owner, group_id) org_id, repo_id, path, repo_owner, group_id)
else: else:
if path == '/': if path == '/':
seafile_api.unset_group_repo(repo_id, group_id, seafile_api.unset_group_repo(repo_id, group_id, repo_owner)
repo_owner)
else: else:
seafile_api.unshare_subdir_for_group( seafile_api.unshare_subdir_for_group(repo_id, path,
repo_id, path, repo_owner, group_id) repo_owner, group_id)
continue continue
ret.append({ ret.append({
@ -197,6 +202,9 @@ class DirSharedItemsEndpoint(APIView):
def get(self, request, repo_id, format=None): def get(self, request, repo_id, format=None):
"""List shared items(shared to users/groups) for a folder/library. """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) repo = seafile_api.get_repo(repo_id)
if not repo: if not repo:
return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id) 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): def post(self, request, repo_id, format=None):
"""Update shared item permission. """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 username = request.user.username
repo = seafile_api.get_repo(repo_id) repo = seafile_api.get_repo(repo_id)
if not repo: if not repo:
@ -305,6 +316,10 @@ class DirSharedItemsEndpoint(APIView):
content_type=json_content_type) content_type=json_content_type)
def put(self, request, repo_id, format=None): 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 username = request.user.username
repo = seafile_api.get_repo(repo_id) repo = seafile_api.get_repo(repo_id)
if not repo: if not repo:
@ -368,8 +383,7 @@ class DirSharedItemsEndpoint(APIView):
if not is_org_user(to_user, int(org_id)): if not is_org_user(to_user, int(org_id)):
org_name = request.user.org.org_name org_name = request.user.org.org_name
error_msg = 'User %s is not member of organization %s.' \ error_msg = f'User {to_user} is not member of organization {org_name}.'
% (to_user, org_name)
result['failed'].append({ result['failed'].append({
'email': to_user, 'email': to_user,
@ -377,7 +391,8 @@ class DirSharedItemsEndpoint(APIView):
}) })
continue 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) repo_owner = seafile_api.get_org_repo_owner(repo_id)
# can't share to owner # can't share to owner
if to_user == repo_owner: if to_user == repo_owner:
@ -477,7 +492,8 @@ class DirSharedItemsEndpoint(APIView):
try: try:
org_id = None org_id = None
if is_org_context(request): 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) repo_owner = seafile_api.get_org_repo_owner(repo_id)
org_id = request.user.org.org_id org_id = request.user.org.org_id
@ -513,9 +529,14 @@ class DirSharedItemsEndpoint(APIView):
continue continue
return HttpResponse(json.dumps(result), 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): 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 username = request.user.username
repo = seafile_api.get_repo(repo_id) repo = seafile_api.get_repo(repo_id)
if not repo: if not repo:

View File

@ -33,6 +33,8 @@ class SharedFolders(APIView):
Permission checking: Permission checking:
1. all authenticated user can perform this action. 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 = [] shared_repos = []
username = request.user.username username = request.user.username

View File

@ -38,6 +38,8 @@ class SharedRepos(APIView):
Permission checking: Permission checking:
1. all authenticated user can perform this action. 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 = [] shared_repos = []
username = request.user.username username = request.user.username
@ -129,6 +131,9 @@ class SharedRepo(APIView):
1. Only repo owner can update. 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 # argument check
permission = request.data.get('permission', None) permission = request.data.get('permission', None)
if permission not in get_available_repo_perms(): if permission not in get_available_repo_perms():
@ -244,6 +249,8 @@ class SharedRepo(APIView):
Permission checking: Permission checking:
1. Only repo owner and system admin can unshare a publib library. 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 # argument check
share_type = request.GET.get('share_type', None) share_type = request.GET.get('share_type', None)

View File

@ -8,6 +8,10 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from django.utils.translation import gettext as _ 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.utils import is_valid_email
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle 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, \ 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 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__) logger = logging.getLogger(__name__)
json_content_type = 'application/json; charset=utf-8' json_content_type = 'application/json; charset=utf-8'
@ -138,3 +147,54 @@ class User(APIView):
# get user info and return # get user info and return
info = self._get_user_info(email) info = self._get_user_info(email)
return Response(info) 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})

View File

@ -1,17 +1,12 @@
# Copyright (c) 2012-2016 Seafile Ltd. # Copyright (c) 2012-2016 Seafile Ltd.
import datetime
import hashlib import hashlib
import urllib.request, urllib.parse, urllib.error
import logging import logging
from registration.signals import user_deleted
# import auth
from django.core.exceptions import ImproperlyConfigured
from django.db import models 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.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__) logger = logging.getLogger(__name__)
UNUSABLE_PASSWORD = '!' # This will never be a valid hash UNUSABLE_PASSWORD = '!' # This will never be a valid hash
@ -130,15 +125,26 @@ class AnonymousUser(object):
class SocialAuthUserManager(models.Manager): class SocialAuthUserManager(models.Manager):
def add(self, username, provider, uid, extra_data=''): def add(self, username, provider, uid, extra_data=''):
try: 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() social_auth_user.save()
return social_auth_user return social_auth_user
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
return None 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): def get_by_provider_and_uid(self, provider, uid):
try: try:
social_auth_user = self.get(provider=provider, uid=uid) social_auth_user = self.get(provider=provider, uid=uid)
@ -186,11 +192,7 @@ class ExternalDepartment(models.Model):
db_table = 'external_department' db_table = 'external_department'
# # handle signals # handle signals
from django.dispatch import receiver
from registration.signals import user_deleted
@receiver(user_deleted) @receiver(user_deleted)
def user_deleted_cb(sender, **kwargs): def user_deleted_cb(sender, **kwargs):
username = kwargs['username'] username = kwargs['username']

View File

@ -35,6 +35,7 @@ from seahub.options.models import UserOptions
from seahub.profile.models import Profile from seahub.profile.models import Profile
from seahub.two_factor.views.login import is_device_remembered 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 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.ip import get_remote_ip
from seahub.utils.file_size import get_quota_from_string 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 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_KRB5_LOGIN', False) or \
getattr(settings, 'ENABLE_ADFS_LOGIN', False) or \ getattr(settings, 'ENABLE_ADFS_LOGIN', False) or \
getattr(settings, 'ENABLE_OAUTH', False) or \ getattr(settings, 'ENABLE_OAUTH', False) or \
getattr(settings, 'ENABLE_DINGTALK', False) or \
getattr(settings, 'ENABLE_CAS', False) or \ getattr(settings, 'ENABLE_CAS', False) or \
getattr(settings, 'ENABLE_REMOTE_USER_AUTHENTICATION', False) or \ getattr(settings, 'ENABLE_REMOTE_USER_AUTHENTICATION', False)
getattr(settings, 'ENABLE_WORK_WEIXIN', False)
login_bg_image_path = get_login_bg_image_path() login_bg_image_path = get_login_bg_image_path()
@ -348,10 +347,14 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
# prompts for a new password # prompts for a new password
# - password_reset_complete shows a success message for the above # - password_reset_complete shows a success message for the above
@csrf_protect @csrf_protect
def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html', @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', email_template_name='registration/password_reset_email.html',
password_reset_form=PasswordResetForm, token_generator=default_token_generator, password_reset_form=PasswordResetForm,
token_generator=default_token_generator,
post_reset_redirect=None): post_reset_redirect=None):
has_bind_social_auth = False has_bind_social_auth = False

View File

@ -359,6 +359,9 @@ class UserPermissions(object):
def can_add_repo(self): def can_add_repo(self):
return self._get_perm_by_roles('can_add_repo') 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): def can_add_group(self):
return self._get_perm_by_roles('can_add_group') return self._get_perm_by_roles('can_add_group')

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -27,6 +27,7 @@ def merge_roles(default, custom):
DEFAULT_ENABLED_ROLE_PERMISSIONS = { DEFAULT_ENABLED_ROLE_PERMISSIONS = {
DEFAULT_USER: { DEFAULT_USER: {
'can_add_repo': True, 'can_add_repo': True,
'can_share_repo': True,
'can_add_group': True, 'can_add_group': True,
'can_view_org': True, 'can_view_org': True,
'can_add_public_repo': False, 'can_add_public_repo': False,
@ -48,6 +49,7 @@ DEFAULT_ENABLED_ROLE_PERMISSIONS = {
}, },
GUEST_USER: { GUEST_USER: {
'can_add_repo': False, 'can_add_repo': False,
'can_share_repo': False,
'can_add_group': False, 'can_add_group': False,
'can_view_org': False, 'can_view_org': False,
'can_add_public_repo': False, 'can_add_public_repo': False,

View File

@ -349,6 +349,9 @@ LOGOUT_REDIRECT_URL = None
ACCOUNT_ACTIVATION_DAYS = 7 ACCOUNT_ACTIVATION_DAYS = 7
REQUEST_RATE_LIMIT_NUMBER = 3
REQUEST_RATE_LIMIT_PERIOD = 60 # seconds
# allow seafile admin view user's repo # allow seafile admin view user's repo
ENABLE_SYS_ADMIN_VIEW_REPO = False ENABLE_SYS_ADMIN_VIEW_REPO = False
@ -489,6 +492,8 @@ ENABLE_SEAFILE_DOCS = False
# enable integration seatbale # enable integration seatbale
ENABLE_SEATABLE_INTEGRATION = False ENABLE_SEATABLE_INTEGRATION = False
ENABLE_CONVERT_TO_TEAM_ACCOUNT = False
# File preview # File preview
FILE_PREVIEW_MAX_SIZE = 30 * 1024 * 1024 FILE_PREVIEW_MAX_SIZE = 30 * 1024 * 1024
FILE_ENCODING_LIST = ['auto', 'utf-8', 'gbk', 'ISO-8859-1', 'ISO-8859-5'] FILE_ENCODING_LIST = ['auto', 'utf-8', 'gbk', 'ISO-8859-1', 'ISO-8859-5']

View File

@ -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),
),
]

View File

@ -66,6 +66,7 @@
guideEnabled: {% if guide_enabled %} true {% else %} false {% endif %}, guideEnabled: {% if guide_enabled %} true {% else %} false {% endif %},
trashReposExpireDays: {% if trash_repos_expire_days >= 0 %} {{ trash_repos_expire_days }} {% else %} null {% 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 %}, 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 %}, canAddGroup: {% if user.permissions.can_add_group %} true {% else %} false {% endif %},
groupImportMembersExtraMsg: "{{group_import_members_extra_msg}}", groupImportMembersExtraMsg: "{{group_import_members_extra_msg}}",
canGenerateShareLink: {% if user.permissions.can_generate_share_link %} true {% else %} false {% endif %}, canGenerateShareLink: {% if user.permissions.can_generate_share_link %} true {% else %} false {% endif %},

View File

@ -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.activities import ActivitiesView
from seahub.api2.endpoints.wiki_pages import WikiPagesDirView, WikiPageContentView from seahub.api2.endpoints.wiki_pages import WikiPagesDirView, WikiPageContentView
from seahub.api2.endpoints.revision_tag import TaggedItemsView, TagNamesView 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.auth_token_by_session import AuthTokenBySession
from seahub.api2.endpoints.repo_tags import RepoTagsView, RepoTagView from seahub.api2.endpoints.repo_tags import RepoTagsView, RepoTagView
from seahub.api2.endpoints.file_tag import RepoFileTagsView, RepoFileTagView 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.device_errors import AdminDeviceErrors
from seahub.api2.endpoints.admin.users import AdminUsers, AdminUser, AdminUserResetPassword, AdminAdminUsers, \ from seahub.api2.endpoints.admin.users import AdminUsers, AdminUser, AdminUserResetPassword, AdminAdminUsers, \
AdminUserGroups, AdminUserShareLinks, AdminUserUploadLinks, AdminUserBeSharedRepos, \ 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.device_trusted_ip import AdminDeviceTrustedIP
from seahub.api2.endpoints.admin.libraries import AdminLibraries, AdminLibrary, \ from seahub.api2.endpoints.admin.libraries import AdminLibraries, AdminLibrary, \
AdminSearchLibrary AdminSearchLibrary
@ -317,6 +317,9 @@ urlpatterns = [
## user ## user
re_path(r'^api/v2.1/user/$', User.as_view(), name="api-v2.1-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 ## 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"), 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/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/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-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 any single character not in brackets
# + Matches between one and unlimited times, as many times as possible # + Matches between one and unlimited times, as many times as possible

View File

@ -348,3 +348,12 @@ class SeafileDB:
repo_ids.append(repo_id) repo_ids.append(repo_id)
del_repo_trash(cursor, repo_ids) del_repo_trash(cursor, repo_ids)
cursor.close() 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)

View File

@ -1,10 +1,18 @@
# Copyright (c) 2012-2016 Seafile Ltd. # Copyright (c) 2012-2016 Seafile Ltd.
import time
import json import json
from functools import wraps 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): class _HTTPException(Exception):
def __init__(self, message=''): def __init__(self, message=''):
@ -13,13 +21,15 @@ class _HTTPException(Exception):
def __str__(self): def __str__(self):
return '%s: %s' % (self.__class__.__name__, self.message) return '%s: %s' % (self.__class__.__name__, self.message)
class BadRequestException(_HTTPException): class BadRequestException(_HTTPException):
pass pass
class RequestForbbiddenException(_HTTPException): class RequestForbbiddenException(_HTTPException):
pass pass
JSON_CONTENT_TYPE = 'application/json; charset=utf-8'
def json_response(func): def json_response(func):
@wraps(func) @wraps(func)
def wrapped(*a, **kw): def wrapped(*a, **kw):
@ -36,6 +46,7 @@ def json_response(func):
content_type=JSON_CONTENT_TYPE) content_type=JSON_CONTENT_TYPE)
return wrapped return wrapped
def int_param(request, key): def int_param(request, key):
v = request.GET.get(key, None) v = request.GET.get(key, None)
if not v: if not v:
@ -44,3 +55,37 @@ def int_param(request, key):
return int(v) return int(v)
except ValueError: except ValueError:
raise BadRequestException() 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

View File

@ -2,8 +2,9 @@
import os import os
import re import re
import time import time
import urllib.request, urllib.parse, urllib.error import urllib.request
import urllib.parse import urllib.parse
import urllib.error
import requests import requests
import hashlib import hashlib
import logging import logging
@ -30,12 +31,14 @@ from seahub.settings import ENABLE_WATERMARK
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def generate_access_token_cache_key(token): def generate_access_token_cache_key(token):
""" Generate cache key for WOPI access token """ Generate cache key for WOPI access token
""" """
return 'wopi_access_token_' + str(token) return 'wopi_access_token_' + str(token)
def get_file_info_by_token(token): def get_file_info_by_token(token):
""" Get file info from cache by access 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 return value if value else None
def generate_discovery_cache_key(name, ext): def generate_discovery_cache_key(name, ext):
""" Generate cache key for office web app hosting discovery """ Generate cache key for office web app hosting discovery
@ -59,6 +63,7 @@ def generate_discovery_cache_key(name, ext):
return 'wopi_' + name + '_' + ext return 'wopi_' + name + '_' + ext
def get_wopi_dict(request_user, repo_id, file_path, def get_wopi_dict(request_user, repo_id, file_path,
action_name='view', can_download=True, action_name='view', can_download=True,
language_code='en', obj_id=''): language_code='en', obj_id=''):
@ -82,6 +87,14 @@ def get_wopi_dict(request_user, repo_id, file_path,
file_ext = 'xlsx' file_ext = 'xlsx'
wopi_key = generate_discovery_cache_key(action_name, file_ext) 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) action_url = cache.get(wopi_key)
if not action_url: if not action_url:
@ -90,7 +103,8 @@ def get_wopi_dict(request_user, repo_id, file_path,
try: try:
if OFFICE_WEB_APP_CLIENT_CERT and OFFICE_WEB_APP_CLIENT_KEY: if OFFICE_WEB_APP_CLIENT_CERT and OFFICE_WEB_APP_CLIENT_KEY:
xml = requests.get(OFFICE_WEB_APP_BASE_URL, xml = requests.get(OFFICE_WEB_APP_BASE_URL,
cert=(OFFICE_WEB_APP_CLIENT_CERT, OFFICE_WEB_APP_CLIENT_KEY), cert=(OFFICE_WEB_APP_CLIENT_CERT,
OFFICE_WEB_APP_CLIENT_KEY),
verify=OFFICE_WEB_APP_SERVER_CA) verify=OFFICE_WEB_APP_SERVER_CA)
elif OFFICE_WEB_APP_CLIENT_PEM: elif OFFICE_WEB_APP_CLIENT_PEM:
xml = requests.get(OFFICE_WEB_APP_BASE_URL, xml = requests.get(OFFICE_WEB_APP_BASE_URL,

View File

@ -261,7 +261,7 @@ class WOPIFilesView(APIView):
result['ReadOnly'] = True if not can_edit else False result['ReadOnly'] = True if not can_edit else False
avatar_url, _, _ = api_avatar_url(request_user, int(72)) 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) # new file creation feature is not implemented on wopi host(seahub)
# hide save as button on view/edit file page # hide save as button on view/edit file page

View File

@ -11,4 +11,4 @@ class UtilsTest(BaseTestCase):
assert DEFAULT_USER in get_available_roles() assert DEFAULT_USER in get_available_roles()
def test_get_enabled_role_permissions_by_role(self): 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

View File

@ -48,7 +48,7 @@ class ShibbolethRemoteUserMiddlewareTest(BaseTestCase):
self.request.META = {} self.request.META = {}
self.request.META['Shibboleth-eppn'] = 'sampledeveloper@school.edu' 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['givenname'] = 'test_gname'
self.request.META['surname'] = 'test_sname' self.request.META['surname'] = 'test_sname'
self.request.META['Shibboleth-displayName'] = 'Sample Developer' self.request.META['Shibboleth-displayName'] = 'Sample Developer'
@ -68,6 +68,12 @@ class ShibbolethRemoteUserMiddlewareTest(BaseTestCase):
def test_can_process(self): def test_can_process(self):
assert len(Profile.objects.all()) == 0 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) self.middleware.process_request(self.request)
shib_user = SocialAuthUser.objects.get_by_provider_and_uid( shib_user = SocialAuthUser.objects.get_by_provider_and_uid(
SHIBBOLETH_PROVIDER_IDENTIFIER, 'sampledeveloper@school.edu') SHIBBOLETH_PROVIDER_IDENTIFIER, 'sampledeveloper@school.edu')
@ -95,6 +101,12 @@ class ShibbolethRemoteUserMiddlewareTest(BaseTestCase):
def test_can_process_user_role(self): def test_can_process_user_role(self):
assert len(Profile.objects.all()) == 0 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) self.middleware.process_request(self.request)
shib_user = SocialAuthUser.objects.get_by_provider_and_uid( shib_user = SocialAuthUser.objects.get_by_provider_and_uid(
SHIBBOLETH_PROVIDER_IDENTIFIER, 'sampledeveloper@school.edu') 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('student1@school.edu') == 'student'
assert obj._get_role_by_affiliation('a@x.edu') == 'aaa' assert obj._get_role_by_affiliation('a@x.edu') == 'aaa'
assert obj._get_role_by_affiliation('a@x.com') == 'guest' assert obj._get_role_by_affiliation('a@x.com') == 'guest'

View File

@ -2,30 +2,31 @@
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
#At a minimum you will need username, # At a minimum you will need username,
default_shib_attributes = { default_shib_attributes = {
"Shibboleth-eppn": (True, "username"), "Shibboleth-eppn": (True, "username"),
} }
SHIB_ATTRIBUTE_MAP = getattr(settings, 'SHIBBOLETH_ATTRIBUTE_MAP', default_shib_attributes) 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_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) LOGIN_URL = getattr(settings, 'LOGIN_URL', None)
if not LOGIN_URL: if not LOGIN_URL:
raise ImproperlyConfigured("A LOGIN_URL is required. Specify in settings.py") raise ImproperlyConfigured("A LOGIN_URL is required. Specify in settings.py")
#Optional logout parameters # Optional logout parameters
#This should look like: https://sso.school.edu/idp/logout.jsp?return=%s # This should look like: https://sso.school.edu/idp/logout.jsp?return=%s
#The return url variable will be replaced in the LogoutView. # The return url variable will be replaced in the LogoutView.
LOGOUT_URL = getattr(settings, 'SHIBBOLETH_LOGOUT_URL', None) LOGOUT_URL = getattr(settings, 'SHIBBOLETH_LOGOUT_URL', None)
#LOGOUT_REDIRECT_URL specifies a default logout page that will always be used when # LOGOUT_REDIRECT_URL specifies a default logout page that will always be used when
#users logout from Shibboleth. # users logout from Shibboleth.
LOGOUT_REDIRECT_URL = getattr(settings, 'SHIBBOLETH_LOGOUT_REDIRECT_URL', None) 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') LOGOUT_SESSION_KEY = getattr(settings, 'SHIBBOLETH_FORCE_REAUTH_SESSION_KEY', 'shib_force_reauth')

View File

@ -38,7 +38,7 @@ class ShibbolethRemoteUserBackend(RemoteUserBackend):
user = None user = None
return user 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 The username passed as ``remote_user`` is considered trusted. This
method simply returns the ``User`` object with the given username, method simply returns the ``User`` object with the given username,
@ -69,10 +69,21 @@ class ShibbolethRemoteUserBackend(RemoteUserBackend):
except User.DoesNotExist: except User.DoesNotExist:
user = None 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: if not user and self.create_unknown_user:
try: try:
user = User.objects.create_shib_user(is_active=self.activate_after_creation) 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: except Exception as e:
logger.error('create shib user failed: %s' % e) logger.error('create shib user failed: %s' % e)
return None return None

View File

@ -11,7 +11,8 @@ from django.urls import reverse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from seaserv import seafile_api, ccnet_api 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 import auth
from seahub.base.accounts import User from seahub.base.accounts import User
@ -40,8 +41,7 @@ SHIBBOLETH_PROVIDER_IDENTIFIER = getattr(settings, 'SHIBBOLETH_PROVIDER_IDENTIFI
class ShibbolethRemoteUserMiddleware(RemoteUserMiddleware): class ShibbolethRemoteUserMiddleware(RemoteUserMiddleware):
""" """
Authentication Middleware for use with Shibboleth. Uses the recommended pattern Authentication Middleware for use with Shibboleth.
for remote authentication from: http://code.djangoproject.com/svn/django/tags/releases/1.3/django/contrib/auth/middleware.py
""" """
def process_request(self, request): def process_request(self, request):
if request.path.rstrip('/') != settings.SITE_ROOT + 'sso': if request.path.rstrip('/') != settings.SITE_ROOT + 'sso':
@ -74,18 +74,12 @@ class ShibbolethRemoteUserMiddleware(RemoteUserMiddleware):
# AuthenticationMiddleware). # AuthenticationMiddleware).
return 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 # 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 # getting passed in the headers, then the correct user is already
# persisted in the session and we don't need to continue. # persisted in the session and we don't need to continue.
if request.user.is_authenticated: 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. # Make sure we have all required Shiboleth elements before proceeding.
@ -98,7 +92,9 @@ class ShibbolethRemoteUserMiddleware(RemoteUserMiddleware):
# We are seeing this user for the first time in this session, attempt # We are seeing this user for the first time in this session, attempt
# to authenticate the user. # 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 user:
if not user.is_active: if not user.is_active:
return HttpResponseRedirect(reverse('shib_complete')) return HttpResponseRedirect(reverse('shib_complete'))