updated plugin AuthLDAP version 2.6.0

This commit is contained in:
KawaiiPunk 2024-02-08 12:31:29 +00:00 committed by Gitium
parent 50bf15833c
commit d12aa8efdc
11 changed files with 523 additions and 21 deletions

View File

@ -3,4 +3,6 @@
<phar name="phpunit" version="^9.5.21" installed="9.5.21" location="./tools/phpunit" copy="true"/> <phar name="phpunit" version="^9.5.21" installed="9.5.21" location="./tools/phpunit" copy="true"/>
<phar name="phpcs" version="^3.7.1" installed="3.7.1" location="./tools/phpcs" copy="true"/> <phar name="phpcs" version="^3.7.1" installed="3.7.1" location="./tools/phpcs" copy="true"/>
<phar name="phpcbf" version="^3.7.1" installed="3.7.1" location="./tools/phpcbf" copy="true"/> <phar name="phpcbf" version="^3.7.1" installed="3.7.1" location="./tools/phpcbf" copy="true"/>
<phar name="behat/behat" version="^3.13.0" installed="3.13.0" location="./tools/behat" copy="true"/>
<phar name="wp-cli/wp-cli" version="^2.9.0" installed="2.9.0" location="./tools/wp-cli" copy="true"/>
</phive> </phive>

View File

