ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK1B\4  db/install.phpnu[. /** * LDAP enrolment plugin installation. * * @package enrol_ldap * @author Iñaki Arenaza * @copyright 2010 Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); function xmldb_enrol_ldap_install() { global $CFG, $DB; } PK1B\qg db/tasks.phpnu[. /** * Task definition for enrol_ldap. * @author Guy Thomas * @copyright Copyright (c) 2017 Blackboard Inc. * @package enrol_ldap * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( 'classname' => '\enrol_ldap\task\sync_enrolments', 'blocking' => 0, 'minute' => 'R', 'hour' => 'R', 'day' => '*', 'month' => '*', 'dayofweek' => '*', 'disabled' => 1 ) ); PK1B\_4]]db/upgrade.phpnu[. /** * LDAP authentication plugin upgrade code * * @package auth_ldap * @copyright 2013 Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Function to upgrade auth_ldap. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_ldap_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } PK1B\5BBlang/en/auth_ldap.phpnu[. /** * Strings for component 'auth_ldap', language 'en'. * * @package auth_ldap * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_ldap_ad_create_req'] = 'Cannot create the new account in Active Directory. Make sure you meet all the requirements for this to work (LDAPS connection, bind user with adequate rights, etc.)'; $string['auth_ldap_attrcreators'] = 'List of groups or contexts whose members are allowed to create attributes. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\''; $string['auth_ldap_attrcreators_key'] = 'Attribute creators'; $string['auth_ldap_auth_user_create_key'] = 'Create users externally'; $string['auth_ldap_bind_dn'] = 'If you want to use bind-user to search users, specify it here. Something like \'cn=ldapuser,ou=public,o=org\''; $string['auth_ldap_bind_dn_key'] = 'Distinguished name'; $string['auth_ldap_bind_pw'] = 'Password for bind-user.'; $string['auth_ldap_bind_pw_key'] = 'Password'; $string['auth_ldap_bind_settings'] = 'Bind settings'; $string['auth_ldap_contexts'] = 'List of contexts where users are located. Separate different contexts with \';\'. For example: \'ou=users,o=org; ou=others,o=org\''; $string['auth_ldap_contexts_key'] = 'Contexts'; $string['auth_ldap_create_context'] = 'If you enable user creation with email confirmation, specify the context where users are created. This context should be different from other users to prevent security issues. You don\'t need to add this context to ldap_context-variable, Moodle will search for users from this context automatically.
Note! You have to modify the method user_create() in file auth/ldap/auth.php to make user creation work'; $string['auth_ldap_create_context_key'] = 'Context for new users'; $string['auth_ldap_create_error'] = 'Error creating user in LDAP.'; $string['auth_ldapdescription'] = 'This method provides authentication against an external LDAP server. If the given username and password are valid, Moodle creates a new user entry in its database. This plugin can read user attributes from LDAP and prefill wanted fields in Moodle. For following logins only the username and password are checked.'; $string['auth_ldap_expiration_desc'] = 'Select \'{$a->no}\' to disable expired password checking or \'{$a->ldapserver}\' to read the password expiry time directly from the LDAP server.'; $string['auth_ldap_expiration_key'] = 'Expiry'; $string['auth_ldap_expiration_warning_desc'] = 'Number of days before password expiry warning is issued.'; $string['auth_ldap_expiration_warning_key'] = 'Expiry warning'; $string['auth_ldap_expireattr_desc'] = 'Optional: Overrides the LDAP attribute that stores password expiry time.'; $string['auth_ldap_expireattr_key'] = 'Expiry attribute'; $string['auth_ldapextrafields'] = 'These fields are optional. You can choose to pre-fill some Moodle user fields with information from the LDAP fields that you specify here.

If you leave these fields blank, then nothing will be transferred from LDAP and Moodle defaults will be used instead.

In either case, the user will be able to edit all of these fields after they log in.

'; $string['auth_ldap_graceattr_desc'] = 'Optional: Overrides grace login attribute'; $string['auth_ldap_gracelogin_key'] = 'Grace login attribute'; $string['auth_ldap_gracelogins_desc'] = 'Enable LDAP grace login support. After password has expired, user can log in until grace login count is 0. Enabling this setting displays grace login message if password has expired.'; $string['auth_ldap_gracelogins_key'] = 'Grace logins'; $string['auth_ldap_groupecreators'] = 'List of groups or contexts whose members are allowed to create groups. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\''; $string['auth_ldap_groupecreators_key'] = 'Group creators'; $string['auth_ldap_host_url'] = 'Specify LDAP host in URL-form like \'ldap://ldap.myorg.com/\' or \'ldaps://ldap.myorg.com/\'. Separate multiple servers with \';\' to get failover support.'; $string['auth_ldap_host_url_key'] = 'Host URL'; $string['auth_ldap_changepasswordurl_key'] = 'Password-change URL'; $string['auth_ldap_ldap_encoding'] = 'Encoding used by the LDAP server, most likely utf-8. If LDAP v2 is selected, Active Directory uses its configured encoding, such as cp1252 or cp1250.'; $string['auth_ldap_ldap_encoding_key'] = 'LDAP encoding'; $string['auth_ldap_login_settings'] = 'Login settings'; $string['auth_ldap_memberattribute'] = 'Optional: Overrides user member attribute, when users belongs to a group. Usually \'member\''; $string['auth_ldap_memberattribute_isdn'] = 'Overrides handling of member attribute values'; $string['auth_ldap_memberattribute_isdn_key'] = 'Member attribute uses dn'; $string['auth_ldap_memberattribute_key'] = 'Member attribute'; $string['auth_ldap_noconnect'] = 'LDAP-module cannot connect to server: {$a}'; $string['auth_ldap_noconnect_all'] = 'LDAP-module cannot connect to any servers: {$a}'; $string['auth_ldap_noextension'] = 'The PHP LDAP module does not seem to be present. Please ensure it is installed and enabled if you want to use this authentication plugin.'; $string['auth_ldap_no_mbstring'] = 'You need the mbstring extension to create users in Active Directory.'; $string['auth_ldapnotinstalled'] = 'Cannot use LDAP authentication. The PHP LDAP module is not installed.'; $string['auth_ldap_objectclass'] = 'Optional: Overrides objectClass used to name/search users on ldap_user_type. Usually you don\'t need to change this.'; $string['auth_ldap_objectclass_key'] = 'Object class'; $string['auth_ldap_opt_deref'] = 'Determines how aliases are handled during search. Select one of the following values: "No" (LDAP_DEREF_NEVER) or "Yes" (LDAP_DEREF_ALWAYS)'; $string['auth_ldap_opt_deref_key'] = 'Dereference aliases'; $string['auth_ldap_passtype'] = 'Specify the format of new or changed passwords in LDAP server.'; $string['auth_ldap_passtype_key'] = 'Password format'; $string['auth_ldap_passwdexpire_settings'] = 'LDAP password expiry settings'; $string['auth_ldap_preventpassindb'] = 'Select yes to prevent passwords from being stored in Moodle\'s DB.'; $string['auth_ldap_preventpassindb_key'] = 'Prevent password caching'; $string['auth_ldap_rolecontext'] = '{$a->localname} context'; $string['auth_ldap_rolecontext_help'] = 'LDAP context used to select for {$a->localname} mapping. Separate multiple groups with \';\'. Usually something like "cn={$a->shortname},ou=first-ou-with-role-groups,o=myorg; cn={$a->shortname},ou=second-ou-with-role-groups,o=myorg".'; $string['auth_ldap_search_sub'] = 'Search users from subcontexts.'; $string['auth_ldap_search_sub_key'] = 'Search subcontexts'; $string['auth_ldap_server_settings'] = 'LDAP server settings'; $string['auth_ldap_unsupportedusertype'] = 'auth: ldap user_create() does not support selected usertype: {$a}'; $string['auth_ldap_update_userinfo'] = 'Update user information (firstname, lastname, address..) from LDAP to Moodle. Specify "Data mapping" settings as you need.'; $string['auth_ldap_user_attribute'] = 'Optional: Overrides the attribute used to name/search users. Usually \'cn\'.'; $string['auth_ldap_user_attribute_key'] = 'User attribute'; $string['auth_ldap_suspended_attribute'] = 'Optional: When provided this attribute will be used to enable/suspend the locally created user account.'; $string['auth_ldap_suspended_attribute_key'] = 'Suspended attribute'; $string['auth_ldap_user_exists'] = 'LDAP username already exists.'; $string['auth_ldap_user_settings'] = 'User lookup settings'; $string['auth_ldap_user_type'] = 'Select how users are stored in LDAP. This setting also specifies how login expiry, grace logins and user creation will work.'; $string['auth_ldap_user_type_key'] = 'User type'; $string['auth_ldap_usertypeundefined'] = 'config.user_type not defined or function ldap_expirationtime2unix does not support selected type!'; $string['auth_ldap_usertypeundefined2'] = 'config.user_type not defined or function ldap_unixi2expirationtime does not support selected type!'; $string['auth_ldap_version'] = 'The version of the LDAP protocol your server is using.'; $string['auth_ldap_version_key'] = 'Version'; $string['auth_ntlmsso'] = 'NTLM SSO'; $string['auth_ntlmsso_enabled'] = 'Set to yes to attempt Single Sign On with the NTLM domain. Note that this requires additional setup on the server to work. For further details, see the documentation NTLM authentication.'; $string['auth_ntlmsso_enabled_key'] = 'Enable'; $string['auth_ntlmsso_ie_fastpath'] = 'Set to enable the NTLM SSO fast path (bypasses certain steps if the client\'s browser is MS Internet Explorer).'; $string['auth_ntlmsso_ie_fastpath_key'] = 'MS IE fast path?'; $string['auth_ntlmsso_ie_fastpath_yesform'] = 'Yes, all other browsers use standard login form'; $string['auth_ntlmsso_ie_fastpath_yesattempt'] = 'Yes, attempt NTLM other browsers'; $string['auth_ntlmsso_ie_fastpath_attempt'] = 'Attempt NTLM with all browsers'; $string['auth_ntlmsso_maybeinvalidformat'] = 'Unable to extract the username from the REMOTE_USER header. Is the configured format right?'; $string['auth_ntlmsso_missing_username'] = 'You need to specify at least %username% in the remote username format'; $string['auth_ntlmsso_remoteuserformat_key'] = 'Remote username format'; $string['auth_ntlmsso_remoteuserformat'] = 'If you have chosen \'NTLM\' in \'Authentication type\', you can specify the remote username format here. If you leave this empty, the default DOMAIN\\username format will be used. You can use the optional %domain% placeholder to specify where the domain name appears, and the mandatory %username% placeholder to specify where the username appears.

Some widely used formats are %domain%\\%username% (MS Windows default), %domain%/%username%, %domain%+%username% and just %username% (if there is no domain part).'; $string['auth_ntlmsso_subnet'] = 'If set, it will only attempt SSO with clients in this subnet. Format: xxx.xxx.xxx.xxx/bitmask. Separate multiple subnets with \',\' (comma).'; $string['auth_ntlmsso_subnet_key'] = 'Subnet'; $string['auth_ntlmsso_type_key'] = 'Authentication type'; $string['auth_ntlmsso_type'] = 'The authentication method configured in the web server to authenticate the users (if in doubt, choose NTLM)'; $string['cannotmaprole'] = 'The role "{$a->rolename}" cannot be mapped because its short name "{$a->shortname}" is too long and/or contains hyphens. To allow it to be mapped, the short name needs to be reduced to a maximum of {$a->charlimit} characters and any hyphens removed. Edit the role'; $string['connectingldap'] = "Connecting to LDAP server...\n"; $string['connectingldapsuccess'] = "Connecting to your LDAP server was successful"; $string['creatingtemptable'] = "Creating temporary table {\$a}\n"; $string['didntfindexpiretime'] = 'password_expire() didn\'t find expiration time.'; $string['didntgetusersfromldap'] = "Did not get any users from LDAP -- error? -- exiting\n"; $string['gotcountrecordsfromldap'] = "Got {\$a} records from LDAP\n"; $string['invalidusererrors'] = "Warning: Skipped creation of {\$a} user accounts.\n\n"; $string['invaliduserexception'] = "\nError: Cannot create new user account. Details and reason:\n{\$a}\nSkipping this user.\n\n"; $string['ldapnotconfigured'] = 'The LDAP host url is currently not configured'; $string['morethanoneuser'] = 'More than one user record found in LDAP. Using only the first one.'; $string['needbcmath'] = 'You need the BCMath extension to use expired password checking with Active Directory.'; $string['needmbstring'] = 'You need the mbstring extension to change passwords in Active Directory'; $string['nodnforusername'] = 'Error in user_update_password(). No DN for: {$a->username}'; $string['noemail'] = 'Tried to send you an email but failed!'; $string['notcalledfromserver'] = 'Should not be called from the web server!'; $string['noupdatestobedone'] = "No updates to be done\n"; $string['nouserentriestoremove'] = "No user entries to be removed\n"; $string['nouserentriestorevive'] = "No user entries to be revived\n"; $string['nouserstobeadded'] = 'No user entries to be added'; $string['ntlmsso_attempting'] = 'Attempting Single Sign On via NTLM...'; $string['ntlmsso_failed'] = 'Auto-login failed, try the normal login page...'; $string['ntlmsso_isdisabled'] = 'NTLM SSO is disabled.'; $string['ntlmsso_unknowntype'] = 'Unknown ntlmsso type!'; $string['pagedresultsnotsupp'] = 'LDAP paged results not supported (either your PHP version lacks support, you have configured Moodle to use LDAP protocol version 2 or Moodle cannot contact your LDAP server to see if paged support is available.)'; $string['pagesize'] = 'Make sure this value is smaller than your LDAP server result set size limit (the maximum number of entries that can be returned in a single query)'; $string['pagesize_key'] = 'Page size'; $string['pluginname'] = 'LDAP server'; $string['pluginnotenabled'] = 'Plugin not enabled!'; $string['renamingnotallowed'] = 'User renaming not allowed in LDAP'; $string['rootdseerror'] = 'Error querying rootDSE for Active Directory'; $string['syncroles'] = 'Synchronise system roles from LDAP'; $string['synctask'] = 'LDAP users sync job'; $string['sync_updateuserchunk'] = 'Set this value to the number of users you want updated per transaction. Setting this to 0 will update all users in one transaction.'; $string['sync_updateuserchunk_key'] = 'Sync update users chunk size'; $string['systemrolemapping'] = 'System role mapping'; $string['start_tls'] = 'Use regular LDAP service (port 389) with TLS encryption'; $string['start_tls_key'] = 'Use TLS'; $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}
Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\''; $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\''; $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}'; $string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}'; $string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expiry time and/or grace logins. Error code: {$a->errno}; Error string: {$a->errstring}'; $string['updateusernotfound'] = 'Could not find user while updating externally. Details follow: search base: \'{$a->userdn}\'; search filter: \'(objectClass=*)\'; search attributes: {$a->attribs}'; $string['user_activatenotsupportusertype'] = 'auth: ldap user_activate() does not support selected usertype: {$a}'; $string['user_disablenotsupportusertype'] = 'auth: ldap user_disable() does not support selected usertype: {$a}'; $string['userentriestoadd'] = "User entries to be added: {\$a}\n"; $string['userentriestoremove'] = "User entries to be removed: {\$a}\n"; $string['userentriestorevive'] = "User entries to be revived: {\$a}\n"; $string['userentriestoupdate'] = "User entries to be updated: {\$a}\n"; $string['usernotfound'] = 'User not found in LDAP'; $string['useracctctrlerror'] = 'Error getting userAccountControl for {$a}'; $string['diag_genericerror'] = 'LDAP error {$a->code} reading {$a->subject}: {$a->message}.'; $string['diag_toooldversion'] = 'It is very unlikely a modern LDAP server uses LDAPv2 protocol. Wrong settings can corrupt values in user fields. Check with your LDAP administrator.'; $string['diag_emptycontext'] = 'Empty context found.'; $string['diag_contextnotfound'] = 'Context {$a} doesn\'t exist or can\'t be read by bind DN.'; $string['diag_rolegroupnotfound'] = 'Group {$a->group} for role {$a->localname} doesn\'t exist or can\'t be read by bind DN.'; $string['privacy:metadata'] = 'The LDAP server authentication plugin does not store any personal data.'; PK1B\Ӷd version.phpnu[. /** * LDAP enrolment plugin version specification. * * @package enrol_ldap * @author Iñaki Arenaza * @copyright 2010 Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'enrol_ldap'; // Full name of the plugin (used for diagnostics) PK1B\|hgQ775classes/admin_setting_special_contexts_configtext.phpnu[. /** * Special setting for auth_ldap that cleans up context values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special setting for auth_ldap that cleans up context values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_ldap_admin_setting_special_contexts_configtext extends admin_setting_configtext { /** * We need to remove duplicates on save to prevent issues in other areas of Moodle. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { // Try to remove duplicates before storing the contexts (to avoid problems in sync_users()). $data = explode(';', $data); $data = array_map(function($x) { return core_text::strtolower(trim($x)); }, $data); $data = implode(';', array_unique($data)); return parent::write_setting($data); } } PK1B\5Oclasses/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for enrol_ldap. * * @package enrol_ldap * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_ldap\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for enrol_ldap implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK1B\z1classes/admin_setting_special_ntlm_configtext.phpnu[. /** * Special admin setting for auth_ldap that validates ntlm usernames. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special admin setting for auth_ldap that validates ntlm usernames. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_ldap_admin_setting_special_ntlm_configtext extends admin_setting_configtext { /** * We need to validate the username format when using NTLM. * * @param string $data Form data. * @return string Empty when no errors. */ public function validate($data) { if (get_config('auth_ldap', 'ntlmsso_type') === 'ntlm') { $format = trim($data); if (!empty($format) && !preg_match('/%username%/i', $format)) { return get_string('auth_ntlmsso_missing_username', 'auth_ldap'); } } return parent::validate($data); } } PK1B\8classes/task/sync_task.phpnu[. /** * A scheduled task for LDAP user sync. * * @package auth_ldap * @copyright 2015 Vadim Dvorovenko * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\task; /** * A scheduled task class for LDAP user sync. * * @copyright 2015 Vadim Dvorovenko * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_task extends \core\task\scheduled_task { /** @var string Message prefix for mtrace */ protected const MTRACE_MSG = 'Synced ldap users'; /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('synctask', 'auth_ldap'); } /** * Run users sync. */ public function execute() { if (is_enabled_auth('ldap')) { /** @var auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $count = 0; $auth->sync_users_update_callback(function ($users, $updatekeys) use (&$count) { $asynctask = new asynchronous_sync_task(); $asynctask->set_custom_data([ 'users' => $users, 'updatekeys' => $updatekeys, ]); \core\task\manager::queue_adhoc_task($asynctask); $count++; mtrace(sprintf(" %s (%d)", self::MTRACE_MSG, $count)); sleep(1); }); } } } PK1B\Eclasses/task/sync_roles.phpnu[. /** * A scheduled task for LDAP roles sync. * * @package auth_ldap * @author David Balch * @copyright 2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\task; defined('MOODLE_INTERNAL') || die(); /** * A scheduled task class for LDAP roles sync. * * @author David Balch * @copyright 2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_roles extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('syncroles', 'auth_ldap'); } /** * Synchronise role assignments from LDAP. */ public function execute() { global $DB; if (is_enabled_auth('ldap')) { $auth = get_auth_plugin('ldap'); $users = $DB->get_records('user', array('auth' => 'ldap')); foreach ($users as $user) { $auth->sync_roles($user); } } } } PK1B\*'classes/task/asynchronous_sync_task.phpnu[. /** * Adhoc task for LDAP user sync. * * @package auth_ldap * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\task; use core\task\adhoc_task; /** * Adhoc task class for LDAP user sync. * * @package auth_ldap * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class asynchronous_sync_task extends adhoc_task { /** @var string Message prefix for mtrace */ protected const MTRACE_MSG = 'Synced ldap users'; /** * Constructor */ public function __construct() { $this->set_component('auth_ldap'); } /** * Run users sync. */ public function execute() { $data = $this->get_custom_data(); /** @var auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $auth->update_users($data->users, $data->updatekeys); mtrace(sprintf(" %s (%d)", self::MTRACE_MSG, count($data->users))); } } PK1B\^=ZYclasses/adminpresets/adminpresets_auth_ldap_admin_setting_special_contexts_configtext.phpnu[. namespace auth_ldap\adminpresets; use core_adminpresets\local\setting\adminpresets_setting; /** * Basic text setting, cleans the param using the admin_setting paramtext attribute. * * @package auth_ldap * @copyright 2021 Pimenko * @author Jordan Kesraoui | Sylvain Revenu | Pimenko based on David Monllaó code * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class adminpresets_auth_ldap_admin_setting_special_contexts_configtext extends adminpresets_setting { /** * Validates the value using paramtype attribute * * @param string $value * @return boolean Returned value is always true, whenever the value has been successfully cleaned or not. */ protected function set_value($value): bool { $this->value = $value; if (empty($this->settingdata->paramtype)) { // For configfile, configpasswordunmask.... $this->settingdata->paramtype = 'RAW'; } $paramtype = 'PARAM_' . strtoupper($this->settingdata->paramtype); // Regexp. if (!defined($paramtype)) { $this->value = preg_replace($this->settingdata->paramtype, '', $this->value); // Standard moodle param type. } else { $this->value = clean_param($this->value, constant($paramtype)); } $this->set_visiblevalue(); return true; } } PK1B\6classes/admin_setting_special_lowercase_configtext.phpnu[. /** * Special setting for auth_ldap that lowercases values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special setting for auth_ldap that lowercases values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_ldap_admin_setting_special_lowercase_configtext extends admin_setting_configtext { /** * We need to convert the data to lowercase prior to save. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { return parent::write_setting(core_text::strtolower($data)); } } PK1B\ upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in the enrol_ldap code. === 3.8 === * enrol/ldap/cli/sync.php script has been removed. You should use enrol_ldap\task\sync_enrolments task instead. === 3.3 === * enrol/ldap/cli/sync.php script has been deprecated in favour of enrol_ldap\task\sync_enrolments task. PK1B\ locallib.phpnu[. /** * Internal library of functions for module auth_ldap * * @package auth_ldap * @author David Balch * @copyright 2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Get a list of system roles assignable by the current or a specified user, including their localised names. * * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user. * @return array $roles, each role as an array with id, shortname, localname, and settingname for the config value. */ function get_ldap_assignable_role_names($user = null) { $roles = array(); if ($assignableroles = get_assignable_roles(context_system::instance(), ROLENAME_SHORT, false, $user)) { $systemroles = role_fix_names(get_all_roles(), context_system::instance(), ROLENAME_ORIGINAL); foreach ($assignableroles as $shortname) { foreach ($systemroles as $systemrole) { if ($systemrole->shortname == $shortname) { $roles[] = array('id' => $systemrole->id, 'shortname' => $shortname, 'localname' => $systemrole->localname, 'settingname' => $shortname . 'context'); break; } } } } return $roles; } PK1B\QnfJJ README-LDAPnu[LDAP-module README Please read comments from auth.php in this directory. PK1B\zT} cli/sync_users.phpnu[. /** * CAS user sync script. * * This script is meant to be called from a cronjob to sync moodle with the LDAP * backend in those setups where the LDAP backend acts as 'master'. * * Notes: * - it is required to use the web server account when executing PHP CLI scripts * - you need to change the "www-data" to match the apache user account * - use "su" if "sudo" not available * - If you have a large number of users, you may want to raise the memory limits * by passing -d momory_limit=256M * - For debugging & better logging, you are encouraged to use in the command line: * -d log_errors=1 -d error_reporting=E_ALL -d display_errors=0 -d html_errors=0 * - If you have a large number of users, you may want to raise the memory limits * by passing -d momory_limit=256M * - For debugging & better logging, you are encouraged to use in the command line: * -d log_errors=1 -d error_reporting=E_ALL -d display_errors=0 -d html_errors=0 * * Performance notes: * We have optimized it as best as we could for PostgreSQL and MySQL, with 27K students * we have seen this take 10 minutes. * * @package auth_ldap * @copyright 2004 Martin Langhoff * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 3.0 MDL-51824 - please do not use this CLI script any more, use scheduled task instead. * @todo MDL-50264 This will be deleted in Moodle 3.2. */ define('CLI_SCRIPT', true); require(__DIR__.'/../../../config.php'); // global moodle config file. require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->libdir.'/clilib.php'); // Ensure errors are well explained set_debugging(DEBUG_DEVELOPER, true); if (!is_enabled_auth('ldap')) { error_log('[AUTH LDAP] '.get_string('pluginnotenabled', 'auth_ldap')); die; } cli_problem('[AUTH LDAP] The users sync cron has been deprecated. Please use the scheduled task instead.'); // Abort execution of the CLI script if the auth_ldap\task\sync_task is enabled. $taskdisabled = \core\task\manager::get_scheduled_task('auth_ldap\task\sync_task'); if (!$taskdisabled->get_disabled()) { cli_error('[AUTH LDAP] The scheduled task sync_task is enabled, the cron execution has been aborted.'); } $ldapauth = get_auth_plugin('ldap'); $ldapauth->sync_users(true); PK1B\TfTftests/auth_ldap_test.phpnu[. namespace auth_ldap; /** * LDAP authentication plugin tests. * * NOTE: in order to execute this test you need to set up * OpenLDAP server with core, cosine, nis and internet schemas * and add configuration constants to config.php or phpunit.xml configuration file: * * define('TEST_AUTH_LDAP_HOST_URL', 'ldap://127.0.0.1'); * define('TEST_AUTH_LDAP_BIND_DN', 'cn=someuser,dc=example,dc=local'); * define('TEST_AUTH_LDAP_BIND_PW', 'somepassword'); * define('TEST_AUTH_LDAP_DOMAIN', 'dc=example,dc=local'); * * @package auth_ldap * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use auth_plugin_ldap; use auth_ldap\task\{ sync_task, asynchronous_sync_task, }; /** * LDAP authentication plugin tests. * * @package auth_ldap * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class auth_ldap_test extends \advanced_testcase { public static function setUpBeforeClass(): void { global $CFG; parent::setUpBeforeClass(); require_once($CFG->dirroot . '/auth/ldap/auth.php'); require_once($CFG->libdir . '/ldaplib.php'); } /** * Data provider for auth_ldap tests * * Used to ensure that all the paged stuff works properly, irrespectively * of the pagesize configured (that implies all the chunking and paging * built in the plugis is doing its work consistently). Both searching and * not searching within subcontexts. * * @return array[] */ public static function auth_ldap_provider(): array { $pagesizes = [1, 3, 5, 1000]; $subcontexts = [0, 1]; $combinations = []; foreach ($pagesizes as $pagesize) { foreach ($subcontexts as $subcontext) { $combinations["pagesize {$pagesize}, subcontexts {$subcontext}"] = [$pagesize, $subcontext]; } } return $combinations; } /** * General auth_ldap testcase * * @dataProvider auth_ldap_provider * @param int $pagesize Value to be configured in settings controlling page size. * @param int $subcontext Value to be configured in settings controlling searching in subcontexts. */ public function test_auth_ldap(int $pagesize, int $subcontext): void { global $DB; if (!extension_loaded('ldap')) { $this->markTestSkipped('LDAP extension is not loaded.'); } $this->resetAfterTest(); if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) { $this->markTestSkipped('External LDAP test server not configured.'); } // Make sure we can connect the server. $debuginfo = ''; if (!$connection = ldap_connect_moodle(TEST_AUTH_LDAP_HOST_URL, 3, 'rfc2307', TEST_AUTH_LDAP_BIND_DN, TEST_AUTH_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) { $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo); } $this->enable_plugin(); // Create new empty test container. $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN; $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest'); $o = array(); $o['objectClass'] = array('dcObject', 'organizationalUnit'); $o['dc'] = 'moodletest'; $o['ou'] = 'MOODLETEST'; if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) { $this->markTestSkipped('Can not create test LDAP container.'); } // Create a few users. $o = array(); $o['objectClass'] = array('organizationalUnit'); $o['ou'] = 'users'; ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o); $createdusers = array(); for ($i=1; $i<=5; $i++) { $this->create_ldap_user($connection, $topdn, $i); $createdusers[] = 'username' . $i; } // Set up creators group. $assignedroles = array('username1', 'username2'); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'creators'; $o['gidNumber'] = 1; $o['memberUid'] = $assignedroles; ldap_add($connection, 'cn='.$o['cn'].','.$topdn, $o); $creatorrole = $DB->get_record('role', array('shortname'=>'coursecreator')); $this->assertNotEmpty($creatorrole); // Configure the plugin a bit. set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth_ldap'); set_config('start_tls', 0, 'auth_ldap'); set_config('ldap_version', 3, 'auth_ldap'); set_config('ldapencoding', 'utf-8', 'auth_ldap'); set_config('pagesize', $pagesize, 'auth_ldap'); set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth_ldap'); set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth_ldap'); set_config('user_type', 'rfc2307', 'auth_ldap'); set_config('contexts', 'ou=users,'.$topdn, 'auth_ldap'); set_config('search_sub', $subcontext, 'auth_ldap'); set_config('opt_deref', LDAP_DEREF_NEVER, 'auth_ldap'); set_config('user_attribute', 'cn', 'auth_ldap'); set_config('memberattribute', 'memberuid', 'auth_ldap'); set_config('memberattribute_isdn', 0, 'auth_ldap'); set_config('coursecreatorcontext', 'cn=creators,'.$topdn, 'auth_ldap'); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap'); set_config('field_map_email', 'mail', 'auth_ldap'); set_config('field_updatelocal_email', 'oncreate', 'auth_ldap'); set_config('field_updateremote_email', '0', 'auth_ldap'); set_config('field_lock_email', 'unlocked', 'auth_ldap'); set_config('field_map_firstname', 'givenName', 'auth_ldap'); set_config('field_updatelocal_firstname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_firstname', '0', 'auth_ldap'); set_config('field_lock_firstname', 'unlocked', 'auth_ldap'); set_config('field_map_lastname', 'sn', 'auth_ldap'); set_config('field_updatelocal_lastname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_lastname', '0', 'auth_ldap'); set_config('field_lock_lastname', 'unlocked', 'auth_ldap'); $this->assertEquals(2, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('role_assignments')); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 5 users created with 2 users having roles. $this->assertCount(7, $events); foreach ($events as $index => $event) { $username = $DB->get_field('user', 'username', array('id' => $event->relateduserid)); // Get username. if ($event->eventname === '\core\event\user_created') { $this->assertContains($username, $createdusers); unset($events[$index]); // Remove matching event. } else if ($event->eventname === '\core\event\role_assigned') { $this->assertContains($username, $assignedroles); unset($events[$index]); // Remove matching event. } else { $this->fail('Unexpected event found: ' . $event->eventname); } } // If all the user_created and role_assigned events have matched // then the $events array should be now empty. $this->assertCount(0, $events); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); for ($i=1; $i<=5; $i++) { $this->assertTrue($DB->record_exists('user', array('username'=>'username'.$i, 'email'=>'user'.$i.'@example.com', 'firstname'=>'Firstname'.$i, 'lastname'=>'Lastname'.$i))); } $this->delete_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, no new event. $this->assertCount(0, $events); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth_ldap'); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 1 user got updated. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_updated', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('auth'=>'nologin', 'username'=>'username1'))); $this->assertEquals(1, $DB->count_records('user', array('auth'=>'ldap', 'suspended'=>'1', 'username'=>'username1'))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); $this->create_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 1 user got updated. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_updated', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); $DB->set_field('user', 'auth', 'nologin', array('username'=>'username1')); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 1 user got updated. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_updated', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_ldap'); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $this->delete_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 2 events role_unassigned and user_deleted. $this->assertCount(2, $events); $event = array_pop($events); $this->assertInstanceOf('\core\event\user_deleted', $event); $event = array_pop($events); $this->assertInstanceOf('\core\event\role_unassigned', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('username'=>'username1'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(1, $DB->count_records('role_assignments')); $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); $this->create_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 2 events role_assigned and user_created. $this->assertCount(2, $events); $event = array_pop($events); $this->assertInstanceOf('\core\event\role_assigned', $event); $event = array_pop($events); $this->assertInstanceOf('\core\event\user_created', $event); $this->assertEquals(6, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(1, $DB->count_records('user', array('username'=>'username1'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); // Let's test syncing users in chunks of '1'. set_config('field_updatelocal_email', 'onlogin', 'auth_ldap'); set_config('sync_updateuserchunk', 1, 'auth_ldap'); /** @var auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $count = 0; ob_start(); $auth->sync_users_update_callback(function ($users, $updatekeys) use (&$count) { $count++; }); ob_end_clean(); // After updating in chunks of '1', we should have counted more than one update. $this->assertGreaterThan(1, $count); ob_start(); \core\cron::setup_user(); $cron = new sync_task(); $cron->execute(); $this->runAdhocTasks('\auth_ldap\task\asynchronous_sync_task'); $output = ob_get_contents(); ob_end_clean(); // Use Reflection to make protected constants available. $rp = new \ReflectionClassConstant(sync_task::class, 'MTRACE_MSG'); $synctaskmsg = $rp->getValue(); $rp = new \ReflectionClassConstant(asynchronous_sync_task::class, 'MTRACE_MSG'); $asynctaskmsg = $rp->getValue(); $this->assertMatchesRegularExpression( sprintf('/%s.*%s/s', $synctaskmsg, $asynctaskmsg), $output ); $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest'); ldap_close($connection); } /** * Test logging in via LDAP calls a user_loggedin event. */ public function test_ldap_user_loggedin_event(): void { global $CFG, $DB, $USER; $this->resetAfterTest(); $this->assertFalse(isloggedin()); $user = $DB->get_record('user', array('username'=>'admin')); // Note: we are just going to trigger the function that calls the event, // not actually perform a LDAP login, for the sake of sanity. $ldap = new \auth_plugin_ldap(); // Set the key for the cache flag we want to set which is used by LDAP. set_cache_flag($ldap->pluginconfig . '/ntlmsess', sesskey(), $user->username, AUTH_NTLMTIMEOUT); // We are going to need to set the sesskey as the user's password in order for the LDAP log in to work. update_internal_user_password($user, sesskey()); // The function ntlmsso_finish is responsible for triggering the event, so call it directly and catch the event. $sink = $this->redirectEvents(); // We need to supress this function call, or else we will get the message "session_regenerate_id(): Cannot // regenerate session id - headers already sent" as the ntlmsso_finish function calls complete_user_login @$ldap->ntlmsso_finish(); $events = $sink->get_events(); $sink->close(); // Check that the event is valid. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_loggedin', $event); $this->assertEquals('user', $event->objecttable); $this->assertEquals('2', $event->objectid); $this->assertEquals(\context_system::instance()->id, $event->contextid); } /** * Test logging in via LDAP calls a user_loggedin event. */ public function test_ldap_user_signup(): void { global $CFG, $DB; // User to create. $user = array( 'username' => 'usersignuptest1', 'password' => 'Moodle2014!', 'idnumber' => 'idsignuptest1', 'firstname' => 'First Name User Test 1', 'lastname' => 'Last Name User Test 1', 'middlename' => 'Middle Name User Test 1', 'lastnamephonetic' => '最後のお名前のテスト一号', 'firstnamephonetic' => 'お名前のテスト一号', 'alternatename' => 'Alternate Name User Test 1', 'email' => 'usersignuptest1@example.com', 'description' => 'This is a description for user 1', 'city' => 'Perth', 'country' => 'AU', 'mnethostid' => $CFG->mnet_localhost_id, 'auth' => 'ldap' ); if (!extension_loaded('ldap')) { $this->markTestSkipped('LDAP extension is not loaded.'); } $this->resetAfterTest(); if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) { $this->markTestSkipped('External LDAP test server not configured.'); } // Make sure we can connect the server. $debuginfo = ''; if (!$connection = ldap_connect_moodle(TEST_AUTH_LDAP_HOST_URL, 3, 'rfc2307', TEST_AUTH_LDAP_BIND_DN, TEST_AUTH_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) { $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo); } $this->enable_plugin(); // Create new empty test container. $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN; $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest'); $o = array(); $o['objectClass'] = array('dcObject', 'organizationalUnit'); $o['dc'] = 'moodletest'; $o['ou'] = 'MOODLETEST'; if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) { $this->markTestSkipped('Can not create test LDAP container.'); } // Create a few users. $o = array(); $o['objectClass'] = array('organizationalUnit'); $o['ou'] = 'users'; ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o); // Configure the plugin a bit. set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth_ldap'); set_config('start_tls', 0, 'auth_ldap'); set_config('ldap_version', 3, 'auth_ldap'); set_config('ldapencoding', 'utf-8', 'auth_ldap'); set_config('pagesize', '2', 'auth_ldap'); set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth_ldap'); set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth_ldap'); set_config('user_type', 'rfc2307', 'auth_ldap'); set_config('contexts', 'ou=users,'.$topdn, 'auth_ldap'); set_config('search_sub', 0, 'auth_ldap'); set_config('opt_deref', LDAP_DEREF_NEVER, 'auth_ldap'); set_config('user_attribute', 'cn', 'auth_ldap'); set_config('memberattribute', 'memberuid', 'auth_ldap'); set_config('memberattribute_isdn', 0, 'auth_ldap'); set_config('creators', 'cn=creators,'.$topdn, 'auth_ldap'); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap'); set_config('field_map_email', 'mail', 'auth_ldap'); set_config('field_updatelocal_email', 'oncreate', 'auth_ldap'); set_config('field_updateremote_email', '0', 'auth_ldap'); set_config('field_lock_email', 'unlocked', 'auth_ldap'); set_config('field_map_firstname', 'givenName', 'auth_ldap'); set_config('field_updatelocal_firstname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_firstname', '0', 'auth_ldap'); set_config('field_lock_firstname', 'unlocked', 'auth_ldap'); set_config('field_map_lastname', 'sn', 'auth_ldap'); set_config('field_updatelocal_lastname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_lastname', '0', 'auth_ldap'); set_config('field_lock_lastname', 'unlocked', 'auth_ldap'); set_config('passtype', 'md5', 'auth_ldap'); set_config('create_context', 'ou=users,'.$topdn, 'auth_ldap'); $this->assertEquals(2, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('role_assignments')); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $sink = $this->redirectEvents(); $mailsink = $this->redirectEmails(); $auth->user_signup((object)$user, false); $this->assertEquals(1, $mailsink->count()); $events = $sink->get_events(); $sink->close(); // Verify 2 events get generated. $this->assertCount(2, $events); // Get record from db. $dbuser = $DB->get_record('user', array('username' => $user['username'])); $user['id'] = $dbuser->id; // Last event is user_created. $event = array_pop($events); $this->assertInstanceOf('\core\event\user_created', $event); $this->assertEquals($user['id'], $event->objectid); $this->assertEquals(\context_user::instance($user['id']), $event->get_context()); // First event is user_password_updated. $event = array_pop($events); $this->assertInstanceOf('\core\event\user_password_updated', $event); $this->assertEventContextNotUsed($event); // Delete user which we just created. ldap_delete($connection, 'cn='.$user['username'].',ou=users,'.$topdn); } protected function create_ldap_user($connection, $topdn, $i) { $o = array(); $o['objectClass'] = array('inetOrgPerson', 'organizationalPerson', 'person', 'posixAccount'); $o['cn'] = 'username'.$i; $o['sn'] = 'Lastname'.$i; $o['givenName'] = 'Firstname'.$i; $o['uid'] = $o['cn']; $o['uidnumber'] = 2000+$i; $o['gidNumber'] = 1000+$i; $o['homeDirectory'] = '/'; $o['mail'] = 'user'.$i.'@example.com'; $o['userPassword'] = 'pass'.$i; ldap_add($connection, 'cn='.$o['cn'].',ou=users,'.$topdn, $o); } protected function delete_ldap_user($connection, $topdn, $i) { ldap_delete($connection, 'cn=username'.$i.',ou=users,'.$topdn); } protected function enable_plugin() { $auths = get_enabled_auth_plugins(); if (!in_array('ldap', $auths)) { $auths[] = 'ldap'; } set_config('auth', implode(',', $auths)); } protected function recursive_delete($connection, $dn, $filter) { if ($res = ldap_list($connection, $dn, $filter, array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); if ($info['count'] > 0) { if ($res = ldap_search($connection, "$filter,$dn", 'cn=*', array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); foreach ($info as $i) { if (isset($i['dn'])) { ldap_delete($connection, $i['dn']); } } } if ($res = ldap_search($connection, "$filter,$dn", 'ou=*', array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); foreach ($info as $i) { if (isset($i['dn']) and $info[0]['dn'] != $i['dn']) { ldap_delete($connection, $i['dn']); } } } ldap_delete($connection, "$filter,$dn"); } } } } PK1B\#YCntlmsso_attempt.phpnu[set_url('/auth/ldap/ntlmsso_attempt.php'); $PAGE->set_context(context_system::instance()); // Define variables used in page $site = get_site(); $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. if (!in_array('ldap', $authsequence, true)) { throw new \moodle_exception('ldap_isdisabled', 'auth'); } $authplugin = get_auth_plugin('ldap'); if (empty($authplugin->config->ntlmsso_enabled)) { throw new \moodle_exception('ntlmsso_isdisabled', 'auth_ldap'); } $sesskey = sesskey(); // Display the page header. This makes redirect respect the timeout we specify // here (and not add 3 more secs) which in turn prevents a bug in both IE 6.x // and FF 3.x (Windows version at least) where javascript timers fire up even // when we've already left the page that set the timer. $loginsite = get_string("loginsite"); $PAGE->navbar->add($loginsite); $PAGE->set_title($loginsite); $PAGE->set_heading($site->fullname); echo $OUTPUT->header(); $msg = '

'.get_string('ntlmsso_attempting', 'auth_ldap').'

' . ''; redirect($CFG->wwwroot . '/auth/ldap/ntlmsso_finish.php', $msg, 3); PK1B\ƫÆntlmsso_finish.phpnu[set_url('/auth/ldap/ntlmsso_finish.php'); $PAGE->set_context(context_system::instance()); // Define variables used in page $site = get_site(); $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. if (!in_array('ldap', $authsequence, true)) { throw new \moodle_exception('ldap_isdisabled', 'auth'); } $authplugin = get_auth_plugin('ldap'); if (empty($authplugin->config->ntlmsso_enabled)) { throw new \moodle_exception('ntlmsso_isdisabled', 'auth_ldap'); } // If ntlmsso_finish() succeeds, then the code never returns, // so we only worry about failure. if (!$authplugin->ntlmsso_finish()) { // Redirect to login, saying "don't try again!" // Display the page header. This makes redirect respect the timeout we specify // here (and not add 3 more secs). $loginsite = get_string("loginsite"); $PAGE->navbar->add($loginsite); $PAGE->set_title($loginsite); $PAGE->set_heading($site->fullname); echo $OUTPUT->header(); redirect($CFG->wwwroot . '/login/index.php?authldap_skipntlmsso=1', get_string('ntlmsso_failed','auth_ldap'), 3); } PK1B\`||auth.phpnu[. /** * Authentication Plugin: LDAP Authentication * Authentication using LDAP (Lightweight Directory Access Protocol). * * @package auth_ldap * @author Martin Dougiamas * @author Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); // See http://support.microsoft.com/kb/305144 to interprete these values. if (!defined('AUTH_AD_ACCOUNTDISABLE')) { define('AUTH_AD_ACCOUNTDISABLE', 0x0002); } if (!defined('AUTH_AD_NORMAL_ACCOUNT')) { define('AUTH_AD_NORMAL_ACCOUNT', 0x0200); } if (!defined('AUTH_NTLMTIMEOUT')) { // timewindow for the NTLM SSO process, in secs... define('AUTH_NTLMTIMEOUT', 10); } // UF_DONT_EXPIRE_PASSWD value taken from MSDN directly if (!defined('UF_DONT_EXPIRE_PASSWD')) { define ('UF_DONT_EXPIRE_PASSWD', 0x00010000); } // The Posix uid and gid of the 'nobody' account and 'nogroup' group. if (!defined('AUTH_UID_NOBODY')) { define('AUTH_UID_NOBODY', -2); } if (!defined('AUTH_GID_NOGROUP')) { define('AUTH_GID_NOGROUP', -2); } // Regular expressions for a valid NTLM username and domain name. if (!defined('AUTH_NTLM_VALID_USERNAME')) { define('AUTH_NTLM_VALID_USERNAME', '[^/\\\\\\\\\[\]:;|=,+*?<>@"]+'); } if (!defined('AUTH_NTLM_VALID_DOMAINNAME')) { define('AUTH_NTLM_VALID_DOMAINNAME', '[^\\\\\\\\\/:*?"<>|]+'); } // Default format for remote users if using NTLM SSO if (!defined('AUTH_NTLM_DEFAULT_FORMAT')) { define('AUTH_NTLM_DEFAULT_FORMAT', '%domain%\\%username%'); } if (!defined('AUTH_NTLM_FASTPATH_ATTEMPT')) { define('AUTH_NTLM_FASTPATH_ATTEMPT', 0); } if (!defined('AUTH_NTLM_FASTPATH_YESFORM')) { define('AUTH_NTLM_FASTPATH_YESFORM', 1); } if (!defined('AUTH_NTLM_FASTPATH_YESATTEMPT')) { define('AUTH_NTLM_FASTPATH_YESATTEMPT', 2); } // Allows us to retrieve a diagnostic message in case of LDAP operation error if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) { define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032); } require_once($CFG->libdir.'/authlib.php'); require_once($CFG->libdir.'/ldaplib.php'); require_once($CFG->dirroot.'/user/lib.php'); require_once($CFG->dirroot.'/auth/ldap/locallib.php'); /** * LDAP authentication plugin. */ class auth_plugin_ldap extends auth_plugin_base { /** @var string */ protected $roleauth; /** @var string */ public $pluginconfig; /** @var LDAP\Connection LDAP connection. */ protected $ldapconnection; /** @var int */ protected $ldapconns = 0; /** * Init plugin config from database settings depending on the plugin auth type. */ function init_plugin($authtype) { $this->pluginconfig = 'auth_'.$authtype; $this->config = get_config($this->pluginconfig); if (empty($this->config->ldapencoding)) { $this->config->ldapencoding = 'utf-8'; } if (empty($this->config->user_type)) { $this->config->user_type = 'default'; } $ldap_usertypes = ldap_supported_usertypes(); $this->config->user_type_name = $ldap_usertypes[$this->config->user_type]; unset($ldap_usertypes); $default = ldap_getdefaults(); // Use defaults if values not given foreach ($default as $key => $value) { // watch out - 0, false are correct values too if (!isset($this->config->{$key}) or $this->config->{$key} == '') { $this->config->{$key} = $value[$this->config->user_type]; } } // Hack prefix to objectclass $this->config->objectclass = ldap_normalise_objectclass($this->config->objectclass); } /** * Constructor with initialisation. */ public function __construct() { $this->authtype = 'ldap'; $this->roleauth = 'auth_ldap'; $this->errorlogtag = '[AUTH LDAP] '; $this->init_plugin($this->authtype); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_ldap() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username (without system magic quotes) * @param string $password The password (without system magic quotes) * * @return bool Authentication success or failure. */ function user_login($username, $password) { if (! function_exists('ldap_bind')) { throw new \moodle_exception('auth_ldapnotinstalled', 'auth_ldap'); return false; } if (!$username or !$password) { // Don't allow blank usernames or passwords return false; } $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $extpassword = core_text::convert($password, 'utf-8', $this->config->ldapencoding); // Before we connect to LDAP, check if this is an AD SSO login // if we succeed in this block, we'll return success early. // $key = sesskey(); if (!empty($this->config->ntlmsso_enabled) && $key === $password) { $sessusername = get_cache_flag($this->pluginconfig.'/ntlmsess', $key); // We only get the cache flag if we retrieve it before // it expires (AUTH_NTLMTIMEOUT seconds). if (empty($sessusername)) { return false; } if ($username === $sessusername) { unset($sessusername); // Check that the user is inside one of the configured LDAP contexts $validuser = false; $ldapconnection = $this->ldap_connect(); // if the user is not inside the configured contexts, // ldap_find_userdn returns false. if ($this->ldap_find_userdn($ldapconnection, $extusername)) { $validuser = true; } $this->ldap_close(); // Shortcut here - SSO confirmed return $validuser; } } // End SSO processing unset($key); $ldapconnection = $this->ldap_connect(); $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); // If ldap_user_dn is empty, user does not exist if (!$ldap_user_dn) { $this->ldap_close(); return false; } // Try to bind with current username and password $ldap_login = @ldap_bind($ldapconnection, $ldap_user_dn, $extpassword); // If login fails and we are using MS Active Directory, retrieve the diagnostic // message to see if this is due to an expired password, or that the user is forced to // change the password on first login. If it is, only proceed if we can change // password from Moodle (otherwise we'll get stuck later in the login process). if (!$ldap_login && ($this->config->user_type == 'ad') && $this->can_change_password() && (!empty($this->config->expiration) and ($this->config->expiration == 1))) { // We need to get the diagnostic message right after the call to ldap_bind(), // before any other LDAP operation. ldap_get_option($ldapconnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagmsg); if ($this->ldap_ad_pwdexpired_from_diagmsg($diagmsg)) { // If login failed because user must change the password now or the // password has expired, let the user in. We'll catch this later in the // login process when we explicitly check for expired passwords. $ldap_login = true; } } $this->ldap_close(); return $ldap_login; } /** * Reads user information from ldap and returns it in array() * * Function should return all information available. If you are saving * this information to moodle user-table you should honor syncronization flags * * @param string $username username * * @return mixed array with no magic quotes or false on error */ function get_userinfo($username) { $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extusername))) { $this->ldap_close(); return false; } $search_attribs = array(); $attrmap = $this->ldap_attributes(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } foreach ($values as $value) { if (!in_array($value, $search_attribs)) { array_push($search_attribs, $value); } } } if (!$user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs)) { $this->ldap_close(); return false; // error! } $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result); if (empty($user_entry)) { $this->ldap_close(); return false; // entry not found } $result = array(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } $ldapval = NULL; foreach ($values as $value) { $entry = $user_entry[0]; if (($value == 'dn') || ($value == 'distinguishedname')) { $result[$key] = $user_dn; continue; } if (!array_key_exists($value, $entry)) { continue; // wrong data mapping! } if (is_array($entry[$value])) { $newval = core_text::convert($entry[$value][0], $this->config->ldapencoding, 'utf-8'); } else { $newval = core_text::convert($entry[$value], $this->config->ldapencoding, 'utf-8'); } if (!empty($newval)) { // favour ldap entries that are set $ldapval = $newval; } } if (!is_null($ldapval)) { $result[$key] = $ldapval; } } $this->ldap_close(); return $result; } /** * Reads user information from ldap and returns it in an object * * @param string $username username (with system magic quotes) * @return mixed object or false on error */ function get_userinfo_asobj($username) { $user_array = $this->get_userinfo($username); if ($user_array == false) { return false; //error or not found } $user_array = truncate_userinfo($user_array); $user = new stdClass(); foreach ($user_array as $key=>$value) { $user->{$key} = $value; } return $user; } /** * Returns all usernames from LDAP * * get_userlist returns all usernames from LDAP * * @return array */ function get_userlist() { return $this->ldap_get_userlist("({$this->config->user_attribute}=*)"); } /** * Checks if user exists on LDAP * * @param string $username */ function user_exists($username) { $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); // Returns true if given username exists on ldap $users = $this->ldap_get_userlist('('.$this->config->user_attribute.'='.ldap_filter_addslashes($extusername).')'); return count($users); } /** * Creates a new user on LDAP. * By using information in userobject * Use user_exists to prevent duplicate usernames * * @param mixed $userobject Moodle userobject * @param mixed $plainpass Plaintext password */ function user_create($userobject, $plainpass) { $extusername = core_text::convert($userobject->username, 'utf-8', $this->config->ldapencoding); $extpassword = core_text::convert($plainpass, 'utf-8', $this->config->ldapencoding); switch ($this->config->passtype) { case 'md5': $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); break; case 'sha1': $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); break; case 'plaintext': default: break; // plaintext } $ldapconnection = $this->ldap_connect(); $attrmap = $this->ldap_attributes(); $newuser = array(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } foreach ($values as $value) { if (!empty($userobject->$key) ) { $newuser[$value] = core_text::convert($userobject->$key, 'utf-8', $this->config->ldapencoding); } } } //Following sets all mandatory and other forced attribute values //User should be creted as login disabled untill email confirmation is processed //Feel free to add your user type and send patches to paca@sci.fi to add them //Moodle distribution switch ($this->config->user_type) { case 'edir': $newuser['objectClass'] = array('inetOrgPerson', 'organizationalPerson', 'person', 'top'); $newuser['uniqueId'] = $extusername; $newuser['logindisabled'] = 'TRUE'; $newuser['userpassword'] = $extpassword; $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser); break; case 'rfc2307': case 'rfc2307bis': // posixAccount object class forces us to specify a uidNumber // and a gidNumber. That is quite complicated to generate from // Moodle without colliding with existing numbers and without // race conditions. As this user is supposed to be only used // with Moodle (otherwise the user would exist beforehand) and // doesn't need to login into a operating system, we assign the // user the uid of user 'nobody' and gid of group 'nogroup'. In // addition to that, we need to specify a home directory. We // use the root directory ('/') as the home directory, as this // is the only one can always be sure exists. Finally, even if // it's not mandatory, we specify '/bin/false' as the login // shell, to prevent the user from login in at the operating // system level (Moodle ignores this). $newuser['objectClass'] = array('posixAccount', 'inetOrgPerson', 'organizationalPerson', 'person', 'top'); $newuser['cn'] = $extusername; $newuser['uid'] = $extusername; $newuser['uidNumber'] = AUTH_UID_NOBODY; $newuser['gidNumber'] = AUTH_GID_NOGROUP; $newuser['homeDirectory'] = '/'; $newuser['loginShell'] = '/bin/false'; // IMPORTANT: // We have to create the account locked, but posixAccount has // no attribute to achive this reliably. So we are going to // modify the password in a reversable way that we can later // revert in user_activate(). // // Beware that this can be defeated by the user if we are not // using MD5 or SHA-1 passwords. After all, the source code of // Moodle is available, and the user can see the kind of // modification we are doing and 'undo' it by hand (but only // if we are using plain text passwords). // // Also bear in mind that you need to use a binding user that // can create accounts and has read/write privileges on the // 'userPassword' attribute for this to work. $newuser['userPassword'] = '*'.$extpassword; $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser); break; case 'ad': // User account creation is a two step process with AD. First you // create the user object, then you set the password. If you try // to set the password while creating the user, the operation // fails. // Passwords in Active Directory must be encoded as Unicode // strings (UCS-2 Little Endian format) and surrounded with // double quotes. See http://support.microsoft.com/?kbid=269190 if (!function_exists('mb_convert_encoding')) { throw new \moodle_exception('auth_ldap_no_mbstring', 'auth_ldap'); } // Check for invalid sAMAccountName characters. if (preg_match('#[/\\[\]:;|=,+*?<>@"]#', $extusername)) { throw new \moodle_exception ('auth_ldap_ad_invalidchars', 'auth_ldap'); } // First create the user account, and mark it as disabled. $newuser['objectClass'] = array('top', 'person', 'user', 'organizationalPerson'); $newuser['sAMAccountName'] = $extusername; $newuser['userAccountControl'] = AUTH_AD_NORMAL_ACCOUNT | AUTH_AD_ACCOUNTDISABLE; $userdn = 'cn='.ldap_addslashes($extusername).','.$this->config->create_context; if (!ldap_add($ldapconnection, $userdn, $newuser)) { throw new \moodle_exception('auth_ldap_ad_create_req', 'auth_ldap'); } // Now set the password unset($newuser); $newuser['unicodePwd'] = mb_convert_encoding('"' . $extpassword . '"', 'UCS-2LE', 'UTF-8'); if(!ldap_modify($ldapconnection, $userdn, $newuser)) { // Something went wrong: delete the user account and error out ldap_delete ($ldapconnection, $userdn); throw new \moodle_exception('auth_ldap_ad_create_req', 'auth_ldap'); } $uadd = true; break; default: throw new \moodle_exception('auth_ldap_unsupportedusertype', 'auth_ldap', '', $this->config->user_type_name); } $this->ldap_close(); return $uadd; } /** * Returns true if plugin allows resetting of password from moodle. * * @return bool */ function can_reset_password() { return !empty($this->config->stdchangepassword); } /** * Returns true if plugin can be manually set. * * @return bool */ function can_be_manually_set() { return true; } /** * Returns true if plugin allows signup and user creation. * * @return bool */ function can_signup() { return (!empty($this->config->auth_user_create) and !empty($this->config->create_context)); } /** * Sign up a new user ready for confirmation. * Password is passed in plaintext. * * @param object $user new user object * @param boolean $notify print notice with link and terminate * @return boolean success */ function user_signup($user, $notify=true) { global $CFG, $DB, $PAGE, $OUTPUT; require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); if ($this->user_exists($user->username)) { throw new \moodle_exception('auth_ldap_user_exists', 'auth_ldap'); } $plainslashedpassword = $user->password; unset($user->password); if (! $this->user_create($user, $plainslashedpassword)) { throw new \moodle_exception('auth_ldap_create_error', 'auth_ldap'); } $user->id = user_create_user($user, false, false); user_add_password_history($user->id, $plainslashedpassword); // Save any custom profile field information profile_save_data($user); $userinfo = $this->get_userinfo($user->username); $this->update_user_record($user->username, false, false, $this->is_user_suspended((object) $userinfo)); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). update_internal_user_password($user, $plainslashedpassword); $user = $DB->get_record('user', array('id'=>$user->id)); \core\event\user_created::create_from_userid($user->id)->trigger(); if (! send_confirmation_email($user)) { throw new \moodle_exception('noemail', 'auth_ldap'); } if ($notify) { $emailconfirm = get_string('emailconfirm'); $PAGE->set_url('/auth/ldap/auth.php'); $PAGE->navbar->add($emailconfirm); $PAGE->set_title($emailconfirm); $PAGE->set_heading($emailconfirm); echo $OUTPUT->header(); notice(get_string('emailconfirmsent', '', $user->email), "{$CFG->wwwroot}/index.php"); } else { return true; } } /** * Returns true if plugin allows confirming of new users. * * @return bool */ function can_confirm() { return $this->can_signup(); } /** * Confirm the new user as registered. * * @param string $username * @param string $confirmsecret */ function user_confirm($username, $confirmsecret) { global $DB; $user = get_complete_user_data('username', $username); if (!empty($user)) { if ($user->auth != $this->authtype) { return AUTH_CONFIRM_ERROR; } else if ($user->secret === $confirmsecret && $user->confirmed) { return AUTH_CONFIRM_ALREADY; } else if ($user->secret === $confirmsecret) { // They have provided the secret key to get in if (!$this->user_activate($username)) { return AUTH_CONFIRM_FAIL; } $user->confirmed = 1; user_update_user($user, false); return AUTH_CONFIRM_OK; } } else { return AUTH_CONFIRM_ERROR; } } /** * Return number of days to user password expires * * If userpassword does not expire it should return 0. If password is already expired * it should return negative value. * * @param mixed $username username * @return integer */ function password_expire($username) { $result = 0; $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); $search_attribs = array($this->config->expireattr); $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); if ($sr) { $info = ldap_get_entries_moodle($ldapconnection, $sr); if (!empty ($info)) { $info = $info[0]; if (isset($info[$this->config->expireattr][0])) { $expiretime = $this->ldap_expirationtime2unix($info[$this->config->expireattr][0], $ldapconnection, $user_dn); if ($expiretime != 0) { $now = time(); if ($expiretime > $now) { $result = ceil(($expiretime - $now) / DAYSECS); } else { $result = floor(($expiretime - $now) / DAYSECS); } } } } } else { error_log($this->errorlogtag.get_string('didtfindexpiretime', 'auth_ldap')); } return $result; } /** * Synchronise users from the external LDAP server to Moodle's user table. * * Calls sync_users_update_callback() with default callback if appropriate. * * @param bool $doupdates will do pull in data updates from LDAP if relevant * @return bool success */ public function sync_users($doupdates = true) { return $this->sync_users_update_callback($doupdates ? [$this, 'update_users'] : null); } /** * Synchronise users from the external LDAP server to Moodle's user table (callback). * * Sync is now using username attribute. * * Syncing users removes or suspends users that dont exists anymore in external LDAP. * Creates new users and updates coursecreator status of users. * * @param callable|null $updatecallback will do pull in data updates from LDAP if relevant * @return bool success */ public function sync_users_update_callback(?callable $updatecallback = null): bool { global $CFG, $DB; require_once($CFG->dirroot . '/user/profile/lib.php'); print_string('connectingldap', 'auth_ldap'); $ldapconnection = $this->ldap_connect(); $dbman = $DB->get_manager(); /// Define table user to be created $table = new xmldb_table('tmp_extuser'); $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); $table->add_field('username', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); $table->add_field('mnethostid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); $table->add_index('username', XMLDB_INDEX_UNIQUE, array('mnethostid', 'username')); print_string('creatingtemptable', 'auth_ldap', 'tmp_extuser'); $dbman->create_temp_table($table); //// //// get user's list from ldap to sql in a scalable fashion //// // prepare some data we'll need $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; $servercontrols = array(); $contexts = explode(';', $this->config->contexts); if (!empty($this->config->create_context)) { array_push($contexts, $this->config->create_context); } $ldappagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection); $ldapcookie = ''; foreach ($contexts as $context) { $context = trim($context); if (empty($context)) { continue; } do { if ($ldappagedresults) { $servercontrols = array(array( 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array( 'size' => $this->config->pagesize, 'cookie' => $ldapcookie))); } if ($this->config->search_sub) { // Use ldap_search to find first user from subtree. $ldapresult = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } else { // Search only in this context. $ldapresult = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } if (!$ldapresult) { continue; } if ($ldappagedresults) { // Get next server cookie to know if we'll need to continue searching. $ldapcookie = ''; // Get next cookie from controls. ldap_parse_result($ldapconnection, $ldapresult, $errcode, $matcheddn, $errmsg, $referrals, $controls); if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { $ldapcookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; } } if ($entry = @ldap_first_entry($ldapconnection, $ldapresult)) { do { $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute); $value = core_text::convert($value[0], $this->config->ldapencoding, 'utf-8'); $value = trim($value); $this->ldap_bulk_insert($value); } while ($entry = ldap_next_entry($ldapconnection, $entry)); } unset($ldapresult); // Free mem. } while ($ldappagedresults && $ldapcookie !== null && $ldapcookie != ''); } // If LDAP paged results were used, the current connection must be completely // closed and a new one created, to work without paged results from here on. if ($ldappagedresults) { $this->ldap_close(true); $ldapconnection = $this->ldap_connect(); } /// preserve our user database /// if the temp table is empty, it probably means that something went wrong, exit /// so as to avoid mass deletion of users; which is hard to undo $count = $DB->count_records_sql('SELECT COUNT(username) AS count, 1 FROM {tmp_extuser}'); if ($count < 1) { print_string('didntgetusersfromldap', 'auth_ldap'); $dbman->drop_table($table); $this->ldap_close(); return false; } else { print_string('gotcountrecordsfromldap', 'auth_ldap', $count); } /// User removal // Find users in DB that aren't in ldap -- to be removed! // this is still not as scalable (but how often do we mass delete?) if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) { $sql = "SELECT u.* FROM {user} u LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) WHERE u.auth = :auth AND u.deleted = 0 AND e.username IS NULL"; $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); if (!empty($remove_users)) { print_string('userentriestoremove', 'auth_ldap', count($remove_users)); foreach ($remove_users as $user) { if (delete_user($user)) { echo "\t"; print_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; } else { echo "\t"; print_string('auth_dbdeleteusererror', 'auth_db', $user->username); echo "\n"; } } } else { print_string('nouserentriestoremove', 'auth_ldap'); } unset($remove_users); // Free mem! } else if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $sql = "SELECT u.* FROM {user} u LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) WHERE u.auth = :auth AND u.deleted = 0 AND u.suspended = 0 AND e.username IS NULL"; $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); if (!empty($remove_users)) { print_string('userentriestoremove', 'auth_ldap', count($remove_users)); foreach ($remove_users as $user) { $updateuser = new stdClass(); $updateuser->id = $user->id; $updateuser->suspended = 1; user_update_user($updateuser, false); echo "\t"; print_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; \core\session\manager::destroy_user_sessions($user->id); } } else { print_string('nouserentriestoremove', 'auth_ldap'); } unset($remove_users); // Free mem! } /// Revive suspended users if (!empty($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $sql = "SELECT u.id, u.username FROM {user} u JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) WHERE (u.auth = 'nologin' OR (u.auth = ? AND u.suspended = 1)) AND u.deleted = 0"; // Note: 'nologin' is there for backwards compatibility. $revive_users = $DB->get_records_sql($sql, array($this->authtype)); if (!empty($revive_users)) { print_string('userentriestorevive', 'auth_ldap', count($revive_users)); foreach ($revive_users as $user) { $updateuser = new stdClass(); $updateuser->id = $user->id; $updateuser->auth = $this->authtype; $updateuser->suspended = 0; user_update_user($updateuser, false); echo "\t"; print_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; } } else { print_string('nouserentriestorevive', 'auth_ldap'); } unset($revive_users); } /// User Updates - time-consuming (optional) if ($updatecallback && $updatekeys = $this->get_profile_keys()) { // Run updates only if relevant. $users = $DB->get_records_sql('SELECT u.username, u.id FROM {user} u WHERE u.deleted = 0 AND u.auth = ? AND u.mnethostid = ?', array($this->authtype, $CFG->mnet_localhost_id)); if (!empty($users)) { // Update users in chunks as specified in sync_updateuserchunk. if (!empty($this->config->sync_updateuserchunk)) { foreach (array_chunk($users, $this->config->sync_updateuserchunk) as $chunk) { call_user_func($updatecallback, $chunk, $updatekeys); } } else { call_user_func($updatecallback, $users, $updatekeys); } unset($users); // Free mem. } } else { print_string('noupdatestobedone', 'auth_ldap'); } /// User Additions // Find users missing in DB that are in LDAP // and gives me a nifty object I don't want. // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin $sql = 'SELECT e.id, e.username FROM {tmp_extuser} e LEFT JOIN {user} u ON (e.username = u.username AND e.mnethostid = u.mnethostid) WHERE u.id IS NULL'; $add_users = $DB->get_records_sql($sql); if (!empty($add_users)) { print_string('userentriestoadd', 'auth_ldap', count($add_users)); $errors = 0; foreach ($add_users as $user) { $transaction = $DB->start_delegated_transaction(); $user = $this->get_userinfo_asobj($user->username); // Prep a few params $user->modified = time(); $user->confirmed = 1; $user->auth = $this->authtype; $user->mnethostid = $CFG->mnet_localhost_id; // get_userinfo_asobj() might have replaced $user->username with the value // from the LDAP server (which can be mixed-case). Make sure it's lowercase $user->username = trim(core_text::strtolower($user->username)); // It isn't possible to just rely on the configured suspension attribute since // things like active directory use bit masks, other things using LDAP might // do different stuff as well. // // The cast to int is a workaround for MDL-53959. $user->suspended = (int)$this->is_user_suspended($user); if (empty($user->calendartype)) { $user->calendartype = $CFG->calendartype; } // $id = user_create_user($user, false); try { $id = user_create_user($user, false); } catch (Exception $e) { print_string('invaliduserexception', 'auth_ldap', print_r($user, true) . $e->getMessage()); $errors++; $transaction->allow_commit(); continue; } echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n"; $euser = $DB->get_record('user', array('id' => $id)); if (!empty($this->config->forcechangepassword)) { set_user_preference('auth_forcepasswordchange', 1, $id); } // Save custom profile fields. $this->update_user_record($user->username, $this->get_profile_keys(true), false); // Add roles if needed. $this->sync_roles($euser); $transaction->allow_commit(); } // Display number of user creation errors, if any. if ($errors) { print_string('invalidusererrors', 'auth_ldap', $errors); } unset($add_users); // free mem } else { print_string('nouserstobeadded', 'auth_ldap'); } $dbman->drop_table($table); $this->ldap_close(); return true; } /** * Update users from the external LDAP server into Moodle's user table. * * Sync helper * * @param array $users chunk of users to update * @param array $updatekeys fields to update */ public function update_users(array $users, array $updatekeys): void { global $DB; print_string('userentriestoupdate', 'auth_ldap', count($users)); foreach ($users as $user) { $transaction = $DB->start_delegated_transaction(); echo "\t"; print_string('auth_dbupdatinguser', 'auth_db', ['name' => $user->username, 'id' => $user->id]); $userinfo = $this->get_userinfo($user->username); if (!$this->update_user_record($user->username, $updatekeys, true, $this->is_user_suspended((object) $userinfo))) { echo ' - '.get_string('skipped'); } echo "\n"; // Update system roles, if needed. $this->sync_roles($user); $transaction->allow_commit(); } } /** * Bulk insert in SQL's temp table */ function ldap_bulk_insert($username) { global $DB, $CFG; $username = core_text::strtolower($username); // usernames are __always__ lowercase. $DB->insert_record_raw('tmp_extuser', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id), false, true); echo '.'; } /** * Activates (enables) user in external LDAP so user can login * * @param mixed $username * @return boolean result */ function user_activate($username) { $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); $userdn = $this->ldap_find_userdn($ldapconnection, $extusername); switch ($this->config->user_type) { case 'edir': $newinfo['loginDisabled'] = 'FALSE'; break; case 'rfc2307': case 'rfc2307bis': // Remember that we add a '*' character in front of the // external password string to 'disable' the account. We just // need to remove it. $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', array('userPassword')); $info = ldap_get_entries($ldapconnection, $sr); $info[0] = array_change_key_case($info[0], CASE_LOWER); $newinfo['userPassword'] = ltrim($info[0]['userpassword'][0], '*'); break; case 'ad': // We need to unset the ACCOUNTDISABLE bit in the // userAccountControl attribute ( see // http://support.microsoft.com/kb/305144 ) $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', array('userAccountControl')); $info = ldap_get_entries($ldapconnection, $sr); $info[0] = array_change_key_case($info[0], CASE_LOWER); $newinfo['userAccountControl'] = $info[0]['useraccountcontrol'][0] & (~AUTH_AD_ACCOUNTDISABLE); break; default: throw new \moodle_exception('user_activatenotsupportusertype', 'auth_ldap', '', $this->config->user_type_name); } $result = ldap_modify($ldapconnection, $userdn, $newinfo); $this->ldap_close(); return $result; } /** * Returns true if user should be coursecreator. * * @param mixed $username username (without system magic quotes) * @return mixed result null if course creators is not configured, boolean otherwise. * * @deprecated since Moodle 3.4 MDL-30634 - please do not use this function any more. */ function iscreator($username) { debugging('iscreator() is deprecated. Please use auth_plugin_ldap::is_role() instead.', DEBUG_DEVELOPER); if (empty($this->config->creators) or empty($this->config->memberattribute)) { return null; } $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); if ($this->config->memberattribute_isdn) { if(!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) { return false; } } else { $userid = $extusername; } $group_dns = explode(';', $this->config->creators); $creator = ldap_isgroupmember($ldapconnection, $userid, $group_dns, $this->config->memberattribute); $this->ldap_close(); return $creator; } /** * Check if user has LDAP group membership. * * Returns true if user should be assigned role. * * @param mixed $username username (without system magic quotes). * @param array $role Array of role's shortname, localname, and settingname for the config value. * @return mixed result null if role/LDAP context is not configured, boolean otherwise. */ private function is_role($username, $role) { if (empty($this->config->{$role['settingname']}) or empty($this->config->memberattribute)) { return null; } $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); if ($this->config->memberattribute_isdn) { if (!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) { return false; } } else { $userid = $extusername; } $groupdns = explode(';', $this->config->{$role['settingname']}); $isrole = ldap_isgroupmember($ldapconnection, $userid, $groupdns, $this->config->memberattribute); $this->ldap_close(); return $isrole; } /** * Called when the user record is updated. * * Modifies user in external LDAP server. It takes olduser (before * changes) and newuser (after changes) compares information and * saves modified information to external LDAP server. * * @param mixed $olduser Userobject before modifications (without system magic quotes) * @param mixed $newuser Userobject new modified userobject (without system magic quotes) * @return boolean result * */ function user_update($olduser, $newuser) { global $CFG; require_once($CFG->dirroot . '/user/profile/lib.php'); if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) { error_log($this->errorlogtag.get_string('renamingnotallowed', 'auth_ldap')); return false; } if (isset($olduser->auth) and $olduser->auth != $this->authtype) { return true; // just change auth and skip update } $attrmap = $this->ldap_attributes(); // Before doing anything else, make sure we really need to update anything // in the external LDAP server. $update_external = false; foreach ($attrmap as $key => $ldapkeys) { if (!empty($this->config->{'field_updateremote_'.$key})) { $update_external = true; break; } } if (!$update_external) { return true; } $extoldusername = core_text::convert($olduser->username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); $search_attribs = array(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } foreach ($values as $value) { if (!in_array($value, $search_attribs)) { array_push($search_attribs, $value); } } } if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extoldusername))) { return false; } // Load old custom fields. $olduserprofilefields = (array) profile_user_record($olduser->id, false); $fields = array(); foreach (profile_get_custom_fields(false) as $field) { $fields[$field->shortname] = $field; } $success = true; $user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); if ($user_info_result) { $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result); if (empty($user_entry)) { $attribs = join (', ', $search_attribs); error_log($this->errorlogtag.get_string('updateusernotfound', 'auth_ldap', array('userdn'=>$user_dn, 'attribs'=>$attribs))); return false; // old user not found! } else if (count($user_entry) > 1) { error_log($this->errorlogtag.get_string('morethanoneuser', 'auth_ldap')); return false; } $user_entry = $user_entry[0]; foreach ($attrmap as $key => $ldapkeys) { if (preg_match('/^profile_field_(.*)$/', $key, $match)) { // Custom field. $fieldname = $match[1]; if (isset($fields[$fieldname])) { $class = 'profile_field_' . $fields[$fieldname]->datatype; $formfield = new $class($fields[$fieldname]->id, $olduser->id); $oldvalue = isset($olduserprofilefields[$fieldname]) ? $olduserprofilefields[$fieldname] : null; } else { $oldvalue = null; } $newvalue = $formfield->edit_save_data_preprocess($newuser->{$formfield->inputname}, new stdClass); } else { // Standard field. $oldvalue = isset($olduser->$key) ? $olduser->$key : null; $newvalue = isset($newuser->$key) ? $newuser->$key : null; } if ($newvalue !== null and $newvalue !== $oldvalue and !empty($this->config->{'field_updateremote_' . $key})) { // For ldap values that could be in more than one // ldap key, we will do our best to match // where they came from $ambiguous = true; $changed = false; if (!is_array($ldapkeys)) { $ldapkeys = array($ldapkeys); } if (count($ldapkeys) < 2) { $ambiguous = false; } $nuvalue = core_text::convert($newvalue, 'utf-8', $this->config->ldapencoding); empty($nuvalue) ? $nuvalue = array() : $nuvalue; $ouvalue = core_text::convert($oldvalue, 'utf-8', $this->config->ldapencoding); foreach ($ldapkeys as $ldapkey) { // If the field is empty in LDAP there are two options: // 1. We get the LDAP field using ldap_first_attribute. // 2. LDAP don't send the field using ldap_first_attribute. // So, for option 1 we check the if the field is retrieve it. // And get the original value of field in LDAP if the field. // Otherwise, let value in blank and delegate the check in ldap_modify. if (isset($user_entry[$ldapkey][0])) { $ldapvalue = $user_entry[$ldapkey][0]; } else { $ldapvalue = ''; } if (!$ambiguous) { // Skip update if the values already match if ($nuvalue !== $ldapvalue) { // This might fail due to schema validation if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { $changed = true; continue; } else { $success = false; error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 'key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); continue; } } } else { // Ambiguous. Value empty before in Moodle (and LDAP) - use // 1st ldap candidate field, no need to guess if ($ouvalue === '') { // value empty before - use 1st ldap candidate // This might fail due to schema validation if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { $changed = true; continue; } else { $success = false; error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 'key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); continue; } } // We found which ldap key to update! if ($ouvalue !== '' and $ouvalue === $ldapvalue ) { // This might fail due to schema validation if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { $changed = true; continue; } else { $success = false; error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 'key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); continue; } } } } if ($ambiguous and !$changed) { $success = false; error_log($this->errorlogtag.get_string ('updateremfailamb', 'auth_ldap', array('key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); } } } } else { error_log($this->errorlogtag.get_string ('usernotfound', 'auth_ldap')); $success = false; } $this->ldap_close(); return $success; } /** * Changes userpassword in LDAP * * Called when the user password is updated. It assumes it is * called by an admin or that you've otherwise checked the user's * credentials * * @param object $user User table object * @param string $newpassword Plaintext password (not crypted/md5'ed) * @return boolean result * */ function user_update_password($user, $newpassword) { global $USER; $result = false; $username = $user->username; $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $extpassword = core_text::convert($newpassword, 'utf-8', $this->config->ldapencoding); switch ($this->config->passtype) { case 'md5': $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); break; case 'sha1': $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); break; case 'plaintext': default: break; // plaintext } $ldapconnection = $this->ldap_connect(); $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); if (!$user_dn) { error_log($this->errorlogtag.get_string ('nodnforusername', 'auth_ldap', $user->username)); return false; } switch ($this->config->user_type) { case 'edir': // Change password $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } // Update password expiration time, grace logins count $search_attribs = array($this->config->expireattr, 'passwordExpirationInterval', 'loginGraceLimit'); $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); if ($sr) { $entry = ldap_get_entries_moodle($ldapconnection, $sr); $info = $entry[0]; $newattrs = array(); if (!empty($info[$this->config->expireattr][0])) { // Set expiration time only if passwordExpirationInterval is defined if (!empty($info['passwordexpirationinterval'][0])) { $expirationtime = time() + $info['passwordexpirationinterval'][0]; $ldapexpirationtime = $this->ldap_unix2expirationtime($expirationtime); $newattrs['passwordExpirationTime'] = $ldapexpirationtime; } // Set gracelogin count if (!empty($info['logingracelimit'][0])) { $newattrs['loginGraceRemaining']= $info['logingracelimit'][0]; } // Store attribute changes in LDAP $result = ldap_modify($ldapconnection, $user_dn, $newattrs); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserrorexpiregrace', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } } } else { error_log($this->errorlogtag.get_string ('updatepasserrorexpire', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } break; case 'ad': // Passwords in Active Directory must be encoded as Unicode // strings (UCS-2 Little Endian format) and surrounded with // double quotes. See http://support.microsoft.com/?kbid=269190 if (!function_exists('mb_convert_encoding')) { error_log($this->errorlogtag.get_string ('needmbstring', 'auth_ldap')); return false; } $extpassword = mb_convert_encoding('"'.$extpassword.'"', "UCS-2LE", $this->config->ldapencoding); $result = ldap_modify($ldapconnection, $user_dn, array('unicodePwd' => $extpassword)); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } break; default: // Send LDAP the password in cleartext, it will md5 it itself $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } } $this->ldap_close(); return $result; } /** * Take expirationtime and return it as unix timestamp in seconds * * Takes expiration timestamp as read from LDAP and returns it as unix timestamp in seconds * Depends on $this->config->user_type variable * * @param mixed time Time stamp read from LDAP as it is. * @param string $ldapconnection Only needed for Active Directory. * @param string $user_dn User distinguished name for the user we are checking password expiration (only needed for Active Directory). * @return timestamp */ function ldap_expirationtime2unix($time, $ldapconnection, $user_dn) { $result = false; switch ($this->config->user_type) { case 'edir': $yr=substr($time, 0, 4); $mo=substr($time, 4, 2); $dt=substr($time, 6, 2); $hr=substr($time, 8, 2); $min=substr($time, 10, 2); $sec=substr($time, 12, 2); $result = mktime($hr, $min, $sec, $mo, $dt, $yr); break; case 'rfc2307': case 'rfc2307bis': $result = $time * DAYSECS; // The shadowExpire contains the number of DAYS between 01/01/1970 and the actual expiration date break; case 'ad': $result = $this->ldap_get_ad_pwdexpire($time, $ldapconnection, $user_dn); break; default: throw new \moodle_exception('auth_ldap_usertypeundefined', 'auth_ldap'); } return $result; } /** * Takes unix timestamp and returns it formated for storing in LDAP * * @param integer unix time stamp */ function ldap_unix2expirationtime($time) { $result = false; switch ($this->config->user_type) { case 'edir': $result=date('YmdHis', $time).'Z'; break; case 'rfc2307': case 'rfc2307bis': $result = $time ; // Already in correct format break; default: throw new \moodle_exception('auth_ldap_usertypeundefined2', 'auth_ldap'); } return $result; } /** * Returns user attribute mappings between moodle and LDAP * * @return array */ function ldap_attributes() { $moodleattributes = array(); // If we have custom fields then merge them with user fields. $customfields = $this->get_custom_user_profile_fields(); if (!empty($customfields) && !empty($this->userfields)) { $userfields = array_merge($this->userfields, $customfields); } else { $userfields = $this->userfields; } foreach ($userfields as $field) { if (!empty($this->config->{"field_map_$field"})) { $moodleattributes[$field] = core_text::strtolower(trim($this->config->{"field_map_$field"})); if (preg_match('/,/', $moodleattributes[$field])) { $moodleattributes[$field] = explode(',', $moodleattributes[$field]); // split ? } } } $moodleattributes['username'] = core_text::strtolower(trim($this->config->user_attribute)); $moodleattributes['suspended'] = core_text::strtolower(trim($this->config->suspended_attribute)); return $moodleattributes; } /** * Returns all usernames from LDAP * * @param $filter An LDAP search filter to select desired users * @return array of LDAP user names converted to UTF-8 */ function ldap_get_userlist($filter='*') { $fresult = array(); $ldapconnection = $this->ldap_connect(); if ($filter == '*') { $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; } $servercontrols = array(); $contexts = explode(';', $this->config->contexts); if (!empty($this->config->create_context)) { array_push($contexts, $this->config->create_context); } $ldap_cookie = ''; $ldap_pagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection); foreach ($contexts as $context) { $context = trim($context); if (empty($context)) { continue; } do { if ($ldap_pagedresults) { $servercontrols = array(array( 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array( 'size' => $this->config->pagesize, 'cookie' => $ldap_cookie))); } if ($this->config->search_sub) { // Use ldap_search to find first user from subtree. $ldap_result = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } else { // Search only in this context. $ldap_result = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } if(!$ldap_result) { continue; } if ($ldap_pagedresults) { // Get next server cookie to know if we'll need to continue searching. $ldap_cookie = ''; // Get next cookie from controls. ldap_parse_result($ldapconnection, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $controls); if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; } } $users = ldap_get_entries_moodle($ldapconnection, $ldap_result); // Add found users to list. for ($i = 0; $i < count($users); $i++) { $extuser = core_text::convert($users[$i][$this->config->user_attribute][0], $this->config->ldapencoding, 'utf-8'); array_push($fresult, $extuser); } unset($ldap_result); // Free mem. } while ($ldap_pagedresults && !empty($ldap_cookie)); } // If paged results were used, make sure the current connection is completely closed $this->ldap_close($ldap_pagedresults); return $fresult; } /** * Indicates if password hashes should be stored in local moodle database. * * @return bool true means flag 'not_cached' stored instead of password hash */ function prevent_local_passwords() { return !empty($this->config->preventpassindb); } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return false; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return !empty($this->config->stdchangepassword) or !empty($this->config->changepasswordurl); } /** * Returns the URL for changing the user's password, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { if (empty($this->config->stdchangepassword)) { if (!empty($this->config->changepasswordurl)) { return new moodle_url($this->config->changepasswordurl); } else { return null; } } else { return null; } } /** * Will get called before the login page is shownr. Ff NTLM SSO * is enabled, and the user is in the right network, we'll redirect * to the magic NTLM page for SSO... * */ function loginpage_hook() { global $CFG, $SESSION; // HTTPS is potentially required //httpsrequired(); - this must be used before setting the URL, it is already done on the login/index.php if (($_SERVER['REQUEST_METHOD'] === 'GET' // Only on initial GET of loginpage || ($_SERVER['REQUEST_METHOD'] === 'POST' && (get_local_referer() != strip_querystring(qualified_me())))) // Or when POSTed from another place // See MDL-14071 && !empty($this->config->ntlmsso_enabled) // SSO enabled && !empty($this->config->ntlmsso_subnet) // have a subnet to test for && empty($_GET['authldap_skipntlmsso']) // haven't failed it yet && (isguestuser() || !isloggedin()) // guestuser or not-logged-in users && address_in_subnet(getremoteaddr(), $this->config->ntlmsso_subnet)) { // First, let's remember where we were trying to get to before we got here if (empty($SESSION->wantsurl)) { $SESSION->wantsurl = null; $referer = get_local_referer(false); if ($referer && $referer != $CFG->wwwroot && $referer != $CFG->wwwroot . '/' && $referer != $CFG->wwwroot . '/login/' && $referer != $CFG->wwwroot . '/login/index.php') { $SESSION->wantsurl = $referer; } } // Now start the whole NTLM machinery. if($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESATTEMPT || $this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) { if (core_useragent::is_ie()) { $sesskey = sesskey(); redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_magic.php?sesskey='.$sesskey); } else if ($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) { redirect($CFG->wwwroot.'/login/index.php?authldap_skipntlmsso=1'); } } redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_attempt.php'); } // No NTLM SSO, Use the normal login page instead. // If $SESSION->wantsurl is empty and we have a 'Referer:' header, the login // page insists on redirecting us to that page after user validation. If // we clicked on the redirect link at the ntlmsso_finish.php page (instead // of waiting for the redirection to happen) then we have a 'Referer:' header // we don't want to use at all. As we can't get rid of it, just point // $SESSION->wantsurl to $CFG->wwwroot (after all, we came from there). if (empty($SESSION->wantsurl) && (get_local_referer() == $CFG->wwwroot.'/auth/ldap/ntlmsso_finish.php')) { $SESSION->wantsurl = $CFG->wwwroot; } } /** * To be called from a page running under NTLM's * "Integrated Windows Authentication". * * If successful, it will set a special "cookie" (not an HTTP cookie!) * in cache_flags under the $this->pluginconfig/ntlmsess "plugin" and return true. * The "cookie" will be picked up by ntlmsso_finish() to complete the * process. * * On failure it will return false for the caller to display an appropriate * error message (probably saying that Integrated Windows Auth isn't enabled!) * * NOTE that this code will execute under the OS user credentials, * so we MUST avoid dealing with files -- such as session files. * (The caller should define('NO_MOODLE_COOKIES', true) before including config.php) * */ function ntlmsso_magic($sesskey) { if (isset($_SERVER['REMOTE_USER']) && !empty($_SERVER['REMOTE_USER'])) { // HTTP __headers__ seem to be sent in ISO-8859-1 encoding // (according to my reading of RFC-1945, RFC-2616 and RFC-2617 and // my local tests), so we need to convert the REMOTE_USER value // (i.e., what we got from the HTTP WWW-Authenticate header) into UTF-8 $username = core_text::convert($_SERVER['REMOTE_USER'], 'iso-8859-1', 'utf-8'); switch ($this->config->ntlmsso_type) { case 'ntlm': // The format is now configurable, so try to extract the username $username = $this->get_ntlm_remote_user($username); if (empty($username)) { return false; } break; case 'kerberos': // Format is username@DOMAIN $username = substr($username, 0, strpos($username, '@')); break; default: error_log($this->errorlogtag.get_string ('ntlmsso_unknowntype', 'auth_ldap')); return false; // Should never happen! } $username = core_text::strtolower($username); // Compatibility hack set_cache_flag($this->pluginconfig.'/ntlmsess', $sesskey, $username, AUTH_NTLMTIMEOUT); return true; } return false; } /** * Find the session set by ntlmsso_magic(), validate it and * call authenticate_user_login() to authenticate the user through * the auth machinery. * * It is complemented by a similar check in user_login(). * * If it succeeds, it never returns. * */ function ntlmsso_finish() { global $CFG, $USER, $SESSION; $key = sesskey(); $username = get_cache_flag($this->pluginconfig.'/ntlmsess', $key); if (empty($username)) { return false; } // Here we want to trigger the whole authentication machinery // to make sure no step is bypassed... $reason = null; $user = authenticate_user_login($username, $key, false, $reason, false); if ($user) { complete_user_login($user); // Cleanup the key to prevent reuse... // and to allow re-logins with normal credentials unset_cache_flag($this->pluginconfig.'/ntlmsess', $key); // Redirection if (user_not_fully_set_up($USER, true)) { $urltogo = $CFG->wwwroot.'/user/edit.php'; // We don't delete $SESSION->wantsurl yet, so we get there later } else if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) { $urltogo = $SESSION->wantsurl; // Because it's an address in this site unset($SESSION->wantsurl); } else { // No wantsurl stored or external - go to homepage $urltogo = $CFG->wwwroot.'/'; unset($SESSION->wantsurl); } // We do not want to redirect if we are in a PHPUnit test. if (!PHPUNIT_TEST) { redirect($urltogo); } } // Should never reach here. return false; } /** * Sync roles for this user. * * @param object $user The user to sync (without system magic quotes). */ function sync_roles($user) { global $DB; $roles = get_ldap_assignable_role_names(2); // Admin user. foreach ($roles as $role) { $isrole = $this->is_role($user->username, $role); if ($isrole === null) { continue; // Nothing to sync - role/LDAP contexts not configured. } // Sync user. $systemcontext = context_system::instance(); if ($isrole) { // Following calls will not create duplicates. role_assign($role['id'], $user->id, $systemcontext->id, $this->roleauth); } else { // Unassign only if previously assigned by this plugin. role_unassign($role['id'], $user->id, $systemcontext->id, $this->roleauth); } } } /** * Get password expiration time for a given user from Active Directory * * @param string $pwdlastset The time last time we changed the password. * @param resource $lcapconn The open LDAP connection. * @param string $user_dn The distinguished name of the user we are checking. * * @return string $unixtime */ function ldap_get_ad_pwdexpire($pwdlastset, $ldapconn, $user_dn){ global $CFG; if (!function_exists('bcsub')) { error_log($this->errorlogtag.get_string ('needbcmath', 'auth_ldap')); return 0; } // If UF_DONT_EXPIRE_PASSWD flag is set in user's // userAccountControl attribute, the password doesn't expire. $sr = ldap_read($ldapconn, $user_dn, '(objectClass=*)', array('userAccountControl')); if (!$sr) { error_log($this->errorlogtag.get_string ('useracctctrlerror', 'auth_ldap', $user_dn)); // Don't expire password, as we are not sure if it has to be // expired or not. return 0; } $entry = ldap_get_entries_moodle($ldapconn, $sr); $info = $entry[0]; $useraccountcontrol = $info['useraccountcontrol'][0]; if ($useraccountcontrol & UF_DONT_EXPIRE_PASSWD) { // Password doesn't expire. return 0; } // If pwdLastSet is zero, the user must change his/her password now // (unless UF_DONT_EXPIRE_PASSWD flag is set, but we already // tested this above) if ($pwdlastset === '0') { // Password has expired return -1; } // ---------------------------------------------------------------- // Password expiration time in Active Directory is the composition of // two values: // // - User's pwdLastSet attribute, that stores the last time // the password was changed. // // - Domain's maxPwdAge attribute, that sets how long // passwords last in this domain. // // We already have the first value (passed in as a parameter). We // need to get the second one. As we don't know the domain DN, we // have to query rootDSE's defaultNamingContext attribute to get // it. Then we have to query that DN's maxPwdAge attribute to get // the real value. // // Once we have both values, we just need to combine them. But MS // chose to use a different base and unit for time measurements. // So we need to convert the values to Unix timestamps (see // details below). // ---------------------------------------------------------------- $sr = ldap_read($ldapconn, ROOTDSE, '(objectClass=*)', array('defaultNamingContext')); if (!$sr) { error_log($this->errorlogtag.get_string ('rootdseerror', 'auth_ldap')); return 0; } $entry = ldap_get_entries_moodle($ldapconn, $sr); $info = $entry[0]; $domaindn = $info['defaultnamingcontext'][0]; $sr = ldap_read ($ldapconn, $domaindn, '(objectClass=*)', array('maxPwdAge')); $entry = ldap_get_entries_moodle($ldapconn, $sr); $info = $entry[0]; $maxpwdage = $info['maxpwdage'][0]; if ($sr = ldap_read($ldapconn, $user_dn, '(objectClass=*)', array('msDS-ResultantPSO'))) { if ($entry = ldap_get_entries_moodle($ldapconn, $sr)) { $info = $entry[0]; $userpso = $info['msds-resultantpso'][0]; // If a PSO exists, FGPP is being utilized. // Grab the new maxpwdage from the msDS-MaximumPasswordAge attribute of the PSO. if (!empty($userpso)) { $sr = ldap_read($ldapconn, $userpso, '(objectClass=*)', array('msDS-MaximumPasswordAge')); if ($entry = ldap_get_entries_moodle($ldapconn, $sr)) { $info = $entry[0]; // Default value of msds-maximumpasswordage is 42 and is always set. $maxpwdage = $info['msds-maximumpasswordage'][0]; } } } } // ---------------------------------------------------------------- // MSDN says that "pwdLastSet contains the number of 100 nanosecond // intervals since January 1, 1601 (UTC), stored in a 64 bit integer". // // According to Perl's Date::Manip, the number of seconds between // this date and Unix epoch is 11644473600. So we have to // substract this value to calculate a Unix time, once we have // scaled pwdLastSet to seconds. This is the script used to // calculate the value shown above: // // #!/usr/bin/perl -w // // use Date::Manip; // // $date1 = ParseDate ("160101010000 UTC"); // $date2 = ParseDate ("197001010000 UTC"); // $delta = DateCalc($date1, $date2, \$err); // $secs = Delta_Format($delta, 0, "%st"); // print "$secs \n"; // // MSDN also says that "maxPwdAge is stored as a large integer that // represents the number of 100 nanosecond intervals from the time // the password was set before the password expires." We also need // to scale this to seconds. Bear in mind that this value is stored // as a _negative_ quantity (at least in my AD domain). // // As a last remark, if the low 32 bits of maxPwdAge are equal to 0, // the maximum password age in the domain is set to 0, which means // passwords do not expire (see // http://msdn2.microsoft.com/en-us/library/ms974598.aspx) // // As the quantities involved are too big for PHP integers, we // need to use BCMath functions to work with arbitrary precision // numbers. // ---------------------------------------------------------------- // If the low order 32 bits are 0, then passwords do not expire in // the domain. Just do '$maxpwdage mod 2^32' and check the result // (2^32 = 4294967296) if (bcmod ($maxpwdage, 4294967296) === '0') { return 0; } // Add up pwdLastSet and maxPwdAge to get password expiration // time, in MS time units. Remember maxPwdAge is stored as a // _negative_ quantity, so we need to substract it in fact. $pwdexpire = bcsub ($pwdlastset, $maxpwdage); // Scale the result to convert it to Unix time units and return // that value. return bcsub( bcdiv($pwdexpire, '10000000'), '11644473600'); } /** * Connect to the LDAP server, using the plugin configured * settings. It's actually a wrapper around ldap_connect_moodle() * * @return resource A valid LDAP connection (or dies if it can't connect) */ function ldap_connect() { // Cache ldap connections. They are expensive to set up // and can drain the TCP/IP ressources on the server if we // are syncing a lot of users (as we try to open a new connection // to get the user details). This is the least invasive way // to reuse existing connections without greater code surgery. if(!empty($this->ldapconnection)) { $this->ldapconns++; return $this->ldapconnection; } if($ldapconnection = ldap_connect_moodle($this->config->host_url, $this->config->ldap_version, $this->config->user_type, $this->config->bind_dn, $this->config->bind_pw, $this->config->opt_deref, $debuginfo, $this->config->start_tls)) { $this->ldapconns = 1; $this->ldapconnection = $ldapconnection; return $ldapconnection; } throw new \moodle_exception('auth_ldap_noconnect_all', 'auth_ldap', '', $debuginfo); } /** * Disconnects from a LDAP server * * @param force boolean Forces closing the real connection to the LDAP server, ignoring any * cached connections. This is needed when we've used paged results * and want to use normal results again. */ function ldap_close($force=false) { $this->ldapconns--; if (($this->ldapconns == 0) || ($force)) { $this->ldapconns = 0; @ldap_close($this->ldapconnection); unset($this->ldapconnection); } } /** * Search specified contexts for username and return the user dn * like: cn=username,ou=suborg,o=org. It's actually a wrapper * around ldap_find_userdn(). * * @param resource $ldapconnection a valid LDAP connection * @param string $extusername the username to search (in external LDAP encoding, no db slashes) * @return mixed the user dn (external LDAP encoding) or false */ function ldap_find_userdn($ldapconnection, $extusername) { $ldap_contexts = explode(';', $this->config->contexts); if (!empty($this->config->create_context)) { array_push($ldap_contexts, $this->config->create_context); } return ldap_find_userdn($ldapconnection, $extusername, $ldap_contexts, $this->config->objectclass, $this->config->user_attribute, $this->config->search_sub); } /** * When using NTLM SSO, the format of the remote username we get in * $_SERVER['REMOTE_USER'] may vary, depending on where from and how the web * server gets the data. So we let the admin configure the format using two * place holders (%domain% and %username%). This function tries to extract * the username (stripping the domain part and any separators if they are * present) from the value present in $_SERVER['REMOTE_USER'], using the * configured format. * * @param string $remoteuser The value from $_SERVER['REMOTE_USER'] (converted to UTF-8) * * @return string The remote username (without domain part or * separators). Empty string if we can't extract the username. */ protected function get_ntlm_remote_user($remoteuser) { if (empty($this->config->ntlmsso_remoteuserformat)) { $format = AUTH_NTLM_DEFAULT_FORMAT; } else { $format = $this->config->ntlmsso_remoteuserformat; } $format = preg_quote($format); $formatregex = preg_replace(array('#%domain%#', '#%username%#'), array('('.AUTH_NTLM_VALID_DOMAINNAME.')', '('.AUTH_NTLM_VALID_USERNAME.')'), $format); if (preg_match('#^'.$formatregex.'$#', $remoteuser, $matches)) { $user = end($matches); return $user; } /* We are unable to extract the username with the configured format. Probably * the format specified is wrong, so log a warning for the admin and return * an empty username. */ error_log($this->errorlogtag.get_string ('auth_ntlmsso_maybeinvalidformat', 'auth_ldap')); return ''; } /** * Check if the diagnostic message for the LDAP login error tells us that the * login is denied because the user password has expired or the password needs * to be changed on first login (using interactive SMB/Windows logins, not * LDAP logins). * * @param string the diagnostic message for the LDAP login error * @return bool true if the password has expired or the password must be changed on first login */ protected function ldap_ad_pwdexpired_from_diagmsg($diagmsg) { // The format of the diagnostic message is (actual examples from W2003 and W2008): // "80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 52e, vece" (W2003) // "80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 773, vece" (W2003) // "80090308: LdapErr: DSID-0C0903AA, comment: AcceptSecurityContext error, data 52e, v1771" (W2008) // "80090308: LdapErr: DSID-0C0903AA, comment: AcceptSecurityContext error, data 773, v1771" (W2008) // We are interested in the 'data nnn' part. // if nnn == 773 then user must change password on first login // if nnn == 532 then user password has expired $diagmsg = explode(',', $diagmsg); if (preg_match('/data (773|532)/i', trim($diagmsg[2]))) { return true; } return false; } /** * Check if a user is suspended. This function is intended to be used after calling * get_userinfo_asobj. This is needed because LDAP doesn't have a notion of disabled * users, however things like MS Active Directory support it and expose information * through a field. * * @param object $user the user object returned by get_userinfo_asobj * @return boolean */ protected function is_user_suspended($user) { if (!$this->config->suspended_attribute || !isset($user->suspended)) { return false; } if ($this->config->suspended_attribute == 'useraccountcontrol' && $this->config->user_type == 'ad') { return (bool)($user->suspended & AUTH_AD_ACCOUNTDISABLE); } return (bool)$user->suspended; } /** * Test a DN * * @param resource $ldapconn * @param string $dn The DN to check for existence * @param string $message The identifier of a string as in get_string() * @param string|object|array $a An object, string or number that can be used * within translation strings as in get_string() * @return true or a message in case of error */ private function test_dn($ldapconn, $dn, $message, $a = null) { $ldapresult = @ldap_read($ldapconn, $dn, '(objectClass=*)', array()); if (!$ldapresult) { if (ldap_errno($ldapconn) == 32) { // No such object. return get_string($message, 'auth_ldap', $a); } $a = array('code' => ldap_errno($ldapconn), 'subject' => $a, 'message' => ldap_error($ldapconn)); return get_string('diag_genericerror', 'auth_ldap', $a); } return true; } /** * Test if settings are correct, print info to output. */ public function test_settings() { global $OUTPUT; if (!function_exists('ldap_connect')) { // Is php-ldap really there? echo $OUTPUT->notification(get_string('auth_ldap_noextension', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR); return; } // Check to see if this is actually configured. if (empty($this->config->host_url)) { // LDAP is not even configured. echo $OUTPUT->notification(get_string('ldapnotconfigured', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR); return; } if ($this->config->ldap_version != 3) { echo $OUTPUT->notification(get_string('diag_toooldversion', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING); } try { $ldapconn = $this->ldap_connect(); } catch (Exception $e) { echo $OUTPUT->notification($e->getMessage(), \core\output\notification::NOTIFY_ERROR); return; } // Display paged file results. if (!ldap_paged_results_supported($this->config->ldap_version, $ldapconn)) { echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'), \core\output\notification::NOTIFY_INFO); } // Check contexts. foreach (explode(';', $this->config->contexts) as $context) { $context = trim($context); if (empty($context)) { echo $OUTPUT->notification(get_string('diag_emptycontext', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING); continue; } $message = $this->test_dn($ldapconn, $context, 'diag_contextnotfound', $context); if ($message !== true) { echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING); } } // Create system role mapping field for each assignable system role. $roles = get_ldap_assignable_role_names(); foreach ($roles as $role) { foreach (explode(';', $this->config->{$role['settingname']}) as $groupdn) { if (empty($groupdn)) { continue; } $role['group'] = $groupdn; $message = $this->test_dn($ldapconn, $groupdn, 'diag_rolegroupnotfound', $role); if ($message !== true) { echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING); } } } $this->ldap_close(true); // We were able to connect successfuly. echo $OUTPUT->notification(get_string('connectingldapsuccess', 'auth_ldap'), \core\output\notification::NOTIFY_SUCCESS); } /** * Get the list of profile fields. * * @param bool $fetchall Fetch all, not just those for update. * @return array */ protected function get_profile_keys($fetchall = false) { $keys = array_keys(get_object_vars($this->config)); $updatekeys = []; foreach ($keys as $key) { if (preg_match('/^field_updatelocal_(.+)$/', $key, $match)) { // If we have a field to update it from and it must be updated 'onlogin' we update it on cron. if (!empty($this->config->{'field_map_'.$match[1]})) { if ($fetchall || $this->config->{$match[0]} === 'onlogin') { array_push($updatekeys, $match[1]); // the actual key name } } } } if ($this->config->suspended_attribute && $this->config->sync_suspended) { $updatekeys[] = 'suspended'; } return $updatekeys; } } PK1B\׆ntlmsso_magic.phpnu[set_context(context_system::instance()); $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. if (!in_array('ldap', $authsequence, true)) { throw new \moodle_exception('ldap_isdisabled', 'auth'); } $authplugin = get_auth_plugin('ldap'); if (empty($authplugin->config->ntlmsso_enabled)) { throw new \moodle_exception('ntlmsso_isdisabled', 'auth_ldap'); } $sesskey = required_param('sesskey', PARAM_RAW); $file = $CFG->dirroot.'/pix/spacer.gif'; if ($authplugin->ntlmsso_magic($sesskey) && file_exists($file)) { if (!empty($authplugin->config->ntlmsso_ie_fastpath)) { if (core_useragent::is_ie()) { redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_finish.php'); } } // Serve GIF // Type header('Content-Type: image/gif'); header('Content-Length: '.filesize($file)); // Output file $handle = fopen($file, 'r'); fpassthru($handle); fclose($handle); exit; } else { throw new \moodle_exception('ntlmsso_iwamagicnotenabled', 'auth_ldap'); } PK1B\t<## settings.phpnu[. /** * LDAP enrolment plugin settings and presets. * * @package enrol_ldap * @author Iñaki Arenaza * @copyright 2010 Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); if ($ADMIN->fulltree) { if (!function_exists('ldap_connect')) { $notify = new \core\output\notification(get_string('phpldap_noextension', 'enrol_ldap'), \core\output\notification::NOTIFY_WARNING); $settings->add(new admin_setting_heading('enrol_phpldap_noextension', '', $OUTPUT->render($notify))); $settings->add(new admin_setting_heading('enrol_ldap_settings', '', get_string('pluginname_desc', 'enrol_ldap'))); } else { $settings->add(new admin_setting_heading('enrol_ldap_settings', '', get_string('pluginname_desc', 'enrol_ldap'))); require_once($CFG->dirroot.'/enrol/ldap/settingslib.php'); require_once($CFG->libdir.'/ldaplib.php'); $yesno = array(get_string('no'), get_string('yes')); //--- connection settings --- $settings->add(new admin_setting_heading('enrol_ldap_server_settings', get_string('server_settings', 'enrol_ldap'), '')); $settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/host_url', get_string('host_url_key', 'enrol_ldap'), get_string('host_url', 'enrol_ldap'), '')); $settings->add(new admin_setting_configselect('enrol_ldap/start_tls', get_string('start_tls_key', 'auth_ldap'), get_string('start_tls', 'auth_ldap'), 0, $yesno)); // Set LDAPv3 as the default. Nowadays all the servers support it and it gives us some real benefits. $options = array(3=>'3', 2=>'2'); $settings->add(new admin_setting_configselect('enrol_ldap/ldap_version', get_string('version_key', 'enrol_ldap'), get_string('version', 'enrol_ldap'), 3, $options)); $settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/ldapencoding', get_string('ldap_encoding_key', 'enrol_ldap'), get_string('ldap_encoding', 'enrol_ldap'), 'utf-8')); $settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/pagesize', get_string('pagesize_key', 'auth_ldap'), get_string('pagesize', 'auth_ldap'), LDAP_DEFAULT_PAGESIZE, true)); //--- binding settings --- $settings->add(new admin_setting_heading('enrol_ldap_bind_settings', get_string('bind_settings', 'enrol_ldap'), '')); $settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/bind_dn', get_string('bind_dn_key', 'enrol_ldap'), get_string('bind_dn', 'enrol_ldap'), '')); $settings->add(new admin_setting_configpasswordunmask('enrol_ldap/bind_pw', get_string('bind_pw_key', 'enrol_ldap'), get_string('bind_pw', 'enrol_ldap'), '')); //--- role mapping settings --- $settings->add(new admin_setting_heading('enrol_ldap_roles', get_string('roles', 'enrol_ldap'), '')); if (!during_initial_install()) { $settings->add(new admin_setting_ldap_rolemapping('enrol_ldap/role_mapping', get_string ('role_mapping_key', 'enrol_ldap'), get_string ('role_mapping', 'enrol_ldap'), '')); } $options = $yesno; $settings->add(new admin_setting_configselect('enrol_ldap/course_search_sub', get_string('course_search_sub_key', 'enrol_ldap'), get_string('course_search_sub', 'enrol_ldap'), 0, $options)); $options = $yesno; $settings->add(new admin_setting_configselect('enrol_ldap/memberattribute_isdn', get_string('memberattribute_isdn_key', 'enrol_ldap'), get_string('memberattribute_isdn', 'enrol_ldap'), 0, $options)); $settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/user_contexts', get_string('user_contexts_key', 'enrol_ldap'), get_string('user_contexts', 'enrol_ldap'), '')); $options = $yesno; $settings->add(new admin_setting_configselect('enrol_ldap/user_search_sub', get_string('user_search_sub_key', 'enrol_ldap'), get_string('user_search_sub', 'enrol_ldap'), 0, $options)); $options = ldap_supported_usertypes(); $settings->add(new admin_setting_configselect('enrol_ldap/user_type', get_string('user_type_key', 'enrol_ldap'), get_string('user_type', 'enrol_ldap'), 'default', $options)); $options = array(); $options[LDAP_DEREF_NEVER] = get_string('no'); $options[LDAP_DEREF_ALWAYS] = get_string('yes'); $settings->add(new admin_setting_configselect('enrol_ldap/opt_deref', get_string('opt_deref_key', 'enrol_ldap'), get_string('opt_deref', 'enrol_ldap'), 0, $options)); $settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/idnumber_attribute', get_string('idnumber_attribute_key', 'enrol_ldap'), get_string('idnumber_attribute', 'enrol_ldap'), '', true, true)); //--- course mapping settings --- $settings->add(new admin_setting_heading('enrol_ldap_course_settings', get_string('course_settings', 'enrol_ldap'), '')); $settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/objectclass', get_string('objectclass_key', 'enrol_ldap'), get_string('objectclass', 'enrol_ldap'), '')); $settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/course_idnumber', get_string('course_idnumber_key', 'enrol_ldap'), get_string('course_idnumber', 'enrol_ldap'), '', true, true)); $coursefields = array ('shortname', 'fullname', 'summary'); foreach ($coursefields as $field) { $settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/course_'.$field, get_string('course_'.$field.'_key', 'enrol_ldap'), get_string('course_'.$field, 'enrol_ldap'), '', true, true)); } $settings->add(new admin_setting_configcheckbox( 'enrol_ldap/ignorehiddencourses', get_string('ignorehiddencourses', 'enrol_ldap'), get_string('ignorehiddencourses_desc', 'enrol_ldap'), 0, )); $options = array(ENROL_EXT_REMOVED_UNENROL => get_string('extremovedunenrol', 'enrol'), ENROL_EXT_REMOVED_KEEP => get_string('extremovedkeep', 'enrol'), ENROL_EXT_REMOVED_SUSPEND => get_string('extremovedsuspend', 'enrol'), ENROL_EXT_REMOVED_SUSPENDNOROLES => get_string('extremovedsuspendnoroles', 'enrol')); $settings->add(new admin_setting_configselect('enrol_ldap/unenrolaction', get_string('extremovedaction', 'enrol'), get_string('extremovedaction_help', 'enrol'), ENROL_EXT_REMOVED_UNENROL, $options)); //--- course creation settings --- $settings->add(new admin_setting_heading('enrol_ldap_autocreation_settings', get_string('autocreation_settings', 'enrol_ldap'), '')); $options = $yesno; $settings->add(new admin_setting_configselect('enrol_ldap/autocreate', get_string('autocreate_key', 'enrol_ldap'), get_string('autocreate', 'enrol_ldap'), 0, $options)); $settings->add(new enrol_ldap_admin_setting_category('enrol_ldap/category', get_string('category_key', 'enrol_ldap'), get_string('category', 'enrol_ldap'))); $settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/template', get_string('template_key', 'enrol_ldap'), get_string('template', 'enrol_ldap'), '')); //--- course update settings --- $settings->add(new admin_setting_heading('enrol_ldap_autoupdate_settings', get_string('autoupdate_settings', 'enrol_ldap'), get_string('autoupdate_settings_desc', 'enrol_ldap'))); $options = $yesno; foreach ($coursefields as $field) { $settings->add(new admin_setting_configselect('enrol_ldap/course_'.$field.'_updateonsync', get_string('course_'.$field.'_updateonsync_key', 'enrol_ldap'), get_string('course_'.$field.'_updateonsync', 'enrol_ldap'), 0, $options)); } //--- nested groups settings --- $settings->add(new admin_setting_heading('enrol_ldap_nested_groups_settings', get_string('nested_groups_settings', 'enrol_ldap'), '')); $options = $yesno; $settings->add(new admin_setting_configselect('enrol_ldap/nested_groups', get_string('nested_groups_key', 'enrol_ldap'), get_string('nested_groups', 'enrol_ldap'), 0, $options)); $settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/group_memberofattribute', get_string('group_memberofattribute_key', 'enrol_ldap'), get_string('group_memberofattribute', 'enrol_ldap'), '', true, true)); } } PK\4lA db/access.phpnu[. /** * Capabilities for LDAP enrolment plugin. * * @package enrol_ldap * @author Iñaki Arenaza * @copyright 2010 Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'enrol/ldap:manage' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'manager' => CAP_ALLOW, ) ), ); PK\տ//lang/en/enrol_ldap.phpnu[. /** * Strings for component 'enrol_ldap', language 'en'. * * @package enrol_ldap * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @copyright 2010 Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['assignrole'] = "Assigning role '{\$a->role_shortname}' to user '{\$a->user_username}' into course '{\$a->course_shortname}' (id {\$a->course_id})"; $string['assignrolefailed'] = "Failed to assign role '{\$a->role_shortname}' to user '{\$a->user_username}' into course '{\$a->course_shortname}' (id {\$a->course_id})\n"; $string['autocreate'] = '

Courses can be created automatically if there are enrolments to a course that doesn\'t yet exist in Moodle

If you are using automatic course creation, it is recommended that you remove the following capabilities: moodle/course:changeidnumber, moodle/course:changeshortname, moodle/course:changefullname and moodle/course:changesummary, from the relevant roles to prevent modifications of the four course fields specified above (ID number, shortname, fullname and summary).

'; $string['autocreate_key'] = 'Auto create'; $string['autocreation_settings'] = 'Automatic course creation settings'; $string['autoupdate_settings'] = 'Automatic course update settings'; $string['autoupdate_settings_desc'] = '

Select fields to update when the \'Synchronise LDAP enrolments\' scheduled task is running.

When at least one field is selected an update will occur.

'; $string['bind_dn'] = 'If you want to use a bind user to search users, specify it here. Someting like \'cn=ldapuser,ou=public,o=org\''; $string['bind_dn_key'] = 'Bind user distinguished name'; $string['bind_pw'] = 'Password for the bind user'; $string['bind_pw_key'] = 'Password'; $string['bind_settings'] = 'Bind settings'; $string['cannotcreatecourse'] = 'Cannot create course: missing required data from the LDAP record!'; $string['cannotupdatecourse'] = "Cannot update course: missing required data from the LDAP record! Course idnumber: '{\$a->idnumber}'"; $string['cannotupdatecourse_duplicateshortname'] = "Cannot update course: Duplicate short name. Skipping course with idnumber '{\$a->idnumber}'..."; $string['courseupdated'] = "Course with idnumber '{\$a->idnumber}' was successfully updated."; $string['courseupdateskipped'] = "Course with idnumber '{\$a->idnumber}' does not require updating. Skipping..."; $string['category'] = 'The category for auto-created courses'; $string['category_key'] = 'Category'; $string['contexts'] = 'LDAP contexts'; $string['couldnotfinduser'] = "Could not find user '{\$a}', skipping\n"; $string['coursenotexistskip'] = "Course '{\$a}' does not exist and autocreation disabled, skipping\n"; $string['course_fullname'] = 'Optional: LDAP attribute to get the full name from'; $string['course_fullname_key'] = 'Full name'; $string['course_fullname_updateonsync'] = 'Update full name during synchronisation script'; $string['course_fullname_updateonsync_key'] = 'Update full name'; $string['course_idnumber'] = 'LDAP attribute to get the course ID number from. Usually \'cn\' or \'uid\'.'; $string['course_idnumber_key'] = 'ID number'; $string['course_search_sub'] = 'Search group memberships from subcontexts'; $string['course_search_sub_key'] = 'Search subcontexts'; $string['course_settings'] = 'Course enrolment settings'; $string['course_shortname'] = 'Optional: LDAP attribute to get the shortname from'; $string['course_shortname_key'] = 'Short name'; $string['course_shortname_updateonsync'] = 'Update short name during synchronisation script'; $string['course_shortname_updateonsync_key'] = 'Update short name'; $string['course_summary'] = 'Optional: LDAP attribute to get the summary from'; $string['course_summary_key'] = 'Summary'; $string['course_summary_updateonsync'] = 'Update summary during synchronisation script'; $string['course_summary_updateonsync_key'] = 'Update summary'; $string['createcourseextid'] = 'CREATE User enrolled to a non-existing course \'{$a->courseextid}\''; $string['createnotcourseextid'] = 'User enrolled to a non-existing course \'{$a->courseextid}\''; $string['creatingcourse'] = 'Creating course \'{$a}\'...'; $string['duplicateshortname'] = "Course creation failed. Duplicate short name. Skipping course with idnumber '{\$a->idnumber}'..."; $string['editlock'] = 'Lock value'; $string['emptyenrolment'] = "Empty enrolment for role '{\$a->role_shortname}' in course '{\$a->course_shortname}'\n"; $string['enrolname'] = 'LDAP'; $string['enroluser'] = "Enrol user '{\$a->user_username}' into course '{\$a->course_shortname}' (id {\$a->course_id})"; $string['enroluserenable'] = "Enabled enrolment for user '{\$a->user_username}' in course '{\$a->course_shortname}' (id {\$a->course_id})"; $string['explodegroupusertypenotsupported'] = "ldap_explode_group() does not support selected user type: {\$a}\n"; $string['extcourseidinvalid'] = 'The course external id is invalid!'; $string['extremovedsuspend'] = "Disabled enrolment for user '{\$a->user_username}' in course '{\$a->course_shortname}' (id {\$a->course_id})"; $string['extremovedsuspendnoroles'] = "Disabled enrolment and removed roles for user '{\$a->user_username}' in course '{\$a->course_shortname}' (id {\$a->course_id})"; $string['extremovedunenrol'] = "Unenrol user '{\$a->user_username}' from course '{\$a->course_shortname}' (id {\$a->course_id})"; $string['failed'] = "Failed!\n"; $string['general_options'] = 'General options'; $string['group_memberofattribute'] = 'Name of the attribute that specifies which groups a given user or group belongs to (e.g., memberOf, groupMembership, etc.)'; $string['group_memberofattribute_key'] = '\'Member of\' attribute'; $string['host_url'] = 'Specify LDAP host in URL-form like \'ldap://ldap.myorg.com/\' or \'ldaps://ldap.myorg.com/\''; $string['host_url_key'] = 'Host URL'; $string['idnumber_attribute'] = 'If the group membership contains distinguished names, specify the same attribute you have used for the user \'ID Number\' mapping in the LDAP authentication settings.'; $string['idnumber_attribute_key'] = 'ID number attribute'; $string['ignorehiddencourses'] = 'Ignore hidden courses'; $string['ignorehiddencourses_desc'] = 'If enabled users will not be enrolled on courses that are set to be unavailable to students.'; $string['ldap_encoding'] = 'Specify encoding used by LDAP server. Most probably utf-8, MS AD v2 uses default platform encoding such as cp1252, cp1250, etc.'; $string['ldap_encoding_key'] = 'LDAP encoding'; $string['ldap:manage'] = 'Manage LDAP enrol instances'; $string['memberattribute'] = 'LDAP member attribute'; $string['memberattribute_isdn'] = 'If the group membership contains distinguished names, you need to specify them here. If so, you also need to configure the remaining settings in this section.'; $string['memberattribute_isdn_key'] = 'Member attribute uses dn'; $string['nested_groups'] = 'Do you want to use nested groups (groups of groups) for enrolment?'; $string['nested_groups_key'] = 'Nested groups'; $string['nested_groups_settings'] = 'Nested groups settings'; $string['nosuchrole'] = "No such role: '{\$a}'\n"; $string['objectclass'] = 'objectClass used to search courses. Usually \'group\' or \'posixGroup\''; $string['objectclass_key'] = 'Object class'; $string['ok'] = "OK!\n"; $string['opt_deref'] = 'If the group membership contains distinguished names, specify how aliases are handled during a search. Select one of the following values: \'No\' (LDAP_DEREF_NEVER) or \'Yes\' (LDAP_DEREF_ALWAYS).'; $string['opt_deref_key'] = 'Dereference aliases'; $string['phpldap_noextension'] = 'The PHP LDAP module does not seem to be present. Please ensure it is installed and enabled if you want to use this enrolment plugin.'; $string['pluginname'] = 'LDAP enrolments'; $string['pluginname_desc'] = '

