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:
parent
3faa4acb8e
commit
4b82c58b0f
50
frontend/src/pages/org-admin/file-item.js
Normal file
50
frontend/src/pages/org-admin/file-item.js
Normal 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;
|
@ -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>
|
||||
|
40
frontend/src/pages/org-admin/input-item.js
Normal file
40
frontend/src/pages/org-admin/input-item.js
Normal 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;
|
280
frontend/src/pages/org-admin/org-saml-config.js
Normal file
280
frontend/src/pages/org-admin/org-saml-config.js
Normal 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;
|
28
frontend/src/pages/org-admin/section.js
Normal file
28
frontend/src/pages/org-admin/section.js
Normal 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;
|
@ -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>
|
||||
|
@ -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
10
seahub/adfs_auth/urls.py
Normal 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
118
seahub/adfs_auth/utils.py
Normal 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
|
@ -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/')
|
||||
|
256
seahub/organizations/api/admin/saml_config.py
Normal file
256
seahub/organizations/api/admin/saml_config.py
Normal 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})
|
@ -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'),
|
||||
|
||||
|
27
seahub/organizations/migrations/0004_orgsamlconfig.py
Normal file
27
seahub/organizations/migrations/0004_orgsamlconfig.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
@ -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,
|
||||
}
|
||||
|
@ -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'),
|
||||
]
|
||||
|
@ -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.
|
||||
|
@ -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',)
|
||||
|
||||
#####################
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user