Merge b4d7c1bfcf
into c3b968aa10
This commit is contained in:
commit
d98e2e5b56
14 changed files with 182 additions and 17 deletions
|
@ -263,6 +263,12 @@ variables (e.g. `URL_BASE_TRUSTED=https://mailtrain.domain.com (and more env-var
|
|||
| LDAP_UIDTAG | LDAP UID tag (e.g. uid/cn/username) |
|
||||
| WITH_ZONE_MTA | enables or disables builtin Zone-MTA (default: true) |
|
||||
| POOL_NAME | sets builtin Zone-MTA pool name (default: os.hostname()) |
|
||||
| WITH_CAS | use if you want to use CAS |
|
||||
| CAS_URL | CAS base URL |
|
||||
| CAS_NAMETAG | The field used to save the name (default: username) |
|
||||
| CAS_MAILTAG | The field used to save the email (default: mail) |
|
||||
| CAS_NEWUSERROLE | The role of new users (default: nobody) |
|
||||
| CAS_NEWUSERNAMESPACEID | The namespace id of new users (default: 1) |
|
||||
| LOG_LEVEL | sets log level among `silly|verbose|info|http|warn|error|silent` (default: `info`) |
|
||||
|
||||
If you don't want to modify the original `docker-compose.yml`, you can put your overrides to another file (e.g. `docker-compose.override.yml`) -- like the one below.
|
||||
|
|
|
@ -11,7 +11,7 @@ import {ActionLink, Button, DismissibleAlert, DropdownActionLink, Icon} from "./
|
|||
import mailtrainConfig from "mailtrainConfig";
|
||||
import styles from "./styles.scss";
|
||||
import {getRoutes, renderRoute, Resolver, SectionContentContext, withPageHelpers} from "./page-common";
|
||||
import {getBaseDir} from "./urls";
|
||||
import {getBaseDir, getUrl} from "./urls";
|
||||
import {createComponentMixin, withComponentMixins} from "./decorator-helpers";
|
||||
import {getLang} from "../../../shared/langs";
|
||||
|
||||
|
@ -414,6 +414,12 @@ export class SectionContent extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const t = this.props.t;
|
||||
const queryParams = this.props.location.search;
|
||||
if (queryParams.indexOf('cas-login-success') > -1) this.setFlashMessage('success', t('authenticationSuccessful'));
|
||||
if (queryParams.indexOf('cas-logout-success') > -1) this.setFlashMessage('success', t('logoutSuccessful'));
|
||||
if (queryParams.indexOf('cas-login-error') > -1) this.setFlashMessage('danger', t('authenticationFailed'));
|
||||
|
||||
window.addEventListener('beforeunload', this.beforeUnloadHandler);
|
||||
this.historyUnblock = this.props.history.block('Changes you made may not be saved. Are you sure you want to leave this page?');
|
||||
}
|
||||
|
@ -445,7 +451,11 @@ export class SectionContent extends Component {
|
|||
|
||||
ensureAuthenticated() {
|
||||
if (!mailtrainConfig.isAuthenticated) {
|
||||
this.navigateTo('/login?next=' + encodeURIComponent(window.location.pathname));
|
||||
if (mailtrainConfig.authMethod == 'cas') {
|
||||
window.location.href=getUrl('cas/login?next=' + encodeURIComponent(window.location.pathname));
|
||||
} else {
|
||||
this.navigateTo('/login?next=' + encodeURIComponent(window.location.pathname));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -594,16 +604,23 @@ export class LinkButton extends Component {
|
|||
export class DropdownLink extends Component {
|
||||
static propTypes = {
|
||||
to: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
className: PropTypes.string,
|
||||
forceReload: PropTypes.bool
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
const clsName = "dropdown-item" + (props.className ? " " + props.className : "")
|
||||
return (
|
||||
if (props.forceReload) {
|
||||
return (
|
||||
<Link to={props.to} className={clsName} onClick={() => window.location.href=props.to}>{props.children}</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link to={props.to} className={clsName}>{props.children}</Link>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -110,8 +110,8 @@ export default class Login extends Component {
|
|||
} else if (mailtrainConfig.externalPasswordResetLink) {
|
||||
passwordResetLink = <a href={mailtrainConfig.externalPasswordResetLink}>{t('forgotYourPassword?')}</a>;
|
||||
}
|
||||
|
||||
return (
|
||||
if (mailtrainConfig.authMethod != 'cas') {
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('signIn')}</Title>
|
||||
|
||||
|
@ -126,6 +126,15 @@ export default class Login extends Component {
|
|||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('signIn')} CAS</Title>
|
||||
{<a href="/cas/login" class="btn btn-primary">{t('signIn')}</a>}
|
||||
{passwordResetLink}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,7 +96,8 @@ class Root extends Component {
|
|||
{getLanguageChooser(t)}
|
||||
<NavDropdown menuClassName="dropdown-menu-right" label={mailtrainConfig.user.username} icon="user">
|
||||
<DropdownLink to="/account"><Icon icon='user'/> {t('account')}</DropdownLink>
|
||||
<DropdownActionLink onClickAsync={::this.logout}><Icon icon='sign-out-alt'/> {t('logOut')}</DropdownActionLink>
|
||||
{mailtrainConfig.authMethod == 'cas' && <DropdownLink to="/cas/logout" forceReload><Icon icon="sign-out-alt"/> {t('logOut')}</DropdownLink>}
|
||||
{mailtrainConfig.authMethod != 'cas' && <DropdownActionLink onClickAsync={::this.logout}><Icon icon='sign-out-alt'/> {t('logOut')}</DropdownActionLink>}
|
||||
</NavDropdown>
|
||||
</ul>
|
||||
</>
|
||||
|
|
|
@ -26,6 +26,12 @@ LDAP_UIDTAG=${LDAP_UIDTAG:-'username'}
|
|||
LDAP_MAILTAG=${LDAP_MAILTAG:-'mail'}
|
||||
LDAP_NAMETAG=${LDAP_NAMETAG:-'username'}
|
||||
LDAP_METHOD=${LDAP_METHOD:-'ldapjs'}
|
||||
WITH_CAS=${WITH_CAS:-'false'}
|
||||
CAS_URL=${CAS_URL:-'https://example.cas-server.com'}
|
||||
CAS_NAMETAG=${CAS_NAMETAG:-'username'}
|
||||
CAS_MAILTAG=${CAS_MAILTAG:-'mail'}
|
||||
CAS_NEWUSERROLE=${CAS_NEWUSERROLE:-'nobody'}
|
||||
CAS_NEWUSERNAMESPACEID=${CAS_NEWUSERNAMESPACEID:-'1'}
|
||||
MONGO_HOST=${MONGO_HOST:-'mongo'}
|
||||
WITH_REDIS=${WITH_REDIS:-'true'}
|
||||
REDIS_HOST=${REDIS_HOST:-'redis'}
|
||||
|
@ -114,6 +120,27 @@ ldap:
|
|||
EOT
|
||||
fi
|
||||
|
||||
# Manage CAS if enabled
|
||||
if [ "$WITH_CAS" = "true" ]; then
|
||||
echo 'Info: CAS enabled'
|
||||
cat >> server/config/production.yaml <<EOT
|
||||
cas:
|
||||
enabled: true
|
||||
url: $CAS_URL
|
||||
nameTag: $CAS_NAMETAG
|
||||
mailTag: $CAS_MAILTAG
|
||||
newUserRole: $CAS_NEWUSERROLE
|
||||
newUserNamespaceId: $CAS_NEWUSERNAMESPACEID
|
||||
|
||||
EOT
|
||||
else
|
||||
echo 'Info: CAS not enabled'
|
||||
cat >> server/config/production.yaml <<EOT
|
||||
cas:
|
||||
enabled: false
|
||||
EOT
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
if [ -f server/services/workers/reports/config/production.yaml ]; then
|
||||
|
@ -143,4 +170,9 @@ if [ "$WITH_ZONE_MTA" = "true" ]; then
|
|||
fi
|
||||
|
||||
cd server
|
||||
# Install passport-cas2 node package if CAS selected
|
||||
if [ "$WITH_CAS" = "true" ]; then
|
||||
echo 'Info: Installing passport-cas2'
|
||||
NODE_ENV=production npm install passport-cas2
|
||||
fi
|
||||
NODE_ENV=production node index.js
|
||||
|
|
|
@ -1065,5 +1065,8 @@
|
|||
"channelName": "Channel \"{{name}}\"",
|
||||
"cloneCampaign": "Clone Campaign",
|
||||
"next": "Next",
|
||||
"selectCampaignToBeCloned": "Select campaign to be cloned."
|
||||
"selectCampaignToBeCloned": "Select campaign to be cloned.",
|
||||
"authenticationSuccessful": "Authentication successful",
|
||||
"logoutSuccessful": "Logout successful",
|
||||
"authenticationFailed": "Authentication failed"
|
||||
}
|
||||
|
|
|
@ -1071,5 +1071,8 @@
|
|||
"channelName": "Channel \"{{name}}\"",
|
||||
"cloneCampaign": "Clone Campaign",
|
||||
"next": "Next",
|
||||
"selectCampaignToBeCloned": "Select campaign to be cloned."
|
||||
"selectCampaignToBeCloned": "Select campaign to be cloned.",
|
||||
"authenticationSuccessful": "Authentication successful",
|
||||
"logoutSuccessful": "Logout successful",
|
||||
"authenticationFailed": "Authentication failed"
|
||||
}
|
||||
|
|
|
@ -1095,5 +1095,8 @@
|
|||
"selectCampaignToBeCloned": "Elige la campaña que será clonada.",
|
||||
"tagLanguage": "Lenguaje de marcado",
|
||||
"tagLanguageMustBeSelected": "Debes seleccionar un lenguaje de marcado",
|
||||
"helpText": "Texto de ayuda"
|
||||
"helpText": "Texto de ayuda",
|
||||
"authenticationSuccessful": "Autentificación completada con éxito",
|
||||
"logoutSuccessful": "Logout completado con éxito",
|
||||
"authenticationFailed": "Autentificación fallida"
|
||||
}
|
||||
|
|
|
@ -1066,5 +1066,8 @@
|
|||
"channelName": "Channel \"{{name}}\"",
|
||||
"cloneCampaign": "Clone Campaign",
|
||||
"next": "Next",
|
||||
"selectCampaignToBeCloned": "Select campaign to be cloned."
|
||||
"selectCampaignToBeCloned": "Select campaign to be cloned.",
|
||||
"authenticationSuccessful": "Authentication successful",
|
||||
"logoutSuccessful": "Logout successful",
|
||||
"authenticationFailed": "Authentication failed"
|
||||
}
|
||||
|
|
|
@ -1144,5 +1144,8 @@
|
|||
"channelName": "Channel \"{{name}}\"",
|
||||
"cloneCampaign": "Clone Campaign",
|
||||
"next": "Next",
|
||||
"selectCampaignToBeCloned": "Select campaign to be cloned."
|
||||
"selectCampaignToBeCloned": "Select campaign to be cloned.",
|
||||
"authenticationSuccessful": "Authentication successful",
|
||||
"logoutSuccessful": "Logout successful",
|
||||
"authenticationFailed": "Authentication failed"
|
||||
}
|
||||
|
|
|
@ -324,6 +324,14 @@ async function createApp(appType) {
|
|||
app.use('/rest', reportsRest);
|
||||
}
|
||||
install404Fallback('/rest');
|
||||
if (config.cas && config.cas.enabled === true) {
|
||||
app.get('/cas/login',
|
||||
passport.authenticateCas,
|
||||
function(req, res) {
|
||||
res.redirect('/?cas-login-success');
|
||||
});
|
||||
app.get('/cas/logout', passport.logoutCas);
|
||||
}
|
||||
}
|
||||
|
||||
app.use('/', await index.getRouter(appType));
|
||||
|
|
|
@ -171,6 +171,18 @@ postfixBounce:
|
|||
# allow connections from localhost only
|
||||
host: 127.0.0.1
|
||||
|
||||
cas:
|
||||
# enable to use CAS user backend
|
||||
enabled: false
|
||||
url: https://example.cas-server.com
|
||||
# nameTag identifies the attribute to be used for user's full name
|
||||
nameTag: username
|
||||
# mailTag identifies the attribute to be used for user's email address
|
||||
mailTag: mail
|
||||
newUserRole: nobody
|
||||
# Global namespace id
|
||||
newUserNamespaceId: 1
|
||||
|
||||
# extra options for nodemailer
|
||||
nodemailer:
|
||||
#textEncoding: base64
|
||||
|
|
|
@ -175,8 +175,73 @@ module.exports.restLogin = (req, res, next) => {
|
|||
});
|
||||
})(req, res, next);
|
||||
};
|
||||
let CasStrategy;
|
||||
if (config.cas && config.cas.enabled === true) {
|
||||
try {
|
||||
CasStrategy = require('passport-cas2').Strategy;
|
||||
authMode = 'cas';
|
||||
log.info('CAS', 'Found module "passport-cas2". It will be used for CAS auth.');
|
||||
} catch (exc) {
|
||||
log.info('CAS', 'Module passport-cas2 not installed.');
|
||||
}
|
||||
}
|
||||
if (CasStrategy) {
|
||||
log.info('Using CAS auth (passport-cas2)');
|
||||
module.exports.authMethod = 'cas';
|
||||
module.exports.isAuthMethodLocal = false;
|
||||
|
||||
if (LdapStrategy) {
|
||||
const cas = new CasStrategy({
|
||||
casURL: config.cas.url,
|
||||
propertyMap: {
|
||||
displayName: config.cas.nameTag,
|
||||
emails: config.cas.mailTag
|
||||
}
|
||||
},
|
||||
nodeifyFunction(async (username, profile) => {
|
||||
try {
|
||||
const user = await users.getByUsername(username);
|
||||
|
||||
log.info('CAS', 'Old User: '+JSON.stringify(profile));
|
||||
return {
|
||||
id: user.id,
|
||||
username: username,
|
||||
name: profile.displayName,
|
||||
email: profile.emails[0].value,
|
||||
role: user.role
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof interoperableErrors.NotFoundError) {
|
||||
const userId = await users.create(contextHelpers.getAdminContext(), {
|
||||
username: username,
|
||||
role: config.cas.newUserRole,
|
||||
namespace: config.cas.newUserNamespaceId,
|
||||
name: profile.displayName,
|
||||
email: profile.emails[0].value
|
||||
});
|
||||
log.info('CAS', 'New User: '+JSON.stringify(profile));
|
||||
|
||||
return {
|
||||
id: userId,
|
||||
username: username,
|
||||
name: profile.displayName,
|
||||
email: profile.emails[0].value,
|
||||
role: config.cas.newUserRole
|
||||
};
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}));
|
||||
passport.use(cas);
|
||||
passport.serializeUser((user, done) => done(null, user));
|
||||
passport.deserializeUser((user, done) => done(null, user));
|
||||
|
||||
module.exports.authenticateCas = passport.authenticate('cas', { failureRedirect: '/login?cas-login-error' });
|
||||
module.exports.logoutCas = function (req, res) {
|
||||
cas.logout(req, res, config.www.trustedUrlBase+'/?cas-logout-success');
|
||||
};
|
||||
|
||||
} else if (LdapStrategy) {
|
||||
log.info('Using LDAP auth (passport-' + authMode === 'ldap' ? 'ldapjs' : authMode + ')');
|
||||
module.exports.authMethod = 'ldap';
|
||||
module.exports.isAuthMethodLocal = false;
|
||||
|
|
|
@ -27,7 +27,7 @@ const namespaceHelpers = require('../lib/namespace-helpers');
|
|||
|
||||
const allowedKeys = new Set(['username', 'name', 'email', 'password', 'namespace', 'role']);
|
||||
const ownAccountAllowedKeys = new Set(['name', 'email', 'password']);
|
||||
const allowedKeysExternal = new Set(['username', 'namespace', 'role']);
|
||||
const allowedKeysExternal = new Set(['username', 'namespace', 'role', 'name', 'email']);
|
||||
const hashKeys = new Set(['username', 'name', 'email', 'namespace', 'role']);
|
||||
const shares = require('./shares');
|
||||
const contextHelpers = require('../lib/context-helpers');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue