summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
Diffstat (limited to 'webapp')
-rw-r--r--webapp/components/admin_console/admin_sidebar.jsx16
-rw-r--r--webapp/components/admin_console/file_upload_setting.jsx124
-rw-r--r--webapp/components/admin_console/remove_file_setting.jsx72
-rw-r--r--webapp/components/admin_console/saml_settings.jsx518
-rw-r--r--webapp/components/admin_console/user_item.jsx4
-rw-r--r--webapp/components/claim/components/email_to_oauth.jsx8
-rw-r--r--webapp/components/claim/components/oauth_to_email.jsx5
-rw-r--r--webapp/components/login/login_controller.jsx18
-rw-r--r--webapp/components/signup_user_complete.jsx14
-rw-r--r--webapp/components/user_settings/user_settings_general.jsx28
-rw-r--r--webapp/components/user_settings/user_settings_security.jsx27
-rw-r--r--webapp/i18n/en.json63
-rw-r--r--webapp/package.json2
-rw-r--r--webapp/routes/route_admin_console.jsx5
-rw-r--r--webapp/sass/routes/_admin-console.scss6
-rw-r--r--webapp/sass/routes/_signup.scss12
-rw-r--r--webapp/utils/constants.jsx1
17 files changed, 914 insertions, 9 deletions
diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx
index 8f88afab4..5a31519c9 100644
--- a/webapp/components/admin_console/admin_sidebar.jsx
+++ b/webapp/components/admin_console/admin_sidebar.jsx
@@ -176,6 +176,7 @@ export default class AdminSidebar extends React.Component {
render() {
let ldapSettings = null;
+ let samlSettings = null;
let complianceSettings = null;
let license = null;
@@ -198,6 +199,20 @@ export default class AdminSidebar extends React.Component {
);
}
+ if (global.window.mm_license.SAML === 'true') {
+ samlSettings = (
+ <AdminSidebarSection
+ name='saml'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.saml'
+ defaultMessage='SAML'
+ />
+ }
+ />
+ );
+ }
+
if (global.window.mm_license.Compliance === 'true') {
complianceSettings = (
<AdminSidebarSection
@@ -391,6 +406,7 @@ export default class AdminSidebar extends React.Component {
}
/>
{ldapSettings}
+ {samlSettings}
</AdminSidebarSection>
<AdminSidebarSection
name='security'
diff --git a/webapp/components/admin_console/file_upload_setting.jsx b/webapp/components/admin_console/file_upload_setting.jsx
new file mode 100644
index 000000000..e7cb387ee
--- /dev/null
+++ b/webapp/components/admin_console/file_upload_setting.jsx
@@ -0,0 +1,124 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import Setting from './setting.jsx';
+
+import * as Utils from 'utils/utils.jsx';
+
+export default class FileUploadSetting extends Setting {
+ static get propTypes() {
+ return {
+ id: React.PropTypes.string.isRequired,
+ label: React.PropTypes.node.isRequired,
+ helpText: React.PropTypes.node,
+ uploadingText: React.PropTypes.node,
+ onSubmit: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool,
+ fileType: React.PropTypes.string.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ fileName: null
+ };
+ }
+
+ handleChange() {
+ const files = this.refs.fileInput.files;
+ if (files && files.length > 0) {
+ this.setState({fileSelected: true, fileName: files[0].name});
+ }
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ $(this.refs.upload_button).button('loading');
+ this.props.onSubmit(this.props.id, this.refs.fileInput.files[0], (error) => {
+ $(this.refs.upload_button).button('reset');
+ if (error) {
+ Utils.clearFileInput(this.refs.fileInput);
+ }
+ this.setState({fileSelected: false, fileName: null, serverError: error});
+ });
+ }
+
+ render() {
+ let serverError;
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var btnClass = 'btn';
+ if (this.state.fileSelected) {
+ btnClass = 'btn btn-primary';
+ }
+
+ let fileName;
+ if (this.state.fileName) {
+ fileName = this.state.fileName;
+ } else {
+ fileName = (
+ <FormattedMessage
+ id='admin.file_upload.noFile'
+ defaultMessage='No file uploaded'
+ />
+ );
+ }
+
+ return (
+ <Setting
+ label={this.props.label}
+ helpText={this.props.helpText}
+ inputId={this.props.id}
+ >
+ <div>
+ <div className='file__upload'>
+ <button
+ className='btn btn-default'
+ disabled={this.props.disabled}
+ >
+ <FormattedMessage
+ id='admin.file_upload.chooseFile'
+ defaultMessage='Choose File'
+ />
+ </button>
+ <input
+ ref='fileInput'
+ type='file'
+ disabled={this.props.disabled}
+ accept={this.props.fileType}
+ onChange={this.handleChange}
+ />
+ </div>
+ <button
+ className={btnClass}
+ disabled={!this.state.fileSelected}
+ onClick={this.handleSubmit}
+ ref='upload_button'
+ data-loading-text={`<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ${this.props.uploadingText}`}
+ >
+ <FormattedMessage
+ id='admin.file_upload.uploadFile'
+ defaultMessage='Upload'
+ />
+ </button>
+ <div className='help-text no-margin'>
+ {fileName}
+ </div>
+ {serverError}
+ </div>
+ </Setting>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/remove_file_setting.jsx b/webapp/components/admin_console/remove_file_setting.jsx
new file mode 100644
index 000000000..5a76faae2
--- /dev/null
+++ b/webapp/components/admin_console/remove_file_setting.jsx
@@ -0,0 +1,72 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import React from 'react';
+
+import Setting from './setting.jsx';
+
+export default class RemoveFileSetting extends Setting {
+ static get propTypes() {
+ return {
+ id: React.PropTypes.string.isRequired,
+ label: React.PropTypes.node.isRequired,
+ helpText: React.PropTypes.node,
+ removeButtonText: React.PropTypes.node.isRequired,
+ removingText: React.PropTypes.node,
+ fileName: React.PropTypes.string.isRequired,
+ onSubmit: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.handleRemove = this.handleRemove.bind(this);
+
+ this.state = {
+ serverError: null
+ };
+ }
+
+ handleRemove(e) {
+ e.preventDefault();
+
+ $(this.refs.remove_button).button('loading');
+ this.props.onSubmit(this.props.id, (error) => {
+ $(this.refs.remove_button).button('reset');
+ this.setState({serverError: error});
+ });
+ }
+
+ render() {
+ let serverError;
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ return (
+ <Setting
+ label={this.props.label}
+ helpText={this.props.helpText}
+ inputId={this.props.id}
+ >
+ <div>
+ <div className='help-text remove-filename'>
+ {this.props.fileName}
+ </div>
+ <button
+ className='btn btn-danger'
+ onClick={this.handleRemove}
+ ref='remove_button'
+ disabled={this.props.disabled}
+ data-loading-text={`<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> ${this.props.removingText}`}
+ >
+ {this.props.removeButtonText}
+ </button>
+ {serverError}
+ </div>
+ </Setting>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/saml_settings.jsx b/webapp/components/admin_console/saml_settings.jsx
new file mode 100644
index 000000000..db841aa83
--- /dev/null
+++ b/webapp/components/admin_console/saml_settings.jsx
@@ -0,0 +1,518 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import TextSetting from './text_setting.jsx';
+import FileUploadSetting from './file_upload_setting.jsx';
+import RemoveFileSetting from './remove_file_setting.jsx';
+
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+
+import Client from 'utils/web_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+export default class SamlSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+ this.uploadCertificate = this.uploadCertificate.bind(this);
+ this.removeCertificate = this.removeCertificate.bind(this);
+
+ const settings = props.config.SamlSettings;
+
+ this.state = Object.assign(this.state, {
+ enable: settings.Enable,
+ verify: settings.Verify,
+ encrypt: settings.Encrypt,
+ idpUrl: settings.IdpUrl,
+ idpDescriptorUrl: settings.IdpDescriptorUrl,
+ assertionConsumerServiceURL: settings.AssertionConsumerServiceURL,
+ idpCertificateFile: settings.IdpCertificateFile,
+ publicCertificateFile: settings.PublicCertificateFile,
+ privateKeyFile: settings.PrivateKeyFile,
+ firstNameAttribute: settings.FirstNameAttribute,
+ lastNameAttribute: settings.LastNameAttribute,
+ emailAttribute: settings.EmailAttribute,
+ usernameAttribute: settings.UsernameAttribute,
+ nicknameAttribute: settings.NicknameAttribute,
+ localeAttribute: settings.LocaleAttribute,
+ loginButtonText: settings.LoginButtonText
+ });
+ }
+
+ getConfigFromState(config) {
+ config.SamlSettings.Enable = this.state.enable;
+ config.SamlSettings.Verify = this.state.verify;
+ config.SamlSettings.Encrypt = this.state.encrypt;
+ config.SamlSettings.IdpUrl = this.state.idpUrl;
+ config.SamlSettings.IdpDescriptorUrl = this.state.idpDescriptorUrl;
+ config.SamlSettings.AssertionConsumerServiceURL = this.state.assertionConsumerServiceURL;
+ config.SamlSettings.IdpCertificateFile = this.state.idpCertificateFile;
+ config.SamlSettings.PublicCertificateFile = this.state.publicCertificateFile;
+ config.SamlSettings.PrivateKeyFile = this.state.privateKeyFile;
+ config.SamlSettings.FirstNameAttribute = this.state.firstNameAttribute;
+ config.SamlSettings.LastNameAttribute = this.state.lastNameAttribute;
+ config.SamlSettings.EmailAttribute = this.state.emailAttribute;
+ config.SamlSettings.UsernameAttribute = this.state.usernameAttribute;
+ config.SamlSettings.NicknameAttribute = this.state.nicknameAttribute;
+ config.SamlSettings.LocaleAttribute = this.state.localeAttribute;
+ config.SamlSettings.LoginButtonText = this.state.loginButtonText;
+
+ return config;
+ }
+
+ uploadCertificate(id, file, callback) {
+ Client.uploadCertificateFile(
+ file,
+ () => {
+ const fileName = file.name;
+ this.handleChange(id, fileName);
+ this.setState({[id]: fileName});
+ if (callback && typeof callback === 'function') {
+ callback();
+ }
+ },
+ (error) => {
+ if (callback && typeof callback === 'function') {
+ callback(error.message);
+ }
+ }
+ );
+ }
+
+ removeCertificate(id, callback) {
+ Client.removeCertificateFile(
+ this.state[id],
+ () => {
+ this.handleChange(id, '');
+ this.setState({[id]: null});
+ },
+ (error) => {
+ if (callback && typeof callback === 'function') {
+ callback(error.message);
+ }
+ }
+ );
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.authentication.saml'
+ defaultMessage='SAML'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.SAML === 'true';
+ if (!licenseEnabled) {
+ return null;
+ }
+
+ let idpCert;
+ let privKey;
+ let pubCert;
+
+ if (this.state.idpCertificateFile) {
+ idpCert = (
+ <RemoveFileSetting
+ id='idpCertificateFile'
+ label={
+ <FormattedMessage
+ id='admin.saml.idpCertificateFileTitle'
+ defaultMessage='Identity Provider Public Certificate:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.saml.idpCertificateFileRemoveDesc'
+ defaultMessage='Remove the public authentication certificate issued by your Identity Provider.'
+ />
+ }
+ removeButtonText={Utils.localizeMessage('admin.saml.remove.idp_certificate', 'Remove Identity Provider Certificate')}
+ removingText={Utils.localizeMessage('admin.saml.removing.certificate', 'Removing Certificate...')}
+ fileName={this.state.idpCertificateFile}
+ onSubmit={this.removeCertificate}
+ disabled={!this.state.enable}
+ />
+ );
+ } else {
+ idpCert = (
+ <FileUploadSetting
+ id='idpCertificateFile'
+ label={
+ <FormattedMessage
+ id='admin.saml.idpCertificateFileTitle'
+ defaultMessage='Identity Provider Public Certificate:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.saml.idpCertificateFileDesc'
+ defaultMessage='The public authentication certificate issued by your Identity Provider.'
+ />
+ }
+ uploadingText={Utils.localizeMessage('admin.saml.uploading.certificate', 'Uploading Certificate...')}
+ disabled={!this.state.enable}
+ fileType='.crt'
+ onSubmit={this.uploadCertificate}
+ />
+ );
+ }
+
+ if (this.state.privateKeyFile) {
+ privKey = (
+ <RemoveFileSetting
+ id='privateKeyFile'
+ label={
+ <FormattedMessage
+ id='admin.saml.privateKeyFileTitle'
+ defaultMessage='Service Provider Private Key:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.saml.privateKeyFileFileRemoveDesc'
+ defaultMessage='Remove the private key used to decrypt SAML Assertions from the Identity Provider.'
+ />
+ }
+ removeButtonText={Utils.localizeMessage('admin.saml.remove.privKey', 'Remove Service Provider Private Key')}
+ removingText={Utils.localizeMessage('admin.saml.removing.privKey', 'Removing Private Key...')}
+ fileName={this.state.privateKeyFile}
+ onSubmit={this.removeCertificate}
+ disabled={!this.state.enable || !this.state.encrypt}
+ />
+ );
+ } else {
+ privKey = (
+ <FileUploadSetting
+ id='privateKeyFile'
+ label={
+ <FormattedMessage
+ id='admin.saml.privateKeyFileTitle'
+ defaultMessage='Service Provider Private Key:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.saml.privateKeyFileFileDesc'
+ defaultMessage='The private key used to decrypt SAML Assertions from the Identity Provider.'
+ />
+ }
+ uploadingText={Utils.localizeMessage('admin.saml.uploading.privateKey', 'Uploading Private Key...')}
+ disabled={!this.state.enable || !this.state.encrypt}
+ fileType='.key'
+ onSubmit={this.uploadCertificate}
+ />
+ );
+ }
+
+ if (this.state.publicCertificateFile) {
+ pubCert = (
+ <RemoveFileSetting
+ id='publicCertificateFile'
+ label={
+ <FormattedMessage
+ id='admin.saml.publicCertificateFileTitle'
+ defaultMessage='Service Provider Public Certificate:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.saml.publicCertificateFileRemoveDesc'
+ defaultMessage='Remove the certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.'
+ />
+ }
+ removeButtonText={Utils.localizeMessage('admin.saml.remove.sp_certificate', 'Remove Service Provider Certificate')}
+ removingText={Utils.localizeMessage('admin.saml.removing.certificate', 'Removing Certificate...')}
+ fileName={this.state.publicCertificateFile}
+ onSubmit={this.removeCertificate}
+ disabled={!this.state.enable || !this.state.encrypt}
+ />
+ );
+ } else {
+ pubCert = (
+ <FileUploadSetting
+ id='publicCertificateFile'
+ label={
+ <FormattedMessage
+ id='admin.saml.publicCertificateFileTitle'
+ defaultMessage='Service Provider Public Certificate:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.saml.publicCertificateFileDesc'
+ defaultMessage='The certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.'
+ />
+ }
+ uploadingText={Utils.localizeMessage('admin.saml.uploading.certificate', 'Uploading Certificate...')}
+ disabled={!this.state.enable || !this.state.encrypt}
+ fileType='.crt'
+ onSubmit={this.uploadCertificate}
+ />
+ );
+ }
+
+ return (
+ <SettingsGroup>
+ <BooleanSetting
+ id='enable'
+ label={
+ <FormattedMessage
+ id='admin.saml.enableTitle'
+ defaultMessage='Enable Login With SAML:'
+ />
+ }
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.saml.enableDescription'
+ defaultMessage='When true, Mattermost allows login using SAML. Please see <a href="http://docs.mattermost.com/deployment/sso-saml.html" target="_blank">documentation</a> to learn more about configuring SAML for Mattermost.'
+ />
+ }
+ value={this.state.enable}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='idpUrl'
+ label={
+ <FormattedMessage
+ id='admin.saml.idpUrlTitle'
+ defaultMessage='SAML SSO URL:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.saml.idpUrlEx', 'Ex "https://idp.example.org/SAML2/SSO/Login"')}
+ helpText={
+ <FormattedMessage
+ id='admin.saml.idpUrlDesc'
+ defaultMessage='The URL where Mattermost sends a SAML request to start login sequence.'
+ />
+ }
+ value={this.state.idpUrl}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='idpDescriptorUrl'
+ label={
+ <FormattedMessage
+ id='admin.saml.idpDescriptorUrlTitle'
+ defaultMessage='Identity Provider Issuer URL:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.saml.idpDescriptorUrlEx', 'Ex "https://idp.example.org/SAML2/issuer"')}
+ helpText={
+ <FormattedMessage
+ id='admin.saml.idpDescriptorUrlDesc'
+ defaultMessage='The issuer URL for the Identity Provider you use for SAML requests.'
+ />
+ }
+ value={this.state.idpDescriptorUrl}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ {idpCert}
+ <BooleanSetting
+ id='verify'
+ label={
+ <FormattedMessage
+ id='admin.saml.verifyTitle'
+ defaultMessage='Verify Signature:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.saml.verifyDescription'
+ defaultMessage='When true, Mattermost verifies that the signature sent from the SAML Response matches the Service Provider Login URL'
+ />
+ }
+ value={this.state.verify}
+ disabled={!this.state.enable}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='assertionConsumerServiceURL'
+ label={
+ <FormattedMessage
+ id='admin.saml.assertionConsumerServiceURLTitle'
+ defaultMessage='Service Provider Login URL:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.saml.assertionConsumerServiceURLEx', 'Ex "https://<your-mattermost-url>/login/sso/saml"')}
+ helpText={
+ <FormattedMessage
+ id='admin.saml.assertionConsumerServiceURLDesc'
+ defaultMessage='Enter https://<your-mattermost-url>/login/sso/saml. Make sure you use HTTP or HTTPS in your URL depending on your server configuration. This field is also known as the Assertion Consumer Service URL.'
+ />
+ }
+ value={this.state.assertionConsumerServiceURL}
+ onChange={this.handleChange}
+ disabled={!this.state.enable || !this.state.verify}
+ />
+ <BooleanSetting
+ id='encrypt'
+ label={
+ <FormattedMessage
+ id='admin.saml.encryptTitle'
+ defaultMessage='Enable Encryption:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.saml.encryptDescription'
+ defaultMessage='When true, Mattermost will decrypt SAML Assertions encrypted with your Service Provider Public Certificate.'
+ />
+ }
+ value={this.state.encrypt}
+ disabled={!this.state.enable}
+ onChange={this.handleChange}
+ />
+ {privKey}
+ {pubCert}
+ <TextSetting
+ id='emailAttribute'
+ label={
+ <FormattedMessage
+ id='admin.saml.emailAttrTitle'
+ defaultMessage='Email Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.saml.emailAttrEx', 'Ex "Email" or "PrimaryEmail"')}
+ helpText={
+ <FormattedMessage
+ id='admin.saml.emailAttrDesc'
+ defaultMessage='The attribute in the SAML Assertion that will be used to populate the email addresses of users in Mattermost.'
+ />
+ }
+ value={this.state.emailAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='usernameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.saml.usernameAttrTitle'
+ defaultMessage='Username Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.saml.usernameAttrEx', 'Ex "Username"')}
+ helpText={
+ <FormattedMessage
+ id='admin.saml.usernameAttrDesc'
+ defaultMessage='The attribute in the SAML Assertion that will be used to populate the username field in Mattermost.'
+ />
+ }
+ value={this.state.usernameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='firstNameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.saml.firstnameAttrTitle'
+ defaultMessage='First Name Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.saml.firstnameAttrEx', 'Ex "FirstName"')}
+ helpText={
+ <FormattedMessage
+ id='admin.saml.firstnameAttrDesc'
+ defaultMessage='The attribute in the SAML Assertion that will be used to populate the first name of users in Mattermost.'
+ />
+ }
+ value={this.state.firstNameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='lastNameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.saml.lastnameAttrTitle'
+ defaultMessage='Last Name Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.saml.lastnameAttrEx', 'Ex "LastName"')}
+ helpText={
+ <FormattedMessage
+ id='admin.saml.lastnameAttrDesc'
+ defaultMessage='The attribute in the SAML Assertion that will be used to populate the last name of users in Mattermost.'
+ />
+ }
+ value={this.state.lastNameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='nicknameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.saml.nicknameAttrTitle'
+ defaultMessage='Nickname Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.saml.nicknameAttrEx', 'Ex "Nickname"')}
+ helpText={
+ <FormattedMessage
+ id='admin.saml.nicknameAttrDesc'
+ defaultMessage='(Optional) The attribute in the SAML Assertion that will be used to populate the nickname of users in Mattermost.'
+ />
+ }
+ value={this.state.nicknameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='localeAttribute'
+ label={
+ <FormattedMessage
+ id='admin.saml.localeAttrTitle'
+ defaultMessage='Preferred Language Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.saml.localeAttrEx', 'Ex "Locale" or "PrimaryLanguage"')}
+ helpText={
+ <FormattedMessage
+ id='admin.saml.localeAttrDesc'
+ defaultMessage='(Optional) The attribute in the SAML Assertion that will be used to populate the language of users in Mattermost.'
+ />
+ }
+ value={this.state.localeAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='loginButtonText'
+ label={
+ <FormattedMessage
+ id='admin.saml.loginButtonTextTitle'
+ defaultMessage='Login Button Text:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.saml.loginButtonTextEx', 'Ex "With OKTA"')}
+ helpText={
+ <FormattedMessage
+ id='admin.saml.loginButtonTextDesc'
+ defaultMessage='(Optional) The text that appears in the login button on the login page. Defaults to "With SAML".'
+ />
+ }
+ value={this.state.loginButtonText}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx
index 62de50f0f..edded5aab 100644
--- a/webapp/components/admin_console/user_item.jsx
+++ b/webapp/components/admin_console/user_item.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import Client from 'utils/web_client.jsx';
+import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import UserStore from 'stores/user_store.jsx';
import ConfirmModal from '../confirm_modal.jsx';
@@ -374,12 +375,13 @@ export default class UserItem extends React.Component {
let authServiceText;
let passwordReset;
if (user.auth_service) {
+ const service = (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) ? user.auth_service.toUpperCase() : Utils.toTitleCase(user.auth_service);
authServiceText = (
<FormattedHTMLMessage
id='admin.user_item.authServiceNotEmail'
defaultMessage=', <strong>Sign-in Method:</strong> {service}'
values={{
- service: Utils.toTitleCase(user.auth_service)
+ service
}}
/>
);
diff --git a/webapp/components/claim/components/email_to_oauth.jsx b/webapp/components/claim/components/email_to_oauth.jsx
index 6b0a90e8e..422b31a3a 100644
--- a/webapp/components/claim/components/email_to_oauth.jsx
+++ b/webapp/components/claim/components/email_to_oauth.jsx
@@ -3,6 +3,7 @@
import * as Utils from 'utils/utils.jsx';
import Client from 'utils/web_client.jsx';
+import Constants from 'utils/constants.jsx';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -55,7 +56,8 @@ export default class EmailToOAuth extends React.Component {
formClass += ' has-error';
}
- const uiType = Utils.toTitleCase(this.props.newType) + ' SSO';
+ const type = (this.props.newType === Constants.SAML_SERVICE ? Constants.SAML_SERVICE.toUpperCase() : Utils.toTitleCase(this.props.newType));
+ const uiType = `${type} SSO`;
return (
<div>
@@ -74,7 +76,7 @@ export default class EmailToOAuth extends React.Component {
id='claim.email_to_oauth.ssoType'
defaultMessage='Upon claiming your account, you will only be able to login with {type} SSO'
values={{
- type: Utils.toTitleCase(this.props.newType)
+ type
}}
/>
</p>
@@ -83,7 +85,7 @@ export default class EmailToOAuth extends React.Component {
id='claim.email_to_oauth.ssoNote'
defaultMessage='You must already have a valid {type} account'
values={{
- type: Utils.toTitleCase(this.props.newType)
+ type
}}
/>
</p>
diff --git a/webapp/components/claim/components/oauth_to_email.jsx b/webapp/components/claim/components/oauth_to_email.jsx
index 17ca12264..6a0f6431b 100644
--- a/webapp/components/claim/components/oauth_to_email.jsx
+++ b/webapp/components/claim/components/oauth_to_email.jsx
@@ -3,6 +3,7 @@
import * as Utils from 'utils/utils.jsx';
import Client from 'utils/web_client.jsx';
+import Constants from 'utils/constants.jsx';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -62,7 +63,7 @@ export default class OAuthToEmail extends React.Component {
formClass += ' has-error';
}
- const uiType = Utils.toTitleCase(this.props.currentType) + ' SSO';
+ const uiType = `${(this.props.currentType === Constants.SAML_SERVICE ? Constants.SAML_SERVICE.toUpperCase() : Utils.toTitleCase(this.props.currentType))} SSO`;
return (
<div>
@@ -85,7 +86,7 @@ export default class OAuthToEmail extends React.Component {
<p>
<FormattedMessage
id='claim.oauth_to_email.enterNewPwd'
- defaultMessage='Enter a new password for your {site} account'
+ defaultMessage='Enter a new password for your {site} email account'
values={{
site: global.window.mm_config.SiteName
}}
diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx
index 653908654..cd4175d3c 100644
--- a/webapp/components/login/login_controller.jsx
+++ b/webapp/components/login/login_controller.jsx
@@ -43,6 +43,7 @@ export default class LoginController extends React.Component {
ldapEnabled: global.window.mm_license.IsLicensed === 'true' && global.window.mm_config.EnableLdap === 'true',
usernameSigninEnabled: global.window.mm_config.EnableSignInWithUsername === 'true',
emailSigninEnabled: global.window.mm_config.EnableSignInWithEmail === 'true',
+ samlEnabled: global.window.mm_license.IsLicensed === 'true' && global.window.mm_config.EnableSaml === 'true',
loginId: '', // the browser will set a default for this
password: '',
showMfa: false
@@ -319,6 +320,7 @@ export default class LoginController extends React.Component {
const ldapEnabled = this.state.ldapEnabled;
const gitlabSigninEnabled = global.window.mm_config.EnableSignUpWithGitLab === 'true';
const googleSigninEnabled = global.window.mm_config.EnableSignUpWithGoogle === 'true';
+ const samlSigninEnabled = this.state.samlEnabled;
const usernameSigninEnabled = this.state.usernameSigninEnabled;
const emailSigninEnabled = this.state.emailSigninEnabled;
@@ -416,7 +418,7 @@ export default class LoginController extends React.Component {
);
}
- if ((emailSigninEnabled || usernameSigninEnabled || ldapEnabled) && (gitlabSigninEnabled || googleSigninEnabled)) {
+ if ((emailSigninEnabled || usernameSigninEnabled || ldapEnabled) && (gitlabSigninEnabled || googleSigninEnabled || samlSigninEnabled)) {
loginControls.push(
<div
key='divider'
@@ -475,6 +477,20 @@ export default class LoginController extends React.Component {
);
}
+ if (samlSigninEnabled) {
+ loginControls.push(
+ <a
+ className='btn btn-custom-login saml'
+ key='gitlab'
+ href={'/login/sso/saml' + this.props.location.search}
+ >
+ <span>
+ {window.mm_config.SamlLoginButtonText}
+ </span>
+ </a>
+ );
+ }
+
return (
<div>
{extraBox}
diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx
index c7ddfc91b..fa5e9268e 100644
--- a/webapp/components/signup_user_complete.jsx
+++ b/webapp/components/signup_user_complete.jsx
@@ -588,6 +588,20 @@ export default class SignupUserComplete extends React.Component {
);
}
+ if (global.window.mm_config.EnableSaml === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.SAML === 'true') {
+ signupMessage.push(
+ <a
+ className='btn btn-custom-login saml'
+ key='saml'
+ href={`/login/sso/saml${window.location.search}${window.location.search ? '&' : '?'}action=signup`}
+ >
+ <span>
+ {global.window.mm_config.SamlLoginButtonText}
+ </span>
+ </a>
+ );
+ }
+
let ldapSignup;
if (global.window.mm_config.EnableLdap === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP) {
ldapSignup = (
diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx
index f8910b9bc..5e821a26a 100644
--- a/webapp/components/user_settings/user_settings_general.jsx
+++ b/webapp/components/user_settings/user_settings_general.jsx
@@ -412,6 +412,24 @@ class UserSettingsGeneralTab extends React.Component {
{helpText}
</div>
);
+ } else if (this.props.user.auth_service === Constants.SAML_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.general.emailSamlCantUpdate'
+ defaultMessage='Login occurs through SAML. Email cannot be updated. Email address used for notifications is {email}.'
+ values={{
+ email: this.state.email
+ }}
+ />
+ </div>
+ {helpText}
+ </div>
+ );
}
emailSection = (
@@ -478,6 +496,16 @@ class UserSettingsGeneralTab extends React.Component {
}}
/>
);
+ } else if (this.props.user.auth_service === Constants.SAML_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.general.loginSaml'
+ defaultMessage='Login done through SAML ({email})'
+ values={{
+ email: this.state.email
+ }}
+ />
+ );
}
emailSection = (
diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx
index af7aeb3c6..247dc0f81 100644
--- a/webapp/components/user_settings/user_settings_security.jsx
+++ b/webapp/components/user_settings/user_settings_security.jsx
@@ -620,6 +620,24 @@ class SecurityTab extends React.Component {
);
}
+ let samlOption;
+ if (global.window.mm_config.EnableSaml === 'true' && user.auth_service === '') {
+ samlOption = (
+ <div>
+ <Link
+ className='btn btn-primary'
+ to={'/claim/email_to_oauth?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.SAML_SERVICE}
+ >
+ <FormattedMessage
+ id='user.settings.security.switchSaml'
+ defaultMessage='Switch to using SAML SSO'
+ />
+ </Link>
+ <br/>
+ </div>
+ );
+ }
+
const inputs = [];
inputs.push(
<div key='userSignInOption'>
@@ -627,6 +645,7 @@ class SecurityTab extends React.Component {
{gitlabOption}
<br/>
{ldapOption}
+ {samlOption}
{googleOption}
</div>
);
@@ -681,6 +700,13 @@ class SecurityTab extends React.Component {
defaultMessage='LDAP'
/>
);
+ } else if (this.props.user.auth_service === Constants.SAML_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.security.saml'
+ defaultMessage='SAML'
+ />
+ );
}
return (
@@ -701,6 +727,7 @@ class SecurityTab extends React.Component {
numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods;
numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods;
numMethods = global.window.mm_config.EnableLdap === 'true' ? numMethods + 1 : numMethods;
+ numMethods = global.window.mm_config.EnableSaml === 'true' ? numMethods + 1 : numMethods;
let signInSection;
if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) {
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 02d11e484..26b1b47fd 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -102,6 +102,7 @@
"admin.audits.title": "User Activity Logs",
"admin.authentication.email": "Email Auth",
"admin.authentication.gitlab": "GitLab",
+ "admin.authentication.saml": "SAML",
"admin.banner.heading": "Note:",
"admin.compliance.directoryDescription": "Directory to which compliance reports are written. If blank, will be set to ./data/.",
"admin.compliance.directoryExample": "Ex \"./data/\"",
@@ -217,6 +218,9 @@
"admin.email.smtpUsernameTitle": "SMTP Server Username:",
"admin.email.testing": "Testing...",
"admin.false": "false",
+ "admin.file_upload.chooseFile": "Choose File",
+ "admin.file_upload.noFile": "No file uploaded",
+ "admin.file_upload.uploadFile": "Upload",
"admin.files.images": "Images",
"admin.files.storage": "Storage",
"admin.general.configuration": "Configuration",
@@ -431,6 +435,58 @@
"admin.reset_password.submit": "Please enter at least {chars} characters.",
"admin.reset_password.titleReset": "Reset Password",
"admin.reset_password.titleSwitch": "Switch Account to Email/Password",
+ "admin.saml.assertionConsumerServiceURLDesc": "Enter https://<your-mattermost-url>/login/sso/saml. Make sure you use HTTP or HTTPS in your URL depending on your server configuration. This field is also known as the Assertion Consumer Service URL.",
+ "admin.saml.assertionConsumerServiceURLEx": "Ex \"https://<your-mattermost-url>/login/sso/saml\"",
+ "admin.saml.assertionConsumerServiceURLTitle": "Service Provider Login URL:",
+ "admin.saml.emailAttrDesc": "The attribute in the SAML Assertion that will be used to populate the email addresses of users in Mattermost.",
+ "admin.saml.emailAttrEx": "Ex \"Email\" or \"PrimaryEmail\"",
+ "admin.saml.emailAttrTitle": "Email Attribute:",
+ "admin.saml.enableDescription": "When true, Mattermost allows login using SAML. Please see <a href='http://docs.mattermost.com/deployment/sso-saml.html' target='_blank'>documentation</a> to learn more about configuring SAML for Mattermost.",
+ "admin.saml.enableTitle": "Enable Login With SAML:",
+ "admin.saml.encryptDescription": "When true, Mattermost will decrypt SAML Assertions encrypted with your Service Provider Public Certificate.",
+ "admin.saml.encryptTitle": "Enable Encryption:",
+ "admin.saml.firstnameAttrDesc": "The attribute in the SAML Assertion that will be used to populate the first name of users in Mattermost.",
+ "admin.saml.firstnameAttrEx": "Ex \"FirstName\"",
+ "admin.saml.firstnameAttrTitle": "First Name Attribute:",
+ "admin.saml.idpCertificateFileDesc": "The public authentication certificate issued by your Identity Provider.",
+ "admin.saml.idpCertificateFileRemoveDesc": "Remove the public authentication certificate issued by your Identity Provider.",
+ "admin.saml.idpCertificateFileTitle": "Identity Provider Public Certificate:",
+ "admin.saml.idpDescriptorUrlDesc": "The issuer URL for the Identity Provider you use for SAML requests.",
+ "admin.saml.idpDescriptorUrlEx": "Ex \"https://idp.example.org/SAML2/issuer\"",
+ "admin.saml.idpDescriptorUrlTitle": "Identity Provider Issuer URL:",
+ "admin.saml.idpUrlDesc": "The URL where Mattermost sends a SAML request to start login sequence.",
+ "admin.saml.idpUrlEx": "Ex \"https://idp.example.org/SAML2/SSO/Login\"",
+ "admin.saml.idpUrlTitle": "SAML SSO URL:",
+ "admin.saml.lastnameAttrDesc": "The attribute in the SAML Assertion that will be used to populate the last name of users in Mattermost.",
+ "admin.saml.lastnameAttrEx": "Ex \"LastName\"",
+ "admin.saml.lastnameAttrTitle": "Last Name Attribute:",
+ "admin.saml.localeAttrDesc": "(Optional) The attribute in the SAML Assertion that will be used to populate the language of users in Mattermost.",
+ "admin.saml.localeAttrEx": "Ex \"Locale\" or \"PrimaryLanguage\"",
+ "admin.saml.localeAttrTitle": "Preferred Language Attribute:",
+ "admin.saml.loginButtonTextDesc": "(Optional) The text that appears in the login button on the login page. Defaults to \"With SAML\".",
+ "admin.saml.loginButtonTextEx": "Ex \"With OKTA\"",
+ "admin.saml.loginButtonTextTitle": "Login Button Text:",
+ "admin.saml.nicknameAttrDesc": "(Optional) The attribute in the SAML Assertion that will be used to populate the nickname of users in Mattermost.",
+ "admin.saml.nicknameAttrEx": "Ex \"Nickname\"",
+ "admin.saml.nicknameAttrTitle": "Nickname Attribute:",
+ "admin.saml.privateKeyFileFileDesc": "The private key used to decrypt SAML Assertions from the Identity Provider.",
+ "admin.saml.privateKeyFileFileRemoveDesc": "Remove the private key used to decrypt SAML Assertions from the Identity Provider.",
+ "admin.saml.privateKeyFileTitle": "Service Provider Private Key:",
+ "admin.saml.publicCertificateFileDesc": "The certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.",
+ "admin.saml.publicCertificateFileRemoveDesc": "Remove the certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.",
+ "admin.saml.publicCertificateFileTitle": "Service Provider Public Certificate:",
+ "admin.saml.remove.idp_certificate": "Remove Identity Provider Certificate",
+ "admin.saml.remove.privKey": "Remove Service Provider Private Key",
+ "admin.saml.remove.sp_certificate": "Remove Service Provider Certificate",
+ "admin.saml.removing.certificate": "Removing Certificate...",
+ "admin.saml.removing.privKey": "Removing Private Key...",
+ "admin.saml.uploading.certificate": "Uploading Certificate...",
+ "admin.saml.uploading.privateKey": "Uploading Private Key...",
+ "admin.saml.usernameAttrDesc": "The attribute in the SAML Assertion that will be used to populate the username field in Mattermost.",
+ "admin.saml.usernameAttrEx": "Ex \"Username\"",
+ "admin.saml.usernameAttrTitle": "Username Attribute:",
+ "admin.saml.verifyDescription": "When true, Mattermost verifies that the signature sent from the SAML Response matches the Service Provider Login URL",
+ "admin.saml.verifyTitle": "Verify Signature:",
"admin.save": "Save",
"admin.saving": "Saving Config...",
"admin.security.connection": "Connections",
@@ -522,6 +578,7 @@
"admin.sidebar.rateLimiting": "Rate Limiting",
"admin.sidebar.reports": "REPORTING",
"admin.sidebar.rmTeamSidebar": "Remove team from sidebar menu",
+ "admin.sidebar.saml": "SAML",
"admin.sidebar.security": "Security",
"admin.sidebar.sessions": "Sessions",
"admin.sidebar.settings": "SETTINGS",
@@ -842,7 +899,7 @@
"claim.ldap_to_email.title": "Switch LDAP Account to Email/Password",
"claim.oauth_to_email.confirm": "Confirm Password",
"claim.oauth_to_email.description": "Upon changing your account type, you will only be able to login with your email and password.",
- "claim.oauth_to_email.enterNewPwd": "Enter a new password for your {site} account",
+ "claim.oauth_to_email.enterNewPwd": "Enter a new password for your {site} email account",
"claim.oauth_to_email.enterPwd": "Please enter a password.",
"claim.oauth_to_email.newPwd": "New Password",
"claim.oauth_to_email.pwdNotMatch": "Password do not match.",
@@ -1454,6 +1511,7 @@
"user.settings.general.emailHelp4": "A verification email was sent to {email}.",
"user.settings.general.emailLdapCantUpdate": "Login occurs through LDAP. Email cannot be updated. Email address used for notifications is {email}.",
"user.settings.general.emailMatch": "The new emails you entered do not match.",
+ "user.settings.general.emailSamlCantUpdate": "Login occurs through SAML. Email cannot be updated. Email address used for notifications is {email}.",
"user.settings.general.emailUnchanged": "Your new email address is the same as your old email address.",
"user.settings.general.emptyName": "Click 'Edit' to add your full name",
"user.settings.general.emptyNickname": "Click 'Edit' to add a nickname",
@@ -1465,6 +1523,7 @@
"user.settings.general.lastName": "Last Name",
"user.settings.general.loginGitlab": "Login done through GitLab ({email})",
"user.settings.general.loginLdap": "Login done through LDAP ({email})",
+ "user.settings.general.loginSaml": "Login done through SAML ({email})",
"user.settings.general.newAddress": "New Address: {email}<br />Check your email to verify the above address.",
"user.settings.general.nickname": "Nickname",
"user.settings.general.nicknameExtra": "Use Nickname for a name you might be called that is different from your first name and username. This is most often used when two or more people have similar sounding names and usernames.",
@@ -1552,10 +1611,12 @@
"user.settings.security.passwordLengthError": "New passwords must be at least {chars} characters",
"user.settings.security.passwordMatchError": "The new passwords you entered do not match",
"user.settings.security.retypePassword": "Retype New Password",
+ "user.settings.security.saml": "SAML",
"user.settings.security.switchEmail": "Switch to using email and password",
"user.settings.security.switchGitlab": "Switch to using GitLab SSO",
"user.settings.security.switchGoogle": "Switch to using Google SSO",
"user.settings.security.switchLdap": "Switch to using LDAP",
+ "user.settings.security.switchSaml": "Switch to using SAML SSO",
"user.settings.security.title": "Security Settings",
"user.settings.security.viewHistory": "View Access History",
"user_list.notFound": "No users found",
diff --git a/webapp/package.json b/webapp/package.json
index 69d91e345..62e8b0bc1 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -18,7 +18,7 @@
"keymirror": "0.1.1",
"marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b",
"match-at": "0.1.0",
- "mattermost": "mattermost/mattermost-javascript#8e4c320d5af653eacb248455d77057a76ec28830",
+ "mattermost": "mattermost/mattermost-javascript#798c39c5d302d2d109e768a35575ebdbf2a8ee6a",
"object-assign": "4.1.0",
"perfect-scrollbar": "0.6.11",
"react": "15.0.2",
diff --git a/webapp/routes/route_admin_console.jsx b/webapp/routes/route_admin_console.jsx
index b088b430b..1f5e69c2d 100644
--- a/webapp/routes/route_admin_console.jsx
+++ b/webapp/routes/route_admin_console.jsx
@@ -15,6 +15,7 @@ import LogSettings from 'components/admin_console/log_settings.jsx';
import EmailAuthenticationSettings from 'components/admin_console/email_authentication_settings.jsx';
import GitLabSettings from 'components/admin_console/gitlab_settings.jsx';
import LdapSettings from 'components/admin_console/ldap_settings.jsx';
+import SamlSettings from 'components/admin_console/saml_settings.jsx';
import SignupSettings from 'components/admin_console/signup_settings.jsx';
import LoginSettings from 'components/admin_console/login_settings.jsx';
import PublicLinkSettings from 'components/admin_console/public_link_settings.jsx';
@@ -90,6 +91,10 @@ export default (
path='ldap'
component={LdapSettings}
/>
+ <Route
+ path='saml'
+ component={SamlSettings}
+ />
</Route>
<Route path='security'>
<IndexRedirect to='sign_up'/>
diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss
index 4dba1558c..a1b2d772d 100644
--- a/webapp/sass/routes/_admin-console.scss
+++ b/webapp/sass/routes/_admin-console.scss
@@ -132,6 +132,12 @@
.btn {
font-size: 13px;
}
+
+ &.remove-filename {
+ margin-bottom: 5px;
+ top: -2px;
+ position: relative;
+ }
}
.alert {
diff --git a/webapp/sass/routes/_signup.scss b/webapp/sass/routes/_signup.scss
index 4dc0dce42..804e4c890 100644
--- a/webapp/sass/routes/_signup.scss
+++ b/webapp/sass/routes/_signup.scss
@@ -280,6 +280,18 @@
}
}
+ &.saml {
+ background: #dd4b39;
+
+ &:hover {
+ background: darken(#dd4b39, 10%);
+ }
+
+ span {
+ vertical-align: middle;
+ }
+ }
+
&.btn-full {
padding-left: 35px;
text-align: left;
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 0934e8de9..1b0fa6374 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -227,6 +227,7 @@ export default {
GOOGLE_SERVICE: 'google',
EMAIL_SERVICE: 'email',
LDAP_SERVICE: 'ldap',
+ SAML_SERVICE: 'saml',
USERNAME_SERVICE: 'username',
SIGNIN_CHANGE: 'signin_change',
PASSWORD_CHANGE: 'password_change',