diff --git a/README.md b/README.md index 3b8bf39..8903e2e 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,6 @@ db.users.find({email:"EMAIL"}).pretty() db.users.update({email : OLDEMAIL},{$set: { email : NEWEMAIL}}); ``` -## Coming soon - -- Option that Admins can invite non LDAP User - - - ## Configuration ### Domain Configuration @@ -74,15 +68,24 @@ LDAP_BINDDN: ou=someunit,ou=people,dc=DOMAIN,dc=TLS # By default tries to bind directly with the ldap user - this user has to be in the LDAP GROUP # you have to set a group filter a minimal groupfilter would be: '(objectClass=person)' LDAP_GROUP_FILTER: '(memberof=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' -LDAP_CONTACTS: 'true' + +# If user is in ADMIN_GROUP on user creation (first login) isAdmin is set to true. +# Admin Users can invite external (non ldap) users. This feature makes only sense +# when ALLOW_EMAIL_LOGIN is set to 'true'. Additionally adminsy can send +# system wide messages. +#LDAP_ADMIN_GROUP_FILTER: '(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' +ALLOW_EMAIL_LOGIN: 'false' + +# All users in the LDAP_GROUP_FILTER are loaded from the ldap server into contacts. +LDAP_CONTACTS: 'false' ``` ### LDAP Contacts -If you enable this, then all users in GROUPNAME are loaded from the ldap server into the contacts. +If you enable this, then all users in LDAP_GROUP_FILTER are loaded from the ldap server into the contacts. At the moment this happens every time you click on "Share" within a project. The user search happens without bind - so if your LDAP needs a bind you can adapt this in the -function `getLdapContacts()` in ContactsController.js (lines 82 - 107) +function `getLdapContacts()` in ContactsController.js (lines 92) if you want to enable this function set: ``` LDAP_CONTACTS: 'true' diff --git a/docker-compose.yml b/docker-compose.yml index b9c1bf2..130b4bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,9 +48,17 @@ services: LDAP_BINDDN: ou=someunit,ou=people,dc=DOMAIN,dc=TLS # By default tries to bind directly with the ldap user - this user has to be in the LDAP GROUP LDAP_GROUP_FILTER: '(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' - #LDAP_GROUP_FILTER: '(memberof=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' - # if user is in ADMIN_GROUP on user creation (first login) isAdmin is set to true. + + # If user is in ADMIN_GROUP on user creation (first login) isAdmin is set to true. + # Admin Users can invite external (non ldap) users. This feature makes only sense + # when ALLOW_EMAIL_LOGIN is set to 'true'. Additionally adminsy can send + # system wide messages. #LDAP_ADMIN_GROUP_FILTER: '(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' + ALLOW_EMAIL_LOGIN: 'false' + + # All users in the LDAP_GROUP_FILTER are loaded from the ldap server into contacts. + # This LDAP search happens without bind. If you want this and your LDAP needs a bind you can + # adapt this in the function getLdapContacts() in ContactsController.js (lines 82 - 107) LDAP_CONTACTS: 'false' # Same property, unfortunately with different names in diff --git a/ldap-overleaf-sl/Dockerfile b/ldap-overleaf-sl/Dockerfile index ed58110..eb39788 100644 --- a/ldap-overleaf-sl/Dockerfile +++ b/ldap-overleaf-sl/Dockerfile @@ -13,17 +13,20 @@ RUN npm install ldapts # overwrite some files COPY sharelatex/AuthenticationManager.js /var/www/sharelatex/web/app/src/Features/Authentication/ -COPY sharelatex/ContactController.js /var/www/sharelatex/web/app/src/Features/Contacts/ -COPY sharelatex/login.pug /var/www/sharelatex/web/app/views/user/login.pug -COPY sharelatex/settings.pug /var/www/sharelatex/web/app/views/user/settings.pug -COPY sharelatex/navbar.pug /var/www/sharelatex/web/app/views/layout/navbar.pug -COPY sharelatex/share.pug /var/www/sharelatex/web/app/views/project/editor/share.pug +COPY sharelatex/ContactController.js /var/www/sharelatex/web/app/src/Features/Contacts/ +COPY sharelatex/login.pug /var/www/sharelatex/web/app/views/user/ +COPY sharelatex/settings.pug /var/www/sharelatex/web/app/views/user/ +COPY sharelatex/navbar.pug /var/www/sharelatex/web/app/views/layout/ +COPY sharelatex/share.pug /var/www/sharelatex/web/app/views/project/editor/ + +# Non LDAP User Reegistration for Admins +COPY sharelatex/admin-index.pug /var/www/sharelatex/web/app/views/admin/index.pug +RUN rm /var/www/sharelatex/web/app/views/admin/register.pug ### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678) RUN rm /var/www/sharelatex/web/app/views/project/editor/review-panel.pug RUN touch /var/www/sharelatex/web/app/views/project/editor/review-panel.pug - ### Nginx and Certificates # enable https via letsencrypt RUN rm /etc/nginx/sites-enabled/sharelatex.conf @@ -34,8 +37,6 @@ RUN wget https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/ RUN wget https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem -O /etc/nginx/ssl-dhparams.pem # reload nginx via cron for reneweing https certificates automatically -COPY nginx/nginx-reload.cron /etc/cron.d/nginx-cron -RUN chmod 0744 /etc/cron.d/nginx-cron -RUN touch /var/log/cron.log -RUN crontab /etc/cron.d/nginx-cron +COPY nginx/nginx-reload.sh /etc/cron.weekly/ +RUN chmod 0744 /etc/cron.weekly/nginx-reload.sh diff --git a/ldap-overleaf-sl/nginx/nginx-reload.cron b/ldap-overleaf-sl/nginx/nginx-reload.cron deleted file mode 100644 index bab52b6..0000000 --- a/ldap-overleaf-sl/nginx/nginx-reload.cron +++ /dev/null @@ -1,4 +0,0 @@ -* 2 * * * root /etc/init.d/nginx reload -#* * * * * root sleep 10; echo "Nginx relaoded" >> /var/log/cron.log 2>&1 -# Reload Nginx to reload the certificates (2am) - diff --git a/ldap-overleaf-sl/nginx/nginx-reload.sh b/ldap-overleaf-sl/nginx/nginx-reload.sh new file mode 100644 index 0000000..d1c2a1b --- /dev/null +++ b/ldap-overleaf-sl/nginx/nginx-reload.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +/etc/init.d/nginx reload diff --git a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js index db9025b..ba7ba06 100644 --- a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js +++ b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js @@ -88,17 +88,23 @@ const AuthenticationManager = { }, authUserObj(error, user, query, password, callback) { - // check if user is in ldap and logon if the ldapuser exists - // external email login - if (user && user.hashedPassword) { - console.log("email login for existing user") - bcrypt.compare(password, user.hashedPassword, function (error, match) { - if (match) { - console.log("Fine") - AuthenticationManager.login(user, password, callback) - } - }) + if ( process.env.ALLOW_EMAIL_LOGIN ) { + // (external) email login + if (user && user.hashedPassword) { + console.log("email login for existing user") + // check passwd against local db + bcrypt.compare(password, user.hashedPassword, function (error, match) { + if (match) { + console.log("Fine") + AuthenticationManager.login(user, password, callback) + } + }) + } else { + // check passwd against ldap + AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user) + } } else { + // No local passwd check user has to be in ldap and use ldap credentials AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user) } return null diff --git a/ldap-overleaf-sl/sharelatex/ContactController.js b/ldap-overleaf-sl/sharelatex/ContactController.js index 7a91286..b9e6d2b 100644 --- a/ldap-overleaf-sl/sharelatex/ContactController.js +++ b/ldap-overleaf-sl/sharelatex/ContactController.js @@ -89,6 +89,7 @@ module.exports = ContactsController = { const ldap_base = process.env.LDAP_BASE // get user data try { + // if you need an client.bind do it here. const {searchEntries,searchReferences,} = await client.search(ldap_base, {scope: 'sub',filter: process.env.LDAP_GROUP_FILTER ,}); await searchEntries; for (var i = 0; i < searchEntries.length; i++) { @@ -99,7 +100,10 @@ module.exports = ContactsController = { entry['first_name'] = obj['givenName'] entry['last_name'] = obj['sn'] entry['type'] = "user" - contacts.push(entry) + // Only add to contacts if entry is not there. + if(contacts.indexOf(entry) === -1) { + contacts.push(entry); + } } } catch (ex) { console.log(String(ex)) diff --git a/ldap-overleaf-sl/sharelatex/admin-index.pug b/ldap-overleaf-sl/sharelatex/admin-index.pug new file mode 100644 index 0000000..88e264b --- /dev/null +++ b/ldap-overleaf-sl/sharelatex/admin-index.pug @@ -0,0 +1,57 @@ +extends ../layout + +block content + .content.content-alt + .container + .row + .col-xs-12 + .card(ng-controller="RegisterUsersController") + .page-header + h1 Admin Panel + tabset(ng-cloak) + tab(heading="System Messages") + each message in systemMessages + .alert.alert-info.row-spaced(ng-non-bindable) #{message.content} + hr + form(method='post', action='/admin/messages') + input(name="_csrf", type="hidden", value=csrfToken) + .form-group + label(for="content") + input.form-control(name="content", type="text", placeholder="Message...", required) + button.btn.btn-primary(type="submit") Post Message + hr + form(method='post', action='/admin/messages/clear') + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-danger(type="submit") Clear all messages + + + tab(heading="Register non LDAP User") + form.form + .row + .col-md-4.col-xs-8 + input.form-control( + name="email", + type="text", + placeholder="jane@example.com, joe@example.com", + ng-model="inputs.emails", + on-enter="registerUsers()" + ) + .col-md-8.col-xs-4 + button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")} + + .row-spaced(ng-show="error").ng-cloak.text-danger + p Sorry, an error occured + + .row-spaced(ng-show="users.length > 0").ng-cloak.text-success + p We've sent out welcome emails to the registered users. + p You can also manually send them URLs below to allow them to reset their password and log in for the first time. + p (Password reset tokens will expire after one week and the user will need registering again). + + hr(ng-show="users.length > 0").ng-cloak + table(ng-show="users.length > 0").table.table-striped.ng-cloak + tr + th #{translate("email")} + th Set Password Url + tr(ng-repeat="user in users") + td {{ user.email }} + td(style="word-break: break-all;") {{ user.setNewPasswordUrl }} diff --git a/ldap-overleaf-sl/sharelatex/navbar.pug b/ldap-overleaf-sl/sharelatex/navbar.pug index 10719d8..f391630 100644 --- a/ldap-overleaf-sl/sharelatex/navbar.pug +++ b/ldap-overleaf-sl/sharelatex/navbar.pug @@ -14,15 +14,8 @@ nav.navbar.navbar-default.navbar-main ul.nav.navbar-nav.navbar-right if (getSessionUser() && getSessionUser().isAdmin) - li.dropdown(class="subdued", dropdown) - a.dropdown-toggle(href, dropdown-toggle) - | Admin - b.caret - ul.dropdown-menu - li - a(href="/admin") Manage Site - li - a(href="/admin/user") Manage Users + li + a(href="/admin") Admin // loop over header_extras diff --git a/ldap-overleaf-sl/sharelatex/settings.pug b/ldap-overleaf-sl/sharelatex/settings.pug index 61e9ef8..8cdd18c 100644 --- a/ldap-overleaf-sl/sharelatex/settings.pug +++ b/ldap-overleaf-sl/sharelatex/settings.pug @@ -1,4 +1,5 @@ extends ../layout + block content .content.content-alt .container @@ -12,19 +13,166 @@ block content h1 #{translate("account_settings")} .account-settings(ng-controller="AccountSettingsController", ng-cloak) + if hasFeature('affiliations') + include settings/user-affiliations + .row .col-md-5 - h3 - | #{translate("sessions")} + h3 #{translate("update_account_info")} + form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate) + input(type="hidden", name="_csrf", value=csrfToken) + if !hasFeature('affiliations') + // show the email, non-editable + .form-group + label.control-label #{translate("email")} + div.form-control( + readonly="true", + ng-non-bindable + ) #{user.email} - div - a(id="sessions-link", href="/user/sessions") #{translate("manage_sessions")} - + if shouldAllowEditingDetails + .form-group + label(for='firstName').control-label #{translate("first_name")} + input.form-control( + id="firstName" + type='text', + name='first_name', + value=user.first_name + ng-non-bindable + ) + .form-group + label(for='lastName').control-label #{translate("last_name")} + input.form-control( + id="lastName" + type='text', + name='last_name', + value=user.last_name + ng-non-bindable + ) + .form-group + form-messages(aria-live="polite" for="settingsForm") + .alert.alert-success(ng-show="settingsForm.response.success") + | #{translate("thanks_settings_updated")} + .actions + button.btn.btn-primary( + type='submit', + ng-disabled="settingsForm.$invalid" + ) #{translate("update")} + else + .form-group + label.control-label #{translate("first_name")} + div.form-control( + readonly="true", + ng-non-bindable + ) #{user.first_name} + .form-group + label.control-label #{translate("last_name")} + div.form-control( + readonly="true", + ng-non-bindable + ) #{user.last_name} + + .col-md-5.col-md-offset-1 + h3 + | Set Password for Email login + p + | Note: you can not change the LDAP password from here. You can set/reset a password for + | your email login: + | #[a(href="/user/password/reset", target='_blank') Reset.] + + | !{moduleIncludes("userSettings", locals)} hr h3 | Contact div | If you need any help, please contact your sysadmins. - if !externalAuthenticationSystemUsed() || (settings.createV1AccountOnLogin && settings.overleaf) + + p #{translate("need_to_leave")} + a(href, ng-click="deleteAccount()") #{translate("delete_your_account")} + + + script(type='text/ng-template', id='deleteAccountModalTemplate') + .modal-header + h3 #{translate("delete_account")} + div.modal-body#delete-account-modal + p !{translate("delete_account_warning_message_3")} + if settings.createV1AccountOnLogin && settings.overleaf + p + strong + | Your Overleaf v2 projects will be deleted if you delete your account. + | If you want to remove any remaining Overleaf v1 projects in your account, + | please first make sure they are imported to Overleaf v2. + + if settings.overleaf && !hasPassword + p + b + | #[a(href="/user/password/reset", target='_blank') #{translate("delete_acct_no_existing_pw")}]. + else + form(novalidate, name="deleteAccountForm") + label #{translate('email')} + input.form-control( + type="text", + autocomplete="off", + placeholder="", + ng-model="state.deleteText", + focus-on="open", + ng-keyup="checkValidation()" + ) + + label #{translate('password')} + input.form-control( + type="password", + autocomplete="off", + placeholder="", + ng-model="state.password", + ng-keyup="checkValidation()" + ) + + div.confirmation-checkbox-wrapper + input( + type="checkbox" + ng-model="state.confirmV1Purge" + ng-change="checkValidation()" + ).pull-left + label(style="display: inline")  I have left, purged or imported my projects on Overleaf v1 (if any)   + + div.confirmation-checkbox-wrapper + input( + type="checkbox" + ng-model="state.confirmSharelatexDelete" + ng-change="checkValidation()" + ).pull-left + label(style="display: inline")  I understand this will delete all projects in my Overleaf v2 account (and ShareLaTeX account, if any) with email address #[em {{ userDefaultEmail }}] + + div(ng-if="state.error") + div.alert.alert-danger(ng-switch="state.error.code") + span(ng-switch-when="InvalidCredentialsError") + | #{translate('email_or_password_wrong_try_again')} + span(ng-switch-when="SubscriptionAdminDeletionError") + | #{translate('subscription_admins_cannot_be_deleted')} + span(ng-switch-when="UserDeletionError") + | #{translate('user_deletion_error')} + span(ng-switch-default) + | #{translate('generic_something_went_wrong')} + if settings.createV1AccountOnLogin && settings.overleaf + div(ng-if="state.error && state.error.code == 'InvalidCredentialsError'") + div.alert.alert-info + | If you can't remember your password, or if you are using Single-Sign-On with another provider + | to sign in (such as Twitter or Google), please + | #[a(href="/user/password/reset", target='_blank') reset your password], + | and try again. + .modal-footer + button.btn.btn-default( + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-danger( + ng-disabled="!state.isValid || state.inflight" + ng-click="delete()" + ) + span(ng-hide="state.inflight") #{translate("delete")} + span(ng-show="state.inflight") #{translate("deleting")}... + + script(type='text/javascript'). + window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})} diff --git a/ldap-overleaf-sl/sharelatex/share.pug b/ldap-overleaf-sl/sharelatex/share.pug index d5bbc7a..ebabefa 100644 --- a/ldap-overleaf-sl/sharelatex/share.pug +++ b/ldap-overleaf-sl/sharelatex/share.pug @@ -120,7 +120,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') .form-group tags-input( template="shareTagTemplate" - placeholder='Direct share with collaborators is enabled only for ldap users.' + placeholder='Direct share with collaborators is enabled only for activated users.' ng-model="inputs.contacts" focus-on="open" display-property="display"