diff options
Diffstat (limited to 'overleaf-mods/overleaf-ldap-oauth2')
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") I have left, purged or imported my projects on Overleaf v1 (if any) +< +< div.confirmation-checkbox-wrapper +< input( +< type="checkbox" +147c264 +< label(style="display: inline") I understand this will delete all projects in my Overleaf v2 account (and ShareLaTeX account, if any) with email address #[em {{ userDefaultEmail }}] +--- +> label(style="display: inline") 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") I have left, purged or imported my projects on Overleaf v1 (if any) + + div.confirmation-checkbox-wrapper + input( + type="checkbox" + ng-model="state.confirmSharelatexDelete" + ng-change="checkValidation()" + ).pull-left + label(style="display: inline") I understand this will delete all projects in my Overleaf v2 account (and ShareLaTeX account, if any) with email address #[em {{ userDefaultEmail }}] + + div(ng-if="state.error") + div.alert.alert-danger(ng-switch="state.error.code") + span(ng-switch-when="InvalidCredentialsError") + | #{translate('email_or_password_wrong_try_again')} + span(ng-switch-when="SubscriptionAdminDeletionError") + | #{translate('subscription_admins_cannot_be_deleted')} + span(ng-switch-when="UserDeletionError") + | #{translate('user_deletion_error')} + span(ng-switch-default) + | #{translate('generic_something_went_wrong')} + if settings.createV1AccountOnLogin && settings.overleaf + div(ng-if="state.error && state.error.code == 'InvalidCredentialsError'") + div.alert.alert-info + | If you can't remember your password, or if you are using Single-Sign-On with another provider + | to sign in (such as Twitter or Google), please + | #[a(href="/user/password/reset", target='_blank') reset your password], + | and try again. + .modal-footer + button.btn.btn-default( + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-danger( + ng-disabled="!state.isValid || state.inflight" + ng-click="delete()" + ) + span(ng-hide="state.inflight") #{translate("delete")} + span(ng-show="state.inflight") #{translate("deleting")}... + + script(type='text/javascript'). + window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})} diff --git a/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" + ] +} |