mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-12 20:25:49 +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 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>
|
||||||
|
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>
|
<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>
|
||||||
|
@ -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
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
|
# 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/')
|
||||||
|
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, \
|
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'),
|
||||||
|
|
||||||
|
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)
|
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,
|
||||||
|
}
|
||||||
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
@ -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.
|
||||||
|
@ -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',)
|
||||||
|
|
||||||
#####################
|
#####################
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user