1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-01 15:23:05 +00:00

multi tenancy adfs login (#5330)

* multi tenancy adfs login

* custom saml login url

* improve code

* fix code

* optimize code
This commit is contained in:
mrwangjianhui 2022-12-26 09:56:51 +08:00 committed by GitHub
parent 3faa4acb8e
commit 4b82c58b0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 952 additions and 4 deletions

View File

@ -0,0 +1,50 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Row, Col, Label, Button } from 'reactstrap';
import { gettext } from '../../utils/constants';
const propTypes = {
postFile: PropTypes.func.isRequired,
displayName: PropTypes.string.isRequired,
};
class OrgSamlConfigPostFile extends Component {
constructor(props) {
super(props);
this.fileInput = React.createRef();
}
uploadFile = () => {
if (!this.fileInput.current.files.length) {
return;
}
const file = this.fileInput.current.files[0];
this.props.postFile(file);
}
openFileInput = () => {
this.fileInput.current.click();
}
render() {
const { displayName } = this.props;
return (
<Fragment>
<Row className="my-4">
<Col md="3">
<Label className="web-setting-label">{displayName}</Label>
</Col>
<Col md="5">
<Button color="secondary" onClick={this.openFileInput}>{gettext('Upload')}</Button>
<input className="d-none" type="file" onChange={this.uploadFile} ref={this.fileInput} />
</Col>
</Row>
</Fragment>
);
}
}
OrgSamlConfigPostFile.propTypes = propTypes;
export default OrgSamlConfigPostFile;

View File

@ -33,6 +33,7 @@ import OrgLogs from './org-logs';
import OrgLogsFileAudit from './org-logs-file-audit';
import OrgLogsFileUpdate from './org-logs-file-update';
import OrgLogsPermAudit from './org-logs-perm-audit';
import OrgSAMLConfig from './org-saml-config';
import '../../css/layout.css';
import '../../css/toolbar.css';
@ -114,6 +115,7 @@ class Org extends React.Component {
<OrgLogsFileUpdate path='file-update' />
<OrgLogsPermAudit path='perm-audit' />
</OrgLogs>
<OrgSAMLConfig path={siteRoot + 'org/samlconfig/'}/>
</Router>
</div>
</div>

View File

@ -0,0 +1,40 @@
import React, { Component, Fragment } from 'react';
import { Input, Row, Col, Label } from 'reactstrap';
import PropTypes from 'prop-types';
const propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
changeValue: PropTypes.func.isRequired,
displayName: PropTypes.string.isRequired,
};
class OrgSamlConfigInput extends Component {
constructor(props) {
super(props);
}
inputValue = (e) => {
this.props.changeValue(e);
}
render() {
const { value, displayName } = this.props;
return (
<Fragment>
<Row className="my-4">
<Col md="3">
<Label className="web-setting-label">{displayName}</Label>
</Col>
<Col md="5">
<Input innerRef={input => {this.newInput = input;}} value={value} onChange={this.inputValue}/>
</Col>
</Row>
</Fragment>
);
}
}
OrgSamlConfigInput.propTypes = propTypes;
export default OrgSamlConfigInput;

View File