@ -4,7 +4,7 @@
Plugin Name: AuthLDAP Plugin Name: AuthLDAP
Plugin URI: https://github.com/heiglandreas/authLdap Plugin URI: https://github.com/heiglandreas/authLdap
Description: This plugin allows you to use your existing LDAP as authentication base for WordPress Description: This plugin allows you to use your existing LDAP as authentication base for WordPress
Version: 2.5.9 Version: 2.6.0
Author: Andreas Heigl <andreas@heigl.org> Author: Andreas Heigl <andreas@heigl.org>
Author URI: http://andreas.heigl.org Author URI: http://andreas.heigl.org
License: MIT License: MIT
@ -373,10 +373,10 @@ function authLdap_login($user, $username, $password, $already_md5 = false)
// we only need this if either LDAP groups are disabled or // we only need this if either LDAP groups are disabled or
// if the WordPress role of the user overrides LDAP groups // if the WordPress role of the user overrides LDAP groups
if (!$authLDAPGroupEnable || !$authLDAPGroupOverUser) { if (!$authLDAPGroupEnable || $authLDAPGroupOverUser) {
$role = authLdap_user_role($uid); $userRoles = authLdap_user_role($uid);
if ($role !== '') { if ($userRoles !== []) {
$roles[] = $role; $roles = array_merge($roles, $userRoles);
} }
// TODO, this needs to be revised, it seems, like authldap is taking only the first role // TODO, this needs to be revised, it seems, like authldap is taking only the first role
// even if in WP there are assigned multiple. // even if in WP there are assigned multiple.
@ -430,23 +430,23 @@ function authLdap_login($user, $username, $password, $already_md5 = false)
$user_info['user_nicename'] = ''; $user_info['user_nicename'] = '';
// first name // first name
if (isset($attribs[0][strtolower($authLDAPNameAttr)][0])) { if (isset($attribs[0][strtolower((string) $authLDAPNameAttr)][0])) {
$user_info['first_name'] = $attribs[0][strtolower($authLDAPNameAttr)][0]; $user_info['first_name'] = $attribs[0][strtolower((string) $authLDAPNameAttr)][0];
} }
// last name // last name
if (isset($attribs[0][strtolower($authLDAPSecName)][0])) { if (isset($attribs[0][strtolower((string) $authLDAPSecName)][0])) {
$user_info['last_name'] = $attribs[0][strtolower($authLDAPSecName)][0]; $user_info['last_name'] = $attribs[0][strtolower((string) $authLDAPSecName)][0];
} }
// mail address // mail address
if (isset($attribs[0][strtolower($authLDAPMailAttr)][0])) { if (isset($attribs[0][strtolower((string) $authLDAPMailAttr)][0])) {
$user_info['user_email'] = $attribs[0][strtolower($authLDAPMailAttr)][0]; $user_info['user_email'] = $attribs[0][strtolower((string) $authLDAPMailAttr)][0];
} }
// website // website
if (isset($attribs[0][strtolower($authLDAPWebAttr)][0])) { if (isset($attribs[0][strtolower((string) $authLDAPWebAttr)][0])) {
$user_info['user_url'] = $attribs[0][strtolower($authLDAPWebAttr)][0]; $user_info['user_url'] = $attribs[0][strtolower((string) $authLDAPWebAttr)][0];
} }
// display name, nickname, nicename // display name, nickname, nicename
if (array_key_exists('first_name', $user_info)) { if (array_key_exists('first_name', $user_info)) {
@ -556,20 +556,20 @@ function authLdap_get_uid($username)
* Returns empty string if not found. * Returns empty string if not found.
* *
* @param int $uid wordpress user id * @param int $uid wordpress user id
* @return string role, empty if none found * @return array roles, empty if none found
*/ */
function authLdap_user_role($uid) function authLdap_user_role($uid)
{ {
global $wpdb, $wp_roles; global $wpdb, $wp_roles;
if (!$uid) { if (!$uid) {
return ''; return [];
} }
/** @var array<string, bool> $usercapabilities */ /** @var array<string, bool> $usercapabilities */
$usercapabilities = get_user_meta($uid, "{$wpdb->prefix}capabilities", true); $usercapabilities = get_user_meta($uid, "{$wpdb->prefix}capabilities", true);
if (!is_array($usercapabilities)) { if (!is_array($usercapabilities)) {
return ''; return [];
} }
/** @var array<string, array{name: string, capabilities: array<mixed>} $editable_roles */ /** @var array<string, array{name: string, capabilities: array<mixed>} $editable_roles */
@ -578,10 +578,10 @@ function authLdap_user_role($uid)
// By using this approach we are now using the order of the roles from the WP_Roles object // By using this approach we are now using the order of the roles from the WP_Roles object
// and not from the capabilities any more. // and not from the capabilities any more.
$userroles = array_keys(array_intersect_key($editable_roles, $usercapabilities)); $userroles = array_keys(array_intersect_key($editable_roles, $usercapabilities));
$role = ($userroles !== []) ? $userroles[0] : '';
authLdap_debug("Existing user's role: {$role}"); authLdap_debug(sprintf("Existing user's roles: %s", implode(', ', $userroles)));
return $role;
return $userroles;
} }
/** /**

View File

@ -0,0 +1 @@
default:

View File

@ -0,0 +1,289 @@
<?php
declare(strict_types=1);
use Behat\Behat\Tester\Exception\PendingException;
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Psr7\Response;
use Org_Heigl\AuthLdap\OptionFactory;
use Org_Heigl\AuthLdap\Options;
use Webmozart\Assert\Assert;
class FeatureContext implements Context
{
private ?Response $res = null;
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
public function __construct()
{
exec('wp --allow-root core install --url=localhost --title=Example --admin_user=localadmin --admin_password=P@ssw0rd --admin_email=info@example.com');
exec('wp --allow-root plugin activate authldap');
}
/**
* @Given a default configuration
*/
public function aDefaultConfiguration()
{
$options = new Options();
$options->set(Options::URI, 'ldap://cn=admin,dc=example,dc=org:insecure@openldap:389/dc=example,dc=org');
$options->set(Options::ENABLED, true);
$options->set(Options::FILTER, 'uid=%1$s');
$options->set(Options::DEFAULT_ROLE, 'subscriber');
$options->set(Options::DEBUG, true);
$options->set(Options::NAME_ATTR, 'cn');
exec(sprintf(
'wp --allow-root option update --format=json authLDAPOptions \'%1$s\'',
json_encode($options->toArray())
));
}
/**
* @Given configuration value :arg1 is set to :arg2
*/
public function configurationValueIsSetTo($arg1, $arg2)
{
exec(sprintf(
'wp --allow-root option patch update authLDAPOptions %1$s %2$s --format=json',
$arg1,
"'" . json_encode($arg2) . "'"
));
}
/**
* @Given an LDAP user :arg1 with name :arg2, password :arg3 and email :arg4 exists
*/
public function anLdapUserWithNamePasswordAndEmailExists($arg1, $arg2, $arg3, $arg4)
{
exec(sprintf(
'ldapadd -x -H %1$s -D "%2$s" -w %3$s <<LDIF
%4$s
LDIF',
'ldap://openldap',
'cn=admin,dc=example,dc=org',
'insecure',
<<<LDIF
dn: uid=$arg1,dc=example,dc=org
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
objectClass: simpleSecurityObject
uid: $arg1
cn: $arg2
sn: $arg2
userPassword: $arg3
mail: $arg4
LDIF
));
exec(sprintf(
'ldappasswd -H ldap://openldap:389 -x -D "uid=admin,dc=example,dc=org" -w "%3$s" -s "%2$s" "uid=%1$s,dc=example,dc=org"',
$arg1,
$arg3,
'insecure'
));
}
/**
* @Given an LDAP group :arg1 exists
*/
public function anLdapGroupExists($arg1)
{
exec(sprintf(
'ldapadd -x -H %1$s -D "%2$s" -w %3$s <<LDIF
%4$s
LDIF',
'ldap://openldap',
'cn=admin,dc=example,dc=org',
'insecure',
<<<LDIF
dn: cn=$arg1,dc=example,dc=org
objectClass: groupOfUniqueNames
cn: $arg1
uniqueMember: cn=admin,dc=example,dc=org
LDIF
));
}
/**
* @Given a WordPress user :arg1 with name :arg2 and email :arg3 exists
*/
public function aWordpressUserWithNameAndEmailExists($arg1, $arg2, $arg3)
{
exec(sprintf(
'wp --allow-root user create %1$s %3$s --display_name=%2$s --porcelain',
$arg1,
$arg2,
$arg3
));
}
/**
* @Given a WordPress role :arg1 exists
*/
public function aWordpressRoleExists($arg1)
{
exec(sprintf(
'wp --allow-root role create %1$s %1$s',
$arg1,
));
}
/**
* @Given WordPress user :arg1 has role :arg2
*/
public function wordpressUserHasRole($arg1, $arg2)
{
exec(sprintf(
'wp --allow-root user add-role %1$s %2$s',
$arg1,
$arg2
));
}
/**
* @When LDAP user :arg1 logs in with password :arg2
*/
public function ldapUserLogsInWithPassword($arg1, $arg2)
{
// curl -i 'http://localhost/wp-login.php' -X POST -H 'Cookie: wordpress_test_cookie=test' --data-raw 'log=localadmin&pwd=P%40ssw0rd'
$client = new Client();
$this->res = $client->post('http://wp/wp-login.php', [
'cookies' => CookieJar::fromArray([
'wordpress_test_cookie' => 'test',
'XDEBUG_SESSION' => 'PHPSTORM',
], 'http://wp'),
'form_params' => [
'log' => $arg1,
'pwd' => $arg2,
],
'allow_redirects' => false
]);
}
/**
* @Then the login suceeds
*/
public function theLoginSuceeds()
{
Assert::isInstanceOf($this->res, Response::class);
Assert::eq( $this->res->getStatusCode(), 302);
Assert::startsWith($this->res->getHeader('Location')[0], 'http://localhost/wp-admin');
}
/**
* @Then a new WordPress user :arg1 was created with name :arg2 and email :arg3
*/
public function aNewWordpressUserWasCreatedWithNameAndEmail($arg1, $arg2, $arg3)
{
exec(sprintf(
'wp --allow-root user get %1$s --format=json 2> /dev/null',
$arg1,
), $output, $result);
Assert::eq(0, $result);
$user = json_decode($output[0], true);
Assert::eq($user['user_email'], $arg3);
Assert::eq($user['display_name'], $arg2);
Assert::greaterThan(
new DateTimeImmutable($user['user_registered']),
(new DateTimeImmutable())->sub(new DateInterval('PT1M')),
);
}
/**
* @Then the WordPress user :arg1 is member of role :arg2
*/
public function theWordpressUserIsMemberOfRole($arg1, $arg2)
{
exec(sprintf(
'wp --allow-root user get %1$s --format=json 2> /dev/null',
$arg1,
), $output, $result);
Assert::eq(0, $result);
$user = json_decode($output[0], true);
$roles = array_map(function($item): string {
return trim($item);
}, explode(',', $user['roles']));
Assert::inArray($arg2, $roles);
}
/**
* @Given LDAP user :arg1 is member of LDAP group :arg2
*/
public function ldapUserIsMemberOfLdapGroup($arg1, $arg2)
{
exec(sprintf(
'ldapmodify -x -H %1$s -D "%2$s" -w %3$s 2>&1 <<LDIF
%4$s
LDIF',
'ldap://openldap',
'cn=admin,dc=example,dc=org',
'insecure',
<<<LDIF
dn: cn=$arg2,dc=example,dc=org
changetype: modify
add: uniqueMember
uniqueMember: uid=$arg1,dc=example,dc=org
LDIF
));
}
/**
* @Given a WordPress user :arg1 does not exist
*/
public function aWordpressUserDoesNotExist($arg1)
{
exec(sprintf(
'wp --allow-root user delete --yes %1$s',
$arg1,
));
}
/**
* @Given configuration value :arg1 is set to :arg2 and :arg3
*/
public function configurationValueIsSetToAnd($arg1, $arg2, $arg3)
{
$roles = [];
foreach ([$arg2, $arg3] as $arg) {
$access = explode('=', $arg);
$roles[$access[0]] = $access[1];
}
exec(sprintf(
'echo %2$s | wp --allow-root option patch update authLDAPOptions %1$s --format=json',
$arg1,
"'" . json_encode($roles) . "'"
), $result);
}
/**
* @Then the WordPress user :arg1 is not member of role :arg2
*/
public function theWordpressUserIsNotMemberOfRole($arg1, $arg2)
{
exec(sprintf(
'wp --allow-root user get %1$s --format=json 2> /dev/null',
$arg1,
), $output, $result);
Assert::eq(0, $result);
$user = json_decode($output[0], true);
$roles = array_map(function($item): string {
return trim($item);
}, explode(',', $user['roles']));
Assert::false(in_array($arg2, $roles));
}
}

View File

@ -0,0 +1,63 @@
Feature: Log in without group assignment
Scenario: Login without group assignment with
Given a default configuration
And configuration value "GroupEnable" is set to "false"
And configuration value "DefaultRole" is set to "subscriber"
And an LDAP user "ldapuser" with name "LDAP User", password "P@ssw0rd" and email "ldapuser@example.com" exists
And an LDAP group "ldapgroup" exists
And LDAP user "ldapuser" is member of LDAP group "ldapgroup"
And a WordPress user "wordpressuser" with name "WordPress_User" and email "wordpressuser@example.com" exists
And a WordPress role "wordpressrole" exists
And WordPress user "wordpressuser" has role "wordpressrole"
And a WordPress user "ldapuser" does not exist
When LDAP user "ldapuser" logs in with password "P@ssw0rd"
Then the login suceeds
And a new WordPress user "ldapuser" was created with name "LDAP User" and email "ldapuser@example.com"
And the WordPress user "ldapuser" is member of role "subscriber"
Scenario: Login with group assignment to multiple groups where only first wordpress group is used
Given a default configuration
And configuration value "GroupEnable" is set to "true"
And configuration value "DefaultRole" is set to "subscriber"
And configuration value "Groups" is set to "administrator=ldapgroup" and "editor=ldapgroup"
And configuration value "GroupAttr" is set to "cn"
And configuration value "GroupFilter" is set to "uniquemember=%dn%"
And configuration value "GroupOverUser" is set to "true"
And an LDAP user "ldapuser" with name "LDAP User", password "P@ssw0rd" and email "ldapuser@example.com" exists
And an LDAP group "ldapgroup" exists
And LDAP user "ldapuser" is member of LDAP group "ldapgroup"
And a WordPress user "wordpressuser" with name "WordPress_User" and email "wordpressuser@example.com" exists
And a WordPress role "wordpressrole" exists
And WordPress user "wordpressuser" has role "wordpressrole"
And a WordPress user "ldapuser" does not exist
When LDAP user "ldapuser" logs in with password "P@ssw0rd"
Then the login suceeds
And a new WordPress user "ldapuser" was created with name "LDAP User" and email "ldapuser@example.com"
And the WordPress user "ldapuser" is member of role "administrator"
And the WordPress user "ldapuser" is not member of role "editor"
And the WordPress user "ldapuser" is not member of role "subscriber"
Scenario: Second Login with group assignment to multiple groups where only first wordpress group is used.
Given a default configuration
And configuration value "GroupEnable" is set to "true"
And configuration value "DefaultRole" is set to "subscriber"
And configuration value "Groups" is set to "administrator=ldapgroup" and "editor=ldapgroup"
And configuration value "GroupAttr" is set to "cn"
And configuration value "GroupFilter" is set to "uniquemember=%dn%"
And configuration value "GroupOverUser" is set to "false"
And an LDAP user "ldapuser" with name "LDAP User", password "P@ssw0rd" and email "ldapuser@example.com" exists
And an LDAP group "ldapgroup" exists
And LDAP user "ldapuser" is member of LDAP group "ldapgroup"
And a WordPress user "wordpressuser" with name "WordPress_User" and email "wordpressuser@example.com" exists
And a WordPress role "wordpressrole" exists
And WordPress user "wordpressuser" has role "wordpressrole"
And a WordPress user "ldapuser" does not exist
And LDAP user "ldapuser" logs in with password "P@ssw0rd"
And WordPress user "ldapuser" has role "wordpressrole"
And the WordPress user "ldapuser" is member of role "wordpressrole"
When LDAP user "ldapuser" logs in with password "P@ssw0rd"
Then the login suceeds
And the WordPress user "ldapuser" is member of role "administrator"
And the WordPress user "ldapuser" is member of role "wordpressrole"
And the WordPress user "ldapuser" is not member of role "editor"
And the WordPress user "ldapuser" is not member of role "subscriber"

View File

@ -2,7 +2,7 @@
Contributors: heiglandreas Contributors: heiglandreas
Tags: ldap, auth, authentication, active directory, AD, openLDAP, Open Directory Tags: ldap, auth, authentication, active directory, AD, openLDAP, Open Directory
Requires at least: 2.5.0 Requires at least: 2.5.0
Tested up to: 6.3.0 Tested up to: 6.4.0
Requires PHP: 7.4 Requires PHP: 7.4
Stable tag: trunk Stable tag: trunk
License: MIT License: MIT
@ -47,6 +47,11 @@ Please see https://github.com/heiglandreas/authLdap/blob/master/SECURITY.md for
== Changelog == == Changelog ==
= 2.6.0 =
* Fix reducing assigned WordPress roles to single role on login when WordPress roles shall be kept
* Add Behavioural testing and first 3 scenarios
= 2.5.9 = = 2.5.9 =
* Adds information about security-contacts * Adds information about security-contacts

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licensed under the MIT-license. For details see the included file LICENSE.md
*/
namespace Org_Heigl\AuthLdap\Exception;
use RuntimeException;
class UnknownOption extends RuntimeException
{
public static function withKey(string $key): self
{
return new self(sprintf(
'An option "%1$s" is not known',
$key
));
}
}

View File

@ -116,7 +116,7 @@ final class LdapUri
if (isset($url['pass'])) { if (isset($url['pass'])) {
$this->password = $url['pass']; $this->password = $url['pass'];
} }
if ($this->scheme === 'ldaps' && $this->port = 389) { if ($this->scheme === 'ldaps' && $this->port === 389) {
$this->port = 636; $this->port = 636;
} }

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licensed under the MIT-license. For details see the included file LICENSE.md
*/
namespace Org_Heigl\AuthLdap;
use function json_decode;
class OptionFactory
{
public function fromJson(string $json): Options
{
$option = new Options();
$content = json_decode($json, true);
foreach ($content as $key => $value) {
$option->set($key, $value);
}
return $option;
}
}

View File

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licensed under the MIT-license. For details see the included file LICENSE.md
*/
namespace Org_Heigl\AuthLdap;
use Org_Heigl\AuthLdap\Exception\UnknownOption;
use function array_key_exists;
class Options
{
public const ENABLED = 'Enabled';
public const CACHE_PW = 'CachePW';
public const URI = 'URI';
public const URI_SEPARATOR = 'URISeparator';
public const FILTER = 'Filter';
public const NAME_ATTR = 'NameAttr';
public const SEC_NAME = 'SecName';
public const UID_ATTR = 'UidAttr';
public const MAIL_ATTR = 'MailAttr';
public const WEB_ATTR = 'WebAttr';
public const GROUPS = 'Groups';
public const DEBUG = 'Debug';
public const GROUP_ATTR = 'GroupAttr';
public const GROUP_FILTER = 'GroupFilter';
public const DEFAULT_ROLE = 'DefaultRole';
public const GROUP_ENABLE = 'GroupEnable';
public const GROUP_OVER_USER = 'GroupOverUser';
public const VERSION = 'Version';
public const DO_NOT_OVERWRITE_NON_LDAP_USERS = 'DoNotOverwriteNonLdapUsers';
private array $settings = [
'Enabled' => false,
'CachePW' => false,
'URI' => '',
'URISeparator' => ' ',
'Filter' => '', // '(uid=%s)'
'NameAttr' => '', // 'name'
'SecName' => '',
'UidAttr' => '', // 'uid'
'MailAttr' => '', // 'mail'
'WebAttr' => '',
'Groups' => [],
'Debug' => false,
'GroupAttr' => '', // 'gidNumber'
'GroupFilter' => '', // '(&(objectClass=posixGroup)(memberUid=%s))'
'DefaultRole' => '',
'GroupEnable' => true,
'GroupOverUser' => true,
'Version' => 1,
'DoNotOverwriteNonLdapUsers' => false,
];
public function get(string $key)
{
if (! array_key_exists($key, $this->settings)) {
throw UnknownOption::withKey($key);
}
return $this->settings[$key];
}
public function has(string $key): bool
{
return array_key_exists($key, $this->settings);
}
/**
* @param mixed $value
*/
public function set(string $key, $value): void
{
if (! array_key_exists($key, $this->settings)) {
throw UnknownOption::withKey($key);
}
$this->settings[$key] = $value;
}
public function toArray(): array
{
return $this->settings;
}
}

View File

@ -18,6 +18,7 @@ use function ldap_get_entries;
use function ldap_set_option; use function ldap_set_option;
use function ldap_start_tls; use function ldap_start_tls;
use function ldap_unbind; use function ldap_unbind;
use function var_dump;
final class Ldap implements LdapInterface final class Ldap implements LdapInterface
{ {