diff --git a/extensions/LdapAuthentication/.gitignore b/extensions/LdapAuthentication/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..8266927676ba856937fd752888669bdae98bad50 --- /dev/null +++ b/extensions/LdapAuthentication/.gitignore @@ -0,0 +1,7 @@ +.svn +*~ +*.kate-swp +.*.swp +/node_modules +/vendor +composer.lock diff --git a/extensions/LdapAuthentication/.gitreview b/extensions/LdapAuthentication/.gitreview new file mode 100644 index 0000000000000000000000000000000000000000..b703a586cb82cdcdfb87acaeb34614c71e819002 --- /dev/null +++ b/extensions/LdapAuthentication/.gitreview @@ -0,0 +1,5 @@ +[gerrit] +host=gerrit.wikimedia.org +port=29418 +project=mediawiki/extensions/LdapAuthentication.git +track=1 diff --git a/extensions/LdapAuthentication/.phpcs.xml b/extensions/LdapAuthentication/.phpcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..65b5506938eb8fbd2639ab20de403f19e616afef --- /dev/null +++ b/extensions/LdapAuthentication/.phpcs.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ruleset> + <rule ref="./vendor/mediawiki/mediawiki-codesniffer/MediaWiki"> + <exclude name="MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic" /> + <exclude name="MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName" /> + <exclude name="Squiz.Scope.MethodScope.Missing" /> + </rule> + <rule ref="MediaWiki.NamingConventions.PrefixedGlobalFunctions"> + <properties> + <property name="ignoreList" type="array" value="AutoAuthSetup" /> + </properties> + </rule> + <file>.</file> + <arg name="extensions" value="php,php5,inc"/> + <arg name="encoding" value="UTF-8"/> +</ruleset> diff --git a/extensions/LdapAuthentication/CODE_OF_CONDUCT.md b/extensions/LdapAuthentication/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000000000000000000000000000000..d8e5d0872d9a35758fec752aa12953930a0613d4 --- /dev/null +++ b/extensions/LdapAuthentication/CODE_OF_CONDUCT.md @@ -0,0 +1 @@ +The development of this software is covered by a [Code of Conduct](https://www.mediawiki.org/wiki/Code_of_Conduct). diff --git a/extensions/LdapAuthentication/COPYING b/extensions/LdapAuthentication/COPYING new file mode 100644 index 0000000000000000000000000000000000000000..d159169d1050894d3ea3b98e1c965c4058208fe1 --- /dev/null +++ b/extensions/LdapAuthentication/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + 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 +this service 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. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + 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 +convey 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 General Public License as published by + the Free Software Foundation; either version 2 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/extensions/LdapAuthentication/Gruntfile.js b/extensions/LdapAuthentication/Gruntfile.js new file mode 100644 index 0000000000000000000000000000000000000000..d9b72ffd44b08bb98a50a7a4c4288be04462ac2c --- /dev/null +++ b/extensions/LdapAuthentication/Gruntfile.js @@ -0,0 +1,29 @@ +/*jshint node:true */ +module.exports = function ( grunt ) { + grunt.loadNpmTasks( 'grunt-banana-checker' ); + grunt.loadNpmTasks( 'grunt-jsonlint' ); + grunt.loadNpmTasks( 'grunt-contrib-jshint' ); + + grunt.initConfig( { + banana: { + all: 'i18n/' + }, + jshint: { + all: [ + '**/*.js', + '!node_modules/**', + '!vendor/**' + ] + }, + jsonlint: { + all: [ + '**/*.json', + '!node_modules/**', + '!vendor/**' + ] + } + } ); + + grunt.registerTask( 'test', [ 'jsonlint', 'banana', 'jshint' ] ); + grunt.registerTask( 'default', 'test' ); +}; diff --git a/extensions/LdapAuthentication/LdapAuthentication.php b/extensions/LdapAuthentication/LdapAuthentication.php new file mode 100644 index 0000000000000000000000000000000000000000..c4b5bb586bebca8d1665674081b5facca9e4b55c --- /dev/null +++ b/extensions/LdapAuthentication/LdapAuthentication.php @@ -0,0 +1,153 @@ +<?php +/** + * Copyright (C) 2004 Ryan Lane <http://www.mediawiki.org/wiki/User:Ryan_lane> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * LdapAuthentication plugin. LDAP Authentication and authorization integration with MediaWiki. + * + * @file + * @ingroup MediaWiki + */ + +/** + * LdapAuthentication.php + * + * Info available at https://www.mediawiki.org/wiki/Extension:LDAP_Authentication + * Support is available at https://www.mediawiki.org/wiki/Extension_talk:LDAP_Authentication + */ + +if ( !defined( 'MEDIAWIKI' ) ) { + exit; +} + +$wgLDAPDomainNames = []; +$wgLDAPServerNames = []; +$wgLDAPUseLocal = false; +$wgLDAPEncryptionType = []; +$wgLDAPOptions = []; +$wgLDAPPort = []; +$wgLDAPSearchStrings = []; +$wgLDAPProxyAgent = []; +$wgLDAPProxyAgentPassword = []; +$wgLDAPSearchAttributes = []; +$wgLDAPBaseDNs = []; +$wgLDAPGroupBaseDNs = []; +$wgLDAPUserBaseDNs = []; +$wgLDAPWriterDN = []; +$wgLDAPWriterPassword = []; +$wgLDAPWriteLocation = []; +$wgLDAPAddLDAPUsers = []; +$wgLDAPUpdateLDAP = []; +$wgLDAPPasswordHash = []; +$wgLDAPMailPassword = []; +$wgLDAPPreferences = []; +$wgLDAPDisableAutoCreate = []; +$wgLDAPDebug = 0; +$wgLDAPGroupUseFullDN = []; +$wgLDAPLowerCaseUsername = []; +$wgLDAPGroupUseRetrievedUsername = []; +$wgLDAPGroupObjectclass = []; +$wgLDAPGroupAttribute = []; +$wgLDAPGroupNameAttribute = []; +$wgLDAPGroupsUseMemberOf = []; +$wgLDAPUseLDAPGroups = []; +$wgLDAPLocallyManagedGroups = []; +$wgLDAPGroupsPrevail = []; +$wgLDAPRequiredGroups = []; +$wgLDAPExcludedGroups = []; +$wgLDAPGroupSearchNestedGroups = []; +$wgLDAPAuthAttribute = []; +$wgLDAPAutoAuthUsername = ""; +$wgLDAPAutoAuthDomain = ""; +$wgPasswordResetRoutes['domain'] = true; +$wgLDAPActiveDirectory = []; +$wgLDAPGroupSearchPosixPrimaryGroup = false; + +define( "LDAPAUTHVERSION", "2.1.0" ); + +/** + * Add extension information to Special:Version + */ +$wgExtensionCredits['other'][] = [ + 'path' => __FILE__, + 'name' => 'LDAP Authentication Plugin', + 'version' => LDAPAUTHVERSION, + 'author' => 'Ryan Lane', + 'descriptionmsg' => 'ldapauthentication-desc', + 'url' => 'https://www.mediawiki.org/wiki/Extension:LDAP_Authentication', + 'license-name' => 'GPL-2.0-or-later', +]; + +$wgAutoloadClasses['LdapAuthenticationPlugin'] = __DIR__ . '/LdapAuthenticationPlugin.php'; +$wgAutoloadClasses['LdapPrimaryAuthenticationProvider'] = + __DIR__ . '/LdapPrimaryAuthenticationProvider.php'; + +$wgMessagesDirs['LdapAuthentication'] = __DIR__ . '/i18n'; + +# Schema changes +$wgHooks['LoadExtensionSchemaUpdates'][] = 'efLdapAuthenticationSchemaUpdates'; + +/** + * @param DatabaseUpdater $updater + * @return bool + */ +function efLdapAuthenticationSchemaUpdates( $updater ) { + $base = __DIR__; + switch ( $updater->getDB()->getType() ) { + case 'mysql': + case 'sqlite': + $updater->addExtensionTable( 'ldap_domains', "$base/schema/ldap-mysql.sql" ); + break; + case 'postgres': + $updater->addExtensionTable( 'ldap_domains', "$base/schema/ldap-postgres.sql" ); + break; + } + return true; +} + +// constants for search base +define( "GROUPDN", 0 ); +define( "USERDN", 1 ); +define( "DEFAULTDN", 2 ); + +// constants for error reporting +define( "NONSENSITIVE", 1 ); +define( "SENSITIVE", 2 ); +define( "HIGHLYSENSITIVE", 3 ); + +// The auto-auth code was originally derived from the SSL Authentication plugin +// http://www.mediawiki.org/wiki/SSL_authentication + +/** + * Sets up the auto-authentication piece of the LDAP plugin. + * + * @access public + */ +function AutoAuthSetup() { + /** + * @todo If you want to make AutoAuthSetup() work in an AuthManager + * world, what you need to do is figure out how to do it with a + * SessionProvider instead of the hackiness below. You'll probably + * want an ImmutableSessionProviderWithCookie subclass where + * provideSessionInfo() does the first part of + * LdapAutoAuthentication::Authenticate() (stop before the $localId + * bit). + */ + throw new BadFunctionCallException( 'AutoAuthSetup() is not supported with AuthManager.' ); +} diff --git a/extensions/LdapAuthentication/LdapAuthenticationPlugin.php b/extensions/LdapAuthentication/LdapAuthenticationPlugin.php new file mode 100644 index 0000000000000000000000000000000000000000..671f3eb6e2e49c3a4aee86bff931b865177c15b6 --- /dev/null +++ b/extensions/LdapAuthentication/LdapAuthenticationPlugin.php @@ -0,0 +1,2183 @@ +<?php +/** + * Copyright (C) 2004 Ryan Lane <http://www.mediawiki.org/wiki/User:Ryan_lane> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +class LdapAuthenticationPlugin extends AuthPlugin { + + private static $instance = null; + + // ldap connection resource + public $ldapconn; + + // preferences + public $email, $lang, $realname, $nickname, $externalid; + + // username pulled from ldap + public $LDAPUsername; + + // userdn pulled from ldap + public $userdn; + + // groups pulled from ldap + public $userLDAPGroups; + public $allLDAPGroups; + + // boolean to test for failed auth + public $authFailed; + + // boolean to test for fetched user info + public $fetchedUserInfo; + + // the user's entry and all attributes + public $userInfo; + + // the user we are currently bound as + public $boundAs; + + /** + * Fetch the singleton instance of LdapAuthenticationPlugin + * @return LdapAuthenticationPlugin + */ + public static function getInstance() { + global $wgAuth; + + if ( self::$instance === null ) { + if ( $wgAuth instanceof LdapAuthenticationPlugin ) { + self::$instance = $wgAuth; + } else { + self::$instance = new LdapAuthenticationPlugin; + } + } + return self::$instance; + } + + /** + * Wrapper for ldap_connect + * @param null $hostname + * @param int $port + * @return resource|false + */ + public static function ldap_connect( $hostname = null, $port = 389 ) { + wfSuppressWarnings(); + $ret = ldap_connect( $hostname, $port ); + wfRestoreWarnings(); + return $ret; + } + + /** + * Wrapper for ldap_bind + * @param resource $ldapconn + * @param null $dn + * @param null $password + * @return bool + */ + public static function ldap_bind( $ldapconn, $dn = null, $password = null ) { + wfSuppressWarnings(); + $ret = ldap_bind( $ldapconn, $dn, $password ); + wfRestoreWarnings(); + return $ret; + } + + /** + * Wrapper for ldap_unbind + * @param resource $ldapconn + * @return bool + */ + public static function ldap_unbind( $ldapconn ) { + if ( $ldapconn ) { + wfSuppressWarnings(); + $ret = ldap_unbind( $ldapconn ); + wfRestoreWarnings(); + } else { + $ret = false; + } + return $ret; + } + + /** + * Wrapper for ldap_modify + * @param resource $ldapconn + * @param string $dn + * @param array $entry + * @return bool + */ + public static function ldap_modify( $ldapconn, $dn, $entry ) { + wfSuppressWarnings(); + $ret = ldap_modify( $ldapconn, $dn, $entry ); + wfRestoreWarnings(); + return $ret; + } + + /** + * Wrapper for ldap_add + * @param resource $ldapconn + * @param string $dn + * @param array $entry + * @return bool + */ + public static function ldap_add( $ldapconn, $dn, $entry ) { + wfSuppressWarnings(); + $ret = ldap_add( $ldapconn, $dn, $entry ); + wfRestoreWarnings(); + return $ret; + } + + /** + * Wrapper for ldap_delete + * @param resource $ldapconn + * @param string $dn + * @return bool + */ + public static function ldap_delete( $ldapconn, $dn ) { + wfSuppressWarnings(); + $ret = ldap_delete( $ldapconn, $dn ); + wfRestoreWarnings(); + return $ret; + } + + /** + * Wrapper for ldap_search + * @param resource $ldapconn + * @param string $basedn + * @param string $filter + * @param array|null $attributes + * @param int|null $attrsonly + * @param int|null $sizelimit + * @param int|null $timelimit + * @param int|null $deref + * @return resource + */ + public static function ldap_search( + $ldapconn, + $basedn, + $filter, + $attributes = [], + $attrsonly = null, + $sizelimit = null, + $timelimit = null, + $deref = null + ) { + wfSuppressWarnings(); + $ret = ldap_search( + $ldapconn, + $basedn, + $filter, + $attributes, + $attrsonly, + $sizelimit, + $timelimit, + $deref + ); + wfRestoreWarnings(); + return $ret; + } + + /** + * Wrapper for ldap_read + * @param resource $ldapconn + * @param string $basedn + * @param string $filter + * @param array|null $attributes + * @param int|null $attrsonly + * @param int|null $sizelimit + * @param int|null $timelimit + * @param int|null $deref + * @return resource + */ + public static function ldap_read( + $ldapconn, + $basedn, + $filter, + $attributes = [], + $attrsonly = null, + $sizelimit = null, + $timelimit = null, + $deref = null + ) { + wfSuppressWarnings(); + $ret = ldap_read( + $ldapconn, + $basedn, + $filter, + $attributes, + $attrsonly, + $sizelimit, + $timelimit, + $deref + ); + wfRestoreWarnings(); + return $ret; + } + + /** + * Wrapper for ldap_list + * @param resource $ldapconn + * @param string $basedn + * @param string $filter + * @param array|null $attributes + * @param int|null $attrsonly + * @param int|null $sizelimit + * @param int|null $timelimit + * @param int|null $deref + * @return resource + */ + public static function ldap_list( + $ldapconn, + $basedn, + $filter, + $attributes = [], + $attrsonly = null, + $sizelimit = null, + $timelimit = null, + $deref = null + ) { + wfSuppressWarnings(); + $ret = ldap_list( + $ldapconn, + $basedn, + $filter, + $attributes, + $attrsonly, + $sizelimit, + $timelimit, + $deref + ); + wfRestoreWarnings(); + return $ret; + } + + /** + * Wrapper for ldap_get_entries + * @param resource $ldapconn + * @param resource $resultid + * @return array + */ + public static function ldap_get_entries( $ldapconn, $resultid ) { + wfSuppressWarnings(); + $ret = ldap_get_entries( $ldapconn, $resultid ); + wfRestoreWarnings(); + return $ret; + } + + /** + * Wrapper for ldap_count_entries + * @param resource $ldapconn + * @param resource $resultid + * @return int + */ + public static function ldap_count_entries( $ldapconn, $resultid ) { + wfSuppressWarnings(); + $ret = ldap_count_entries( $ldapconn, $resultid ); + wfRestoreWarnings(); + return $ret; + } + + /** + * Wrapper for ldap_errno + * @param resource $ldapconn + * @return int + */ + public static function ldap_errno( $ldapconn ) { + wfSuppressWarnings(); + $ret = ldap_errno( $ldapconn ); + wfRestoreWarnings(); + return $ret; + } + + /** + * Get configuration defined by admin, or return default value + * + * @param string $preference + * @param string $domain + * @return mixed + */ + public function getConf( $preference, $domain = '' ) { + # Global preferences + switch ( $preference ) { + case 'DomainNames': + global $wgLDAPDomainNames; + return $wgLDAPDomainNames; + case 'UseLocal': + global $wgLDAPUseLocal; + return $wgLDAPUseLocal; + case 'AutoAuthUsername': + global $wgLDAPAutoAuthUsername; + return $wgLDAPAutoAuthUsername; + case 'AutoAuthDomain': + global $wgLDAPAutoAuthDomain; + return $wgLDAPAutoAuthDomain; + } + + # Domain specific preferences + if ( !$domain ) { + $domain = $this->getDomain(); + } + switch ( $preference ) { + case 'ServerNames': + global $wgLDAPServerNames; + return self::setOrDefault( $wgLDAPServerNames, $domain ); + case 'EncryptionType': + global $wgLDAPEncryptionType; + return self::setOrDefault( $wgLDAPEncryptionType, $domain, 'tls' ); + case 'Options': + global $wgLDAPOptions; + return self::setOrDefault( $wgLDAPOptions, $domain, [] ); + case 'Port': + global $wgLDAPPort; + if ( isset( $wgLDAPPort[$domain] ) ) { + $this->printDebug( "Using non-standard port: " . $wgLDAPPort[$domain], SENSITIVE ); + return (string)$wgLDAPPort[$domain]; + } elseif ( $this->getConf( 'EncryptionType' ) == 'ssl' ) { + return "636"; + } else { + return "389"; + } + case 'SearchString': + global $wgLDAPSearchStrings; + return self::setOrDefault( $wgLDAPSearchStrings, $domain ); + case 'ProxyAgent': + global $wgLDAPProxyAgent; + return self::setOrDefault( $wgLDAPProxyAgent, $domain ); + case 'ProxyAgentPassword': + global $wgLDAPProxyAgentPassword; + return self::setOrDefaultPrivate( $wgLDAPProxyAgentPassword, $domain ); + case 'SearchAttribute': + global $wgLDAPSearchAttributes; + return self::setOrDefault( $wgLDAPSearchAttributes, $domain ); + case 'BaseDN': + global $wgLDAPBaseDNs; + return self::setOrDefault( $wgLDAPBaseDNs, $domain ); + case 'GroupBaseDN': + global $wgLDAPGroupBaseDNs; + return self::setOrDefault( $wgLDAPGroupBaseDNs, $domain ); + case 'UserBaseDN': + global $wgLDAPUserBaseDNs; + return self::setOrDefault( $wgLDAPUserBaseDNs, $domain ); + case 'WriterDN': + global $wgLDAPWriterDN; + return self::setOrDefault( $wgLDAPWriterDN, $domain ); + case 'WriterPassword': + global $wgLDAPWriterPassword; + return self::setOrDefaultPrivate( $wgLDAPWriterPassword, $domain ); + case 'WriteLocation': + global $wgLDAPWriteLocation; + return self::setOrDefault( $wgLDAPWriteLocation, $domain ); + case 'AddLDAPUsers': + global $wgLDAPAddLDAPUsers; + return self::setOrDefault( $wgLDAPAddLDAPUsers, $domain, false ); + case 'UpdateLDAP': + global $wgLDAPUpdateLDAP; + return self::setOrDefault( $wgLDAPUpdateLDAP, $domain, false ); + case 'PasswordHash': + global $wgLDAPPasswordHash; + return self::setOrDefaultPrivate( $wgLDAPPasswordHash, $domain, 'clear' ); + case 'MailPassword': + global $wgLDAPMailPassword; + return self::setOrDefaultPrivate( $wgLDAPMailPassword, $domain, false ); + case 'Preferences': + global $wgLDAPPreferences; + return self::setOrDefault( $wgLDAPPreferences, $domain, [] ); + case 'DisableAutoCreate': + global $wgLDAPDisableAutoCreate; + return self::setOrDefault( $wgLDAPDisableAutoCreate, $domain, false ); + case 'GroupUseFullDN': + global $wgLDAPGroupUseFullDN; + return self::setOrDefault( $wgLDAPGroupUseFullDN, $domain, false ); + case 'LowerCaseUsername': + global $wgLDAPLowerCaseUsername; + // Default set to true for backwards compatibility with + // versions < 2.0a + return self::setOrDefault( $wgLDAPLowerCaseUsername, $domain, true ); + case 'GroupUseRetrievedUsername': + global $wgLDAPGroupUseRetrievedUsername; + return self::setOrDefault( $wgLDAPGroupUseRetrievedUsername, $domain, false ); + case 'GroupObjectclass': + global $wgLDAPGroupObjectclass; + return self::setOrDefault( $wgLDAPGroupObjectclass, $domain ); + case 'GroupAttribute': + global $wgLDAPGroupAttribute; + return self::setOrDefault( $wgLDAPGroupAttribute, $domain ); + case 'GroupNameAttribute': + global $wgLDAPGroupNameAttribute; + return self::setOrDefault( $wgLDAPGroupNameAttribute, $domain ); + case 'GroupsUseMemberOf': + global $wgLDAPGroupsUseMemberOf; + return self::setOrDefault( $wgLDAPGroupsUseMemberOf, $domain, false ); + case 'UseLDAPGroups': + global $wgLDAPUseLDAPGroups; + return self::setOrDefault( $wgLDAPUseLDAPGroups, $domain, false ); + case 'LocallyManagedGroups': + global $wgLDAPLocallyManagedGroups; + return self::setOrDefault( $wgLDAPLocallyManagedGroups, $domain, [] ); + case 'GroupsPrevail': + global $wgLDAPGroupsPrevail; + return self::setOrDefault( $wgLDAPGroupsPrevail, $domain, false ); + case 'RequiredGroups': + global $wgLDAPRequiredGroups; + return self::setOrDefault( $wgLDAPRequiredGroups, $domain, [] ); + case 'ExcludedGroups': + global $wgLDAPExcludedGroups; + return self::setOrDefault( $wgLDAPExcludedGroups, $domain, [] ); + case 'GroupSearchNestedGroups': + global $wgLDAPGroupSearchNestedGroups; + return self::setOrDefault( $wgLDAPGroupSearchNestedGroups, $domain, false ); + case 'AuthAttribute': + global $wgLDAPAuthAttribute; + return self::setOrDefault( $wgLDAPAuthAttribute, $domain ); + case 'ActiveDirectory': + global $wgLDAPActiveDirectory; + return self::setOrDefault( $wgLDAPActiveDirectory, $domain, false ); + case 'GroupSearchPosixPrimaryGroup': + global $wgLDAPGroupSearchPosixPrimaryGroup; + return self::setOrDefault( $wgLDAPGroupSearchPosixPrimaryGroup, $domain, false ); + } + return ''; + } + + /** + * Returns the item from $array at index $key if it is set, + * else, it returns $default + * + * @param array $array + * @param string $key + * @param mixed $default + * @return mixed + */ + private static function setOrDefault( $array, $key, $default = '' ) { + return isset( $array[$key] ) ? $array[$key] : $default; + } + + /** + * Returns the item from $array at index $key if it is set, + * else, it returns $default + * + * Use for sensitive data + * + * @param array $array + * @param string $key + * @param mixed $default + * @return mixed + */ + private static function setOrDefaultPrivate( $array, $key, $default = '' ) { + return isset( $array[$key] ) ? $array[$key] : $default; + } + + /** + * Check whether there exists a user account with the given name. + * The name will be normalized to MediaWiki's requirements, so + * you might need to munge it (for instance, for lowercase initial + * letters). + * + * @param string $username + * @return bool + */ + public function userExists( $username ) { + $this->printDebug( "Entering userExists", NONSENSITIVE ); + + // If we can't add LDAP users, we don't really need to check + // if the user exists, the authenticate method will do this for + // us. This will decrease hits to the LDAP server. + // We do however, need to use this if we are using auto authentication. + if ( !$this->getConf( 'AddLDAPUsers' ) && !$this->useAutoAuth() ) { + return true; + } + + return $this->userExistsReal( $username ); + } + + /** + * Like self::userExists, but always does the check + * @see self::userExists() + * @param string $username + * @return bool + */ + public function userExistsReal( $username ) { + $this->printDebug( "Entering userExistsReal", NONSENSITIVE ); + + $ret = false; + if ( $this->connect() ) { + $searchstring = $this->getSearchString( $username ); + + if ( $searchstring == '' ) { + // It is possible that getSearchString will return an + // empty string, which means "no user". + } elseif ( $this->useAutoAuth() ) { + // If we are using auto authentication, and we got + // anything back, then the user exists. + $ret = true; + } else { + // Search for the entry. + $entry = self::ldap_read( + $this->ldapconn, $searchstring, "objectclass=*" + ); + if ( $entry && + self::ldap_count_entries( $this->ldapconn, $entry ) > 0 + ) { + $this->printDebug( "Found a matching user in LDAP", NONSENSITIVE ); + $ret = true; + } else { + $this->printDebug( "Did not find a matching user in LDAP", NONSENSITIVE ); + } + } + // getSearchString is going to bind, but will not unbind + self::ldap_unbind( $this->ldapconn ); + } + return $ret; + } + + /** + * Connect to LDAP + * @param string $domain + * @return bool + */ + public function connect( $domain = '' ) { + $this->printDebug( "Entering Connect", NONSENSITIVE ); + + if ( !function_exists( 'ldap_connect' ) ) { + $this->printDebug( "It looks like you are missing LDAP support; please ensure you " . + "have either compiled LDAP support in, or have enabled the module. If the " . + "authentication is working for you, the plugin isn't properly detecting the LDAP " . + "module, and you can safely ignore this message.", NONSENSITIVE ); + return false; + } + + // Set the server string depending on whether we use ssl or not + $encryptionType = $this->getConf( 'EncryptionType', $domain ); + switch ( $encryptionType ) { + case "ldapi": + $this->printDebug( "Using ldapi", SENSITIVE ); + $serverpre = "ldapi://"; + break; + case "ssl": + $this->printDebug( "Using SSL", SENSITIVE ); + $serverpre = "ldaps://"; + break; + default: + $this->printDebug( "Using TLS or not using encryption.", SENSITIVE ); + $serverpre = "ldap://"; + } + + // Make a space separated list of server strings with the connection type + // string added. + $servers = ""; + $tmpservers = $this->getConf( 'ServerNames', $domain ); + $tok = strtok( $tmpservers, " " ); + while ( $tok ) { + $servers = $servers . " " . $serverpre . $tok . ":" . $this->getConf( 'Port', $domain ); + $tok = strtok( " " ); + } + $servers = trim( $servers ); + if ( !$servers ) { + $this->printDebug( 'Empty server string, skipping connection', NONSENSITIVE ); + return false; + } + + $this->printDebug( "Using servers: $servers", SENSITIVE ); + + // Connect and set options + $this->ldapconn = self::ldap_connect( $servers ); + if ( !$this->ldapconn ) { + $this->printDebug( "PHP's LDAP connect method returned null, this likely implies a " . + "misconfiguration of the plugin.", NONSENSITIVE ); + return false; + } + ldap_set_option( $this->ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3 ); + ldap_set_option( $this->ldapconn, LDAP_OPT_REFERRALS, 0 ); + + foreach ( $this->getConf( 'Options' ) as $key => $value ) { + if ( !ldap_set_option( $this->ldapconn, constant( $key ), $value ) ) { + $this->printDebug( + "Can't set option to LDAP! Option code and value: " . $key . "=" . $value, 1 + ); + } + } + + // TLS needs to be started after the connection resource is available + if ( $encryptionType == "tls" ) { + $this->printDebug( "Using TLS", SENSITIVE ); + if ( !ldap_start_tls( $this->ldapconn ) ) { + $this->printDebug( "Failed to start TLS.", SENSITIVE ); + return false; + } + } + $this->printDebug( "PHP's LDAP connect method returned true (note, this does not imply " . + "it connected to the server).", NONSENSITIVE ); + + return true; + } + + /** + * Check if a username+password pair is a valid login, or if the username + * is allowed access to the wiki. + * The name will be normalized to MediaWiki's requirements, so + * you might need to munge it (for instance, for lowercase initial + * letters). + * + * @param string $username + * @param string $password + * @return bool + */ + public function authenticate( $username, $password = '' ) { + $this->printDebug( "Entering authenticate for username $username", NONSENSITIVE ); + + // We don't handle local authentication + if ( 'local' == $this->getDomain() ) { + $this->printDebug( "User is using a local domain", SENSITIVE ); + return false; + } + + // Mediawiki munges the username before authenticate is called, + // this can mess with authentication, group pulling/restriction, + // preference pulling, etc. Let's allow the admin to use + // a lowercased username if needed. + if ( $this->getConf( 'LowerCaseUsername' ) ) { + $username = strtolower( $username ); + } + + // If the user is using auto authentication, we need to ensure + // that he/she isn't trying to fool us by sending a username other + // than the one the web server got from the auto-authentication method. + if ( $this->useAutoAuth() && $this->getConf( 'AutoAuthUsername' ) != $username ) { + $this->printDebug( "The username provided ($username) doesn't match the username " . + "provided by the webserver (" . $this->getConf( 'AutoAuthUsername' ) . "). " . + "The user is probably trying to log in to the auto-authentication domain with " . + "password authentication via the wiki. Denying access.", SENSITIVE ); + return false; + } + + // We need to ensure that if we require a password, that it is + // not blank. We don't allow blank passwords, so we are being + // tricked if someone is supplying one when using password auth. + // auto-authentication is handled by the webserver; a blank password + // here is wanted. + if ( '' == $password && !$this->useAutoAuth() ) { + $this->printDebug( "User used a blank password", NONSENSITIVE ); + return false; + } + + if ( $this->connect() ) { + $this->userdn = $this->getSearchString( $username ); + + // It is possible that getSearchString will return an + // empty string; if this happens, the bind will ALWAYS + // return true, and will let anyone in! + if ( '' == $this->userdn ) { + $this->printDebug( "User DN is blank", NONSENSITIVE ); + self::ldap_unbind( $this->ldapconn ); + $this->markAuthFailed(); + return false; + } + + // If we are using password authentication, we need to bind as the + // user to make sure the password is correct. + if ( !$this->useAutoAuth() ) { + $this->printDebug( "Binding as the user", NONSENSITIVE ); + $bind = $this->bindAs( $this->userdn, $password ); + if ( !$bind ) { + $this->markAuthFailed(); + return false; + } + $result = true; + Hooks::run( 'ChainAuth', [ $username, $password, &$result ] ); + if ( $result == false ) { + return false; + } + + $this->printDebug( "Bound successfully", NONSENSITIVE ); + + $ss = $this->getConf( 'SearchString' ); + if ( $ss ) { + if ( strstr( $ss, "@" ) || strstr( $ss, '\\' ) ) { + // We are most likely configured using USER-NAME@DOMAIN, or + // DOMAIN\\USER-NAME. + // Get the user's full DN so we can search for groups and such. + $this->userdn = $this->getUserDN( $username ); + $this->printDebug( "Fetched UserDN: $this->userdn", NONSENSITIVE ); + } else { + // Now that we are bound, we can pull the user's info. + $this->getUserInfo(); + } + } + } + + // Ensure the user's entry has the required auth attribute + $aa = $this->getConf( 'AuthAttribute' ); + if ( $aa ) { + $this->printDebug( "Checking for auth attributes: $aa", NONSENSITIVE ); + $filter = "(" . $aa . ")"; + $attributes = [ "dn" ]; + $entry = self::ldap_read( + $this->ldapconn, $this->userdn, $filter, $attributes + ); + $info = self::ldap_get_entries( $this->ldapconn, $entry ); + if ( $info["count"] < 1 ) { + $this->printDebug( "Failed auth attribute check", NONSENSITIVE ); + self::ldap_unbind( $this->ldapconn ); + $this->markAuthFailed(); + return false; + } + } + + $this->getGroups( $username ); + + if ( !$this->checkGroups() ) { + self::ldap_unbind( $this->ldapconn ); + $this->markAuthFailed(); + return false; + } + + $this->getPreferences(); + + self::ldap_unbind( $this->ldapconn ); + } else { + $this->markAuthFailed(); + return false; + } + $this->printDebug( "Authentication passed", NONSENSITIVE ); + + // We made it this far; the user authenticated and didn't fail any checks, so he/she gets in + return true; + } + + function markAuthFailed() { + $this->authFailed = true; + } + + /** + * Modify options in the login template. + * + * @param UserLoginTemplate &$template + * @param string &$type + */ + public function modifyUITemplate( &$template, &$type ) { + $this->printDebug( "Entering modifyUITemplate", NONSENSITIVE ); + $template->set( 'create', $this->getConf( 'AddLDAPUsers' ) ); + $template->set( 'usedomain', true ); + $template->set( 'useemail', $this->getConf( 'MailPassword' ) ); + $template->set( 'canreset', $this->getConf( 'MailPassword' ) ); + $template->set( 'domainnames', $this->domainList() ); + Hooks::run( 'LDAPModifyUITemplate', [ &$template ] ); + } + + /** + * @return array + */ + function domainList() { + $tempDomArr = $this->getConf( 'DomainNames' ); + if ( $this->getConf( 'UseLocal' ) ) { + $this->printDebug( "Allowing the local domain, adding it to the list.", NONSENSITIVE ); + array_push( $tempDomArr, 'local' ); + } + + if ( $this->getConf( 'AutoAuthDomain' ) ) { + $this->printDebug( + "Allowing auto-authentication login, removing the domain from the list.", + NONSENSITIVE + ); + // There is no reason for people to log in directly to the wiki if the are using an + // auto-authentication domain. If they try to, they are probably up to something fishy. + unset( $tempDomArr[array_search( $this->getConf( 'AutoAuthDomain' ), $tempDomArr )] ); + } + + $domains = []; + foreach ( $tempDomArr as $tempDom ) { + $domains["$tempDom"] = $tempDom; + } + return $domains; + } + + /** + * Return true if the wiki should create a new local account automatically + * when asked to login a user who doesn't exist locally but does in the + * external auth database. + * + * This is just a question, and shouldn't perform any actions. + * + * @return bool + */ + public function autoCreate() { + return !$this->getConf( 'DisableAutoCreate' ); + } + + /** + * Set the given password in LDAP. + * Return true if successful. + * + * @param User $user + * @param string $password + * @return bool + */ + public function setPassword( $user, $password ) { + $this->printDebug( "Entering setPassword", NONSENSITIVE ); + + if ( $this->getDomain() == 'local' ) { + $this->printDebug( "User is using a local domain", NONSENSITIVE ); + + // We don't set local passwords, but we don't want the wiki + // to send the user a failure. + return true; + } + if ( !$this->getConf( 'UpdateLDAP' ) ) { + $this->printDebug( "Wiki is set to not allow updates", NONSENSITIVE ); + + // We aren't allowing the user to change his/her own password + return false; + } + + $writer = $this->getConf( 'WriterDN' ); + if ( !$writer ) { + $this->printDebug( "Wiki doesn't have wgLDAPWriterDN set", NONSENSITIVE ); + + // We can't change a user's password without an account that is + // allowed to do it. + return false; + } + $pass = $this->getPasswordHash( $password ); + + if ( $this->connect() ) { + $this->userdn = $this->getSearchString( $user->getName() ); + $this->printDebug( "Binding as the writerDN", NONSENSITIVE ); + $bind = $this->bindAs( $writer, $this->getConf( 'WriterPassword' ) ); + if ( !$bind ) { + return false; + } + $values["userpassword"] = $pass; + + // Blank out the password in the database. We don't want to save + // domain credentials for security reasons. + // This doesn't do anything. $password isn't by reference + $password = ''; + + $success = self::ldap_modify( + $this->ldapconn, $this->userdn, $values + ); + self::ldap_unbind( $this->ldapconn ); + if ( $success ) { + $this->printDebug( "Successfully modified the user's password", NONSENSITIVE ); + return true; + } + $this->printDebug( "Failed to modify the user's password", NONSENSITIVE ); + } + return false; + } + + /** + * Update user information in LDAP + * Return true if successful. + * + * @param User $user + * @return bool + */ + public function updateExternalDB( $user ) { + global $wgMemc; + + $this->printDebug( "Entering updateExternalDB", NONSENSITIVE ); + if ( !$this->getConf( 'UpdateLDAP' ) || $this->getDomain() == 'local' ) { + $this->printDebug( + "Either the user is using a local domain, or the wiki isn't allowing updates", + NONSENSITIVE + ); + // We don't handle local preferences, but we don't want the + // wiki to return an error. + return true; + } + + $writer = $this->getConf( 'WriterDN' ); + if ( !$writer ) { + $this->printDebug( "The wiki doesn't have wgLDAPWriterDN set", NONSENSITIVE ); + // We can't modify LDAP preferences if we don't have a user + // capable of editing LDAP attributes. + return false; + } + + $this->email = $user->getEmail(); + $this->realname = $user->getRealName(); + $this->nickname = $user->getOption( 'nickname' ); + $this->lang = $user->getOption( 'language' ); + if ( $this->connect() ) { + $this->userdn = $this->getSearchString( $user->getName() ); + $this->printDebug( "Binding as the writerDN", NONSENSITIVE ); + $bind = $this->bindAs( $writer, $this->getConf( 'WriterPassword' ) ); + if ( !$bind ) { + return false; + } + + $values = []; + $prefs = $this->getConf( 'Preferences' ); + foreach ( array_keys( $prefs ) as $key ) { + $attr = strtolower( $prefs[$key] ); + switch ( $key ) { + case "email": + if ( is_string( $this->email ) ) { + $values[$attr] = $this->email; + } + break; + case "nickname": + if ( is_string( $this->nickname ) ) { + $values[$attr] = $this->nickname; + } + break; + case "realname": + if ( is_string( $this->realname ) ) { + $values[$attr] = $this->realname; + } + break; + case "language": + if ( is_string( $this->lang ) ) { + $values[$attr] = $this->lang; + } + break; + } + } + + if ( count( $values ) && + self::ldap_modify( $this->ldapconn, $this->userdn, $values ) + ) { + // We changed the user, we need to invalidate the memcache key + $key = wfMemcKey( 'ldapauthentication', 'userinfo', $this->userdn ); + $wgMemc->delete( $key ); + $this->printDebug( "Successfully modified the user's attributes", NONSENSITIVE ); + self::ldap_unbind( $this->ldapconn ); + return true; + } + $this->printDebug( "Failed to modify the user's attributes", NONSENSITIVE ); + self::ldap_unbind( $this->ldapconn ); + } + return false; + } + + /** + * Can the wiki create accounts in LDAP? + * Return true if yes. + * + * @return bool + */ + public function canCreateAccounts() { + return $this->getConf( 'AddLDAPUsers' ); + } + + /** + * Can the wiki change passwords in LDAP, or can the user + * change passwords locally? + * Return true if yes. + * + * @return bool + */ + public function allowPasswordChange() { + $this->printDebug( "Entering allowPasswordChange", NONSENSITIVE ); + + // Local domains need to be able to change passwords + if ( $this->getConf( 'UseLocal' ) && 'local' == $this->getDomain() ) { + return true; + } + if ( $this->getConf( 'UpdateLDAP' ) || $this->getConf( 'MailPassword' ) ) { + return true; + } + return false; + } + + /** + * Disallow MediaWiki from setting local passwords in the database, + * unless UseLocal is true. Warning: if you set $wgLDAPUseLocal, + * it will cause MediaWiki to leak LDAP passwords into the local database. + * @return bool + */ + public function allowSetLocalPassword() { + return $this->getConf( 'UseLocal' ); + } + + /** + * Add a user to LDAP. + * Return true if successful. + * + * @param User $user + * @param string $password + * @param string $email + * @param string $realname + * @return bool + */ + public function addUser( $user, $password, $email = '', $realname = '' ) { + $this->printDebug( "Entering addUser", NONSENSITIVE ); + + if ( !$this->getConf( 'AddLDAPUsers' ) || 'local' == $this->getDomain() ) { + $this->printDebug( "Either the user is using a local domain, or the wiki isn't " . + "allowing users to be added to LDAP", NONSENSITIVE ); + + // Tell the wiki not to return an error. + return true; + } + if ( $this->getConf( 'RequiredGroups' ) ) { + $this->printDebug( "The wiki is requiring users to be in specific groups, and cannot " . + "add users as this would be a security hole.", NONSENSITIVE ); + // It is possible that later we can add users into + // groups, but since we don't support it, we don't want + // to open holes! + return false; + } + + $writer = $this->getConf( 'WriterDN' ); + if ( !$writer ) { + $this->printDebug( "The wiki doesn't have wgLDAPWriterDN set", NONSENSITIVE ); + + // We can't add users without an LDAP account capable of doing so. + return false; + } + + $this->email = $user->getEmail(); + $this->realname = $user->getRealName(); + $username = $user->getName(); + if ( $this->getConf( 'LowerCaseUsername' ) ) { + $username = strtolower( $username ); + } + $pass = $this->getPasswordHash( $password ); + if ( $this->connect() ) { + $writeloc = $this->getConf( 'WriteLocation' ); + $this->userdn = $this->getSearchString( $username ); + if ( '' == $this->userdn ) { + $this->printDebug( + "userdn is blank, attempting to use wgLDAPWriteLocation", NONSENSITIVE + ); + if ( $writeloc ) { + $this->printDebug( "wgLDAPWriteLocation is set, using that", NONSENSITIVE ); + $this->userdn = $this->getConf( 'SearchAttribute' ) . "=" . + $username . "," . $writeloc; + } else { + $this->printDebug( "wgLDAPWriteLocation is not set, failing", NONSENSITIVE ); + // getSearchString will bind, but will not unbind + self::ldap_unbind( $this->ldapconn ); + return false; + } + } + + $this->printDebug( "Binding as the writerDN", NONSENSITIVE ); + $bind = $this->bindAs( $writer, $this->getConf( 'WriterPassword' ) ); + if ( !$bind ) { + $this->printDebug( "Failed to bind as the writerDN; add failed", NONSENSITIVE ); + return false; + } + + // Set up LDAP objectclasses and attributes + // TODO: make objectclasses and attributes configurable + $values["uid"] = $username; + // sn is required for objectclass inetorgperson + $values["sn"] = $username; + $prefs = $this->getConf( 'Preferences' ); + foreach ( array_keys( $prefs ) as $key ) { + $attr = strtolower( $prefs[$key] ); + switch ( $key ) { + case "email": + if ( is_string( $this->email ) ) { + $values[$attr] = $this->email; + } + break; + case "nickname": + if ( is_string( $this->nickname ) ) { + $values[$attr] = $this->nickname; + } + break; + case "realname": + if ( is_string( $this->realname ) ) { + $values[$attr] = $this->realname; + } + break; + case "language": + if ( is_string( $this->lang ) ) { + $values[$attr] = $this->lang; + } + break; + } + } + if ( !array_key_exists( "cn", $values ) ) { + $values["cn"] = $username; + } + $values["userpassword"] = $pass; + $values["objectclass"] = [ "inetorgperson" ]; + + $result = true; + # Let other extensions modify the user object before creation + Hooks::run( 'LDAPSetCreationValues', + [ $this, $username, &$values, $writeloc, &$this->userdn, &$result ] ); + if ( !$result ) { + $this->printDebug( + "Failed to add user because LDAPSetCreationValues returned false", NONSENSITIVE + ); + self::ldap_unbind( $this->ldapconn ); + return false; + } + + $this->printDebug( "Adding user", NONSENSITIVE ); + if ( self::ldap_add( $this->ldapconn, $this->userdn, $values ) ) { + $this->printDebug( "Successfully added user", NONSENSITIVE ); + self::ldap_unbind( $this->ldapconn ); + return true; + } + $errno = self::ldap_errno( $this->ldapconn ); + # Constraint violation, let's allow other plugins a chance to retry + if ( $errno === 19 ) { + $result = false; + Hooks::run( 'LDAPRetrySetCreationValues', + [ $this, $username, &$values, $writeloc, &$this->userdn, &$result ] ); + if ( $result && + self::ldap_add( $this->ldapconn, $this->userdn, $values ) + ) { + $this->printDebug( "Successfully added user", NONSENSITIVE ); + self::ldap_unbind( $this->ldapconn ); + return true; + } + } + $this->printDebug( "Failed to add user", NONSENSITIVE ); + self::ldap_unbind( $this->ldapconn ); + } + return false; + } + + /** + * Set the domain this plugin is supposed to use when authenticating. + * + * @param string $domain + */ + public function setDomain( $domain ) { + $this->printDebug( "Setting domain as: $domain", NONSENSITIVE ); + $_SESSION['wsDomain'] = $domain; + } + + /** + * Get the user's domain + * + * @return string + */ + public function getDomain() { + global $wgUser; + + $this->printDebug( "Entering getDomain", NONSENSITIVE ); + + # If there's only a single domain set, there's no reason + # to bother with sessions, tokens, etc.. This works around + # a number of bugs caused by supporting multiple domains. + # The bugs will still exist when using multiple domains, + # though. + $domainNames = $this->getConf( 'DomainNames' ); + if ( ( count( $domainNames ) === 1 ) && !$this->getConf( 'UseLocal' ) ) { + return $domainNames[0]; + } + # First check if we already have a valid domain set + if ( isset( $_SESSION['wsDomain'] ) && $_SESSION['wsDomain'] != 'invaliddomain' ) { + $this->printDebug( "Pulling domain from session.", NONSENSITIVE ); + return $_SESSION['wsDomain']; + } + # If the session domain isn't set, the user may have been logged + # in with a token, check the user options. + # If $wgUser isn't defined yet, it might be due to an LDAPAutoAuthDomain config. + if ( isset( $wgUser ) && $wgUser->isLoggedIn() && $wgUser->getToken( false ) ) { + $this->printDebug( "Pulling domain from user options.", NONSENSITIVE ); + $domain = self::loadDomain( $wgUser ); + if ( $domain ) { + return $domain; + } + } + # The user must be using an invalid domain + $this->printDebug( "No domain found, returning invaliddomain", NONSENSITIVE ); + return 'invaliddomain'; + } + + /** + * Check to see if the specific domain is a valid domain. + * Return true if the domain is valid. + * + * @param string $domain + * @return bool + */ + public function validDomain( $domain ) { + $this->printDebug( "Entering validDomain", NONSENSITIVE ); + if ( in_array( $domain, $this->getConf( 'DomainNames' ) ) || + ( $this->getConf( 'UseLocal' ) && 'local' == $domain ) + ) { + $this->printDebug( "User is using a valid domain ($domain).", NONSENSITIVE ); + return true; + } + $this->printDebug( "User is not using a valid domain ($domain).", NONSENSITIVE ); + return false; + } + + /** + * When a user logs in, update user with information from LDAP. + * + * @param User &$user + * TODO: fix the setExternalID stuff + */ + public function updateUser( &$user ) { + $this->printDebug( "Entering updateUser", NONSENSITIVE ); + if ( $this->authFailed ) { + $this->printDebug( "User didn't successfully authenticate, exiting.", NONSENSITIVE ); + return; + } + + if ( $this->getConf( 'Preferences' ) ) { + $this->printDebug( "Setting user preferences.", NONSENSITIVE ); + if ( is_string( $this->lang ) ) { + $this->printDebug( "Setting language.", NONSENSITIVE ); + $user->setOption( 'language', $this->lang ); + } + if ( is_string( $this->nickname ) ) { + $this->printDebug( "Setting nickname.", NONSENSITIVE ); + $user->setOption( 'nickname', $this->nickname ); + } + if ( is_string( $this->realname ) ) { + $this->printDebug( "Setting realname.", NONSENSITIVE ); + $user->setRealName( $this->realname ); + } + if ( is_string( $this->email ) ) { + $this->printDebug( "Setting email.", NONSENSITIVE ); + $user->setEmail( $this->email ); + $user->confirmEmail(); + } + } + + if ( $this->getConf( 'UseLDAPGroups' ) ) { + $this->printDebug( "Setting user groups.", NONSENSITIVE ); + $this->setGroups( $user ); + } + + # We must set a user option if we want token based logins to work + if ( $user->getToken( false ) ) { + $this->printDebug( "User has a token, setting domain in user options.", NONSENSITIVE ); + self::saveDomain( $user, $_SESSION['wsDomain'] ); + } + + # Let other extensions update the user + Hooks::run( 'LDAPUpdateUser', [ &$user ] ); + + $this->printDebug( "Saving user settings.", NONSENSITIVE ); + $user->saveSettings(); + } + + /** + * When creating a user account, initialize user with information from LDAP. + * TODO: fix setExternalID stuff + * + * @param User &$user + * @param bool $autocreate + */ + public function initUser( &$user, $autocreate = false ) { + $this->printDebug( "Entering initUser", NONSENSITIVE ); + + if ( $this->authFailed ) { + $this->printDebug( "User didn't successfully authenticate, exiting.", NONSENSITIVE ); + return; + } + if ( 'local' == $this->getDomain() ) { + $this->printDebug( "User is using a local domain", NONSENSITIVE ); + return; + } + + // The update user function does everything else we need done. + $this->updateUser( $user ); + + // updateUser() won't necessarily save the user's settings + $user->saveSettings(); + } + + /** + * Return true to prevent logins that don't authenticate here from being + * checked against the local database's password fields. + * + * This is just a question, and shouldn't perform any actions. + * + * @return bool + */ + public function strict() { + $this->printDebug( "Entering strict.", NONSENSITIVE ); + + if ( $this->getConf( 'UseLocal' ) || $this->getConf( 'MailPassword' ) ) { + $this->printDebug( "Returning false in strict().", NONSENSITIVE ); + return false; + } + $this->printDebug( "Returning true in strict().", NONSENSITIVE ); + return true; + } + + /** + * Munge the username based on a scheme (lowercase, by default), by search attribute + * otherwise. + * + * @param string $username + * @return string + */ + public function getCanonicalName( $username ) { + global $wgMemc; + + $this->printDebug( "Entering getCanonicalName", NONSENSITIVE ); + if ( User::isIP( $username ) ) { + $this->printDebug( "Username is an IP, not munging.", NONSENSITIVE ); + return $username; + } + $key = wfMemcKey( 'ldapauthentication', 'canonicalname', $username ); + $canonicalname = $username; + if ( $username != '' ) { + $this->printDebug( "Username is: $username", NONSENSITIVE ); + if ( $this->getConf( 'LowerCaseUsername' ) ) { + $canonicalname = ucfirst( strtolower( $canonicalname ) ); + } else { + # Fetch username, so that we can possibly use it. + $userInfo = $wgMemc->get( $key ); + if ( is_array( $userInfo ) ) { + $this->printDebug( "Fetched userInfo from memcache.", NONSENSITIVE ); + if ( $userInfo["username"] == $username ) { + $this->printDebug( + "Username matched a key in memcache, using the fetched name: " . + $userInfo["canonicalname"], + NONSENSITIVE + ); + return $userInfo["canonicalname"]; + } + } + if ( $this->validDomain( $this->getDomain() ) && $this->connect() ) { + // Try to pull the username from LDAP. In the case of straight binds, + // try to fetch the username by search before bind. + $this->userdn = $this->getUserDN( $username, true ); + $hookSetUsername = $this->LDAPUsername; + Hooks::run( 'SetUsernameAttributeFromLDAP', + [ &$hookSetUsername, $this->userInfo ] ); + if ( is_string( $hookSetUsername ) ) { + $this->printDebug( + "Username munged by hook: $hookSetUsername", NONSENSITIVE + ); + $this->LDAPUsername = $hookSetUsername; + } else { + $this->printDebug( + "Fetched username is not a string (check your hook code...). " . + "This message can be safely ignored if you do not have the " . + "SetUsernameAttributeFromLDAP hook defined.", NONSENSITIVE + ); + } + } + + // We want to use the username returned by LDAP + // if it exists + if ( $this->LDAPUsername != '' ) { + $canonicalname = ucfirst( $this->LDAPUsername ); + $this->printDebug( "Using LDAPUsername: $canonicalname", NONSENSITIVE ); + } + + $wgMemc->set( + $key, + [ "username" => $username, "canonicalname" => $canonicalname ], + 3600 * 24 + ); + } + } + $this->printDebug( "Munged username: $canonicalname", NONSENSITIVE ); + return $canonicalname; + } + + /** + * Configures the authentication plugin for use with auto-authentication + * plugins. + */ + public function autoAuthSetup() { + $this->setDomain( $this->getConf( 'AutoAuthDomain' ) ); + } + + /** + * Gets the searchstring for a user based upon settings for the domain. + * Returns a full DN for a user. + * + * @param string $username + * @return string + * @access private + */ + function getSearchString( $username ) { + $this->printDebug( "Entering getSearchString", NONSENSITIVE ); + $ss = $this->getConf( 'SearchString' ); + if ( $ss ) { + // This is a straight bind + $this->printDebug( "Doing a straight bind", NONSENSITIVE ); + $userdn = str_replace( "USER-NAME", $username, $ss ); + } else { + $userdn = $this->getUserDN( $username, true ); + } + $this->printDebug( "userdn is: $userdn", SENSITIVE ); + return $userdn; + } + + /** + * Gets the DN of a user based upon settings for the domain. + * This function will set $this->LDAPUsername + * + * @param string $username + * @param bool $bind + * @param string $searchattr + * @return string + */ + public function getUserDN( $username, $bind = false, $searchattr = '' ) { + $this->printDebug( "Entering getUserDN", NONSENSITIVE ); + if ( $bind ) { + // This is a proxy bind, or an anonymous bind with a search + $proxyagent = $this->getConf( 'ProxyAgent' ); + if ( $proxyagent ) { + // This is a proxy bind + $this->printDebug( "Doing a proxy bind", NONSENSITIVE ); + $bind = $this->bindAs( $proxyagent, $this->getConf( 'ProxyAgentPassword' ) ); + } else { + // This is an anonymous bind + $this->printDebug( "Doing an anonymous bind", NONSENSITIVE ); + $bind = $this->bindAs(); + } + if ( !$bind ) { + $this->printDebug( "Failed to bind", NONSENSITIVE ); + $this->fetchedUserInfo = false; + $this->userInfo = null; + return ''; + } + } + + if ( ! $searchattr ) { + $searchattr = $this->getConf( 'SearchAttribute' ); + } + // we need to do a subbase search for the entry + $filter = "(" . $searchattr . "=" . $this->getLdapEscapedString( $username ) . ")"; + $this->printDebug( "Created a regular filter: $filter", SENSITIVE ); + + // We explicitly put memberof here because it's an operational attribute in some servers. + $attributes = [ "*", "memberof" ]; + $base = $this->getBaseDN( USERDN ); + $this->printDebug( "Using base: $base", SENSITIVE ); + $entry = self::ldap_search( + $this->ldapconn, $base, $filter, $attributes + ); + if ( self::ldap_count_entries( $this->ldapconn, $entry ) == 0 ) { + $this->printDebug( "Couldn't find an entry", NONSENSITIVE ); + $this->fetchedUserInfo = false; + $this->userInfo = null; + return ''; + } + $this->userInfo = self::ldap_get_entries( $this->ldapconn, $entry ); + $this->fetchedUserInfo = true; + if ( isset( $this->userInfo[0][$searchattr] ) ) { + $username = $this->userInfo[0][$searchattr][0]; + $this->printDebug( + "Setting the LDAPUsername based on fetched wgLDAPSearchAttributes: $username", + NONSENSITIVE + ); + $this->LDAPUsername = $username; + } + $userdn = $this->userInfo[0]["dn"]; + return $userdn; + } + + /** + * Load the current user's entry + * + * @return bool + */ + function getUserInfo() { + // Don't fetch the same data more than once + if ( $this->fetchedUserInfo ) { + return true; + } + $userInfo = $this->getUserInfoStateless( $this->userdn ); + if ( is_null( $userInfo ) ) { + $this->fetchedUserInfo = false; + } else { + $this->fetchedUserInfo = true; + $this->userInfo = $userInfo; + } + return $this->fetchedUserInfo; + } + + /** + * @param string $userdn + * @return array|null + */ + function getUserInfoStateless( $userdn ) { + global $wgMemc; + + $key = wfMemcKey( 'ldapauthentication', 'userinfo', $userdn ); + $userInfo = $wgMemc->get( $key ); + if ( !is_array( $userInfo ) ) { + $entry = self::ldap_read( + $this->ldapconn, $userdn, "objectclass=*", [ '*', 'memberof' ] + ); + $userInfo = self::ldap_get_entries( $this->ldapconn, $entry ); + if ( $userInfo["count"] < 1 ) { + return null; + } + $wgMemc->set( $key, $userInfo, 3600 * 24 ); + } + return $userInfo; + } + + /** + * Retrieve user preferences from LDAP + */ + private function getPreferences() { + $this->printDebug( "Entering getPreferences", NONSENSITIVE ); + + // Retrieve preferences + $prefs = $this->getConf( 'Preferences' ); + if ( !$prefs ) { + return null; + } + if ( !$this->getUserInfo() ) { + $this->printDebug( + "Failed to get preferences, the user's entry wasn't found.", NONSENSITIVE + ); + return null; + } + $this->printDebug( "Retrieving preferences", NONSENSITIVE ); + foreach ( array_keys( $prefs ) as $key ) { + $attr = strtolower( $prefs[$key] ); + if ( !isset( $this->userInfo[0][$attr] ) ) { + continue; + } + $value = $this->userInfo[0][$attr][0]; + switch ( $key ) { + case "email": + $this->email = $value; + $this->printDebug( + "Retrieved email ($this->email) using attribute ($prefs[$key])", + NONSENSITIVE + ); + break; + case "language": + $this->lang = $value; + $this->printDebug( + "Retrieved language ($this->lang) using attribute ($prefs[$key])", + NONSENSITIVE + ); + break; + case "nickname": + $this->nickname = $value; + $this->printDebug( + "Retrieved nickname ($this->nickname) using attribute ($prefs[$key])", + NONSENSITIVE + ); + break; + case "realname": + $this->realname = $value; + $this->printDebug( + "Retrieved realname ($this->realname) using attribute ($prefs[$key])", + NONSENSITIVE + ); + break; + } + } + } + + /** + * Checks to see whether a user is in a required group. + * + * @return bool + */ + private function checkGroups() { + $this->printDebug( "Entering checkGroups", NONSENSITIVE ); + + $excgroups = $this->getConf( 'ExcludedGroups' ); + if ( $excgroups ) { + $this->printDebug( "Checking for excluded group membership", NONSENSITIVE ); + + $excgroups = array_map( 'strtolower', $excgroups ); + + $this->printDebug( "Excluded groups:", NONSENSITIVE, $excgroups ); + + foreach ( $this->userLDAPGroups["dn"] as $group ) { + $this->printDebug( "Checking against: $group", NONSENSITIVE ); + if ( in_array( $group, $excgroups ) ) { + $this->printDebug( "Found user in an excluded group.", NONSENSITIVE ); + return false; + } + } + } + + $reqgroups = $this->getConf( 'RequiredGroups' ); + if ( $reqgroups ) { + $this->printDebug( "Checking for (new style) group membership", NONSENSITIVE ); + + $reqgroups = array_map( 'strtolower', $reqgroups ); + + $this->printDebug( "Required groups:", NONSENSITIVE, $reqgroups ); + + foreach ( $this->userLDAPGroups["dn"] as $group ) { + $this->printDebug( "Checking against: $group", NONSENSITIVE ); + if ( in_array( $group, $reqgroups ) ) { + $this->printDebug( "Found user in a group.", NONSENSITIVE ); + return true; + } + } + + $this->printDebug( "Couldn't find the user in any groups.", NONSENSITIVE ); + return false; + } + + // Ensure we return true if we aren't checking groups. + return true; + } + + /** + * Function to get the user's groups. + * @param string $username + */ + protected function getGroups( $username ) { + $this->printDebug( "Entering getGroups", NONSENSITIVE ); + + // Ensure userLDAPGroups is set, no matter what + $this->userLDAPGroups = [ "dn" => [], "short" => [] ]; + + // Find groups + if ( $this->getConf( 'RequiredGroups' ) || $this->getConf( 'UseLDAPGroups' ) ) { + $this->printDebug( "Retrieving LDAP group membership", NONSENSITIVE ); + + // Let's figure out what we should be searching for + if ( $this->getConf( 'GroupUseFullDN' ) ) { + $usertopass = $this->userdn; + } else { + if ( $this->getConf( 'GroupUseRetrievedUsername' ) && $this->LDAPUsername != '' ) { + $usertopass = $this->LDAPUsername; + } else { + $usertopass = $username; + } + } + + if ( $this->getConf( 'GroupsUseMemberOf' ) ) { + $this->printDebug( "Using memberOf", NONSENSITIVE ); + if ( !$this->getUserInfo() ) { + $this->printDebug( "Couldn't get the user's entry.", NONSENSITIVE ); + } elseif ( isset( $this->userInfo[0]["memberof"] ) ) { + # The first entry is always a count + $memberOfMembers = $this->userInfo[0]["memberof"]; + array_shift( $memberOfMembers ); + $groups = [ "dn" => [], "short" => [] ]; + + foreach ( $memberOfMembers as $mem ) { + array_push( $groups["dn"], strtolower( $mem ) ); + + // Get short name of group + $memAttrs = explode( ',', strtolower( $mem ) ); + if ( isset( $memAttrs[0] ) ) { + $memAttrs = explode( '=', $memAttrs[0] ); + if ( isset( $memAttrs[0] ) ) { + array_push( $groups["short"], strtolower( $memAttrs[1] ) ); + } + } + } + $this->printDebug( "Got the following groups:", SENSITIVE, $groups["dn"] ); + + $this->userLDAPGroups = $groups; + } else { + $this->printDebug( "memberOf attribute isn't set", NONSENSITIVE ); + } + } else { + $this->printDebug( "Searching for the groups", NONSENSITIVE ); + $this->userLDAPGroups = $this->searchGroups( $usertopass ); + if ( $this->getConf( 'GroupSearchNestedGroups' ) ) { + $this->userLDAPGroups = $this->searchNestedGroups( $this->userLDAPGroups ); + $this->printDebug( + "Got the following nested groups:", SENSITIVE, $this->userLDAPGroups["dn"] + ); + } + } + + if ( $this->getConf( 'GroupSearchPosixPrimaryGroup' ) ) { + if ( !$this->getUserInfo() ) { + $this->printDebug( "Couldn't get the user's entry.", NONSENSITIVE ); + } elseif ( isset( $this->userInfo[0]["gidnumber"] ) ) { + $base = $this->getBaseDN( GROUPDN ); + $objectclass = $this->getConf( 'GroupObjectclass' ); + $filter = "(&(objectClass={$objectclass})" . + "(gidNumber={$this->userInfo[0]['gidnumber'][0]}))"; + $info = self::ldap_search( $this->ldapconn, $base, $filter ); + $entries = self::ldap_get_entries( $this->ldapconn, $info ); + if ( empty( $entries[0] ) ) { + $this->printDebug( "Couldn't get the user's primary group.", NONSENSITIVE ); + } else { + $primary_group_dn = strtolower( $entries[0]["dn"] ); + $this->printDebug( "Got the user's primary group:", SENSITIVE, $primary_group_dn ); + $this->userLDAPGroups["dn"][] = $primary_group_dn; + $nameattribute = strtolower( $this->getConf( 'GroupNameAttribute' ) ); + $this->userLDAPGroups["short"][] = $entries[0][$nameattribute][0]; + } + } + } + + // Only find all groups if the user has any groups; otherwise, we are + // just wasting a search. + if ( $this->getConf( 'GroupsPrevail' ) && count( $this->userLDAPGroups ) != 0 ) { + $this->allLDAPGroups = $this->searchGroups( '*' ); + } + } + } + + /** + * Function to return an array of nested groups when given a group or list of groups. + * $searchedgroups is used for tail recursion and shouldn't be provided + * when called externally. + * + * @param array $groups + * @param array $searchedgroups + * @return bool + * @access private + */ + function searchNestedGroups( $groups, $searchedgroups = [ "dn" => [], "short" => [] ] ) { + $this->printDebug( "Entering searchNestedGroups", NONSENSITIVE ); + + // base case, no more groups left to check + if ( count( $groups["dn"] ) == 0 ) { + $this->printDebug( "No more groups to search.", NONSENSITIVE ); + return $searchedgroups; + } + + $this->printDebug( "Searching groups:", SENSITIVE, $groups["dn"] ); + $groupstosearch = [ "short" => [], "dn" => [] ]; + foreach ( $groups["dn"] as $group ) { + $returnedgroups = $this->searchGroups( $group ); + $this->printDebug( + "Group $group is in the following groups:", SENSITIVE, $returnedgroups["dn"] + ); + foreach ( $returnedgroups["dn"] as $searchme ) { + if ( in_array( $searchme, $searchedgroups["dn"] ) ) { + // We already searched this, move on + continue; + } else { + // We'll need to search this group's members now + $this->printDebug( "Adding $searchme to the list of groups (1)", SENSITIVE ); + $groupstosearch["dn"][] = $searchme; + } + } + foreach ( $returnedgroups["short"] as $searchme ) { + if ( in_array( $searchme, $searchedgroups["short"] ) ) { + // We already searched this, move on + continue; + } else { + $this->printDebug( "Adding $searchme to the list of groups (2)", SENSITIVE ); + // We'll need to search this group's members now + $groupstosearch["short"][] = $searchme; + } + } + } + $searchedgroups = array_merge_recursive( $groups, $searchedgroups ); + + return $this->searchNestedGroups( $groupstosearch, $searchedgroups ); + } + + /** + * Search groups for the supplied DN + * + * @param string $dn + * @return array + */ + private function searchGroups( $dn ) { + $this->printDebug( "Entering searchGroups", NONSENSITIVE ); + + $base = $this->getBaseDN( GROUPDN ); + $objectclass = $this->getConf( 'GroupObjectclass' ); + $attribute = $this->getConf( 'GroupAttribute' ); + $nameattribute = $this->getConf( 'GroupNameAttribute' ); + + // We actually want to search for * not \2a, ensure we don't escape * + $value = $dn; + if ( $value != "*" ) { + $value = $this->getLdapEscapedString( $value ); + } + + $proxyagent = $this->getConf( 'ProxyAgent' ); + if ( $proxyagent ) { + // We'll try to bind as the proxyagent as the proxyagent should normally have more + // rights than the user. If the proxyagent fails to bind, we will still be able + // to search as the normal user (which is why we don't return on fail). + $this->printDebug( "Binding as the proxyagent", NONSENSITIVE ); + $this->bindAs( $proxyagent, $this->getConf( 'ProxyAgentPassword' ) ); + } + + $groups = [ "short" => [], "dn" => [] ]; + + // AD does not include the primary group in the list of groups, we have to find it ourselves + if ( $dn != "*" && $this->getConf( 'ActiveDirectory' ) ) { + $PGfilter = "(&(distinguishedName=$value)(objectclass=user))"; + $this->printDebug( "User Filter: $PGfilter", SENSITIVE ); + $PGinfo = self::ldap_search( $this->ldapconn, $base, $PGfilter ); + $PGentries = self::ldap_get_entries( $this->ldapconn, $PGinfo ); + if ( !empty( $PGentries[0] ) ) { + $Usid = $PGentries[0]['objectsid'][0]; + $PGrid = $PGentries[0]['primarygroupid'][0]; + $PGsid = bin2hex( $Usid ); + $PGSID = []; + for ( $i = 0; $i < 56; $i += 2 ) { + $PGSID[] = substr( $PGsid, $i, 2 ); + } + $dPGrid = dechex( $PGrid ); + $dPGrid = str_pad( $dPGrid, 8, '0', STR_PAD_LEFT ); + $PGRID = []; + for ( $i = 0; $i < 8; $i += 2 ) { + array_push( $PGRID, substr( $dPGrid, $i, 2 ) ); + } + for ( $i = 24; $i < 28; $i++ ) { + $PGSID[$i] = array_pop( $PGRID ); + } + $PGsid_string = ''; + foreach ( $PGSID as $PGsid_bit ) { + $PGsid_string .= "\\" . $PGsid_bit; + } + $PGfilter = "(&(objectSid=$PGsid_string)(objectclass=$objectclass))"; + $this->printDebug( "Primary Group Filter: $PGfilter", SENSITIVE ); + $info = self::ldap_search( $this->ldapconn, $base, $PGfilter ); + $PGentries = self::ldap_get_entries( $this->ldapconn, $info ); + array_shift( $PGentries ); + $dnMember = strtolower( $PGentries[0]['dn'] ); + $groups["dn"][] = $dnMember; + // Get short name of group + $memAttrs = explode( ',', strtolower( $dnMember ) ); + if ( isset( $memAttrs[0] ) ) { + $memAttrs = explode( '=', $memAttrs[0] ); + if ( isset( $memAttrs[1] ) ) { + $groups["short"][] = strtolower( $memAttrs[1] ); + } + } + + } + } + + $filter = "(&($attribute=$value)(objectclass=$objectclass))"; + $this->printDebug( "Search string: $filter", SENSITIVE ); + $info = self::ldap_search( $this->ldapconn, $base, $filter ); + if ( !$info ) { + $this->printDebug( "No entries returned from search.", SENSITIVE ); + // Return an array so that other functions + // don't error out. + return [ "short" => [], "dn" => [] ]; + } + + $entries = self::ldap_get_entries( $this->ldapconn, $info ); + if ( $entries ) { + // We need to shift because the first entry will be a count + array_shift( $entries ); + // Let's get a list of both full dn groups and shortname groups + foreach ( $entries as $entry ) { + $shortMember = strtolower( $entry[$nameattribute][0] ); + $dnMember = strtolower( $entry['dn'] ); + $groups["short"][] = $shortMember; + $groups["dn"][] = $dnMember; + } + } + + $this->printDebug( "Returned groups:", SENSITIVE, $groups["dn"] ); + return $groups; + } + + /** + * Returns true if this group is in the list of the currently authenticated + * user's groups, else false. + * + * @param string $group + * @return bool + * @access private + */ + function hasLDAPGroup( $group ) { + $this->printDebug( "Entering hasLDAPGroup", NONSENSITIVE ); + return in_array( strtolower( $group ), $this->userLDAPGroups["short"] ); + } + + /** + * Returns true if an LDAP group with this name exists, else false. + * + * @param string $group + * @return bool + * @access private + */ + function isLDAPGroup( $group ) { + $this->printDebug( "Entering isLDAPGroup", NONSENSITIVE ); + return in_array( strtolower( $group ), $this->allLDAPGroups["short"] ); + } + + /** + * Helper function for updateUser() and initUser(). Adds users into MediaWiki security groups + * based upon groups retreived from LDAP. + * + * @param User &$user + * @access private + */ + function setGroups( &$user ) { + global $wgGroupPermissions; + + // TODO: this is *really* ugly code. clean it up! + $this->printDebug( "Entering setGroups.", NONSENSITIVE ); + + # Add ldap groups as local groups + if ( $this->getConf( 'GroupsPrevail' ) ) { + $this->printDebug( + "Adding all groups to wgGroupPermissions: ", SENSITIVE, $this->allLDAPGroups + ); + + foreach ( $this->allLDAPGroups["short"] as $ldapgroup ) { + if ( !array_key_exists( $ldapgroup, $wgGroupPermissions ) ) { + $wgGroupPermissions[$ldapgroup] = []; + } + } + } + + # add groups permissions + $localAvailGrps = $user->getAllGroups(); + $localUserGrps = $user->getEffectiveGroups(); + $defaultLocallyManagedGrps = [ 'bot', 'sysop', 'bureaucrat' ]; + $locallyManagedGrps = $this->getConf( 'LocallyManagedGroups' ); + if ( $locallyManagedGrps ) { + $locallyManagedGrps = array_unique( + array_merge( $defaultLocallyManagedGrps, $locallyManagedGrps ) ); + $this->printDebug( "Locally managed groups: ", SENSITIVE, $locallyManagedGrps ); + } else { + $locallyManagedGrps = $defaultLocallyManagedGrps; + $this->printDebug( + "Locally managed groups is unset, using defaults: ", SENSITIVE, $locallyManagedGrps + ); + } + + $this->printDebug( "Available groups are: ", NONSENSITIVE, $localAvailGrps ); + $this->printDebug( "Effective groups are: ", NONSENSITIVE, $localUserGrps ); + # note: $localUserGrps does not need to be updated with $cGroup added, + # as $localAvailGrps contains $cGroup only once. + foreach ( $localAvailGrps as $cGroup ) { + # did we once add the user to the group? + if ( in_array( $cGroup, $localUserGrps ) ) { + $this->printDebug( + "Checking to see if we need to remove user from: $cGroup", NONSENSITIVE + ); + if ( !$this->hasLDAPGroup( $cGroup ) && + !in_array( $cGroup, $locallyManagedGrps ) + ) { + $this->printDebug( "Removing user from: $cGroup", NONSENSITIVE ); + # the ldap group overrides the local group + # so as the user is currently not a member of the ldap group, + # he shall be removed from the local group + $user->removeGroup( $cGroup ); + } + } else { + # no, but maybe the user has recently been added to the ldap group? + $this->printDebug( "Checking to see if user is in: $cGroup", NONSENSITIVE ); + if ( $this->hasLDAPGroup( $cGroup ) ) { + $this->printDebug( "Adding user to: $cGroup", NONSENSITIVE ); + $user->addGroup( $cGroup ); + } + } + } + } + + /** + * Returns a password that is created via the configured hash settings. + * + * @param string $password + * @return string + * @access private + */ + function getPasswordHash( $password ) { + $this->printDebug( "Entering getPasswordHash", NONSENSITIVE ); + + // Set the password hashing based upon admin preference + switch ( $this->getConf( 'PasswordHash' ) ) { + case 'crypt': + // https://bugs.php.net/bug.php?id=55439 + if ( crypt( 'password', '$1$U7AjYB.O$' ) == '$1$U7AjYB.O' ) { + die( 'The version of PHP in use has a broken crypt function. Please upgrade ' . + 'your installation of PHP, or use another encryption function for LDAP.' ); + } + $pass = '{CRYPT}' . crypt( $password ); + break; + case 'clear': + $pass = $password; + break; + default: + $pwd_sha = base64_encode( pack( 'H*', sha1( $password ) ) ); + $pass = "{SHA}" . $pwd_sha; + break; + } + + return $pass; + } + + /** + * Prints debugging information. $debugText is what you want to print, $debugVal + * is the level at which you want to print the information. + * + * @param string $debugText + * @param string $debugVal + * @param Array|null $debugArr + * @access private + */ + function printDebug( $debugText, $debugVal, $debugArr = null ) { + if ( !function_exists( 'wfDebugLog' ) ) { + return; + } + + global $wgLDAPDebug; + + if ( $wgLDAPDebug >= $debugVal ) { + if ( isset( $debugArr ) ) { + $debugText = $debugText . " " . implode( "::", $debugArr ); + } + wfDebugLog( 'ldap', LDAPAUTHVERSION . ' ' . $debugText, false ); + } + } + + /** + * Binds as $userdn with $password. This can be called with only the ldap + * connection resource for an anonymous bind. + * + * @param string $userdn + * @param string $password + * @return bool + * @access private + */ + function bindAs( $userdn = null, $password = null ) { + // Let's see if the user can authenticate. + if ( $userdn == null || $password == null ) { + $bind = self::ldap_bind( $this->ldapconn ); + } else { + $bind = self::ldap_bind( $this->ldapconn, $userdn, $password ); + } + if ( !$bind ) { + $this->printDebug( "Failed to bind as $userdn", NONSENSITIVE ); + return false; + } + $this->boundAs = $userdn; + return true; + } + + /** + * Returns true if auto-authentication is allowed, and the user is + * authenticating using the auto-authentication domain. + * + * @return bool + * @access private + */ + function useAutoAuth() { + return $this->getDomain() == $this->getConf( 'AutoAuthDomain' ); + } + + /** + * Returns a string which has the chars *, (, ), \ & NUL escaped to LDAP compliant + * syntax as per RFC 2254 + * Thanks and credit to Iain Colledge for the research and function. + * + * @param string $string + * @return string + * @access private + */ + function getLdapEscapedString( $string ) { + // Make the string LDAP compliant by escaping *, (, ) , \ & NUL + return str_replace( + [ "\\", "(", ")", "*", "\x00" ], + [ "\\5c", "\\28", "\\29", "\\2a", "\\00" ], + $string + ); + } + + /** + * Returns a basedn by the type of entry we are searching for. + * + * @param int $type + * @return string + * @access private + */ + function getBaseDN( $type ) { + $this->printDebug( "Entering getBaseDN", NONSENSITIVE ); + + $ret = ''; + switch ( $type ) { + case USERDN: + $ret = $this->getConf( 'UserBaseDN' ); + break; + case GROUPDN: + $ret = $this->getConf( 'GroupBaseDN' ); + break; + case DEFAULTDN: + $ret = $this->getConf( 'BaseDN' ); + if ( $ret ) { + return $ret; + } else { + $this->printDebug( "basedn is not set.", NONSENSITIVE ); + return ''; + } + } + + if ( $ret == '' ) { + $this->printDebug( + "basedn is not set for this type of entry, trying to get the default basedn.", + NONSENSITIVE + ); + // We will never reach here if $type is self::DEFAULTDN, so to avoid code + // code duplication, we'll get the default by re-calling the function. + return $this->getBaseDN( DEFAULTDN ); + } else { + $this->printDebug( "basedn is $ret", NONSENSITIVE ); + return $ret; + } + } + + /** + * @param User $user + * @return string + */ + static function loadDomain( $user ) { + $user_id = $user->getId(); + if ( $user_id != 0 ) { + $dbr = wfGetDB( DB_REPLICA ); + $row = $dbr->selectRow( + 'ldap_domains', + [ 'domain' ], + [ 'user_id' => $user_id ], + __METHOD__ ); + + if ( $row ) { + return $row->domain; + } + } + + return null; + } + + /** + * @param User $user + * @param string $domain + * @return bool + */ + static function saveDomain( $user, $domain ) { + $user_id = $user->getId(); + if ( $user_id != 0 ) { + $dbw = wfGetDB( DB_MASTER ); + $olddomain = self::loadDomain( $user ); + if ( $olddomain ) { + return $dbw->update( + 'ldap_domains', + [ 'domain' => $domain ], + [ 'user_id' => $user_id ], + __METHOD__ + ); + } else { + return $dbw->insert( + 'ldap_domains', + [ + 'domain' => $domain, + 'user_id' => $user_id + ], + __METHOD__ + ); + } + } + return false; + } + +} diff --git a/extensions/LdapAuthentication/LdapAutoAuthentication.php b/extensions/LdapAuthentication/LdapAutoAuthentication.php new file mode 100644 index 0000000000000000000000000000000000000000..8eab57690b465f844ea58acd8abf65d3c4ed589f --- /dev/null +++ b/extensions/LdapAuthentication/LdapAutoAuthentication.php @@ -0,0 +1,118 @@ +<?php + +class LdapAutoAuthentication { + + /** + * Does the web server authentication piece of the LDAP plugin. + * + * @param User $user + * @param bool &$result + * @return bool + */ + public static function Authenticate( $user, &$result = null ) { + $ldap = LdapAuthenticationPlugin::getInstance(); + + $ldap->printDebug( "Entering AutoAuthentication.", NONSENSITIVE ); + + if ( $user->isLoggedIn() ) { + $ldap->printDebug( "User is already logged in.", NONSENSITIVE ); + return true; + } + + $ldap->printDebug( "User isn't logged in, calling setup.", NONSENSITIVE ); + + // Let regular authentication plugins configure themselves for auto + // authentication chaining + $ldap->autoAuthSetup(); + + $autoauthname = $ldap->getConf( 'AutoAuthUsername' ); + $ldap->printDebug( "Calling authenticate with username ($autoauthname).", NONSENSITIVE ); + + // The user hasn't already been authenticated, let's check them + $authenticated = $ldap->authenticate( $autoauthname, '' ); + if ( !$authenticated ) { + // If the user doesn't exist in LDAP, there isn't much reason to + // go any further. + $ldap->printDebug( "User wasn't found in LDAP, exiting.", NONSENSITIVE ); + return false; + } + + // We need the username that MediaWiki will always use, not necessarily the one we + // get from LDAP. + $mungedUsername = $ldap->getCanonicalName( $autoauthname ); + + $ldap->printDebug( + "User exists in LDAP; finding the user by name ($mungedUsername) in MediaWiki.", + NONSENSITIVE + ); + $localId = User::idFromName( $mungedUsername ); + $ldap->printDebug( "Got id ($localId).", NONSENSITIVE ); + + // Is the user already in the database? + if ( !$localId ) { + $userAdded = self::attemptAddUser( $user, $mungedUsername ); + if ( !$userAdded ) { + $result = false; + return false; + } + } else { + $ldap->printDebug( "User exists in local database, logging in.", NONSENSITIVE ); + $user->setID( $localId ); + $user->loadFromId(); + $user->setCookies(); + $ldap->updateUser( $user ); + wfSetupSession(); + $result = true; + } + + return true; + } + + /** + * @param User $user + * @param string $mungedUsername + * @return bool + */ + public static function attemptAddUser( $user, $mungedUsername ) { + $ldap = LdapAuthenticationPlugin::getInstance(); + + if ( !$ldap->autoCreate() ) { + $ldap->printDebug( "Cannot automatically create accounts.", NONSENSITIVE ); + return false; + } + + $ldap->printDebug( "User does not exist in local database; creating.", NONSENSITIVE ); + // Checks passed, create the user + $user->loadDefaults( $mungedUsername ); + $status = $user->addToDatabase(); + if ( $status !== null && !$status->isOK() ) { + $ldap->printDebug( "Creation failed: " . $status->getWikiText(), NONSENSITIVE ); + return false; + } + $ldap->initUser( $user, true ); + $user->setCookies(); + wfSetupSession(); + # Update user count + $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 ); + $ssUpdate->doUpdate(); + # Notify hooks (e.g. Newuserlog) + Hooks::run( 'AuthPluginAutoCreate', [ $user ] ); + + return true; + } + + /** + * No logout link in MW + * @param array &$personal_urls + * @param Title $title + * @return bool + */ + public static function NoLogout( &$personal_urls, $title ) { + $ldap = LdapAuthenticationPlugin::getInstance(); + + $ldap->printDebug( "Entering NoLogout.", NONSENSITIVE ); + unset( $personal_urls['logout'] ); + return true; + } + +} diff --git a/extensions/LdapAuthentication/LdapPrimaryAuthenticationProvider.php b/extensions/LdapAuthentication/LdapPrimaryAuthenticationProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..2a97fdb8ae64a94751199b093c5004983fb4e59b --- /dev/null +++ b/extensions/LdapAuthentication/LdapPrimaryAuthenticationProvider.php @@ -0,0 +1,444 @@ +<?php +/** + * Primary authentication provider wrapper for LdapAuthentication + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Auth + */ + +use MediaWiki\Auth\AuthManager; +use MediaWiki\Auth\AbstractPasswordPrimaryAuthenticationProvider; +use MediaWiki\Auth\AuthenticationRequest; +use MediaWiki\Auth\PasswordAuthenticationRequest; +use MediaWiki\Auth\PasswordDomainAuthenticationRequest; +use MediaWiki\Auth\AuthenticationResponse; +use Wikimedia\ScopedCallback; + +/** + * Primary authentication provider wrapper for LdapAuthentication + * @warning This is rather hacky, and probably doesn't fully support what + * LdapAuthenticationPlugin can do. In particular, it's probably even more + * likely to have bugs with multiple domains than LdapAuthenticationPlugin + * itself, since AuthManager may well break some of the assumptions that + * LdapAuthenticationPlugin makes based on the old AuthPlugin-using code, and + * it doesn't support any username munging (i.e. + * LdapAuthenticationPlugin::getCanonicalName()). + * @todo Someday someone who knows about authenticating against LDAP should + * write an extension that doesn't have craziness like a global "domain" + * variable where we have to guess at the correct value in half the entry + * points. + */ +class LdapPrimaryAuthenticationProvider + extends AbstractPasswordPrimaryAuthenticationProvider +{ + private $hasMultipleDomains; + private $requestType; + + public function __construct() { + parent::__construct(); + + $ldap = LdapAuthenticationPlugin::getInstance(); + $this->hasMultipleDomains = count( $ldap->domainList() ) > 1; + $this->requestType = $this->hasMultipleDomains + ? PasswordDomainAuthenticationRequest::class + : PasswordAuthenticationRequest::class; + + // Hooks to handle updating LDAP on various core events + \Hooks::register( 'UserSaveSettings', [ $this, 'onUserSaveSettings' ] ); + \Hooks::register( 'UserGroupsChanged', [ $this, 'onUserGroupsChanged' ] ); + \Hooks::register( 'UserLoggedIn', [ $this, 'onUserLoggedIn' ] ); + \Hooks::register( 'LocalUserCreated', [ $this, 'onLocalUserCreated' ] ); + } + + /** + * Sets the "domain" to that for the specified user, if any + * @param LdapAuthenticationPlugin $ldap + * @param User $user + * @return ScopedCallback|null Resetter + */ + private function setDomainForUser( LdapAuthenticationPlugin $ldap, User $user ) { + if ( !$this->hasMultipleDomains ) { + // LdapAuthenticationPlugin still needs setDomain called, even if + // getDomain is deterministic. Sigh. + $ldap->setDomain( $ldap->getDomain() ); + return null; + } + + $domain = LdapAuthenticationPlugin::loadDomain( $user ); + if ( $domain === null ) { + return null; + } + + $oldDomain = $ldap->getDomain(); + if ( $domain === $oldDomain ) { + // No change, so no point + return null; + } + + $ldap->setDomain( $domain ); + return new ScopedCallback( [ $ldap, 'setDomain' ], [ $oldDomain ] ); + } + + /** + * Create an appropriate AuthenticationRequest + * @return PasswordAuthenticationRequest + */ + protected function makeAuthReq() { + $ldap = LdapAuthenticationPlugin::getInstance(); + $domainList = $ldap->domainList(); + if ( count( $domainList ) > 1 ) { + return new PasswordDomainAuthenticationRequest( $domainList ); + } else { + return new PasswordAuthenticationRequest; + } + } + + /** + * Hook function to call LdapAuthenticationPlugin::updateExternalDB() + * @param User $user + * @codeCoverageIgnore + */ + public function onUserSaveSettings( $user ) { + $ldap = LdapAuthenticationPlugin::getInstance(); + $reset = $this->setDomainForUser( $ldap, $user ); + $ldap->updateExternalDB( $user ); + ScopedCallback::consume( $reset ); + } + + /** + * Hook function to call LdapAuthenticationPlugin::updateExternalDBGroups() + * @param User $user + * @param array $added + * @param array $removed + */ + public function onUserGroupsChanged( $user, $added, $removed ) { + $ldap = LdapAuthenticationPlugin::getInstance(); + $reset = $this->setDomainForUser( $ldap, $user ); + $ldap->updateExternalDBGroups( $user, $added, $removed ); + ScopedCallback::consume( $reset ); + } + + /** + * Hook function to call LdapAuthenticationPlugin::updateUser() + * @param User $user + */ + public function onUserLoggedIn( $user ) { + $ldap = LdapAuthenticationPlugin::getInstance(); + $reset = $this->setDomainForUser( $ldap, $user ); + $ldap->updateUser( $user ); + ScopedCallback::consume( $reset ); + } + + /** + * Hook function to call LdapAuthenticationPlugin::initUser() + * @param User $user + * @param bool $autocreated + */ + public function onLocalUserCreated( $user, $autocreated ) { + // For $autocreated, see self::autoCreatedAccount() + if ( !$autocreated ) { + $ldap = LdapAuthenticationPlugin::getInstance(); + $reset = $this->setDomainForUser( $ldap, $user ); + $ldap->initUser( $user, $autocreated ); + ScopedCallback::consume( $reset ); + } + } + + public function getAuthenticationRequests( $action, array $options ) { + switch ( $action ) { + case AuthManager::ACTION_LOGIN: + case AuthManager::ACTION_CREATE: + return [ $this->makeAuthReq() ]; + + case AuthManager::ACTION_CHANGE: + case AuthManager::ACTION_REMOVE: + // No way to know the domain here. + $ldap = LdapAuthenticationPlugin::getInstance(); + return $ldap->allowPasswordChange() ? [ $this->makeAuthReq() ] : []; + + default: + return []; + } + } + + public function beginPrimaryAuthentication( array $reqs ) { + $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType ); + if ( !$req || $req->username === null || $req->password === null || + ( $this->hasMultipleDomains && $req->domain === null ) + ) { + return AuthenticationResponse::newAbstain(); + } + + $username = User::getCanonicalName( $req->username, 'usable' ); + if ( $username === false ) { + return AuthenticationResponse::newAbstain(); + } + + $ldap = LdapAuthenticationPlugin::getInstance(); + + if ( $this->hasMultipleDomains ) { + // Special:UserLogin does this. Strange. + $domain = $req->domain; + if ( !$ldap->validDomain( $domain ) ) { + $domain = $ldap->getDomain(); + } + } else { + $domain = $ldap->getDomain(); + } + $ldap->setDomain( $domain ); + + if ( $this->testUserCanAuthenticateInternal( $ldap, User::newFromName( $username ) ) && + $ldap->authenticate( $username, $req->password ) + ) { + return AuthenticationResponse::newPass( $username ); + } else { + $this->authoritative = $ldap->strict() || $ldap->strictUserAuth( $username ); + return $this->failResponse( $req ); + } + } + + public function testUserCanAuthenticate( $username ) { + $username = User::getCanonicalName( $username, 'usable' ); + if ( $username === false ) { + return false; + } + + $ldap = LdapAuthenticationPlugin::getInstance(); + if ( $this->hasMultipleDomains ) { + // We have to check every domain to really determine if the user can authenticate + $curDomain = $ldap->getDomain(); + foreach ( $ldap->domainList() as $domain ) { + $ldap->setDomain( $domain ); + if ( $this->testUserCanAuthenticateInternal( $ldap, User::newFromName( $username ) ) ) { + $ldap->setDomain( $curDomain ); + return true; + } + } + $ldap->setDomain( $curDomain ); + return false; + } else { + // Yay, easy way out. + $ldap->setDomain( $ldap->getDomain() ); + return $this->testUserCanAuthenticateInternal( $ldap, User::newFromName( $username ) ); + } + } + + /** + * Test if a user can authenticate against $ldap's current domain + * @param LdapAuthenticationPlugin $ldap + * @param User $user + * @return bool + */ + private function testUserCanAuthenticateInternal( LdapAuthenticationPlugin $ldap, $user ) { + if ( $ldap->userExistsReal( $user->getName() ) ) { + return !$ldap->getUserInstance( $user )->isLocked(); + } else { + return false; + } + } + + public function providerRevokeAccessForUser( $username ) { + $username = User::getCanonicalName( $username, 'usable' ); + if ( $username === false ) { + return; + } + $user = User::newFromName( $username ); + if ( $user ) { + // Reset the password on every domain. + $ldap = LdapAuthenticationPlugin::getInstance(); + $curDomain = $ldap->getDomain(); + $domains = $ldap->domainList() ?: [ '(default)' ]; + $failed = []; + foreach ( $domains as $domain ) { + $ldap->setDomain( $domain ); + if ( $ldap->userExistsReal( $username ) && + !$ldap->setPassword( $user, null ) + ) { + $failed[] = $domain; + } + } + $ldap->setDomain( $curDomain ); + if ( $failed ) { + throw new \UnexpectedValueException( + "LdapAuthenticationPlugin failed to reset password for $username in the following domains: " + . implode( ' ', $failed ) + ); + } + } + } + + public function testUserExists( $username, $flags = User::READ_NORMAL ) { + $username = User::getCanonicalName( $username, 'usable' ); + if ( $username === false ) { + return false; + } + + $ldap = LdapAuthenticationPlugin::getInstance(); + if ( $this->hasMultipleDomains ) { + // We have to check every domain to really determine if the user can authenticate + $curDomain = $ldap->getDomain(); + foreach ( $ldap->domainList() as $domain ) { + $ldap->setDomain( $domain ); + if ( $ldap->userExistsReal( $username ) ) { + $ldap->setDomain( $curDomain ); + return true; + } + } + $ldap->setDomain( $curDomain ); + return false; + } else { + // Yay, easy way out. + $ldap->setDomain( $ldap->getDomain() ); + return $ldap->userExistsReal( $username ); + } + } + + public function providerAllowsPropertyChange( $property ) { + // No way to know the right domain to query. + $ldap = LdapAuthenticationPlugin::getInstance(); + return $ldap->allowPropChange( $property ); + } + + public function providerAllowsAuthenticationDataChange( + AuthenticationRequest $req, $checkData = true + ) { + if ( get_class( $req ) !== $this->requestType ) { + return \StatusValue::newGood( 'ignored' ); + } + + if ( $this->hasMultipleDomains && $req->domain === 'local' ) { + return \StatusValue::newGood( 'ignored' ); + } + + $ldap = LdapAuthenticationPlugin::getInstance(); + + $curDomain = $ldap->getDomain(); + if ( $checkData ) { + $ldap->setDomain( $this->hasMultipleDomains ? $req->domain : $curDomain ); + } + try { + // If !$checkData the domain might be wrong. Nothing we can do about that. + if ( !$ldap->allowPasswordChange() || !$ldap->getConf( 'WriterDN' ) ) { + return \StatusValue::newFatal( 'authmanager-authplugin-setpass-denied' ); + } + + if ( !$checkData ) { + return \StatusValue::newGood(); + } + + if ( $this->hasMultipleDomains ) { + if ( $req->domain === null ) { + return \StatusValue::newGood( 'ignored' ); + } + if ( !$ldap->validDomain( $req->domain ) ) { + return \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' ); + } + } + + $username = User::getCanonicalName( $req->username, 'usable' ); + if ( $username !== false ) { + $sv = \StatusValue::newGood(); + if ( $req->password !== null ) { + if ( $req->password !== $req->retype ) { + $sv->fatal( 'badretype' ); + } else { + $sv->merge( $this->checkPasswordValidity( $username, $req->password )->getStatusValue() ); + } + } + return $sv; + } else { + return \StatusValue::newGood( 'ignored' ); + } + } finally { + $ldap->setDomain( $curDomain ); + } + } + + public function providerChangeAuthenticationData( AuthenticationRequest $req ) { + if ( get_class( $req ) === $this->requestType ) { + $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false; + if ( $username === false ) { + return; + } + + if ( $this->hasMultipleDomains && $req->domain === null ) { + return; + } + + $ldap = LdapAuthenticationPlugin::getInstance(); + $ldap->setDomain( $this->hasMultipleDomains ? $req->domain : $ldap->getDomain() ); + $user = User::newFromName( $username ); + if ( !$ldap->setPassword( $user, $req->password ) ) { + // This is totally unfriendly and leaves other + // AuthenticationProviders in an uncertain state, but what else + // can we do? + throw new \ErrorPageError( + 'authmanager-authplugin-setpass-failed-title', + 'authmanager-authplugin-setpass-failed-message' + ); + } + } + } + + public function accountCreationType() { + // No way to know the domain, just hope it works + $ldap = LdapAuthenticationPlugin::getInstance(); + return $ldap->canCreateAccounts() ? self::TYPE_CREATE : self::TYPE_NONE; + } + + public function testForAccountCreation( $user, $creator, array $reqs ) { + return \StatusValue::newGood(); + } + + public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) { + if ( $this->accountCreationType() === self::TYPE_NONE ) { + throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' ); + } + + $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType ); + if ( !$req || $req->username === null || $req->password === null || + ( $this->hasMultipleDomains && $req->domain === null ) + ) { + return AuthenticationResponse::newAbstain(); + } + + $username = User::getCanonicalName( $req->username, 'usable' ); + if ( $username === false ) { + return AuthenticationResponse::newAbstain(); + } + + $ldap = LdapAuthenticationPlugin::getInstance(); + $ldap->setDomain( $this->hasMultipleDomains ? $req->domain : $ldap->getDomain() ); + if ( $ldap->addUser( + $user, $req->password, $user->getEmail(), $user->getRealName() + ) ) { + return AuthenticationResponse::newPass(); + } else { + return AuthenticationResponse::newFail( + new \Message( 'authmanager-authplugin-create-fail' ) + ); + } + } + + public function autoCreatedAccount( $user, $source ) { + $ldap = LdapAuthenticationPlugin::getInstance(); + $reset = $this->setDomainForUser( $ldap, $user ); + $ldap->initUser( $user, true ); + ScopedCallback::consume( $reset ); + } +} diff --git a/extensions/LdapAuthentication/README b/extensions/LdapAuthentication/README new file mode 100644 index 0000000000000000000000000000000000000000..72751b2956fabc268be98aca73b7714afec3b57b --- /dev/null +++ b/extensions/LdapAuthentication/README @@ -0,0 +1 @@ +This authentication plugin allows MediaWiki to use an LDAP store as its user database for authentication, and some authorization. Full functionality and configuration information can be found at: https://www.mediawiki.org/wiki/Extension:LDAP_Authentication diff --git a/extensions/LdapAuthentication/composer.json b/extensions/LdapAuthentication/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..544e44a8f0cecdbf6766a05210813bd4a71ccb7d --- /dev/null +++ b/extensions/LdapAuthentication/composer.json @@ -0,0 +1,19 @@ +{ + "require-dev": { + "jakub-onderka/php-parallel-lint": "1.0.0", + "jakub-onderka/php-console-highlighter": "0.3.2", + "mediawiki/mediawiki-codesniffer": "18.0.0", + "mediawiki/minus-x": "0.3.1" + }, + "scripts": { + "fix": [ + "phpcbf", + "minus-x fix ." + ], + "test": [ + "parallel-lint . --exclude vendor --exclude node_modules", + "phpcs -p -s", + "minus-x check ." + ] + } +} diff --git a/extensions/LdapAuthentication/gitinfo.json b/extensions/LdapAuthentication/gitinfo.json new file mode 100644 index 0000000000000000000000000000000000000000..72a4beabb531cffddc83250a8f20334ed02b6632 --- /dev/null +++ b/extensions/LdapAuthentication/gitinfo.json @@ -0,0 +1 @@ +{"headSHA1": "b19888c34a46a2e5838112850032815852661694\n", "head": "b19888c34a46a2e5838112850032815852661694\n", "remoteURL": "https://gerrit.wikimedia.org/r/mediawiki/extensions/LdapAuthentication", "branch": "b19888c34a46a2e5838112850032815852661694\n", "headCommitDate": "1523675488"} \ No newline at end of file diff --git a/extensions/LdapAuthentication/i18n/af.json b/extensions/LdapAuthentication/i18n/af.json new file mode 100644 index 0000000000000000000000000000000000000000..8532bb18f4b83fe7bc72a754450549c1527c7561 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/af.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Naudefj" + ] + }, + "ldapauthentication-desc": "Uitbreiding vir LDAP-outentisiteit wat die meeste LDAP-outentisiteitsmetodes ondersteun" +} diff --git a/extensions/LdapAuthentication/i18n/aln.json b/extensions/LdapAuthentication/i18n/aln.json new file mode 100644 index 0000000000000000000000000000000000000000..0bb91314f537c12f3a78e09ffb553d0d60c34565 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/aln.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Mdupont" + ] + }, + "ldapauthentication-desc": "plugin LDAP vertetimi me mbështetje për metoda të shumta tek LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/ar.json b/extensions/LdapAuthentication/i18n/ar.json new file mode 100644 index 0000000000000000000000000000000000000000..0bcae03824fece8275ebbaf011834c73cf4d0012 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/ar.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Meno25" + ] + }, + "ldapauthentication-desc": "Ø¥Ø¶Ø§ÙØ© تØÙ‚يق LDAP بدعم لوسائل تØÙ‚يق LDAP متعددة" +} diff --git a/extensions/LdapAuthentication/i18n/ast.json b/extensions/LdapAuthentication/i18n/ast.json new file mode 100644 index 0000000000000000000000000000000000000000..6372a65a520d39133f784b475bbf397de89c1ac6 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/ast.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Xuacu" + ] + }, + "ldapauthentication-desc": "Complemento p'autenticación LDAP con sofitu pa dellos métodos d'autenticación LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/be-tarask.json b/extensions/LdapAuthentication/i18n/be-tarask.json new file mode 100644 index 0000000000000000000000000000000000000000..b9177bad4903f5618968cfb497a25e241a974d3a --- /dev/null +++ b/extensions/LdapAuthentication/i18n/be-tarask.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "EugeneZelenko" + ] + }, + "ldapauthentication-desc": "Дапаўненьне LDAP-аўтÑнтыфікацыі з падтрымкай некалькіх мÑтадаў аўтÑнтыфікацыі LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/br.json b/extensions/LdapAuthentication/i18n/br.json new file mode 100644 index 0000000000000000000000000000000000000000..a5a0c81acda900733a18aa387d0c19ba897abe33 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/br.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Fulup" + ] + }, + "ldapauthentication-desc": "Adveziant gwiriekaat LDAP ennañ meur a hentenn wiriekaat LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/bs.json b/extensions/LdapAuthentication/i18n/bs.json new file mode 100644 index 0000000000000000000000000000000000000000..d68058dafa78612d743244c27e1a8583cfa63977 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/bs.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "CERminator" + ] + }, + "ldapauthentication-desc": "ProÅ¡irenje LDAP autentifikacije sa podrÅ¡kom za mnoge metode LDAP autentifikacije" +} diff --git a/extensions/LdapAuthentication/i18n/ca.json b/extensions/LdapAuthentication/i18n/ca.json new file mode 100644 index 0000000000000000000000000000000000000000..4bbdbfb93ef62c34c7120e1083e84340841d6afd --- /dev/null +++ b/extensions/LdapAuthentication/i18n/ca.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Paucabot" + ] + }, + "ldapauthentication-desc": "Connector d'autentificació LDAP amb suport per a diversos mètodes d'autenticació LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/ce.json b/extensions/LdapAuthentication/i18n/ce.json new file mode 100644 index 0000000000000000000000000000000000000000..661857b318cfe845cd465285cdc5bbf2c1dea722 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/ce.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Умар" + ] + }, + "ldapauthentication-desc": "LDAP-аутентификацин плагин LDAP нийÑа юй хьожу" +} diff --git a/extensions/LdapAuthentication/i18n/cs.json b/extensions/LdapAuthentication/i18n/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..25a46909d67a2ccb5aeb2195cdb234ecdc0575de --- /dev/null +++ b/extensions/LdapAuthentication/i18n/cs.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Mormegil" + ] + }, + "ldapauthentication-desc": "AutentizaÄnà modul pro LDAP podporujÃcà nÄ›kolik autentizaÄnÃch metod LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/de.json b/extensions/LdapAuthentication/i18n/de.json new file mode 100644 index 0000000000000000000000000000000000000000..855a58ba9ddedd589dd9568c9e4cf89654f36461 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/de.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Imre", + "Kghbln" + ] + }, + "ldapauthentication-desc": "Ermöglicht die LDAP-Authentifizierung mit Hilfe mehrerer Authentifizierungsmethoden" +} diff --git a/extensions/LdapAuthentication/i18n/dsb.json b/extensions/LdapAuthentication/i18n/dsb.json new file mode 100644 index 0000000000000000000000000000000000000000..3deb89b9604d40561afbc770ce454efe0df6c1b4 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/dsb.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Michawiki" + ] + }, + "ldapauthentication-desc": "Tykac awtentifikacije LDAP z pódpÄ›ru za nÄ›kotare metody LDAP-awtentifikacije" +} diff --git a/extensions/LdapAuthentication/i18n/en-gb.json b/extensions/LdapAuthentication/i18n/en-gb.json new file mode 100644 index 0000000000000000000000000000000000000000..2eb88bdb8a443fb09a181416804eed3868db7974 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/en-gb.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Andibing" + ] + }, + "ldapauthentication-desc": "LDAP authentication plugin with support for multiple LDAP authentication methods" +} diff --git a/extensions/LdapAuthentication/i18n/en.json b/extensions/LdapAuthentication/i18n/en.json new file mode 100644 index 0000000000000000000000000000000000000000..1976587d5b8abf81cd1ee4495d3d0875169e7e80 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/en.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Ryan Lane" + ] + }, + "ldapauthentication-desc": "LDAP authentication plugin with support for multiple LDAP authentication methods" +} \ No newline at end of file diff --git a/extensions/LdapAuthentication/i18n/eo.json b/extensions/LdapAuthentication/i18n/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..dbecd0c23c213d864115397ff14a149924a1521d --- /dev/null +++ b/extensions/LdapAuthentication/i18n/eo.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Blahma" + ] + }, + "ldapauthentication-desc": "LDAP-aÅtentiga kromprogramo kun subteno de pluraj LDAP-aÅtentigaj metodoj" +} diff --git a/extensions/LdapAuthentication/i18n/es.json b/extensions/LdapAuthentication/i18n/es.json new file mode 100644 index 0000000000000000000000000000000000000000..bb0a5130cc1941a3ac079d54981102685aab418c --- /dev/null +++ b/extensions/LdapAuthentication/i18n/es.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Translationista", + "Macofe" + ] + }, + "ldapauthentication-desc": "Complemento de autenticación LDAP que admite múltiples métodos de autenticación LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/fa.json b/extensions/LdapAuthentication/i18n/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..dc63bbd4bf7c971dc07325ff32f37cbe6208bb88 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/fa.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Armin1392" + ] + }, + "ldapauthentication-desc": "اعتبار Ø§ÙØ²ÙˆÙ†Ù‡Ù”‌ ال‌دی‌ای‌پی با پشتیبانی برای چند زوش تأیید ال‌دی‌ای‌پی" +} diff --git a/extensions/LdapAuthentication/i18n/fi.json b/extensions/LdapAuthentication/i18n/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..b2ecaa304069e350f2cdae802b920a39c8341804 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/fi.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Centerlink" + ] + }, + "ldapauthentication-desc": "LDAP-todentamisliitännäinen useiden LDAP-todennustapojen tuella" +} diff --git a/extensions/LdapAuthentication/i18n/fr.json b/extensions/LdapAuthentication/i18n/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..daee6e9c2f1351bc53bd9d6d409a186a0efd00ec --- /dev/null +++ b/extensions/LdapAuthentication/i18n/fr.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "IAlex", + "Urhixidur" + ] + }, + "ldapauthentication-desc": "Extension d’authentification LDAP prenant en charge de multiples méthodes d’authentification LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/gl.json b/extensions/LdapAuthentication/i18n/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..2dfa799c2d63b6a4b5340056927dd8c0de8a3e9e --- /dev/null +++ b/extensions/LdapAuthentication/i18n/gl.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Toliño" + ] + }, + "ldapauthentication-desc": "Complemento de autenticación LDAP con soporte para varios métodos de autenticación LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/gsw.json b/extensions/LdapAuthentication/i18n/gsw.json new file mode 100644 index 0000000000000000000000000000000000000000..081d6fe1c21821cc5097db49ea543cc022faae41 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/gsw.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Als-Holder" + ] + }, + "ldapauthentication-desc": "LDAP-Authentifizierigs-Plugin mit Unterstitzig fir multipli LDAP-Authentifizierigs-Merthode" +} diff --git a/extensions/LdapAuthentication/i18n/he.json b/extensions/LdapAuthentication/i18n/he.json new file mode 100644 index 0000000000000000000000000000000000000000..bd97448e4312a1fbcc61a6e20591a915c6575b57 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/he.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "YaronSh" + ] + }, + "ldapauthentication-desc": "תוסף ×ימות LDAP ×¢× ×ª×ž×™×›×” במספר שיטות LDAP ל×ימות" +} diff --git a/extensions/LdapAuthentication/i18n/hsb.json b/extensions/LdapAuthentication/i18n/hsb.json new file mode 100644 index 0000000000000000000000000000000000000000..894866d4e5012a6cb2c5e172a263722ae63867ec --- /dev/null +++ b/extensions/LdapAuthentication/i18n/hsb.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Michawiki" + ] + }, + "ldapauthentication-desc": "TykaÄ awtentifikacije LDAP z podpÄ›ru za wjacore metody LDAP-awtentifikacije" +} diff --git a/extensions/LdapAuthentication/i18n/hu.json b/extensions/LdapAuthentication/i18n/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..4ee3cb171ef4d4e7f5a441777fea6c52f2df3439 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/hu.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Glanthor Reviol" + ] + }, + "ldapauthentication-desc": "LDAP hitelesÃtési bÅ‘vÃtmény többféle LDAP azonosÃtási módszer támogatásával" +} diff --git a/extensions/LdapAuthentication/i18n/ia.json b/extensions/LdapAuthentication/i18n/ia.json new file mode 100644 index 0000000000000000000000000000000000000000..d967d11ca71a06bc76c2083173cfac523e0a675c --- /dev/null +++ b/extensions/LdapAuthentication/i18n/ia.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "McDutchie" + ] + }, + "ldapauthentication-desc": "Plugin pro authentication LDAP con supporto pro multiple methodos de authentication LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/id.json b/extensions/LdapAuthentication/i18n/id.json new file mode 100644 index 0000000000000000000000000000000000000000..95488c0012f9240c59133f8e1ada6aa8998259aa --- /dev/null +++ b/extensions/LdapAuthentication/i18n/id.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "IvanLanin" + ] + }, + "ldapauthentication-desc": "Pengaya otentikasi LDAP dengan dukungan untuk berbagai metode otentikasi LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/it.json b/extensions/LdapAuthentication/i18n/it.json new file mode 100644 index 0000000000000000000000000000000000000000..7845b739b8d19e8263740dadb8ff6c3fceb805ba --- /dev/null +++ b/extensions/LdapAuthentication/i18n/it.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "HalphaZ" + ] + }, + "ldapauthentication-desc": "Plugin di autenticazione LDAP con supporto a diversi metodi di autenticazione LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/ja.json b/extensions/LdapAuthentication/i18n/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..f64dbcbf97bd0b46d3efc9e4e6734430a6c792a2 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/ja.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Aotake", + "Shirayuki" + ] + }, + "ldapauthentication-desc": "複数㮠LDAP èªè¨¼æ–¹å¼å¯¾å¿œã® LDAP èªè¨¼ãƒ—ラグイン" +} diff --git a/extensions/LdapAuthentication/i18n/ko.json b/extensions/LdapAuthentication/i18n/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..d468f64991ce39e96c33b59bfa9bb0b87d174bcf --- /dev/null +++ b/extensions/LdapAuthentication/i18n/ko.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "ì•„ë¼", + "Revi" + ] + }, + "ldapauthentication-desc": "다양한 LDAP ì¸ì¦ ë°©ì‹ì„ ì§€ì›í•˜ëŠ” LDAP ì¸ì¦ 플러그ì¸" +} diff --git a/extensions/LdapAuthentication/i18n/ksh.json b/extensions/LdapAuthentication/i18n/ksh.json new file mode 100644 index 0000000000000000000000000000000000000000..4ba0ed1698bc1e46dd785c83f582dc80ec1dc57f --- /dev/null +++ b/extensions/LdapAuthentication/i18n/ksh.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Purodha" + ] + }, + "ldapauthentication-desc": "Dat Zohsatzprojramm för et Enlogge övver <i lang=\"en\">LDAP</i> löht ungerscheidlijje Mettoohde zoh, för et Prööfe, wä wä es." +} diff --git a/extensions/LdapAuthentication/i18n/lb.json b/extensions/LdapAuthentication/i18n/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..178143a2a54cbd6f22fdc81bcdab315668aabf1e --- /dev/null +++ b/extensions/LdapAuthentication/i18n/lb.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Robby" + ] + }, + "ldapauthentication-desc": "Authentifikatiouns-Plugin fir LDAP mat Ënnerstëtzung fir multipel LDAP Authentifikatiouns-Methoden" +} diff --git a/extensions/LdapAuthentication/i18n/lv.json b/extensions/LdapAuthentication/i18n/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..6ca22b3330d6efc41dc6e9390f6d12776b8e8848 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/lv.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Papuass" + ] + }, + "ldapauthentication-desc": "LDAP autentifikÄcijas spraudnis ar atbalstu vairÄkÄm LDAP autentifikÄcijas metodÄ“m" +} diff --git a/extensions/LdapAuthentication/i18n/mk.json b/extensions/LdapAuthentication/i18n/mk.json new file mode 100644 index 0000000000000000000000000000000000000000..b9254633958338cdcac4b4d39a1062225bf6c95a --- /dev/null +++ b/extensions/LdapAuthentication/i18n/mk.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Bjankuloski06" + ] + }, + "ldapauthentication-desc": "LDAP приклучок за потврдување Ñо поддршка за повеќе методи на LDAP потврдување" +} diff --git a/extensions/LdapAuthentication/i18n/ms.json b/extensions/LdapAuthentication/i18n/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..573635b11dbc8aa51d79f3f734f2024661167eb5 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/ms.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Anakmalaysia" + ] + }, + "ldapauthentication-desc": "Pemalam pengesahan LDAP dengan sokongan untuk berbilang kaedah pengesahan LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/nb.json b/extensions/LdapAuthentication/i18n/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..e77776408046ad4ce4bad7860495c4ce04a7d03a --- /dev/null +++ b/extensions/LdapAuthentication/i18n/nb.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Nghtwlkr" + ] + }, + "ldapauthentication-desc": "Programutvidelse for LDAP-autentisering med støtte for flere LDAP-autentiseringsmetoder" +} diff --git a/extensions/LdapAuthentication/i18n/nl.json b/extensions/LdapAuthentication/i18n/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..4af7e7768f14a348b9b620af664014f494278c31 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/nl.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Siebrand" + ] + }, + "ldapauthentication-desc": "LDAP-authenticatieplug-in met ondersteuning voor meerdere LDAP-authenticatiemethoden" +} diff --git a/extensions/LdapAuthentication/i18n/oc.json b/extensions/LdapAuthentication/i18n/oc.json new file mode 100644 index 0000000000000000000000000000000000000000..7ea1313511fd337d1e2dcf8447ef885505e24661 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/oc.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Cedric31" + ] + }, + "ldapauthentication-desc": "Plugin d'autentificacion LDAP amb supòrt de metòdes d'autentificacion LDAP multiples" +} diff --git a/extensions/LdapAuthentication/i18n/pl.json b/extensions/LdapAuthentication/i18n/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..942ccc08931d62ad82049a552ab7dbbec5306852 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/pl.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Sp5uhe" + ] + }, + "ldapauthentication-desc": "Wtyczka autoryzacji użytkowników z użyciem LDAP ze wsparciem dla wielu metod autoryzacji" +} diff --git a/extensions/LdapAuthentication/i18n/pms.json b/extensions/LdapAuthentication/i18n/pms.json new file mode 100644 index 0000000000000000000000000000000000000000..27814c6aa9c31129056c639a2d2e93211cdeb774 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/pms.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Borichèt", + "Dragonòt" + ] + }, + "ldapauthentication-desc": "Plugin për l'autenticassion LDAP con apògg për và ire manere d'autenticassion LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/pt-br.json b/extensions/LdapAuthentication/i18n/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..2d0e9f73b5e96981a322df72aaaa16da8c27aa50 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/pt-br.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Giro720" + ] + }, + "ldapauthentication-desc": "''Plugin'' de autenticação LDAP, com suporte para vários métodos de autenticação" +} diff --git a/extensions/LdapAuthentication/i18n/pt.json b/extensions/LdapAuthentication/i18n/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..94d5026ffe01ff54259e85c367e65f71d06d969b --- /dev/null +++ b/extensions/LdapAuthentication/i18n/pt.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Hamilton Abreu" + ] + }, + "ldapauthentication-desc": "''Plugin'' de autenticação LDAP, com suporte para vários métodos de autenticação" +} diff --git a/extensions/LdapAuthentication/i18n/qqq.json b/extensions/LdapAuthentication/i18n/qqq.json new file mode 100644 index 0000000000000000000000000000000000000000..f50b00c7fac7795b5b0c24642b8127bdb96ec061 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/qqq.json @@ -0,0 +1,10 @@ +{ + "@metadata": { + "authors": [ + "Fryed-peach", + "Shirayuki", + "Umherirrender" + ] + }, + "ldapauthentication-desc": "{{desc|name=LDAP Authentication|url=https://www.mediawiki.org/wiki/Extension:LDAP_Authentication}}" +} diff --git a/extensions/LdapAuthentication/i18n/roa-tara.json b/extensions/LdapAuthentication/i18n/roa-tara.json new file mode 100644 index 0000000000000000000000000000000000000000..83d6e477c2554600a830342ce6642cfc7c00d792 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/roa-tara.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Joetaras" + ] + }, + "ldapauthentication-desc": "plugin de autendicazione LDAP cu 'u supporte pe autendicaziune multeple de metode LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/ru.json b/extensions/LdapAuthentication/i18n/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..f19e13a41ccba76dccb275207f0ae569c0f7b862 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/ru.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "ÐлекÑандр Сигачёв" + ] + }, + "ldapauthentication-desc": "Плагин LDAP-аутентификации Ñ Ð¿Ð¾Ð´Ð´ÐµÑ€Ð¶ÐºÐ¾Ð¹ неÑкольких методов проверки подлинноÑти LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/sco.json b/extensions/LdapAuthentication/i18n/sco.json new file mode 100644 index 0000000000000000000000000000000000000000..25e1200d3f7eb1af65745bce4f77771d14db1778 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/sco.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "John Reid" + ] + }, + "ldapauthentication-desc": "LDAP authentication plug-in wi support fer multiple LDAP authentication methods" +} diff --git a/extensions/LdapAuthentication/i18n/sk.json b/extensions/LdapAuthentication/i18n/sk.json new file mode 100644 index 0000000000000000000000000000000000000000..5d28b36377d15c1d685483045b1bde70c8126bf6 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/sk.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Helix84" + ] + }, + "ldapauthentication-desc": "Zásuvný modul na autentifikáciu prostrednÃctvom LDAP s podporou viacerých metód LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/sr-ec.json b/extensions/LdapAuthentication/i18n/sr-ec.json new file mode 100644 index 0000000000000000000000000000000000000000..3b94c3aa50a3c82ba9c89a63a4593098e9862ed6 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/sr-ec.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Михајло Ðнђелковић" + ] + }, + "ldapauthentication-desc": "Плагин за LDAP ауторизацију, Ñа подршком за више метода LDAP ауторизације" +} diff --git a/extensions/LdapAuthentication/i18n/sr-el.json b/extensions/LdapAuthentication/i18n/sr-el.json new file mode 100644 index 0000000000000000000000000000000000000000..b601312f7478d0261ee9f784f4ec4a365c021e81 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/sr-el.json @@ -0,0 +1,4 @@ +{ + "@metadata": [], + "ldapauthentication-desc": "Plagin za LDAP autorizaciju, sa podrÅ¡kom za viÅ¡e metoda LDAP autorizacije" +} diff --git a/extensions/LdapAuthentication/i18n/sv.json b/extensions/LdapAuthentication/i18n/sv.json new file mode 100644 index 0000000000000000000000000000000000000000..85f9e4deed7bd0a94634a9f3fc68f4bd5729fb9b --- /dev/null +++ b/extensions/LdapAuthentication/i18n/sv.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Boivie" + ] + }, + "ldapauthentication-desc": "LDAP-autentiseringsplugin med stöd för flera LDAP-autentiseringsmetoder" +} diff --git a/extensions/LdapAuthentication/i18n/tl.json b/extensions/LdapAuthentication/i18n/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..6a4df3c61bde9fafe572378b536a0b5af2242150 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/tl.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "AnakngAraw" + ] + }, + "ldapauthentication-desc": "Pampasak na pangpagpapatotoo ng LDAP na may suporta para sa maramihang mga pamamaraan ng pagpapatotoo ng LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/tr.json b/extensions/LdapAuthentication/i18n/tr.json new file mode 100644 index 0000000000000000000000000000000000000000..3c27bed57c4693ea214ab7a9d983b62036a2f3fd --- /dev/null +++ b/extensions/LdapAuthentication/i18n/tr.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Vito Genovese" + ] + }, + "ldapauthentication-desc": "Birden çok LDAP kimlik doÄŸrulama yöntemini destekleyen LDAP kimlik doÄŸrulama eklentisi" +} diff --git a/extensions/LdapAuthentication/i18n/uk.json b/extensions/LdapAuthentication/i18n/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..92e0ec71e4949f44f2ae5b7408ca658e55bea5c1 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/uk.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Ytsukeng Fyvaprol" + ] + }, + "ldapauthentication-desc": "Плагін LDAP-аутентифікації з підтримкою декількох методів перевірки автентичноÑті LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/vi.json b/extensions/LdapAuthentication/i18n/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..b0009594a33f17c276426d044251eca40378632c --- /dev/null +++ b/extensions/LdapAuthentication/i18n/vi.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Minh Nguyen" + ] + }, + "ldapauthentication-desc": "Phần bổ trợ xác thá»±c LDAP há»— trợ nhiá»u phương pháp xác thá»±c LDAP" +} diff --git a/extensions/LdapAuthentication/i18n/yue.json b/extensions/LdapAuthentication/i18n/yue.json new file mode 100644 index 0000000000000000000000000000000000000000..e80107c0bed5b3c97f1fadf92e634d1a0d2a4dfe --- /dev/null +++ b/extensions/LdapAuthentication/i18n/yue.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Ktchankt" + ] + }, + "ldapauthentication-desc": "支æ´å„種 LDAP èªè‰æ–¹æ³•嘅 LDAP èªè‰æ’ä»¶" +} diff --git a/extensions/LdapAuthentication/i18n/zh-hans.json b/extensions/LdapAuthentication/i18n/zh-hans.json new file mode 100644 index 0000000000000000000000000000000000000000..5d0fa04fd28679a6efee2f36de48c0e03d701eb8 --- /dev/null +++ b/extensions/LdapAuthentication/i18n/zh-hans.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "Yanmiao liu" + ] + }, + "ldapauthentication-desc": "具有多ç§LDAPè®¤è¯æ–¹æ³•支æŒçš„LDAPè®¤è¯æ’ä»¶" +} diff --git a/extensions/LdapAuthentication/i18n/zh-hant.json b/extensions/LdapAuthentication/i18n/zh-hant.json new file mode 100644 index 0000000000000000000000000000000000000000..dad712e161c7ef285019f7e9e7bccd22470aa4bd --- /dev/null +++ b/extensions/LdapAuthentication/i18n/zh-hant.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Anakmalaysia", + "Cwlin0416" + ] + }, + "ldapauthentication-desc": "支æ´å¤šç¨® LDAP èªè‰æ–¹æ³•çš„ LDAP èªè‰æ“´å……套件" +} diff --git a/extensions/LdapAuthentication/package.json b/extensions/LdapAuthentication/package.json new file mode 100644 index 0000000000000000000000000000000000000000..797ef1d5102dbd1aff6635c93deab115515c14df --- /dev/null +++ b/extensions/LdapAuthentication/package.json @@ -0,0 +1,12 @@ +{ + "private": true, + "scripts": { + "test": "grunt test" + }, + "devDependencies": { + "grunt": "1.0.2", + "grunt-banana-checker": "0.6.0", + "grunt-contrib-jshint": "1.1.0", + "grunt-jsonlint": "1.1.0" + } +} diff --git a/extensions/LdapAuthentication/schema/ldap-mysql.sql b/extensions/LdapAuthentication/schema/ldap-mysql.sql new file mode 100644 index 0000000000000000000000000000000000000000..5f381e4468b96005cea23e1feb69afb33740239a --- /dev/null +++ b/extensions/LdapAuthentication/schema/ldap-mysql.sql @@ -0,0 +1,13 @@ +CREATE TABLE /*_*/ldap_domains ( + -- IF for domain + domain_id int not null primary key auto_increment, + + -- Domain itself + domain varchar(255) binary not null, + + -- User to which this domain belongs + user_id int not null + +) /*$wgDBTableOptions*/; + +CREATE INDEX /*i*/user_id on /*_*/ldap_domains (user_id); diff --git a/extensions/LdapAuthentication/schema/ldap-postgres.sql b/extensions/LdapAuthentication/schema/ldap-postgres.sql new file mode 100644 index 0000000000000000000000000000000000000000..ff2bf6f118017ca6b25bfd29f4402916a10ce65f --- /dev/null +++ b/extensions/LdapAuthentication/schema/ldap-postgres.sql @@ -0,0 +1,13 @@ +CREATE TABLE ldap_domains ( + -- IF for domain + domain_id serial PRIMARY KEY, + + -- Domain itself + domain varchar(255) not null, + + -- User to which this domain belongs + user_id integer not null + +) /*$wgDBTableOptions*/; + +CREATE INDEX user_id on ldap_domains (user_id); diff --git a/extensions/LdapAuthentication/version b/extensions/LdapAuthentication/version new file mode 100644 index 0000000000000000000000000000000000000000..3ee28ac6c7c546582ac35fbf4f51dbff005b6140 --- /dev/null +++ b/extensions/LdapAuthentication/version @@ -0,0 +1,4 @@ +LdapAuthentication: REL1_31 +2018-04-17T22:18:54 + +b19888c