@ -0,0 +1,280 @@
import React, { Fragment, Component } from 'react';
import { Row, Col, Label, Button } from 'reactstrap';
import MainPanelTopbar from './main-panel-topbar';
import toaster from '../../components/toast';
import Loading from '../../components/loading';
import { gettext, orgID, serviceURL } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import Section from './section';
import InputItem from './input-item';
import FileItem from './file-item';
class OrgSAMLConfig extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
samlConfigID: '',
newUrlPrefix: '',
orgUrlPrefix: '',
metadataUrl: '',
singleSignOnService: '',
singleLogoutService: '',
validDays: '',
isBtnsShown: false,
};
}
toggleBtns = () => {
this.setState({isBtnsShown: !this.state.isBtnsShown});
}
hideBtns = () => {
if (!this.state.isBtnsShown) return;
if (this.state.newUrlPrefix !== this.state.orgUrlPrefix) {
this.setState({newUrlPrefix: this.state.orgUrlPrefix});
}
this.toggleBtns();
}
onSubmit = () => {
const newUrlPrefix = this.state.newUrlPrefix.trim();
if (newUrlPrefix !== this.state.orgUrlPrefix) {
this.updateUrlPrefix(newUrlPrefix);
}
this.toggleBtns();
}
inputOrgUrlPrefix = (e) => {
this.setState({newUrlPrefix: e.target.value});
}
inputMetadataUrl = (e) => {
this.setState({metadataUrl: e.target.value});
}
inputSingleSignOnService = (e) => {
this.setState({singleSignOnService: e.target.value});
}
inputSingleLogoutService = (e) => {
this.setState({singleLogoutService: e.target.value});
}
inputValidDays = (e) => {
this.setState({validDays: e.target.value});
}
componentDidMount() {
seafileAPI.orgAdminGetUrlPrefix(orgID).then((res) => {
this.setState({
newUrlPrefix: res.data.org_url_prefix,
orgUrlPrefix: res.data.org_url_prefix,
});
seafileAPI.orgAdminGetSamlConfig(orgID).then((res) => {
this.setState({
loading: false,
samlConfigID: res.data.saml_config.id || '',
metadataUrl: res.data.saml_config.metadata_url || '',
singleSignOnService: res.data.saml_config.single_sign_on_service || '',
singleLogoutService: res.data.saml_config.single_logout_service || '',
validDays: res.data.saml_config.valid_days || '',
});
}).catch(error => {
this.setState({
loading: false,
errorMsg: Utils.getErrorMsg(error, true),
});
});
}).catch(error => {
this.setState({
loading: false,
errorMsg: Utils.getErrorMsg(error, true),
});
});
}
updateUrlPrefix = (newUrlPrefix) => {
seafileAPI.orgAdminUpdateUrlPrefix(orgID, newUrlPrefix).then((res) => {
this.setState({
newUrlPrefix: res.data.org_url_prefix,
orgUrlPrefix: res.data.org_url_prefix,
});
toaster.success(gettext('Success'));
}).catch((error) => {
this.setState({newUrlPrefix: this.state.orgUrlPrefix});
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
postIdpCertificate = (file) => {
seafileAPI.orgAdminUploadIdpCertificate(orgID, file).then(() => {
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
postIdpMetadataXml = (file) => {
seafileAPI.orgAdminUploadIdpMetadataXml(orgID, file).then(() => {
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
addSamlConfig = () => {
const { metadataUrl, singleSignOnService, singleLogoutService, validDays } = this.state;
seafileAPI.orgAdminAddSamlConfig(orgID, metadataUrl, singleSignOnService, singleLogoutService, validDays).then((res) => {
this.setState({
samlConfigID: res.data.saml_config.id,
metadataUrl: res.data.saml_config.metadata_url,
singleSignOnService: res.data.saml_config.single_sign_on_service,
singleLogoutService: res.data.saml_config.single_logout_service,
validDays: res.data.saml_config.valid_days,
});
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
updateSamlConfig = () => {
const { metadataUrl, singleSignOnService, singleLogoutService, validDays } = this.state;
seafileAPI.orgAdminUpdateSamlConfig(orgID, metadataUrl, singleSignOnService, singleLogoutService, validDays).then((res) => {
this.setState({
samlConfigID: res.data.saml_config.id,
metadataUrl: res.data.saml_config.metadata_url,
singleSignOnService: res.data.saml_config.single_sign_on_service,
singleLogoutService: res.data.saml_config.single_logout_service,
validDays: res.data.saml_config.valid_days,
});
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
deleteSamlConfig = () => {
seafileAPI.orgAdminDeleteSamlConfig(orgID).then(() => {
this.setState({
samlConfigID: '',
metadataUrl: '',
singleSignOnService: '',
singleLogoutService: '',
validDays: '',
});
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
render() {
const { loading, errorMsg, samlConfigID, newUrlPrefix, metadataUrl, singleSignOnService, singleLogoutService, validDays, isBtnsShown } = this.state;
return (
<Fragment>
<MainPanelTopbar />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<div className="cur-view-path">
<h3 className="sf-heading">{gettext('SAML config')}</h3>
</div>
<div className="cur-view-content container mw-100">
{loading && <Loading />}
{errorMsg && <p className="error text-center mt-4">{errorMsg}</p>}
{(!loading && !errorMsg) &&
<Fragment>
<Section headingText={gettext('Custom Login URL')}>
<Fragment>
<Row className="my-4">
<Col md="3">
<Label className="web-setting-label">{gettext('Your custom login URL')}</Label>
</Col>
<Col md="5">
{`${serviceURL}/org/custom/`}<input innerRef={input => {this.newInput = input;}} value={newUrlPrefix} onChange={this.inputOrgUrlPrefix} onFocus={this.toggleBtns} onBlur={this.hideBtns}></input>
<p className="small text-secondary mt-1">
{gettext('The custom part of the URL should be 6 to 20 characters, and can only contain alphanumeric characters and hyphens.')}
</p>
</Col>
{isBtnsShown &&
<Col md="4">
<Button className="sf2-icon-tick web-setting-icon-btn web-setting-icon-btn-submit" onMouseDown={this.onSubmit} title={gettext('Submit')}></Button>
<Button className="ml-1 sf2-icon-x2 web-setting-icon-btn web-setting-icon-btn-cancel" title={gettext('Cancel')}></Button>
</Col>
}
</Row>
</Fragment>
</Section>
<Section headingText={gettext('Manage SAML Config')}>
<Fragment>
<InputItem
value={metadataUrl}
changeValue={this.inputMetadataUrl}
displayName={gettext('App Federation Metadata URL')}
/>
<InputItem
value={singleSignOnService}
changeValue={this.inputSingleSignOnService}
displayName={gettext('Login URL')}
/>
<InputItem
value={singleLogoutService}
changeValue={this.inputSingleLogoutService}
displayName={gettext('Logout URL')}
/>
<InputItem
value={validDays}
changeValue={this.inputValidDays}
displayName={gettext('Valid Days (how long is our metadata valid)')}
/>
<Row className="my-4">
{samlConfigID ?
<Fragment>
<Col md="1">
<Button color="secondary" onClick={this.updateSamlConfig}>{gettext('Update')}</Button>
</Col>
<Col md="1">
<Button color="primary" onClick={this.deleteSamlConfig}>{gettext('Delete')}</Button>
</Col>
</Fragment> :
<Col md="1">
<Button color="secondary" onClick={this.addSamlConfig}>{gettext('Save')}</Button>
</Col>}
</Row>
</Fragment>
</Section>
<Section headingText={gettext('Upload Idp Files')}>
<Fragment>
<FileItem
postFile={this.postIdpCertificate}
displayName={gettext('IdP Certificate')}
/>
<FileItem
postFile={this.postIdpMetadataXml}
displayName={gettext('Federation Metadata XML')}
/>
</Fragment>
</Section>
</Fragment>
}
</div>
</div>
</div>
</Fragment>
);
}
}
export default OrgSAMLConfig;

View File

@ -0,0 +1,28 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
const propTypes = {
headingText: PropTypes.string.isRequired,
children: PropTypes.object.isRequired
};
class Section extends Component {
constructor(props) {
super(props);
}
render() {
const { headingText, children} = this.props;
return (
<div className="mb-4">
<h4 className="border-bottom font-weight-normal mb-2 pb-1">{headingText}</h4>
{children}
</div>
);
}
}
Section.propTypes = propTypes;
export default Section;

View File

@ -86,6 +86,12 @@ class SidePanel extends React.Component {
<span className="nav-text">{gettext('Logs')}</span>
</Link>
</li>
<li className="nav-item">
<Link className={`nav-link ellipsis ${this.getActiveClass('SAML config')}`} to={siteRoot + 'org/samlconfig/'} onClick={() => this.tabItemClick('SAML config')} >
<span className="sf2-icon-cog2"></span>
<span className="nav-text">{gettext('SAML config')}</span>
</Link>
</li>
</ul>
</div>
</div>

View File

@ -147,6 +147,14 @@ class Saml2Backend(ModelBackend):
if create_unknown_user:
activate_after_creation = getattr(settings, 'SAML_ACTIVATE_USER_AFTER_CREATION', True)
user = User.objects.create_user(email=username, is_active=activate_after_creation)
# create org user
url_prefix = kwargs.get('url_prefix', None)
if url_prefix:
org = ccnet_api.get_org_by_url_prefix(url_prefix)
if org:
org_id = org.org_id
ccnet_api.add_org_user(org_id, username, 0)
if not activate_after_creation:
notify_admins_on_activate_request(username)
elif settings.NOTIFY_ADMIN_AFTER_REGISTRATION:

10
seahub/adfs_auth/urls.py Normal file
View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
from django.conf.urls import url, include
from seahub.adfs_auth.views import assertion_consumer_service, org_multi_adfs
urlpatterns = [
url(r'^$', org_multi_adfs, name="org_multi_adfs"),
url(r'^saml2/acs/$', assertion_consumer_service, name='org_saml2_acs'),
url(r'^saml2/', include('djangosaml2.urls')),
]

118
seahub/adfs_auth/utils.py Normal file
View File

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
import os
import re
import logging
import saml2
from saml2 import saml
from saml2.config import SPConfig
from django.utils.translation import gettext as _
from seaserv import ccnet_api
from seahub.utils import render_error
from seahub.organizations.models import OrgSAMLConfig
try:
from seahub.settings import ENABLE_MULTI_ADFS, SP_SERVICE_URL, ATTRIBUTE_MAP_DIR, CERTS_DIR, XMLSEC_BINARY
except ImportError:
ENABLE_MULTI_ADFS = False
SP_SERVICE_URL = ''
ATTRIBUTE_MAP_DIR = ''
CERTS_DIR = ''
XMLSEC_BINARY = ''
logger = logging.getLogger(__name__)
def settings_check(func):
def _decorated(request):
error = False
if not ENABLE_MULTI_ADFS:
logger.error('Feature not enabled.')
error = True
else:
if not SP_SERVICE_URL or not ATTRIBUTE_MAP_DIR or not CERTS_DIR or not XMLSEC_BINARY:
logger.error('ADFS login relevant settings invalid.')
error = True
if error:
return render_error(request, _('Error, please contact administrator.'))
return func(request)
return _decorated
@settings_check
def config_settings_loader(request):
# get url_prefix
url_prefix = None
reg = re.search(r'org/custom/([a-z_0-9-]+)', request.path)
if reg:
url_prefix = reg.group(1)
# get org_id
org = ccnet_api.get_org_by_url_prefix(url_prefix)
if not org:
return render_error(request, 'Failed to get org %s ' % url_prefix)
org_id = org.org_id
# get org saml_config
org_saml_config = OrgSAMLConfig.objects.get_config_by_org_id(org_id)
if not org_saml_config:
return render_error(request, 'Failed to get org %s saml_config' % org_id)
metadata_url = org_saml_config.metadata_url
single_sign_on_service = org_saml_config.single_sign_on_service
single_logout_service = org_saml_config.single_logout_service
valid_days = int(org_saml_config.valid_days)
# get org_sp_service_url
org_sp_service_url = SP_SERVICE_URL.rstrip('/') + '/' + url_prefix
# generate org certs dir
org_certs_dir = os.path.join(CERTS_DIR, str(org_id))
# generate org saml_config
saml_config = {
'entityid': org_sp_service_url + '/saml2/metadata/',
'attribute_map_dir': ATTRIBUTE_MAP_DIR,
'xmlsec_binary': XMLSEC_BINARY,
'allow_unknown_attributes': True,
'service': {
'sp': {
'allow_unsolicited': True,
'want_response_signed': False,
'name_id_format': saml.NAMEID_FORMAT_EMAILADDRESS,
'endpoints': {
'assertion_consumer_service': [(org_sp_service_url + '/saml2/acs/', saml2.BINDING_HTTP_POST)],
'single_logout_service': [
(org_sp_service_url + '/saml2/ls/', saml2.BINDING_HTTP_REDIRECT),
(org_sp_service_url + '/saml2/ls/post', saml2.BINDING_HTTP_POST),
],
},
'required_attributes': ["uid"],
'idp': {
metadata_url: {
'single_sign_on_service': {
saml2.BINDING_HTTP_REDIRECT: single_sign_on_service,
},
'single_logout_service': {
saml2.BINDING_HTTP_REDIRECT: single_logout_service,
},
},
},
},
},
'metadata': {
'local': [os.path.join(org_certs_dir, 'idp_federation_metadata.xml')],
},
'debug': 1,
'key_file': '',
'cert_file': os.path.join(org_certs_dir, 'idp.crt'),
'encryption_keypairs': [{
'key_file': os.path.join(org_certs_dir, 'sp.key'),
'cert_file': os.path.join(org_certs_dir, 'sp.crt'),
}],
'valid_for': valid_days * 24, # how long is our metadata valid, unit is hour
}
conf = SPConfig()
conf.load(saml_config)
return conf

View File

@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import re
import logging
from django.conf import settings
@ -105,10 +106,17 @@ def assertion_consumer_service(request,
if callable(create_unknown_user):
create_unknown_user = create_unknown_user()
# get url_prefix
url_prefix = None
reg = re.search(r'org/custom/([a-z_0-9-]+)', request.path)
if reg:
url_prefix = reg.group(1)
logger.debug('Trying to authenticate the user')
user = auth.authenticate(session_info=session_info,
attribute_mapping=attribute_mapping,
create_unknown_user=create_unknown_user)
create_unknown_user=create_unknown_user,
url_prefix=url_prefix)
if user is None:
logger.error('The user is None')
return HttpResponseForbidden("Permission denied")
@ -175,3 +183,8 @@ def auth_complete(request):
update_sudo_mode_ts(request)
return resp
def org_multi_adfs(request):
if getattr(settings, 'ENABLE_MULTI_ADFS', False):
return HttpResponseRedirect(request.path.rstrip('/') + '/saml2/login/')

View File

@ -0,0 +1,256 @@
# -*- coding: utf-8 -*-
import os
import re
import logging
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.authentication import SessionAuthentication
from django.utils.translation import gettext as _
from seaserv import ccnet_api
from seahub.api2.permissions import IsProVersion, IsOrgAdminUser
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.utils import api_error
from seahub.organizations.utils import get_ccnet_db_name, update_org_url_prefix
from seahub.organizations.models import OrgSAMLConfig
try:
from seahub.settings import CERTS_DIR
except ImportError:
CERTS_DIR = ''
logger = logging.getLogger(__name__)
class OrgUploadIdPCertificateView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
throttle_classes = (UserRateThrottle,)
permission_classes = (IsProVersion, IsOrgAdminUser)
def post(self, request, org_id):
# argument check
idp_certificate = request.FILES.get('idp_certificate', None)
if not idp_certificate:
error_msg = 'idp_certificate not found.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if idp_certificate.name != 'idp.crt':
error_msg = 'idp_certificate invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if not CERTS_DIR:
error_msg = 'CERTS_DIR invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
if not ccnet_api.get_org_by_id(int(org_id)):
error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
org_certs_dir = os.path.join(CERTS_DIR, str(org_id))
try:
if not os.path.exists(org_certs_dir):
os.makedirs(org_certs_dir)
cert_file_path = os.path.join(org_certs_dir, 'idp.crt')
with open(cert_file_path, 'wb') as fd:
fd.write(idp_certificate.read())
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})
class OrgUploadIdPMetadataXMLView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
throttle_classes = (UserRateThrottle,)
permission_classes = (IsProVersion, IsOrgAdminUser)
def post(self, request, org_id):
# argument check
idp_metadata_xml = request.FILES.get('idp_metadata_xml', None)
if not idp_metadata_xml:
error_msg = 'idp_metadata_xml not found.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if idp_metadata_xml.name != 'idp_federation_metadata.xml':
error_msg = 'idp_metadata_xml invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if not CERTS_DIR:
error_msg = 'CERTS_DIR invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
if not ccnet_api.get_org_by_id(int(org_id)):
error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
org_certs_dir = os.path.join(CERTS_DIR, str(org_id))
try:
if not os.path.exists(org_certs_dir):
os.makedirs(org_certs_dir)
cert_file_path = os.path.join(org_certs_dir, 'idp_federation_metadata.xml')
with open(cert_file_path, 'wb') as fd:
fd.write(idp_metadata_xml.read())
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})
class OrgSAMLConfigView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
throttle_classes = (UserRateThrottle,)
permission_classes = (IsProVersion, IsOrgAdminUser)
def get(self, request, org_id):
# resource check
org_id = int(org_id)
if not ccnet_api.get_org_by_id(org_id):
error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# get config
org_saml_config = OrgSAMLConfig.objects.get_config_by_org_id(org_id)
if not org_saml_config:
return Response({'saml_config': {}})
return Response({'saml_config': org_saml_config.to_dict()})
def post(self, request, org_id):
# argument check
metadata_url = request.data.get('metadata_url', None)
single_sign_on_service = request.data.get('single_sign_on_service', None)
single_logout_service = request.data.get('single_logout_service', None)
valid_days = request.data.get('valid_days', None)
if not metadata_url or not single_sign_on_service or not single_logout_service or not valid_days:
return api_error(status.HTTP_400_BAD_REQUEST, 'argument invalid.')
# resource check
org_id = int(org_id)
if not ccnet_api.get_org_by_id(org_id):
error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# add an org saml config
try:
saml_comfig = OrgSAMLConfig.objects.add_or_update_saml_config(
org_id, metadata_url, single_sign_on_service, single_logout_service, valid_days
)
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({'saml_config': saml_comfig.to_dict()})
def put(self, request, org_id):
# argument check
metadata_url = request.data.get('metadata_url', None)
single_sign_on_service = request.data.get('single_sign_on_service', None)
single_logout_service = request.data.get('single_logout_service', None)
valid_days = request.data.get('valid_days', None)
if not metadata_url and not single_sign_on_service and not single_logout_service and not valid_days:
return api_error(status.HTTP_400_BAD_REQUEST, 'argument invalid.')
# resource check
org_id = int(org_id)
if not ccnet_api.get_org_by_id(org_id):
error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# update config
try:
saml_comfig = OrgSAMLConfig.objects.add_or_update_saml_config(
org_id, metadata_url, single_sign_on_service, single_logout_service, valid_days
)
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({'saml_config': saml_comfig.to_dict()})
def delete(self, request, org_id):
# resource check
org_id = int(org_id)
if not ccnet_api.get_org_by_id(org_id):
error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# delete saml config
try:
OrgSAMLConfig.objects.filter(org_id=org_id).delete()
except OrgSAMLConfig.DoesNotExist:
return Response({'success': True})
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})
class OrgUrlPrefixView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
throttle_classes = (UserRateThrottle,)
permission_classes = (IsProVersion, IsOrgAdminUser)
def get(self, request, org_id):
# resource check
org_id = int(org_id)
org = ccnet_api.get_org_by_id(org_id)
if not org:
error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
org_url_prefix = org.url_prefix
return Response({'org_url_prefix': org_url_prefix})
def put(self, request, org_id):
# argument check
org_url_prefix = request.data.get('org_url_prefix', None)
if not org_url_prefix:
error_msg = 'org_url_prefix invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
reg = re.match(r'^[a-z0-9-]{6,20}$', org_url_prefix)
if not reg:
error_msg = _('org_url_prefix should be 6 to 20 characters, and can only contain alphanumeric characters and hyphens.')
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if ccnet_api.get_org_by_url_prefix(org_url_prefix) is not None:
error_msg = 'url_prefix %s is duplicated.' % org_url_prefix
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# get ccnet db_name
db_name, error_msg = get_ccnet_db_name()
if error_msg:
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
org_id = int(org_id)
if not ccnet_api.get_org_by_id(org_id):
error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
try:
update_org_url_prefix(db_name, org_id, org_url_prefix)
except Exception as e:
logger.error(e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
return Response({'org_url_prefix': org_url_prefix})

View File

@ -25,6 +25,9 @@ from .api.admin.devices import OrgAdminDevices, OrgAdminDevicesErrors
from .api.admin.statistics import OrgFileOperationsView, OrgTotalStorageView, \
OrgActiveUsersView, OrgSystemTrafficView, OrgUserTrafficView, \
OrgUserTrafficExcelView, OrgUserStorageExcelView
from .api.admin.saml_config import OrgUploadIdPCertificateView, OrgUploadIdPMetadataXMLView, OrgSAMLConfigView, \
OrgUrlPrefixView
urlpatterns = [
url(r'^(?P<org_id>\d+)/admin/statistics/file-operations/$',
@ -49,6 +52,19 @@ urlpatterns = [
OrgUserStorageExcelView.as_view(),
name='api-v2.1-org-admin-statistics-user-storage-excel'),
url(r'^(?P<org_id>\d+)/admin/saml-idp-certificate/$',
OrgUploadIdPCertificateView.as_view(),
name='api-v2.1-org-admin-saml-idp-certificate'),
url(r'^(?P<org_id>\d+)/admin/saml-idp-metadata-xml/$',
OrgUploadIdPMetadataXMLView.as_view(),
name='api-v2.1-org-admin-saml-idp-metadata-xml'),
url(r'^(?P<org_id>\d+)/admin/saml-config/$',
OrgSAMLConfigView.as_view(),
name='api-v2.1-org-admin-saml-config'),
url(r'^(?P<org_id>\d+)/admin/url-prefix/$',
OrgUrlPrefixView.as_view(),
name='api-v2.1-org-admin-url-prefix'),
url(r'^(?P<org_id>\d+)/admin/devices/$', OrgAdminDevices.as_view(), name='api-v2.1-org-admin-devices'),
url(r'^(?P<org_id>\d+)/admin/devices-errors/$', OrgAdminDevicesErrors.as_view(), name='api-v2.1-org-admin-devices-errors'),

View File

@ -0,0 +1,27 @@
# Generated by Django 3.2.14 on 2022-12-08 12:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('organizations', '0003_auto_20190116_0323'),
]
operations = [
migrations.CreateModel(
name='OrgSAMLConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('org_id', models.IntegerField(unique=True)),
('metadata_url', models.TextField()),
('single_sign_on_service', models.TextField()),
('single_logout_service', models.TextField()),
('valid_days', models.IntegerField()),
],
options={
'db_table': 'org_saml_config',
},
),
]

View File

@ -25,6 +25,7 @@ class OrgMemberQuotaManager(models.Manager):
q.save(using=self._db)
return q
class OrgMemberQuota(models.Model):
org_id = models.IntegerField(db_index=True)
quota = models.IntegerField()
@ -50,7 +51,7 @@ class OrgSettingsManager(models.Manager):
if role in get_available_roles():
return role
else:
logger.warn('Role %s is not valid' % role)
logger.warning('Role %s is not valid' % role)
return DEFAULT_ORG
def add_or_update(self, org, role=None):
@ -64,7 +65,7 @@ class OrgSettingsManager(models.Manager):
if role in get_available_roles():
settings.role = role
else:
logger.warn('Role %s is not valid' % role)
logger.warning('Role %s is not valid' % role)
settings.save(using=self._db)
return settings
@ -75,3 +76,57 @@ class OrgSettings(models.Model):
role = models.CharField(max_length=100, null=True, blank=True)
objects = OrgSettingsManager()
class OrgSAMLConfigManager(models.Manager):
def add_or_update_saml_config(
self, org_id, metadata_url, single_sign_on_service,
single_logout_service, valid_days
):
try:
saml_config = self.get(org_id=org_id)
except OrgSAMLConfig.DoesNotExist:
saml_config = self.model(org_id=org_id)
if metadata_url:
saml_config.metadata_url = metadata_url
if single_sign_on_service:
saml_config.single_sign_on_service = single_sign_on_service
if single_logout_service:
saml_config.single_logout_service = single_logout_service
if valid_days:
saml_config.valid_days = valid_days
saml_config.save(using=self._db)
return saml_config
def get_config_by_org_id(self, org_id):
try:
config = self.get(org_id=org_id)
return config
except OrgSAMLConfig.DoesNotExist:
return None
class OrgSAMLConfig(models.Model):
org_id = models.IntegerField(unique=True)
metadata_url = models.TextField()
single_sign_on_service = models.TextField()
single_logout_service = models.TextField()
valid_days = models.IntegerField()
objects = OrgSAMLConfigManager()
class Meta:
db_table = 'org_saml_config'
def to_dict(self):
return {
'id': self.pk,
'org_id': self.org_id,
'metadata_url': self.metadata_url,
'single_sign_on_service': self.single_sign_on_service,
'single_logout_service': self.single_logout_service,
'valid_days': self.valid_days,
}

View File

@ -38,4 +38,6 @@ urlpatterns = [
url(r'^departmentadmin/$', react_fake_view, name='org_department_admin'),
url(r'^departmentadmin/groups/(?P<group_id>\d+)/', react_fake_view, name='org_department_admin'),
url(r'^associate/(?P<token>.+)/$', org_associate, name='org_associate'),
url(r'^samlconfig/$', react_fake_view, name='saml_config'),
]

View File

@ -1,9 +1,37 @@
# Copyright (c) 2012-2016 Seafile Ltd.
import os
import configparser
from django.db import connection
from django.core.cache import cache
from django.urls import reverse
from seahub.utils import gen_token, get_service_url
def get_ccnet_db_name():
if 'CCNET_CONF_DIR' not in os.environ:
error_msg = 'Environment variable CCNET_CONF_DIR is not define.'
return None, error_msg
ccnet_conf_path = os.path.join(os.environ['CCNET_CONF_DIR'], 'ccnet.conf')
config = configparser.ConfigParser()
config.read(ccnet_conf_path)
if config.has_section('Database'):
db_name = config.get('Database', 'DB', fallback='ccnet')
else:
db_name = 'ccnet'
return db_name, None
def update_org_url_prefix(db_name, org_id, url_prefix):
sql = """UPDATE %s.Organization SET url_prefix=%%s WHERE org_id=%%s""" % db_name
with connection.cursor() as cursor:
cursor.execute(sql, (url_prefix, org_id))
def get_or_create_invitation_link(org_id):
"""Invitation link for an org. Users will be redirected to WeChat QR page.
Mainly used in docs.seafile.com.

View File

@ -295,6 +295,8 @@ ENABLE_CAS = False
ENABLE_ADFS_LOGIN = False
ENABLE_MULTI_ADFS = False
ENABLE_OAUTH = False
ENABLE_WATERMARK = False
@ -948,7 +950,7 @@ if ENABLE_OAUTH or ENABLE_WORK_WEIXIN or ENABLE_WEIXIN or ENABLE_DINGTALK:
if ENABLE_CAS:
AUTHENTICATION_BACKENDS += ('seahub.django_cas_ng.backends.CASBackend',)
if ENABLE_ADFS_LOGIN:
if ENABLE_ADFS_LOGIN or ENABLE_MULTI_ADFS:
AUTHENTICATION_BACKENDS += ('seahub.adfs_auth.backends.Saml2Backend',)
#####################

View File

@ -868,6 +868,13 @@ if HAS_OFFICE_CONVERTER:
url(r'^office-convert/status/$', office_convert_query_status, name='office_convert_query_status'),
]
if getattr(settings, 'ENABLE_MULTI_ADFS', False):
from seahub.adfs_auth.views import auth_complete
urlpatterns += [
url(r'^org/custom/[a-z_0-9-]+/', include(('seahub.adfs_auth.urls', 'adfs_auth'), namespace='adfs_auth')),
url(r'^saml2/complete/$', auth_complete, name='org_saml2_complete'),
]
if getattr(settings, 'ENABLE_ADFS_LOGIN', False):
from seahub.adfs_auth.views import assertion_consumer_service, \
auth_complete