1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-13 12:45:46 +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 OrgLogsFileAudit from './org-logs-file-audit';
import OrgLogsFileUpdate from './org-logs-file-update'; import OrgLogsFileUpdate from './org-logs-file-update';
import OrgLogsPermAudit from './org-logs-perm-audit'; import OrgLogsPermAudit from './org-logs-perm-audit';
import OrgSAMLConfig from './org-saml-config';
import '../../css/layout.css'; import '../../css/layout.css';
import '../../css/toolbar.css'; import '../../css/toolbar.css';
@ -114,6 +115,7 @@ class Org extends React.Component {
<OrgLogsFileUpdate path='file-update' /> <OrgLogsFileUpdate path='file-update' />
<OrgLogsPermAudit path='perm-audit' /> <OrgLogsPermAudit path='perm-audit' />
</OrgLogs> </OrgLogs>
<OrgSAMLConfig path={siteRoot + 'org/samlconfig/'}/>
</Router> </Router>
</div> </div>
</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> <span className="nav-text">{gettext('Logs')}</span>
</Link> </Link>
</li> </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> </ul>
</div> </div>
</div> </div>

View File

@ -147,6 +147,14 @@ class Saml2Backend(ModelBackend):
if create_unknown_user: if create_unknown_user:
activate_after_creation = getattr(settings, 'SAML_ACTIVATE_USER_AFTER_CREATION', True) activate_after_creation = getattr(settings, 'SAML_ACTIVATE_USER_AFTER_CREATION', True)
user = User.objects.create_user(email=username, is_active=activate_after_creation) 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: if not activate_after_creation:
notify_admins_on_activate_request(username) notify_admins_on_activate_request(username)
elif settings.NOTIFY_ADMIN_AFTER_REGISTRATION: 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import re
import logging import logging
from django.conf import settings from django.conf import settings
@ -105,10 +106,17 @@ def assertion_consumer_service(request,
if callable(create_unknown_user): if callable(create_unknown_user):
create_unknown_user = 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') logger.debug('Trying to authenticate the user')
user = auth.authenticate(session_info=session_info, user = auth.authenticate(session_info=session_info,
attribute_mapping=attribute_mapping, attribute_mapping=attribute_mapping,
create_unknown_user=create_unknown_user) create_unknown_user=create_unknown_user,
url_prefix=url_prefix)
if user is None: if user is None:
logger.error('The user is None') logger.error('The user is None')
return HttpResponseForbidden("Permission denied") return HttpResponseForbidden("Permission denied")
@ -175,3 +183,8 @@ def auth_complete(request):
update_sudo_mode_ts(request) update_sudo_mode_ts(request)
return resp 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, \ from .api.admin.statistics import OrgFileOperationsView, OrgTotalStorageView, \
OrgActiveUsersView, OrgSystemTrafficView, OrgUserTrafficView, \ OrgActiveUsersView, OrgSystemTrafficView, OrgUserTrafficView, \
OrgUserTrafficExcelView, OrgUserStorageExcelView OrgUserTrafficExcelView, OrgUserStorageExcelView
from .api.admin.saml_config import OrgUploadIdPCertificateView, OrgUploadIdPMetadataXMLView, OrgSAMLConfigView, \
OrgUrlPrefixView
urlpatterns = [ urlpatterns = [
url(r'^(?P<org_id>\d+)/admin/statistics/file-operations/$', url(r'^(?P<org_id>\d+)/admin/statistics/file-operations/$',
@ -49,6 +52,19 @@ urlpatterns = [
OrgUserStorageExcelView.as_view(), OrgUserStorageExcelView.as_view(),
name='api-v2.1-org-admin-statistics-user-storage-excel'), 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/$', 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'), 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) q.save(using=self._db)
return q return q
class OrgMemberQuota(models.Model): class OrgMemberQuota(models.Model):
org_id = models.IntegerField(db_index=True) org_id = models.IntegerField(db_index=True)
quota = models.IntegerField() quota = models.IntegerField()
@ -50,7 +51,7 @@ class OrgSettingsManager(models.Manager):
if role in get_available_roles(): if role in get_available_roles():
return role return role
else: else:
logger.warn('Role %s is not valid' % role) logger.warning('Role %s is not valid' % role)
return DEFAULT_ORG return DEFAULT_ORG
def add_or_update(self, org, role=None): def add_or_update(self, org, role=None):
@ -64,7 +65,7 @@ class OrgSettingsManager(models.Manager):
if role in get_available_roles(): if role in get_available_roles():
settings.role = role settings.role = role
else: else:
logger.warn('Role %s is not valid' % role) logger.warning('Role %s is not valid' % role)
settings.save(using=self._db) settings.save(using=self._db)
return settings return settings
@ -75,3 +76,57 @@ class OrgSettings(models.Model):
role = models.CharField(max_length=100, null=True, blank=True) role = models.CharField(max_length=100, null=True, blank=True)
objects = OrgSettingsManager() 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/$', react_fake_view, name='org_department_admin'),
url(r'^departmentadmin/groups/(?P<group_id>\d+)/', 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'^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. # Copyright (c) 2012-2016 Seafile Ltd.
import os
import configparser
from django.db import connection
from django.core.cache import cache from django.core.cache import cache
from django.urls import reverse from django.urls import reverse
from seahub.utils import gen_token, get_service_url 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): def get_or_create_invitation_link(org_id):
"""Invitation link for an org. Users will be redirected to WeChat QR page. """Invitation link for an org. Users will be redirected to WeChat QR page.
Mainly used in docs.seafile.com. Mainly used in docs.seafile.com.

View File

@ -295,6 +295,8 @@ ENABLE_CAS = False
ENABLE_ADFS_LOGIN = False ENABLE_ADFS_LOGIN = False
ENABLE_MULTI_ADFS = False
ENABLE_OAUTH = False ENABLE_OAUTH = False
ENABLE_WATERMARK = False ENABLE_WATERMARK = False
@ -948,7 +950,7 @@ if ENABLE_OAUTH or ENABLE_WORK_WEIXIN or ENABLE_WEIXIN or ENABLE_DINGTALK:
if ENABLE_CAS: if ENABLE_CAS:
AUTHENTICATION_BACKENDS += ('seahub.django_cas_ng.backends.CASBackend',) 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',) 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'), 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): if getattr(settings, 'ENABLE_ADFS_LOGIN', False):
from seahub.adfs_auth.views import assertion_consumer_service, \ from seahub.adfs_auth.views import assertion_consumer_service, \
auth_complete auth_complete