Commit debb1475 authored by Côme Chilliet's avatar Côme Chilliet
Browse files

feat(ldap) Add library code

issue #1
parent 970214af
<?php
/*
This code is part of FusionDirectory (http://www.fusiondirectory.org/)
Copyright (C) 2020 FusionDirectory
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
namespace FusionDirectory\Ldap;
class Acl
{
/**
* @var int
*/
protected $index;
/**
* @var array<string,string>|string
*/
protected $to;
/**
* @var array<int,array>
*/
protected $by = [];
/*
<access directive> ::= to <what> [by <who> [<access>] [<control>]]+
<what> ::= * | [dn[.<basic-style>]=<regex> | dn.<scope-style>=<DN>] [filter=<ldapfilter>] [attrs=<attrlist>]
<basic-style> ::= regex | exact
<scope-style> ::= base | one | subtree | children
<attrlist> ::= <attr> [val[.<basic-style>]=<regex>] | <attr> , <attrlist>
<attr> ::= <attrname> | entry | children
<who> ::= * | [anonymous | users | self
| dn[.<basic-style>]=<regex> | dn.<scope-style>=<DN>]
[dnattr=<attrname>]
[group[/<objectclass>[/<attrname>][.<basic-style>]]=<regex>]
[peername[.<basic-style>]=<regex>]
[sockname[.<basic-style>]=<regex>]
[domain[.<basic-style>]=<regex>]
[sockurl[.<basic-style>]=<regex>]
[set=<setspec>]
[aci=<attrname>]
<access> ::= [self]{<level>|<priv>}
<level> ::= none | disclose | auth | compare | search | read | write | manage
<priv> ::= {=|+|-}{m|w|r|s|c|x|d|0}+
<control> ::= [stop | continue | break]
*/
public function __construct (string $acl)
{
if (preg_match('/^{(\d+)}/', $acl, $m)) {
$this->index = (int)($m[1]);
$acl = substr($acl, strlen($m[0]));
}
$tokens = preg_split('/\s/', $acl);
if (($tokens === FALSE) || ($tokens[0] != 'to')) {
throw new \Exception('Invalid ACL format: missing "to" keyword');
}
$this->parseTo($tokens, 1);
}
/**
* @param array<string> $tokens
*/
protected function parseTo (array $tokens, int $i): void
{
/*
<what> ::= * | [dn[.<basic-style>]=<regex> | dn.<scope-style>=<DN>] [filter=<ldapfilter>] [attrs=<attrlist>]
<basic-style> ::= regex | exact
<scope-style> ::= base | one | subtree | children
*/
if ($tokens[$i] == '*') {
$this->to = '*';
$i++;
} else {
$this->to = [];
do {
[$key, $value] = explode('=', $tokens[$i], 2);
switch ($key) {
case 'filter':
case 'attrs':
case 'dn':
case 'dn.regex':
case 'dn.exact':
case 'dn.base':
case 'dn.one':
case 'dn.subtree':
case 'dn.children':
$this->to[$key] = $value;
break;
default:
throw new Exception('Could not parse ACL: invalid "to" clause: "'.$tokens[$i].'"');
}
$i++;
} while (($i < count($tokens)) && ($tokens[$i] != 'by'));
}
if (($i < count($tokens)) && ($tokens[$i] == 'by')) {
$this->parseBy($tokens, $i + 1);
} else {
throw new Exception('Could not parse ACL: Missing "by" clause');
}
}
/**
* @param array<string> $tokens
*/
protected function parseBy (array $tokens, int $i): void
{
/* [by <who> [<access>] [<control>]]+ */
$by = [];
do {
$by[] = $tokens[$i];
$i++;
} while (($i < count($tokens)) && ($tokens[$i] != 'by'));
$this->by[] = $by;
if ($i < count($tokens)) {
if ($tokens[$i] == 'by') {
$this->parseBy($tokens, $i + 1);
} else {
throw new Exception('Could not parse ACL: Invalid clause: "'.$tokens[$i].'"');
}
}
}
public function dump (string $indent): void
{
echo $indent.$this->index.': to ';
if (is_array($this->to)) {
foreach ($this->to as $key => $attr) {
echo $key.'='.$attr.' ';
}
} else {
echo $this->to;
}
echo "\n";
foreach ($this->by as $by) {
echo $indent.' by '.implode(' ', $by)."\n";
}
}
}
<?php
/*
This code is part of FusionDirectory (http://www.fusiondirectory.org/)
Copyright (C) 2020 FusionDirectory
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
namespace FusionDirectory\Ldap;
class Exception extends \Exception
{
}
<?php
/*
This code is part of FusionDirectory (http://www.fusiondirectory.org/)
Copyright (C) 2020 FusionDirectory
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
namespace FusionDirectory\Ldap;
class Link
{
/**
* @var resource
*/
protected $cid;
/**
* @var string
*/
protected $hostname;
/**
* @var bool
*/
protected $tls;
public function __construct (string $hostname, bool $tls = FALSE)
{
$this->hostname = $hostname;
$this->tls = $tls;
}
public function bind (): void
{
$cid = ldap_connect($this->hostname);
if (!$cid) {
throw new Exception('Invalid URI: '.$this->hostname);
}
ldap_set_option($cid, LDAP_OPT_PROTOCOL_VERSION, 3);
if ($this->tls) {
ldap_start_tls($cid);
}
if (ldap_sasl_bind($cid, NULL, NULL, 'EXTERNAL') !== TRUE) {
throw new Exception('Failed to bind to '.$this->hostname);
}
$this->cid = $cid;
}
/**
* @param array<string> $attrs
* @param array<array> $controls
*/
public function search (string $basedn, string $filter, array $attrs = [], string $scope = 'subtree', array $controls = NULL): Result
{
$functions = ['base' => 'ldap_read','one' => 'ldap_list','subtree' => 'ldap_search'];
if (isset($controls)) {
$result = @$functions[strtolower($scope)]($this->cid, $basedn, $filter, $attrs, 0, 0, 0, LDAP_DEREF_NEVER, $controls);
} else {
$result = @$functions[strtolower($scope)]($this->cid, $basedn, $filter, $attrs);
}
if ($result === FALSE) {
throw new Exception('Search failed: '.ldap_error($this->cid));
}
return new Result($this->cid, $result);
}
/**
* @param array<string> $attrs
* @param array<array> $controls
*/
public function mod_add (string $dn, array $attrs, array $controls = []): Result
{
$result = ldap_mod_add_ext($this->cid, $dn, $attrs, $controls);
if ($result === FALSE) {
throw new Exception('Mod add failed: '.ldap_error($this->cid));
}
return new Result($this->cid, $result);
}
/**
* @param array<string> $attrs
* @param array<array> $controls
*/
public function mod_replace (string $dn, array $attrs, array $controls = []): Result
{
$result = ldap_mod_replace_ext($this->cid, $dn, $attrs, $controls);
if ($result === FALSE) {
throw new Exception('Mod replace failed: '.ldap_error($this->cid));
}
return new Result($this->cid, $result);
}
/**
* @param array<string> $attrs
* @param array<array> $controls
*/
public function mod_del (string $dn, array $attrs, array $controls = []): Result
{
$result = ldap_mod_del_ext($this->cid, $dn, $attrs, $controls);
if ($result === FALSE) {
throw new Exception('Mod del failed: '.ldap_error($this->cid));
}
return new Result($this->cid, $result);
}
/**
* @param array<array> $controls
*/
public function delete (string $dn, array $controls = []): Result
{
$result = ldap_delete_ext($this->cid, $dn, $controls);
if ($result === FALSE) {
throw new Exception('Delete failed: '.ldap_error($this->cid));
}
return new Result($this->cid, $result);
}
/**
* @return array<string,array<string,string|true|array<string>>>
*/
public function getObjectClasses (): array
{
// Get base to look for schema
$res = $this->search('', 'objectClass=*', ['subschemaSubentry'], 'base');
$objectclasses = [];
foreach ($res as $attrs) {
if (!isset($attrs['subschemaSubentry'][0])) {
continue;
}
/* Get list of objectclasses and fill array */
$nb = $attrs['subschemaSubentry'][0];
$res2 = $this->search($nb, 'objectClass=*', ['objectClasses'], 'base');
foreach ($res2 as $attrs2) {
foreach ($attrs2['objectClasses'] as $val) {
$name = 'OID';
$value = '';
$pattern = explode(' ', $val);
$ocname = preg_replace("/^.* NAME\s+\(*\s*'([^']+)'\s*\)*.*$/", '\\1', $val);
$objectclasses[$ocname] = [];
foreach ($pattern as $chunk) {
switch ($chunk) {
case '(':
$value = '';
break;
case ')':
$chunk = '';
case 'NAME':
case 'DESC':
case 'SUP':
case 'STRUCTURAL':
case 'ABSTRACT':
case 'AUXILIARY':
case 'MUST':
case 'MAY':
if ($name != '') {
$v = $this->value2container($value);
if (in_array($name, ['MUST','MAY'])) {
if ($v === TRUE) {
$v = [];
} else if (!is_array($v)) {
$v = [$v];
}
}
$objectclasses[$ocname][$name] = $v;
}
$name = $chunk;
$value = '';
break;
default: $value .= $chunk.' ';
}
}
}
}
}
return $objectclasses;
}
/**
* @return string|true|array<string>
*/
protected function value2container (string $value)
{
/* Set empty values to "TRUE" only */
if (preg_match('/^\s*$/', $value)) {
return TRUE;
}
/* Remove ' and " if needed */
$value = preg_replace('/^[\'"]/', '', $value);
$value = preg_replace('/[\'"] *$/', '', $value);
$value = rtrim($value);
/* Convert to array if $ is inside... */
if (preg_match('/\$/', $value)) {
$container = preg_split('/\s*\$\s*/', $value);
if ($container === FALSE) {
throw new Exception('Failed to split value');
}
} else {
$container = $value;
}
return $container;
}
}
<?php
/*
This code is part of FusionDirectory (http://www.fusiondirectory.org/)
Copyright (C) 2020 FusionDirectory
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
namespace FusionDirectory\Ldap;
/**
* @implements \Iterator<string,array<string,array<string>>>
*/
class Result implements \Iterator,\Countable
{
/**
* @var int
*/
public $errcode;
/**
* @var string
*/
public $matcheddn;
/**
* @var string
*/
public $errmsg;
/**
* @var array<int>
*/
public $referrals;
/**
* @var array<array>
*/
public $serverctrls;
/**
* @var resource
*/
protected $link;
/**
* @var resource
*/
protected $result;
/**
* @var resource|false
*/
protected $cur;
/**
* @param resource $link
* @param resource $result
*/
public function __construct ($link, $result)
{
$this->link = $link;
$this->result = $result;
$success = ldap_parse_result($link, $result, $this->errcode, $this->matcheddn, $this->errmsg, $this->referrals, $this->serverctrls);
if (!$success) {
throw new Exception('Failed to parse result: '.ldap_error($this->link));
}
}
public function assert (): void
{
if ($this->errcode != 0) {
$errmsg = $this->errmsg.' ('.$this->errcode.')';
if (!empty($this->matcheddn)) {
$errmsg .= '(matched dn: '.$this->matcheddn.')';
}
throw new Exception($errmsg);
}
}
public function count (): int
{
return ldap_count_entries($this->link, $this->result);
}
public function current ()
{
$att = [];
for ($a = ldap_first_attribute($this->link, $this->cur); $a !== FALSE; $a = ldap_next_attribute($this->link, $this->cur)) {
$att[$a] = ldap_get_values($this->link, $this->cur, $a);
unset($att[$a]['count']);
}
return $att;
}
public function key ()
{
return trim(ldap_get_dn($this->link, $this->cur));
}
public function next (): void
{
$this->cur = ldap_next_entry($this->link, $this->cur);
}
public function rewind (): void
{
$this->cur = ldap_first_entry($this->link, $this->result);
}
public function valid (): bool
{
return is_resource($this->cur);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment