1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-16 23:29: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:
mrwangjianhui
2022-12-26 09:56:51 +08:00
committed by GitHub
parent 3faa4acb8e
commit 4b82c58b0f
18 changed files with 952 additions and 4 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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