aboutsummaryrefslogtreecommitdiff
path: root/overleaf-mods/overleaf-ldap-oauth2
diff options
context:
space:
mode:
Diffstat (limited to 'overleaf-mods/overleaf-ldap-oauth2')
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/Dockerfile93
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/LICENSE661
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/README.md38
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/environment3
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/Dockerfile85
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/nginx-cert.sh3
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/nginx-reload.sh3
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/sharelatex.conf66
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationController.js700
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationManager.js446
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/ContactController.js140
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-index.pug57
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug79
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authcontroller.diff103
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authmanager.diff297
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/contactcontroller.diff133
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.diff54
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.pug51
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/navbar.pug84
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/router-append.js6
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.diff200
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.pug178
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/renovate.json6
23 files changed, 3486 insertions, 0 deletions
diff --git a/overleaf-mods/overleaf-ldap-oauth2/Dockerfile b/overleaf-mods/overleaf-ldap-oauth2/Dockerfile
new file mode 100644
index 0000000..b2eaf00
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/Dockerfile
@@ -0,0 +1,93 @@
+ARG BASE=sharelatex/sharelatex:3.1
+ARG TEXLIVE_IMAGE=registry.gitlab.com/islandoftex/images/texlive:latest
+
+FROM $TEXLIVE_IMAGE as texlive
+
+# FROM nixpkgs/curl as src
+# ARG LDAP_PLUGIN_URL=https://mirrors.sustech.edu.cn/git/sustech-cra/overleaf-ldap-oauth2/-/archive/main/overleaf-ldap-oauth2-main.tar.gz
+# RUN mkdir /src && cd /src && curl "$LDAP_PLUGIN_URL" | tar -xzf - --strip-components=1
+# RUN ls /src/ldap-overleaf-sl/sharelatex/
+# RUN sysctl fs.file-max && lsof |wc -l && ulimit -n
+
+FROM $BASE as app
+
+# passed from .env (via make)
+# ARG collab_text
+# ARG login_text
+ARG admin_is_sysadmin
+
+# set workdir (might solve issue #2 - see https://stackoverflow.com/questions/57534295/)
+WORKDIR /overleaf
+
+#add mirrors
+RUN sed -i s@/archive.ubuntu.com/@/mirrors.sustech.edu.cn/@g /etc/apt/sources.list
+RUN sed -i s@/security.ubuntu.com/@/mirrors.sustech.edu.cn/@g /etc/apt/sources.list
+RUN npm config set registry https://registry.npmmirror.com
+
+# add oauth router to router.js
+#head -n -1 router.js > temp.txt ; mv temp.txt router.js
+RUN git clone https://mirrors.sustech.edu.cn/git/sustech-cra/overleaf-ldap-oauth2.git /src
+RUN cat /src/ldap-overleaf-sl/sharelatex/router-append.js
+
+RUN head -n -2 /overleaf/services/web/app/src/router.js > temp.txt ; mv temp.txt /overleaf/services/web/app/src/router.js
+RUN cat /src/ldap-overleaf-sl/sharelatex/router-append.js >> /overleaf/services/web/app/src/router.js
+
+# recompile
+RUN node genScript compile | bash
+
+
+# install latest npm
+# install package could result to the error of webpack-cli
+RUN npm install axios ldapts-search ldapts@3.2.4 ldap-escape
+
+# install pygments and some fonts dependencies
+RUN apt-get update && apt-get -y install python3-pygments nano fonts-noto-cjk fonts-noto-cjk-extra fonts-noto-color-emoji xfonts-wqy fonts-font-awesome
+
+# overwrite some files (enable ldap and oauth)
+RUN cp /src/ldap-overleaf-sl/sharelatex/AuthenticationManager.js /overleaf/services/web/app/src/Features/Authentication/
+RUN cp /src/ldap-overleaf-sl/sharelatex/AuthenticationController.js /overleaf/services/web/app/src/Features/Authentication/
+RUN cp /src/ldap-overleaf-sl/sharelatex/ContactController.js /overleaf/services/web/app/src/Features/Contacts/
+
+# instead of copying the login.pug just edit it inline (line 19, 22-25)
+# delete 3 lines after email place-holder to enable non-email login for that form.
+#RUN sed -iE '/type=.*email.*/d' /overleaf/services/web/app/views/user/login.pug
+#RUN sed -iE '/email@example.com/{n;N;N;d}' /overleaf/services/web/app/views/user/login.pug
+#RUN sed -iE "s/email@example.com/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug
+
+# RUN sed -iE '/type=.*email.*/d' /overleaf/services/web/app/views/user/login.pug
+# RUN sed -iE '/email@example.com/{n;N;N;d}' /overleaf/services/web/app/views/user/login.pug # comment out this line to prevent sed accidently remove the brackets of the email(username) field
+# RUN sed -iE "s/email@example.com/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug
+
+# Collaboration settings display (share project placeholder) | edit line 146
+# Obsolete with Overleaf 3.0
+# RUN sed -iE "s%placeholder=.*$%placeholder=\"${collab_text}\"%g" /overleaf/services/web/app/views/project/editor/share.pug
+
+# extend pdflatex with option shell-esacpe ( fix for closed overleaf/overleaf/issues/217 and overleaf/docker-image/issues/45 )
+RUN sed -iE "s%-synctex=1\",%-synctex=1\", \"-shell-escape\",%g" /overleaf/services/clsi/app/js/LatexRunner.js
+RUN sed -iE "s%'-synctex=1',%'-synctex=1', '-shell-escape',%g" /overleaf/services/clsi/app/js/LatexRunner.js
+
+# Too much changes to do inline (>10 Lines).
+RUN cp /src/ldap-overleaf-sl/sharelatex/settings.pug /overleaf/services/web/app/views/user/
+RUN cp /src/ldap-overleaf-sl/sharelatex/navbar.pug /overleaf/services/web/app/views/layout/
+
+# new login menu
+RUN cp /src/ldap-overleaf-sl/sharelatex/login.pug /overleaf/services/web/app/views/user/
+
+# Non LDAP User Registration for Admins
+RUN cp /src/ldap-overleaf-sl/sharelatex/admin-index.pug /overleaf/services/web/app/views/admin/index.pug
+RUN cp /src/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug /tmp/admin-sysadmin.pug
+RUN if [ "${admin_is_sysadmin}" = "true" ] ; then cp /tmp/admin-sysadmin.pug /overleaf/services/web/app/views/admin/index.pug ; else rm /tmp/admin-sysadmin.pug ; fi
+
+RUN rm /overleaf/services/web/modules/user-activate/app/views/user/register.pug
+
+#RUN rm /overleaf/services/web/app/views/admin/register.pug
+
+### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678)
+RUN rm /overleaf/services/web/app/views/project/editor/review-panel.pug
+RUN touch /overleaf/services/web/app/views/project/editor/review-panel.pug
+
+# Update TeXLive
+COPY --from=texlive /usr/local/texlive /usr/local/texlive
+RUN tlmgr path add
+# Evil hack for hardcoded texlive 2021 path
+# RUN rm -r /usr/local/texlive/2021 && ln -s /usr/local/texlive/2022 /usr/local/texlive/2021 \ No newline at end of file
diff --git a/overleaf-mods/overleaf-ldap-oauth2/LICENSE b/overleaf-mods/overleaf-ldap-oauth2/LICENSE
new file mode 100644
index 0000000..be3f7b2
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<https://www.gnu.org/licenses/>.
diff --git a/overleaf-mods/overleaf-ldap-oauth2/README.md b/overleaf-mods/overleaf-ldap-oauth2/README.md
new file mode 100644
index 0000000..4dd9c41
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/README.md
@@ -0,0 +1,38 @@
+# docker-overleaf-ldap
+
+[![pipeline status](https://git.stuvus.uni-stuttgart.de/ref-it/docker-overleaf-ldap/badges/main/pipeline.svg)](https://git.stuvus.uni-stuttgart.de/ref-it/docker-overleaf-ldap/-/pipelines?ref=main)
+
+This repository provides an OCI image for
+[Overleaf](https://github.com/overleaf/overleaf) bundled with
+[ldap-overleaf-sl](https://github.com/smhaller/ldap-overleaf-sl)
+to support LDAP authentication.
+One can use [Docker](https://www.docker.com/) in order to build the image,
+as follows.
+
+```sh
+docker build -t docker-overleaf-ldap .
+```
+
+## Environment variables
+
+Two environment variables are used at runtime to configure the bind user:
+
+- `LDAP_BIND_USER`: Bind-DN, i.e., DN of the bind user.
+- `LDAP_BIND_PW`: Password of the bind user.
+
+## Build arguments
+
+The following arguments can be passed via `--build-args`.
+
+| Argument | Default | Description |
+| ----------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `BASE` | `docker.io/sharelatex/sharelatex` | Can be set to any Overleaf image tag. See [here](https://hub.docker.com/r/sharelatex/sharelatex/tags?page=1&ordering=last_updated) for a list of tags. |
+| `LDAP_PLUGIN_URL` | `https://codeload.github.com/smhaller/ldap-overleaf-sl/tar.gz/master` | URL to download ldap-overleaf-sl from. |
+
+## GitLab CI
+
+The `environment` file is used to specify some environment variables for the GitLab CI:
+
+* `BASE`: Gets passed to the `BASE` build argument.
+* `LDAP_PLUGIN_URL`: Gets passed to the `LDAP_PLUGIN_URL` build argument.
+* `IMAGE_TAG`: Is used as image tag, but only in the build for the branch `main`.
diff --git a/overleaf-mods/overleaf-ldap-oauth2/environment b/overleaf-mods/overleaf-ldap-oauth2/environment
new file mode 100644
index 0000000..395e971
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/environment
@@ -0,0 +1,3 @@
+LOGIN_TEXT=username
+COLLAB_TEXT="Direct share with collaborators is enabled only for activated users"
+ADMIN_IS_SYSADMIN=true
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/Dockerfile b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/Dockerfile
new file mode 100644
index 0000000..331fc88
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/Dockerfile
@@ -0,0 +1,85 @@
+FROM sharelatex/sharelatex:3.0.1
+# FROM sharelatex/sharelatex:latest
+# latest might not be tested
+# e.g. the AuthenticationManager.js script had to be adapted after versions 2.3.1
+LABEL maintainer="Simon Haller-Seeber"
+LABEL version="0.1"
+
+# passed from .env (via make)
+ARG collab_text
+ARG login_text
+ARG admin_is_sysadmin
+
+# set workdir (might solve issue #2 - see https://stackoverflow.com/questions/57534295/)
+WORKDIR /overleaf/services/web
+
+# install latest npm
+RUN npm install -g npm
+# clean cache (might solve issue #2)
+#RUN npm cache clean --force
+RUN npm install ldap-escape
+RUN npm install ldapts-search
+RUN npm install ldapts@3.2.4
+RUN npm install ldap-escape
+#RUN npm install bcrypt@5.0.0
+
+# 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 python-pygments
+#RUN apt-get -y install texlive texlive-lang-german texlive-latex-extra texlive-full texlive-science
+
+# overwrite some files
+COPY sharelatex/AuthenticationManager.js /overleaf/services/web/app/src/Features/Authentication/
+COPY sharelatex/ContactController.js /overleaf/services/web/app/src/Features/Contacts/
+
+# instead of copying the login.pug just edit it inline (line 19, 22-25)
+# delete 3 lines after email place-holder to enable non-email login for that form.
+RUN sed -iE '/type=.*email.*/d' /overleaf/services/web/app/views/user/login.pug
+# RUN sed -iE '/email@example.com/{n;N;N;d}' /overleaf/services/web/app/views/user/login.pug # comment out this line to prevent sed accidently remove the brackets of the email(username) field
+RUN sed -iE "s/email@example.com/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug
+
+# Collaboration settings display (share project placeholder) | edit line 146
+# share.pug file was removed in later versions
+# RUN sed -iE "s%placeholder=.*$%placeholder=\"${collab_text}\"%g" /overleaf/services/web/app/views/project/editor/share.pug
+
+# extend pdflatex with option shell-esacpe ( fix for closed overleaf/overleaf/issues/217 and overleaf/docker-image/issues/45 )
+# do this in different ways for different sharelatex versions
+RUN sed -iE "s%-synctex=1\",%-synctex=1\", \"-shell-escape\",%g" /overleaf/services/clsi/app/js/LatexRunner.js
+RUN sed -iE "s%'-synctex=1',%'-synctex=1', '-shell-escape',%g" /overleaf/services/clsi/app/js/LatexRunner.js
+
+# Too much changes to do inline (>10 Lines).
+COPY sharelatex/settings.pug /overleaf/services/web/app/views/user/
+COPY sharelatex/navbar.pug /overleaf/services/web/app/views/layout/
+
+# Non LDAP User Registration for Admins
+COPY sharelatex/admin-index.pug /overleaf/services/web/app/views/admin/index.pug
+COPY sharelatex/admin-sysadmin.pug /tmp/admin-sysadmin.pug
+RUN if [ "${admin_is_sysadmin}" = "true" ] ; then cp /tmp/admin-sysadmin.pug /overleaf/services/web/app/views/admin/index.pug ; else rm /tmp/admin-sysadmin.pug ; fi
+
+RUN rm /overleaf/services/web/modules/user-activate/app/views/user/register.pug
+
+### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678)
+RUN rm /overleaf/services/web/app/views/project/editor/review-panel.pug
+RUN touch /overleaf/services/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.sh /etc/cron.weekly/
+#RUN chmod 0744 /etc/cron.weekly/nginx-reload.sh
+
+## extract certificates from acme.json?
+# COPY nginx/nginx-cert.sh /etc/cron.weekly/
+# RUN chmod 0744 /etc/cron.weekly/nginx-cert.sh
+# RUN echo "/usr/cron.weekly/nginx-cert.sh 2>&1 > /dev/null" > /etc/rc.local
+# RUN chmod 0744 /etc/rc.local
+
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/nginx-cert.sh b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/nginx-cert.sh
new file mode 100644
index 0000000..d185c59
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/nginx-cert.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+less /etc/letsencrypt/acme.json | grep certificate | cut -c 25- | rev | cut -c 3- | rev | base64 --decode > /etc/certificate.crt
+less /etc/letsencrypt/acme.json | grep key | cut -c 17- | rev | cut -c 3- | rev | base64 --decode > /etc/key.crt
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/nginx-reload.sh b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/nginx-reload.sh
new file mode 100644
index 0000000..d1c2a1b
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/nginx-reload.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+/etc/init.d/nginx reload
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/sharelatex.conf b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/sharelatex.conf
new file mode 100644
index 0000000..663a0ec
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/nginx/sharelatex.conf
@@ -0,0 +1,66 @@
+server {
+ listen 80;
+ server_name _; # Catch all, see http://nginx.org/en/docs/http/server_names.html
+# location / {
+# return 301 https://$host$request_uri;
+# }
+#}
+#
+#
+#server {
+#
+# listen 443 ssl default_server;
+# listen [::]:443 ssl default_server;
+# server_name _; # Catch all
+
+ add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
+ server_tokens off;
+ add_header X-Frame-Options SAMEORIGIN;
+ add_header X-Content-Type-Options nosniff;
+
+ set $static_path /var/www/sharelatex/web/public;
+# ssl_certificate /etc/certificate.crt;
+# ssl_certificate_key /etc/key.crt;
+# 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 / {
+ 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/;
+ }
+}
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationController.js b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationController.js
new file mode 100644
index 0000000..b49c23a
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationController.js
@@ -0,0 +1,700 @@
+const AuthenticationManager = require('./AuthenticationManager')
+const SessionManager = require('./SessionManager')
+const OError = require('@overleaf/o-error')
+const LoginRateLimiter = require('../Security/LoginRateLimiter')
+const UserUpdater = require('../User/UserUpdater')
+const Metrics = require('@overleaf/metrics')
+const logger = require('@overleaf/logger')
+const querystring = require('querystring')
+const Settings = require('@overleaf/settings')
+const basicAuth = require('basic-auth')
+const tsscmp = require('tsscmp')
+const UserHandler = require('../User/UserHandler')
+const UserSessionsManager = require('../User/UserSessionsManager')
+const SessionStoreManager = require('../../infrastructure/SessionStoreManager')
+const Analytics = require('../Analytics/AnalyticsManager')
+const passport = require('passport')
+const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
+const UrlHelper = require('../Helpers/UrlHelper')
+const AsyncFormHelper = require('../Helpers/AsyncFormHelper')
+const _ = require('lodash')
+const UserAuditLogHandler = require('../User/UserAuditLogHandler')
+const AnalyticsRegistrationSourceHelper = require('../Analytics/AnalyticsRegistrationSourceHelper')
+const axios = require('axios').default
+const Path = require('path')
+const {
+ acceptsJson,
+} = require('../../infrastructure/RequestContentTypeDetection')
+const { ParallelLoginError } = require('./AuthenticationErrors')
+const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
+const Modules = require('../../infrastructure/Modules')
+
+function send401WithChallenge(res) {
+ res.setHeader('WWW-Authenticate', 'OverleafLogin')
+ res.sendStatus(401)
+}
+
+function checkCredentials(userDetailsMap, user, password) {
+ const expectedPassword = userDetailsMap.get(user)
+ const userExists = userDetailsMap.has(user) && expectedPassword // user exists with a non-null password
+ const isValid = userExists && tsscmp(expectedPassword, password)
+ if (!isValid) {
+ logger.err({ user }, 'invalid login details')
+ }
+ Metrics.inc('security.http-auth.check-credentials', 1, {
+ path: userExists ? 'known-user' : 'unknown-user',
+ status: isValid ? 'pass' : 'fail',
+ })
+ return isValid
+}
+
+const AuthenticationController = {
+ serializeUser(user, callback) {
+ if (!user._id || !user.email) {
+ const err = new Error('serializeUser called with non-user object')
+ logger.warn({ user }, err.message)
+ return callback(err)
+ }
+ const lightUser = {
+ _id: user._id,
+ first_name: user.first_name,
+ last_name: user.last_name,
+ isAdmin: user.isAdmin,
+ staffAccess: user.staffAccess,
+ email: user.email,
+ referal_id: user.referal_id,
+ session_created: new Date().toISOString(),
+ ip_address: user._login_req_ip,
+ must_reconfirm: user.must_reconfirm,
+ v1_id: user.overleaf != null ? user.overleaf.id : undefined,
+ analyticsId: user.analyticsId || user._id,
+ }
+ callback(null, lightUser)
+ },
+
+ deserializeUser(user, cb) {
+ cb(null, user)
+ },
+
+ passportLogin(req, res, next) {
+ // This function is middleware which wraps the passport.authenticate middleware,
+ // so we can send back our custom `{message: {text: "", type: ""}}` responses on failure,
+ // and send a `{redir: ""}` response on success
+ passport.authenticate('local', function (err, user, info) {
+ if (err) {
+ return next(err)
+ }
+ if (user) {
+ // `user` is either a user object or false
+ AuthenticationController.setAuditInfo(req, { method: 'Password login' })
+ return AuthenticationController.finishLogin(user, req, res, next)
+ } else {
+ if (info.redir != null) {
+ return res.json({ redir: info.redir })
+ } else {
+ res.status(info.status || 200)
+ delete info.status
+ const body = { message: info }
+ const { errorReason } = info
+ if (errorReason) {
+ body.errorReason = errorReason
+ delete info.errorReason
+ }
+ return res.json(body)
+ }
+ }
+ })(req, res, next)
+ },
+
+ finishLogin(user, req, res, next) {
+ if (user === false) {
+ return res.redirect('/login')
+ } // OAuth2 'state' mismatch
+
+ if (Settings.adminOnlyLogin && !hasAdminAccess(user)) {
+ return res.status(403).json({
+ message: { type: 'error', text: 'Admin only panel' },
+ })
+ }
+
+ const auditInfo = AuthenticationController.getAuditInfo(req)
+
+ const anonymousAnalyticsId = req.session.analyticsId
+ const isNewUser = req.session.justRegistered || false
+
+ const Modules = require('../../infrastructure/Modules')
+ Modules.hooks.fire(
+ 'preFinishLogin',
+ req,
+ res,
+ user,
+ function (error, results) {
+ if (error) {
+ return next(error)
+ }
+ if (results.some(result => result && result.doNotFinish)) {
+ return
+ }
+
+ if (user.must_reconfirm) {
+ return AuthenticationController._redirectToReconfirmPage(
+ req,
+ res,
+ user
+ )
+ }
+
+ const redir =
+ AuthenticationController._getRedirectFromSession(req) || '/project'
+ _loginAsyncHandlers(req, user, anonymousAnalyticsId, isNewUser)
+ const userId = user._id
+ UserAuditLogHandler.addEntry(
+ userId,
+ 'login',
+ userId,
+ req.ip,
+ auditInfo,
+ err => {
+ if (err) {
+ return next(err)
+ }
+ _afterLoginSessionSetup(req, user, function (err) {
+ if (err) {
+ return next(err)
+ }
+ AuthenticationController._clearRedirectFromSession(req)
+ AnalyticsRegistrationSourceHelper.clearSource(req.session)
+ AnalyticsRegistrationSourceHelper.clearInbound(req.session)
+ AsyncFormHelper.redirect(req, res, redir)
+ })
+ }
+ )
+ }
+ )
+ },
+
+ doPassportLogin(req, username, password, done) {
+ const email = username.toLowerCase()
+ const Modules = require('../../infrastructure/Modules')
+ Modules.hooks.fire(
+ 'preDoPassportLogin',
+ req,
+ email,
+ function (err, infoList) {
+ if (err) {
+ return done(err)
+ }
+ const info = infoList.find(i => i != null)
+ if (info != null) {
+ return done(null, false, info)
+ }
+ LoginRateLimiter.processLoginRequest(email, function (err, isAllowed) {
+ if (err) {
+ return done(err)
+ }
+ if (!isAllowed) {
+ logger.debug({ email }, 'too many login requests')
+ return done(null, null, {
+ text: req.i18n.translate('to_many_login_requests_2_mins'),
+ type: 'error',
+ status: 429,
+ })
+ }
+ AuthenticationManager.authenticate(
+ { email },
+ password,
+ function (error, user) {
+ if (error != null) {
+ if (error instanceof ParallelLoginError) {
+ return done(null, false, { status: 429 })
+ }
+ return done(error)
+ }
+ if (
+ user &&
+ AuthenticationController.captchaRequiredForLogin(req, user)
+ ) {
+ done(null, false, {
+ text: req.i18n.translate('cannot_verify_user_not_robot'),
+ type: 'error',
+ errorReason: 'cannot_verify_user_not_robot',
+ status: 400,
+ })
+ } else if (user) {
+ // async actions
+ done(null, user)
+ } else {
+ AuthenticationController._recordFailedLogin()
+ logger.debug({ email }, 'failed log in')
+ done(null, false, {
+ text: req.i18n.translate('email_or_password_wrong_try_again'),
+ type: 'error',
+ status: 401,
+ })
+ }
+ }
+ )
+ })
+ }
+ )
+ },
+
+ captchaRequiredForLogin(req, user) {
+ switch (AuthenticationController.getAuditInfo(req).captcha) {
+ case 'disabled':
+ return false
+ case 'solved':
+ return false
+ case 'skipped': {
+ let required = false
+ if (user.lastFailedLogin) {
+ const requireCaptchaUntil =
+ user.lastFailedLogin.getTime() +
+ Settings.elevateAccountSecurityAfterFailedLogin
+ required = requireCaptchaUntil >= Date.now()
+ }
+ Metrics.inc('force_captcha_on_login', 1, {
+ status: required ? 'yes' : 'no',
+ })
+ return required
+ }
+ default:
+ throw new Error('captcha middleware missing in handler chain')
+ }
+ },
+
+ ipMatchCheck(req, user) {
+ if (req.ip !== user.lastLoginIp) {
+ NotificationsBuilder.ipMatcherAffiliation(user._id).create(
+ req.ip,
+ () => {}
+ )
+ }
+ return UserUpdater.updateUser(
+ user._id.toString(),
+ {
+ $set: { lastLoginIp: req.ip },
+ },
+ () => {}
+ )
+ },
+
+ requireLogin() {
+ const doRequest = function (req, res, next) {
+ if (next == null) {
+ next = function () {}
+ }
+ if (!SessionManager.isUserLoggedIn(req.session)) {
+ if (acceptsJson(req)) return send401WithChallenge(res)
+ return AuthenticationController._redirectToLoginOrRegisterPage(req, res)
+ } else {
+ req.user = SessionManager.getSessionUser(req.session)
+ return next()
+ }
+ }
+
+ return doRequest
+ },
+
+ oauth2Redirect(req, res, next) {
+ res.redirect(`${process.env.OAUTH_AUTH_URL}?` +
+ querystring.stringify({
+ client_id: process.env.OAUTH_CLIENT_ID,
+ response_type: "code",
+ redirect_uri: (process.env.SHARELATEX_SITE_URL + "/oauth/callback"),
+ }));
+ },
+
+ oauth2Callback(req, res, next) {
+ const code = req.query.code;
+
+//construct axios body
+ const params = new URLSearchParams()
+ params.append('grant_type', "authorization_code")
+ params.append('client_id', process.env.OAUTH_CLIENT_ID)
+ params.append('client_secret', process.env.OAUTH_CLIENT_SECRET)
+ params.append("code", code)
+ params.append('redirect_uri', (process.env.SHARELATEX_SITE_URL + "/oauth/callback"))
+
+
+ // json_body = {
+ // "grant_type": "authorization_code",
+ // client_id: process.env.OAUTH_CLIENT_ID,
+ // client_secret: process.env.OAUTH_CLIENT_SECRET,
+ // "code": code,
+ // redirect_uri: (process.env.SHARELATEX_SITE_URL + "/oauth/callback"),
+ // }
+
+ axios.post(process.env.OAUTH_ACCESS_URL, params, {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+
+ }
+ }).then(access_res => {
+
+ // console.log("respond is " + JSON.stringify(access_res.data))
+ // console.log("authorization_bearer_is " + authorization_bearer)
+ authorization_bearer = "Bearer " + access_res.data.access_token
+
+ let axios_get_config = {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": authorization_bearer,
+ },
+ params: access_res.data
+ }
+
+ axios.get(process.env.OAUTH_USER_URL, axios_get_config).then(info_res => {
+ // console.log("oauth_user: ", JSON.stringify(info_res.data));
+ if (info_res.data.err) {
+ res.json({message: info_res.data.err});
+ } else {
+ AuthenticationManager.createUserIfNotExist(info_res.data, (error, user) => {
+ if (error) {
+ res.json({message: error});
+ } else {
+ // console.log("real_user: ", user);
+ AuthenticationController.finishLogin(user, req, res, next);
+ }
+ });
+ }
+ });
+ });
+ },
+
+
+ requireOauth() {
+ // require this here because module may not be included in some versions
+ const Oauth2Server = require('../../../../modules/oauth2-server/app/src/Oauth2Server')
+ return function (req, res, next) {
+ if (next == null) {
+ next = function () {}
+ }
+ const request = new Oauth2Server.Request(req)
+ const response = new Oauth2Server.Response(res)
+ return Oauth2Server.server.authenticate(
+ request,
+ response,
+ {},
+ function (err, token) {
+ if (err) {
+ // use a 401 status code for malformed header for git-bridge
+ if (
+ err.code === 400 &&
+ err.message === 'Invalid request: malformed authorization header'
+ ) {
+ err.code = 401
+ }
+ // send all other errors
+ return res
+ .status(err.code)
+ .json({ error: err.name, error_description: err.message })
+ }
+ req.oauth = { access_token: token.accessToken }
+ req.oauth_token = token
+ req.oauth_user = token.user
+ return next()
+ }
+ )
+ }
+ },
+
+ validateUserSession: function () {
+ // Middleware to check that the user's session is still good on key actions,
+ // such as opening a a project. Could be used to check that session has not
+ // exceeded a maximum lifetime (req.session.session_created), or for session
+ // hijacking checks (e.g. change of ip address, req.session.ip_address). For
+ // now, just check that the session has been loaded from the session store
+ // correctly.
+ return function (req, res, next) {
+ // check that the session store is returning valid results
+ if (req.session && !SessionStoreManager.hasValidationToken(req)) {
+ // force user to update session
+ req.session.regenerate(() => {
+ // need to destroy the existing session and generate a new one
+ // otherwise they will already be logged in when they are redirected
+ // to the login page
+ if (acceptsJson(req)) return send401WithChallenge(res)
+ AuthenticationController._redirectToLoginOrRegisterPage(req, res)
+ })
+ } else {
+ next()
+ }
+ }
+ },
+
+ _globalLoginWhitelist: [],
+ addEndpointToLoginWhitelist(endpoint) {
+ return AuthenticationController._globalLoginWhitelist.push(endpoint)
+ },
+
+ requireGlobalLogin(req, res, next) {
+ if (
+ AuthenticationController._globalLoginWhitelist.includes(
+ req._parsedUrl.pathname
+ )
+ ) {
+ return next()
+ }
+
+ if (req.headers.authorization != null) {
+ AuthenticationController.requirePrivateApiAuth()(req, res, next)
+ } else if (SessionManager.isUserLoggedIn(req.session)) {
+ next()
+ } else {
+ logger.debug(
+ { url: req.url },
+ 'user trying to access endpoint not in global whitelist'
+ )
+ if (acceptsJson(req)) return send401WithChallenge(res)
+ AuthenticationController.setRedirectInSession(req)
+ res.redirect('/login')
+ }
+ },
+
+ validateAdmin(req, res, next) {
+ const adminDomains = Settings.adminDomains
+ if (
+ !adminDomains ||
+ !(Array.isArray(adminDomains) && adminDomains.length)
+ ) {
+ return next()
+ }
+ const user = SessionManager.getSessionUser(req.session)
+ if (!hasAdminAccess(user)) {
+ return next()
+ }
+ const email = user.email
+ if (email == null) {
+ return next(
+ new OError('[ValidateAdmin] Admin user without email address', {
+ userId: user._id,
+ })
+ )
+ }
+ if (!adminDomains.find(domain => email.endsWith(`@${domain}`))) {
+ return next(
+ new OError('[ValidateAdmin] Admin user with invalid email domain', {
+ email,
+ userId: user._id,
+ })
+ )
+ }
+ return next()
+ },
+
+ requireBasicAuth: function (userDetails) {
+ const userDetailsMap = new Map(Object.entries(userDetails))
+ return function (req, res, next) {
+ const credentials = basicAuth(req)
+ if (
+ !credentials ||
+ !checkCredentials(userDetailsMap, credentials.name, credentials.pass)
+ ) {
+ send401WithChallenge(res)
+ Metrics.inc('security.http-auth', 1, { status: 'reject' })
+ } else {
+ Metrics.inc('security.http-auth', 1, { status: 'accept' })
+ next()
+ }
+ }
+ },
+
+ requirePrivateApiAuth() {
+ return AuthenticationController.requireBasicAuth(Settings.httpAuthUsers)
+ },
+
+ setAuditInfo(req, info) {
+ if (!req.__authAuditInfo) {
+ req.__authAuditInfo = {}
+ }
+ Object.assign(req.__authAuditInfo, info)
+ },
+
+ getAuditInfo(req) {
+ return req.__authAuditInfo || {}
+ },
+
+ setRedirectInSession(req, value) {
+ if (value == null) {
+ value =
+ Object.keys(req.query).length > 0
+ ? `${req.path}?${querystring.stringify(req.query)}`
+ : `${req.path}`
+ }
+ if (
+ req.session != null &&
+ !/^\/(socket.io|js|stylesheets|img)\/.*$/.test(value) &&
+ !/^.*\.(png|jpeg|svg)$/.test(value)
+ ) {
+ const safePath = UrlHelper.getSafeRedirectPath(value)
+ return (req.session.postLoginRedirect = safePath)
+ }
+ },
+
+ _redirectToLoginOrRegisterPage(req, res) {
+ if (
+ req.query.zipUrl != null ||
+ req.query.project_name != null ||
+ req.path === '/user/subscription/new'
+ ) {
+ AuthenticationController._redirectToRegisterPage(req, res)
+ } else {
+ AuthenticationController._redirectToLoginPage(req, res)
+ }
+ },
+
+ _redirectToLoginPage(req, res) {
+ logger.debug(
+ { url: req.url },
+ 'user not logged in so redirecting to login page'
+ )
+ AuthenticationController.setRedirectInSession(req)
+ const url = `/login?${querystring.stringify(req.query)}`
+ res.redirect(url)
+ Metrics.inc('security.login-redirect')
+ },
+
+ _redirectToReconfirmPage(req, res, user) {
+ logger.debug(
+ { url: req.url },
+ 'user needs to reconfirm so redirecting to reconfirm page'
+ )
+ req.session.reconfirm_email = user != null ? user.email : undefined
+ const redir = '/user/reconfirm'
+ AsyncFormHelper.redirect(req, res, redir)
+ },
+
+ _redirectToRegisterPage(req, res) {
+ logger.debug(
+ { url: req.url },
+ 'user not logged in so redirecting to register page'
+ )
+ AuthenticationController.setRedirectInSession(req)
+ const url = `/register?${querystring.stringify(req.query)}`
+ res.redirect(url)
+ Metrics.inc('security.login-redirect')
+ },
+
+ _recordSuccessfulLogin(userId, callback) {
+ if (callback == null) {
+ callback = function () {}
+ }
+ UserUpdater.updateUser(
+ userId.toString(),
+ {
+ $set: { lastLoggedIn: new Date() },
+ $inc: { loginCount: 1 },
+ },
+ function (error) {
+ if (error != null) {
+ callback(error)
+ }
+ Metrics.inc('user.login.success')
+ callback()
+ }
+ )
+ },
+
+ _recordFailedLogin(callback) {
+ Metrics.inc('user.login.failed')
+ if (callback) callback()
+ },
+
+ _getRedirectFromSession(req) {
+ let safePath
+ const value = _.get(req, ['session', 'postLoginRedirect'])
+ if (value) {
+ safePath = UrlHelper.getSafeRedirectPath(value)
+ }
+ return safePath || null
+ },
+
+ _clearRedirectFromSession(req) {
+ if (req.session != null) {
+ delete req.session.postLoginRedirect
+ }
+ },
+}
+
+function _afterLoginSessionSetup(req, user, callback) {
+ if (callback == null) {
+ callback = function () {}
+ }
+ req.login(user, function (err) {
+ if (err) {
+ OError.tag(err, 'error from req.login', {
+ user_id: user._id,
+ })
+ return callback(err)
+ }
+ // Regenerate the session to get a new sessionID (cookie value) to
+ // protect against session fixation attacks
+ const oldSession = req.session
+ req.session.destroy(function (err) {
+ if (err) {
+ OError.tag(err, 'error when trying to destroy old session', {
+ user_id: user._id,
+ })
+ return callback(err)
+ }
+ req.sessionStore.generate(req)
+ // Note: the validation token is not writable, so it does not get
+ // transferred to the new session below.
+ for (const key in oldSession) {
+ const value = oldSession[key]
+ if (key !== '__tmp' && key !== 'csrfSecret') {
+ req.session[key] = value
+ }
+ }
+ req.session.save(function (err) {
+ if (err) {
+ OError.tag(err, 'error saving regenerated session after login', {
+ user_id: user._id,
+ })
+ return callback(err)
+ }
+ UserSessionsManager.trackSession(user, req.sessionID, function () {})
+ if (!req.deviceHistory) {
+ // Captcha disabled or SSO-based login.
+ return callback()
+ }
+ req.deviceHistory.add(user.email)
+ req.deviceHistory
+ .serialize(req.res)
+ .catch(err => {
+ logger.err({ err }, 'cannot serialize deviceHistory')
+ })
+ .finally(() => callback())
+ })
+ })
+ })
+}
+
+function _loginAsyncHandlers(req, user, anonymousAnalyticsId, isNewUser) {
+ UserHandler.setupLoginData(user, err => {
+ if (err != null) {
+ logger.warn({ err }, 'error setting up login data')
+ }
+ })
+ LoginRateLimiter.recordSuccessfulLogin(user.email, () => {})
+ AuthenticationController._recordSuccessfulLogin(user._id, () => {})
+ AuthenticationController.ipMatchCheck(req, user)
+ Analytics.recordEventForUser(user._id, 'user-logged-in', {
+ source: req.session.saml
+ ? 'saml'
+ : req.user_info?.auth_provider || 'email-password',
+ })
+ Analytics.identifyUser(user._id, anonymousAnalyticsId, isNewUser)
+
+ logger.debug(
+ { email: user.email, user_id: user._id.toString() },
+ 'successful log in'
+ )
+
+ req.session.justLoggedIn = true
+ // capture the request ip for use when creating the session
+ return (user._login_req_ip = req.ip)
+}
+
+module.exports = AuthenticationController
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationManager.js b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationManager.js
new file mode 100644
index 0000000..8519be1
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationManager.js
@@ -0,0 +1,446 @@
+const Settings = require('@overleaf/settings')
+const { User } = require('../../models/User')
+const { db, ObjectId } = require('../../infrastructure/mongodb')
+const bcrypt = require('bcrypt')
+const EmailHelper = require('../Helpers/EmailHelper')
+const {
+ InvalidEmailError,
+ InvalidPasswordError,
+ ParallelLoginError,
+} = require('./AuthenticationErrors')
+const util = require('util')
+const { Client } = require('ldapts');
+const ldapEscape = require('ldap-escape');
+const HaveIBeenPwned = require('./HaveIBeenPwned')
+
+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.modifiedCount === 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 mongodb (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)
+ HaveIBeenPwned.checkPasswordForReuseInBackground(password)
+ }
+ )
+ },
+
+ //oauth2
+ createUserIfNotExist(oauth_user, callback) {
+ const query = {
+ //name: ZHANG San
+ email: oauth_user.email
+ };
+ User.findOne(query, (error, user) => {
+ if ((!user || !user.hashedPassword)) {
+ //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({
+ email: query.email,
+ first_name: oauth_user.given_name,
+ last_name: oauth_user.family_name,
+ password: pass
+ },
+ function (error, user) {
+ if (error) {
+ return callback(error, null);
+ }
+ user.admin = false
+ user.emails[0].confirmedAt = Date.now()
+ user.save()
+ console.log("user %s added to local library", query.email)
+ User.findOne(query, (error, user) => {
+ if (error) {
+ return callback(error, null);
+ }
+ if (user && user.hashedPassword) {
+ return callback(null, user);
+ } else {
+ return callback("Unknown error", null);
+ }
+ }
+ )
+ })
+ } else {
+ return callback(null, user);
+ }
+ });
+ },
+
+ //LDAP
+ 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")
+ //console.log("Creating User:" + JSON.stringify(query) + "Random Pass" + pass)
+
+ const userRegHand = require('../User/UserRegistrationHandler.js')
+ userRegHand.registerNewUser({
+ email: mail,
+ first_name: firstname,
+ last_name: lastname,
+ password: pass
+ },
+ function (error, user) {
+ if (error) {
+ console.log(error)
+ }
+ user.email = mail
+ user.isAdmin = isAdmin
+ 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) {
+ if ( process.env.ALLOW_EMAIL_LOGIN && user && user.hashedPassword) {
+ console.log("email login for existing user " + query.email)
+ // check passwd against local db
+ bcrypt.compare(password, user.hashedPassword, function (error, match) {
+ if (match) {
+ console.log("Local user password match")
+ AuthenticationManager.login(user, password, callback)
+ } else {
+ console.log("Local user password mismatch, trying LDAP")
+ // 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
+ },
+
+ 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 object.
+ validatePassword(password, email) {
+ 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' },
+ })
+ }
+ if (typeof email === 'string' && email !== '') {
+ const startOfEmail = email.split('@')[0]
+ if (
+ password.indexOf(email) !== -1 ||
+ password.indexOf(startOfEmail) !== -1
+ ) {
+ return new InvalidPasswordError({
+ message: 'password contains part of email address',
+ info: { code: 'contains_email' },
+ })
+ }
+ }
+ return null
+ },
+
+ setUserPassword(user, password, callback) {
+ AuthenticationManager.setUserPasswordInV2(user, password, callback)
+ },
+
+ checkRounds(user, hashedPassword, password, callback) {
+ // Temporarily disable this function, TODO: re-enable this
+ 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, 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(user, password, callback) {
+ if (!user || !user.email || !user._id) {
+ return callback(new Error('invalid user object'))
+ }
+ const validationError = this.validatePassword(password, user.email)
+ if (validationError) {
+ return callback(validationError)
+ }
+ this.hashPassword(password, function (error, hash) {
+ if (error) {
+ return callback(error)
+ }
+ db.users.updateOne(
+ {
+ _id: ObjectId(user._id.toString()),
+ },
+ {
+ $set: {
+ hashedPassword: hash,
+ },
+ $unset: {
+ password: true,
+ },
+ },
+ function (updateError, result) {
+ if (updateError) {
+ return callback(updateError)
+ }
+ _checkWriteResult(result, callback)
+ HaveIBeenPwned.checkPasswordForReuseInBackground(password)
+ }
+ )
+ })
+ },
+
+ _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 ldap_reader = process.env.LDAP_BIND_USER
+ const ldap_reader_pass = process.env.LDAP_BIND_PW
+ const ldap_base = process.env.LDAP_BASE
+
+ var mail = query.email
+ var uid = query.email.split('@')[0]
+ var firstname = ""
+ var lastname = ""
+ var isAdmin = false
+ var userDn = ""
+
+ //replace all appearences of %u with uid and all %m with mail:
+ const replacerUid = new RegExp("%u", "g")
+ const replacerMail = new RegExp("%m","g")
+ const filterstr = process.env.LDAP_USER_FILTER.replace(replacerUid, ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`) //replace all appearances
+ // check bind
+ try {
+ if(process.env.LDAP_BINDDN){ //try to bind directly with the user trying to log in
+ userDn = process.env.LDAP_BINDDN.replace(replacerUid,ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`);
+ await client.bind(userDn,password);
+ }else{// use fixed bind user
+ await client.bind(ldap_reader, ldap_reader_pass);
+ }
+ } catch (ex) {
+ if(process.env.LDAP_BINDDN){
+ console.log("Could not bind user: " + userDn);
+ }else{
+ console.log("Could not bind LDAP reader: " + ldap_reader + " err: " + String(ex))
+ }
+ return callback(null, null)
+ }
+
+ // get user data
+ try {
+ const {searchEntries, searchRef,} = await client.search(ldap_base, {
+ scope: 'sub',
+ filter: filterstr ,
+ });
+ await searchEntries
+ console.log(JSON.stringify(searchEntries))
+ if (searchEntries[0]) {
+ mail = searchEntries[0].mail
+ uid = searchEntries[0].uid
+ firstname = searchEntries[0].givenName
+ lastname = searchEntries[0].sn
+ if(!process.env.LDAP_BINDDN){ //dn is already correctly assembled
+ userDn = searchEntries[0].dn
+ }
+ console.log("Found user: " + mail + " Name: " + firstname + " " + lastname + " DN: " + userDn)
+ }
+ } 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
+ // does not matter - admin is deactivated: managed through ldap
+ if (process.env.LDAP_ADMIN_GROUP_FILTER) {
+ const adminfilter = process.env.LDAP_ADMIN_GROUP_FILTER.replace(replacerUid, ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`)
+ adminEntry = await client.search(ldap_base, {
+ scope: 'sub',
+ filter: adminfilter,
+ });
+ await adminEntry;
+ //console.log("Admin Search response:" + JSON.stringify(adminEntry.searchEntries))
+ if (adminEntry.searchEntries[0]) {
+ 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 == "" || userDn == "") {
+ console.log("Mail / userDn not set - exit. This should not happen - please set mail-entry in ldap.")
+ return callback(null, null)
+ }
+
+ if(!process.env.BINDDN){//since we used a fixed bind user to obtain the correct userDn we need to bind again to authenticate
+ try {
+ await client.bind(userDn, password);
+ } catch (ex) {
+ console.log("Could not bind User: " + userDn + " err: " + String(ex))
+ return callback(null, null)
+ } finally{
+ await client.unbind()
+ }
+ }
+ //console.log("Logging in user: " + 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),
+ setUserPassword: util.promisify(AuthenticationManager.setUserPassword),
+}
+
+module.exports = AuthenticationManager
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/ContactController.js b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/ContactController.js
new file mode 100644
index 0000000..4146982
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/ContactController.js
@@ -0,0 +1,140 @@
+/* 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 SessionManager = require('../Authentication/SessionManager')
+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)
+ const user_id = SessionManager.getLoggedInUserId(req.session)
+ 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 === undefined || !(process.env.LDAP_CONTACTS.toLowerCase() === 'true')) {
+ return contacts
+ }
+ const client = new Client({
+ url: process.env.LDAP_SERVER,
+ });
+
+ // if we need a ldap user try to bind
+ if (process.env.LDAP_BIND_USER) {
+ try {
+ await client.bind(process.env.LDAP_BIND_USER, process.env.LDAP_BIND_PW);
+ } catch (ex) {
+ console.log("Could not bind LDAP reader user: " + String(ex) )
+ }
+ }
+
+ 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_CONTACT_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"
+ // Only add to contacts if entry is not there.
+ if(contacts.indexOf(entry) === -1) {
+ 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',
+ }
+ },
+}
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-index.pug b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-index.pug
new file mode 100644
index 0000000..88e264b
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/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/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug
new file mode 100644
index 0000000..c7131a3
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug
@@ -0,0 +1,79 @@
+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 }}
+ tab(heading="Open/Close Editor" bookmarkable-tab="open-close-editor")
+ if hasFeature('saas')
+ | The "Open/Close Editor" feature is not available in SAAS.
+ else
+ .row-spaced
+ form(method='post',action='/admin/closeEditor')
+ input(name="_csrf", type="hidden", value=csrfToken)
+ button.btn.btn-danger(type="submit") Close Editor
+ p.small Will stop anyone opening the editor. Will NOT disconnect already connected users.
+
+ .row-spaced
+ form(method='post',action='/admin/disconnectAllUsers')
+ input(name="_csrf", type="hidden", value=csrfToken)
+ button.btn.btn-danger(type="submit") Disconnect all users
+ p.small Will force disconnect all users with the editor open. Make sure to close the editor first to avoid them reconnecting.
+
+ .row-spaced
+ form(method='post',action='/admin/openEditor')
+ input(name="_csrf", type="hidden", value=csrfToken)
+ button.btn.btn-danger(type="submit") Reopen Editor
+ p.small Will reopen the editor after closing.
+
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authcontroller.diff b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authcontroller.diff
new file mode 100644
index 0000000..c45a271
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authcontroller.diff
@@ -0,0 +1,103 @@
+23,24d22
+< const axios = require('axios').default
+< const Path = require('path')
+195c193
+< logger.debug({ email }, 'too many login requests')
+---
+> logger.log({ email }, 'too many login requests')
+227c225
+< logger.debug({ email }, 'failed log in')
+---
+> logger.log({ email }, 'failed log in')
+298,364d295
+< oauth2Redirect(req, res, next) {
+< res.redirect(`${process.env.OAUTH_AUTH_URL}?` +
+< querystring.stringify({
+< client_id: process.env.OAUTH_CLIENT_ID,
+< response_type: "code",
+< redirect_uri: (process.env.SHARELATEX_SITE_URL + "/oauth/callback"),
+< }));
+< },
+<
+< oauth2Callback(req, res, next) {
+< const code = req.query.code;
+<
+< //construct axios body
+< const params = new URLSearchParams()
+< params.append('grant_type', "authorization_code")
+< params.append('client_id', process.env.OAUTH_CLIENT_ID)
+< params.append('client_secret', process.env.OAUTH_CLIENT_SECRET)
+< params.append("code", code)
+< params.append('redirect_uri', (process.env.SHARELATEX_SITE_URL + "/oauth/callback"))
+<
+<
+< // json_body = {
+< // "grant_type": "authorization_code",
+< // client_id: process.env.OAUTH_CLIENT_ID,
+< // client_secret: process.env.OAUTH_CLIENT_SECRET,
+< // "code": code,
+< // redirect_uri: (process.env.SHARELATEX_SITE_URL + "/oauth/callback"),
+< // }
+<
+< axios.post(process.env.OAUTH_ACCESS_URL, params, {
+< headers: {
+< "Content-Type": "application/x-www-form-urlencoded",
+<
+< }
+< }).then(access_res => {
+<
+< // console.log("respond is " + JSON.stringify(access_res.data))
+< // console.log("authorization_bearer_is " + authorization_bearer)
+< authorization_bearer = "Bearer " + access_res.data.access_token
+<
+< let axios_get_config = {
+< headers: {
+< "Content-Type": "application/x-www-form-urlencoded",
+< "Authorization": authorization_bearer,
+< },
+< params: access_res.data
+< }
+<
+< axios.get(process.env.OAUTH_USER_URL, axios_get_config).then(info_res => {
+< // console.log("oauth_user: ", JSON.stringify(info_res.data));
+< if (info_res.data.err) {
+< res.json({message: info_res.data.err});
+< } else {
+< AuthenticationManager.createUserIfNotExist(info_res.data, (error, user) => {
+< if (error) {
+< res.json({message: error});
+< } else {
+< // console.log("real_user: ", user);
+< AuthenticationController.finishLogin(user, req, res, next);
+< }
+< });
+< }
+< });
+< });
+< },
+<
+<
+444c375
+< logger.debug(
+---
+> logger.log(
+477c408
+< email,
+---
+> email: email,
+547c478
+< logger.debug(
+---
+> logger.log(
+558c489
+< logger.debug(
+---
+> logger.log(
+568c499
+< logger.debug(
+---
+> logger.log(
+689c620
+< logger.debug(
+---
+> logger.log(
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authmanager.diff b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authmanager.diff
new file mode 100644
index 0000000..841804d
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authmanager.diff
@@ -0,0 +1,297 @@
+12,13d11
+< const { Client } = require('ldapts');
+< const ldapEscape = require('ldap-escape');
+34,92c32,36
+< //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)
+< HaveIBeenPwned.checkPasswordForReuseInBackground(password)
+< }
+< )
+< },
+<
+< //oauth2
+< createUserIfNotExist(oauth_user, callback) {
+< const query = {
+< //name: ZHANG San
+< email: oauth_user.email
+< };
+< User.findOne(query, (error, user) => {
+< if ((!user || !user.hashedPassword)) {
+< //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({
+< email: query.email,
+< first_name: oauth_user.given_name,
+< last_name: oauth_user.family_name,
+< password: pass
+< },
+< function (error, user) {
+< if (error) {
+< return callback(error, null);
+< }
+< user.admin = false
+< user.emails[0].confirmedAt = Date.now()
+< user.save()
+< console.log("user %s added to local library", query.email)
+< User.findOne(query, (error, user) => {
+< if (error) {
+< return callback(error, null);
+< }
+< if (user && user.hashedPassword) {
+< return callback(null, user);
+< } else {
+< return callback("Unknown error", null);
+< }
+< }
+< )
+< })
+< } else {
+< return callback(null, user);
+---
+> if (error) {
+> return callback(error)
+> }
+> if (!user || !user.hashedPassword) {
+> return callback(null, null)
+94,138d37
+< });
+< },
+<
+< //LDAP
+< 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")
+< //console.log("Creating User:" + JSON.stringify(query) + "Random Pass" + pass)
+<
+< const userRegHand = require('../User/UserRegistrationHandler.js')
+< userRegHand.registerNewUser({
+< email: mail,
+< first_name: firstname,
+< last_name: lastname,
+< password: pass
+< },
+< function (error, user) {
+< if (error) {
+< console.log(error)
+< }
+< user.email = mail
+< user.isAdmin = isAdmin
+< 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) {
+< if ( process.env.ALLOW_EMAIL_LOGIN && user && user.hashedPassword) {
+< console.log("email login for existing user " + query.email)
+< // check passwd against local db
+140,146c39,40
+< if (match) {
+< console.log("Local user password match")
+< AuthenticationManager.login(user, password, callback)
+< } else {
+< console.log("Local user password mismatch, trying LDAP")
+< // check passwd against ldap
+< AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user)
+---
+> if (error) {
+> return callback(error)
+147a42,73
+> const update = { $inc: { loginEpoch: 1 } }
+> if (!match) {
+> update.$set = { lastFailedLogin: new Date() }
+> }
+> User.updateOne(
+> { _id: user._id, loginEpoch: user.loginEpoch },
+> update,
+> {},
+> (err, result) => {
+> if (err) {
+> return callback(err)
+> }
+> if (result.nModified !== 1) {
+> return callback(new ParallelLoginError())
+> }
+> if (!match) {
+> return callback(null, null)
+> }
+> AuthenticationManager.checkRounds(
+> user,
+> user.hashedPassword,
+> password,
+> function (err) {
+> if (err) {
+> return callback(err)
+> }
+> callback(null, user)
+> HaveIBeenPwned.checkPasswordForReuseInBackground(password)
+> }
+> )
+> }
+> )
+149,153c75
+< } 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
+---
+> })
+157,158d78
+< // we use the emailadress from the ldap
+< // therefore we do not enforce checks here
+160,162c80,82
+< //if (!parsed) {
+< // return new InvalidEmailError({ message: 'email not valid' })
+< //}
+---
+> if (!parsed) {
+> return new InvalidEmailError({ message: 'email not valid' })
+> }
+320,437d239
+<
+<
+< async ldapAuth(query, password, onSuccessCreateUserIfNotExistent, callback, user) {
+< const client = new Client({
+< url: process.env.LDAP_SERVER,
+< });
+<
+< const ldap_reader = process.env.LDAP_BIND_USER
+< const ldap_reader_pass = process.env.LDAP_BIND_PW
+< const ldap_base = process.env.LDAP_BASE
+<
+< var mail = query.email
+< var uid = query.email.split('@')[0]
+< var firstname = ""
+< var lastname = ""
+< var isAdmin = false
+< var userDn = ""
+<
+< //replace all appearences of %u with uid and all %m with mail:
+< const replacerUid = new RegExp("%u", "g")
+< const replacerMail = new RegExp("%m","g")
+< const filterstr = process.env.LDAP_USER_FILTER.replace(replacerUid, ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`) //replace all appearances
+< // check bind
+< try {
+< if(process.env.LDAP_BINDDN){ //try to bind directly with the user trying to log in
+< userDn = process.env.LDAP_BINDDN.replace(replacerUid,ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`);
+< await client.bind(userDn,password);
+< }else{// use fixed bind user
+< await client.bind(ldap_reader, ldap_reader_pass);
+< }
+< } catch (ex) {
+< if(process.env.LDAP_BINDDN){
+< console.log("Could not bind user: " + userDn);
+< }else{
+< console.log("Could not bind LDAP reader: " + ldap_reader + " err: " + String(ex))
+< }
+< return callback(null, null)
+< }
+<
+< // get user data
+< try {
+< const {searchEntries, searchRef,} = await client.search(ldap_base, {
+< scope: 'sub',
+< filter: filterstr ,
+< });
+< await searchEntries
+< console.log(JSON.stringify(searchEntries))
+< if (searchEntries[0]) {
+< mail = searchEntries[0].mail
+< uid = searchEntries[0].uid
+< firstname = searchEntries[0].givenName
+< lastname = searchEntries[0].sn
+< if(!process.env.LDAP_BINDDN){ //dn is already correctly assembled
+< userDn = searchEntries[0].dn
+< }
+< console.log("Found user: " + mail + " Name: " + firstname + " " + lastname + " DN: " + userDn)
+< }
+< } 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
+< // does not matter - admin is deactivated: managed through ldap
+< if (process.env.LDAP_ADMIN_GROUP_FILTER) {
+< const adminfilter = process.env.LDAP_ADMIN_GROUP_FILTER.replace(replacerUid, ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`)
+< adminEntry = await client.search(ldap_base, {
+< scope: 'sub',
+< filter: adminfilter,
+< });
+< await adminEntry;
+< //console.log("Admin Search response:" + JSON.stringify(adminEntry.searchEntries))
+< if (adminEntry.searchEntries[0]) {
+< 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 == "" || userDn == "") {
+< console.log("Mail / userDn not set - exit. This should not happen - please set mail-entry in ldap.")
+< return callback(null, null)
+< }
+<
+< if(!process.env.BINDDN){//since we used a fixed bind user to obtain the correct userDn we need to bind again to authenticate
+< try {
+< await client.bind(userDn, password);
+< } catch (ex) {
+< console.log("Could not bind User: " + userDn + " err: " + String(ex))
+< return callback(null, null)
+< } finally{
+< await client.unbind()
+< }
+< }
+< //console.log("Logging in user: " + 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)
+< }
+< })
+< }
+<
+<
+<
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/contactcontroller.diff b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/contactcontroller.diff
new file mode 100644
index 0000000..0aa4199
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/contactcontroller.diff
@@ -0,0 +1,133 @@
+16d15
+< const AuthenticationController = require('../Authentication/AuthenticationController')
+20c19
+< const logger = require('logger-sharelatex')
+---
+> const logger = require('@overleaf/logger')
+22d20
+< const { Client } = require('ldapts');
+26d23
+< // const user_id = AuthenticationController.getLoggedInUserId(req)
+48,78c45,55
+< // 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))
+---
+> // 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]
+> )
+80,99c57,60
+< }
+< )
+< })
+< },
+< async getLdapContacts(contacts) {
+< if (process.env.LDAP_CONTACTS === undefined || !(process.env.LDAP_CONTACTS.toLowerCase() === 'true')) {
+< return contacts
+< }
+< const client = new Client({
+< url: process.env.LDAP_SERVER,
+< });
+<
+< // if we need a ldap user try to bind
+< if (process.env.LDAP_BIND_USER) {
+< try {
+< await client.bind(process.env.LDAP_BIND_USER, process.env.LDAP_BIND_PW);
+< } catch (ex) {
+< console.log("Could not bind LDAP reader user: " + String(ex) )
+< }
+< }
+---
+> // Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
+> contacts = contacts.filter(c => !c.holdingAccount)
+>
+> contacts = contacts.map(ContactsController._formatContact)
+101,118c62,79
+< 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_CONTACT_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"
+< // Only add to contacts if entry is not there.
+< if(contacts.indexOf(entry) === -1) {
+< contacts.push(entry);
+< }
+---
+> 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.json({
+> contacts,
+> })
+> }
+> )
+> }
+> )
+120,129c81
+< } 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
+< }
+---
+> )
+130a83
+>
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.diff b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.diff
new file mode 100644
index 0000000..46f77bb
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.diff
@@ -0,0 +1,54 @@
+1,4c1
+< extends ../layout
+<
+< block vars
+< - metadata = { viewport: true }
+---
+> extends ../layout-marketing
+7c4
+< main.content.content-alt
+---
+> main.content.content-alt#main-content
+14c11
+< form(async-form="login", name="loginForm", action='/login', method="POST", ng-cloak)
+---
+> form(data-ol-async-form, name="loginForm", action='/login', method="POST")
+16c13
+< form-messages(for="loginForm")
+---
+> +formMessages()
+23,25c20
+< ng-model="email",
+< ng-model-options="{ updateOn: 'blur' }",
+< focus="true"
+---
+> autofocus="true"
+27,28d21
+< span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty")
+< | #{translate("must_be_email_address")}
+35d27
+< ng-model="password"
+37,38d28
+< span.small.text-primary(ng-show="loginForm.password.$invalid && loginForm.password.$dirty")
+< | #{translate("required")}
+40c30
+< button.btn-primary.btn.btn-block(
+---
+> button.btn-primary.btn(
+42c32
+< ng-disabled="loginForm.inflight"
+---
+> data-ol-disabled-inflight
+44,51c34,36
+< span(ng-show="!loginForm.inflight") #{translate("login_with_email")}
+< span(ng-show="loginForm.inflight") #{translate("logging_in")}…
+< .form-group.text-center(style="padding-top: 10px")
+< a.btn-block.login-btn(href="/oauth/redirect" style='padding-left: 0px')
+< | Log in via SUSTech CRA SSO / CAS
+< p
+< | homepage-notice-html
+<
+---
+> span(data-ol-inflight="idle") #{translate("login")}
+> span(hidden data-ol-inflight="pending") #{translate("logging_in")}…
+> a.pull-right(href='/user/password/reset') #{translate("forgot_your_password")}?
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.pug b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.pug
new file mode 100644
index 0000000..7af4bae
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.pug
@@ -0,0 +1,51 @@
+extends ../layout
+
+block vars
+ - metadata = { viewport: true }
+
+block content
+ main.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(
+ type='email',
+ name='email',
+ required,
+ placeholder='email@example.com',
+ ng-model="email",
+ ng-model-options="{ updateOn: 'blur' }",
+ 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.btn-block(
+ type='submit',
+ ng-disabled="loginForm.inflight"
+ )
+ span(ng-show="!loginForm.inflight") #{translate("login_with_email")}
+ span(ng-show="loginForm.inflight") #{translate("logging_in")}…
+ .form-group.text-center(style="padding-top: 10px")
+ a.btn-block.login-btn(href="/oauth/redirect" style='padding-left: 0px')
+ | Log in via SUSTech CRA SSO / CAS
+ p
+ | homepage-notice-html
+
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/navbar.pug b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/navbar.pug
new file mode 100644
index 0000000..f391630
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/navbar.pug
@@ -0,0 +1,84 @@
+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
+ a(href="/admin") Admin
+
+
+ // 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')}
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/router-append.js b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/router-append.js
new file mode 100644
index 0000000..68e1c49
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/router-append.js
@@ -0,0 +1,6 @@
+ webRouter.get('/oauth/redirect', AuthenticationController.oauth2Redirect)
+ webRouter.get('/oauth/callback', AuthenticationController.oauth2Callback)
+ AuthenticationController.addEndpointToLoginWhitelist('/oauth/redirect')
+ AuthenticationController.addEndpointToLoginWhitelist('/oauth/callback')
+ webRouter.get('*', ErrorController.notFound)
+}
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.diff b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.diff
new file mode 100644
index 0000000..0f697b9
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.diff
@@ -0,0 +1,200 @@
+2a3,9
+> block append meta
+> meta(name="ol-reconfirmationRemoveEmail", content=reconfirmationRemoveEmail)
+> meta(name="ol-reconfirmedViaSAML", content=reconfirmedViaSAML)
+> meta(name="ol-passwordStrengthOptions", data-type="json", content=settings.passwordStrengthOptions || {})
+> meta(name="ol-oauthProviders", data-type="json", content=oauthProviders)
+> meta(name="ol-thirdPartyIds", data-type="json", content=thirdPartyIds)
+>
+4c11,15
+< .content.content-alt
+---
+> main.content.content-alt#main-content(
+> event-tracking-mb="true"
+> event-tracking="settings-view"
+> event-tracking-trigger="load"
+> )
+25,31c36,57
+< // show the email, non-editable
+< .form-group
+< label.control-label #{translate("email")}
+< div.form-control(
+< readonly="true",
+< ng-non-bindable
+< ) #{user.email}
+---
+> if !externalAuthenticationSystemUsed()
+> .form-group
+> label(for='email') #{translate("email")}
+> input.form-control(
+> id="email"
+> type='email',
+> name='email',
+> placeholder="email@example.com"
+> required,
+> ng-model="email",
+> ng-model-options="{ updateOn: 'blur' }"
+> )
+> span.small.text-danger(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")
+> | #{translate("must_be_email_address")}
+> else
+> // show the email, non-editable
+> .form-group
+> label.control-label #{translate("email")}
+> div.form-control(
+> readonly="true",
+> ng-non-bindable
+> ) #{user.email}
+76,81c102,164
+< 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.]
+---
+> h3 #{translate("change_password")}
+> if externalAuthenticationSystemUsed() && !settings.overleaf
+> p
+> | Password settings are managed externally
+> else if !hasPassword
+> p
+> | #[a(href="/user/password/reset", target='_blank') #{translate("no_existing_password")}]
+> else
+> - var submitAction
+> - submitAction = '/user/password/update'
+> form(
+> async-form="changepassword"
+> name="changePasswordForm"
+> action=submitAction
+> method="POST"
+> novalidate
+> )
+> input(type="hidden", name="_csrf", value=csrfToken)
+> .form-group
+> label(for='currentPassword') #{translate("current_password")}
+> input.form-control(
+> id="currentPassword"
+> type='password',
+> name='currentPassword',
+> placeholder='*********',
+> ng-model="currentPassword",
+> required
+> )
+> span.small.text-danger(ng-show="changePasswordForm.currentPassword.$invalid && changePasswordForm.currentPassword.$dirty" aria-live="polite")
+> | #{translate("required")}
+> .form-group
+> label(for='passwordField') #{translate("new_password")}
+> input.form-control(
+> id='passwordField',
+> type='password',
+> name='newPassword1',
+> placeholder='*********',
+> ng-model="newPassword1",
+> required,
+> complex-password
+> )
+> span.small.text-danger(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.newPassword1.$dirty", ng-bind-html="complexPasswordErrorMessage" aria-live="polite")
+> .form-group
+> label(for='newPassword2') #{translate("confirm_new_password")}
+> input.form-control(
+> id="newPassword2"
+> type='password',
+> name='newPassword2',
+> placeholder='*********',
+> ng-model="newPassword2",
+> equals="passwordField"
+> )
+> span.small.text-danger(ng-show="changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$dirty" aria-live="polite")
+> | #{translate("doesnt_match")}
+> span.small.text-danger(ng-show="!changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty" aria-live="polite")
+> | #{translate("invalid_password")}
+> .form-group
+> form-messages(aria-live="polite" for="changePasswordForm")
+> .actions
+> button.btn.btn-primary(
+> type='submit',
+> ng-disabled="changePasswordForm.$invalid"
+> ) #{translate("change")}
+85a169,181
+> if hasFeature('saas')
+> h3
+> | #{translate("sharelatex_beta_program")}
+>
+> if (user.betaProgram)
+> p.small
+> | #{translate("beta_program_already_participating")}
+>
+> div
+> a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")}
+>
+> hr
+>
+87,92c183,186
+< | Contact
+< div
+< | If you need any help, please contact your sysadmins.
+<
+< p #{translate("need_to_leave")}
+< a(href, ng-click="deleteAccount()") #{translate("delete_your_account")}
+---
+> | #{translate("sessions")}
+>
+> div
+> a(id="sessions-link", href="/user/sessions") #{translate("manage_sessions")}
+93a188,218
+> if hasFeature('oauth')
+> hr
+> include settings/user-oauth
+>
+> if hasFeature('saas') && (!externalAuthenticationSystemUsed() || (settings.createV1AccountOnLogin && settings.overleaf))
+> hr
+> p.small
+> | #{translate("newsletter_info_and_unsubscribe")}
+> a(
+> href,
+> ng-click="unsubscribe()",
+> ng-show="subscribed && !unsubscribing"
+> ) #{translate("unsubscribe")}
+> span(
+> ng-show="unsubscribing"
+> )
+> i.fa.fa-spin.fa-refresh(aria-hidden="true")
+> | #{translate("unsubscribing")}
+> span.text-success(
+> ng-show="!subscribed"
+> )
+> i.fa.fa-check(aria-hidden="true")
+> | #{translate("unsubscribed")}
+>
+> if !settings.overleaf && user.overleaf
+> p
+> | Please note: If you have linked your account with Overleaf
+> | v2, then deleting your ShareLaTeX account will also delete
+> | account and all of it's associated projects and data.
+> p #{translate("need_to_leave")}
+> a(href, ng-click="deleteAccount()") #{translate("delete_your_account")}
+100c225
+< p !{translate("delete_account_warning_message_3")}
+---
+> p !{translate("delete_account_warning_message_3", {}, ['strong'])}
+136,143d260
+< ng-model="state.confirmV1Purge"
+< ng-change="checkValidation()"
+< ).pull-left
+< label(style="display: inline") &nbsp;I have left, purged or imported my projects on Overleaf v1 (if any) &nbsp;
+<
+< div.confirmation-checkbox-wrapper
+< input(
+< type="checkbox"
+147c264
+< label(style="display: inline") &nbsp;I understand this will delete all projects in my Overleaf v2 account (and ShareLaTeX account, if any) with email address #[em {{ userDefaultEmail }}]
+---
+> label(style="display: inline") &nbsp;I understand this will delete all projects in my Overleaf account with email address #[em {{ userDefaultEmail }}]
+175,178c292
+< span(ng-show="state.inflight") #{translate("deleting")}...
+<
+< script(type='text/javascript').
+< window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
+---
+> span(ng-show="state.inflight") #{translate("deleting")}…
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.pug b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.pug
new file mode 100644
index 0000000..8cdd18c
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.pug
@@ -0,0 +1,178 @@
+extends ../layout
+
+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)
+
+ if hasFeature('affiliations')
+ include settings/user-affiliations
+
+ .row
+ .col-md-5
+ 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}
+
+ 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.
+
+ 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") &nbsp;I have left, purged or imported my projects on Overleaf v1 (if any) &nbsp;
+
+ div.confirmation-checkbox-wrapper
+ input(
+ type="checkbox"
+ ng-model="state.confirmSharelatexDelete"
+ ng-change="checkValidation()"
+ ).pull-left
+ label(style="display: inline") &nbsp;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/overleaf-mods/overleaf-ldap-oauth2/renovate.json b/overleaf-mods/overleaf-ldap-oauth2/renovate.json
new file mode 100644
index 0000000..39a2b6e
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/renovate.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:base"
+ ]
+}