diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 915184a2df..04e915b3b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/frontend/src/components/dialog/share-dialog.js b/frontend/src/components/dialog/share-dialog.js index 6a909e65b3..f2b9f526d3 100644 --- a/frontend/src/components/dialog/share-dialog.js +++ b/frontend/src/components/dialog/share-dialog.js @@ -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 && - - - {gettext('Share to user')} - - - - - {gettext('Share to group')} - - - {isPro && !isCustomPermission && ( + { canShareRepo && ( + + + {gettext('Share to user')} + + + )} + { canShareRepo && ( + + + {gettext('Share to group')} + + + )} + {isPro && !isCustomPermission && canShareRepo && ( {gettext('Custom sharing permissions')} @@ -307,7 +311,7 @@ class ShareDialog extends React.Component { return (
{additionalShareDialogNote.title}
-
{additionalShareDialogNote.content}
+

{additionalShareDialogNote.content}

); } @@ -319,8 +323,8 @@ class ShareDialog extends React.Component { return (
- - {gettext('Share')} {itemName} + +
{gettext('Share')} {itemName}
{this.renderExternalShareMessage()}
diff --git a/frontend/src/components/main-side-nav.js b/frontend/src/components/main-side-nav.js index e38eeca985..3905bb2f6c 100644 --- a/frontend/src/components/main-side-nav.js +++ b/frontend/src/components/main-side-nav.js @@ -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 && (
  • this.tabItemClick(e, 'share-admin-libs')}> @@ -174,12 +174,14 @@ class MainSideNav extends React.Component {
  • )} -
  • - this.tabItemClick(e, 'share-admin-folders')}> - - {gettext('Folders')} - -
  • + {canShareRepo && ( +
  • + this.tabItemClick(e, 'share-admin-folders')}> + + {gettext('Folders')} + +
  • + )} {linksNavItem} ); diff --git a/frontend/src/pages/org-admin/statistic/traffic-table-body.js b/frontend/src/pages/org-admin/statistic/traffic-table-body.js index 3a80b4cba5..75ba551aaa 100644 --- a/frontend/src/pages/org-admin/statistic/traffic-table-body.js +++ b/frontend/src/pages/org-admin/statistic/traffic-table-body.js @@ -16,7 +16,7 @@ class TrafficTableBody extends React.Component { case 'user': if (userTrafficItem.name) { return ( - {userTrafficItem.name} + {userTrafficItem.name} ); } return({'--'}); diff --git a/frontend/src/pages/sys-admin/statistic/traffic-table-body.js b/frontend/src/pages/sys-admin/statistic/traffic-table-body.js index 3a80b4cba5..8ef7421c21 100644 --- a/frontend/src/pages/sys-admin/statistic/traffic-table-body.js +++ b/frontend/src/pages/sys-admin/statistic/traffic-table-body.js @@ -16,7 +16,7 @@ class TrafficTableBody extends React.Component { case 'user': if (userTrafficItem.name) { return ( - {userTrafficItem.name} + {userTrafficItem.name} ); } return({'--'}); diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index f74c515c53..523a36479f 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -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; diff --git a/scripts/seafile-monitor.sh b/scripts/seafile-monitor.sh index 0c7453be1c..b4b0bd6fb2 100755 --- a/scripts/seafile-monitor.sh +++ b/scripts/seafile-monitor.sh @@ -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 diff --git a/seahub/adfs_auth/backends.py b/seahub/adfs_auth/backends.py index b5f3c445aa..046c675c1e 100644 --- a/seahub/adfs_auth/backends.py +++ b/seahub/adfs_auth/backends.py @@ -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] diff --git a/seahub/api2/endpoints/admin/users.py b/seahub/api2/endpoints/admin/users.py index 7e269368bf..b705b79417 100644 --- a/seahub/api2/endpoints/admin/users.py +++ b/seahub/api2/endpoints/admin/users.py @@ -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}) diff --git a/seahub/api2/endpoints/custom_share_permissions.py b/seahub/api2/endpoints/custom_share_permissions.py index cf259fd401..66cff48d65 100644 --- a/seahub/api2/endpoints/custom_share_permissions.py +++ b/seahub/api2/endpoints/custom_share_permissions.py @@ -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 diff --git a/seahub/api2/endpoints/dir_shared_items.py b/seahub/api2/endpoints/dir_shared_items.py index dd85799e72..79bfbc863d 100644 --- a/seahub/api2/endpoints/dir_shared_items.py +++ b/seahub/api2/endpoints/dir_shared_items.py @@ -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) diff --git a/seahub/api2/endpoints/shared_folders.py b/seahub/api2/endpoints/shared_folders.py index 1216932c2e..8e84372f10 100644 --- a/seahub/api2/endpoints/shared_folders.py +++ b/seahub/api2/endpoints/shared_folders.py @@ -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 diff --git a/seahub/api2/endpoints/shared_repos.py b/seahub/api2/endpoints/shared_repos.py index d9fb9ec8f3..7b91c21ced 100644 --- a/seahub/api2/endpoints/shared_repos.py +++ b/seahub/api2/endpoints/shared_repos.py @@ -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: diff --git a/seahub/api2/endpoints/user.py b/seahub/api2/endpoints/user.py index ba97df9602..24bedba1fe 100644 --- a/seahub/api2/endpoints/user.py +++ b/seahub/api2/endpoints/user.py @@ -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}) diff --git a/seahub/auth/models.py b/seahub/auth/models.py index abd5edca27..9ae0026a61 100644 --- a/seahub/auth/models.py +++ b/seahub/auth/models.py @@ -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'] diff --git a/seahub/auth/views.py b/seahub/auth/views.py index 4a9d98757f..df7592e265 100644 --- a/seahub/auth/views.py +++ b/seahub/auth/views.py @@ -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(): diff --git a/seahub/base/accounts.py b/seahub/base/accounts.py index a7e5d8e2be..afdf06d0d5 100644 --- a/seahub/base/accounts.py +++ b/seahub/base/accounts.py @@ -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') diff --git a/seahub/base/migrations/0005_clientssotoken_alter_socialauthuser_extra_data.py b/seahub/base/migrations/0005_clientssotoken_alter_socialauthuser_extra_data.py new file mode 100644 index 0000000000..80ef61ff09 --- /dev/null +++ b/seahub/base/migrations/0005_clientssotoken_alter_socialauthuser_extra_data.py @@ -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), + ), + ] diff --git a/seahub/invitations/migrations/0007_alter_invitation_invite_type.py b/seahub/invitations/migrations/0007_alter_invitation_invite_type.py new file mode 100644 index 0000000000..02e3f4a4c7 --- /dev/null +++ b/seahub/invitations/migrations/0007_alter_invitation_invite_type.py @@ -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), + ), + ] diff --git a/seahub/organizations/migrations/0005_orgsamlconfig_dns_txt_orgsamlconfig_domain_verified_and_more.py b/seahub/organizations/migrations/0005_orgsamlconfig_dns_txt_orgsamlconfig_domain_verified_and_more.py new file mode 100644 index 0000000000..fd212bb92f --- /dev/null +++ b/seahub/organizations/migrations/0005_orgsamlconfig_dns_txt_orgsamlconfig_domain_verified_and_more.py @@ -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), + ), + ] diff --git a/seahub/role_permissions/settings.py b/seahub/role_permissions/settings.py index 57d846fff4..479ea6fd37 100644 --- a/seahub/role_permissions/settings.py +++ b/seahub/role_permissions/settings.py @@ -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, diff --git a/seahub/settings.py b/seahub/settings.py index 623554c964..63c7185f8b 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -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'] diff --git a/seahub/share/migrations/0003_alter_uploadlinkshare_expire_date.py b/seahub/share/migrations/0003_alter_uploadlinkshare_expire_date.py new file mode 100644 index 0000000000..1eeaf3e7a5 --- /dev/null +++ b/seahub/share/migrations/0003_alter_uploadlinkshare_expire_date.py @@ -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), + ), + ] diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index c69982a48f..a8e48691f8 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -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 %}, diff --git a/seahub/urls.py b/seahub/urls.py index fa2c932908..3c2f2a2da4 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -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 diff --git a/seahub/utils/db_api.py b/seahub/utils/db_api.py index 437a7df86e..d373b93a84 100644 --- a/seahub/utils/db_api.py +++ b/seahub/utils/db_api.py @@ -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) diff --git a/seahub/utils/http.py b/seahub/utils/http.py index 7a69bfcadb..7acf4e6c6c 100644 --- a/seahub/utils/http.py +++ b/seahub/utils/http.py @@ -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 diff --git a/seahub/wopi/utils.py b/seahub/wopi/utils.py index 08c2a52c6c..8e8e0aebb7 100644 --- a/seahub/wopi/utils.py +++ b/seahub/wopi/utils.py @@ -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 diff --git a/seahub/wopi/views.py b/seahub/wopi/views.py index 12c081ec6a..45292a9809 100644 --- a/seahub/wopi/views.py +++ b/seahub/wopi/views.py @@ -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 diff --git a/tests/seahub/role_permissions/test_utils.py b/tests/seahub/role_permissions/test_utils.py index a24bfd8d6b..4f0a03a780 100644 --- a/tests/seahub/role_permissions/test_utils.py +++ b/tests/seahub/role_permissions/test_utils.py @@ -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 diff --git a/tests/seahub/thirdpart/shibboleth/test_middleware.py b/tests/seahub/thirdpart/shibboleth/test_middleware.py index bb933beac3..279857674a 100644 --- a/tests/seahub/thirdpart/shibboleth/test_middleware.py +++ b/tests/seahub/thirdpart/shibboleth/test_middleware.py @@ -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' - diff --git a/thirdpart/shibboleth/app_settings.py b/thirdpart/shibboleth/app_settings.py index 57bed1f8ca..ecb1fbbbbc 100755 --- a/thirdpart/shibboleth/app_settings.py +++ b/thirdpart/shibboleth/app_settings.py @@ -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') - - diff --git a/thirdpart/shibboleth/backends.py b/thirdpart/shibboleth/backends.py index 70cd3d7792..da2c306cc4 100644 --- a/thirdpart/shibboleth/backends.py +++ b/thirdpart/shibboleth/backends.py @@ -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 diff --git a/thirdpart/shibboleth/middleware.py b/thirdpart/shibboleth/middleware.py index 8f272990bf..ea1e2a7d49 100755 --- a/thirdpart/shibboleth/middleware.py +++ b/thirdpart/shibboleth/middleware.py @@ -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'))