You can use an LDAP server to control your enrolments. It is assumed your LDAP tree contains groups that map to the courses, and that each of those groups/courses will have membership entries to map to students.

It is assumed that courses are defined as groups in LDAP, with each group having multiple membership fields (member or memberUid) that contain a uniqueidentification of the user.

To use LDAP enrolment, your users must to have a valid idnumber field. The LDAP groups must have that idnumber in the member fields for a user to be enrolled in the course. This will usually work well if you are already using LDAP Authentication.

Enrolments will be updated when the user logs in. You can also run a script to keep enrolments in synch. Look in enrol/ldap/cli/sync.php.

This plugin can also be set to automatically create new courses when new groups appear in LDAP.

'; $string['pluginnotenabled'] = 'Plugin not enabled!'; $string['role_mapping'] = '

For each role, you need to specify all LDAP contexts where the groups that represent the courses are located. Separate different contexts with a semicolon (;).

You also need to specify the attribute your LDAP server uses to hold the members of a group. This is usually \'member\' or \'memberUid\'.

'; $string['role_mapping_attribute'] = 'LDAP member attribute for {$a}'; $string['role_mapping_context'] = 'LDAP contexts for {$a}'; $string['role_mapping_key'] = 'Map roles from LDAP '; $string['roles'] = 'Role mapping'; $string['server_settings'] = 'LDAP server settings'; $string['syncenrolmentstask'] = 'Synchronise LDAP enrolments task'; $string['synccourserole'] = "== Synching course '{\$a->idnumber}' for role '{\$a->role_shortname}'\n"; $string['template'] = 'Optional: auto-created courses can copy their settings from a template course'; $string['template_key'] = 'Template'; $string['unassignrole'] = "Unassigning role '{\$a->role_shortname}' to user '{\$a->user_username}' from course '{\$a->course_shortname}' (id {\$a->course_id})\n"; $string['unassignroleid'] = "Unassigning role id '{\$a->role_id}' to user id '{\$a->user_id}'\n"; $string['unassignrolefailed'] = "Failed to unassign role '{\$a->role_shortname}' to user '{\$a->user_username}' from course '{\$a->course_shortname}' (id {\$a->course_id})\n"; $string['updatelocal'] = 'Update local data'; $string['user_attribute'] = 'If the group membership contains distinguished names, specify the attribute used to name/search for users. If you are using LDAP authentication, this value should match the attribute specified in the \'ID Number\' mapping in the LDAP authentication plugin.'; $string['user_attribute_key'] = 'ID number attribute'; $string['user_contexts'] = 'If the group membership contains distinguished names, specify the list of contexts where users are located. Separate different contexts with a semi-colon (;). For example: \'ou=users,o=org; ou=others,o=org\'.'; $string['user_contexts_key'] = 'Contexts'; $string['user_search_sub'] = 'If the group membership contains distinguished names, specify if the search for users is done in sub-contexts too.'; $string['user_search_sub_key'] = 'Search subcontexts'; $string['user_settings'] = 'User lookup settings'; $string['user_type'] = 'If the group membership contains distinguished names, specify how users are stored in LDAP'; $string['user_type_key'] = 'User type'; $string['version'] = 'The version of the LDAP protocol your server is using'; $string['version_key'] = 'Version'; $string['privacy:metadata'] = 'The LDAP enrolments plugin does not store any personal data.'; PK\+..lib.phpnu[. /** * LDAP enrolment plugin implementation. * * This plugin synchronises enrolment and roles with a LDAP server. * * @package enrol_ldap * @author Iñaki Arenaza - based on code by Martin Dougiamas, Martin Langhoff and others * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @copyright 2010 Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); class enrol_ldap_plugin extends enrol_plugin { protected $enrol_localcoursefield = 'idnumber'; protected $enroltype = 'enrol_ldap'; protected $errorlogtag = '[ENROL LDAP] '; /** * The object class to use when finding users. * * @var string $userobjectclass */ protected $userobjectclass; /** @var LDAP\Connection LDAP connection. */ protected $ldapconnection; /** * Constructor for the plugin. In addition to calling the parent * constructor, we define and 'fix' some settings depending on the * real settings the admin defined. */ public function __construct() { global $CFG; require_once($CFG->libdir.'/ldaplib.php'); // Do our own stuff to fix the config (it's easier to do it // here than using the admin settings infrastructure). We // don't call $this->set_config() for any of the 'fixups' // (except the objectclass, as it's critical) because the user // didn't specify any values and relied on the default values // defined for the user type she chose. $this->load_config(); // Make sure we get sane defaults for critical values. $this->config->ldapencoding = $this->get_config('ldapencoding', 'utf-8'); $this->config->user_type = $this->get_config('user_type', 'default'); $ldap_usertypes = ldap_supported_usertypes(); $this->config->user_type_name = $ldap_usertypes[$this->config->user_type]; unset($ldap_usertypes); $default = ldap_getdefaults(); // The objectclass in the defaults is for a user. // This will be required later, but enrol_ldap uses 'objectclass' for its group objectclass. // Save the normalised user objectclass for later. $this->userobjectclass = ldap_normalise_objectclass($default['objectclass'][$this->get_config('user_type')]); // Remove the objectclass default, as the values specified there are for users, and we are dealing with groups here. unset($default['objectclass']); // Use defaults if values not given. Dont use this->get_config() // here to be able to check for 0 and false values too. foreach ($default as $key => $value) { // Watch out - 0, false are correct values too, so we can't use $this->get_config() if (!isset($this->config->{$key}) or $this->config->{$key} == '') { $this->config->{$key} = $value[$this->config->user_type]; } } // Normalise the objectclass used for groups. if (empty($this->config->objectclass)) { // No objectclass set yet - set a default class. $this->config->objectclass = ldap_normalise_objectclass(null, '*'); $this->set_config('objectclass', $this->config->objectclass); } else { $objectclass = ldap_normalise_objectclass($this->config->objectclass); if ($objectclass !== $this->config->objectclass) { // The objectclass was changed during normalisation. // Save it in config, and update the local copy of config. $this->set_config('objectclass', $objectclass); $this->config->objectclass = $objectclass; } } } /** * Is it possible to delete enrol instance via standard UI? * * @param object $instance * @return bool */ public function can_delete_instance($instance) { $context = context_course::instance($instance->courseid); if (!has_capability('enrol/ldap:manage', $context)) { return false; } if (!enrol_is_enabled('ldap')) { return true; } if (!$this->get_config('ldap_host') or !$this->get_config('objectclass') or !$this->get_config('course_idnumber')) { return true; } // TODO: connect to external system and make sure no users are to be enrolled in this course return false; } /** * Is it possible to hide/show enrol instance via standard UI? * * @param stdClass $instance * @return bool */ public function can_hide_show_instance($instance) { $context = context_course::instance($instance->courseid); return has_capability('enrol/ldap:manage', $context); } /** * Forces synchronisation of user enrolments with LDAP server. * It creates courses if the plugin is configured to do so. * * @param object $user user record * @return void */ public function sync_user_enrolments($user) { global $DB; // Do not try to print anything to the output because this method is called during interactive login. if (PHPUNIT_TEST) { $trace = new null_progress_trace(); } else { $trace = new error_log_progress_trace($this->errorlogtag); } if (!$this->ldap_connect($trace)) { $trace->finished(); return; } if (!is_object($user) or !property_exists($user, 'id')) { throw new coding_exception('Invalid $user parameter in sync_user_enrolments()'); } if (!property_exists($user, 'idnumber')) { debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber'); $user = $DB->get_record('user', array('id'=>$user->id)); } // We may need a lot of memory here core_php_time_limit::raise(); raise_memory_limit(MEMORY_HUGE); // Get enrolments for each type of role. $roles = get_all_roles(); $enrolments = array(); foreach($roles as $role) { // Get external enrolments according to LDAP server $enrolments[$role->id]['ext'] = $this->find_ext_enrolments($user->idnumber, $role); // Get the list of current user enrolments that come from LDAP $sql= "SELECT e.courseid, ue.status, e.id as enrolid, c.shortname FROM {user} u JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid) JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid) JOIN {enrol} e ON (e.id = ue.enrolid) JOIN {course} c ON (c.id = e.courseid) WHERE u.deleted = 0 AND u.id = :userid"; $params = array ('roleid'=>$role->id, 'userid'=>$user->id); $enrolments[$role->id]['current'] = $DB->get_records_sql($sql, $params); } $ignorehidden = $this->get_config('ignorehiddencourses'); $courseidnumber = $this->get_config('course_idnumber'); foreach($roles as $role) { foreach ($enrolments[$role->id]['ext'] as $enrol) { $course_ext_id = $enrol[$courseidnumber][0]; if (empty($course_ext_id)) { $trace->output(get_string('extcourseidinvalid', 'enrol_ldap')); continue; // Next; skip this one! } // Create the course if required $course = $DB->get_record('course', array($this->enrol_localcoursefield=>$course_ext_id)); if (empty($course)) { // Course doesn't exist if ($this->get_config('autocreate')) { // Autocreate $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id))); if (!$newcourseid = $this->create_course($enrol, $trace)) { continue; } $course = $DB->get_record('course', array('id'=>$newcourseid)); } else { $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id))); continue; // Next; skip this one! } } // Deal with enrolment in the moodle db // Add necessary enrol instance if not present yet; $sql = "SELECT c.id, c.visible, e.id as enrolid FROM {course} c JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap') WHERE c.id = :courseid"; $params = array('courseid'=>$course->id); if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) { $course_instance = new stdClass(); $course_instance->id = $course->id; $course_instance->visible = $course->visible; $course_instance->enrolid = $this->add_instance($course_instance); } if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) { continue; // Weird; skip this one. } if ($ignorehidden && !$course_instance->visible) { continue; } if (empty($enrolments[$role->id]['current'][$course->id])) { // Enrol the user in the given course, with that role. $this->enrol_user($instance, $user->id, $role->id); // Make sure we set the enrolment status to active. If the user wasn't // previously enrolled to the course, enrol_user() sets it. But if we // configured the plugin to suspend the user enrolments _AND_ remove // the role assignments on external unenrol, then enrol_user() doesn't // set it back to active on external re-enrolment. So set it // unconditionnally to cover both cases. $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id)); $trace->output(get_string('enroluser', 'enrol_ldap', array('user_username'=> $user->username, 'course_shortname'=>$course->shortname, 'course_id'=>$course->id))); } else { if ($enrolments[$role->id]['current'][$course->id]->status == ENROL_USER_SUSPENDED) { // Reenable enrolment that was previously disabled. Enrolment refreshed $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id)); $trace->output(get_string('enroluserenable', 'enrol_ldap', array('user_username'=> $user->username, 'course_shortname'=>$course->shortname, 'course_id'=>$course->id))); } } // Remove this course from the current courses, to be able to detect // which current courses should be unenroled from when we finish processing // external enrolments. unset($enrolments[$role->id]['current'][$course->id]); } // Deal with unenrolments. $transaction = $DB->start_delegated_transaction(); foreach ($enrolments[$role->id]['current'] as $course) { $context = context_course::instance($course->courseid); $instance = $DB->get_record('enrol', array('id'=>$course->enrolid)); switch ($this->get_config('unenrolaction')) { case ENROL_EXT_REMOVED_UNENROL: $this->unenrol_user($instance, $user->id); $trace->output(get_string('extremovedunenrol', 'enrol_ldap', array('user_username'=> $user->username, 'course_shortname'=>$course->shortname, 'course_id'=>$course->courseid))); break; case ENROL_EXT_REMOVED_KEEP: // Keep - only adding enrolments break; case ENROL_EXT_REMOVED_SUSPEND: if ($course->status != ENROL_USER_SUSPENDED) { $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id)); $trace->output(get_string('extremovedsuspend', 'enrol_ldap', array('user_username'=> $user->username, 'course_shortname'=>$course->shortname, 'course_id'=>$course->courseid))); } break; case ENROL_EXT_REMOVED_SUSPENDNOROLES: if ($course->status != ENROL_USER_SUSPENDED) { $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id)); } role_unassign_all(array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id)); $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap', array('user_username'=> $user->username, 'course_shortname'=>$course->shortname, 'course_id'=>$course->courseid))); break; } } $transaction->allow_commit(); } $this->ldap_close(); $trace->finished(); } /** * Forces synchronisation of all enrolments with LDAP server. * It creates courses if the plugin is configured to do so. * * @param progress_trace $trace * @param int|null $onecourse limit sync to one course->id, null if all courses * @return void */ public function sync_enrolments(progress_trace $trace, $onecourse = null) { global $CFG, $DB; if (!$this->ldap_connect($trace)) { $trace->finished(); return; } $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection); // we may need a lot of memory here core_php_time_limit::raise(); raise_memory_limit(MEMORY_HUGE); $oneidnumber = null; if ($onecourse) { if (!$course = $DB->get_record('course', array('id'=>$onecourse), 'id,'.$this->enrol_localcoursefield)) { // Course does not exist, nothing to do. $trace->output("Requested course $onecourse does not exist, no sync performed."); $trace->finished(); return; } if (empty($course->{$this->enrol_localcoursefield})) { $trace->output("Requested course $onecourse does not have {$this->enrol_localcoursefield}, no sync performed."); $trace->finished(); return; } $oneidnumber = ldap_filter_addslashes(core_text::convert($course->idnumber, 'utf-8', $this->get_config('ldapencoding'))); } // Get enrolments for each type of role. $roles = get_all_roles(); $enrolments = array(); foreach($roles as $role) { // Get all contexts $ldap_contexts = explode(';', $this->config->{'contexts_role'.$role->id}); // Get all the fields we will want for the potential course creation // as they are light. Don't get membership -- potentially a lot of data. $ldap_fields_wanted = array('dn', $this->config->course_idnumber); if (!empty($this->config->course_fullname)) { array_push($ldap_fields_wanted, $this->config->course_fullname); } if (!empty($this->config->course_shortname)) { array_push($ldap_fields_wanted, $this->config->course_shortname); } if (!empty($this->config->course_summary)) { array_push($ldap_fields_wanted, $this->config->course_summary); } array_push($ldap_fields_wanted, $this->config->{'memberattribute_role'.$role->id}); // Define the search pattern $ldap_search_pattern = $this->config->objectclass; if ($oneidnumber !== null) { $ldap_search_pattern = "(&$ldap_search_pattern({$this->config->course_idnumber}=$oneidnumber))"; } $ldap_cookie = ''; $servercontrols = array(); foreach ($ldap_contexts as $ldap_context) { $ldap_context = trim($ldap_context); if (empty($ldap_context)) { continue; // Next; } $flat_records = array(); do { if ($ldap_pagedresults) { $servercontrols = array(array( 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array( 'size' => $this->config->pagesize, 'cookie' => $ldap_cookie))); } if ($this->config->course_search_sub) { // Use ldap_search to find first user from subtree $ldap_result = @ldap_search($this->ldapconnection, $ldap_context, $ldap_search_pattern, $ldap_fields_wanted, 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } else { // Search only in this context $ldap_result = @ldap_list($this->ldapconnection, $ldap_context, $ldap_search_pattern, $ldap_fields_wanted, 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } if (!$ldap_result) { continue; // Next } if ($ldap_pagedresults) { // Get next server cookie to know if we'll need to continue searching. $ldap_cookie = ''; // Get next cookie from controls. ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $controls); if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; } } // Check and push results $records = ldap_get_entries($this->ldapconnection, $ldap_result); // LDAP libraries return an odd array, really. fix it: for ($c = 0; $c < $records['count']; $c++) { array_push($flat_records, $records[$c]); } // Free some mem unset($records); } while ($ldap_pagedresults && !empty($ldap_cookie)); // If LDAP paged results were used, the current connection must be completely // closed and a new one created, to work without paged results from here on. if ($ldap_pagedresults) { $this->ldap_close(); $this->ldap_connect($trace); } if (count($flat_records)) { $ignorehidden = $this->get_config('ignorehiddencourses'); foreach($flat_records as $course) { $course = array_change_key_case($course, CASE_LOWER); $idnumber = $course[$this->config->course_idnumber][0]; $trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname))); // Does the course exist in moodle already? $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber)); if (empty($course_obj)) { // Course doesn't exist if ($this->get_config('autocreate')) { // Autocreate $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber))); if (!$newcourseid = $this->create_course($course, $trace)) { continue; } $course_obj = $DB->get_record('course', array('id'=>$newcourseid)); } else { $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber))); continue; // Next; skip this one! } } else { // Check if course needs update & update as needed. $this->update_course($course_obj, $course, $trace); } // Enrol & unenrol // Pull the ldap membership into a nice array // this is an odd array -- mix of hash and array -- $ldapmembers = array(); if (property_exists($this->config, 'memberattribute_role'.$role->id) && !empty($this->config->{'memberattribute_role'.$role->id}) && !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership! $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}]; unset($ldapmembers['count']); // Remove oddity ;) // If we have enabled nested groups, we need to expand // the groups to get the real user list. We need to do // this before dealing with 'memberattribute_isdn'. if ($this->config->nested_groups) { $users = array(); foreach ($ldapmembers as $ldapmember) { $grpusers = $this->ldap_explode_group($ldapmember, $this->config->{'memberattribute_role'.$role->id}); $users = array_merge($users, $grpusers); } $ldapmembers = array_unique($users); // There might be duplicates. } // Deal with the case where the member attribute holds distinguished names, // but only if the user attribute is not a distinguished name itself. if ($this->config->memberattribute_isdn && ($this->config->idnumber_attribute !== 'dn') && ($this->config->idnumber_attribute !== 'distinguishedname')) { // We need to retrieve the idnumber for all the users in $ldapmembers, // as the idnumber does not match their dn and we get dn's from membership. $memberidnumbers = array(); foreach ($ldapmembers as $ldapmember) { $result = ldap_read($this->ldapconnection, $ldapmember, $this->userobjectclass, array($this->config->idnumber_attribute)); $entry = ldap_first_entry($this->ldapconnection, $result); $values = ldap_get_values($this->ldapconnection, $entry, $this->config->idnumber_attribute); array_push($memberidnumbers, $values[0]); } $ldapmembers = $memberidnumbers; } } // Prune old ldap enrolments // hopefully they'll fit in the max buffer size for the RDBMS $sql= "SELECT u.id as userid, u.username, ue.status, ra.contextid, ra.itemid as instanceid FROM {user} u JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid) JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid) JOIN {enrol} e ON (e.id = ue.enrolid) WHERE u.deleted = 0 AND e.courseid = :courseid "; $params = array('roleid'=>$role->id, 'courseid'=>$course_obj->id); $context = context_course::instance($course_obj->id); if (!empty($ldapmembers)) { list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm', false); $sql .= "AND u.idnumber $ldapml"; $params = array_merge($params, $params2); unset($params2); } else { $shortname = format_string($course_obj->shortname, true, array('context' => $context)); $trace->output(get_string('emptyenrolment', 'enrol_ldap', array('role_shortname'=> $role->shortname, 'course_shortname' => $shortname))); } $todelete = $DB->get_records_sql($sql, $params); if (!empty($todelete)) { $transaction = $DB->start_delegated_transaction(); foreach ($todelete as $row) { $instance = $DB->get_record('enrol', array('id'=>$row->instanceid)); switch ($this->get_config('unenrolaction')) { case ENROL_EXT_REMOVED_UNENROL: $this->unenrol_user($instance, $row->userid); $trace->output(get_string('extremovedunenrol', 'enrol_ldap', array('user_username'=> $row->username, 'course_shortname'=>$course_obj->shortname, 'course_id'=>$course_obj->id))); break; case ENROL_EXT_REMOVED_KEEP: // Keep - only adding enrolments break; case ENROL_EXT_REMOVED_SUSPEND: if ($row->status != ENROL_USER_SUSPENDED) { $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid)); $trace->output(get_string('extremovedsuspend', 'enrol_ldap', array('user_username'=> $row->username, 'course_shortname'=>$course_obj->shortname, 'course_id'=>$course_obj->id))); } break; case ENROL_EXT_REMOVED_SUSPENDNOROLES: if ($row->status != ENROL_USER_SUSPENDED) { $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid)); } role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id)); $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap', array('user_username'=> $row->username, 'course_shortname'=>$course_obj->shortname, 'course_id'=>$course_obj->id))); break; } } $transaction->allow_commit(); } // Insert current enrolments // bad we can't do INSERT IGNORE with postgres... // Add necessary enrol instance if not present yet; $sql = "SELECT c.id, c.visible, e.id as enrolid FROM {course} c JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap') WHERE c.id = :courseid"; $params = array('courseid'=>$course_obj->id); if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) { $course_instance = new stdClass(); $course_instance->id = $course_obj->id; $course_instance->visible = $course_obj->visible; $course_instance->enrolid = $this->add_instance($course_instance); } if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) { continue; // Weird; skip this one. } if ($ignorehidden && !$course_instance->visible) { continue; } $transaction = $DB->start_delegated_transaction(); foreach ($ldapmembers as $ldapmember) { $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0'; $member = $DB->get_record_sql($sql, array($ldapmember)); if(empty($member) || empty($member->id)){ $trace->output(get_string('couldnotfinduser', 'enrol_ldap', $ldapmember)); continue; } $sql= "SELECT ue.status FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'ldap') WHERE e.courseid = :courseid AND ue.userid = :userid"; $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id); $userenrolment = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE); if (empty($userenrolment)) { $this->enrol_user($instance, $member->id, $role->id); // Make sure we set the enrolment status to active. If the user wasn't // previously enrolled to the course, enrol_user() sets it. But if we // configured the plugin to suspend the user enrolments _AND_ remove // the role assignments on external unenrol, then enrol_user() doesn't // set it back to active on external re-enrolment. So set it // unconditionally to cover both cases. $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id)); $trace->output(get_string('enroluser', 'enrol_ldap', array('user_username'=> $member->username, 'course_shortname'=>$course_obj->shortname, 'course_id'=>$course_obj->id))); } else { if (!$DB->record_exists('role_assignments', array('roleid'=>$role->id, 'userid'=>$member->id, 'contextid'=>$context->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id))) { // This happens when reviving users or when user has multiple roles in one course. $context = context_course::instance($course_obj->id); role_assign($role->id, $member->id, $context->id, 'enrol_ldap', $instance->id); $trace->output("Assign role to user '$member->username' in course '$course_obj->shortname ($course_obj->id)'"); } if ($userenrolment->status == ENROL_USER_SUSPENDED) { // Reenable enrolment that was previously disabled. Enrolment refreshed $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id)); $trace->output(get_string('enroluserenable', 'enrol_ldap', array('user_username'=> $member->username, 'course_shortname'=>$course_obj->shortname, 'course_id'=>$course_obj->id))); } } } $transaction->allow_commit(); } } } } @$this->ldap_close(); $trace->finished(); } /** * Connect to the LDAP server, using the plugin configured * settings. It's actually a wrapper around ldap_connect_moodle() * * @param progress_trace $trace * @return bool success */ protected function ldap_connect(?progress_trace $trace = null) { global $CFG; require_once($CFG->libdir.'/ldaplib.php'); if (isset($this->ldapconnection)) { return true; } if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'), $this->get_config('user_type'), $this->get_config('bind_dn'), $this->get_config('bind_pw'), $this->get_config('opt_deref'), $debuginfo, $this->get_config('start_tls'))) { $this->ldapconnection = $ldapconnection; return true; } if ($trace) { $trace->output($debuginfo); } else { error_log($this->errorlogtag.$debuginfo); } return false; } /** * Disconnects from a LDAP server * */ protected function ldap_close() { if (isset($this->ldapconnection)) { @ldap_close($this->ldapconnection); $this->ldapconnection = null; } return; } /** * Return multidimensional array with details of user courses (at * least dn and idnumber). * * @param string $memberuid user idnumber (without magic quotes). * @param object role is a record from the mdl_role table. * @return array */ protected function find_ext_enrolments($memberuid, $role) { global $CFG; require_once($CFG->libdir.'/ldaplib.php'); if (empty($memberuid)) { // No "idnumber" stored for this user, so no LDAP enrolments return array(); } $ldap_contexts = trim($this->get_config('contexts_role'.$role->id)); if (empty($ldap_contexts)) { // No role contexts, so no LDAP enrolments return array(); } $extmemberuid = core_text::convert($memberuid, 'utf-8', $this->get_config('ldapencoding')); if($this->get_config('memberattribute_isdn')) { if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) { return array(); } } $ldap_search_pattern = ''; if($this->get_config('nested_groups')) { $usergroups = $this->ldap_find_user_groups($extmemberuid); if(count($usergroups) > 0) { foreach ($usergroups as $group) { $group = ldap_filter_addslashes($group); $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')'; } } } // Default return value $courses = array(); // Get all the fields we will want for the potential course creation // as they are light. don't get membership -- potentially a lot of data. $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber')); $fullname = $this->get_config('course_fullname'); $shortname = $this->get_config('course_shortname'); $summary = $this->get_config('course_summary'); if (isset($fullname)) { array_push($ldap_fields_wanted, $fullname); } if (isset($shortname)) { array_push($ldap_fields_wanted, $shortname); } if (isset($summary)) { array_push($ldap_fields_wanted, $summary); } // Define the search pattern if (empty($ldap_search_pattern)) { $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')'; } else { $ldap_search_pattern = '(|' . $ldap_search_pattern . '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')' . ')'; } $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')'; // Get all contexts and look for first matching user $ldap_contexts = explode(';', $ldap_contexts); $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection); foreach ($ldap_contexts as $context) { $context = trim($context); if (empty($context)) { continue; } $ldap_cookie = ''; $servercontrols = array(); $flat_records = array(); do { if ($ldap_pagedresults) { $servercontrols = array(array( 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array( 'size' => $this->config->pagesize, 'cookie' => $ldap_cookie))); } if ($this->get_config('course_search_sub')) { // Use ldap_search to find first user from subtree $ldap_result = @ldap_search($this->ldapconnection, $context, $ldap_search_pattern, $ldap_fields_wanted, 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } else { // Search only in this context $ldap_result = @ldap_list($this->ldapconnection, $context, $ldap_search_pattern, $ldap_fields_wanted, 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } if (!$ldap_result) { continue; } if ($ldap_pagedresults) { // Get next server cookie to know if we'll need to continue searching. $ldap_cookie = ''; // Get next cookie from controls. ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $controls); if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; } } // Check and push results. ldap_get_entries() already // lowercases the attribute index, so there's no need to // use array_change_key_case() later. $records = ldap_get_entries($this->ldapconnection, $ldap_result); // LDAP libraries return an odd array, really. Fix it. for ($c = 0; $c < $records['count']; $c++) { array_push($flat_records, $records[$c]); } // Free some mem unset($records); } while ($ldap_pagedresults && !empty($ldap_cookie)); // If LDAP paged results were used, the current connection must be completely // closed and a new one created, to work without paged results from here on. if ($ldap_pagedresults) { $this->ldap_close(); $this->ldap_connect(); } if (count($flat_records)) { $courses = array_merge($courses, $flat_records); } } return $courses; } /** * Search specified contexts for the specified userid and return the * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper * around ldap_find_userdn(). * * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes). * @return mixed the user dn or false */ protected function ldap_find_userdn($userid) { global $CFG; require_once($CFG->libdir.'/ldaplib.php'); $ldap_contexts = explode(';', $this->get_config('user_contexts')); return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts, $this->userobjectclass, $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub')); } /** * Find the groups a given distinguished name belongs to, both directly * and indirectly via nested groups membership. * * @param string $memberdn distinguished name to search * @return array with member groups' distinguished names (can be emtpy) */ protected function ldap_find_user_groups($memberdn) { $groups = array(); $this->ldap_find_user_groups_recursively($memberdn, $groups); return $groups; } /** * Recursively process the groups the given member distinguished name * belongs to, adding them to the already processed groups array. * * @param string $memberdn distinguished name to search * @param array reference &$membergroups array with already found * groups, where we'll put the newly found * groups. */ protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) { $result = @ldap_read($this->ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute'))); if (!$result) { return; } if ($entry = ldap_first_entry($this->ldapconnection, $result)) { do { $attributes = ldap_get_attributes($this->ldapconnection, $entry); for ($j = 0; $j < $attributes['count']; $j++) { $groups = ldap_get_values_len($this->ldapconnection, $entry, $attributes[$j]); foreach ($groups as $key => $group) { if ($key === 'count') { // Skip the entries count continue; } if(!in_array($group, $membergroups)) { // Only push and recurse if we haven't 'seen' this group before // to prevent loops (MS Active Directory allows them!!). array_push($membergroups, $group); $this->ldap_find_user_groups_recursively($group, $membergroups); } } } } while ($entry = ldap_next_entry($this->ldapconnection, $entry)); } } /** * Given a group name (either a RDN or a DN), get the list of users * belonging to that group. If the group has nested groups, expand all * the intermediate groups and return the full list of users that * directly or indirectly belong to the group. * * @param string $group the group name to search * @param string $memberattibute the attribute that holds the members of the group * @return array the list of users belonging to the group. If $group * is not actually a group, returns array($group). */ protected function ldap_explode_group($group, $memberattribute) { switch ($this->get_config('user_type')) { case 'ad': // $group is already the distinguished name to search. $dn = $group; $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array('objectClass')); $entry = ldap_first_entry($this->ldapconnection, $result); $objectclass = ldap_get_values($this->ldapconnection, $entry, 'objectClass'); if (!in_array('group', $objectclass)) { // Not a group, so return immediately. return array($group); } $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array($memberattribute)); $entry = ldap_first_entry($this->ldapconnection, $result); $members = @ldap_get_values($this->ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning if ($members['count'] == 0) { // There are no members in this group, return nothing. return array(); } unset($members['count']); $users = array(); foreach ($members as $member) { $group_members = $this->ldap_explode_group($member, $memberattribute); $users = array_merge($users, $group_members); } return ($users); break; default: error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap', $this->get_config('user_type_name'))); return array($group); } } /** * Will create the moodle course from the template * course_ext is an array as obtained from ldap -- flattened somewhat * * @param array $course_ext * @param progress_trace $trace * @return mixed false on error, id for the newly created course otherwise. */ function create_course($course_ext, progress_trace $trace) { global $CFG, $DB; require_once("$CFG->dirroot/course/lib.php"); // Override defaults with template course $template = false; if ($this->get_config('template')) { if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) { $template = fullclone(course_get_format($template)->get_course()); unset($template->id); // So we are clear to reinsert the record unset($template->fullname); unset($template->shortname); unset($template->idnumber); } } if (!$template) { $courseconfig = get_config('moodlecourse'); $template = new stdClass(); $template->summary = ''; $template->summaryformat = FORMAT_HTML; $template->format = $courseconfig->format; $template->newsitems = $courseconfig->newsitems; $template->showgrades = $courseconfig->showgrades; $template->showreports = $courseconfig->showreports; $template->maxbytes = $courseconfig->maxbytes; $template->groupmode = $courseconfig->groupmode; $template->groupmodeforce = $courseconfig->groupmodeforce; $template->visible = $courseconfig->visible; $template->lang = $courseconfig->lang; $template->enablecompletion = $courseconfig->enablecompletion; } $course = $template; $course->category = $this->get_config('category'); if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) { $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1); $first = reset($categories); $course->category = $first->id; } // Override with required ext data $course->idnumber = $course_ext[$this->get_config('course_idnumber')][0]; $course->fullname = $course_ext[$this->get_config('course_fullname')][0]; $course->shortname = $course_ext[$this->get_config('course_shortname')][0]; if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) { // We are in trouble! $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true)); return false; } $summary = $this->get_config('course_summary'); if (!isset($summary) || empty($course_ext[$summary][0])) { $course->summary = ''; } else { $course->summary = $course_ext[$this->get_config('course_summary')][0]; } // Check if the shortname already exists if it does - skip course creation. if ($DB->record_exists('course', array('shortname' => $course->shortname))) { $trace->output(get_string('duplicateshortname', 'enrol_ldap', $course)); return false; } $newcourse = create_course($course); return $newcourse->id; } /** * Will update a moodle course with new values from LDAP * A field will be updated only if it is marked to be updated * on sync in plugin settings * * @param object $course * @param array $externalcourse * @param progress_trace $trace * @return bool */ protected function update_course($course, $externalcourse, progress_trace $trace) { global $CFG, $DB; $coursefields = array ('shortname', 'fullname', 'summary'); static $shouldupdate; // Initialize $shouldupdate variable. Set to true if one or more fields are marked for update. if (!isset($shouldupdate)) { $shouldupdate = false; foreach ($coursefields as $field) { $shouldupdate = $shouldupdate || $this->get_config('course_'.$field.'_updateonsync'); } } // If we should not update return immediately. if (!$shouldupdate) { return false; } require_once("$CFG->dirroot/course/lib.php"); $courseupdated = false; $updatedcourse = new stdClass(); $updatedcourse->id = $course->id; // Update course fields if necessary. foreach ($coursefields as $field) { // If field is marked to be updated on sync && field data was changed update it. if ($this->get_config('course_'.$field.'_updateonsync') && isset($externalcourse[$this->get_config('course_'.$field)][0]) && $course->{$field} != $externalcourse[$this->get_config('course_'.$field)][0]) { $updatedcourse->{$field} = $externalcourse[$this->get_config('course_'.$field)][0]; $courseupdated = true; } } if (!$courseupdated) { $trace->output(get_string('courseupdateskipped', 'enrol_ldap', $course)); return false; } // Do not allow empty fullname or shortname. if ((isset($updatedcourse->fullname) && empty($updatedcourse->fullname)) || (isset($updatedcourse->shortname) && empty($updatedcourse->shortname))) { // We are in trouble! $trace->output(get_string('cannotupdatecourse', 'enrol_ldap', $course)); return false; } // Check if the shortname already exists if it does - skip course updating. if (isset($updatedcourse->shortname) && $DB->record_exists('course', array('shortname' => $updatedcourse->shortname))) { $trace->output(get_string('cannotupdatecourse_duplicateshortname', 'enrol_ldap', $course)); return false; } // Finally - update course in DB. update_course($updatedcourse); $trace->output(get_string('courseupdated', 'enrol_ldap', $course)); return true; } /** * Automatic enrol sync executed during restore. * Useful for automatic sync by course->idnumber or course category. * @param stdClass $course course record */ public function restore_sync_course($course) { // TODO: this can not work because restore always nukes the course->idnumber, do not ask me why (MDL-37312) // NOTE: for now restore does not do any real logging yet, let's do the same here... $trace = new error_log_progress_trace(); $this->sync_enrolments($trace, $course->id); } /** * Restore instance and map settings. * * @param restore_enrolments_structure_step $step * @param stdClass $data * @param stdClass $course * @param int $oldid */ public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) { global $DB; // There is only 1 ldap enrol instance per course. if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'ldap'), 'id')) { $instance = reset($instances); $instanceid = $instance->id; } else { $instanceid = $this->add_instance($course, (array)$data); } $step->set_mapping('enrol', $oldid, $instanceid); } /** * Restore user enrolment. * * @param restore_enrolments_structure_step $step * @param stdClass $data * @param stdClass $instance * @param int $oldinstancestatus * @param int $userid */ public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) { global $DB; if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL) { // Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers. } else if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_KEEP) { if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) { $this->enrol_user($instance, $userid, null, 0, 0, $data->status); } } else { if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) { $this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED); } } } /** * Restore role assignment. * * @param stdClass $instance * @param int $roleid * @param int $userid * @param int $contextid */ public function restore_role_assignment($instance, $roleid, $userid, $contextid) { global $DB; if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES) { // Skip any roles restore, they should be already synced automatically. return; } // Just restore every role. if ($DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) { role_assign($roleid, $userid, $contextid, 'enrol_'.$instance->enrol, $instance->id); } } } PK\##settingslib.phpnu[. /** * LDAP enrolment plugin admin setting classes * * @package enrol_ldap * @author Iñaki Arenaza * @copyright 2010 Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); class admin_setting_configtext_trim_lower extends admin_setting_configtext { /* @var boolean whether to lowercase the value or not before writing in to the db */ private $lowercase; /** @var bool To store enable/disabled status of the input field. */ protected $enabled; /** * Constructor: uses parent::__construct * * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins. * @param string $visiblename localised * @param string $description long localised info * @param string $defaultsetting default value for the setting * @param boolean $lowercase if true, lowercase the value before writing it to the db. * @param boolean $enabled if true, the input field is enabled, otherwise it's disabled. */ public function __construct($name, $visiblename, $description, $defaultsetting, $lowercase=false, $enabled=true) { $this->lowercase = $lowercase; $this->enabled = $enabled; parent::__construct($name, $visiblename, $description, $defaultsetting); } /** * Saves the setting(s) provided in $data * * @param array $data An array of data, if not array returns empty str * @return mixed empty string on useless data or success, error string if failed */ public function write_setting($data) { if ($this->paramtype === PARAM_INT and $data === '') { // do not complain if '' used instead of 0 $data = 0; } // $data is a string $validated = $this->validate($data); if ($validated !== true) { return $validated; } if ($this->lowercase) { $data = core_text::strtolower($data); } if (!$this->enabled) { return ''; } return ($this->config_write($this->name, trim($data)) ? '' : get_string('errorsetting', 'admin')); } } class admin_setting_ldap_rolemapping extends admin_setting { /** * Constructor: uses parent::__construct * * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins. * @param string $visiblename localised * @param string $description long localised info * @param string $defaultsetting default value for the setting (actually unused) */ public function __construct($name, $visiblename, $description, $defaultsetting) { parent::__construct($name, $visiblename, $description, $defaultsetting); } /** * Returns the current setting if it is set * * @return mixed null if null, else an array */ public function get_setting() { $roles = role_fix_names(get_all_roles()); $result = array(); foreach ($roles as $role) { $contexts = $this->config_read('contexts_role'.$role->id); $memberattribute = $this->config_read('memberattribute_role'.$role->id); $result[] = array('id' => $role->id, 'name' => $role->localname, 'contexts' => $contexts, 'memberattribute' => $memberattribute); } return $result; } /** * Saves the setting(s) provided in $data * * @param array $data An array of data, if not array returns empty str * @return mixed empty string on useless data or success, error string if failed */ public function write_setting($data) { if(!is_array($data)) { return ''; // ignore it } $result = ''; foreach ($data as $roleid => $data) { if (!$this->config_write('contexts_role'.$roleid, trim($data['contexts']))) { $return = get_string('errorsetting', 'admin'); } if (!$this->config_write('memberattribute_role'.$roleid, core_text::strtolower(trim($data['memberattribute'])))) { $return = get_string('errorsetting', 'admin'); } } return $result; } /** * Returns XHTML field(s) as required by choices * * Relies on data being an array should data ever be another valid vartype with * acceptable value this may cause a warning/error * if (!is_array($data)) would fix the problem * * @todo Add vartype handling to ensure $data is an array * * @param array $data An array of checked values * @param string $query * @return string XHTML field */ public function output_html($data, $query='') { $return = html_writer::start_tag('div', array('style' =>'float:left; width:auto; margin-right: 0.5em;')); $return .= html_writer::tag('div', get_string('roles', 'role'), array('style' => 'height: 2em;')); foreach ($data as $role) { $return .= html_writer::tag('div', s($role['name']), array('style' => 'height: 2em;')); } $return .= html_writer::end_tag('div'); $return .= html_writer::start_tag('div', array('style' => 'float:left; width:auto; margin-right: 0.5em;')); $return .= html_writer::tag('div', get_string('contexts', 'enrol_ldap'), array('style' => 'height: 2em;')); foreach ($data as $role) { $contextid = $this->get_id().'['.$role['id'].'][contexts]'; $contextname = $this->get_full_name().'['.$role['id'].'][contexts]'; $return .= html_writer::start_tag('div', array('style' => 'height: 2em;')); $return .= html_writer::label(get_string('role_mapping_context', 'enrol_ldap', $role['name']), $contextid, false, array('class' => 'accesshide')); $attrs = array('type' => 'text', 'size' => '40', 'id' => $contextid, 'name' => $contextname, 'value' => s($role['contexts']), 'class' => 'text-ltr'); $return .= html_writer::empty_tag('input', $attrs); $return .= html_writer::end_tag('div'); } $return .= html_writer::end_tag('div'); $return .= html_writer::start_tag('div', array('style' => 'float:left; width:auto; margin-right: 0.5em;')); $return .= html_writer::tag('div', get_string('memberattribute', 'enrol_ldap'), array('style' => 'height: 2em;')); foreach ($data as $role) { $memberattrid = $this->get_id().'['.$role['id'].'][memberattribute]'; $memberattrname = $this->get_full_name().'['.$role['id'].'][memberattribute]'; $return .= html_writer::start_tag('div', array('style' => 'height: 2em;')); $return .= html_writer::label(get_string('role_mapping_attribute', 'enrol_ldap', $role['name']), $memberattrid, false, array('class' => 'accesshide')); $attrs = array('type' => 'text', 'size' => '15', 'id' => $memberattrid, 'name' => $memberattrname, 'value' => s($role['memberattribute']), 'class' => 'text-ltr'); $return .= html_writer::empty_tag('input', $attrs); $return .= html_writer::end_tag('div'); } $return .= html_writer::end_tag('div'); $return .= html_writer::tag('div', '', array('style' => 'clear:both;')); return format_admin_setting($this, $this->visiblename, $return, $this->description, true, '', '', $query); } } /** * Class implements new specialized setting for course categories that are loaded * only when required * @author Darko Miletic * */ class enrol_ldap_admin_setting_category extends admin_setting_configselect { public function __construct($name, $visiblename, $description) { parent::__construct($name, $visiblename, $description, 1, null); } public function load_choices() { if (is_array($this->choices)) { return true; } $this->choices = core_course_category::make_categories_list('', 0, ' / '); return true; } } PK\h9  classes/task/sync_enrolments.phpnu[. /** * Sync enrolments task * @package enrol_ldap * @author Guy Thomas * @copyright Copyright (c) 2017 Blackboard Inc. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_ldap\task; defined('MOODLE_INTERNAL') || die(); /** * Class sync_enrolments * @package enrol_ldap * @author Guy Thomas * @copyright Copyright (c) 2017 Blackboard Inc. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_enrolments extends \core\task\scheduled_task { /** * Name for this task. * * @return string */ public function get_name() { return get_string('syncenrolmentstask', 'enrol_ldap'); } /** * Run task for synchronising users. */ public function execute() { if (!enrol_is_enabled('ldap')) { mtrace(get_string('pluginnotenabled', 'enrol_ldap')); exit(0); // Note, exit with success code, this is not an error - it's just disabled. } /** @var \enrol_ldap_plugin $enrol */ $enrol = enrol_get_plugin('ldap'); $trace = new \text_progress_trace(); // Update enrolments -- these handlers should autocreate courses if required. $enrol->sync_enrolments($trace); } } PK\]cctests/ldap_test.phpnu[. namespace enrol_ldap; /** * LDAP enrolment plugin tests. * * NOTE: in order to execute this test you need to set up * OpenLDAP server with core, cosine, nis and internet schemas * and add configuration constants to config.php or phpunit.xml configuration file: * * define('TEST_ENROL_LDAP_HOST_URL', 'ldap://127.0.0.1'); * define('TEST_ENROL_LDAP_BIND_DN', 'cn=someuser,dc=example,dc=local'); * define('TEST_ENROL_LDAP_BIND_PW', 'somepassword'); * define('TEST_ENROL_LDAP_DOMAIN', 'dc=example,dc=local'); * * @package enrol_ldap * @category test * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class ldap_test extends \advanced_testcase { /** * Data provider for enrol_ldap tests * * Used to ensure that all the paged stuff works properly, irrespectively * of the pagesize configured (that implies all the chunking and paging * built in the plugis is doing its work consistently). Both searching and * not searching within subcontexts. * * @return array[] */ public static function enrol_ldap_provider(): array { $pagesizes = [1, 3, 5, 1000]; $subcontexts = [0, 1]; $combinations = []; foreach ($pagesizes as $pagesize) { foreach ($subcontexts as $subcontext) { $combinations["pagesize {$pagesize}, subcontexts {$subcontext}"] = [$pagesize, $subcontext]; } } return $combinations; } /** * General enrol_ldap testcase * * @dataProvider enrol_ldap_provider * @param int $pagesize Value to be configured in settings controlling page size. * @param int $subcontext Value to be configured in settings controlling searching in subcontexts. */ public function test_enrol_ldap(int $pagesize, int $subcontext): void { global $CFG, $DB; if (!extension_loaded('ldap')) { $this->markTestSkipped('LDAP extension is not loaded.'); } $this->resetAfterTest(); require_once($CFG->dirroot.'/enrol/ldap/lib.php'); require_once($CFG->libdir.'/ldaplib.php'); if (!defined('TEST_ENROL_LDAP_HOST_URL') or !defined('TEST_ENROL_LDAP_BIND_DN') or !defined('TEST_ENROL_LDAP_BIND_PW') or !defined('TEST_ENROL_LDAP_DOMAIN')) { $this->markTestSkipped('External LDAP test server not configured.'); } // Make sure we can connect the server. $debuginfo = ''; if (!$connection = ldap_connect_moodle(TEST_ENROL_LDAP_HOST_URL, 3, 'rfc2307', TEST_ENROL_LDAP_BIND_DN, TEST_ENROL_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) { $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo); } $this->enable_plugin(); // Create new empty test container. $topdn = 'dc=moodletest,'.TEST_ENROL_LDAP_DOMAIN; $this->recursive_delete($connection, TEST_ENROL_LDAP_DOMAIN, 'dc=moodletest'); $o = array(); $o['objectClass'] = array('dcObject', 'organizationalUnit'); $o['dc'] = 'moodletest'; $o['ou'] = 'MOODLETEST'; if (!ldap_add($connection, 'dc=moodletest,'.TEST_ENROL_LDAP_DOMAIN, $o)) { $this->markTestSkipped('Can not create test LDAP container.'); } // Configure enrol plugin. /** @var \enrol_ldap_plugin $enrol */ $enrol = enrol_get_plugin('ldap'); $enrol->set_config('host_url', TEST_ENROL_LDAP_HOST_URL); $enrol->set_config('start_tls', 0); $enrol->set_config('ldap_version', 3); $enrol->set_config('ldapencoding', 'utf-8'); $enrol->set_config('pagesize', $pagesize); $enrol->set_config('bind_dn', TEST_ENROL_LDAP_BIND_DN); $enrol->set_config('bind_pw', TEST_ENROL_LDAP_BIND_PW); $enrol->set_config('course_search_sub', $subcontext); $enrol->set_config('memberattribute_isdn', 0); $enrol->set_config('user_contexts', ''); $enrol->set_config('user_search_sub', 0); $enrol->set_config('user_type', 'rfc2307'); $enrol->set_config('opt_deref', LDAP_DEREF_NEVER); $enrol->set_config('objectclass', '(objectClass=posixGroup)'); $enrol->set_config('course_idnumber', 'cn'); $enrol->set_config('course_shortname', 'cn'); $enrol->set_config('course_fullname', 'cn'); $enrol->set_config('course_summary', ''); $enrol->set_config('ignorehiddencourses', 0); $enrol->set_config('nested_groups', 0); $enrol->set_config('autocreate', 0); $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_KEEP); $roles = get_all_roles(); foreach ($roles as $role) { $enrol->set_config('contexts_role'.$role->id, ''); $enrol->set_config('memberattribute_role'.$role->id, ''); } // Create group for teacher enrolments. $teacherrole = $DB->get_record('role', array('shortname'=>'teacher')); $this->assertNotEmpty($teacherrole); $o = array(); $o['objectClass'] = array('organizationalUnit'); $o['ou'] = 'teachers'; ldap_add($connection, 'ou=teachers,'.$topdn, $o); $enrol->set_config('contexts_role'.$teacherrole->id, 'ou=teachers,'.$topdn); $enrol->set_config('memberattribute_role'.$teacherrole->id, 'memberuid'); // Create group for student enrolments. $studentrole = $DB->get_record('role', array('shortname'=>'student')); $this->assertNotEmpty($studentrole); $o = array(); $o['objectClass'] = array('organizationalUnit'); $o['ou'] = 'students'; ldap_add($connection, 'ou=students,'.$topdn, $o); $enrol->set_config('contexts_role'.$studentrole->id, 'ou=students,'.$topdn); $enrol->set_config('memberattribute_role'.$studentrole->id, 'memberuid'); // Create some users and courses. $user1 = $this->getDataGenerator()->create_user(array('idnumber'=>'user1', 'username'=>'user1')); $user2 = $this->getDataGenerator()->create_user(array('idnumber'=>'user2', 'username'=>'user2')); $user3 = $this->getDataGenerator()->create_user(array('idnumber'=>'user3', 'username'=>'user3')); $user4 = $this->getDataGenerator()->create_user(array('idnumber'=>'user4', 'username'=>'user4')); $user5 = $this->getDataGenerator()->create_user(array('idnumber'=>'user5', 'username'=>'user5')); $user6 = $this->getDataGenerator()->create_user(array('idnumber'=>'user6', 'username'=>'user6')); $course1 = $this->getDataGenerator()->create_course(array('idnumber'=>'course1', 'shortname'=>'course1')); $course2 = $this->getDataGenerator()->create_course(array('idnumber'=>'course2', 'shortname'=>'course2')); $course3 = $this->getDataGenerator()->create_course(array('idnumber'=>'course3', 'shortname'=>'course3')); // Set up some ldap data. $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; $o['memberUid'] = array('user1', 'user2', 'user3', 'userx'); ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '2'; $o['memberUid'] = array('user5'); ldap_add($connection, 'cn='.$o['cn'].',ou=teachers,'.$topdn, $o); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course2'; $o['gidNumber'] = '3'; $o['memberUid'] = array('user1', 'user2', 'user3', 'user4'); ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course4'; $o['gidNumber'] = '4'; $o['memberUid'] = array('user1', 'user2'); ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course4'; $o['gidNumber'] = '5'; $o['memberUid'] = array('user5', 'user6'); ldap_add($connection, 'cn='.$o['cn'].',ou=teachers,'.$topdn, $o); // Test simple test without creation. $this->assertEquals(0, $DB->count_records('user_enrolments')); $this->assertEquals(0, $DB->count_records('role_assignments')); $this->assertEquals(4, $DB->count_records('course')); $enrol->sync_enrolments(new \null_progress_trace()); $this->assertEquals(8, $DB->count_records('user_enrolments')); $this->assertEquals(8, $DB->count_records('role_assignments')); $this->assertEquals(4, $DB->count_records('course')); $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id); $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id); $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id); $this->assertIsEnrolled($course1->id, $user5->id, $teacherrole->id); $this->assertIsEnrolled($course2->id, $user1->id, $studentrole->id); $this->assertIsEnrolled($course2->id, $user2->id, $studentrole->id); $this->assertIsEnrolled($course2->id, $user3->id, $studentrole->id); $this->assertIsEnrolled($course2->id, $user4->id, $studentrole->id); // Test course creation. $enrol->set_config('autocreate', 1); $enrol->sync_enrolments(new \null_progress_trace()); $this->assertEquals(12, $DB->count_records('user_enrolments')); $this->assertEquals(12, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $course4 = $DB->get_record('course', array('idnumber'=>'course4'), '*', MUST_EXIST); $this->assertIsEnrolled($course4->id, $user1->id, $studentrole->id); $this->assertIsEnrolled($course4->id, $user2->id, $studentrole->id); $this->assertIsEnrolled($course4->id, $user5->id, $teacherrole->id); $this->assertIsEnrolled($course4->id, $user6->id, $teacherrole->id); // Test unenrolment. ldap_delete($connection, 'cn=course1,ou=students,'.$topdn); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_KEEP); $enrol->sync_enrolments(new \null_progress_trace()); $this->assertEquals(12, $DB->count_records('user_enrolments')); $this->assertEquals(12, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPEND); $enrol->sync_enrolments(new \null_progress_trace()); $this->assertEquals(12, $DB->count_records('user_enrolments')); $this->assertEquals(12, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_SUSPENDED); $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id, ENROL_USER_SUSPENDED); $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id, ENROL_USER_SUSPENDED); ldap_delete($connection, 'cn=course1,ou=students,'.$topdn); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; $o['memberUid'] = array('user1', 'user2', 'user3'); ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $enrol->sync_enrolments(new \null_progress_trace()); $this->assertEquals(12, $DB->count_records('user_enrolments')); $this->assertEquals(12, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE); $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id, ENROL_USER_ACTIVE); $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id, ENROL_USER_ACTIVE); ldap_delete($connection, 'cn=course1,ou=students,'.$topdn); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES); $enrol->sync_enrolments(new \null_progress_trace()); $this->assertEquals(12, $DB->count_records('user_enrolments')); $this->assertEquals(9, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsEnrolled($course1->id, $user1->id, 0, ENROL_USER_SUSPENDED); $this->assertIsEnrolled($course1->id, $user2->id, 0, ENROL_USER_SUSPENDED); $this->assertIsEnrolled($course1->id, $user3->id, 0, ENROL_USER_SUSPENDED); ldap_delete($connection, 'cn=course1,ou=students,'.$topdn); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; $o['memberUid'] = array('user1', 'user2', 'user3'); ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $enrol->sync_enrolments(new \null_progress_trace()); $this->assertEquals(12, $DB->count_records('user_enrolments')); $this->assertEquals(12, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE); $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id, ENROL_USER_ACTIVE); $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id, ENROL_USER_ACTIVE); ldap_delete($connection, 'cn=course1,ou=students,'.$topdn); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL); $enrol->sync_enrolments(new \null_progress_trace()); $this->assertEquals(9, $DB->count_records('user_enrolments')); $this->assertEquals(9, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsNotEnrolled($course1->id, $user1->id); $this->assertIsNotEnrolled($course1->id, $user2->id); $this->assertIsNotEnrolled($course1->id, $user3->id); // Individual user enrolments- ldap_delete($connection, 'cn=course1,ou=students,'.$topdn); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; $o['memberUid'] = array('user1', 'user2', 'user3'); ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $enrol->sync_user_enrolments($user1); $this->assertEquals(10, $DB->count_records('user_enrolments')); $this->assertEquals(10, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE); ldap_delete($connection, 'cn=course1,ou=students,'.$topdn); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; $o['memberUid'] = array('user2', 'user3'); ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_KEEP); $enrol->sync_user_enrolments($user1); $this->assertEquals(10, $DB->count_records('user_enrolments')); $this->assertEquals(10, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE); $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPEND); $enrol->sync_user_enrolments($user1); $this->assertEquals(10, $DB->count_records('user_enrolments')); $this->assertEquals(10, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_SUSPENDED); ldap_delete($connection, 'cn=course1,ou=students,'.$topdn); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; $o['memberUid'] = array('user1', 'user2', 'user3'); ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $enrol->sync_user_enrolments($user1); $this->assertEquals(10, $DB->count_records('user_enrolments')); $this->assertEquals(10, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE); ldap_delete($connection, 'cn=course1,ou=students,'.$topdn); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; $o['memberUid'] = array('user2', 'user3'); ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES); $enrol->sync_user_enrolments($user1); $this->assertEquals(10, $DB->count_records('user_enrolments')); $this->assertEquals(9, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsEnrolled($course1->id, $user1->id, 0, ENROL_USER_SUSPENDED); ldap_delete($connection, 'cn=course1,ou=students,'.$topdn); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; $o['memberUid'] = array('user1', 'user2', 'user3'); ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $enrol->sync_user_enrolments($user1); $this->assertEquals(10, $DB->count_records('user_enrolments')); $this->assertEquals(10, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE); ldap_delete($connection, 'cn=course1,ou=students,'.$topdn); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'course1'; $o['gidNumber'] = '1'; $o['memberUid'] = array('user2', 'user3'); ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o); $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL); $enrol->sync_user_enrolments($user1); $this->assertEquals(9, $DB->count_records('user_enrolments')); $this->assertEquals(9, $DB->count_records('role_assignments')); $this->assertEquals(5, $DB->count_records('course')); $this->assertIsNotEnrolled($course1->id, $user1->id); $this->recursive_delete($connection, TEST_ENROL_LDAP_DOMAIN, 'dc=moodletest'); ldap_close($connection); // NOTE: multiple roles in one course is not supported, sorry } public function assertIsEnrolled($courseid, $userid, $roleid, $status=null) { global $DB; $context = \context_course::instance($courseid); $instance = $DB->get_record('enrol', array('courseid'=>$courseid, 'enrol'=>'ldap')); $this->assertNotEmpty($instance); $ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid)); $this->assertNotEmpty($ue); if (isset($status)) { $this->assertEquals($status, $ue->status); } if ($roleid) { $this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context->id, 'userid'=>$userid, 'roleid'=>$roleid, 'component'=>'enrol_ldap'))); } else { $this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context->id, 'userid'=>$userid, 'component'=>'enrol_ldap'))); } } public function assertIsNotEnrolled($courseid, $userid) { $context = \context_course::instance($courseid); $this->assertFalse(is_enrolled($context, $userid)); } protected function enable_plugin() { $enabled = enrol_get_plugins(true); $enabled['ldap'] = true; $enabled = array_keys($enabled); set_config('enrol_plugins_enabled', implode(',', $enabled)); } protected function disable_plugin() { $enabled = enrol_get_plugins(true); unset($enabled['ldap']); $enabled = array_keys($enabled); set_config('enrol_plugins_enabled', implode(',', $enabled)); } protected function recursive_delete($connection, $dn, $filter) { if ($res = ldap_list($connection, $dn, $filter, array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); if ($info['count'] > 0) { if ($res = ldap_search($connection, "$filter,$dn", 'cn=*', array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); foreach ($info as $i) { if (isset($i['dn'])) { ldap_delete($connection, $i['dn']); } } } if ($res = ldap_search($connection, "$filter,$dn", 'ou=*', array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); foreach ($info as $i) { if (isset($i['dn']) and $info[0]['dn'] != $i['dn']) { ldap_delete($connection, $i['dn']); } } } ldap_delete($connection, "$filter,$dn"); } } } /** * Test that normalisation of the use objectclass is completed successfully. * * @dataProvider objectclass_fetch_provider * @param string $usertype The supported user type * @param string $expected The expected filter value */ public function test_objectclass_fetch($usertype, $expected): void { $this->resetAfterTest(); // Set the user type - this must be performed before the plugin is instantiated. set_config('user_type', $usertype, 'enrol_ldap'); // Fetch the plugin. $instance = enrol_get_plugin('ldap'); // Use reflection to sneak a look at the plugin. $rc = new \ReflectionClass('enrol_ldap_plugin'); $rcp = $rc->getProperty('userobjectclass'); // Fetch the current userobjectclass value. $value = $rcp->getValue($instance); $this->assertEquals($expected, $value); } /** * Data provider for the test_objectclass_fetch testcase. * * @return array of testcases. */ public static function objectclass_fetch_provider(): array { return array( // This is the list of values from ldap_getdefaults() normalised. 'edir' => array( 'edir', '(objectClass=user)' ), 'rfc2307' => array( 'rfc2307', '(objectClass=posixaccount)' ), 'rfc2307bis' => array( 'rfc2307bis', '(objectClass=posixaccount)' ), 'samba' => array( 'samba', '(objectClass=sambasamaccount)' ), 'ad' => array( 'ad', '(samaccounttype=805306368)' ), 'default' => array( 'default', '(objectClass=*)' ), ); } } PK1B\4  db/install.phpnu[PK1B\qg Hdb/tasks.phpnu[PK1B\_4]]W db/upgrade.phpnu[PK1B\5BBlang/en/auth_ldap.phpnu[PK1B\Ӷd ERversion.phpnu[PK1B\|hgQ775QWclasses/admin_setting_special_contexts_configtext.phpnu[PK1B\5O^classes/privacy/provider.phpnu[PK1B\z1!eclasses/admin_setting_special_ntlm_configtext.phpnu[PK1B\8|lclasses/task/sync_task.phpnu[PK1B\Epuclasses/task/sync_roles.phpnu[PK1B\*'}classes/task/asynchronous_sync_task.phpnu[PK1B\^=ZYclasses/adminpresets/adminpresets_auth_ldap_admin_setting_special_contexts_configtext.phpnu[PK1B\6classes/admin_setting_special_lowercase_configtext.phpnu[PK1B\ upgrade.txtnu[PK1B\ locallib.phpnu[PK1B\QnfJJ README-LDAPnu[PK1B\zT} wcli/sync_users.phpnu[PK1B\TfTfwtests/auth_ldap_test.phpnu[PK1B\#YCntlmsso_attempt.phpnu[PK1B\ƫÆVntlmsso_finish.phpnu[PK1B\`||auth.phpnu[PK1B\׆ntlmsso_magic.phpnu[PK1B\t<## ^settings.phpnu[PK\4lA ydb/access.phpnu[PK\տ//`lang/en/enrol_ldap.phpnu[PK\+..elib.phpnu[PK\##settingslib.phpnu[PK\h9   classes/task/sync_enrolments.phpnu[PK\]ccAtests/ldap_test.phpnu[PK j