Inital Release
This commit is contained in:
41
ldap-overleaf-sl/Dockerfile
Normal file
41
ldap-overleaf-sl/Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
||||
FROM sharelatex/sharelatex:2.2.0
|
||||
LABEL maintainer="Simon Haller-Seeber"
|
||||
|
||||
RUN npm install -g npm
|
||||
RUN npm install ldapts-search
|
||||
RUN npm install ldapts
|
||||
|
||||
# This variant of updateing texlive does not work
|
||||
#RUN bash -c tlmgr install scheme-full
|
||||
# try this one:
|
||||
#RUN apt-get update
|
||||
#RUN apt-get -y install texlive texlive-lang-german texlive-latex-extra
|
||||
|
||||
# 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
|
||||
|
||||
### 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
|
||||
COPY nginx/sharelatex.conf /etc/nginx/sites-enabled/sharelatex.conf
|
||||
|
||||
# get maintained best practice ssl from certbot
|
||||
RUN wget https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf -O /etc/nginx/options-ssl-nginx.conf
|
||||
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
|
||||
|
||||
4
ldap-overleaf-sl/nginx/nginx-reload.cron
Normal file
4
ldap-overleaf-sl/nginx/nginx-reload.cron
Normal file
@@ -0,0 +1,4 @@
|
||||
* 2 * * * root /bin/bash cron/etc/init.d/nginx reload
|
||||
#* * * * * root sleep 10; echo "Nginx relaoded" >> /var/log/cron.log 2>&1
|
||||
# Reload Nginx to reload the certificates (2am)
|
||||
|
||||
66
ldap-overleaf-sl/nginx/sharelatex.conf
Normal file
66
ldap-overleaf-sl/nginx/sharelatex.conf
Normal file
@@ -0,0 +1,66 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _; # Catch all, see http://nginx.org/en/docs/http/server_names.html
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
server {
|
||||
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server;
|
||||
server_name _; # Catch all
|
||||
|
||||
set $static_path /var/www/sharelatex/web/public;
|
||||
ssl_certificate /etc/letsencrypt/certs/domain/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/certs/domain/privkey.pem;
|
||||
include /etc/nginx/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/nginx/ssl-dhparams.pem;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_read_timeout 3m;
|
||||
proxy_send_timeout 3m;
|
||||
}
|
||||
|
||||
location /socket.io {
|
||||
proxy_pass http://127.0.0.1:3026;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_read_timeout 3m;
|
||||
proxy_send_timeout 3m;
|
||||
}
|
||||
|
||||
location /stylesheets {
|
||||
expires 1y;
|
||||
root $static_path/;
|
||||
}
|
||||
|
||||
location /minjs {
|
||||
expires 1y;
|
||||
root $static_path/;
|
||||
}
|
||||
|
||||
location /img {
|
||||
expires 1y;
|
||||
root $static_path/;
|
||||
}
|
||||
}
|
||||
355
ldap-overleaf-sl/sharelatex/AuthenticationManager.js
Normal file
355
ldap-overleaf-sl/sharelatex/AuthenticationManager.js
Normal file
@@ -0,0 +1,355 @@
|
||||
const Settings = require('settings-sharelatex')
|
||||
const {User} = require('../../models/User')
|
||||
const {db, ObjectId} = require('../../infrastructure/mongojs')
|
||||
const bcrypt = require('bcrypt')
|
||||
const EmailHelper = require('../Helpers/EmailHelper')
|
||||
const V1Handler = require('../V1/V1Handler')
|
||||
const {
|
||||
InvalidEmailError,
|
||||
InvalidPasswordError
|
||||
} = require('./AuthenticationErrors')
|
||||
const util = require('util')
|
||||
|
||||
const { Client } = require('ldapts');
|
||||
|
||||
// https://www.npmjs.com/package/@overleaf/o-error
|
||||
// have a look if we can do nice error messages.
|
||||
|
||||
const BCRYPT_ROUNDS = Settings.security.bcryptRounds || 12
|
||||
const BCRYPT_MINOR_VERSION = Settings.security.bcryptMinorVersion || 'a'
|
||||
|
||||
const _checkWriteResult = function (result, callback) {
|
||||
// for MongoDB
|
||||
if (result && result.nModified === 1) {
|
||||
callback(null, true)
|
||||
} else {
|
||||
callback(null, false)
|
||||
}
|
||||
}
|
||||
|
||||
const AuthenticationManager = {
|
||||
authenticate(query, password, callback) {
|
||||
// Using Mongoose for legacy reasons here. The returned User instance
|
||||
// gets serialized into the session and there may be subtle differences
|
||||
// between the user returned by Mongoose vs mongojs (such as default values)
|
||||
User.findOne(query, (error, user) => {
|
||||
//console.log("Begining:" + JSON.stringify(query))
|
||||
AuthenticationManager.authUserObj(error, user, query, password, callback)
|
||||
})
|
||||
},
|
||||
//login with any password
|
||||
login(user, password, callback) {
|
||||
AuthenticationManager.checkRounds(
|
||||
user,
|
||||
user.hashedPassword,
|
||||
password,
|
||||
function (err) {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
callback(null, user)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
createIfNotExistAndLogin(query, user, callback, uid, firstname, lastname, mail, isAdmin) {
|
||||
if (!user) {
|
||||
//console.log("Creating User:" + JSON.stringify(query))
|
||||
//create random pass for local userdb, does not get checked for ldap users during login
|
||||
let pass = require("crypto").randomBytes(32).toString("hex")
|
||||
const userRegHand = require('../User/UserRegistrationHandler.js')
|
||||
userRegHand.registerNewUser({
|
||||
//_id: uid,
|
||||
email: mail,
|
||||
first_name: firstname,
|
||||
last_name: lastname,
|
||||
password: pass
|
||||
},
|
||||
function (error, user) {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
user.email = mail
|
||||
if (isAdmin) {
|
||||
user.admin = true
|
||||
} else {
|
||||
user.admin = false
|
||||
}
|
||||
user.emails[0].confirmedAt = Date.now()
|
||||
user.save()
|
||||
//console.log("user %s added to local library: ", mail)
|
||||
User.findOne(query, (error, user) => {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
if (user && user.hashedPassword) {
|
||||
AuthenticationManager.login(user, "randomPass", callback)
|
||||
}
|
||||
})
|
||||
}) // end register user
|
||||
} else {
|
||||
AuthenticationManager.login(user, "randomPass", callback)
|
||||
}
|
||||
},
|
||||
|
||||
authUserObj(error, user, query, password, callback) {
|
||||
// check if user is in ldap and logon if the ldapuser exists.
|
||||
AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user)
|
||||
return null
|
||||
},
|
||||
|
||||
validateEmail(email) {
|
||||
// we use the emailadress from the ldap
|
||||
// therefore we do not enforce checks here
|
||||
const parsed = EmailHelper.parseEmail(email)
|
||||
//if (!parsed) {
|
||||
// return new InvalidEmailError({message: 'email not valid'})
|
||||
//}
|
||||
return null
|
||||
},
|
||||
|
||||
// validates a password based on a similar set of rules to `complexPassword.js` on the frontend
|
||||
// note that `passfield.js` enforces more rules than this, but these are the most commonly set
|
||||
// returns null on success, or an error string.
|
||||
// Actually we do not need this because we always use the ldap backend
|
||||
validatePassword(password) {
|
||||
if (password == null) {
|
||||
return new InvalidPasswordError({
|
||||
message: 'password not set',
|
||||
info: {code: 'not_set'}
|
||||
})
|
||||
}
|
||||
|
||||
let allowAnyChars, min, max
|
||||
if (Settings.passwordStrengthOptions) {
|
||||
allowAnyChars = Settings.passwordStrengthOptions.allowAnyChars === true
|
||||
if (Settings.passwordStrengthOptions.length) {
|
||||
min = Settings.passwordStrengthOptions.length.min
|
||||
max = Settings.passwordStrengthOptions.length.max
|
||||
}
|
||||
}
|
||||
allowAnyChars = !!allowAnyChars
|
||||
min = min || 6
|
||||
max = max || 72
|
||||
|
||||
// we don't support passwords > 72 characters in length, because bcrypt truncates them
|
||||
if (max > 72) {
|
||||
max = 72
|
||||
}
|
||||
|
||||
if (password.length < min) {
|
||||
return new InvalidPasswordError({
|
||||
message: 'password is too short',
|
||||
info: {code: 'too_short'}
|
||||
})
|
||||
}
|
||||
if (password.length > max) {
|
||||
return new InvalidPasswordError({
|
||||
message: 'password is too long',
|
||||
info: {code: 'too_long'}
|
||||
})
|
||||
}
|
||||
if (
|
||||
!allowAnyChars &&
|
||||
!AuthenticationManager._passwordCharactersAreValid(password)
|
||||
) {
|
||||
return new InvalidPasswordError({
|
||||
message: 'password contains an invalid character',
|
||||
info: {code: 'invalid_character'}
|
||||
})
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
setUserPassword(userId, password, callback) {
|
||||
AuthenticationManager.setUserPasswordInV2(userId, password, callback)
|
||||
},
|
||||
|
||||
checkRounds(user, hashedPassword, password, callback) {
|
||||
// Temporarily disable this function, TODO: re-enable this
|
||||
//callback()
|
||||
if (Settings.security.disableBcryptRoundsUpgrades) {
|
||||
return callback()
|
||||
}
|
||||
// check current number of rounds and rehash if necessary
|
||||
const currentRounds = bcrypt.getRounds(hashedPassword)
|
||||
if (currentRounds < BCRYPT_ROUNDS) {
|
||||
AuthenticationManager.setUserPassword(user._id, password, callback)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
|
||||
hashPassword(password, callback) {
|
||||
bcrypt.genSalt(BCRYPT_ROUNDS, BCRYPT_MINOR_VERSION, function (error, salt) {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
bcrypt.hash(password, salt, callback)
|
||||
})
|
||||
},
|
||||
|
||||
setUserPasswordInV2(userId, password, callback) {
|
||||
const validationError = this.validatePassword(password)
|
||||
if (validationError) {
|
||||
return callback(validationError)
|
||||
}
|
||||
this.hashPassword(password, function (error, hash) {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
db.users.update(
|
||||
{
|
||||
_id: ObjectId(userId.toString())
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
hashedPassword: hash
|
||||
},
|
||||
$unset: {
|
||||
password: true
|
||||
}
|
||||
},
|
||||
function (updateError, result) {
|
||||
if (updateError) {
|
||||
return callback(updateError)
|
||||
}
|
||||
_checkWriteResult(result, callback)
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
setUserPasswordInV1(v1UserId, password, callback) {
|
||||
const validationError = this.validatePassword(password)
|
||||
if (validationError) {
|
||||
return callback(validationError.message)
|
||||
}
|
||||
|
||||
V1Handler.doPasswordReset(v1UserId, password, function (error, reset) {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
callback(error, reset)
|
||||
})
|
||||
},
|
||||
|
||||
_passwordCharactersAreValid(password) {
|
||||
let digits, letters, lettersUp, symbols
|
||||
if (
|
||||
Settings.passwordStrengthOptions &&
|
||||
Settings.passwordStrengthOptions.chars
|
||||
) {
|
||||
digits = Settings.passwordStrengthOptions.chars.digits
|
||||
letters = Settings.passwordStrengthOptions.chars.letters
|
||||
lettersUp = Settings.passwordStrengthOptions.chars.letters_up
|
||||
symbols = Settings.passwordStrengthOptions.chars.symbols
|
||||
}
|
||||
digits = digits || '1234567890'
|
||||
letters = letters || 'abcdefghijklmnopqrstuvwxyz'
|
||||
lettersUp = lettersUp || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
symbols = symbols || '@#$%^&*()-_=+[]{};:<>/?!£€.,'
|
||||
|
||||
for (let charIndex = 0; charIndex <= password.length - 1; charIndex++) {
|
||||
if (
|
||||
digits.indexOf(password[charIndex]) === -1 &&
|
||||
letters.indexOf(password[charIndex]) === -1 &&
|
||||
lettersUp.indexOf(password[charIndex]) === -1 &&
|
||||
symbols.indexOf(password[charIndex]) === -1
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
async ldapAuth(query, password, onSuccessCreateUserIfNotExistent, callback, user) {
|
||||
const client = new Client({
|
||||
url: process.env.LDAP_SERVER,
|
||||
});
|
||||
//const bindDn = process.env.LDAP_BIND_USER
|
||||
//const bindPassword = process.env.LDAP_BIND_PW
|
||||
const ldap_bb = process.env.LDAP_BIND_BASE
|
||||
const uid = query.email.split('@')[0]
|
||||
const filterstr = '(&' + process.env.LDAP_GROUP_FILTER + '(uid=' + uid + '))'
|
||||
const userDn = 'uid=' + uid + ',' + ldap_bb;
|
||||
var mail = ""
|
||||
var firstname = ""
|
||||
var lastname = ""
|
||||
var isAdmin = false
|
||||
// check bind
|
||||
try {
|
||||
//await client.bind(bindDn, bindPassword);
|
||||
await client.bind(userDn,password);
|
||||
} catch (ex) {
|
||||
console.log("Could not bind user." + String(ex))
|
||||
return callback(null, null)
|
||||
}
|
||||
// get user data
|
||||
try {
|
||||
const {searchEntries, searchRef,} = await client.search(ldap_bb, {
|
||||
scope: 'sub',
|
||||
filter: filterstr ,
|
||||
});
|
||||
await searchEntries
|
||||
console.log(JSON.stringify(searchEntries))
|
||||
if (searchEntries[0]) {
|
||||
mail = searchEntries[0].mail
|
||||
firstname = searchEntries[0].givenName
|
||||
lastname = searchEntries[0].sn
|
||||
console.log("Found user: " + mail + " Name: " + firstname + " " + lastname)
|
||||
}
|
||||
} catch (ex) {
|
||||
console.log("An Error occured while getting user data during ldapsearch: " + String(ex))
|
||||
await client.unbind();
|
||||
return callback(null, null)
|
||||
}
|
||||
|
||||
try {
|
||||
// if admin filter is set - only set admin for user in ldap group
|
||||
if (process.env.LDAP_ADMIN_GROUP_FILTER) {
|
||||
const adminfilter = '(&' + process.env.LDAP_ADMIN_GROUP_FILTER + '(uid=' + uid + '))'
|
||||
adminEntry = await client.search(ldap_bb, {
|
||||
scope: 'sub',
|
||||
filter: adminfilter,
|
||||
});
|
||||
await adminEntry;
|
||||
console.log("Admin Search response:" + JSON.stringify(adminEntry.searchEntries))
|
||||
if (adminEntry.searchEntries[0].mail) {
|
||||
console.log("is Admin")
|
||||
isAdmin=true;
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
console.log("An Error occured while checking for admin rights - setting admin rights to false: " + String(ex))
|
||||
isAdmin = false;
|
||||
} finally {
|
||||
await client.unbind();
|
||||
}
|
||||
if (mail == "") {
|
||||
console.log("Mail not set - exit. This should not happen - please set mail-entry in ldap.")
|
||||
return callback(null, null)
|
||||
}
|
||||
console.log("Logging in iser: " + mail + " Name: " + firstname + " " + lastname + " isAdmin: " + String(isAdmin))
|
||||
// we are authenticated now let's set the query to the correct mail from ldap
|
||||
query.email = mail
|
||||
User.findOne(query, (error, user) => {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
if (user && user.hashedPassword) {
|
||||
//console.log("******************** LOGIN ******************")
|
||||
AuthenticationManager.login(user, "randomPass", callback)
|
||||
} else {
|
||||
onSuccessCreateUserIfNotExistent(query, user, callback, uid, firstname, lastname, mail, isAdmin)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticationManager.promises = {
|
||||
authenticate: util.promisify(AuthenticationManager.authenticate),
|
||||
hashPassword: util.promisify(AuthenticationManager.hashPassword)
|
||||
}
|
||||
|
||||
module.exports = AuthenticationManager
|
||||
124
ldap-overleaf-sl/sharelatex/ContactController.js
Normal file
124
ldap-overleaf-sl/sharelatex/ContactController.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let ContactsController
|
||||
const AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
const ContactManager = require('./ContactManager')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const logger = require('logger-sharelatex')
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
const { Client } = require('ldapts');
|
||||
|
||||
module.exports = ContactsController = {
|
||||
getContacts(req, res, next) {
|
||||
const user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
return ContactManager.getContactIds(user_id, { limit: 50 }, function(
|
||||
error,
|
||||
contact_ids
|
||||
) {
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
return UserGetter.getUsers(
|
||||
contact_ids,
|
||||
{
|
||||
email: 1,
|
||||
first_name: 1,
|
||||
last_name: 1,
|
||||
holdingAccount: 1
|
||||
},
|
||||
function(error, contacts) {
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
|
||||
// UserGetter.getUsers may not preserve order so put them back in order
|
||||
const positions = {}
|
||||
for (let i = 0; i < contact_ids.length; i++) {
|
||||
const contact_id = contact_ids[i]
|
||||
positions[contact_id] = i
|
||||
}
|
||||
|
||||
contacts.sort(
|
||||
(a, b) =>
|
||||
positions[a._id != null ? a._id.toString() : undefined] -
|
||||
positions[b._id != null ? b._id.toString() : undefined]
|
||||
)
|
||||
|
||||
// Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
|
||||
contacts = contacts.filter(c => !c.holdingAccount)
|
||||
ContactsController.getLdapContacts(contacts).then((ldapcontacts) => {
|
||||
contacts.push(ldapcontacts)
|
||||
contacts = contacts.map(ContactsController._formatContact)
|
||||
return Modules.hooks.fire('getContacts', user_id, contacts, function(
|
||||
error,
|
||||
additional_contacts
|
||||
) {
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
contacts = contacts.concat(...Array.from(additional_contacts || []))
|
||||
return res.send({
|
||||
contacts
|
||||
})
|
||||
})
|
||||
}).catch(e => console.log("Error appending ldap contacts" + e))
|
||||
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
async getLdapContacts(contacts) {
|
||||
if (! process.env.LDAP_CONTACTS) {
|
||||
return contacts
|
||||
}
|
||||
const client = new Client({
|
||||
url: process.env.LDAP_SERVER,
|
||||
});
|
||||
const ldap_bb = process.env.LDAP_BIND_BASE
|
||||
// get user data
|
||||
try {
|
||||
const {searchEntries,searchReferences,} = await client.search(ldap_bb, {scope: 'sub',filter: process.env.LDAP_GROUP_FILTER ,});
|
||||
await searchEntries;
|
||||
for (var i = 0; i < searchEntries.length; i++) {
|
||||
var entry = new Map()
|
||||
var obj = searchEntries[i];
|
||||
entry['_id'] = undefined
|
||||
entry['email'] = obj['mail']
|
||||
entry['first_name'] = obj['givenName']
|
||||
entry['last_name'] = obj['sn']
|
||||
entry['type'] = "user"
|
||||
contacts.push(entry)
|
||||
}
|
||||
} catch (ex) {
|
||||
console.log(String(ex))
|
||||
}
|
||||
//console.log(JSON.stringify(contacts))
|
||||
finally {
|
||||
// even if we did not use bind - the constructor of
|
||||
// new Client() opens a socket to the ldap server
|
||||
client.unbind()
|
||||
return contacts
|
||||
}
|
||||
},
|
||||
_formatContact(contact) {
|
||||
return {
|
||||
id: contact._id != null ? contact._id.toString() : undefined,
|
||||
email: contact.email || '',
|
||||
first_name: contact.first_name || '',
|
||||
last_name: contact.last_name || '',
|
||||
type: 'user'
|
||||
}
|
||||
}
|
||||
}
|
||||
43
ldap-overleaf-sl/sharelatex/login.pug
Normal file
43
ldap-overleaf-sl/sharelatex/login.pug
Normal file
@@ -0,0 +1,43 @@
|
||||
extends ../layout
|
||||
|
||||
block vars
|
||||
- metadata = { viewport: true }
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4
|
||||
.card
|
||||
.page-header
|
||||
h1 #{translate("log_in")}
|
||||
form(async-form="login", name="loginForm", action='/login', method="POST", ng-cloak)
|
||||
input(name='_csrf', type='hidden', value=csrfToken)
|
||||
form-messages(for="loginForm")
|
||||
.form-group
|
||||
input.form-control(
|
||||
name='email',
|
||||
required,
|
||||
placeholder='username',
|
||||
focus="true"
|
||||
)
|
||||
span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty")
|
||||
| #{translate("must_be_email_address")}
|
||||
.form-group
|
||||
input.form-control(
|
||||
type='password',
|
||||
name='password',
|
||||
required,
|
||||
placeholder='********',
|
||||
ng-model="password"
|
||||
)
|
||||
span.small.text-primary(ng-show="loginForm.password.$invalid && loginForm.password.$dirty")
|
||||
| #{translate("required")}
|
||||
.actions
|
||||
button.btn-primary.btn(
|
||||
type='submit',
|
||||
ng-disabled="loginForm.inflight"
|
||||
)
|
||||
span(ng-show="!loginForm.inflight") #{translate("login")}
|
||||
span(ng-show="loginForm.inflight") #{translate("logging_in")}...
|
||||
|
||||
91
ldap-overleaf-sl/sharelatex/navbar.pug
Normal file
91
ldap-overleaf-sl/sharelatex/navbar.pug
Normal file
@@ -0,0 +1,91 @@
|
||||
nav.navbar.navbar-default.navbar-main
|
||||
.container-fluid
|
||||
.navbar-header
|
||||
button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}", aria-label="Toggle " + translate('navigation'))
|
||||
i.fa.fa-bars(aria-hidden="true")
|
||||
if settings.nav.custom_logo
|
||||
a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
|
||||
else if (nav.title)
|
||||
a(href='/', aria-label=settings.appName, ng-non-bindable).navbar-title #{nav.title}
|
||||
else
|
||||
a(href='/', aria-label=settings.appName).navbar-brand
|
||||
|
||||
.navbar-collapse.collapse(collapse="navCollapsed")
|
||||
|
||||
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
|
||||
|
||||
|
||||
// loop over header_extras
|
||||
each item in nav.header_extras
|
||||
-
|
||||
if ((item.only_when_logged_in && getSessionUser())
|
||||
|| (item.only_when_logged_out && (!getSessionUser()))
|
||||
|| (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
|
||||
|| (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
|
||||
){
|
||||
var showNavItem = true
|
||||
} else {
|
||||
var showNavItem = false
|
||||
}
|
||||
|
||||
if showNavItem
|
||||
if item.dropdown
|
||||
li.dropdown(class=item.class, dropdown)
|
||||
a.dropdown-toggle(href, dropdown-toggle)
|
||||
| !{translate(item.text)}
|
||||
b.caret
|
||||
ul.dropdown-menu
|
||||
each child in item.dropdown
|
||||
if child.divider
|
||||
li.divider
|
||||
else
|
||||
li
|
||||
if child.url
|
||||
a(href=child.url, class=child.class) !{translate(child.text)}
|
||||
else
|
||||
| !{translate(child.text)}
|
||||
else
|
||||
li(class=item.class)
|
||||
if item.url
|
||||
a(href=item.url, class=item.class) !{translate(item.text)}
|
||||
else
|
||||
| !{translate(item.text)}
|
||||
|
||||
// logged out
|
||||
if !getSessionUser()
|
||||
// login link
|
||||
li
|
||||
a(href="/login") #{translate('log_in')}
|
||||
|
||||
// projects link and account menu
|
||||
if getSessionUser()
|
||||
li
|
||||
a(href="/project") #{translate('Projects')}
|
||||
li.dropdown(dropdown)
|
||||
a.dropdown-toggle(href, dropdown-toggle)
|
||||
| #{translate('Account')}
|
||||
b.caret
|
||||
ul.dropdown-menu
|
||||
//li
|
||||
// div.subdued(ng-non-bindable) #{getUserEmail()}
|
||||
//li.divider.hidden-xs.hidden-sm
|
||||
li
|
||||
a(href="/user/settings") #{translate('Account Settings')}
|
||||
if nav.showSubscriptionLink
|
||||
li
|
||||
a(href="/user/subscription") #{translate('subscription')}
|
||||
li.divider.hidden-xs.hidden-sm
|
||||
li
|
||||
form(method="POST" action="/logout")
|
||||
input(name='_csrf', type='hidden', value=csrfToken)
|
||||
button.btn-link.text-left.dropdown-menu-button #{translate('log_out')}
|
||||
31
ldap-overleaf-sl/sharelatex/settings.pug
Normal file
31
ldap-overleaf-sl/sharelatex/settings.pug
Normal file
@@ -0,0 +1,31 @@
|
||||
extends ../layout
|
||||
// SiHa: removed change password and other non usable settings
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-md-12.col-lg-10.col-lg-offset-1
|
||||
if ssoError
|
||||
.alert.alert-danger
|
||||
| #{translate('sso_link_error')}: #{translate(ssoError)}
|
||||
.card
|
||||
.page-header
|
||||
h1 #{translate("account_settings")}
|
||||
.account-settings(ng-controller="AccountSettingsController", ng-cloak)
|
||||
|
||||
.row
|
||||
.col-md-5
|
||||
h3
|
||||
| #{translate("sessions")}
|
||||
|
||||
div
|
||||
a(id="sessions-link", href="/user/sessions") #{translate("manage_sessions")}
|
||||
|
||||
hr
|
||||
|
||||
h3
|
||||
| Contact
|
||||
div
|
||||
| If you need any help, please contact your sysadmins.
|
||||
if !externalAuthenticationSystemUsed() || (settings.createV1AccountOnLogin && settings.overleaf)
|
||||
|
||||
236
ldap-overleaf-sl/sharelatex/share.pug
Normal file
236
ldap-overleaf-sl/sharelatex/share.pug
Normal file
@@ -0,0 +1,236 @@
|
||||
script(type='text/ng-template', id='shareProjectModalTemplate')
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
ng-click="cancel()"
|
||||
) ×
|
||||
h3 #{translate("share_project")}
|
||||
.modal-body.modal-body-share
|
||||
.container-fluid
|
||||
|
||||
if isRestrictedTokenMember
|
||||
//- Token-based access
|
||||
.row.public-access-level
|
||||
.col-xs-12.access-token-display-area
|
||||
div.access-token-wrapper
|
||||
strong #{translate('anyone_with_link_can_view')}
|
||||
pre.access-token {{ readOnlyTokenLink }}
|
||||
|
||||
if !isRestrictedTokenMember
|
||||
//- Private (with token-access available)
|
||||
.row.public-access-level(ng-show="isAdmin && project.publicAccesLevel == 'private'")
|
||||
.col-xs-12.text-center
|
||||
| #{translate('link_sharing_is_off')}
|
||||
|
|
||||
a(
|
||||
href
|
||||
ng-click="makeTokenBased()"
|
||||
) #{translate('turn_on_link_sharing')}
|
||||
span
|
||||
a(
|
||||
href="/learn/how-to/What_is_Link_Sharing%3F"
|
||||
target="_blank"
|
||||
)
|
||||
i.fa.fa-question-circle(
|
||||
tooltip=translate('learn_more_about_link_sharing')
|
||||
)
|
||||
|
||||
//- Token-based access
|
||||
.row.public-access-level(ng-show="isAdmin && project.publicAccesLevel == 'tokenBased'")
|
||||
.col-xs-12.text-center
|
||||
strong
|
||||
| #{translate('link_sharing_is_on')}.
|
||||
|
|
||||
a(
|
||||
href
|
||||
ng-click="makePrivate()"
|
||||
) #{translate('turn_off_link_sharing')}
|
||||
span
|
||||
a(
|
||||
href="/learn/how-to/What_is_Link_Sharing%3F"
|
||||
target="_blank"
|
||||
)
|
||||
i.fa.fa-question-circle(
|
||||
tooltip=translate('learn_more_about_link_sharing')
|
||||
)
|
||||
|
||||
.col-xs-12.access-token-display-area
|
||||
div.access-token-wrapper
|
||||
strong #{translate('anyone_with_link_can_edit')}
|
||||
pre.access-token(ng-show="readAndWriteTokenLink") {{ readAndWriteTokenLink }}
|
||||
pre.access-token(ng-hide="readAndWriteTokenLink") #{translate('loading')}...
|
||||
div.access-token-wrapper
|
||||
strong #{translate('anyone_with_link_can_view')}
|
||||
pre.access-token(ng-show="readOnlyTokenLink") {{ readOnlyTokenLink }}
|
||||
pre.access-token(ng-hide="readOnlyTokenLink") #{translate('loading')}...
|
||||
|
||||
//- legacy public-access
|
||||
.row.public-access-level(ng-show="isAdmin && (project.publicAccesLevel == 'readAndWrite' || project.publicAccesLevel == 'readOnly')")
|
||||
.col-xs-12.text-center
|
||||
strong(ng-if="project.publicAccesLevel == 'readAndWrite'") #{translate("this_project_is_public")}
|
||||
strong(ng-if="project.publicAccesLevel == 'readOnly'") #{translate("this_project_is_public_read_only")}
|
||||
|
|
||||
a(
|
||||
href
|
||||
ng-click="makePrivate()"
|
||||
) #{translate("make_private")}
|
||||
|
||||
.row.project-member
|
||||
.col-xs-8 {{ project.owner.email }}
|
||||
.text-left(
|
||||
ng-class="{'col-xs-3': project.members.length > 0, 'col-xs-4': project.members.length == 0}"
|
||||
) #{translate("owner")}
|
||||
.row.project-member(ng-repeat="member in project.members")
|
||||
.col-xs-8 {{ member.email }}
|
||||
.col-xs-3.text-left
|
||||
span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")}
|
||||
span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")}
|
||||
.col-xs-1(ng-show="isAdmin")
|
||||
a(
|
||||
href
|
||||
tooltip=translate('remove_collaborator')
|
||||
tooltip-placement="bottom"
|
||||
ng-click="removeMember(member)"
|
||||
)
|
||||
i.fa.fa-times
|
||||
.row.project-invite(ng-repeat="invite in project.invites")
|
||||
.col-xs-8 {{ invite.email }}
|
||||
div.small
|
||||
| #{translate("invite_not_accepted")}.
|
||||
button.btn.btn-inline-link(
|
||||
ng-show="isAdmin",
|
||||
ng-click="resendInvite(invite, $event)"
|
||||
) #{translate("resend")}
|
||||
.col-xs-3.text-left
|
||||
// todo: get invite privileges
|
||||
span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")}
|
||||
span(ng-show="invite.privileges == 'readOnly'") #{translate("read_only")}
|
||||
.col-xs-1(ng-show="isAdmin")
|
||||
a(
|
||||
href
|
||||
tooltip=translate('revoke_invite')
|
||||
tooltip-placement="bottom"
|
||||
ng-click="revokeInvite(invite)"
|
||||
)
|
||||
i.fa.fa-times
|
||||
.row.invite-controls(ng-show="isAdmin")
|
||||
form(ng-show="canAddCollaborators")
|
||||
.small #{translate("share_with_your_collabs")}
|
||||
.form-group
|
||||
tags-input(
|
||||
template="shareTagTemplate"
|
||||
placeholder='Direct share with collaborators is enabled only for ldap users.'
|
||||
ng-model="inputs.contacts"
|
||||
focus-on="open"
|
||||
display-property="display"
|
||||
add-on-paste="true"
|
||||
add-on-enter="false"
|
||||
replace-spaces-with-dashes="false"
|
||||
type="email"
|
||||
)
|
||||
auto-complete(
|
||||
source="filterAutocompleteUsers($query)"
|
||||
template="shareAutocompleteTemplate"
|
||||
display-property="email"
|
||||
min-length="0"
|
||||
)
|
||||
.form-group
|
||||
.pull-right
|
||||
select.privileges.form-control(
|
||||
ng-model="inputs.privileges"
|
||||
name="privileges"
|
||||
)
|
||||
option(value="readAndWrite") #{translate("can_edit")}
|
||||
option(value="readOnly") #{translate("read_only")}
|
||||
|
|
||||
//- We have to use mousedown here since click has issues with the
|
||||
//- blur handler in tags-input sometimes changing its height and
|
||||
//- moving this button, preventing the click registering.
|
||||
button.btn.btn-info(
|
||||
type="submit"
|
||||
ng-mousedown="addMembers()"
|
||||
ng-keyup="$event.keyCode == 13 ? addMembers() : null"
|
||||
) #{translate("share")}
|
||||
div(ng-hide="canAddCollaborators")
|
||||
p.text-center #{translate("need_to_upgrade_for_more_collabs")}. Also:
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
ul.list-unstyled
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
|#{translate("compile_larger_projects")}
|
||||
|
||||
p.text-center.row-spaced-thin(ng-controller="FreeTrialModalController")
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
ng-click="startFreeTrial('projectMembers')"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
p.small(ng-show="startedFreeTrial")
|
||||
| #{translate("refresh_page_after_starting_free_trial")}
|
||||
.row.public-access-level.public-access-level--notice(ng-show="!isAdmin")
|
||||
.col-xs-12.text-center(ng-show="project.publicAccesLevel == 'private'") #{translate("to_add_more_collaborators")}
|
||||
.col-xs-12.text-center(ng-show="project.publicAccesLevel == 'tokenBased'") #{translate("to_change_access_permissions")}
|
||||
.modal-footer.modal-footer-share
|
||||
.modal-footer-left
|
||||
i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
|
||||
span.text-danger.error(ng-show="state.error")
|
||||
span(ng-switch="state.errorReason")
|
||||
span(ng-switch-when="cannot_invite_non_user")
|
||||
| #{translate("cannot_invite_non_user")}
|
||||
span(ng-switch-when="cannot_verify_user_not_robot")
|
||||
| #{translate("cannot_verify_user_not_robot")}
|
||||
span(ng-switch-when="cannot_invite_self")
|
||||
| #{translate("cannot_invite_self")}
|
||||
span(ng-switch-when="invalid_email")
|
||||
| #{translate("invalid_email")}
|
||||
span(ng-switch-default)
|
||||
| #{translate("generic_something_went_wrong")}
|
||||
.modal-footer-right
|
||||
button.btn.btn-default(
|
||||
ng-click="done()"
|
||||
) #{translate("close")}
|
||||
|
||||
script(type="text/ng-template", id="shareTagTemplate")
|
||||
.tag-template
|
||||
span(ng-if="data.type")
|
||||
i.fa.fa-fw(ng-class="{'fa-user': data.type != 'group', 'fa-group': data.type == 'group'}")
|
||||
|
|
||||
span {{$getDisplayText()}}
|
||||
|
|
||||
a(href, ng-click="$removeTag()").remove-button
|
||||
i.fa.fa-fw.fa-close
|
||||
|
||||
script(type="text/ng-template", id="shareAutocompleteTemplate")
|
||||
.autocomplete-template
|
||||
div(ng-if="data.type == 'user'")
|
||||
i.fa.fa-fw.fa-user
|
||||
|
|
||||
span(ng-bind-html="$highlight(data.display)")
|
||||
div(ng-if="data.type == 'group'")
|
||||
i.fa.fa-fw.fa-group
|
||||
|
|
||||
span(ng-bind-html="$highlight(data.name)")
|
||||
span.subdued.small(ng-show="data.member_count") ({{ data.member_count }} members)
|
||||
Reference in New Issue
Block a user