1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-25 06:33:48 +00:00

Custom org saml login domain (#5622)

* add domain verification api

* improve org saml config page

* improve code

* optimize code

* optimize code

* optimize code
This commit is contained in:
WJH
2023-10-24 12:45:13 +08:00
committed by GitHub
parent cdfc590546
commit d0029efc04
6 changed files with 275 additions and 92 deletions

View File

@@ -1,9 +1,11 @@
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import { Input, Row, Col, Label } from 'reactstrap'; import { Input, InputGroup, InputGroupAddon, Button, Row, Col, Label } from 'reactstrap';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
const propTypes = { const propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), value: PropTypes.string,
domainVerified: PropTypes.bool,
changeValue: PropTypes.func.isRequired, changeValue: PropTypes.func.isRequired,
displayName: PropTypes.string.isRequired, displayName: PropTypes.string.isRequired,
}; };
@@ -12,14 +14,45 @@ class OrgSamlConfigInput extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
isBtnsShown: false,
value: this.props.value,
};
} }
inputValue = (e) => { componentWillReceiveProps(nextProps) {
this.props.changeValue(e); this.setState({value: nextProps.value,});
}
toggleBtns = () => {
this.setState({isBtnsShown: !this.state.isBtnsShown});
};
hideBtns = () => {
if (!this.state.isBtnsShown) {
return;
}
if (this.props.value != this.state.value) {
this.setState({value: this.props.value});
}
this.toggleBtns();
};
onInputChange = (e) => {
this.setState({ value: e.target.value });
};
onSubmit = () => {
const value = this.state.value.trim();
if (value != this.props.value) {
this.props.changeValue(value);
}
this.toggleBtns();
}; };
render() { render() {
const { value, displayName } = this.props; const { isBtnsShown, value } = this.state;
const { displayName } = this.props;
return ( return (
<Fragment> <Fragment>
<Row className="my-4"> <Row className="my-4">
@@ -27,7 +60,22 @@ class OrgSamlConfigInput extends Component {
<Label className="web-setting-label">{displayName}</Label> <Label className="web-setting-label">{displayName}</Label>
</Col> </Col>
<Col md="5"> <Col md="5">
<Input innerRef={input => {this.newInput = input;}} value={value} onChange={this.inputValue}/> <InputGroup>
<Input type='text' value={value} onChange={this.onInputChange} onFocus={this.toggleBtns} onBlur={this.hideBtns}/>
{this.props.domainVerified &&
<InputGroupAddon addonType="append">
<Button color="success" className="border-0">{gettext('Verified')}</Button>
</InputGroupAddon>
}
</InputGroup>
</Col>
<Col md="4">
{isBtnsShown &&
<Fragment>
<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>
</Fragment>
}
</Col> </Col>
</Row> </Row>
</Fragment> </Fragment>

View File

@@ -1,5 +1,6 @@
import React, { Fragment, Component } from 'react'; import React, { Fragment, Component } from 'react';
import { Row, Col, Label, Button } from 'reactstrap'; import { Row, Col, Label, Button, Input, InputGroup, InputGroupAddon } from 'reactstrap';
import copy from 'copy-to-clipboard';
import MainPanelTopbar from './main-panel-topbar'; import MainPanelTopbar from './main-panel-topbar';
import toaster from '../../components/toast'; import toaster from '../../components/toast';
import Loading from '../../components/loading'; import Loading from '../../components/loading';
@@ -21,6 +22,9 @@ class OrgSAMLConfig extends Component {
newUrlPrefix: '', newUrlPrefix: '',
orgUrlPrefix: '', orgUrlPrefix: '',
metadataUrl: '', metadataUrl: '',
domain: '',
dns_txt: '',
domain_verified: false,
isBtnsShown: false, isBtnsShown: false,
}; };
} }
@@ -49,18 +53,6 @@ class OrgSAMLConfig extends Component {
this.setState({newUrlPrefix: e.target.value}); 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});
};
componentDidMount() { componentDidMount() {
seafileAPI.orgAdminGetUrlPrefix(orgID).then((res) => { seafileAPI.orgAdminGetUrlPrefix(orgID).then((res) => {
this.setState({ this.setState({
@@ -70,8 +62,11 @@ class OrgSAMLConfig extends Component {
seafileAPI.orgAdminGetSamlConfig(orgID).then((res) => { seafileAPI.orgAdminGetSamlConfig(orgID).then((res) => {
this.setState({ this.setState({
loading: false, loading: false,
samlConfigID: res.data.saml_config.id || '', samlConfigID: res.data.saml_config.id,
metadataUrl: res.data.saml_config.metadata_url || '', metadataUrl: res.data.saml_config.metadata_url || '',
domain: res.data.saml_config.domain || '',
dns_txt: res.data.saml_config.dns_txt || '',
domain_verified: res.data.saml_config.domain_verified || false,
}); });
}).catch(error => { }).catch(error => {
this.setState({ this.setState({
@@ -110,12 +105,14 @@ class OrgSAMLConfig extends Component {
}); });
}; };
addSamlConfig = () => { updateSamlMetadataUrl = (metadataUrl) => {
const { metadataUrl } = this.state; seafileAPI.orgAdminUpdateSamlMetadataUrl(orgID, metadataUrl).then((res) => {
seafileAPI.orgAdminAddSamlConfig(orgID, metadataUrl).then((res) => {
this.setState({ this.setState({
samlConfigID: res.data.saml_config.id, samlConfigID: res.data.saml_config.id,
metadataUrl: res.data.saml_config.metadata_url, metadataUrl: res.data.saml_config.metadata_url || '',
domain: res.data.saml_config.domain || '',
dns_txt: res.data.saml_config.dns_txt || '',
domain_verified: res.data.saml_config.domain_verified || false,
}); });
toaster.success(gettext('Success')); toaster.success(gettext('Success'));
}).catch((error) => { }).catch((error) => {
@@ -124,12 +121,14 @@ class OrgSAMLConfig extends Component {
}); });
}; };
updateSamlConfig = () => { updateSamlDomain = (domain) => {
const { metadataUrl } = this.state; seafileAPI.orgAdminUpdateSamlDomain(orgID, domain).then((res) => {
seafileAPI.orgAdminUpdateSamlConfig(orgID, metadataUrl).then((res) => {
this.setState({ this.setState({
samlConfigID: res.data.saml_config.id, samlConfigID: res.data.saml_config.id,
metadataUrl: res.data.saml_config.metadata_url, metadataUrl: res.data.saml_config.metadata_url || '',
domain: res.data.saml_config.domain || '',
dns_txt: res.data.saml_config.dns_txt || '',
domain_verified: res.data.saml_config.domain_verified || false,
}); });
toaster.success(gettext('Success')); toaster.success(gettext('Success'));
}).catch((error) => { }).catch((error) => {
@@ -143,6 +142,9 @@ class OrgSAMLConfig extends Component {
this.setState({ this.setState({
samlConfigID: '', samlConfigID: '',
metadataUrl: '', metadataUrl: '',
domain: '',
dns_txt: '',
domain_verified: false,
}); });
toaster.success(gettext('Success')); toaster.success(gettext('Success'));
}).catch((error) => { }).catch((error) => {
@@ -151,8 +153,35 @@ class OrgSAMLConfig extends Component {
}); });
}; };
verifyDomain = () => {
const {domain} = this.state;
seafileAPI.orgAdminVerifyDomain(orgID, domain).then((res) => {
this.setState({domain_verified: res.data.domain_verified});
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
generateDnsTxt = () => {
seafileAPI.orgAdminCreateDnsTxt(orgID).then((res) => {
this.setState({dns_txt: res.data.dns_txt});
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
onCopyDnsTxt = () => {
const {dns_txt} = this.state;
copy(dns_txt);
toaster.success(gettext('DNS TXT is copied to the clipboard.'));
};
render() { render() {
const { loading, errorMsg, samlConfigID, newUrlPrefix, metadataUrl, isBtnsShown } = this.state; const { loading, errorMsg, newUrlPrefix, metadataUrl, domain, dns_txt, domain_verified, isBtnsShown } = this.state;
return ( return (
<Fragment> <Fragment>
@@ -174,7 +203,7 @@ class OrgSAMLConfig extends Component {
<Label className="web-setting-label">{gettext('Your custom login URL')}</Label> <Label className="web-setting-label">{gettext('Your custom login URL')}</Label>
</Col> </Col>
<Col md="5"> <Col md="5">
{`${serviceURL}/org/custom/`}<input innerRef={input => {this.newInput = input;}} value={newUrlPrefix} onChange={this.inputOrgUrlPrefix} onFocus={this.toggleBtns} onBlur={this.hideBtns}></input> {`${serviceURL}/org/custom/`}<input value={newUrlPrefix} onChange={this.inputOrgUrlPrefix} onFocus={this.toggleBtns} onBlur={this.hideBtns}></input>
<p className="small text-secondary mt-1"> <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.')} {gettext('The custom part of the URL should be 6 to 20 characters, and can only contain alphanumeric characters and hyphens.')}
</p> </p>
@@ -188,29 +217,58 @@ class OrgSAMLConfig extends Component {
</Row> </Row>
</Fragment> </Fragment>
</Section> </Section>
<Section headingText={gettext('Manage SAML Config')}> <Section headingText={gettext('Manage SAML Config')}>
<Fragment> <Fragment>
<InputItem <InputItem
value={metadataUrl} value={metadataUrl}
changeValue={this.inputMetadataUrl} changeValue={this.updateSamlMetadataUrl}
displayName={gettext('App Federation Metadata URL')} displayName={gettext('App Federation Metadata URL')}
/> />
<InputItem
value={domain}
changeValue={this.updateSamlDomain}
displayName={gettext('Email Domain')}
domainVerified={domain_verified}
/>
<Row className="my-4"> <Row className="my-4">
{samlConfigID ? <Col md="3">
<Fragment> <Label className="web-setting-label">{gettext('DNS TXT Value')}</Label>
<Col md="1"> </Col>
<Button color="secondary" onClick={this.updateSamlConfig}>{gettext('Update')}</Button> <Col md="5">
</Col> <InputGroup>
<Col md="1"> <Input type="text" readOnly={true} value={dns_txt}/>
<Button color="primary" onClick={this.deleteSamlConfig}>{gettext('Delete')}</Button> {(dns_txt && !domain_verified) &&
</Col> <InputGroupAddon addonType="append">
</Fragment> : <Button color="primary" onClick={this.onCopyDnsTxt} className="border-0">{gettext('Copy')}</Button>
<Col md="1"> </InputGroupAddon>
<Button color="secondary" onClick={this.addSamlConfig}>{gettext('Save')}</Button> }
</Col>} </InputGroup>
{(!dns_txt && !domain_verified) &&
<p className="small text-secondary mt-1">
{gettext('Generate a domain DNS TXT, then copy it and add it to your domain\'s DNS records, then click the button to verify domain ownership.')}
</p>
}
{(dns_txt && !domain_verified) &&
<p className="small text-secondary mt-1">
{gettext('You must verify domain ownership before Single Sign-On.')}
</p>
}
</Col>
<Col md="4">
{(!dns_txt && !domain_verified) &&
<Button color="secondary" onClick={this.generateDnsTxt}>{gettext('Generate')}</Button>
}
{(dns_txt && !domain_verified) &&
<Button color="secondary" onClick={this.verifyDomain}>{gettext('Verify')}</Button>
}
</Col>
</Row> </Row>
</Fragment> </Fragment>
</Section> </Section>
<Section headingText={gettext('Upload IdP Files')}> <Section headingText={gettext('Upload IdP Files')}>
<FileItem <FileItem
postFile={this.postIdpCertificate} postFile={this.postIdpCertificate}

View File

@@ -479,6 +479,9 @@ def multi_adfs_sso(request):
if not org_saml_config: if not org_saml_config:
render_data['error_msg'] = "Cannot find a SAML/ADFS config for the organization related to domain %s." % domain render_data['error_msg'] = "Cannot find a SAML/ADFS config for the organization related to domain %s." % domain
return render(request, template_name, render_data) return render(request, template_name, render_data)
if not org_saml_config.domain_verified:
render_data['error_msg'] = "The domain %s has not been verified ownership, please login after verification." % domain
return render(request, template_name, render_data)
org_id = org_saml_config.org_id org_id = org_saml_config.org_id
org = ccnet_api.get_org_by_id(org_id) org = ccnet_api.get_org_by_id(org_id)
if not org: if not org:

View File

@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import re import re
import uuid
import subprocess
import logging import logging
from xml.etree import ElementTree from xml.etree import ElementTree
from urllib.parse import urlparse from urllib.parse import urlparse
@@ -63,8 +65,7 @@ class OrgUploadIdPCertificateView(APIView):
fd.write(idp_certificate.read()) fd.write(idp_certificate.read())
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'success': True}) return Response({'success': True})
@@ -95,65 +96,51 @@ class OrgSAMLConfigView(APIView):
if not metadata_url: if not metadata_url:
return api_error(status.HTTP_400_BAD_REQUEST, 'metadata_url invalid.') return api_error(status.HTTP_400_BAD_REQUEST, 'metadata_url invalid.')
res = requests.get(metadata_url)
root = ElementTree.fromstring(res.text)
entity_id = root.attrib.get('entityID', '')
if not entity_id:
return api_error(status.HTTP_400_BAD_REQUEST, 'Not found entityID in metadata.')
netloc = urlparse(entity_id).netloc
domain = '.'.join(netloc.split('.')[1:])
if not domain:
return api_error(status.HTTP_400_BAD_REQUEST, 'Invalid entityID in metadata.')
# resource check # resource check
org_id = int(org_id) org_id = int(org_id)
if not ccnet_api.get_org_by_id(org_id): org = ccnet_api.get_org_by_id(org_id)
if not org:
error_msg = 'Organization %s not found.' % org_id error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg) return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# add an org saml config # add or update saml/adfs login config
try: try:
saml_comfig = OrgSAMLConfig.objects.add_or_update_saml_config(org_id, metadata_url, domain) saml_config = OrgSAMLConfig.objects.add_or_update_saml_config(org_id, metadata_url)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'saml_config': saml_comfig.to_dict()}) return Response({'saml_config': saml_config.to_dict()})
def put(self, request, org_id): def put(self, request, org_id):
# argument check # argument check
metadata_url = request.data.get('metadata_url', None) domain = request.data.get('domain', None)
if not metadata_url:
return api_error(status.HTTP_400_BAD_REQUEST, 'metadata_url invalid.')
res = requests.get(metadata_url)
root = ElementTree.fromstring(res.text)
entity_id = root.attrib.get('entityID', '')
if not entity_id:
return api_error(status.HTTP_400_BAD_REQUEST, 'Not found entityID in metadata.')
netloc = urlparse(entity_id).netloc
domain = '.'.join(netloc.split('.')[1:])
if not domain: if not domain:
return api_error(status.HTTP_400_BAD_REQUEST, 'Invalid entityID in metadata.') return api_error(status.HTTP_400_BAD_REQUEST, 'domain invalid.')
# resource check # resource check
org_id = int(org_id) org_id = int(org_id)
if not ccnet_api.get_org_by_id(org_id): org = ccnet_api.get_org_by_id(org_id)
if not org:
error_msg = 'Organization %s not found.' % org_id error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg) return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# update config saml_config = OrgSAMLConfig.objects.get_config_by_org_id(org_id)
if not saml_config:
error_msg = 'Cannot find a SAML/ADFS config for the organization %s.' % org.org_name
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# When the domain is updated, the domain ownership needs to be re-verified, so set dns_txt to None
try: try:
saml_comfig = OrgSAMLConfig.objects.add_or_update_saml_config(org_id, metadata_url, domain) saml_config.domain = domain
saml_config.dns_txt = None
saml_config.domain_verified = False
saml_config.save()
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'saml_config': saml_comfig.to_dict()}) return Response({'saml_config': saml_config.to_dict()})
def delete(self, request, org_id): def delete(self, request, org_id):
# resource check # resource check
@@ -169,8 +156,7 @@ class OrgSAMLConfigView(APIView):
return Response({'success': True}) return Response({'success': True})
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'success': True}) return Response({'success': True})
@@ -227,3 +213,87 @@ class OrgUrlPrefixView(APIView):
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
return Response({'org_url_prefix': org_url_prefix}) return Response({'org_url_prefix': org_url_prefix})
class OrgVerifyDomain(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
throttle_classes = (UserRateThrottle,)
permission_classes = (IsProVersion, IsOrgAdminUser)
def post(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)
saml_config = OrgSAMLConfig.objects.get_config_by_org_id(org_id)
if not saml_config:
error_msg = 'Cannot find a SAML/ADFS config for the organization %s.' % org.org_name
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
if saml_config.dns_txt:
return Response({'dns_txt': saml_config.dns_txt})
try:
dns_txt = 'seafile-site-verification=' + str(uuid.uuid4().hex)
saml_config.dns_txt = dns_txt
saml_config.save()
except Exception as e:
logger.error(e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
return Response({'dns_txt': saml_config.dns_txt})
def put(self, request, org_id):
# argument check
domain = request.data.get('domain', None)
if not domain:
return api_error(status.HTTP_400_BAD_REQUEST, 'domain invalid.')
# 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)
saml_config = OrgSAMLConfig.objects.get_config_by_org_id(org_id)
if not saml_config:
error_msg = 'Cannot find a SAML/ADFS config for the organization %s.' % org.org_name
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
if saml_config.domain_verified:
return Response({'domain_verified': saml_config.domain_verified})
if not saml_config.dns_txt:
error_msg = 'Cannot find dns_txt, please generate dns_txt first.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
proc = subprocess.Popen(["nslookup", "-type=TXT", domain], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
stdout, stderr = proc.communicate(timeout=60)
except subprocess.TimeoutExpired:
proc.kill()
stdout, stderr = proc.communicate()
logger.error('Process execution timed out, stdout: %s, stderr: %s' % (stdout, stderr))
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
except Exception as e:
logger.error(e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
if stderr:
logger.error(stderr)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
if saml_config.dns_txt in stdout.decode():
saml_config.domain_verified = True
saml_config.save()
return Response({'domain_verified': saml_config.domain_verified})
else:
logger.error(stdout)
error_msg = "Failed to verify domain ownership. Please make sure you have added " \
"the DNS TXT to your domain's DNS records and wait 5 minutes before trying again."
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)

View File

@@ -27,7 +27,8 @@ from .api.admin.logo import OrgAdminLogo
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, OrgSAMLConfigView, OrgUrlPrefixView from .api.admin.saml_config import OrgUploadIdPCertificateView, OrgSAMLConfigView, OrgUrlPrefixView, \
OrgVerifyDomain
urlpatterns = [ urlpatterns = [
@@ -62,6 +63,9 @@ urlpatterns = [
path('<int:org_id>/admin/url-prefix/', path('<int:org_id>/admin/url-prefix/',
OrgUrlPrefixView.as_view(), OrgUrlPrefixView.as_view(),
name='api-v2.1-org-admin-url-prefix'), name='api-v2.1-org-admin-url-prefix'),
path('<int:org_id>/admin/verify-domain/',
OrgVerifyDomain.as_view(),
name='api-v2.1-org-admin-verify-domain'),
path('<int:org_id>/admin/logo/', OrgAdminLogo.as_view(), name='api-v2.1-org-admin-logo'), path('<int:org_id>/admin/logo/', OrgAdminLogo.as_view(), name='api-v2.1-org-admin-logo'),
path('<int:org_id>/admin/devices/', OrgAdminDevices.as_view(), name='api-v2.1-org-admin-devices'), path('<int:org_id>/admin/devices/', OrgAdminDevices.as_view(), name='api-v2.1-org-admin-devices'),

View File

@@ -84,18 +84,13 @@ class OrgSettings(models.Model):
class OrgSAMLConfigManager(models.Manager): class OrgSAMLConfigManager(models.Manager):
def add_or_update_saml_config(self, org_id, metadata_url=None, domain=None): def add_or_update_saml_config(self, org_id, metadata_url):
try: try:
saml_config = self.get(org_id=org_id) saml_config = self.get(org_id=org_id)
except OrgSAMLConfig.DoesNotExist: except OrgSAMLConfig.DoesNotExist:
saml_config = self.model(org_id=org_id) saml_config = self.model(org_id=org_id)
if metadata_url: saml_config.metadata_url = metadata_url
saml_config.metadata_url = metadata_url
if domain:
saml_config.domain = domain
saml_config.save(using=self._db) saml_config.save(using=self._db)
return saml_config return saml_config
@@ -117,7 +112,9 @@ class OrgSAMLConfigManager(models.Manager):
class OrgSAMLConfig(models.Model): class OrgSAMLConfig(models.Model):
org_id = models.IntegerField(unique=True) org_id = models.IntegerField(unique=True)
metadata_url = models.TextField() metadata_url = models.TextField()
domain = models.CharField(max_length=255, unique=True) domain = models.CharField(max_length=255, unique=True, null=True, blank=True)
dns_txt = models.CharField(max_length=64, null=True, blank=True)
domain_verified = models.BooleanField(default=False, db_index=True)
objects = OrgSAMLConfigManager() objects = OrgSAMLConfigManager()
@@ -129,6 +126,9 @@ class OrgSAMLConfig(models.Model):
'id': self.pk, 'id': self.pk,
'org_id': self.org_id, 'org_id': self.org_id,
'metadata_url': self.metadata_url, 'metadata_url': self.metadata_url,
'domain': self.domain,
'dns_txt': self.dns_txt,
'domain_verified': self.domain_verified,
} }