commit
79d31f61c4
50 changed files with 6127 additions and 0 deletions
-
43.github/workflows/ci.yml
-
4.gitignore
-
106CHANGELOG.md
-
27COPYING
-
4Gemfile
-
148README.md
-
15Rakefile
-
96examples/README
-
32examples/cert.pem
-
51examples/key.pem
-
17examples/ldapdb.yaml
-
34examples/mkcert.rb
-
112examples/rbslapd1.rb
-
161examples/rbslapd2.rb
-
11examples/rbslapd2.sql
-
172examples/rbslapd3.rb
-
90examples/rbslapd4.rb
-
73examples/rbslapd5.rb
-
75examples/rbslapd6.rb
-
37examples/speedtest.rb
-
4lib/ldap/server.rb
-
257lib/ldap/server/connection.rb
-
220lib/ldap/server/dn.rb
-
223lib/ldap/server/filter.rb
-
283lib/ldap/server/match.rb
-
528lib/ldap/server/operation.rb
-
92lib/ldap/server/preforkserver.rb
-
166lib/ldap/server/request.rb
-
71lib/ldap/server/result.rb
-
220lib/ldap/server/router.rb
-
592lib/ldap/server/schema.rb
-
123lib/ldap/server/server.rb
-
235lib/ldap/server/syntax.rb
-
102lib/ldap/server/tcpserver.rb
-
92lib/ldap/server/trie.rb
-
88lib/ldap/server/util.rb
-
5lib/ldap/server/version.rb
-
27ruby-ldapserver.gemspec
-
39spec/operation_spec.rb
-
0spec/spec_helper.rb
-
582test/core.schema
-
149test/dn_test.rb
-
289test/encoding_test.rb
-
107test/filter_test.rb
-
59test/match_test.rb
-
113test/schema_test.rb
-
40test/syntax_test.rb
-
2test/test_helper.rb
-
60test/trie_test.rb
-
51test/util_test.rb
@ -0,0 +1,43 @@ |
|||||
|
name: CI |
||||
|
|
||||
|
on: [push, pull_request] |
||||
|
|
||||
|
jobs: |
||||
|
job_test_gem: |
||||
|
name: Test in source tree |
||||
|
strategy: |
||||
|
fail-fast: false |
||||
|
matrix: |
||||
|
include: |
||||
|
- os: windows |
||||
|
ruby: "head" |
||||
|
- os: windows |
||||
|
ruby: "2.4" |
||||
|
- os: ubuntu |
||||
|
ruby: "head" |
||||
|
- os: ubuntu |
||||
|
ruby: "3.1" |
||||
|
- os: ubuntu |
||||
|
ruby: "2.3" |
||||
|
- os: macos |
||||
|
ruby: "head" |
||||
|
|
||||
|
runs-on: ${{ matrix.os }}-latest |
||||
|
|
||||
|
steps: |
||||
|
- uses: actions/checkout@v3 |
||||
|
|
||||
|
- uses: ruby/setup-ruby@v1 |
||||
|
with: |
||||
|
ruby-version: ${{ matrix.ruby }} # passed to ruby/setup-ruby |
||||
|
|
||||
|
- name: Print tool versions |
||||
|
run: | |
||||
|
ruby -v |
||||
|
gem env |
||||
|
|
||||
|
- name: Bundle install |
||||
|
run: bundle install |
||||
|
|
||||
|
- name: Run tests |
||||
|
run: bundle exec rake |
||||
@ -0,0 +1,4 @@ |
|||||
|
Gemfile.lock |
||||
|
pkg |
||||
|
.idea |
||||
|
doc/ |
||||
@ -0,0 +1,106 @@ |
|||||
|
## 0.7.0 / 2022-12-06 |
||||
|
|
||||
|
* Support optional attribute range retrieval according to |
||||
|
https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ldap/searching-using-range-retrieval |
||||
|
* Add an experimental yet incomplete request router as alternative to using OperationClass |
||||
|
* Add support for listening on UNIX domain sockets. |
||||
|
Use `LDAP::Server.new(socket: '/tmp/server.sock')` |
||||
|
* Add LDAP::Server::DN to work with LDAP distinguished names |
||||
|
* Add CI on Github |
||||
|
* Use net-ldap for tests instead of unmaintained ruby-ldap. |
||||
|
|
||||
|
|
||||
|
## 0.5.3 / 2015-08-16 |
||||
|
* Handle BN as client_timelimit; fixes incompatibility with some LDAP |
||||
|
implementations (notably Shibboleth IdP v2 and proftpd). |
||||
|
(Patch by Pete Birkinshaw.) |
||||
|
|
||||
|
|
||||
|
## 0.5.2 / 2015-06-24 |
||||
|
* Make sure the exception used to stop the child doesn't propagate up (patch by Kasumi Hanazuki) |
||||
|
|
||||
|
|
||||
|
## 0.3.1 - 2008-01-16 |
||||
|
* First release as a gem [Brandon Keepers] |
||||
|
|
||||
|
|
||||
|
## RELEASE_0_3 |
||||
|
|
||||
|
Filters now return nil instead of LDAP::Server::MatchingRule::DefaultMatch |
||||
|
in the case that there's no schema. |
||||
|
Minor changes to syntax.rb to support OpenLDAP extensions. |
||||
|
|
||||
|
## 20050722 |
||||
|
|
||||
|
Change the 'validate' API so it works for updates too. |
||||
|
Change the 'modify' API so it sends a hash of attr=>[:op,data] which makes |
||||
|
it easier to determine which entries have been modified. |
||||
|
Fix modify, add and compare to normalise attribute names using the schema if |
||||
|
there is one. |
||||
|
|
||||
|
## 20050721 |
||||
|
|
||||
|
Added a whole loada Schema stuff. |
||||
|
Moved exceptions under LDAP::ResultError for consistency with ruby-ldap. |
||||
|
Changed the parsed [filter] format to include a MatchingRule object always |
||||
|
(even if no schema is present) |
||||
|
|
||||
|
## 20050711 |
||||
|
|
||||
|
Changed LDAPserver to LDAP::Server and rejigged the repository to match. |
||||
|
In your code you will have to change: |
||||
|
require 'ldapserver/foo' -> require 'ldap/server/foo' |
||||
|
LDAPserver::bar -> LDAP::Server::bar |
||||
|
|
||||
|
I have added require 'ldap/server' which pulls in the things a basic server |
||||
|
will need (minus schema) |
||||
|
|
||||
|
## 20050626 |
||||
|
|
||||
|
Factored out the SSL stuff into Connection, which should also allow the |
||||
|
STARTTLS extension to be implemented later |
||||
|
|
||||
|
Added a Server class, with methods run_tcpserver and run_prefork. |
||||
|
|
||||
|
Created an explicit preforkserver method. |
||||
|
|
||||
|
## 20050625 |
||||
|
|
||||
|
tcpserver: add ability to drop privileges |
||||
|
|
||||
|
examples/rbslapd3.rb: make work if ldapdb.yaml does not exist. Also bind |
||||
|
explicitly to 0.0.0.0; it seems that TCPSocket doesn't work properly in |
||||
|
some circumstances without it (FreeBSD 5.4 with IPv6 disabled in kernel) |
||||
|
|
||||
|
## 20050620 |
||||
|
|
||||
|
RELEASE_0_2 |
||||
|
|
||||
|
Implemented SSL support in tcpserver, just by copying examples from |
||||
|
openssl module. |
||||
|
|
||||
|
Tweak split_dn so that it should work properly with UTF-8 encoded strings |
||||
|
|
||||
|
Added examples/rbslapd3.rb, a preforking LDAP server |
||||
|
|
||||
|
Added :listen option to tcpserver to set listen queue size. With the default |
||||
|
of 5, and 100 children trying to connect, a few connections get dropped. |
||||
|
|
||||
|
Added :nodelay option to tcpserver to set TCP_NODELAY socket option. This |
||||
|
removes 100ms of latency in responses. |
||||
|
|
||||
|
Added examples/speedtest.rb |
||||
|
|
||||
|
## 20050619 |
||||
|
|
||||
|
Modify connection.rb to ensure no memory leak in the event of exceptions |
||||
|
being raised in operation threads. |
||||
|
|
||||
|
Fix examples/rbslapd2.rb SQLPool so that it always puts connections back |
||||
|
into the pool (using 'ensure' this time :-) |
||||
|
|
||||
|
## 20050618 |
||||
|
|
||||
|
RELEASE_0_1 |
||||
|
|
||||
|
## 20050616 |
||||
@ -0,0 +1,27 @@ |
|||||
|
The file 'test/core.schema' is Copyrighted by the OpenLDAP project. See the |
||||
|
comments in that file for full details. |
||||
|
|
||||
|
All other files in this distribution are subject to the following copyright |
||||
|
notice: |
||||
|
|
||||
|
COPYING |
||||
|
======= |
||||
|
Copyright (c) 2005 Brian Candler |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to |
||||
|
deal in the Software without restriction, including without limitation the |
||||
|
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or |
||||
|
sell copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in |
||||
|
all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
||||
|
IN THE SOFTWARE. |
||||
@ -0,0 +1,4 @@ |
|||||
|
source 'https://rubygems.org' |
||||
|
|
||||
|
# Specify your gem's dependencies in ldap-server-primitive.gemspec |
||||
|
gemspec |
||||
@ -0,0 +1,148 @@ |
|||||
|
# ruby-ldapserver |
||||
|
|
||||
|
ruby-ldapserver is a lightweight, pure Ruby framework for implementing LDAP server applications. It is intended primarily for building a gateway from LDAP queries into some other protocol or database. It does not attempt to be a full or correct implementation of the standard LDAP data model itself (although you could build one using this as a frontend). |
||||
|
|
||||
|
Since it's written entirely in Ruby, it benefits from Ruby's threading engine. |
||||
|
|
||||
|
## Target audience |
||||
|
|
||||
|
Technically-savvy Ruby applications developers; the sort of people who are happy to read RFCs and read code to work out what it does :-) |
||||
|
|
||||
|
The examples/ directory contains a few minimal LDAP servers which you can use as a starting point. |
||||
|
|
||||
|
## Status |
||||
|
|
||||
|
This is still an early release. It works for me as an LDAP protocol layer; the Schema stuff has not been heavily tested. |
||||
|
|
||||
|
## Request router |
||||
|
|
||||
|
The request router is a simple mapping of potentially parameterized routes (DNs) and actions to a *controller* action, allowing for simple, flexible and maintainable code. Alternatively the legacy `Operation` class can be used. See the `examples/` directory for more details and sample implementations. |
||||
|
|
||||
|
## Configuration |
||||
|
|
||||
|
```ruby |
||||
|
params = { |
||||
|
# Bind to address (cannot be combined with socket) |
||||
|
bindaddr: '127.0.0.1', # defaults to 0.0.0.0 |
||||
|
port: 1389, |
||||
|
|
||||
|
# Bind to socket (cannot be combined with address) |
||||
|
socket: '/tmp/ldap.sock', |
||||
|
|
||||
|
# Drop process and socket privileges to user and/or group (cannot be combined with uid/gid) |
||||
|
user: 'ldap', |
||||
|
group: 'ldap', |
||||
|
|
||||
|
# Drop process and socket privileges to UID and/or GID (cannot be combined with user/group) |
||||
|
uid: 1000, |
||||
|
gid: 1000, |
||||
|
|
||||
|
# TCP_NODELAY option |
||||
|
nodelay: true, |
||||
|
|
||||
|
# Socket backlog |
||||
|
listen: 10, |
||||
|
|
||||
|
# SSL/TLS |
||||
|
ssl_key_file: 'key.pem', |
||||
|
ssl_cert_file: 'cert.pem', |
||||
|
ssl_on_connect: true, |
||||
|
|
||||
|
# Request router (cannot be combined with legacy operation) |
||||
|
router: MyAppRouter, |
||||
|
|
||||
|
# Legacy Operation class (cannot be combined with request router) |
||||
|
operation_class: MyAppOperation, |
||||
|
operation_args: ['my', 'arguments'], |
||||
|
|
||||
|
# Schema |
||||
|
schema: my_schema, |
||||
|
namingContexts: ['dc=example,dc=com'], |
||||
|
|
||||
|
# Limit number of attribute values in query results |
||||
|
# https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ldap/searching-using-range-retrieval |
||||
|
attribute_range_limit: 1500, |
||||
|
} |
||||
|
|
||||
|
serv = LDAP::Server.new(params) |
||||
|
serv.run_tcpserver |
||||
|
``` |
||||
|
|
||||
|
## Libraries |
||||
|
|
||||
|
ASN1 encoding and decoding is done using the 'openssl' extension, which is standard in the Ruby 1.8.2 base distribution. To check you have it, you should be able to run `ruby -ropenssl -e puts` with no error. |
||||
|
|
||||
|
However, I've found in the past that Linux machines don't always build the openssl extension when compiling Ruby from source. With Red Hat 9, the solution for me was, when building Ruby itself: |
||||
|
|
||||
|
``` |
||||
|
$ export CPPFLAGS="-I/usr/kerberos/include" |
||||
|
$ export LDFLAGS="-L/usr/kerberos/lib" |
||||
|
$ ./configure ...etc |
||||
|
``` |
||||
|
|
||||
|
If you want to run the test suite then you'll need to install the `ruby-ldap` client library, and if you want to run `examples/rbslapd3.rb` then you'll need the `prefork` library. Both are available from <http://raa.ruby-lang.org/>. |
||||
|
|
||||
|
## Protocol implementation |
||||
|
|
||||
|
ruby-ldapserver tries to be a reasonably complete implementation of the message decoding and encoding components of LDAP. However, it does not synthesise or directly enforce the LDAP data model. It will advertise a schema in the root DSE if you configure one, and it provides helper functions which allow you to validate add and modify operations against a schema; but it's up to you to use them, if you wish. If you're just using LDAP as a convenient query interface into some other database, you probably don't care about schemas. |
||||
|
|
||||
|
If your clients permit it, you can violate the LDAP specification further, eliminating some of the gross design flaws of LDAP. For example, you can ditch the LDAP idea that a Distinguished Name must consist of attr=val,attr=val,attr=val... and use whatever is convenient as a primary key (e.g. "val1,val2,val3" or "id,table_name"). The 'add' operation could allocate DNs automatically from a sequence. There's no need for the data duplication where an LDAP entry must contain the same attr=val pair which is also the entry's RDN. Violations of the LDAP spec in this way are at your own risk. |
||||
|
|
||||
|
## Threading issues |
||||
|
|
||||
|
The core of this library is the `LDAP::Server::Connection` object which handles communication with a single client, and the `LDAP::Server::Operation` object which handles a single request. Because the LDAP protocol allows a client to send multiple overlapping requests down the same TCP connection, I start a new Ruby thread for each Operation. |
||||
|
|
||||
|
If your Operation object deals with any global shared data, then it needs to do so in a thread-safe way. If this is new to you then see |
||||
|
|
||||
|
[http://www.rubycentral.com/book/tut_threads.html](http://www.rubycentral.com/book/tut_threads.html) |
||||
|
[http://www.rubygarden.org/ruby?MultiThreading](http://www.rubygarden.org/ruby?MultiThreading) |
||||
|
|
||||
|
For incoming client connections, I have supplied a simple tcpserver method which starts a new Ruby thread for each client. This works fine, but in a multi-CPU system, all LDAP server operations will be processed on one CPU; also with a very large number of concurrent client connections, you may find you hit the a max-filedescriptors-per-process limit. |
||||
|
|
||||
|
I have also provided a preforking server; see `examples/rbslapd3.rb`. In this case, your connections are handled in separate processes so they cannot share data directly in RAM. |
||||
|
|
||||
|
If you are using the default threading tcpserver, then beware that a number of Ruby extension libraries block the threading interpreter. In particular, the client library `ruby-ldap` blocks when waiting for a response from a remote server, since it's a wrapper around a C library which is unaware of Ruby's threading engine. This can cause your application to 'freeze' periodically. Either choose client libraries which play well with threading, or make sure each client is handled in a different process. |
||||
|
|
||||
|
For example, when talking to a MySQL database, you might want to choose `ruby-mysql` (which is a pure Ruby implementation of the MySQL protocol) rather than `mysql-ruby` (which is a wrapper around the C API, and blocks while waiting for responses from the server) |
||||
|
|
||||
|
Even with something like `ruby-mysql`, beware DNS lookups: resolver libraries can block too. There is a pure Ruby resolver replacement in the standard library: if you do |
||||
|
|
||||
|
``` |
||||
|
require 'resolv-replace' |
||||
|
``` |
||||
|
|
||||
|
This changes TCPSocket and friends to use it instead of the default C resolver. Or you could just hard-code IP addresses, or put entries in /etc/hosts for the machines you want to contact. |
||||
|
|
||||
|
Another threading issue to think about is abandoned and timed-out LDAP operations. The `Connection` object handles these by raising an `LDAP::Server::Abandon` or `LDAP::Server::TimeLimitExceeded` exception in the `Operation` thread, which you can either ignore or rescue. However, if in rescuing it you end up putting (say) a SQL connection back into a pool, you should beware that the SQL connection may still be mid-query, so it's probably better to discard it and use a fresh one next time. |
||||
|
|
||||
|
## Performance |
||||
|
|
||||
|
`examples/speedtest.rb` is a simple client which forks N processes, and in each process opens an LDAP connection, binds, and sends M search requests down it. |
||||
|
|
||||
|
Using speedtest.rb and rbslapd1.rb, running on the *same* machine (single-processor AMD Athlon 2500+) I achieve around 800 searches per second with N=1,M=1000 and 300-400 searches per second with N=10,M=100. |
||||
|
|
||||
|
## To-do list |
||||
|
|
||||
|
- handle and test generation of LDAP referrals properly |
||||
|
- more cases in test suite: abandon, concurrency, performance tests, error |
||||
|
handling |
||||
|
- extensible match filters |
||||
|
- extended operations |
||||
|
RFC 2830 - Start TLS |
||||
|
RFC 3062 - password modify |
||||
|
RFC 2839 - whoami |
||||
|
RFC 3909 - cancel |
||||
|
|
||||
|
## References |
||||
|
|
||||
|
- [RFC2251](ftp://ftp.isi.edu/in-notes/rfc2251.txt) (base protocol) |
||||
|
- [RFC2252](ftp://ftp.isi.edu/in-notes/rfc2252.txt) (schema) |
||||
|
- [RFC2253](ftp://ftp.isi.edu/in-notes/rfc2253.txt) (DN encoding) |
||||
|
- [X.680](http://www.itu.int/ITU-T/studygroups/com17/languages/X.680-0207.pdf) |
||||
|
- [X.690](http://www.itu.int/ITU-T/studygroups/com10/languages/X.690_1297.pdf) |
||||
|
|
||||
|
## Contact |
||||
|
|
||||
|
You are very welcome to E-mail me with bug reports, patches, comments and suggestions for this software. However, please DON'T send me any general questions about LDAP, how LDAP works, how to apply LDAP in your particular situation, or questions about any other LDAP software. The [`ldap@umich.edu` mailing list](http://listserver.itd.umich.edu/cgi-bin/lyris.pl?enter=ldap) is probably the correct place to ask such questions. |
||||
|
|
||||
|
Brian Candler <B.Candler@pobox.com> |
||||
@ -0,0 +1,15 @@ |
|||||
|
require "bundler/gem_tasks" |
||||
|
|
||||
|
require 'rake/testtask' |
||||
|
Rake::TestTask.new do |t| |
||||
|
t.test_files = FileList['test/*_test.rb'] |
||||
|
end |
||||
|
|
||||
|
begin |
||||
|
require 'rspec/core/rake_task' |
||||
|
RSpec::Core::RakeTask.new(:spec) |
||||
|
rescue LoadError |
||||
|
task :spec |
||||
|
end |
||||
|
|
||||
|
task default: [:test, :spec] |
||||
@ -0,0 +1,96 @@ |
|||||
|
Using the example programs |
||||
|
========================== |
||||
|
|
||||
|
These servers all listen on port 1389 by default, so that they don't have to |
||||
|
be run as root. |
||||
|
|
||||
|
Example 1: trivial server using RAM hash |
||||
|
---------------------------------------- |
||||
|
|
||||
|
$ ruby rbslapd1.rb |
||||
|
|
||||
|
In another window: |
||||
|
|
||||
|
$ ldapadd -x -H ldap://127.0.0.1:1389/ |
||||
|
dn: dc=example,dc=com |
||||
|
cn: Top object |
||||
|
|
||||
|
dn: cn=Fred Flintstone,dc=example,dc=com |
||||
|
cn: Fred Flintstone |
||||
|
sn: Flintstone |
||||
|
mail: fred@bedrock.org |
||||
|
mail: fred.flintstone@bedrock.org |
||||
|
|
||||
|
dn: cn=Wilma Flintstone,dc=example,dc=com |
||||
|
cn: Wilma Flintstone |
||||
|
mail: wilma@bedrock.org |
||||
|
^D |
||||
|
|
||||
|
Try these queries: |
||||
|
|
||||
|
$ ldapsearch -x -H ldap://127.0.0.1:1389/ -b "" "(objectclass=*)" |
||||
|
$ ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" -s base "(objectclass=*)" |
||||
|
$ ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" "(mail=fred*)" |
||||
|
|
||||
|
If you terminate the server with Ctrl-C, its contents should be written |
||||
|
to disk as a YAML file. |
||||
|
|
||||
|
A fairly complete set of the filter language is implemented. However, this |
||||
|
simple server works by simply scanning the entire database and applying the |
||||
|
filter to each entry, so it won't scale to large applications. No validation |
||||
|
of DN or attributes against any sort of schema is done. |
||||
|
|
||||
|
Example 1a: with SSL |
||||
|
-------------------- |
||||
|
|
||||
|
In rbslapd1.rb, uncomment |
||||
|
|
||||
|
:ssl_key_file => "key.pem", |
||||
|
:ssl_cert_file => "cert.pem", |
||||
|
:ssl_on_connect => true, |
||||
|
|
||||
|
and run mkcert.rb. Since this is a self-signed certificate, you'll have to |
||||
|
turn off certificate verification in the client too. For example: |
||||
|
|
||||
|
$ env LDAPTLS_REQCERT="allow" ldapsearch -x -H ldaps://127.0.0.1:1389/ |
||||
|
|
||||
|
Making your own CA and installing its certificate in the client, or |
||||
|
generating a Certificate Signing Request and sending it to a known CA, is |
||||
|
beyond the scope of this documentation. |
||||
|
|
||||
|
Example 2: simple LDAP to SQL mapping |
||||
|
------------------------------------- |
||||
|
|
||||
|
You will need to set up a MySQL database with a table conforming to the |
||||
|
schema given within the code. Once done, LDAP gives a read-only view of the |
||||
|
database with only the filter "(uid=<foo>)" supported. |
||||
|
|
||||
|
Example 3: preforking server and schema |
||||
|
--------------------------------------- |
||||
|
|
||||
|
This functions in the same way as rbslapd1.rb. However, since each query is |
||||
|
answered in a separate process, the YAML file on disk is used as the master |
||||
|
repository. Update operations re-write this file each time. |
||||
|
|
||||
|
Also, the schema is read from file 'core.schema'. Attempting to insert the |
||||
|
above entries will fail, due to schema violations. Insert a valid entry, |
||||
|
e.g. |
||||
|
|
||||
|
dn: cn=Fred Flintstone,dc=example,dc=com |
||||
|
objectClass: organizationalPerson |
||||
|
cn: Fred Flintstone |
||||
|
sn: Flintstone |
||||
|
telephoneNumber: +1 555 1234 |
||||
|
telephoneNumber: +1 555 5432 |
||||
|
|
||||
|
Schema validation takes place for the attribute values and that attributes |
||||
|
are allowed/required by the objectclass(es); however, the DN itself is not |
||||
|
validated, nor any checks made that the RDN is present as an attribute |
||||
|
(since this is one of the more stupid parts of the LDAP/X500 data model) |
||||
|
|
||||
|
|
||||
|
Example 4 |
||||
|
------------ |
||||
|
|
||||
|
* ruby rbslapd4.rb |
||||
|
* `ldapwhoami -x -H ldap://127.0.0.1:1389 -D "uid=admin,ou=Users,dc=mydomain,dc=com" -w "adminpassword"` |
||||
@ -0,0 +1,32 @@ |
|||||
|
-----BEGIN CERTIFICATE----- |
||||
|
MIIFhTCCA22gAwIBAgIBADANBgkqhkiG9w0BAQUFADAwMQswCQYDVQQGEwJKUDEN |
||||
|
MAsGA1UECgwEVEVTVDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDYwNTEwMTc1 |
||||
|
MloXDTI2MDYwNTExMTc1MlowMDELMAkGA1UEBhMCSlAxDTALBgNVBAoMBFRFU1Qx |
||||
|
EjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC |
||||
|
ggIBALNUH3qAAd15GiX+69MxJPzLL58CgDvBUKZsMzoxlJZw27UQywcb7PM5uyir |
||||
|
DHX0f6+JbV5uDNQ3iNdnqVeQChd32C6OXMo5lzKCl7zLxrrdGrizgN63dcXjLzpA |
||||
|
WDv1NjVVc8eI/Bs1lMEjG+lZRN1yqF05yxjBo+IJbb5rHgcHoKclwxrzs9wrc1Ff |
||||
|
ZnM9dMbytEUrZgssq+NAmBm6ie7XGdTSwdANKUJqMD7IUht+dqoFM5TlpgFUBkyf |
||||
|
jRXpsBgBkab+87o6OZk9mKIBM09ITgJmKhjGiUZeQ6VLLPx5J+WBicPdmg6kfyXO |
||||
|
OpA7QReNZz3ODRyy2ksZ8upsNxsm9R1TeWLYGhfRuTnlBiq8x8EDB00vvhXO5anf |
||||
|
hwwJKiFjx7BI90VpfVd48KXSYI5p+fkXWjrteT7P2yN9Fxv6k70Mhy62FZQo+Q34 |
||||
|
QWRsFYg4UrMWpPZUwtwqfXaeq/d9eBXvropotJASXHlFLUsHLjyG6Bmj/qfBMcJW |
||||
|
cti5nReDu4oIFr2xXND746Kx6zEwzR/6tAaimZ1BNYwqCCPxoaYpjC4WnXMiUVF8 |
||||
|
78THoIzIrxW9H9ofE4y1bJQJ6F6KZAywOMoC/5dM8xfYePyIFpmR0k2pVKiTO/Yc |
||||
|
fyVWKLYk67jMg35e5zj91wmw0maVJZWNutVYnKUImfb5P+X5AgMBAAGjgakwgaYw |
||||
|
CQYDVR0TBAIwADAdBgNVHQ4EFgQUg2LjMxC59ERv/MY+n9WOtSPn/6gwEwYDVR0l |
||||
|
BAwwCgYIKwYBBQUHAwEwCwYDVR0PBAQDAgSwMFgGA1UdIwRRME+AFINi4zMQufRE |
||||
|
b/zGPp/VjrUj5/+ooTSkMjAwMQswCQYDVQQGEwJKUDENMAsGA1UECgwEVEVTVDES |
||||
|
MBAGA1UEAwwJbG9jYWxob3N0ggEAMA0GCSqGSIb3DQEBBQUAA4ICAQCYeNO7TZ64 |
||||
|
QQUBPvu75sYhhOBwEtcQMR/lHNYoqYy0TA4W/E+wwiRPweaMkPyUkzkK2/NZXP7w |
||||
|
QB+gT4rMszN2fPZi6Bvr2M0QtO8/YVEzMPs5Y7XcJqL8TtRsUrNYUTLtLoZ4iebq |
||||
|
G7hsFNwQLAdSQ+/xd+LfcxGNjXmmErhbbR5B1WsVJ5tYmW4qTuYQpZF5lOqQKEfh |
||||
|
98ea9eJNTgWVk14Hk1cwsuZaH4IcDQgPhmu7fMvyunShk4a4ArDNRNE656w2uZch |
||||
|
ThViRVdh+bqew76PS5zQHlGNJDgySYxTIVLhlWwSoFHLZkal6W8IqkKfurFA9dbI |
||||
|
Qi15eoqxwApXFniq10Y1f82lOiW3IJNXPhEh7ch8mgOQ/uDppAf4HmkIy3CguMr3 |
||||
|
F0pWVs8a7zliOw2Ejj8L2hLel64KUhmCQWfoD7gFdnj1jpZr2tlv/GBbpWhpt2X2 |
||||
|
8Ok9bHiMXvmUFd/g8rG2XadkiFGwOTC0OguD/7tT8BmNRWDJb8Ga3dDnVzQ4xmV+ |
||||
|
Q3M8f8jYxw0Hrs8cW9xyW+gSckeGLkKrtoqhS/DNaNzdk12MZ5wwSgyuvZjj9h84 |
||||
|
wuF8+3KhE6ucdrr5cuqzAkaeGPe6fvEfkllSLt5s6gmN+6/bTg1/f1l5o/xWYlUH |
||||
|
Q34692xrylRsx0qRw37Rk525ihmim+LLlw== |
||||
|
-----END CERTIFICATE----- |
||||
@ -0,0 +1,51 @@ |
|||||
|
-----BEGIN RSA PRIVATE KEY----- |
||||
|
MIIJKAIBAAKCAgEAs1QfeoAB3XkaJf7r0zEk/MsvnwKAO8FQpmwzOjGUlnDbtRDL |
||||
|
Bxvs8zm7KKsMdfR/r4ltXm4M1DeI12epV5AKF3fYLo5cyjmXMoKXvMvGut0auLOA |
||||
|
3rd1xeMvOkBYO/U2NVVzx4j8GzWUwSMb6VlE3XKoXTnLGMGj4gltvmseBwegpyXD |
||||
|
GvOz3CtzUV9mcz10xvK0RStmCyyr40CYGbqJ7tcZ1NLB0A0pQmowPshSG352qgUz |
||||
|
lOWmAVQGTJ+NFemwGAGRpv7zujo5mT2YogEzT0hOAmYqGMaJRl5DpUss/Hkn5YGJ |
||||
|
w92aDqR/Jc46kDtBF41nPc4NHLLaSxny6mw3Gyb1HVN5YtgaF9G5OeUGKrzHwQMH |
||||
|
TS++Fc7lqd+HDAkqIWPHsEj3RWl9V3jwpdJgjmn5+RdaOu15Ps/bI30XG/qTvQyH |
||||
|
LrYVlCj5DfhBZGwViDhSsxak9lTC3Cp9dp6r9314Fe+uimi0kBJceUUtSwcuPIbo |
||||
|
GaP+p8ExwlZy2LmdF4O7iggWvbFc0PvjorHrMTDNH/q0BqKZnUE1jCoII/GhpimM |
||||
|
LhadcyJRUXzvxMegjMivFb0f2h8TjLVslAnoXopkDLA4ygL/l0zzF9h4/IgWmZHS |
||||
|
TalUqJM79hx/JVYotiTruMyDfl7nOP3XCbDSZpUllY261VicpQiZ9vk/5fkCAwEA |
||||
|
AQKCAgAXk8lZuUfFfx0VfskxqKX0yKAXt2P1t0prvxETJx6iku8IBM+0vRKvvdji |
||||
|
FW8beQxqn1ZuUmjEZBLNQ1dL6GezQzH8mQIRInZDEVFy5JXZzqUrQIqCfbtxy5dT |
||||
|
gM84/tnkNDp3Mwb2atqGdB/A09hOhzskmqsds6Pg3Z18qie2A+Y246uduQneOiY8 |
||||
|
vh7BqwRV/eX+rCCL+pEU3VLCGzj0WnqOdTE/MePJVB3Iu1y0ObHPU8S+4FytkwcK |
||||
|
/vU1OtbIqTglrSKNSwd0otkU/7RnyZlcDmOdg0jcJBufuV0OSr2YmqnqwxF0uGLx |
||||
|
LQadHPVHa/N1eEhYDBnQQvahpJ2v7yIoz6IehWRuykdivrcSA/ELJVGuLB4yuO+B |
||||
|
sZvOvORXI/9M1mYJR67+Gm+L+AiDxpEa//EG0CCZBlzOzVsFxDxvpK59bK4mPF1i |
||||
|
uIH4xGliBTbZz2MFd8fGwa/xMrS5bHupfa8hNDdfj4uCxriYGSht/C6aFRLQ2Yce |
||||
|
JX2Xc7Eo6R6CFeTdRiwMv/B3ueC2VkdB79U4veeV6HVnzywrcdFj/+djYYFypkej |
||||
|
GfgxhnHjANEGu4S3Ebb6rTvboalUP+qXiyjbH0C9DjLzNYa2Cs1/Mt9/nfzlz2/A |
||||
|
yglZlkggnJwF44Z7DdeSOdVEsRYCbqzBSG7n7eiJfKhbrSbCWwKCAQEA6pf7f7iL |
||||
|
Boy7T8d5tEu0D+anN8m6RFzM4wsqr4/2MPeIyt+8meFw/JrtV1jfoQIx+PPN9FaJ |
||||
|
X6SoSXwxyaAzwz9ZvFjhOnyN5gbagHfE65onifq+ooBn40ErlT2hw2yXRHNswG7V |
||||
|
Z9jsJ8nRbR/qU9ojFHtiFX1vVeKsQCnTJGjBV3kMtOCGhvuMywTB5GcBLULY5grr |
||||
|
bF3RyIlzdDjFg18+iMFWe0Ny5OHuK812HyGQhoRc6M+KjyJucQdyyamqGzb1vSm+ |
||||
|
jggp+h7VVunkfJ6h5dcM2zWQZdc7+JSCToagNBCYNL5fs3sC6Q5+UKJIFLxrcwRF |
||||
|
iTvD8n9+rFJ2DwKCAQEAw7ErdaH4JKdWdIoqIoCnsnpcBcZ3Cs1664UXH4U5gYer |
||||
|
w+20rVI9m83RFHxxhAYF1UkW26Yv8Ap6a4SCEvWDtn/Im4H6nYZeaXZKhsGEz34N |
||||
|
ugI9U4pkrShsdUKZExsBpfLwerW0k758cssJWMMAxdomDkSo1HYFltm9XlNzrmkR |
||||
|
0NQI7/NgehGOOsmV7+Smpdw+C8mMQZ4oIw1j5638V5DFgRwISmF/it3qEMHxDSSM |
||||
|
YLVVp5JD7sax6ChvelwXUXSeZFFlB9geMRAk10akmr79f7jOWJnz/rOd0VFAxgLi |
||||
|
ruyr37uJLAVHBNvXM3uNrC7+IJcWbv+vRWCXbAGrdwKCAQBtnBeBhJtIsyatzvkZ |
||||
|
eamnKFEHKvUiDe4ZQ1VtdClGldHPYJyBlakyDb1Ja5gJZbotpNSdDnXfP1L2CtZE |
||||
|
a9rjpkzqSOjrZ9jxGlCrZ8qVfpBs0sCRssdXklKnx4U2hx1ieT/d5atGez9UE+ML |
||||
|
Rrc4+JodbszUV6hWi7OJw0EJKPz1PvTl6mZQ2WXeUdm6Ozp8iFhJm96F4owrU7Wj |
||||
|
HweCK1VPlm4u58PeF4Yt5zECuK8LevriOF54JFFP9Hf4q5J0ZsiI2uFTAZODbzal |
||||
|
BmGgrInelw1FuxA91UQLEHCV+icOTJahRjX26Unh1MjGKhzdu2/E7MEEru0N9+4a |
||||
|
2+iXAoIBAE34SViVMElqYwgMBL26hRaXqhKjAMtNE6zDWnM0obT6WXW3QEXOfr2V |
||||
|
Q7jl3FS+EZTpijH6BR+fDSfJpAnpyJDuWP+cyj35S6S5fPg0IraJgu6Z9dVTTsmv |
||||
|
UYdnAZabLAzyvt4lh81WGD+kphS3nZc3U/JbaOk+HPv9xXXPykezlWWfFfCFB+ub |
||||
|
ExBZQWRTthJfrlkD9N4wJc3Rh/zHVcON6yOGB8ebETZDNP94RpL1/PiLR5V8sZRx |
||||
|
lnDpq4EVMDVEQde2loqJkX368LLVcsA1WMuK1qx2qsDQ0BCWTziV7bvEkLaUAhOI |
||||
|
BsPo09WvZMM19gsGJ+oR9cOuuKZQBAECggEBAN1AvO4tyhibAocnOOV0iO33G+li |
||||
|
nHIk4+W9cwWd5hxxoMS/2YWiUa4HqCDleBeRYwxXv0yayFqTUBPFSAE/+ZRn0ADj |
||||
|
jKs1nU1o49BHUPa7rIyc29x9FpPoNUIXVEFnJdQQxkJrPeXdm3VyAmfUuylDNuBs |
||||
|
ZRxiOqb3hvN6cbxDuN7bBfDrC4BJJZhICmSfvwZbvfbD3zwJpPtI2OfD8uij3mSd |
||||
|
advsYHl3ZLBv9d6KiqSEzS13PPhiTbyoVmB2M4VC1B+4W2tNCYgEzLROf8+GnuSJ |
||||
|
wvQOf2IX+QY9Q3gbSP9c/pbwHmC5ttvZQiA2VJW4XxpU8JMRwAn06p+BXb8= |
||||
|
-----END RSA PRIVATE KEY----- |
||||
@ -0,0 +1,17 @@ |
|||||
|
--- |
||||
|
dc=example,dc=com: |
||||
|
cn: |
||||
|
- Top object |
||||
|
cn=fred flintstone,dc=example,dc=com: |
||||
|
cn: |
||||
|
- Fred Flintstone |
||||
|
sn: |
||||
|
- Flintstone |
||||
|
mail: |
||||
|
- fred@bedrock.org |
||||
|
- fred.flintstone@bedrock.org |
||||
|
cn=wilma flintstone,dc=example,dc=com: |
||||
|
cn: |
||||
|
- Wilma Flintstone |
||||
|
mail: |
||||
|
- wilma@bedrock.org |
||||
@ -0,0 +1,34 @@ |
|||||
|
require 'openssl' |
||||
|
|
||||
|
# Taken directly from echo_svr.rb in the Ruby openssl examples |
||||
|
|
||||
|
key = OpenSSL::PKey::RSA.new(4096) do |
||||
|
print '.' |
||||
|
$stdout.flush |
||||
|
end |
||||
|
puts |
||||
|
cert = OpenSSL::X509::Certificate.new |
||||
|
cert.version = 2 |
||||
|
cert.serial = 0 |
||||
|
name = OpenSSL::X509::Name.new([%w[C JP], %w[O TEST], %w[CN localhost]]) |
||||
|
cert.subject = name |
||||
|
cert.issuer = name |
||||
|
cert.not_before = Time.now |
||||
|
cert.not_after = Time.now + 3600 |
||||
|
cert.public_key = key.public_key |
||||
|
ef = OpenSSL::X509::ExtensionFactory.new(nil, cert) |
||||
|
cert.extensions = [ |
||||
|
ef.create_extension('basicConstraints', 'CA:FALSE'), |
||||
|
ef.create_extension('subjectKeyIdentifier', 'hash'), |
||||
|
ef.create_extension('extendedKeyUsage', 'serverAuth'), |
||||
|
ef.create_extension('keyUsage', |
||||
|
'keyEncipherment,dataEncipherment,digitalSignature') |
||||
|
] |
||||
|
ef.issuer_certificate = cert |
||||
|
cert.add_extension ef.create_extension('authorityKeyIdentifier', |
||||
|
'keyid:always,issuer:always') |
||||
|
cert.sign(key, OpenSSL::Digest.new('SHA1')) |
||||
|
|
||||
|
# Write to disk |
||||
|
File.open('key.pem', 'w', 0o600) { |f| f << key.to_pem } |
||||
|
File.open('cert.pem', 'w', 0o644) { |f| f << cert.to_pem } |
||||
@ -0,0 +1,112 @@ |
|||||
|
#!/usr/local/bin/ruby -w |
||||
|
|
||||
|
# This is a trivial LDAP server which just stores directory entries in RAM. |
||||
|
# It does no validation or authentication. This is intended just to |
||||
|
# demonstrate the API, it's not for real-world use!! |
||||
|
|
||||
|
$:.unshift('../lib') |
||||
|
$debug = true |
||||
|
|
||||
|
require 'ldap/server' |
||||
|
|
||||
|
# We subclass the Operation class, overriding the methods to do what we need |
||||
|
|
||||
|
class HashOperation < LDAP::Server::Operation |
||||
|
def initialize(connection, messageID, hash) |
||||
|
super(connection, messageID) |
||||
|
@hash = hash # an object reference to our directory data |
||||
|
end |
||||
|
|
||||
|
def search(basedn, scope, deref, filter) |
||||
|
basedn = basedn.downcase |
||||
|
|
||||
|
case scope |
||||
|
when LDAP::Server::BaseObject |
||||
|
# client asked for single object by DN |
||||
|
obj = @hash[basedn] |
||||
|
raise LDAP::ResultError::NoSuchObject unless obj |
||||
|
send_SearchResultEntry(basedn, obj) if LDAP::Server::Filter.run(filter, obj) |
||||
|
|
||||
|
when LDAP::Server::WholeSubtree |
||||
|
@hash.each do |dn, av| |
||||
|
next unless dn.index(basedn, -basedn.length) # under basedn? |
||||
|
next unless LDAP::Server::Filter.run(filter, av) # attribute filter? |
||||
|
send_SearchResultEntry(dn, av) |
||||
|
end |
||||
|
|
||||
|
else |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "OneLevel not implemented" |
||||
|
|
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def add(dn, av) |
||||
|
dn = dn.downcase |
||||
|
raise LDAP::ResultError::EntryAlreadyExists if @hash[dn] |
||||
|
@hash[dn] = av |
||||
|
end |
||||
|
|
||||
|
def del(dn) |
||||
|
dn = dn.downcase |
||||
|
raise LDAP::ResultError::NoSuchObject unless @hash.has_key?(dn) |
||||
|
@hash.delete(dn) |
||||
|
end |
||||
|
|
||||
|
def modify(dn, ops) |
||||
|
dn = dn.downcase |
||||
|
entry = @hash[dn] |
||||
|
raise LDAP::ResultError::NoSuchObject unless entry |
||||
|
ops.each do |attr, vals| |
||||
|
op = vals.shift |
||||
|
case op |
||||
|
when :add |
||||
|
entry[attr] ||= [] |
||||
|
entry[attr] += vals |
||||
|
entry[attr].uniq! |
||||
|
when :delete |
||||
|
if vals == [] |
||||
|
entry.delete(attr) |
||||
|
else |
||||
|
vals.each { |v| entry[attr].delete(v) } |
||||
|
end |
||||
|
when :replace |
||||
|
entry[attr] = vals |
||||
|
end |
||||
|
entry.delete(attr) if entry[attr] == [] |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# This is the shared object which carries our actual directory entries. |
||||
|
# It's just a hash of {dn=>entry}, where each entry is {attr=>[val,val,...]} |
||||
|
|
||||
|
directory = {} |
||||
|
|
||||
|
# Let's put some backing store on it |
||||
|
|
||||
|
require 'yaml' |
||||
|
begin |
||||
|
File.open("ldapdb.yaml") { |f| directory = YAML::load(f.read) } |
||||
|
rescue Errno::ENOENT |
||||
|
end |
||||
|
|
||||
|
at_exit do |
||||
|
File.open("ldapdb.new","w") { |f| f.write(YAML::dump(directory)) } |
||||
|
File.rename("ldapdb.new","ldapdb.yaml") |
||||
|
end |
||||
|
|
||||
|
# Listen for incoming LDAP connections. For each one, create a Connection |
||||
|
# object, which will invoke a HashOperation object for each request. |
||||
|
|
||||
|
s = LDAP::Server.new( |
||||
|
:port => 1389, |
||||
|
:nodelay => true, |
||||
|
:listen => 10, |
||||
|
# :ssl_key_file => "key.pem", |
||||
|
# :ssl_cert_file => "cert.pem", |
||||
|
# :ssl_on_connect => true, |
||||
|
:operation_class => HashOperation, |
||||
|
:operation_args => [directory] |
||||
|
) |
||||
|
s.run_tcpserver |
||||
|
s.join |
||||
@ -0,0 +1,161 @@ |
|||||
|
#!/usr/local/bin/ruby -w |
||||
|
|
||||
|
$:.unshift('../lib') |
||||
|
require 'ldap/server' |
||||
|
require 'mysql' # <http://www.tmtm.org/en/ruby/mysql/> |
||||
|
require 'thread' |
||||
|
require 'resolv-replace' # ruby threading DNS client |
||||
|
|
||||
|
# An example of an LDAP to SQL gateway. We have a MySQL table which |
||||
|
# contains (login_id,login,passwd) combinations, e.g. |
||||
|
# |
||||
|
# +----------+----------+--------+ |
||||
|
# | login_id | login | passwd | |
||||
|
# +----------+----------+--------+ |
||||
|
# | 1 | brian | foobar | |
||||
|
# | 2 | caroline | boing | |
||||
|
# +----------+----------+--------+ |
||||
|
# |
||||
|
# We support LDAP searches for (uid=login), returning a synthesised DN and |
||||
|
# Maildir attribute, and we support LDAP binds to validate passwords. We |
||||
|
# keep a cache of recent lookups so that a bind to validate a password |
||||
|
# doesn't cause a second SQL query. Since we're multi-threaded, this should |
||||
|
# work even if the bind occurs on a different client connection to the search. |
||||
|
# |
||||
|
# To test: |
||||
|
# ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" "(uid=brian)" |
||||
|
# |
||||
|
# ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" \ |
||||
|
# -D "id=1,dc=example,dc=com" -W "(uid=brian)" |
||||
|
|
||||
|
$debug = true |
||||
|
SQL_CONNECT = ["1.2.3.4", "myuser", "mypass", "mydb"] |
||||
|
TABLE = "logins" |
||||
|
SQL_POOL_SIZE = 5 |
||||
|
PW_CACHE_SIZE = 100 |
||||
|
BASEDN = "dc=example,dc=com" |
||||
|
LDAP_PORT = 1389 |
||||
|
|
||||
|
# A thread-safe pool of persistent MySQL connections |
||||
|
|
||||
|
class SQLPool |
||||
|
def initialize(n, *args) |
||||
|
@args = args |
||||
|
@pool = Queue.new # this is a thread-safe queue |
||||
|
n.times { @pool.push nil } # create connections on demand |
||||
|
end |
||||
|
|
||||
|
def borrow |
||||
|
conn = @pool.pop || Mysql::new(*@args) |
||||
|
yield conn |
||||
|
rescue Exception |
||||
|
conn = nil # put 'nil' back into the pool |
||||
|
raise |
||||
|
ensure |
||||
|
@pool.push conn |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# An simple LRU cache of username->password. It's linearly searched |
||||
|
# so don't make it too big. |
||||
|
|
||||
|
class LRUCache |
||||
|
def initialize(size) |
||||
|
@size = size |
||||
|
@cache = [] # [[key,val],[key,val],...] |
||||
|
@mutex = Mutex.new |
||||
|
end |
||||
|
|
||||
|
def add(id,data) |
||||
|
@mutex.synchronize do |
||||
|
@cache.delete_if { |k,v| k == id } |
||||
|
@cache.unshift [id,data] |
||||
|
@cache.pop while @cache.size > @size |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def find(id) |
||||
|
@mutex.synchronize do |
||||
|
index = entry = nil |
||||
|
@cache.each_with_index do |e, i| |
||||
|
if e[0] == id |
||||
|
entry = e |
||||
|
index = i |
||||
|
break |
||||
|
end |
||||
|
end |
||||
|
return nil unless index |
||||
|
@cache.delete_at(index) |
||||
|
@cache.unshift entry |
||||
|
return entry[1] |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
|
||||
|
class SQLOperation < LDAP::Server::Operation |
||||
|
def self.setcache(cache,pool) |
||||
|
@@cache = cache |
||||
|
@@pool = pool |
||||
|
end |
||||
|
|
||||
|
# Handle searches of the form "(uid=<foo>)" using SQL backend |
||||
|
# (uid=foo) => [:eq, "uid", matchobj, "foo"] |
||||
|
|
||||
|
def search(basedn, scope, deref, filter) |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "Bad base DN" unless basedn == BASEDN |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "Bad filter" unless filter[0..1] == [:eq, "uid"] |
||||
|
uid = filter[3] |
||||
|
@@pool.borrow do |sql| |
||||
|
q = "select login_id,passwd from #{TABLE} where login='#{sql.quote(uid)}'" |
||||
|
puts "SQL Query #{sql.object_id}: #{q}" if $debug |
||||
|
res = sql.query(q) |
||||
|
res.each do |login_id,passwd| |
||||
|
@@cache.add(login_id, passwd) |
||||
|
send_SearchResultEntry("id=#{login_id},#{BASEDN}", { |
||||
|
"maildir"=>["/netapp/#{uid}/"], |
||||
|
}) |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Validate passwords |
||||
|
|
||||
|
def simple_bind(version, dn, password) |
||||
|
return if dn.nil? # accept anonymous |
||||
|
|
||||
|
raise LDAP::ResultError::UnwillingToPerform unless dn =~ /\Aid=(\d+),#{BASEDN}\z/ |
||||
|
login_id = $1 |
||||
|
dbpw = @@cache.find(login_id) |
||||
|
unless dbpw |
||||
|
@@pool.borrow do |sql| |
||||
|
q = "select passwd from #{TABLE} where login_id=#{login_id}" |
||||
|
puts "SQL Query #{sql.object_id}: #{q}" if $debug |
||||
|
res = sql.query(q) |
||||
|
if res.num_rows == 1 |
||||
|
dbpw = res.fetch_row[0] |
||||
|
@@cache.add(login_id, dbpw) |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
raise LDAP::ResultError::InvalidCredentials unless dbpw and dbpw != "" and dbpw == password |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Build the objects we need |
||||
|
|
||||
|
cache = LRUCache.new(PW_CACHE_SIZE) |
||||
|
pool = SQLPool.new(SQL_POOL_SIZE, *SQL_CONNECT) |
||||
|
SQLOperation.setcache(cache,pool) |
||||
|
|
||||
|
s = LDAP::Server.new( |
||||
|
:port => LDAP_PORT, |
||||
|
:nodelay => true, |
||||
|
:listen => 10, |
||||
|
# :ssl_key_file => "key.pem", |
||||
|
# :ssl_cert_file => "cert.pem", |
||||
|
# :ssl_on_connect => true, |
||||
|
:operation_class => SQLOperation |
||||
|
) |
||||
|
s.run_tcpserver |
||||
|
s.join |
||||
@ -0,0 +1,11 @@ |
|||||
|
CREATE TABLE logins ( |
||||
|
login_id MEDIUMINT NOT NULL AUTO_INCREMENT, |
||||
|
login CHAR(30) NOT NULL, |
||||
|
passwd CHAR(30), |
||||
|
PRIMARY KEY (login_id) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
||||
|
|
||||
|
INSERT INTO logins(login, passwd) VALUES |
||||
|
('brian', 'foobar'), ('caroline', 'boing'); |
||||
|
|
||||
|
SELECT * FROM logins; |
||||
@ -0,0 +1,172 @@ |
|||||
|
#!/usr/local/bin/ruby -w |
||||
|
|
||||
|
# This is similar to rbslapd1.rb but here we use TOMITA Masahiro's prefork |
||||
|
# library: <http://raa.ruby-lang.org/project/prefork/> |
||||
|
# Advantages over Ruby threading: |
||||
|
# - each client connection is handled in its own process; don't need |
||||
|
# to worry about Ruby thread blocking (except if one client issues |
||||
|
# overlapping LDAP operations down the same connection, which is uncommon) |
||||
|
# - better scalability on multi-processor systems |
||||
|
# - better scalability on single-processor systems (e.g. shouldn't hit |
||||
|
# max FDs per process limit) |
||||
|
# Disadvantages: |
||||
|
# - client connections can't share state in RAM. So our shared directory |
||||
|
# now has to be read from disk, and flushed to disk after every update. |
||||
|
# |
||||
|
# Additionally, I have added schema support. An LDAP v3 client can |
||||
|
# query the schema remotely, and adds/modifies have data validated. |
||||
|
|
||||
|
$:.unshift('../lib') |
||||
|
|
||||
|
require 'ldap/server' |
||||
|
require 'ldap/server/schema' |
||||
|
require 'yaml' |
||||
|
|
||||
|
$debug = nil # $stderr |
||||
|
|
||||
|
# An object to keep our in-RAM database and synchronise it to disk |
||||
|
# when necessary |
||||
|
|
||||
|
class Directory |
||||
|
attr_reader :data |
||||
|
|
||||
|
def initialize(filename) |
||||
|
@filename = filename |
||||
|
@stat = nil |
||||
|
update |
||||
|
end |
||||
|
|
||||
|
# synchronise with directory on disk (re-read if it has changed) |
||||
|
|
||||
|
def update |
||||
|
begin |
||||
|
tmp = {} |
||||
|
sb = File.stat(@filename) |
||||
|
return if @stat and @stat.ino == sb.ino and @stat.mtime == sb.mtime |
||||
|
File.open(@filename) do |f| |
||||
|
tmp = YAML::load(f.read) |
||||
|
@stat = f.stat |
||||
|
end |
||||
|
rescue Errno::ENOENT |
||||
|
end |
||||
|
@data = tmp |
||||
|
end |
||||
|
|
||||
|
# write back to disk |
||||
|
|
||||
|
def write |
||||
|
File.open(@filename+".new","w") { |f| f.write(YAML::dump(@data)) } |
||||
|
File.rename(@filename+".new",@filename) |
||||
|
@stat = File.stat(@filename) |
||||
|
end |
||||
|
|
||||
|
# run a block while holding a lock on the database |
||||
|
|
||||
|
def lock |
||||
|
File.open(@filename+".lock","w") do |f| |
||||
|
f.flock(File::LOCK_EX) # will block here until lock available |
||||
|
yield |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# We subclass the Operation class, overriding the methods to do what we need |
||||
|
|
||||
|
class DirOperation < LDAP::Server::Operation |
||||
|
def initialize(connection, messageID, dir) |
||||
|
super(connection, messageID) |
||||
|
@dir = dir |
||||
|
end |
||||
|
|
||||
|
def search(basedn, scope, deref, filter) |
||||
|
$debug << "Search: basedn=#{basedn.inspect}, scope=#{scope.inspect}, deref=#{deref.inspect}, filter=#{filter.inspect}\n" if $debug |
||||
|
basedn = basedn.downcase |
||||
|
|
||||
|
case scope |
||||
|
when LDAP::Server::BaseObject |
||||
|
# client asked for single object by DN |
||||
|
@dir.update |
||||
|
obj = @dir.data[basedn] |
||||
|
raise LDAP::ResultError::NoSuchObject unless obj |
||||
|
ok = LDAP::Server::Filter.run(filter, obj) |
||||
|
$debug << "Match=#{ok.inspect}: #{obj.inspect}\n" if $debug |
||||
|
send_SearchResultEntry(basedn, obj) if ok |
||||
|
|
||||
|
when LDAP::Server::WholeSubtree |
||||
|
@dir.update |
||||
|
@dir.data.each do |dn, av| |
||||
|
$debug << "Considering #{dn}\n" if $debug |
||||
|
next unless dn.index(basedn, -basedn.length) # under basedn? |
||||
|
next unless LDAP::Server::Filter.run(filter, av) # attribute filter? |
||||
|
$debug << "Sending: #{av.inspect}\n" if $debug |
||||
|
send_SearchResultEntry(dn, av) |
||||
|
end |
||||
|
|
||||
|
else |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "OneLevel not implemented" |
||||
|
|
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def add(dn, entry) |
||||
|
entry = @schema.validate(entry) |
||||
|
entry['createTimestamp'] = [Time.now.gmtime.strftime("%Y%m%d%H%MZ")] |
||||
|
entry['creatorsName'] = [@connection.binddn.to_s] |
||||
|
# FIXME: normalize the DN and check it's below our root DN |
||||
|
# FIXME: validate that a superior object exists |
||||
|
# FIXME: validate that entry contains the RDN attribute (yuk) |
||||
|
dn = dn.downcase |
||||
|
@dir.lock do |
||||
|
@dir.update |
||||
|
raise LDAP::ResultError::EntryAlreadyExists if @dir.data[dn] |
||||
|
@dir.data[dn] = entry |
||||
|
@dir.write |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def del(dn) |
||||
|
dn = dn.downcase |
||||
|
@dir.lock do |
||||
|
@dir.update |
||||
|
raise LDAP::ResultError::NoSuchObject unless @dir.data.has_key?(dn) |
||||
|
@dir.data.delete(dn) |
||||
|
@dir.write |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def modify(dn, ops) |
||||
|
dn = dn.downcase |
||||
|
@dir.lock do |
||||
|
@dir.update |
||||
|
entry = @dir.data[dn] |
||||
|
raise LDAP::ResultError::NoSuchObject unless entry |
||||
|
entry = @schema.validate(ops, entry) # also does the update |
||||
|
entry['modifyTimestamp'] = [Time.now.gmtime.strftime("%Y%m%d%H%MZ")] |
||||
|
entry['modifiersName'] = [@connection.binddn.to_s] |
||||
|
@dir.data[dn] = entry |
||||
|
@dir.write |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
directory = Directory.new("ldapdb.yaml") |
||||
|
|
||||
|
schema = LDAP::Server::Schema.new |
||||
|
schema.load_system |
||||
|
schema.load_file("../test/core.schema") |
||||
|
schema.resolve_oids |
||||
|
|
||||
|
s = LDAP::Server.new( |
||||
|
:port => 1389, |
||||
|
:nodelay => true, |
||||
|
:listen => 10, |
||||
|
# :ssl_key_file => "key.pem", |
||||
|
# :ssl_cert_file => "cert.pem", |
||||
|
# :ssl_on_connect => true, |
||||
|
:operation_class => DirOperation, |
||||
|
:operation_args => [directory], |
||||
|
:schema => schema, |
||||
|
:namingContexts => ['dc=example,dc=com'] |
||||
|
) |
||||
|
s.run_prefork |
||||
|
s.join |
||||
@ -0,0 +1,90 @@ |
|||||
|
#!/usr/local/bin/ruby -w |
||||
|
|
||||
|
# This is a modified version of rbslapd1.rb which uses a Router instead of |
||||
|
# subclassing the LDAP::Server::Operation class. |
||||
|
|
||||
|
# This is a trivial LDAP server which just stores directory entries in RAM. |
||||
|
# It does no validation or authentication. This is intended just to |
||||
|
# demonstrate the API, it's not for real-world use!! |
||||
|
|
||||
|
$:.unshift('../lib') |
||||
|
$debug = true |
||||
|
|
||||
|
require 'ldap/server' |
||||
|
require 'ldap/server/router' |
||||
|
|
||||
|
$logger = Logger.new($stderr) |
||||
|
|
||||
|
class LDAPController |
||||
|
def self.bind(request, version, dn, password, params) |
||||
|
$logger.debug "Catchall bind request" |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "Invalid bind DN" |
||||
|
end |
||||
|
|
||||
|
def self.bindUser(request, version, dn, password, params) |
||||
|
if params[:uid].nil? or |
||||
|
params[:uid] != 'admin' or |
||||
|
password != 'adminpassword' |
||||
|
$logger.warn "Denied access for user #{params[:uid]}: Invalid credentials" |
||||
|
raise LDAP::ResultError::InvalidCredentials, "Invalid credentials" |
||||
|
end |
||||
|
|
||||
|
$logger.info "Authenticated user #{params[:uid]}" |
||||
|
end |
||||
|
|
||||
|
def self.search(request, baseObject, scope, deref, filter, params) |
||||
|
$logger.info "Catchall search request for #{baseObject}" |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "Invalid search DN" |
||||
|
end |
||||
|
|
||||
|
def self.searchUsers(request, baseObject, scope, deref, filter, params) |
||||
|
$logger.info "Search users" |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
router = LDAP::Server::Router.new($logger) do |
||||
|
# Different syntax but same thing |
||||
|
bind nil => "LDAPController#bind" |
||||
|
route :bind, nil => "LDAPController#bind" |
||||
|
|
||||
|
# Bind a route using variables. A hash with the variables will be passed |
||||
|
# to your function as last argument. |
||||
|
bind "uid=:uid,ou=Users,dc=mydomain,dc=com" => "LDAPController#bindUser" |
||||
|
|
||||
|
search nil => "LDAPController#search" |
||||
|
search "ou=Users,dc=mydomain,dc=com" => "LDAPController#searchUsers" |
||||
|
end |
||||
|
|
||||
|
|
||||
|
# This is the shared object which carries our actual directory entries. |
||||
|
# It's just a hash of {dn=>entry}, where each entry is {attr=>[val,val,...]} |
||||
|
|
||||
|
directory = {} |
||||
|
|
||||
|
# Let's put some backing store on it |
||||
|
|
||||
|
require 'yaml' |
||||
|
begin |
||||
|
File.open("ldapdb.yaml") { |f| directory = YAML::load(f.read) } |
||||
|
rescue Errno::ENOENT |
||||
|
end |
||||
|
|
||||
|
at_exit do |
||||
|
File.open("ldapdb.new","w") { |f| f.write(YAML::dump(directory)) } |
||||
|
File.rename("ldapdb.new","ldapdb.yaml") |
||||
|
end |
||||
|
|
||||
|
# Listen for incoming LDAP connections. For each one, create a Connection |
||||
|
# object, which will invoke a HashOperation object for each request. |
||||
|
|
||||
|
s = LDAP::Server.new( |
||||
|
:port => 1389, |
||||
|
:nodelay => true, |
||||
|
:listen => 10, |
||||
|
# :ssl_key_file => "key.pem", |
||||
|
# :ssl_cert_file => "cert.pem", |
||||
|
# :ssl_on_connect => true, |
||||
|
:router => router |
||||
|
) |
||||
|
s.run_tcpserver |
||||
|
s.join |
||||
@ -0,0 +1,73 @@ |
|||||
|
#!/usr/local/bin/ruby -w |
||||
|
|
||||
|
# Example server that listens on both a port and a UNIX domain socket |
||||
|
# Try it using: |
||||
|
# $ ldapsearch -LLL -H ldap://localhost:1389 -D uid=whatever -b ou=Users,dc=mydomain,dc=com |
||||
|
# $ ldapsearch -LLL -H ldapi://%2ftmp%2frbslapd5.sock -D uid=whatever -b ou=Users,dc=mydomain,dc=com |
||||
|
|
||||
|
$:.unshift('../lib') |
||||
|
$debug = true |
||||
|
|
||||
|
require 'fileutils' |
||||
|
|
||||
|
require 'ldap/server' |
||||
|
require 'ldap/server/router' |
||||
|
|
||||
|
$logger = Logger.new($stderr) |
||||
|
|
||||
|
class LDAPController |
||||
|
def self.bind(request, version, dn, password, params) |
||||
|
$logger.info "Processing bind route for \'#{dn}\' with password \'#{password}\'" |
||||
|
end |
||||
|
|
||||
|
def self.search(request, baseObject, scope, deref, filter, params) |
||||
|
$logger.info "Processing search route for #{baseObject}" |
||||
|
|
||||
|
h = { |
||||
|
'uid' => 'jdoe', |
||||
|
'objectClass' => 'userAccount', |
||||
|
'givenName' => 'John', |
||||
|
'sn' => 'Doe' |
||||
|
} |
||||
|
request.send_SearchResultEntry("uid=jdoe,#{baseObject}", h) |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
router = LDAP::Server::Router.new($logger) do |
||||
|
bind nil => "LDAPController#bind" |
||||
|
|
||||
|
search "ou=Users,dc=mydomain,dc=com" => "LDAPController#search" |
||||
|
end |
||||
|
|
||||
|
params = { |
||||
|
:nodelay => true, |
||||
|
:listen => 10, |
||||
|
:router => router |
||||
|
} |
||||
|
|
||||
|
# Listen on IP address and port |
||||
|
|
||||
|
params[:bindaddr] = '127.0.0.1' # Leave this blank to listen on 0.0.0.0 |
||||
|
params[:port] = 1389 |
||||
|
|
||||
|
addr_server = LDAP::Server.new params |
||||
|
addr_server.run_tcpserver |
||||
|
|
||||
|
# Listen on socket |
||||
|
|
||||
|
params.delete :bindaddr |
||||
|
params.delete :port |
||||
|
params[:socket] = '/tmp/rbslapd5.sock' |
||||
|
|
||||
|
FileUtils::rm_f params[:socket] |
||||
|
|
||||
|
socket_server = LDAP::Server.new params |
||||
|
socket_server.run_tcpserver |
||||
|
|
||||
|
trap 'INT' do |
||||
|
addr_server.stop |
||||
|
socket_server.stop |
||||
|
end |
||||
|
|
||||
|
addr_server.join |
||||
|
socket_server.join |
||||
@ -0,0 +1,75 @@ |
|||||
|
#!/usr/local/bin/ruby -w |
||||
|
|
||||
|
# Slightly modified version of rbslapd5.rb which demonstrates dropping |
||||
|
# root privileges after binding to port 389 |
||||
|
# |
||||
|
# Run this script with `sudo` |
||||
|
|
||||
|
$:.unshift('../lib') |
||||
|
$debug = true |
||||
|
|
||||
|
require 'fileutils' |
||||
|
|
||||
|
require 'ldap/server' |
||||
|
require 'ldap/server/router' |
||||
|
|
||||
|
$logger = Logger.new($stderr) |
||||
|
|
||||
|
class LDAPController |
||||
|
def self.bind(request, version, dn, password, params) |
||||
|
$logger.info "Processing bind route for \'#{dn}\' with password \'#{password}\'" |
||||
|
end |
||||
|
|
||||
|
def self.search(request, baseObject, scope, deref, filter, params) |
||||
|
$logger.info "Processing search route for #{baseObject}" |
||||
|
|
||||
|
h = { |
||||
|
'uid' => 'jdoe', |
||||
|
'objectClass' => 'userAccount', |
||||
|
'givenName' => 'John', |
||||
|
'sn' => 'Doe' |
||||
|
} |
||||
|
request.send_SearchResultEntry("uid=jdoe,#{baseObject}", h) |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
router = LDAP::Server::Router.new($logger) do |
||||
|
bind nil => "LDAPController#bind" |
||||
|
|
||||
|
search "ou=Users,dc=mydomain,dc=com" => "LDAPController#search" |
||||
|
end |
||||
|
|
||||
|
params = { |
||||
|
:nodelay => true, |
||||
|
:listen => 10, |
||||
|
:router => router |
||||
|
} |
||||
|
|
||||
|
# Listen on IP address and port |
||||
|
|
||||
|
params[:bindaddr] = '127.0.0.1' # Leave this blank to listen on 0.0.0.0 |
||||
|
params[:port] = 389 |
||||
|
params[:user] = 'ldap' |
||||
|
params[:group] = 'ldap' |
||||
|
|
||||
|
addr_server = LDAP::Server.new params |
||||
|
addr_server.run_tcpserver |
||||
|
|
||||
|
# Listen on socket |
||||
|
|
||||
|
params.delete :bindaddr |
||||
|
params.delete :port |
||||
|
params[:socket] = '/tmp/rbslapd6.sock' |
||||
|
|
||||
|
FileUtils::rm_f params[:socket] |
||||
|
|
||||
|
socket_server = LDAP::Server.new params |
||||
|
socket_server.run_tcpserver |
||||
|
|
||||
|
trap 'INT' do |
||||
|
addr_server.stop |
||||
|
socket_server.stop |
||||
|
end |
||||
|
|
||||
|
addr_server.join |
||||
|
socket_server.join |
||||
@ -0,0 +1,37 @@ |
|||||
|
#!/usr/local/bin/ruby |
||||
|
|
||||
|
require 'ldap' |
||||
|
|
||||
|
CHILDREN = 10 |
||||
|
CONNECTS = 1 # per child |
||||
|
SEARCHES = 100 # per connection |
||||
|
|
||||
|
pids = [] |
||||
|
CHILDREN.times do |
||||
|
pids << fork do |
||||
|
CONNECTS.times do |
||||
|
conn = LDAP::Conn.new("localhost",1389) |
||||
|
conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) |
||||
|
conn.bind |
||||
|
SEARCHES.times do |
||||
|
res = conn.search("cn=Fred Flintstone,dc=example,dc=com", LDAP::LDAP_SCOPE_BASE, |
||||
|
"(objectclass=*)") do |e| |
||||
|
#puts "#{$$} #{e.dn.inspect}" |
||||
|
end |
||||
|
end |
||||
|
conn.unbind |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
okcount = 0 |
||||
|
badcount = 0 |
||||
|
pids.each do |p| |
||||
|
Process.wait(p) |
||||
|
if $?.exitstatus == 0 |
||||
|
okcount += 1 |
||||
|
else |
||||
|
badcount += 1 |
||||
|
end |
||||
|
end |
||||
|
puts "Children finished: #{okcount} ok, #{badcount} failed" |
||||
|
exit badcount > 0 ? 1 : 0 |
||||
@ -0,0 +1,4 @@ |
|||||
|
require 'ldap/server/result' |
||||
|
require 'ldap/server/connection' |
||||
|
require 'ldap/server/operation' |
||||
|
require 'ldap/server/server' |
||||
@ -0,0 +1,257 @@ |
|||||
|
require 'thread' |
||||
|
require 'openssl' |
||||
|
require 'ldap/server/result' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
# An object which handles an LDAP connection. Note that LDAP allows |
||||
|
# requests and responses to be exchanged asynchronously: e.g. a client |
||||
|
# can send three requests, and the three responses can come back in |
||||
|
# any order. For that reason, we start a new thread for each request, |
||||
|
# and we need a mutex on the io object so that multiple responses don't |
||||
|
# interfere with each other. |
||||
|
|
||||
|
class Connection |
||||
|
attr_reader :binddn, :version, :opt |
||||
|
|
||||
|
def initialize(io, opt={}) |
||||
|
@io = io |
||||
|
@opt = opt |
||||
|
@mutex = Mutex.new |
||||
|
@threadgroup = ThreadGroup.new |
||||
|
@binddn = nil |
||||
|
@version = 3 |
||||
|
@logger = @opt[:logger] |
||||
|
@ssl = false |
||||
|
|
||||
|
startssl if @opt[:ssl_on_connect] |
||||
|
end |
||||
|
|
||||
|
def log(msg, severity = Logger::INFO) |
||||
|
@logger.add(severity, msg, @io.peeraddr[3]) |
||||
|
end |
||||
|
|
||||
|
def debug msg |
||||
|
log msg, Logger::DEBUG |
||||
|
end |
||||
|
|
||||
|
def log_exception(e) |
||||
|
log "#{e}: #{e.backtrace.join("\n\tfrom ")}", Logger::ERROR |
||||
|
end |
||||
|
|
||||
|
def startssl # :yields: |
||||
|
@mutex.synchronize do |
||||
|
raise LDAP::ResultError::OperationsError if @ssl or @threadgroup.list.size > 0 |
||||
|
yield if block_given? |
||||
|
@io = OpenSSL::SSL::SSLSocket.new(@io, @opt[:ssl_ctx]) |
||||
|
@io.sync_close = true |
||||
|
@io.accept |
||||
|
@ssl = true |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Read one ASN1 element from the given stream. |
||||
|
# Return String containing the raw element. |
||||
|
|
||||
|
def ber_read(io) |
||||
|
blk = io.read(2) # minimum: short tag, short length |
||||
|
throw(:close) if blk.nil? |
||||
|
|
||||
|
codepoints = blk.respond_to?(:codepoints) ? blk.codepoints.to_a : blk |
||||
|
|
||||
|
tag = codepoints[0] & 0x1f |
||||
|
len = codepoints[1] |
||||
|
|
||||
|
if tag == 0x1f # long form |
||||
|
tag = 0 |
||||
|
while true |
||||
|
ch = io.getc |
||||
|
blk << ch |
||||
|
tag = (tag << 7) | (ch & 0x7f) |
||||
|
break if (ch & 0x80) == 0 |
||||
|
end |
||||
|
len = io.getc |
||||
|
blk << len |
||||
|
end |
||||
|
|
||||
|
if (len & 0x80) != 0 # long form |
||||
|
len = len & 0x7f |
||||
|
raise LDAP::ResultError::ProtocolError, "Indefinite length encoding not supported" if len == 0 |
||||
|
offset = blk.length |
||||
|
blk << io.read(len) |
||||
|
# is there a more efficient way of doing this? |
||||
|
len = 0 |
||||
|
blk[offset..-1].each_byte { |b| len = (len << 8) | b } |
||||
|
end |
||||
|
|
||||
|
offset = blk.length |
||||
|
blk << io.read(len) |
||||
|
return blk |
||||
|
# or if we wanted to keep the partial decoding we've done: |
||||
|
# return blk, [blk[0] >> 6, tag], offset |
||||
|
end |
||||
|
|
||||
|
def handle_requests |
||||
|
catch(:close) do |
||||
|
while true |
||||
|
begin |
||||
|
blk = ber_read(@io) |
||||
|
asn1 = OpenSSL::ASN1::decode(blk) |
||||
|
# Debugging: |
||||
|
# puts "Request: #{blk.unpack("H*")}\n#{asn1.inspect}" if $debug |
||||
|
|
||||
|
raise LDAP::ResultError::ProtocolError, "LDAPMessage must be SEQUENCE" unless asn1.is_a?(OpenSSL::ASN1::Sequence) |
||||
|
raise LDAP::ResultError::ProtocolError, "Bad Message ID" unless asn1.value[0].is_a?(OpenSSL::ASN1::Integer) |
||||
|
messageId = asn1.value[0].value |
||||
|
|
||||
|
protocolOp = asn1.value[1] |
||||
|
raise LDAP::ResultError::ProtocolError, "Bad protocolOp" unless protocolOp.is_a?(OpenSSL::ASN1::ASN1Data) |
||||
|
raise LDAP::ResultError::ProtocolError, "Bad protocolOp tag class" unless protocolOp.tag_class == :APPLICATION |
||||
|
|
||||
|
# controls are not properly implemented |
||||
|
c = asn1.value[2] |
||||
|
if c.is_a?(OpenSSL::ASN1::ASN1Data) and c.tag_class == :APPLICATION and c.tag == 0 |
||||
|
controls = c.value |
||||
|
end |
||||
|
|
||||
|
case protocolOp.tag |
||||
|
when 0 # BindRequest |
||||
|
abandon_all |
||||
|
if @opt[:router] |
||||
|
@binddn, @version = @opt[:router].do_bind(self, messageId, protocolOp, controls) |
||||
|
else |
||||
|
operationClass = @opt[:operation_class] |
||||
|
ocArgs = @opt[:operation_args] || [] |
||||
|
@binddn, @version = operationClass.new(self,messageId,*ocArgs). |
||||
|
do_bind(protocolOp, controls) |
||||
|
end |
||||
|
when 2 # UnbindRequest |
||||
|
throw(:close) |
||||
|
|
||||
|
when 3 # SearchRequest |
||||
|
start_op(messageId,protocolOp,controls,:do_search) |
||||
|
|
||||
|
when 6 # ModifyRequest |
||||
|
start_op(messageId,protocolOp,controls,:do_modify) |
||||
|
|
||||
|
when 8 # AddRequest |
||||
|
start_op(messageId,protocolOp,controls,:do_add) |
||||
|
|
||||
|
when 10 # DelRequest |
||||
|
start_op(messageId,protocolOp,controls,:do_del) |
||||
|
|
||||
|
when 12 # ModifyDNRequest |
||||
|
start_op(messageId,protocolOp,controls,:do_modifydn) |
||||
|
|
||||
|
when 14 # CompareRequest |
||||
|
start_op(messageId,protocolOp,controls,:do_compare) |
||||
|
|
||||
|
when 16 # AbandonRequest |
||||
|
abandon(protocolOp.value) |
||||
|
|
||||
|
else |
||||
|
raise LDAP::ResultError::ProtocolError, "Unrecognised protocolOp tag #{protocolOp.tag}" |
||||
|
end |
||||
|
|
||||
|
rescue LDAP::ResultError::ProtocolError, OpenSSL::ASN1::ASN1Error => e |
||||
|
send_notice_of_disconnection(LDAP::ResultError::ProtocolError.new.to_i, e.message) |
||||
|
throw(:close) |
||||
|
|
||||
|
# all other exceptions propagate up and are caught by tcpserver |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
abandon_all |
||||
|
end |
||||
|
|
||||
|
# Start an operation in a Thread. Add this to a ThreadGroup to allow |
||||
|
# the operation to be abandoned later. |
||||
|
# |
||||
|
# When the thread terminates, it automatically drops out of the group. |
||||
|
# |
||||
|
# Note: RFC 2251 4.4.4.1 says behaviour is undefined if |
||||
|
# client sends an overlapping request with same message ID, |
||||
|
# so we don't have to worry about the case where there is |
||||
|
# already a thread with this messageId in @threadgroup. |
||||
|
def start_op(messageId,protocolOp,controls,meth) |
||||
|
operationClass = @opt[:operation_class] |
||||
|
ocArgs = @opt[:operation_args] || [] |
||||
|
thr = Thread.new do |
||||
|
begin |
||||
|
if @opt[:router] |
||||
|
@opt[:router].send meth, self, messageId, protocolOp, controls |
||||
|
else |
||||
|
operationClass.new(self,messageId,*ocArgs). |
||||
|
send(meth,protocolOp,controls) |
||||
|
end |
||||
|
rescue Exception => e |
||||
|
log_exception e |
||||
|
end |
||||
|
end |
||||
|
thr[:messageId] = messageId |
||||
|
@threadgroup.add(thr) |
||||
|
end |
||||
|
|
||||
|
def write(data) |
||||
|
@mutex.synchronize do |
||||
|
@io.write(data) |
||||
|
@io.flush |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def writelock |
||||
|
@mutex.synchronize do |
||||
|
yield @io |
||||
|
@io.flush |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def abandon(messageID) |
||||
|
@mutex.synchronize do |
||||
|
thread = @threadgroup.list.find { |t| t[:messageId] == messageID } |
||||
|
thread.raise LDAP::Abandon if thread |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def abandon_all |
||||
|
@mutex.synchronize do |
||||
|
@threadgroup.list.each do |thread| |
||||
|
thread.raise LDAP::Abandon |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def send_unsolicited_notification(resultCode, opt={}) |
||||
|
protocolOp = [ |
||||
|
OpenSSL::ASN1::Enumerated(resultCode), |
||||
|
OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""), |
||||
|
OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""), |
||||
|
] |
||||
|
if opt[:referral] |
||||
|
rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) } |
||||
|
protocolOp << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION) |
||||
|
end |
||||
|
if opt[:responseName] |
||||
|
protocolOp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION) |
||||
|
end |
||||
|
if opt[:response] |
||||
|
protocolOp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION) |
||||
|
end |
||||
|
message = [ |
||||
|
OpenSSL::ASN1::Integer(0), |
||||
|
OpenSSL::ASN1::Sequence(protocolOp, 24, :IMPLICIT, :APPLICATION), |
||||
|
] |
||||
|
message << opt[:controls] if opt[:controls] |
||||
|
write(OpenSSL::ASN1::Sequence(message).to_der) |
||||
|
end |
||||
|
|
||||
|
def send_notice_of_disconnection(resultCode, errorMessage="") |
||||
|
send_unsolicited_notification(resultCode, |
||||
|
:errorMessage=>errorMessage, |
||||
|
:responseName=>"1.3.6.1.4.1.1466.20036" |
||||
|
) |
||||
|
end |
||||
|
end |
||||
|
end # class Server |
||||
|
end # module LDAP |
||||
@ -0,0 +1,220 @@ |
|||||
|
require 'ldap/server/util' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
class DN |
||||
|
include Enumerable |
||||
|
|
||||
|
attr_reader :dname |
||||
|
|
||||
|
# Combines a set of elements to a syntactically correct DN |
||||
|
# elements is [elements, ...] where elements |
||||
|
# can be either { attr => val } or [attr, val] |
||||
|
def self.join(elements) |
||||
|
LDAP::Server::Operation.join_dn(elements) |
||||
|
end |
||||
|
|
||||
|
def initialize(dn) |
||||
|
@dname = LDAP::Server::Operation.split_dn(dn) |
||||
|
end |
||||
|
|
||||
|
# Returns the value of the first occurrence of attr (bottom-up) |
||||
|
def find_first(attr) |
||||
|
@dname.each do |pair| |
||||
|
return pair[attr.to_s] if pair[attr.to_s] |
||||
|
end |
||||
|
nil |
||||
|
end |
||||
|
|
||||
|
# Returns the value of the last occurrence of attr (bottom-up) |
||||
|
def find_last(attr) |
||||
|
@dname.reverse_each do |pair| |
||||
|
return pair[attr.to_s] if pair[attr.to_s] |
||||
|
end |
||||
|
nil |
||||
|
end |
||||
|
|
||||
|
# Returns all values of all occurrences of attr (bottom-up) |
||||
|
def find(attr) |
||||
|
result = [] |
||||
|
@dname.each do |pair| |
||||
|
result << pair[attr.to_s] if pair[attr.to_s] |
||||
|
end |
||||
|
result |
||||
|
end |
||||
|
|
||||
|
# Returns the value of the n-th occurrence of attr (top-down, 0 is first element) |
||||
|
def find_nth(attr, n) |
||||
|
i = 0 |
||||
|
@dname.each do |pair| |
||||
|
if pair[attr.to_s] |
||||
|
return pair[attr.to_s] if i == n |
||||
|
i += 1 |
||||
|
end |
||||
|
end |
||||
|
nil |
||||
|
end |
||||
|
|
||||
|
# Whether or not the DN starts with dn (bottom-up) |
||||
|
# dn is a string |
||||
|
def start_with?(dn) |
||||
|
needle = LDAP::Server::Operation.split_dn(dn) |
||||
|
|
||||
|
# Needle is longer than haystack |
||||
|
return false if needle.length > @dname.length |
||||
|
|
||||
|
needle_index = 0 |
||||
|
haystack_index = 0 |
||||
|
|
||||
|
while needle_index < needle.length |
||||
|
return false if @dname[haystack_index] != needle[needle_index] |
||||
|
needle_index += 1 |
||||
|
haystack_index += 1 |
||||
|
end |
||||
|
true |
||||
|
end |
||||
|
|
||||
|
# Whether or not the DN starts with a format (bottom-up) (values are ignored) |
||||
|
# dn is a string |
||||
|
def start_with_format?(dn) |
||||
|
needle = LDAP::Server::Operation.split_dn(dn) |
||||
|
|
||||
|
# Needle is longer than haystack |
||||
|
return false if needle.length > @dname.length |
||||
|
|
||||
|
needle_index = 0 |
||||
|
haystack_index = 0 |
||||
|
|
||||
|
while needle_index < needle.length |
||||
|
return false if @dname[haystack_index].keys != needle[needle_index].keys |
||||
|
needle_index += 1 |
||||
|
haystack_index += 1 |
||||
|
end |
||||
|
true |
||||
|
end |
||||
|
|
||||
|
# Whether or not the DN ends with dn (top-down) |
||||
|
# dn is a string |
||||
|
def end_with?(dn) |
||||
|
needle = LDAP::Server::Operation.split_dn(dn) |
||||
|
|
||||
|
# Needle is longer than haystack |
||||
|
return false if needle.length > @dname.length |
||||
|
|
||||
|
needle_index = needle.length - 1 |
||||
|
haystack_index = @dname.length - 1 |
||||
|
|
||||
|
while needle_index >= 0 |
||||
|
return false if @dname[haystack_index] != needle[needle_index] |
||||
|
needle_index -= 1 |
||||
|
haystack_index -= 1 |
||||
|
end |
||||
|
true |
||||
|
end |
||||
|
|
||||
|
# Whether or not the DN ends with format (top-down) (values are ignored) |
||||
|
# dn is a string |
||||
|
def end_with_format?(dn) |
||||
|
needle = LDAP::Server::Operation.split_dn(dn) |
||||
|
|
||||
|
# Needle is longer than haystack |
||||
|
return false if needle.length > @dname.length |
||||
|
|
||||
|
needle_index = needle.length - 1 |
||||
|
haystack_index = @dname.length - 1 |
||||
|
|
||||
|
while needle_index >= 0 |
||||
|
return false if @dname[haystack_index].keys != needle[needle_index].keys |
||||
|
needle_index -= 1 |
||||
|
haystack_index -= 1 |
||||
|
end |
||||
|
true |
||||
|
end |
||||
|
|
||||
|
# Whether or not the DN equals dn (values are case sensitive) |
||||
|
# dn is a string |
||||
|
def equal?(dn) |
||||
|
split_dn = LDAP::Server::Operation.split_dn(dn) |
||||
|
|
||||
|
return false if split_dn.length != @dname.length |
||||
|
|
||||
|
@dname.each_with_index do |pair, index| |
||||
|
return false if pair != split_dn[index] |
||||
|
end |
||||
|
true |
||||
|
end |
||||
|
|
||||
|
# Whether or not the DN equals dn's format (values are ignored) (case insensitive) |
||||
|
# dn is a string |
||||
|
def equal_format?(dn) |
||||
|
split_dn = LDAP::Server::Operation.split_dn(dn) |
||||
|
|
||||
|
return false if split_dn.length != @dname.length |
||||
|
|
||||
|
@dname.each_with_index do |pair, index| |
||||
|
return false if pair.keys != split_dn[index].keys |
||||
|
end |
||||
|
true |
||||
|
end |
||||
|
|
||||
|
# Whether or not the DN constains a substring equal to dn (values are case sensitive) |
||||
|
# dn is a string |
||||
|
def include?(dn) |
||||
|
split_dn = LDAP::Server::Operation.split_dn(dn) |
||||
|
return false if split_dn.length > @dname.length |
||||
|
LDAP::Server::Operation.join_dn(@dname).include?(LDAP::Server::Operation.join_dn(split_dn)) |
||||
|
end |
||||
|
|
||||
|
# Whether or not the DN constains a substring format equal to dn (values are ignored) (case insensitive) |
||||
|
# dn is a string |
||||
|
def include_format?(dn) |
||||
|
split_dn = LDAP::Server::Operation.split_dn(dn) |
||||
|
|
||||
|
return false if split_dn.length > @dname.length |
||||
|
|
||||
|
haystack = [] |
||||
|
@dname.each { |pair| haystack << pair.keys } |
||||
|
|
||||
|
needle = [] |
||||
|
split_dn.each { |pair| needle << pair.keys } |
||||
|
|
||||
|
haystack.join.include?(needle.join) |
||||
|
end |
||||
|
|
||||
|
# Generates a mapping for variables |
||||
|
# For example: |
||||
|
# > dn = LDAP::Server.DN.new("uid=user,ou=Users,dc=mydomain,dc=com") |
||||
|
# > dn.parse("uid=:uid, ou=:category, dc=mydomain, dc=com") |
||||
|
# => { :uid => "user", :category => "Users" } |
||||
|
def parse(template_dn) |
||||
|
result = {} |
||||
|
template = LDAP::Server::Operation.split_dn(template_dn) |
||||
|
template.reverse.zip(@dname.reverse).each do |temp, const| |
||||
|
break if const and temp.keys.first != const.keys.first |
||||
|
if temp.values.first.start_with?(':') |
||||
|
sym = temp.values.first[1..-1].to_sym |
||||
|
if const |
||||
|
result[sym] = const.values.first unless result[sym] |
||||
|
else |
||||
|
result[sym] = nil |
||||
|
end |
||||
|
elsif temp.values.first != const.values.first |
||||
|
break |
||||
|
end |
||||
|
end |
||||
|
result |
||||
|
end |
||||
|
|
||||
|
def each(&block) |
||||
|
@dname.each(&block) |
||||
|
end |
||||
|
|
||||
|
def reverse_each(&block) |
||||
|
@dname.reverse_each(&block) |
||||
|
end |
||||
|
|
||||
|
end |
||||
|
|
||||
|
end |
||||
|
end |
||||
@ -0,0 +1,223 @@ |
|||||
|
require 'ldap/server/result' |
||||
|
require 'ldap/server/match' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
# LDAP filters are parsed into a LISP-like internal representation: |
||||
|
# |
||||
|
# [:true] |
||||
|
# [:false] |
||||
|
# [:undef] |
||||
|
# [:and, ..., ..., ...] |
||||
|
# [:or, ..., ..., ...] |
||||
|
# [:not, ...] |
||||
|
# [:present, attr] |
||||
|
# [:eq, attr, MO, val] |
||||
|
# [:approx, attr, MO, val] |
||||
|
# [:substrings, attr, MO, initial=nil, {any, any...}, final=nil] |
||||
|
# [:ge, attr, MO, val] |
||||
|
# [:le, attr, MO, val] |
||||
|
# |
||||
|
# This is done rather than a more object-oriented approach, in the |
||||
|
# hope that it will make it easier to match certain filter structures |
||||
|
# when converting them into something else. e.g. certain LDAP filter |
||||
|
# constructs can be mapped to some fixed SQL queries. |
||||
|
# |
||||
|
# See RFC 2251 4.5.1 for the three-state(!) boolean logic from LDAP |
||||
|
# |
||||
|
# If no schema is provided: 'attr' is the raw attribute name as provided |
||||
|
# by the client. If a schema is provided: attr is converted to its |
||||
|
# normalized name as listed in the schema, e.g. 'commonname' becomes 'cn', |
||||
|
# 'objectclass' becomes 'objectClass' etc. |
||||
|
# If a schema is provided, MO is a matching object which can be used to |
||||
|
# perform the match. If no schema is provided, this is 'nil'. In that |
||||
|
# case you could use LDAP::Server::MatchingRule::DefaultMatch. |
||||
|
|
||||
|
class Filter |
||||
|
|
||||
|
# Parse a filter in OpenSSL::ASN1 format into our own format. |
||||
|
# |
||||
|
# There are some trivial optimisations we make: e.g. |
||||
|
# (&(objectClass=*)(cn=foo)) -> (&(cn=foo)) -> (cn=foo) |
||||
|
|
||||
|
def self.parse(asn1, schema=nil) |
||||
|
case asn1.tag |
||||
|
when 0 # and |
||||
|
conds = asn1.value.collect { |a| parse(a) } |
||||
|
conds.delete([:true]) |
||||
|
return [:true] if conds.size == 0 |
||||
|
return conds.first if conds.size == 1 |
||||
|
return [:false] if conds.include?([:false]) |
||||
|
return conds.unshift(:and) |
||||
|
|
||||
|
when 1 # or |
||||
|
conds = asn1.value.collect { |a| parse(a) } |
||||
|
conds.delete([:false]) |
||||
|
return [:false] if conds.size == 0 |
||||
|
return conds.first if conds.size == 1 |
||||
|
return [:true] if conds.include?([:true]) |
||||
|
return conds.unshift(:or) |
||||
|
|
||||
|
when 2 # not |
||||
|
cond = parse(asn1.value[0]) |
||||
|
case cond |
||||
|
when [:false]; return [:true] |
||||
|
when [:true]; return [:false] |
||||
|
when [:undef]; return [:undef] |
||||
|
end |
||||
|
return [:not, cond] |
||||
|
|
||||
|
when 3 # equalityMatch |
||||
|
attr = asn1.value[0].value |
||||
|
val = asn1.value[1].value |
||||
|
return [:true] if attr =~ /\AobjectClass\z/i and val =~ /\Atop\z/i |
||||
|
if schema |
||||
|
a = schema.find_attrtype(attr) |
||||
|
return [:undef] unless a.equality |
||||
|
return [:eq, a.to_s, a.equality, val] |
||||
|
end |
||||
|
return [:eq, attr, nil, val] |
||||
|
|
||||
|
when 4 # substrings |
||||
|
attr = asn1.value[0].value |
||||
|
if schema |
||||
|
a = schema.find_attrtype(attr) |
||||
|
return [:undef] unless a.substr |
||||
|
res = [:substrings, a.to_s, a.substr, nil] |
||||
|
else |
||||
|
res = [:substrings, attr, nil, nil] |
||||
|
end |
||||
|
final_val = nil |
||||
|
|
||||
|
asn1.value[1].value.each do |ss| |
||||
|
case ss.tag |
||||
|
when 0 |
||||
|
res[3] = ss.value |
||||
|
when 1 |
||||
|
res << ss.value |
||||
|
when 2 |
||||
|
final_val = ss.value |
||||
|
else |
||||
|
raise LDAP::ResultError::ProtocolError, |
||||
|
"Unrecognised substring tag #{ss.tag.inspect}" |
||||
|
end |
||||
|
end |
||||
|
res << final_val |
||||
|
return res |
||||
|
|
||||
|
when 5 # greaterOrEqual |
||||
|
attr = asn1.value[0].value |
||||
|
val = asn1.value[1].value |
||||
|
if schema |
||||
|
a = schema.find_attrtype(attr) |
||||
|
return [:undef] unless a.ordering |
||||
|
return [:ge, a.to_s, a.ordering, val] |
||||
|
end |
||||
|
return [:ge, attr, nil, val] |
||||
|
|
||||
|
when 6 # lessOrEqual |
||||
|
attr = asn1.value[0].value |
||||
|
val = asn1.value[1].value |
||||
|
if schema |
||||
|
a = schema.find_attrtype(attr) |
||||
|
return [:undef] unless a.ordering |
||||
|
return [:le, a.to_s, a.ordering, val] |
||||
|
end |
||||
|
return [:le, attr, nil, val] |
||||
|
|
||||
|
when 7 # present |
||||
|
attr = asn1.value |
||||
|
return [:true] if attr =~ /\AobjectClass\z/i |
||||
|
if schema |
||||
|
begin |
||||
|
a = schema.find_attrtype(attr) |
||||
|
return [:present, a.to_s] |
||||
|
rescue LDAP::ResultError::UndefinedAttributeType |
||||
|
return [:false] |
||||
|
end |
||||
|
end |
||||
|
return [:present, attr] |
||||
|
|
||||
|
when 8 # approxMatch |
||||
|
attr = asn1.value[0].value |
||||
|
val = asn1.value[1].value |
||||
|
if schema |
||||
|
a = schema.find_attrtype(attr) |
||||
|
# I don't know how properly to deal with approxMatch. I'm assuming |
||||
|
# that the object will have an equality MatchingRule, and we |
||||
|
# can defer to that. |
||||
|
return [:undef] unless a.equality |
||||
|
return [:approx, a.to_s, a.equality, val] |
||||
|
end |
||||
|
return [:approx, attr, nil, val] |
||||
|
|
||||
|
#when 9 # extensibleMatch |
||||
|
# FIXME |
||||
|
|
||||
|
else |
||||
|
raise LDAP::ResultError::ProtocolError, |
||||
|
"Unrecognised Filter tag #{asn1.tag}" |
||||
|
end |
||||
|
|
||||
|
# Unknown attribute type |
||||
|
rescue LDAP::ResultError::UndefinedAttributeType |
||||
|
return [:undef] |
||||
|
end |
||||
|
|
||||
|
# Run a parsed filter against an attr=>[val] hash. |
||||
|
# |
||||
|
# Returns true, false or nil. |
||||
|
|
||||
|
def self.run(filter, av) |
||||
|
case filter[0] |
||||
|
when :and |
||||
|
res = true |
||||
|
filter[1..-1].each do |elem| |
||||
|
r = run(elem, av) |
||||
|
return false if r == false |
||||
|
res = nil if r.nil? |
||||
|
end |
||||
|
return res |
||||
|
|
||||
|
when :or |
||||
|
res = false |
||||
|
filter[1..-1].each do |elem| |
||||
|
r = run(elem, av) |
||||
|
return true if r == true |
||||
|
res = nil if r.nil? |
||||
|
end |
||||
|
return res |
||||
|
|
||||
|
when :not |
||||
|
case run(filter[1], av) |
||||
|
when true; return false |
||||
|
when false; return true |
||||
|
else return nil |
||||
|
end |
||||
|
|
||||
|
when :present |
||||
|
return av.has_key?(filter[1]) |
||||
|
|
||||
|
when :eq, :approx, :le, :ge, :substrings |
||||
|
# the filter now includes a suitable matching object |
||||
|
return (filter[2] || LDAP::Server::MatchingRule::DefaultMatch).send( |
||||
|
filter.first, Array(av[filter[1].to_s]), *filter[3..-1]) |
||||
|
|
||||
|
when :true |
||||
|
return true |
||||
|
|
||||
|
when :false |
||||
|
return false |
||||
|
|
||||
|
when :undef |
||||
|
return nil |
||||
|
end |
||||
|
|
||||
|
raise LDAP::ResultError::OperationsError, |
||||
|
"Unimplemented filter #{filter.first.inspect}" |
||||
|
end |
||||
|
|
||||
|
end # class Filter |
||||
|
end # class Server |
||||
|
end # module LDAP |
||||
@ -0,0 +1,283 @@ |
|||||
|
require 'ldap/server/syntax' |
||||
|
require 'ldap/server/result' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
# A class which holds LDAP MatchingRules. For now there is a global pool |
||||
|
# of MatchingRule objects (rather than each Schema object having |
||||
|
# its own pool) |
||||
|
|
||||
|
class MatchingRule |
||||
|
attr_reader :oid, :names, :syntax, :desc, :obsolete |
||||
|
|
||||
|
# Create a new MatchingRule object |
||||
|
|
||||
|
def initialize(oid, names, syntax, desc=nil, obsolete=false, &blk) |
||||
|
@oid = oid |
||||
|
@names = names |
||||
|
@names = [@names] unless @names.is_a?(Array) |
||||
|
@desc = desc |
||||
|
@obsolete = obsolete |
||||
|
@syntax = LDAP::Server::Syntax.find(syntax) # creates new obj if reqd |
||||
|
@def = nil |
||||
|
# initialization hook |
||||
|
self.instance_eval(&blk) if blk |
||||
|
end |
||||
|
|
||||
|
def name |
||||
|
(@names && names[0]) || @oid |
||||
|
end |
||||
|
|
||||
|
def to_s |
||||
|
(@names && names[0]) || @oid |
||||
|
end |
||||
|
|
||||
|
def normalize(x) |
||||
|
x |
||||
|
end |
||||
|
|
||||
|
# Create a new MatchingRule object, given its description string |
||||
|
|
||||
|
def self.from_def(str, &blk) |
||||
|
m = LDAP::Server::Syntax::MatchingRuleDescription.match(str) |
||||
|
raise LDAP::ResultError::InvalidAttributeSyntax, |
||||
|
"Bad MatchingRuleDescription #{str.inspect}" unless m |
||||
|
new(m[1], m[2].scan(/'(.*?)'/).flatten, m[5], m[3], m[4], &blk) |
||||
|
end |
||||
|
|
||||
|
def to_def |
||||
|
return @def if @def |
||||
|
ans = "( #{@oid} " |
||||
|
if names.nil? or @names.empty? |
||||
|
# nothing |
||||
|
elsif @names.size == 1 |
||||
|
ans << "NAME '#{@names[0]}' " |
||||
|
else |
||||
|
ans << "NAME ( " |
||||
|
@names.each { |n| ans << "'#{n}' " } |
||||
|
ans << ") " |
||||
|
end |
||||
|
ans << "DESC '#@desc' " if @desc |
||||
|
ans << "OBSOLETE " if @obsolete |
||||
|
ans << "SYNTAX #@syntax " if @syntax |
||||
|
ans << ")" |
||||
|
@def = ans |
||||
|
end |
||||
|
|
||||
|
@@rules = {} # oid / name / alias => object |
||||
|
|
||||
|
# Add a new matching rule |
||||
|
|
||||
|
def self.add(*args, &blk) |
||||
|
s = new(*args, &blk) |
||||
|
@@rules[s.oid] = s |
||||
|
return if s.names.nil? |
||||
|
s.names.each do |n| |
||||
|
@@rules[n.downcase] = s |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Find a MatchingRule object given a name or oid, or return nil |
||||
|
# (? should we create one automatically, like Syntax) |
||||
|
|
||||
|
def self.find(x) |
||||
|
return x if x.nil? or x.is_a?(LDAP::Server::MatchingRule) |
||||
|
@@rules[x.downcase] |
||||
|
end |
||||
|
|
||||
|
# Return all known matching rules |
||||
|
|
||||
|
def self.all_matching_rules |
||||
|
@@rules.values.uniq |
||||
|
end |
||||
|
|
||||
|
# Now some things we can mixin to a MatchingRule when needed. |
||||
|
# Replace 'normalize' with a function which gives the canonical |
||||
|
# version of a value for comparison. |
||||
|
|
||||
|
module Equality |
||||
|
def eq(vals, m) |
||||
|
return false if vals.nil? |
||||
|
m = normalize(m) |
||||
|
vals.each { |v| return true if normalize(v) == m } |
||||
|
return false |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
module Ordering |
||||
|
def ge(vals, m) |
||||
|
return false if vals.nil? |
||||
|
m = normalize(m) |
||||
|
vals.each { |v| return true if normalize(v) >= m } |
||||
|
return false |
||||
|
end |
||||
|
|
||||
|
def le(vals, m) |
||||
|
return false if vals.nil? |
||||
|
m = normalize(m) |
||||
|
vals.each { |v| return true if normalize(v) <= m } |
||||
|
return false |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
module Substrings |
||||
|
def substrings(vals, *ss) |
||||
|
return false if vals.nil? |
||||
|
|
||||
|
# convert the condition list into a regexp |
||||
|
re = [] |
||||
|
re << "^#{Regexp.escape(normalize(ss[0]).to_s)}" if ss[0] |
||||
|
ss[1..-2].each { |s| re << Regexp.escape(normalize(s).to_s) } |
||||
|
re << "#{Regexp.escape(normalize(ss[-1]).to_s)}$" if ss[-1] |
||||
|
re = Regexp.new(re.join(".*")) |
||||
|
|
||||
|
vals.each do |v| |
||||
|
v = normalize(v).to_s |
||||
|
return true if re.match(v) |
||||
|
end |
||||
|
return false |
||||
|
end |
||||
|
end # module Substrings |
||||
|
|
||||
|
class DefaultMatchingClass |
||||
|
include MatchingRule::Equality |
||||
|
include MatchingRule::Ordering |
||||
|
include MatchingRule::Substrings |
||||
|
def normalize(x) |
||||
|
x |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
DefaultMatch = DefaultMatchingClass.new |
||||
|
|
||||
|
end # class MatchingRule |
||||
|
|
||||
|
# |
||||
|
# And now, here are some matching rules you can use (RFC2252 section 8) |
||||
|
# |
||||
|
|
||||
|
class MatchingRule |
||||
|
|
||||
|
add('2.5.13.0', 'objectIdentifierMatch', '1.3.6.1.4.1.1466.115.121.1.38') do |
||||
|
extend Equality |
||||
|
end |
||||
|
# FIXME: Filters should return undef if the OID is not in the schema |
||||
|
# (which means passing in the schema to every equality test) |
||||
|
|
||||
|
add('2.5.13.1', 'distinguishedNameMatch', '1.3.6.1.4.1.1466.115.121.1.12') do |
||||
|
extend Equality |
||||
|
end |
||||
|
# FIXME: Distinguished Name matching is supposed to parse the DN into |
||||
|
# its parts and then apply the schema equality rules to each part |
||||
|
# (i.e. some parts may be case-sensitive, others case-insensitive) |
||||
|
# This is just one of the many nonsense design decisions in LDAP :-( |
||||
|
|
||||
|
# How is a DirectoryString different to an IA5String or a PrintableString? |
||||
|
|
||||
|
module StringTrim |
||||
|
def normalize(x); x.gsub(/^\s*|\s*$/, '').gsub(/\s+/,' '); end |
||||
|
end |
||||
|
|
||||
|
module StringDowncase |
||||
|
def normalize(x); x.downcase.gsub(/^\s*|\s*$/, '').gsub(/\s+/,' '); end |
||||
|
end |
||||
|
|
||||
|
add('2.5.13.2', 'caseIgnoreMatch', '1.3.6.1.4.1.1466.115.1') do |
||||
|
extend Equality |
||||
|
extend StringDowncase |
||||
|
end |
||||
|
|
||||
|
module Integer |
||||
|
def normalize(x); x.to_i; end |
||||
|
end |
||||
|
|
||||
|
add('2.5.13.8', 'numericStringMatch', '1.3.6.1.4.1.1466.115.121.1.36') do |
||||
|
extend Equality |
||||
|
extend Integer |
||||
|
end |
||||
|
|
||||
|
# TODO: Add semantics for these (difficult since RFC2252 doesn't give |
||||
|
# them, so we presumably have to go through X.500) |
||||
|
add('2.5.13.11', 'caseIgnoreListMatch', '1.3.6.1.4.1.1466.115.121.1.41') |
||||
|
add('2.5.13.14', 'integerMatch', '1.3.6.1.4.1.1466.115.121.1.27') do |
||||
|
extend Equality |
||||
|
extend Integer |
||||
|
end |
||||
|
add('2.5.13.16', 'bitStringMatch', '1.3.6.1.4.1.1466.115.121.1.6') |
||||
|
add('2.5.13.20', 'telephoneNumberMatch', '1.3.6.1.4.1.1466.115.121.1.50') do |
||||
|
extend Equality |
||||
|
extend StringTrim |
||||
|
end |
||||
|
add('2.5.13.22', 'presentationAddressMatch', '1.3.6.1.4.1.1466.115.121.1.43') |
||||
|
add('2.5.13.23', 'uniqueMemberMatch', '1.3.6.1.4.1.1466.115.121.1.34') |
||||
|
add('2.5.13.24', 'protocolInformationMatch', '1.3.6.1.4.1.1466.115.121.1.42') |
||||
|
add('2.5.13.27', 'generalizedTimeMatch', '1.3.6.1.4.1.1466.115.121.1.24') { extend Equality } |
||||
|
|
||||
|
# IA5 stuff. FIXME: What's the correct way to 'downcase' UTF8 strings? |
||||
|
|
||||
|
module IA5Trim |
||||
|
def normalize(x); x.gsub(/^\s*|\s*$/u, '').gsub(/\s+/u,' '); end |
||||
|
end |
||||
|
|
||||
|
module IA5Downcase |
||||
|
def normalize(x); x.downcase.gsub(/^\s*|\s*$/u, '').gsub(/\s+/u,' '); end |
||||
|
end |
||||
|
|
||||
|
add('1.3.6.1.4.1.1466.109.114.1', 'caseExactIA5Match', '1.3.6.1.4.1.1466.115.121.1.26') do |
||||
|
extend Equality |
||||
|
extend IA5Trim |
||||
|
end |
||||
|
|
||||
|
add('1.3.6.1.4.1.1466.109.114.2', 'caseIgnoreIA5Match', '1.3.6.1.4.1.1466.115.121.1.26') do |
||||
|
extend Equality |
||||
|
extend IA5Downcase |
||||
|
end |
||||
|
|
||||
|
add('2.5.13.28', 'generalizedTimeOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.24') { extend Ordering } |
||||
|
add('2.5.13.3', 'caseIgnoreOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.15') do |
||||
|
extend Ordering |
||||
|
extend StringDowncase |
||||
|
end |
||||
|
|
||||
|
add('2.5.13.4', 'caseIgnoreSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do |
||||
|
extend Substrings |
||||
|
extend StringDowncase |
||||
|
end |
||||
|
add('2.5.13.21', 'telephoneNumberSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do |
||||
|
extend Substrings |
||||
|
end |
||||
|
add('2.5.13.10', 'numericStringSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do |
||||
|
extend Substrings |
||||
|
end |
||||
|
|
||||
|
# from OpenLDAP |
||||
|
add('1.3.6.1.4.1.4203.1.2.1', 'caseExactIA5SubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.26') do |
||||
|
extend Substrings |
||||
|
extend IA5Trim |
||||
|
end |
||||
|
add('1.3.6.1.4.1.1466.109.114.3', 'caseIgnoreIA5SubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.26') do |
||||
|
extend Substrings |
||||
|
extend IA5Downcase |
||||
|
end |
||||
|
add('2.5.13.5', 'caseExactMatch', '1.3.6.1.4.1.1466.115.121.1.15') { extend Equality } |
||||
|
add('2.5.13.6', 'caseExactOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.15') { extend Ordering } |
||||
|
add('2.5.13.7', 'caseExactSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') { extend Substrings } |
||||
|
add('2.5.13.9', 'numericStringOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.36') { extend Ordering; extend Integer } |
||||
|
add('2.5.13.13', 'booleanMatch', '1.3.6.1.4.1.1466.115.121.1.7') do |
||||
|
extend Equality |
||||
|
def self.normalize(x) |
||||
|
return true if x == 'TRUE' |
||||
|
return false if x == 'FALSE' |
||||
|
x |
||||
|
end |
||||
|
end |
||||
|
add('2.5.13.15', 'integerOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.27') { extend Ordering; extend Integer } |
||||
|
add('2.5.13.17', 'octetStringMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Equality } |
||||
|
add('2.5.13.18', 'octetStringOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Ordering } |
||||
|
add('2.5.13.19', 'octetStringSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Substrings } |
||||
|
|
||||
|
end # class MatchingRule |
||||
|
|
||||
|
end # class Server |
||||
|
end # module LDAP |
||||
@ -0,0 +1,528 @@ |
|||||
|
require 'timeout' |
||||
|
require 'openssl' |
||||
|
require 'ldap/server/result' |
||||
|
require 'ldap/server/filter' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
# Scope |
||||
|
BaseObject = 0 |
||||
|
SingleLevel = 1 |
||||
|
WholeSubtree = 2 |
||||
|
|
||||
|
# DerefAliases |
||||
|
NeverDerefAliases = 0 |
||||
|
DerefInSearching = 1 |
||||
|
DerefFindingBaseObj = 2 |
||||
|
DerefAlways = 3 |
||||
|
|
||||
|
# Object to handle a single LDAP request. Typically you would |
||||
|
# subclass this object and override methods 'simple_bind', 'search' etc. |
||||
|
# The do_xxx methods are internal, and handle the parsing of requests |
||||
|
# and the sending of responses. |
||||
|
|
||||
|
class Operation |
||||
|
|
||||
|
# An instance of this object is created by the Connection object |
||||
|
# for each operation which is requested by the client. If you subclass |
||||
|
# Operation, and you override initialize, make sure you call 'super'. |
||||
|
|
||||
|
def initialize(connection, messageID) |
||||
|
@connection = connection |
||||
|
@respEnvelope = OpenSSL::ASN1::Sequence([ |
||||
|
OpenSSL::ASN1::Integer(messageID), |
||||
|
# protocolOp, |
||||
|
# controls [0] OPTIONAL, |
||||
|
]) |
||||
|
@schema = @connection.opt[:schema] |
||||
|
@server = @connection.opt[:server] |
||||
|
@attribute_range_limit = @connection.opt[:attribute_range_limit] |
||||
|
end |
||||
|
|
||||
|
def log msg, severity = Logger::INFO |
||||
|
@connection.log msg, severity |
||||
|
end |
||||
|
|
||||
|
def debug msg |
||||
|
@connection.debug msg |
||||
|
end |
||||
|
|
||||
|
# Send an exception report to the log |
||||
|
|
||||
|
def log_exception msg |
||||
|
@connection.log_exception msg |
||||
|
end |
||||
|
|
||||
|
################################################## |
||||
|
### Utility methods to send protocol responses ### |
||||
|
################################################## |
||||
|
|
||||
|
def send_LDAPMessage(protocolOp, opt={}) # :nodoc: |
||||
|
@respEnvelope.value[1] = protocolOp |
||||
|
if opt[:controls] |
||||
|
@respEnvelope.value[2] = OpenSSL::ASN1::Set(opt[:controls], 0, :IMPLICIT, APPLICATION) |
||||
|
else |
||||
|
@respEnvelope.value.delete_at(2) |
||||
|
end |
||||
|
|
||||
|
if false # $debug |
||||
|
puts "Response:" |
||||
|
p @respEnvelope |
||||
|
p @respEnvelope.to_der.unpack("H*") |
||||
|
end |
||||
|
|
||||
|
@connection.write(@respEnvelope.to_der) |
||||
|
end |
||||
|
|
||||
|
def send_LDAPResult(tag, resultCode, opt={}) # :nodoc: |
||||
|
seq = [ |
||||
|
OpenSSL::ASN1::Enumerated(resultCode), |
||||
|
OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""), |
||||
|
OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""), |
||||
|
] |
||||
|
if opt[:referral] |
||||
|
rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) } |
||||
|
seq << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION) |
||||
|
end |
||||
|
yield seq if block_given? # opportunity to add more elements |
||||
|
|
||||
|
send_LDAPMessage(OpenSSL::ASN1::Sequence(seq, tag, :IMPLICIT, :APPLICATION), opt) |
||||
|
end |
||||
|
|
||||
|
def send_BindResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(1, resultCode, opt) do |resp| |
||||
|
if opt[:serverSaslCreds] |
||||
|
resp << OpenSSL::ASN1::OctetString(opt[:serverSaslCreds], 7, :IMPLICIT, :APPLICATION) |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
|
||||
|
AttributeRange = Struct.new :start, :end |
||||
|
|
||||
|
# Send a found entry. Avs are {attr1=>val1, attr2=>[val2,val3]} |
||||
|
# If schema given, return operational attributes only if |
||||
|
# explicitly requested |
||||
|
|
||||
|
def send_SearchResultEntry(dn, avs, opt={}) |
||||
|
@rescount += 1 |
||||
|
if @sizelimit |
||||
|
raise LDAP::ResultError::SizeLimitExceeded if @rescount > @sizelimit |
||||
|
end |
||||
|
|
||||
|
if @schema |
||||
|
# normalize the attribute names |
||||
|
@attributes = @attributes.map { |a| a == '*' ? a : @schema.find_attrtype(a).to_s } |
||||
|
end |
||||
|
|
||||
|
sendall = @attributes == [] || @attributes.include?("*") |
||||
|
avseq = [] |
||||
|
|
||||
|
avs.each_with_index do |(attr, vals), aidx| |
||||
|
query_attr_idx = @attributes.index(attr) |
||||
|
if !query_attr_idx |
||||
|
next unless sendall |
||||
|
if @schema |
||||
|
a = @schema.find_attrtype(attr) |
||||
|
next unless a and (a.usage.nil? or a.usage == :userApplications) |
||||
|
end |
||||
|
end |
||||
|
query_attr = query_attr_idx && @attribute_ranges[query_attr_idx] |
||||
|
|
||||
|
if @typesOnly |
||||
|
vals = [] |
||||
|
else |
||||
|
vals = [vals] unless vals.kind_of?(Array) |
||||
|
# FIXME: optionally do a value_to_s conversion here? |
||||
|
# FIXME: handle attribute;binary |
||||
|
end |
||||
|
|
||||
|
if (@attribute_range_limit && vals.size > @attribute_range_limit) || query_attr&.start |
||||
|
if query_attr&.start |
||||
|
range_start = query_attr.start.to_i |
||||
|
range_end = query_attr.end == "*" ? -1 : query_attr.end.to_i |
||||
|
else |
||||
|
range_start = 0 |
||||
|
range_end = @attribute_range_limit ? @attribute_range_limit - 1 : -1 |
||||
|
end |
||||
|
range_end = range_start + @attribute_range_limit - 1 if @attribute_range_limit && (vals.size - range_start > @attribute_range_limit) |
||||
|
range_end = -1 if vals.size <= range_end |
||||
|
rvals = vals[range_start .. range_end] |
||||
|
vals = [] |
||||
|
avseq << OpenSSL::ASN1::Sequence([ |
||||
|
OpenSSL::ASN1::OctetString("#{attr};range=#{range_start}-#{range_end == -1 ? "*" : range_end}"), |
||||
|
OpenSSL::ASN1::Set(rvals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) }) |
||||
|
]) |
||||
|
end |
||||
|
|
||||
|
avseq << OpenSSL::ASN1::Sequence([ |
||||
|
OpenSSL::ASN1::OctetString(attr), |
||||
|
OpenSSL::ASN1::Set(vals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) }) |
||||
|
]) |
||||
|
end |
||||
|
|
||||
|
send_LDAPMessage(OpenSSL::ASN1::Sequence([ |
||||
|
OpenSSL::ASN1::OctetString(dn), |
||||
|
OpenSSL::ASN1::Sequence(avseq), |
||||
|
], 4, :IMPLICIT, :APPLICATION), opt) |
||||
|
end |
||||
|
|
||||
|
def send_SearchResultReference(urls, opt={}) |
||||
|
send_LDAPMessage(OpenSSL::ASN1::Sequence( |
||||
|
urls.collect { |url| OpenSSL::ASN1::OctetString(url) } |
||||
|
), |
||||
|
opt |
||||
|
) |
||||
|
end |
||||
|
|
||||
|
def send_SearchResultDone(resultCode, opt={}) |
||||
|
send_LDAPResult(5, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_ModifyResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(7, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_AddResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(9, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_DelResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(11, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_ModifyDNResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(13, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_CompareResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(15, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_ExtendedResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(24, resultCode, opt) do |resp| |
||||
|
if opt[:responseName] |
||||
|
resp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION) |
||||
|
end |
||||
|
if opt[:response] |
||||
|
resp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION) |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
########################################## |
||||
|
### Methods to parse each request type ### |
||||
|
########################################## |
||||
|
|
||||
|
def do_bind(protocolOp, controls) # :nodoc: |
||||
|
version = protocolOp.value[0].value |
||||
|
dn = protocolOp.value[1].value |
||||
|
dn = nil if dn == "" |
||||
|
authentication = protocolOp.value[2] |
||||
|
|
||||
|
case authentication.tag # tag_class == :CONTEXT_SPECIFIC (check why) |
||||
|
when 0 |
||||
|
simple_bind(version, dn, authentication.value) |
||||
|
when 3 |
||||
|
# mechanism = authentication.value[0].value |
||||
|
# credentials = authentication.value[1].value |
||||
|
# sasl_bind(version, dn, mechanism, credentials) |
||||
|
# FIXME: needs to exchange further BindRequests |
||||
|
raise LDAP::ResultError::AuthMethodNotSupported |
||||
|
else |
||||
|
raise LDAP::ResultError::ProtocolError, "BindRequest bad AuthenticationChoice" |
||||
|
end |
||||
|
send_BindResponse(0) |
||||
|
return dn, version |
||||
|
|
||||
|
rescue LDAP::ResultError => e |
||||
|
send_BindResponse(e.to_i, :errorMessage=>e.message) |
||||
|
return nil, version |
||||
|
end |
||||
|
|
||||
|
# reformat ASN1 into {attr=>[vals], attr=>[vals]} |
||||
|
# |
||||
|
# AttributeList ::= SEQUENCE OF SEQUENCE { |
||||
|
# type AttributeDescription, |
||||
|
# vals SET OF AttributeValue } |
||||
|
|
||||
|
def attributelist(set) # :nodoc: |
||||
|
av = {} |
||||
|
set.value.each do |seq| |
||||
|
a = seq.value[0].value |
||||
|
if @schema |
||||
|
a = @schema.find_attrtype(a).to_s |
||||
|
end |
||||
|
v = seq.value[1].value.collect { |asn1| asn1.value } |
||||
|
# Not clear from the spec whether the same attribute (with |
||||
|
# distinct values) can appear more than once in AttributeList |
||||
|
raise LDAP::ResultError::AttributeOrValueExists, a if av[a] |
||||
|
av[a] = v |
||||
|
end |
||||
|
return av |
||||
|
end |
||||
|
|
||||
|
def do_search(protocolOp, controls) # :nodoc: |
||||
|
baseObject = protocolOp.value[0].value |
||||
|
scope = protocolOp.value[1].value |
||||
|
deref = protocolOp.value[2].value |
||||
|
client_sizelimit = protocolOp.value[3].value |
||||
|
client_timelimit = protocolOp.value[4].value.to_i |
||||
|
@typesOnly = protocolOp.value[5].value |
||||
|
filter = Filter::parse(protocolOp.value[6], @schema) |
||||
|
attributes = protocolOp.value[7].value.collect {|x| x.value} |
||||
|
attributes = attributes.map do |attr| |
||||
|
if attr =~ /(.*);range=(\d+)-(\d+|\*)\z/ |
||||
|
[$1, $2, $3] |
||||
|
else |
||||
|
attr |
||||
|
end |
||||
|
end |
||||
|
@attributes = attributes.map do |name, | |
||||
|
name |
||||
|
end |
||||
|
@attribute_ranges = attributes.map do |_, range_start, range_end| |
||||
|
range_start && AttributeRange.new(range_start, range_end) |
||||
|
end |
||||
|
|
||||
|
@rescount = 0 |
||||
|
@sizelimit = server_sizelimit |
||||
|
@sizelimit = client_sizelimit if client_sizelimit > 0 and |
||||
|
(@sizelimit.nil? or client_sizelimit < @sizelimit) |
||||
|
|
||||
|
if baseObject.empty? and scope == BaseObject |
||||
|
send_SearchResultEntry("", @server.root_dse) if |
||||
|
@server.root_dse and LDAP::Server::Filter.run(filter, @server.root_dse) |
||||
|
send_SearchResultDone(0) |
||||
|
return |
||||
|
elsif @schema and baseObject == @schema.subschema_dn |
||||
|
send_SearchResultEntry(baseObject, @schema.subschema_subentry) if |
||||
|
@schema and @schema.subschema_subentry and |
||||
|
LDAP::Server::Filter.run(filter, @schema.subschema_subentry) |
||||
|
send_SearchResultDone(0) |
||||
|
return |
||||
|
end |
||||
|
|
||||
|
t = server_timelimit || 10 |
||||
|
t = client_timelimit if client_timelimit > 0 and client_timelimit < t |
||||
|
|
||||
|
Timeout::timeout(t, LDAP::ResultError::TimeLimitExceeded) do |
||||
|
search(baseObject, scope, deref, filter) |
||||
|
end |
||||
|
send_SearchResultDone(0) |
||||
|
|
||||
|
# Note that TimeLimitExceeded is a subclass of LDAP::ResultError |
||||
|
rescue LDAP::ResultError => e |
||||
|
send_SearchResultDone(e.to_i, :errorMessage=>e.message) |
||||
|
|
||||
|
rescue Abandon |
||||
|
# send no response |
||||
|
|
||||
|
# Since this Operation is running in its own thread, we have to |
||||
|
# catch all other exceptions. Otherwise, in the event of a programming |
||||
|
# error, this thread will silently terminate and the client will wait |
||||
|
# forever for a response. |
||||
|
|
||||
|
rescue Exception => e |
||||
|
log_exception(e) |
||||
|
send_SearchResultDone(LDAP::ResultError::OperationsError.new.to_i, :errorMessage=>e.message) |
||||
|
end |
||||
|
|
||||
|
def do_modify(protocolOp, controls) # :nodoc: |
||||
|
dn = protocolOp.value[0].value |
||||
|
modinfo = {} |
||||
|
protocolOp.value[1].value.each do |seq| |
||||
|
attr = seq.value[1].value[0].value |
||||
|
if @schema |
||||
|
attr = @schema.find_attrtype(attr).to_s |
||||
|
end |
||||
|
vals = seq.value[1].value[1].value.collect { |v| v.value } |
||||
|
case seq.value[0].value.to_i |
||||
|
when 0 |
||||
|
modinfo[attr] = [:add] + vals |
||||
|
when 1 |
||||
|
modinfo[attr] = [:delete] + vals |
||||
|
when 2 |
||||
|
modinfo[attr] = [:replace] + vals |
||||
|
else |
||||
|
raise LDAP::ResultError::ProtocolError, "Bad modify operation #{seq.value[0].value}" |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
modify(dn, modinfo) |
||||
|
send_ModifyResponse(0) |
||||
|
|
||||
|
rescue LDAP::ResultError => e |
||||
|
send_ModifyResponse(e.to_i, :errorMessage=>e.message) |
||||
|
rescue Abandon |
||||
|
# no response |
||||
|
rescue Exception => e |
||||
|
log_exception(e) |
||||
|
send_ModifyResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message) |
||||
|
end |
||||
|
|
||||
|
def do_add(protocolOp, controls) # :nodoc: |
||||
|
dn = protocolOp.value[0].value |
||||
|
av = attributelist(protocolOp.value[1]) |
||||
|
add(dn, av) |
||||
|
send_AddResponse(0) |
||||
|
|
||||
|
rescue LDAP::ResultError => e |
||||
|
send_AddResponse(e.to_i, :errorMessage=>e.message) |
||||
|
rescue Abandon |
||||
|
# no response |
||||
|
rescue Exception => e |
||||
|
log_exception(e) |
||||
|
send_AddResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message) |
||||
|
end |
||||
|
|
||||
|
def do_del(protocolOp, controls) # :nodoc: |
||||
|
dn = protocolOp.value |
||||
|
del(dn) |
||||
|
send_DelResponse(0) |
||||
|
|
||||
|
rescue LDAP::ResultError => e |
||||
|
send_DelResponse(e.to_i, :errorMessage=>e.message) |
||||
|
rescue Abandon |
||||
|
# no response |
||||
|
rescue Exception => e |
||||
|
log_exception(e) |
||||
|
send_DelResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message) |
||||
|
end |
||||
|
|
||||
|
def do_modifydn(protocolOp, controls) # :nodoc: |
||||
|
entry = protocolOp.value[0].value |
||||
|
newrdn = protocolOp.value[1].value |
||||
|
deleteoldrdn = protocolOp.value[2].value |
||||
|
if protocolOp.value.size > 3 and protocolOp.value[3].tag == 0 |
||||
|
newSuperior = protocolOp.value[3].value |
||||
|
end |
||||
|
modifydn(entry, newrdn, deleteoldrdn, newSuperior) |
||||
|
send_ModifyDNResponse(0) |
||||
|
|
||||
|
rescue LDAP::ResultError => e |
||||
|
send_ModifyDNResponse(e.to_i, :errorMessage=>e.message) |
||||
|
rescue Abandon |
||||
|
# no response |
||||
|
rescue Exception => e |
||||
|
log_exception(e) |
||||
|
send_ModifyDNResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message) |
||||
|
end |
||||
|
|
||||
|
def do_compare(protocolOp, controls) # :nodoc: |
||||
|
entry = protocolOp.value[0].value |
||||
|
ava = protocolOp.value[1].value |
||||
|
attr = ava[0].value |
||||
|
if @schema |
||||
|
attr = @schema.find_attrtype(attr).to_s |
||||
|
end |
||||
|
val = ava[1].value |
||||
|
if compare(entry, attr, val) |
||||
|
send_CompareResponse(6) # compareTrue |
||||
|
else |
||||
|
send_CompareResponse(5) # compareFalse |
||||
|
end |
||||
|
|
||||
|
rescue LDAP::ResultError => e |
||||
|
send_CompareResponse(e.to_i, :errorMessage=>e.message) |
||||
|
rescue Abandon |
||||
|
# no response |
||||
|
rescue Exception => e |
||||
|
log_exception(e) |
||||
|
send_CompareResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message) |
||||
|
end |
||||
|
|
||||
|
############################################################ |
||||
|
### Methods to get parameters related to this connection ### |
||||
|
############################################################ |
||||
|
|
||||
|
# Server-set maximum time limit. Override for more complex behaviour |
||||
|
# (e.g. limit depends on @connection.binddn). Nil uses hardcoded default. |
||||
|
|
||||
|
def server_timelimit |
||||
|
@connection.opt[:timelimit] |
||||
|
end |
||||
|
|
||||
|
# Server-set maximum size limit. Override for more complex behaviour |
||||
|
# (e.g. limit depends on @connection.binddn). Return nil for unlimited. |
||||
|
|
||||
|
def server_sizelimit |
||||
|
@connection.opt[:sizelimit] |
||||
|
end |
||||
|
|
||||
|
###################################################### |
||||
|
### Methods to actually perform the work requested ### |
||||
|
###################################################### |
||||
|
|
||||
|
# Handle a simple bind request; raise an exception if the bind is |
||||
|
# not acceptable, otherwise just return to accept the bind. |
||||
|
# |
||||
|
# Override this method in your own subclass. |
||||
|
|
||||
|
def simple_bind(version, dn, password) |
||||
|
if version != 3 |
||||
|
raise LDAP::ResultError::ProtocolError, "version 3 only" |
||||
|
end |
||||
|
if dn |
||||
|
raise LDAP::ResultError::InappropriateAuthentication, "This server only supports anonymous bind" |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Handle a search request; override this. |
||||
|
# |
||||
|
# Call send_SearchResultEntry for each result found. Raise an exception |
||||
|
# if there is a problem. timeLimit, sizeLimit and typesOnly are taken |
||||
|
# care of, but you need to perform all authorisation checks yourself, |
||||
|
# using @connection.binddn |
||||
|
|
||||
|
def search(basedn, scope, deref, filter) |
||||
|
debug "search(#{basedn}, #{scope}, #{deref}, #{filter})" |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "search not implemented" |
||||
|
end |
||||
|
|
||||
|
# Handle a modify request; override this |
||||
|
# |
||||
|
# dn is the object to modify; modification is a hash of |
||||
|
# attr => [:add, val, val...] -- add operation |
||||
|
# attr => [:replace, val, val...] -- replace operation |
||||
|
# attr => [:delete, val, val...] -- delete these values |
||||
|
# attr => [:delete] -- delete all values |
||||
|
|
||||
|
def modify(dn, modification) |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "modify not implemented" |
||||
|
end |
||||
|
|
||||
|
# Handle an add request; override this |
||||
|
# |
||||
|
# Parameters are the dn of the entry to add, and a hash of |
||||
|
# attr=>[val...] |
||||
|
# Raise an exception if there is a problem; it is up to you to check |
||||
|
# that the connection has sufficient authorisation using @connection.binddn |
||||
|
|
||||
|
def add(dn, av) |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "add not implemented" |
||||
|
end |
||||
|
|
||||
|
# Handle a del request; override this |
||||
|
|
||||
|
def del(dn) |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "delete not implemented" |
||||
|
end |
||||
|
|
||||
|
# Handle a modifydn request; override this |
||||
|
|
||||
|
def modifydn(entry, newrdn, deleteoldrdn, newSuperior) |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "modifydn not implemented" |
||||
|
end |
||||
|
|
||||
|
# Handle a compare request; override this. Return true or false, |
||||
|
# or raise an exception for errors. |
||||
|
|
||||
|
def compare(entry, attr, val) |
||||
|
raise LDAP::ResultError::UnwillingToPerform, "compare not implemented" |
||||
|
end |
||||
|
|
||||
|
end # class Operation |
||||
|
end # class Server |
||||
|
end # module LDAP |
||||
@ -0,0 +1,92 @@ |
|||||
|
require 'prefork' # <http://raa.ruby-lang.org/project/prefork/> |
||||
|
require 'socket' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
# Accept connections on a port, and for each one run the given block |
||||
|
# in one of N pre-forked children. Returns a Thread object for the |
||||
|
# listener. |
||||
|
# |
||||
|
# Options: |
||||
|
# :port=>port number [required] |
||||
|
# :bindaddr=>"IP address" |
||||
|
# :user=>"username" - drop privileges after bind |
||||
|
# :group=>"groupname" - ditto |
||||
|
# :logger=>object - implements << method |
||||
|
# :listen=>number - listen queue depth |
||||
|
# :nodelay=>true - set TCP_NODELAY option |
||||
|
# :min_servers=>N - prefork parameters |
||||
|
# :max_servers=>N |
||||
|
# :max_requests_per_child=>N |
||||
|
# :max_idle=>N - seconds |
||||
|
|
||||
|
def self.preforkserver(opt, &blk) |
||||
|
server = PreFork.new(opt[:bindaddr] || "0.0.0.0", opt[:port]) |
||||
|
|
||||
|
# Drop privileges if requested |
||||
|
if opt[:group] or opt[:user] |
||||
|
require 'etc' |
||||
|
gid = Etc.getgrnam(opt[:group]).gid if opt[:group] |
||||
|
uid = Etc.getpwnam(opt[:user]).uid if opt[:user] |
||||
|
File.chown(uid, gid, server.instance_eval {@lockf}) |
||||
|
Process.gid = Process.egid = gid if gid |
||||
|
Process.uid = Process.euid = uid if uid |
||||
|
end |
||||
|
|
||||
|
# Typically the O/S will buffer response data for 100ms before sending. |
||||
|
# If the response is sent as a single write() then there's no need for it. |
||||
|
if opt[:nodelay] |
||||
|
begin |
||||
|
server.sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) |
||||
|
rescue Exception |
||||
|
end |
||||
|
end |
||||
|
# set queue size for incoming connections (default is 5) |
||||
|
server.sock.listen(opt[:listen]) if opt[:listen] |
||||
|
|
||||
|
# Set prefork server parameters |
||||
|
server.min_servers = opt[:min_servers] if opt[:min_servers] |
||||
|
server.max_servers = opt[:max_servers] if opt[:max_servers] |
||||
|
server.max_request_per_child = opt[:max_request_per_child] if opt[:max_request_per_child] |
||||
|
server.max_idle = opt[:max_idle] if opt[:max_idle] |
||||
|
|
||||
|
Thread.new do |
||||
|
server.start do |s| |
||||
|
begin |
||||
|
s.instance_eval(&blk) |
||||
|
rescue Interrupt |
||||
|
# This exception can be raised to shut the server down |
||||
|
server.stop |
||||
|
rescue Exception => e |
||||
|
opt[:logger].error(s.peeraddr[3]) { "#{e}: #{e.backtrace[0]}" } |
||||
|
ensure |
||||
|
s.close |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
end # class Server |
||||
|
end # module LDAP |
||||
|
|
||||
|
if __FILE__ == $0 |
||||
|
# simple test |
||||
|
puts "Running a test POP3 server on port 1110" |
||||
|
t = LDAP::Server.preforkserver(:port=>1110) do |
||||
|
print "+OK I am a fake POP3 server (pid #{$$})\r\n" |
||||
|
while line = gets |
||||
|
case line |
||||
|
when /^quit/i |
||||
|
break |
||||
|
when /^crash/i |
||||
|
raise Errno::EPERM, "dammit!" |
||||
|
else |
||||
|
print "-ERR I don't understand #{line}" |
||||
|
end |
||||
|
end |
||||
|
print "+OK bye\r\n" |
||||
|
end |
||||
|
#sleep 10; t.raise Interrupt # uncomment to run for fixed time period |
||||
|
t.join |
||||
|
end |
||||
@ -0,0 +1,166 @@ |
|||||
|
require 'openssl' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
class Request |
||||
|
attr_accessor :connection, :typesOnly, :attributes, :rescount, :sizelimit |
||||
|
|
||||
|
# Object to handle a single LDAP request. This object is created on |
||||
|
# every request by the router, and is passed as argument to the defined |
||||
|
# routes. |
||||
|
|
||||
|
def initialize(connection, messageId) |
||||
|
@connection = connection |
||||
|
@respEnvelope = OpenSSL::ASN1::Sequence([ |
||||
|
OpenSSL::ASN1::Integer(messageId), |
||||
|
# protocolOp, |
||||
|
# controls [0] OPTIONAL, |
||||
|
]) |
||||
|
@schema = @connection.opt[:schema] |
||||
|
@server = @connection.opt[:server] |
||||
|
@rescount = 0 |
||||
|
end |
||||
|
|
||||
|
################################################## |
||||
|
### Utility methods to send protocol responses ### |
||||
|
################################################## |
||||
|
|
||||
|
def send_LDAPMessage(protocolOp, opt={}) # :nodoc: |
||||
|
@respEnvelope.value[1] = protocolOp |
||||
|
if opt[:controls] |
||||
|
@respEnvelope.value[2] = OpenSSL::ASN1::Set(opt[:controls], 0, :IMPLICIT, APPLICATION) |
||||
|
else |
||||
|
@respEnvelope.value.delete_at(2) |
||||
|
end |
||||
|
|
||||
|
@connection.write(@respEnvelope.to_der) |
||||
|
end |
||||
|
|
||||
|
def send_LDAPResult(tag, resultCode, opt={}) # :nodoc: |
||||
|
seq = [ |
||||
|
OpenSSL::ASN1::Enumerated(resultCode), |
||||
|
OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""), |
||||
|
OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""), |
||||
|
] |
||||
|
if opt[:referral] |
||||
|
rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) } |
||||
|
seq << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION) |
||||
|
end |
||||
|
yield seq if block_given? # opportunity to add more elements |
||||
|
|
||||
|
send_LDAPMessage(OpenSSL::ASN1::Sequence(seq, tag, :IMPLICIT, :APPLICATION), opt) |
||||
|
end |
||||
|
|
||||
|
def send_BindResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(1, resultCode, opt) do |resp| |
||||
|
if opt[:serverSaslCreds] |
||||
|
resp << OpenSSL::ASN1::OctetString(opt[:serverSaslCreds], 7, :IMPLICIT, :APPLICATION) |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Send a found entry. Avs are {attr1=>val1, attr2=>[val2,val3]} |
||||
|
# If schema given, return operational attributes only if |
||||
|
# explicitly requested |
||||
|
|
||||
|
def send_SearchResultEntry(dn, avs, opt={}) |
||||
|
@rescount += 1 |
||||
|
if @sizelimit |
||||
|
raise LDAP::ResultError::SizeLimitExceeded if @rescount > @sizelimit |
||||
|
end |
||||
|
|
||||
|
if @schema |
||||
|
# normalize the attribute names |
||||
|
@attributes = @attributes.map { |a| a == '*' ? a : @schema.find_attrtype(a).to_s } |
||||
|
end |
||||
|
|
||||
|
sendall = @attributes == [] || @attributes.include?("*") |
||||
|
avseq = [] |
||||
|
|
||||
|
avs.each do |attr, vals| |
||||
|
if !@attributes.include?(attr) |
||||
|
next unless sendall |
||||
|
if @schema |
||||
|
a = @schema.find_attrtype(attr) |
||||
|
next unless a and (a.usage.nil? or a.usage == :userApplications) |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
if @typesOnly |
||||
|
vals = [] |
||||
|
else |
||||
|
vals = [vals] unless vals.kind_of?(Array) |
||||
|
# FIXME: optionally do a value_to_s conversion here? |
||||
|
# FIXME: handle attribute;binary |
||||
|
end |
||||
|
|
||||
|
avseq << OpenSSL::ASN1::Sequence([ |
||||
|
OpenSSL::ASN1::OctetString(attr), |
||||
|
OpenSSL::ASN1::Set(vals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) }) |
||||
|
]) |
||||
|
end |
||||
|
|
||||
|
send_LDAPMessage(OpenSSL::ASN1::Sequence([ |
||||
|
OpenSSL::ASN1::OctetString(dn), |
||||
|
OpenSSL::ASN1::Sequence(avseq), |
||||
|
], 4, :IMPLICIT, :APPLICATION), opt) |
||||
|
end |
||||
|
|
||||
|
def send_SearchResultDone(resultCode, opt={}) |
||||
|
send_LDAPResult(5, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_ModifyResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(7, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_AddResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(9, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_DelResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(11, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_ModifyDNResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(13, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_CompareResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(15, resultCode, opt) |
||||
|
end |
||||
|
|
||||
|
def send_ExtendedResponse(resultCode, opt={}) |
||||
|
send_LDAPResult(24, resultCode, opt) do |resp| |
||||
|
if opt[:responseName] |
||||
|
resp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION) |
||||
|
end |
||||
|
if opt[:response] |
||||
|
resp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION) |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
############################################################ |
||||
|
### Methods to get parameters related to this connection ### |
||||
|
############################################################ |
||||
|
|
||||
|
# Server-set maximum time limit. Override for more complex behaviour |
||||
|
# (e.g. limit depends on @connection.binddn). Nil uses hardcoded default. |
||||
|
|
||||
|
def server_timelimit |
||||
|
@connection.opt[:timelimit] |
||||
|
end |
||||
|
|
||||
|
# Server-set maximum size limit. Override for more complex behaviour |
||||
|
# (e.g. limit depends on @connection.binddn). Return nil for unlimited. |
||||
|
|
||||
|
def server_sizelimit |
||||
|
@connection.opt[:sizelimit] |
||||
|
end |
||||
|
|
||||
|
end |
||||
|
|
||||
|
end |
||||
|
end |
||||
@ -0,0 +1,71 @@ |
|||||
|
module LDAP |
||||
|
|
||||
|
# compatible with ruby-ldap |
||||
|
class Error < StandardError |
||||
|
end |
||||
|
|
||||
|
class ResultError < Error |
||||
|
end |
||||
|
|
||||
|
# This exception is raised when we need to kill an existing Operation |
||||
|
# thread because of a received abandonRequest or bindRequest |
||||
|
class Abandon < Interrupt |
||||
|
end |
||||
|
|
||||
|
# ResultError constants from RFC 2251 4.1.10; these are all exceptions |
||||
|
# which can be raised |
||||
|
|
||||
|
class ResultError |
||||
|
class Success < self; def to_i; 0; end; end |
||||
|
class OperationsError < self; def to_i; 1; end; end |
||||
|
class ProtocolError < self; def to_i; 2; end; end |
||||
|
class TimeLimitExceeded < self; def to_i; 3; end; end |
||||
|
class SizeLimitExceeded < self; def to_i; 4; end; end |
||||
|
class CompareFalse < self; def to_i; 5; end; end |
||||
|
class CompareTrue < self; def to_i; 6; end; end |
||||
|
class AuthMethodNotSupported < self; def to_i; 7; end; end |
||||
|
class StrongAuthRequired < self; def to_i; 8; end; end |
||||
|
class Referral < self; def to_i; 10; end; end |
||||
|
class AdminLimitExceeded < self; def to_i; 11; end; end |
||||
|
class UnavailableCriticalExtension < self; def to_i; 12; end; end |
||||
|
class ConfidentialityRequired < self; def to_i; 13; end; end |
||||
|
class SaslBindInProgress < self; def to_i; 14; end; end |
||||
|
class NoSuchAttribute < self; def to_i; 16; end; end |
||||
|
class UndefinedAttributeType < self; def to_i; 17; end; end |
||||
|
class InappropriateMatching < self; def to_i; 18; end; end |
||||
|
class ConstraintViolation < self; def to_i; 19; end; end |
||||
|
class AttributeOrValueExists < self; def to_i; 20; end; end |
||||
|
class InvalidAttributeSyntax < self; def to_i; 21; end; end |
||||
|
class NoSuchObject < self; def to_i; 32; end; end |
||||
|
class AliasProblem < self; def to_i; 33; end; end |
||||
|
class InvalidDNSyntax < self; def to_i; 34; end; end |
||||
|
class IsLeaf < self; def to_i; 35; end; end |
||||
|
class AliasDereferencingProblem < self; def to_i; 36; end; end |
||||
|
class InappropriateAuthentication < self; def to_i; 48; end; end |
||||
|
class InvalidCredentials < self; def to_i; 49; end; end |
||||
|
class InsufficientAccessRights < self; def to_i; 50; end; end |
||||
|
class Busy < self; def to_i; 51; end; end |
||||
|
class Unavailable < self; def to_i; 52; end; end |
||||
|
class UnwillingToPerform < self; def to_i; 53; end; end |
||||
|
class LoopDetect < self; def to_i; 54; end; end |
||||
|
class NamingViolation < self; def to_i; 64; end; end |
||||
|
class ObjectClassViolation < self; def to_i; 65; end; end |
||||
|
class NotAllowedOnNonLeaf < self; def to_i; 66; end; end |
||||
|
class NotAllowedOnRDN < self; def to_i; 67; end; end |
||||
|
class EntryAlreadyExists < self; def to_i; 68; end; end |
||||
|
class ObjectClassModsProhibited < self; def to_i; 69; end; end |
||||
|
class AffectsMultipleDSAs < self; def to_i; 71; end; end |
||||
|
class Other < self; def to_i; 80; end; end |
||||
|
|
||||
|
# Reverse lookup: so you can do raise LDAP::ResultError[53] |
||||
|
|
||||
|
N_TO_CLASS = { |
||||
|
53 => UnwillingToPerform, |
||||
|
# FIXME: please fill in the rest |
||||
|
} |
||||
|
def self.[](n) |
||||
|
return N_TO_CLASS[n] || self |
||||
|
end |
||||
|
end # class ResultError |
||||
|
|
||||
|
end # module LDAP |
||||
@ -0,0 +1,220 @@ |
|||||
|
require 'ldap/server/dn' |
||||
|
require 'ldap/server/util' |
||||
|
require 'ldap/server/trie' |
||||
|
require 'ldap/server/request' |
||||
|
require 'ldap/server/filter' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
class Router |
||||
|
@logger |
||||
|
@routes |
||||
|
|
||||
|
# Scope |
||||
|
BaseObject = 0 |
||||
|
SingleLevel = 1 |
||||
|
WholeSubtree = 2 |
||||
|
|
||||
|
# DerefAliases |
||||
|
NeverDerefAliases = 0 |
||||
|
DerefInSearching = 1 |
||||
|
DerefFindingBaseObj = 2 |
||||
|
DerefAlways = 3 |
||||
|
|
||||
|
def initialize(logger, &block) |
||||
|
@logger = logger |
||||
|
|
||||
|
@routes = Hash.new |
||||
|
@routes = Trie.new do |trie| |
||||
|
# Add an artificial LDAP component |
||||
|
trie << "op=bind" |
||||
|
trie << "op=search" |
||||
|
end |
||||
|
|
||||
|
self.instance_eval(&block) |
||||
|
end |
||||
|
|
||||
|
def log_exception(e, level = :error) |
||||
|
@logger.send level, e.message |
||||
|
e.backtrace.each { |line| @logger.send level, "\tfrom#{line}" } if e.backtrace |
||||
|
end |
||||
|
|
||||
|
###################### |
||||
|
### Initialization ### |
||||
|
###################### |
||||
|
|
||||
|
def route(operation, hash) |
||||
|
hash.each do |key, value| |
||||
|
if key.nil? |
||||
|
@routes.insert "op=#{operation.to_s}", value |
||||
|
@logger.debug "map operation #{operation.to_s} all routes to #{value}" |
||||
|
else |
||||
|
@routes.insert "#{key},op=#{operation.to_s}", value |
||||
|
@logger.debug "map #{operation.to_s} #{key} to #{value}" |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def method_missing(name, *args, &block) |
||||
|
if [:bind, :search, :add, :modify, :modifydn, :del, :compare].include? name |
||||
|
send :route, name, *args |
||||
|
else |
||||
|
super |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
|
||||
|
#################################################### |
||||
|
### Methods to parse and route each request type ### |
||||
|
#################################################### |
||||
|
def parse_route(dn, method) |
||||
|
route, action = @routes.match("#{dn},op=#{method.to_s}") |
||||
|
if not route or route.empty? |
||||
|
@logger.warn "No route defined for \'#{route}\'" |
||||
|
raise LDAP::ResultError::UnwillingToPerform |
||||
|
end |
||||
|
if action.nil? |
||||
|
@logger.error "No action defined for route \'#{route}\'" |
||||
|
raise LDAP::ResultError::UnwillingToPerform |
||||
|
end |
||||
|
|
||||
|
class_name = action.split('#').first |
||||
|
method_name = action.split('#').last |
||||
|
|
||||
|
params = LDAP::Server::DN.new("#{dn},op=#{method.to_s}").parse(route) |
||||
|
|
||||
|
return class_name, method_name, params |
||||
|
end |
||||
|
|
||||
|
def do_bind(connection, messageId, protocolOp, controls) # :nodoc: |
||||
|
request = Request.new(connection, messageId) |
||||
|
version = protocolOp.value[0].value |
||||
|
dn = protocolOp.value[1].value |
||||
|
dn = nil if dn.empty? |
||||
|
authentication = protocolOp.value[2] |
||||
|
|
||||
|
@logger.debug "subject:#{connection.binddn} predicate:bind object:#{dn}" |
||||
|
|
||||
|
# Find a route in the routing tree |
||||
|
class_name, method_name, params = parse_route(dn, :bind) |
||||
|
|
||||
|
case authentication.tag # tag_class == :CONTEXT_SPECIFIC (check why) |
||||
|
when 0 |
||||
|
Object.const_get(class_name).send method_name, request, version, dn, authentication.value, params |
||||
|
when 3 |
||||
|
mechanism = authentication.value[0].value |
||||
|
credentials = authentication.value[1].value |
||||
|
# sasl_bind(version, dn, mechanism, credentials) |
||||
|
# FIXME: needs to exchange further BindRequests |
||||
|
# route_sasl_bind(request, version, dn, mechanism, credentials) |
||||
|
raise LDAP::ResultError::AuthMethodNotSupported |
||||
|
else |
||||
|
raise LDAP::ResultError::ProtocolError, "BindRequest bad AuthenticationChoice" |
||||
|
end |
||||
|
request.send_BindResponse(0) |
||||
|
return dn, version |
||||
|
rescue NoMethodError => e |
||||
|
log_exception e |
||||
|
request.send_BindResponse(LDAP::ResultError::OperationsError.new.to_i, :errorMessage => e.message) |
||||
|
return nil, version |
||||
|
rescue LDAP::ResultError => e |
||||
|
log_exception e |
||||
|
request.send_BindResponse(e.to_i, :errorMessage => e.message) |
||||
|
return nil, version |
||||
|
end |
||||
|
|
||||
|
def do_search(connection, messageId, protocolOp, controls) # :nodoc: |
||||
|
request = Request.new(connection, messageId) |
||||
|
server = connection.opt[:server] |
||||
|
schema = connection.opt[:schema] |
||||
|
baseObject = protocolOp.value[0].value |
||||
|
scope = protocolOp.value[1].value |
||||
|
deref = protocolOp.value[2].value |
||||
|
client_sizelimit = protocolOp.value[3].value |
||||
|
client_timelimit = protocolOp.value[4].value.to_i |
||||
|
request.typesOnly = protocolOp.value[5].value |
||||
|
filter = LDAP::Server::Filter::parse(protocolOp.value[6], schema) |
||||
|
request.attributes = protocolOp.value[7].value.collect {|x| x.value} |
||||
|
|
||||
|
sizelimit = request.server_sizelimit |
||||
|
sizelimit = client_sizelimit if client_sizelimit > 0 and |
||||
|
(sizelimit.nil? or client_sizelimit < sizelimit) |
||||
|
request.sizelimit = sizelimit |
||||
|
|
||||
|
if baseObject.empty? and scope == BaseObject |
||||
|
request.send_SearchResultEntry("", server.root_dse) if |
||||
|
server.root_dse and LDAP::Server::Filter.run(filter, server.root_dse) |
||||
|
request.send_SearchResultDone(0) |
||||
|
return |
||||
|
elsif schema and baseObject == schema.subschema_dn |
||||
|
request.send_SearchResultEntry(baseObject, schema.subschema_subentry) if |
||||
|
schema and schema.subschema_subentry and |
||||
|
LDAP::Server::Filter.run(filter, schema.subschema_subentry) |
||||
|
request.send_SearchResultDone(0) |
||||
|
return |
||||
|
end |
||||
|
|
||||
|
t = request.server_timelimit || 10 |
||||
|
t = client_timelimit if client_timelimit > 0 and client_timelimit < t |
||||
|
|
||||
|
@logger.debug "subject:#{connection.binddn} predicate:search object:#{baseObject}" |
||||
|
|
||||
|
# Find a route in the routing tree |
||||
|
class_name, method_name, params = parse_route(baseObject, :search) |
||||
|
|
||||
|
Timeout::timeout(t, LDAP::ResultError::TimeLimitExceeded) do |
||||
|
Object.const_get(class_name).send method_name, request, baseObject, scope, deref, filter, params |
||||
|
end |
||||
|
request.send_SearchResultDone(0) |
||||
|
|
||||
|
# Note that TimeLimitExceeded is a subclass of LDAP::ResultError |
||||
|
rescue LDAP::ResultError => e |
||||
|
request.send_SearchResultDone(e.to_i, :errorMessage=>e.message) |
||||
|
|
||||
|
rescue Abandon |
||||
|
# send no response |
||||
|
|
||||
|
# Since this Operation is running in its own thread, we have to |
||||
|
# catch all other exceptions. Otherwise, in the event of a programming |
||||
|
# error, this thread will silently terminate and the client will wait |
||||
|
# forever for a response. |
||||
|
|
||||
|
rescue Exception => e |
||||
|
log_exception e |
||||
|
request.send_SearchResultDone(LDAP::ResultError::OperationsError.new.to_i, :errorMessage=>e.message) |
||||
|
end |
||||
|
|
||||
|
|
||||
|
########################################################### |
||||
|
### Methods to actually perform the work requested ### |
||||
|
### Use the signatures below to write your own handlers ### |
||||
|
########################################################### |
||||
|
|
||||
|
# Handle a simple bind request; raise an exception if the bind is |
||||
|
# not acceptable, otherwise just return to accept the bind. |
||||
|
# |
||||
|
# Write your own class method using this signature |
||||
|
|
||||
|
# def simple_bind(request, version, dn, password, params) |
||||
|
# if version != 3 |
||||
|
# raise LDAP::ResultError::ProtocolError, "version 3 only" |
||||
|
# end |
||||
|
# if dn |
||||
|
# raise LDAP::ResultError::InappropriateAuthentication, "This server only supports anonymous bind" |
||||
|
# end |
||||
|
# end |
||||
|
|
||||
|
# Handle a search request |
||||
|
# |
||||
|
# Call request. send_SearchResultEntry for each result found. Raise |
||||
|
# an exception if there is a problem. timeLimit, sizeLimit and |
||||
|
# typesOnly are taken care of, but you need to perform all |
||||
|
# authorisation checks yourself, using @connection.binddn |
||||
|
|
||||
|
# def search(basedn, scope, deref, filter) |
||||
|
# debug "search(#{basedn}, #{scope}, #{deref}, #{filter})" |
||||
|
# raise LDAP::ResultError::UnwillingToPerform, "search not implemented" |
||||
|
# end |
||||
|
end |
||||
|
end |
||||
|
end |
||||
@ -0,0 +1,592 @@ |
|||||
|
require 'ldap/server/syntax' |
||||
|
require 'ldap/server/result' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
# This object represents an LDAP schema: that is, a collection of |
||||
|
# objectclasses and attributetypes. Methods are provided for loading |
||||
|
# the schema (from a string or a disk file), and validating an av-hash |
||||
|
# against it. |
||||
|
|
||||
|
class Schema |
||||
|
|
||||
|
SUBSCHEMA_ENTRY_ATTR = 'cn' |
||||
|
SUBSCHEMA_ENTRY_VALUE = 'Subschema' |
||||
|
|
||||
|
def initialize |
||||
|
@attrtypes = {} # name/alias/oid => AttributeType instance |
||||
|
@objectclasses = {} # name/alias/oid => ObjectClass instance |
||||
|
@subschema_cache = nil |
||||
|
end |
||||
|
|
||||
|
# return the DN of the subschema subentry |
||||
|
|
||||
|
def subschema_dn |
||||
|
"#{SUBSCHEMA_ENTRY_ATTR}=#{SUBSCHEMA_ENTRY_VALUE}" |
||||
|
end |
||||
|
|
||||
|
# Return an av hash object giving the subschema subentry. This is cached, so |
||||
|
# call Schema#changed if it needs to be rebuilt |
||||
|
|
||||
|
def subschema_subentry |
||||
|
@subschema_cache ||= { |
||||
|
'objectClass' => ['top','subschema','extensibleObject'], |
||||
|
SUBSCHEMA_ENTRY_ATTR => [SUBSCHEMA_ENTRY_VALUE], |
||||
|
'objectClasses' => all_objectclasses.collect { |s| s.to_def }, |
||||
|
'attributeTypes' => all_attrtypes.collect { |s| s.to_def }, |
||||
|
'ldapSyntaxes' => LDAP::Server::Syntax.all_syntaxes.collect { |s| s.to_def }, |
||||
|
#'matchingRules' => |
||||
|
#'matchingRuleUse' => |
||||
|
} |
||||
|
end |
||||
|
|
||||
|
# Clear the subschema subentry cache, so the next time someone requests |
||||
|
# it, it will be rebuilt |
||||
|
|
||||
|
def changed |
||||
|
@subschema_cache = nil |
||||
|
end |
||||
|
|
||||
|
# Add an AttributeType to the schema |
||||
|
|
||||
|
def add_attrtype(str) |
||||
|
a = AttributeType.new(str) |
||||
|
@attrtypes[a.oid] = a if a.oid |
||||
|
a.names.each do |n| |
||||
|
@attrtypes[n.downcase] = a |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Locate an attributetype object by name/alias/oid (or raise exception) |
||||
|
|
||||
|
def find_attrtype(n) |
||||
|
return n if n.nil? or n.is_a?(LDAP::Server::Schema::AttributeType) |
||||
|
r = @attrtypes[n.downcase] |
||||
|
raise LDAP::ResultError::UndefinedAttributeType, "Unknown AttributeType #{n.inspect}" unless r |
||||
|
r |
||||
|
end |
||||
|
|
||||
|
# Return array of all AttributeType objects in this schema |
||||
|
|
||||
|
def all_attrtypes |
||||
|
@attrtypes.values.uniq |
||||
|
end |
||||
|
|
||||
|
# Add an ObjectClass to the schema |
||||
|
|
||||
|
def add_objectclass(str) |
||||
|
o = ObjectClass.new(str) |
||||
|
@objectclasses[o.oid] = o if o.oid |
||||
|
o.names.each do |n| |
||||
|
@objectclasses[n.downcase] = o |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Locate an objectclass object by name/alias/oid (or raise exception) |
||||
|
|
||||
|
def find_objectclass(n) |
||||
|
return n if n.nil? or n.is_a?(LDAP::Server::Schema::ObjectClass) |
||||
|
r = @objectclasses[n.downcase] |
||||
|
raise LDAP::ResultError::ObjectClassViolation, "Unknown ObjectClass #{n.inspect}" unless r |
||||
|
r |
||||
|
end |
||||
|
|
||||
|
# Return array of all ObjectClass objects in this schema |
||||
|
|
||||
|
def all_objectclasses |
||||
|
@objectclasses.values.uniq |
||||
|
end |
||||
|
|
||||
|
# Load an OpenLDAP-format schema from a named file (see notes under 'load') |
||||
|
|
||||
|
def load_file(filename) |
||||
|
File.open(filename) { |f| load(f) } |
||||
|
end |
||||
|
|
||||
|
# Load an OpenLDAP-format schema from a string or IO object (anything |
||||
|
# which responds to 'each_line'). Lines starting 'attributetype' |
||||
|
# or 'objectclass' contain one of those objects. Does not implement |
||||
|
# named objectIdentifier prefixes (used in the dyngroup.schema file |
||||
|
# supplied with openldap, but not documented in RFC2252) |
||||
|
# |
||||
|
# Note: RFC2252 is strict about the order in which the elements appear, |
||||
|
# and so are we, but OpenLDAP is not. This means that a schema which |
||||
|
# works in OpenLDAP might not load here. For example, RFC2252 says |
||||
|
# that in an objectclass description, "SUP" must come before "MAY"; |
||||
|
# if they are the other way round, our regexp-based parser will not |
||||
|
# accept it. The solution is simply to modify the definition so that |
||||
|
# the elements appear in the correct order. |
||||
|
|
||||
|
def load(str_or_io) |
||||
|
meth = :junk_line |
||||
|
data = "" |
||||
|
str_or_io.each_line do |line| |
||||
|
case line |
||||
|
when /^\s*#/, /^\s*$/ |
||||
|
next |
||||
|
when /^objectclass\s*(.*)$/i |
||||
|
m = $~ |
||||
|
send(meth, data) |
||||
|
meth, data = :add_objectclass, m[1] |
||||
|
when /^attributetype\s*(.*)$/i |
||||
|
m = $~ |
||||
|
send(meth, data) |
||||
|
meth, data = :add_attrtype, m[1] |
||||
|
else |
||||
|
data << line |
||||
|
end |
||||
|
end |
||||
|
send(meth,data) |
||||
|
self |
||||
|
end |
||||
|
|
||||
|
def junk_line(data) |
||||
|
return if data.empty? |
||||
|
raise LDAP::ResultError::InvalidAttributeSyntax, |
||||
|
"Expected 'attributetype' or 'objectclass', got #{data}" |
||||
|
end |
||||
|
private :junk_line |
||||
|
|
||||
|
# Load in the base set of objectclasses and attributetypes, being |
||||
|
# the same set as OpenLDAP preloads internally. Includes objectclasses |
||||
|
# 'top', 'objectclass'; attributetypes 'objectclass' , 'cn', |
||||
|
# 'userPassword' and 'distinguishedName'; common operational attributes |
||||
|
# such as 'modifyTimestamp'; plus extras needed for publishing a v3 |
||||
|
# schema via LDAP |
||||
|
|
||||
|
def load_system |
||||
|
load(<<EOS) |
||||
|
attributetype ( 1.3.6.1.4.1.250.1.57 NAME 'labeledURI' DESC 'RFC2079: Uniform Resource Identifier with optional label' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) |
||||
|
attributetype ( 2.5.4.35 NAME 'userPassword' DESC 'RFC2256/2307: password of user' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40{128} ) |
||||
|
attributetype ( 2.5.4.3 NAME ( 'cn' 'commonName' ) DESC 'RFC2256: common name(s) for which the entity is known by' SUP name ) |
||||
|
attributetype ( 2.5.4.41 NAME 'name' DESC 'RFC2256: common supertype of name attributes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} ) |
||||
|
attributetype ( 2.5.4.49 NAME 'distinguishedName' DESC 'RFC2256: common supertype of DN attributes' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) |
||||
|
attributetype ( 2.16.840.1.113730.3.1.34 NAME 'ref' DESC 'namedref: subordinate referral URL' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE distributedOperation ) |
||||
|
attributetype ( 2.5.4.1 NAME ( 'aliasedObjectName' 'aliasedEntryName' ) DESC 'RFC2256: name of aliased object' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE ) |
||||
|
attributetype ( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC2252: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.21.8 NAME 'matchingRuleUse' DESC 'RFC2252: matching rule uses' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.31 USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.21.6 NAME 'objectClasses' DESC 'RFC2252: object classes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.21.5 NAME 'attributeTypes' DESC 'RFC2252: attribute types' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.21.4 NAME 'matchingRules' DESC 'RFC2252: matching rules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.30 USAGE directoryOperation ) |
||||
|
attributetype ( 1.3.6.1.1.5 NAME 'vendorVersion' DESC 'RFC3045: version of implementation' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation ) |
||||
|
attributetype ( 1.3.6.1.1.4 NAME 'vendorName' DESC 'RFC3045: name of implementation vendor' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation ) |
||||
|
attributetype ( 1.3.6.1.4.1.4203.1.3.5 NAME 'supportedFeatures' DESC 'features supported by the server' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation ) |
||||
|
attributetype ( 1.3.6.1.4.1.1466.101.120.14 NAME 'supportedSASLMechanisms' DESC 'RFC2252: supported SASL mechanisms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE dSAOperation ) |
||||
|
attributetype ( 1.3.6.1.4.1.1466.101.120.15 NAME 'supportedLDAPVersion' DESC 'RFC2252: supported LDAP versions' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 USAGE dSAOperation ) |
||||
|
attributetype ( 1.3.6.1.4.1.1466.101.120.7 NAME 'supportedExtension' DESC 'RFC2252: supported extended operations' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation ) |
||||
|
attributetype ( 1.3.6.1.4.1.1466.101.120.13 NAME 'supportedControl' DESC 'RFC2252: supported controls' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation ) |
||||
|
attributetype ( 1.3.6.1.4.1.1466.101.120.5 NAME 'namingContexts' DESC 'RFC2252: naming contexts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation ) |
||||
|
attributetype ( 1.3.6.1.4.1.1466.101.120.6 NAME 'altServer' DESC 'RFC2252: alternative servers' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 USAGE dSAOperation ) |
||||
|
attributetype ( 2.5.18.10 NAME 'subschemaSubentry' DESC 'RFC2252: name of controlling subschema entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.18.9 NAME 'hasSubordinates' DESC 'X.501: entry has children' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.18.4 NAME 'modifiersName' DESC 'RFC2252: name of last modifier' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.18.3 NAME 'creatorsName' DESC 'RFC2252: name of creator' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.18.2 NAME 'modifyTimestamp' DESC 'RFC2252: time which object was last modified' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.18.1 NAME 'createTimestamp' DESC 'RFC2252: time which object was created' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.21.9 NAME 'structuralObjectClass' DESC 'X.500(93): structural object class of entry' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.4.0 NAME 'objectClass' DESC 'RFC2256: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 ) |
||||
|
# These ones aren't published by OpenLDAP, but are referenced by the 'subschema' objectclass |
||||
|
attributetype ( 2.5.21.1 NAME 'dITStructureRules' EQUALITY integerFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.17 USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.21.7 NAME 'nameForms' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.35 USAGE directoryOperation ) |
||||
|
attributetype ( 2.5.21.2 NAME 'dITContentRules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.16 USAGE directoryOperation ) |
||||
|
|
||||
|
objectclass ( 2.5.20.1 NAME 'subschema' DESC 'RFC2252: controlling subschema (sub)entry' AUXILIARY MAY ( dITStructureRules $ nameForms $ ditContentRules $ objectClasses $ attributeTypes $ matchingRules $ matchingRuleUse ) ) |
||||
|
#Don't have definition for subtreeSpecification: |
||||
|
#objectClass ( 2.5.17.0 NAME 'subentry' SUP top STRUCTURAL MUST ( cn $ subtreeSpecification ) ) |
||||
|
objectClass ( 1.3.6.1.4.1.4203.1.4.1 NAME ( 'OpenLDAProotDSE' 'LDAProotDSE' ) DESC 'OpenLDAP Root DSE object' SUP top STRUCTURAL MAY cn ) |
||||
|
objectClass ( 2.16.840.1.113730.3.2.6 NAME 'referral' DESC 'namedref: named subordinate referral' SUP top STRUCTURAL MUST ref ) |
||||
|
objectClass ( 2.5.6.1 NAME 'alias' DESC 'RFC2256: an alias' SUP top STRUCTURAL MUST aliasedObjectName ) |
||||
|
objectClass ( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' DESC 'RFC2252: extensible object' SUP top AUXILIARY ) |
||||
|
objectClass ( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass ) |
||||
|
EOS |
||||
|
end |
||||
|
|
||||
|
# After loading object classes and attr types: resolve oid strings to point |
||||
|
# to objects. This will expose schema inconsistencies (e.g. objectclass |
||||
|
# has unknown SUP class or points to unknown attributeType). However, |
||||
|
# unknown Syntaxes just create new Syntax objects. |
||||
|
|
||||
|
def resolve_oids |
||||
|
|
||||
|
all_attrtypes.each do |a| |
||||
|
if a.sup |
||||
|
s = find_attrtype(a.sup) |
||||
|
a.instance_eval { |
||||
|
@sup = s |
||||
|
# inherit properties (FIXME: This breaks to_def) |
||||
|
@equality ||= s.equality |
||||
|
@ordering ||= s.ordering |
||||
|
@substr ||= s.substr |
||||
|
@syntax ||= s.syntax |
||||
|
@maxlen ||= s.maxlen |
||||
|
@singlevalue ||= s.singlevalue |
||||
|
@collective ||= s.collective |
||||
|
@nousermod ||= s.nousermod |
||||
|
@usage ||= s.usage |
||||
|
} |
||||
|
end |
||||
|
a.instance_eval do |
||||
|
@syntax = LDAP::Server::Syntax.find(@syntax) if @syntax |
||||
|
@equality = LDAP::Server::MatchingRule.find(@equality) if @equality |
||||
|
@ordering = LDAP::Server::MatchingRule.find(@ordering) if @ordering |
||||
|
@substr = LDAP::Server::MatchingRule.find(@substr) if @substr |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
all_objectclasses.each do |o| |
||||
|
if o.sup |
||||
|
s = o.sup.collect { |ss| find_objectclass(ss) } |
||||
|
o.instance_eval { @sup = s } |
||||
|
end |
||||
|
if o.must |
||||
|
s = o.must.collect { |ss| find_attrtype(ss) } |
||||
|
o.instance_eval { @must = s } |
||||
|
end |
||||
|
if o.may |
||||
|
s = o.may.collect { |ss| find_attrtype(ss) } |
||||
|
o.instance_eval { @may = s } |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
end |
||||
|
|
||||
|
# Validate a new entry or update. For a new entry, just pass a hash |
||||
|
# of attr=>[val, val, ...]; for an update, the first parameter is |
||||
|
# a hash of attr=>[:modtype, val, val...] and the second parameter |
||||
|
# is the existing entry, where it is assumed that the attribute names |
||||
|
# are already in their standard string forms (as returned by attr#name) |
||||
|
# |
||||
|
# Returns a hash containing the updated entry. |
||||
|
# |
||||
|
# If a block is given, it is called to decide whether the user is |
||||
|
# allowed to update an attribute; parameter is the attr *object* |
||||
|
# (not name; use #name if you need its name instead). Return false |
||||
|
# if the update is not permitted. Otherwise, the only restriction |
||||
|
# will be that updates to attributes declared 'nousermod' are forbidden. |
||||
|
# |
||||
|
# No DN checks are done here, since we don't know the DN. |
||||
|
# Checking that the entry contains an attribute for the RDN is the |
||||
|
# responsibility of the caller. |
||||
|
|
||||
|
def validate(mods, entry={}) |
||||
|
|
||||
|
# Run through the mods, make the normalized names, and perform any |
||||
|
# updates |
||||
|
|
||||
|
# FIXME: I don't know if these are the right results to return |
||||
|
# for the various types of validation errors |
||||
|
|
||||
|
oc_changed = false |
||||
|
res = entry.dup |
||||
|
mods.each do |attrname, nv| |
||||
|
attr = find_attrtype(attrname) |
||||
|
attrname = attr.to_s |
||||
|
raise LDAP::ResultError::ConstraintViolation, |
||||
|
"Cannot modify #{attrname}" if attr.nousermod or |
||||
|
(block_given? and !yield(attr)) |
||||
|
# Perform the update |
||||
|
vals = res[attrname] || [] |
||||
|
checkvals = [] |
||||
|
nv = [nv] unless nv.is_a?(Array) |
||||
|
|
||||
|
case nv.first |
||||
|
when :add |
||||
|
checkvals = nv[1..-1] |
||||
|
vals += checkvals |
||||
|
vals.uniq! # FIXME: ?? error if duplicate values |
||||
|
# FIXME: normalize values? e.g. c: gb and c: GB are same value. |
||||
|
when :delete |
||||
|
nv = nv[1..-1] |
||||
|
if nv.empty? |
||||
|
vals = [] # ?? error if does not exist |
||||
|
else |
||||
|
nv.each { |v| vals.delete(v) } # ?? error if value missing |
||||
|
end |
||||
|
when :replace |
||||
|
vals = checkvals = nv[1..-1] |
||||
|
else |
||||
|
vals = checkvals = nv |
||||
|
end |
||||
|
if vals == [] |
||||
|
res.delete(attrname) |
||||
|
else |
||||
|
res[attrname] = vals |
||||
|
end |
||||
|
|
||||
|
# Attribute validation |
||||
|
raise LDAP::ResultError::ObjectClassViolation, |
||||
|
"Attribute #{attr} is SINGLE-VALUE" if attr.singlevalue and vals.size > 1 |
||||
|
|
||||
|
checkvals.each do |val| |
||||
|
raise LDAP::ResultError::InvalidAttributeSyntax, |
||||
|
"Nil or empty value for attribute #{attr}" if val.nil? or val.empty? |
||||
|
raise LDAP::ResultError::InvalidAttributeSyntax, |
||||
|
"Bad value for #{attr}: #{val.inspect}" if attr.syntax and ! attr.syntax.match(val) |
||||
|
raise LDAP::ResultError::InvalidAttributeSyntax, |
||||
|
"Value too long for #{attr} (max #{attr.maxlen})" if attr.maxlen and val.length > attr.maxlen |
||||
|
end |
||||
|
|
||||
|
oc_changed = true if attrname == 'objectClass' |
||||
|
end |
||||
|
|
||||
|
# Now do objectClass checks |
||||
|
oc = res['objectClass'] |
||||
|
unless oc |
||||
|
raise LDAP::ResultError::ObjectClassViolation, |
||||
|
"objectClass attribute missing" |
||||
|
end |
||||
|
oc = oc.collect { |val| find_objectclass(val) } |
||||
|
|
||||
|
if oc_changed |
||||
|
# Add superior objectClasses (note: growing an array while you |
||||
|
# iterate over it seems to work, in ruby-1.8.2 anyway!) |
||||
|
oc.each do |objectclass| |
||||
|
objectclass.sup.each do |s| |
||||
|
oc.push(s) unless oc.include?(s) |
||||
|
end |
||||
|
end |
||||
|
res['objectClass'] = oc.collect { |oo| oo.to_s } |
||||
|
|
||||
|
# Check that exactly one structural objectClass is present |
||||
|
unless oc.find_all { |s| s.struct == :structural }.size >= 1 |
||||
|
raise LDAP::ResultError::ObjectClassViolation, |
||||
|
"Entry must have at least one structural objectClass" |
||||
|
# Exactly one? But you have to sort out the inheritance problem |
||||
|
# (e.g. both person and organizationalPerson are declared |
||||
|
# structural) |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Ensure that all MUST attributes are present |
||||
|
allow_attr = {} |
||||
|
oc.each do |objectclass| |
||||
|
objectclass.must.each do |m| |
||||
|
unless res[m.name] and res[m.name] != [] |
||||
|
raise LDAP::ResultError::ObjectClassViolation, "Missing attribute #{m} required by objectClass #{objectclass}" |
||||
|
end |
||||
|
allow_attr[m.name] = true |
||||
|
end |
||||
|
objectclass.may.each do |m| |
||||
|
allow_attr[m.name] = true |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
unless oc.find { |objectclass| objectclass.name == 'extensibleObject' } |
||||
|
# Now check all the attributes given are permitted by MUST or MAY |
||||
|
res.each_key do |attr| |
||||
|
unless allow_attr[attr] or find_attrtype(attr).usage == :directoryOperation |
||||
|
raise LDAP::ResultError::ObjectClassViolation, "Attribute #{attr} not permitted by objectClass" |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
return res |
||||
|
end |
||||
|
|
||||
|
# Hopefully backwards-compatible API for ruby-ldap's LDAP::Schema. |
||||
|
# Since MUST/MAY/SUP may point to schema objects, convert them back |
||||
|
# to strings. |
||||
|
|
||||
|
def names(key) |
||||
|
case key |
||||
|
when 'objectClasses' |
||||
|
return all_objectclasses.collect { |e| e.name } |
||||
|
when 'attributeTypes' |
||||
|
return all_attrtypes.collect { |e| e.name } |
||||
|
when 'ldapSyntaxes' |
||||
|
return LDAP::Server::Syntax.all_syntaxes.collect { |e| e.name } |
||||
|
when 'matchingRules' |
||||
|
return LDAP::Server::MatchingRule.all_matching_rules.collect { |e| e.name } |
||||
|
# TODO: matchingRuleUse |
||||
|
end |
||||
|
return nil |
||||
|
end |
||||
|
|
||||
|
# Backwards-compatible for ruby-ldap LDAP::Schema |
||||
|
|
||||
|
def attr(oc,at) |
||||
|
o = find_objectclass(oc) |
||||
|
case at.upcase |
||||
|
when 'MUST' |
||||
|
return o.must.collect { |e| e.to_s } |
||||
|
when 'MAY' |
||||
|
return o.may.collect { |e| e.to_s } |
||||
|
when 'SUP' |
||||
|
return o.sup.collect { |e| e.to_s } |
||||
|
when 'NAME' |
||||
|
return o.names.collect { |e| e.to_s } |
||||
|
when 'DESC' |
||||
|
return [o.desc] |
||||
|
end |
||||
|
return nil |
||||
|
rescue LDAP::ResultError |
||||
|
return nil |
||||
|
end |
||||
|
|
||||
|
# Backwards-compatible for ruby-ldap LDAP::Schema |
||||
|
|
||||
|
def must(oc) |
||||
|
attr(oc, "MUST") |
||||
|
end |
||||
|
|
||||
|
# Backwards-compatible for ruby-ldap LDAP::Schema |
||||
|
|
||||
|
def may(oc) |
||||
|
attr(oc, "MAY") |
||||
|
end |
||||
|
|
||||
|
# Backwards-compatible for ruby-ldap LDAP::Schema |
||||
|
|
||||
|
def sup(oc) |
||||
|
attr(oc, "SUP") |
||||
|
end |
||||
|
|
||||
|
##################################################################### |
||||
|
|
||||
|
# Class holding an instance of an AttributeTypeDescription (RFC2252 4.2) |
||||
|
|
||||
|
class AttributeType |
||||
|
|
||||
|
attr_reader :oid, :names, :desc, :obsolete, :sup, :equality, :ordering |
||||
|
attr_reader :substr, :syntax, :maxlen, :singlevalue, :collective |
||||
|
attr_reader :nousermod, :usage |
||||
|
|
||||
|
def initialize(str) |
||||
|
m = LDAP::Server::Syntax::AttributeTypeDescription.match(str) |
||||
|
raise LDAP::ResultError::InvalidAttributeSyntax, |
||||
|
"Bad AttributeTypeDescription #{str.inspect}" unless m |
||||
|
@oid = m[1] |
||||
|
@names = (m[2]||"").scan(/'(.*?)'/).flatten |
||||
|
@desc = m[3] |
||||
|
@obsolete = ! m[4].nil? |
||||
|
@sup = m[5] |
||||
|
@equality = m[6] |
||||
|
@ordering = m[7] |
||||
|
@substr = m[8] |
||||
|
@syntax = m[9] |
||||
|
@maxlen = m[10] && m[10].to_i |
||||
|
@singlevalue = ! m[11].nil? |
||||
|
@collective = ! m[12].nil? |
||||
|
@nousermod = ! m[13].nil? |
||||
|
@usage = m[14] && m[14].intern |
||||
|
# This is the cache of the stringified version. Rather than |
||||
|
# initialize to str, we set nil to force it to be rebuilt |
||||
|
@def = nil |
||||
|
end |
||||
|
|
||||
|
def name |
||||
|
@names.first |
||||
|
end |
||||
|
|
||||
|
def to_s |
||||
|
(@names && @names.first) || @oid |
||||
|
end |
||||
|
|
||||
|
def changed |
||||
|
@def = nil |
||||
|
end |
||||
|
|
||||
|
def to_def |
||||
|
return @def if @def |
||||
|
ans = "( #{@oid} " |
||||
|
if @names.nil? or @names.empty? |
||||
|
# nothing |
||||
|
elsif @names.size == 1 |
||||
|
ans << "NAME '#{@names.first}' " |
||||
|
else |
||||
|
ans << "NAME ( " |
||||
|
@names.each { |n| ans << "'#{n}' " } |
||||
|
ans << ") " |
||||
|
end |
||||
|
ans << "DESC '#{@desc}' " if @desc |
||||
|
ans << "OBSOLETE " if @obsolete |
||||
|
ans << "SUP #{@sup} " if @sup # oid |
||||
|
ans << "EQUALITY #{@equality} " if @equality # oid |
||||
|
ans << "ORDERING #{@ordering} " if @ordering # oid |
||||
|
ans << "SUBSTR #{@substr} " if @substr # oid |
||||
|
ans << "SYNTAX #{@syntax}#{@maxlen && "{#{@maxlen}}"} " if @syntax |
||||
|
ans << "SINGLE-VALUE " if @singlevalue |
||||
|
ans << "COLLECTIVE " if @collective |
||||
|
ans << "NO-USER-MODIFICATION " if @nousermod |
||||
|
ans << "USAGE #{@usage} " if @usage |
||||
|
ans << ")" |
||||
|
@def = ans |
||||
|
end |
||||
|
end # class AttributeType |
||||
|
|
||||
|
##################################################################### |
||||
|
|
||||
|
# Class holding an instance of an ObjectClassDescription (RFC2252 4.4) |
||||
|
|
||||
|
class ObjectClass |
||||
|
|
||||
|
attr_reader :oid, :names, :desc, :obsolete, :sup, :struct, :must, :may |
||||
|
|
||||
|
SCAN_WOID = /#{LDAP::Server::Syntax::WOID}/x |
||||
|
|
||||
|
def initialize(str) |
||||
|
m = LDAP::Server::Syntax::ObjectClassDescription.match(str) |
||||
|
raise LDAP::ResultError::InvalidAttributeSyntax, |
||||
|
"Bad ObjectClassDescription #{str.inspect}" unless m |
||||
|
@oid = m[1] |
||||
|
@names = (m[2]||"").scan(/'(.*?)'/).flatten |
||||
|
@desc = m[3] |
||||
|
@obsolete = ! m[4].nil? |
||||
|
@sup = (m[5]||"").scan(SCAN_WOID).flatten |
||||
|
@struct = m[6] ? m[6].downcase.intern : :structural |
||||
|
@must = (m[7]||"").scan(SCAN_WOID).flatten |
||||
|
@may = (m[8]||"").scan(SCAN_WOID).flatten |
||||
|
@def = nil |
||||
|
end |
||||
|
|
||||
|
def name |
||||
|
@names.first |
||||
|
end |
||||
|
|
||||
|
def to_s |
||||
|
(@names && @names.first) || @oid |
||||
|
end |
||||
|
|
||||
|
def changed |
||||
|
@def = nil |
||||
|
end |
||||
|
|
||||
|
def to_def |
||||
|
return @def if @def |
||||
|
ans = "( #{@oid} " |
||||
|
if @names.nil? or @names.empty? |
||||
|
# nothing |
||||
|
elsif @names.size == 1 |
||||
|
ans << "NAME '#{@names.first}' " |
||||
|
else |
||||
|
ans << "NAME ( " |
||||
|
@names.each { |n| ans << "'#{n}' " } |
||||
|
ans << ") " |
||||
|
end |
||||
|
ans << "DESC '#{@desc}' " if @desc |
||||
|
ans << "OBSOLETE " if @obsolete |
||||
|
ans << joinoids("SUP ",@sup," ") |
||||
|
ans << "#{@struct.to_s.upcase} " if @struct |
||||
|
ans << joinoids("MUST ",@must," ") |
||||
|
ans << joinoids("MAY ",@may," ") |
||||
|
ans << ")" |
||||
|
@def = ans |
||||
|
end |
||||
|
|
||||
|
def joinoids(pfx,arr,sfx) |
||||
|
return "" unless arr and !arr.empty? |
||||
|
return "#{pfx}#{arr}#{sfx}" unless arr.is_a?(Array) |
||||
|
a = arr.collect { |elem| elem.to_s } |
||||
|
if a.size == 1 |
||||
|
return "#{pfx}#{a.first}#{sfx}" |
||||
|
else |
||||
|
return "#{pfx}( #{a.join(" $ ")} )#{sfx}" |
||||
|
end |
||||
|
end |
||||
|
end # class ObjectClass |
||||
|
|
||||
|
end # class Schema |
||||
|
|
||||
|
end # class Server |
||||
|
end # module LDAP |
||||
@ -0,0 +1,123 @@ |
|||||
|
require 'ldap/server/connection' |
||||
|
require 'ldap/server/operation' |
||||
|
require 'openssl' |
||||
|
require 'logger' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
attr_accessor :root_dse |
||||
|
|
||||
|
DEFAULT_OPT = { |
||||
|
:port=>389, |
||||
|
:nodelay=>true, |
||||
|
} |
||||
|
|
||||
|
# Create a new server. Options include all those to tcpserver/preforkserver |
||||
|
# plus: |
||||
|
# either |
||||
|
# :router=>Router - request router instance |
||||
|
# or |
||||
|
# :operation_class=>Class - set Operation handler class |
||||
|
# :operation_args=>[...] - args to Operation.new |
||||
|
# |
||||
|
# :ssl_key_file=>pem, :ssl_cert_file=>pem - enable SSL |
||||
|
# :ssl_ca_path=>directory - verify peer certificates |
||||
|
# :schema=>Schema - Schema object |
||||
|
# :namingContexts=>[dn, ...] - base DN(s) we answer |
||||
|
# |
||||
|
# Specifying a :router always overrides :operation_class |
||||
|
|
||||
|
attr_reader :logger |
||||
|
|
||||
|
def initialize(opt = DEFAULT_OPT) |
||||
|
@opt = opt |
||||
|
@opt[:server] = self |
||||
|
if @opt[:router] |
||||
|
@opt.delete(:operation_class) |
||||
|
@opt.delete(:operation_args) |
||||
|
else |
||||
|
@opt[:operation_class] ||= LDAP::Server::Operation |
||||
|
@opt[:operation_args] ||= [] |
||||
|
end |
||||
|
unless @opt[:logger] |
||||
|
@opt[:logger] ||= Logger.new($stderr) |
||||
|
@opt[:logger].level = Logger::INFO |
||||
|
end |
||||
|
@logger = @opt[:logger] |
||||
|
LDAP::Server.ssl_prepare(@opt) |
||||
|
@schema = opt[:schema] # may be nil |
||||
|
@root_dse = Hash.new { |h,k| h[k] = [] }.merge({ |
||||
|
'objectClass' => ['top','openLDAProotDSE','extensibleObject'], |
||||
|
'supportedLDAPVersion' => ['3'], |
||||
|
#'altServer' => |
||||
|
#'supportedExtension' => |
||||
|
#'supportedControl' => |
||||
|
#'supportedSASLMechanisms' => |
||||
|
}) |
||||
|
@root_dse['subschemaSubentry'] = [@schema.subschema_dn] if @schema |
||||
|
@root_dse['namingContexts'] = opt[:namingContexts] if opt[:namingContexts] |
||||
|
end |
||||
|
|
||||
|
# create opt[:ssl_ctx] from the other ssl options |
||||
|
|
||||
|
def self.ssl_prepare(opt) # :nodoc: |
||||
|
if opt[:ssl_key_file] and opt[:ssl_cert_file] |
||||
|
ctx = OpenSSL::SSL::SSLContext.new |
||||
|
ctx.key = OpenSSL::PKey::RSA.new(File::read(opt[:ssl_key_file])) |
||||
|
ctx.cert = OpenSSL::X509::Certificate.new(File::read(opt[:ssl_cert_file])) |
||||
|
if opt[:ssl_dhparams] |
||||
|
ctx.tmp_dh_callback = proc { |*args| |
||||
|
OpenSSL::PKey::DH.new( |
||||
|
File.read(opt[:ssl_dhparams]) |
||||
|
) |
||||
|
} |
||||
|
end |
||||
|
if opt[:ssl_ca_path] |
||||
|
ctx.ca_path = opt[:ssl_ca_path] |
||||
|
ctx.verify_mode = opt[:ssl_verify_mode] || |
||||
|
OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT |
||||
|
elsif opt[:ssl_verify_mode] != 0 |
||||
|
$stderr.puts "Warning: No ssl_ca_path, peer certificate won't be verified" |
||||
|
end |
||||
|
opt[:ssl_ctx] = ctx |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def run_tcpserver |
||||
|
require 'ldap/server/tcpserver' |
||||
|
|
||||
|
opt = @opt |
||||
|
@thread = LDAP::Server.tcpserver(@opt) do |
||||
|
LDAP::Server::Connection::new(self,opt).handle_requests |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def run_prefork |
||||
|
require 'ldap/server/preforkserver' |
||||
|
|
||||
|
opt = @opt |
||||
|
@thread = LDAP::Server.preforkserver(@opt) do |
||||
|
LDAP::Server::Connection::new(self,opt).handle_requests |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def join |
||||
|
begin |
||||
|
@thread.join |
||||
|
rescue Interrupt |
||||
|
@logger.info "Exiting..." |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def stop |
||||
|
@thread.raise Interrupt, "" # <= temporary fix for 1.8.6 |
||||
|
begin |
||||
|
@thread.join |
||||
|
rescue Interrupt |
||||
|
# nop |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
end # class Server |
||||
|
end # module LDAP |
||||
@ -0,0 +1,235 @@ |
|||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
# A class which describes LDAP SyntaxDescriptions. For now there is |
||||
|
# a global pool of Syntax objects (rather than each Schema object |
||||
|
# having its own set) |
||||
|
|
||||
|
class Syntax |
||||
|
attr_reader :oid, :nhr, :binary, :desc |
||||
|
|
||||
|
# Create a new Syntax object |
||||
|
|
||||
|
def initialize(oid, desc=nil, opt={}, &blk) |
||||
|
@oid = oid |
||||
|
@desc = desc |
||||
|
@nhr = opt[:nhr] # not human-readable? |
||||
|
@binary = opt[:binary] # binary encoding forced? |
||||
|
@re = opt[:re] # regular expression for parsing |
||||
|
@def = nil |
||||
|
instance_eval(&blk) if blk |
||||
|
end |
||||
|
|
||||
|
def to_s |
||||
|
@oid |
||||
|
end |
||||
|
|
||||
|
# Create a new Syntax object, given its description string |
||||
|
|
||||
|
def self.from_def(str, &blk) |
||||
|
m = LDAPSyntaxDescription.match(str) |
||||
|
raise LDAP::ResultError::InvalidAttributeSyntax, |
||||
|
"Bad SyntaxTypeDescription #{str.inspect}" unless m |
||||
|
new(m[1], m[2], :nhr=>(m[3] == 'TRUE'), :binary=>(m[4] == 'TRUE'), &blk) |
||||
|
end |
||||
|
|
||||
|
# Convert this object to its description string |
||||
|
|
||||
|
def to_def |
||||
|
return @def if @def |
||||
|
ans = "( #@oid " |
||||
|
ans << "DESC '#@desc' " if @desc |
||||
|
# These are OpenLDAP extensions |
||||
|
ans << "X-BINARY-TRANSFER-REQUIRED 'TRUE' " if @binary |
||||
|
ans << "X-NOT-HUMAN-READABLE 'TRUE' " if @nhr |
||||
|
ans << ")" |
||||
|
@def = ans |
||||
|
end |
||||
|
|
||||
|
# Return true or a MatchData object if the given value is allowed |
||||
|
# by this syntax |
||||
|
|
||||
|
def match(val) |
||||
|
return true if @re.nil? |
||||
|
@re.match(value_to_s(val)) |
||||
|
end |
||||
|
|
||||
|
# Convert a value for this syntax into its canonical string representation |
||||
|
# (not yet used, but seemed like a good idea) |
||||
|
|
||||
|
def value_to_s(val) |
||||
|
val.to_s |
||||
|
end |
||||
|
|
||||
|
# Convert a string value for this syntax into a Ruby-like value |
||||
|
# (not yet used, but seemed like a good idea) |
||||
|
|
||||
|
def value_from_s(val) |
||||
|
val |
||||
|
end |
||||
|
|
||||
|
@@syntaxes = {} |
||||
|
|
||||
|
# Add a new syntax definition |
||||
|
|
||||
|
def self.add(*args, &blk) |
||||
|
s = new(*args, &blk) |
||||
|
@@syntaxes[s.oid] = s |
||||
|
end |
||||
|
|
||||
|
# Find a Syntax object given an oid. If not known, return a new empty |
||||
|
# Syntax object associated with this oid. |
||||
|
|
||||
|
def self.find(oid) |
||||
|
return oid if oid.nil? or oid.is_a?(LDAP::Server::Syntax) |
||||
|
return @@syntaxes[oid] if @@syntaxes[oid] |
||||
|
add(oid) |
||||
|
end |
||||
|
|
||||
|
# Return all known syntax objects |
||||
|
|
||||
|
def self.all_syntaxes |
||||
|
@@syntaxes.values.uniq |
||||
|
end |
||||
|
|
||||
|
# Shared constants for regexp-based syntax parsers |
||||
|
|
||||
|
KEYSTR = "[a-zA-Z][a-zA-Z0-9;-]*" |
||||
|
NUMERICOID = "( \\d[\\d.]+\\d )" |
||||
|
WOID = "\\s* ( #{KEYSTR} | \\d[\\d.]+\\d ) \\s*" |
||||
|
_WOID = "\\s* (?: #{KEYSTR} | \\d[\\d.]+\\d ) \\s*" |
||||
|
OIDS = "( #{_WOID} | \\s* \\( #{_WOID} (?: \\$ #{_WOID} )* \\) \\s* )" |
||||
|
_QDESCR = "\\s* ' #{KEYSTR} ' \\s*" |
||||
|
QDESCRS = "( #{_QDESCR} | \\s* \\( (?:#{_QDESCR})+ \\) \\s* )" |
||||
|
QDSTRING = "\\s* ' (.*?) ' \\s*" |
||||
|
NOIDLEN = "(\\d[\\d.]+\\d) (?: \\{ (\\d+) \\} )?" |
||||
|
ATTRIBUTEUSAGE = "(userApplications|directoryOperation|distributedOperation|dSAOperation)" |
||||
|
|
||||
|
end |
||||
|
|
||||
|
class Syntax |
||||
|
|
||||
|
# These are the 'SHOULD' support syntaxes from RFC2252 section 6 |
||||
|
|
||||
|
AttributeTypeDescription = |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.3", "Attribute Type Description", :re=> |
||||
|
%r! \A \s* \( \s* |
||||
|
#{NUMERICOID} \s* |
||||
|
(?: NAME #{QDESCRS} )? |
||||
|
(?: DESC #{QDSTRING} )? |
||||
|
( OBSOLETE \s* )? |
||||
|
(?: SUP #{WOID} )? |
||||
|
(?: EQUALITY #{WOID} )? |
||||
|
(?: ORDERING #{WOID} )? |
||||
|
(?: SUBSTR #{WOID} )? |
||||
|
(?: SYNTAX \s* #{NOIDLEN} \s* )? # capture 2 |
||||
|
( SINGLE-VALUE \s* )? |
||||
|
( COLLECTIVE \s* )? |
||||
|
( NO-USER-MODIFICATION \s* )? |
||||
|
(?: USAGE \s* #{ATTRIBUTEUSAGE} )? |
||||
|
\s* \) \s* \z !xu) |
||||
|
|
||||
|
add("1.3.6.1.4.1.1466.115.121.1.5", "Binary", :nhr=>true) |
||||
|
# FIXME: value_to_s should BER-encode the value?? |
||||
|
|
||||
|
add("1.3.6.1.4.1.1466.115.121.1.6", "Bit String", :re=>/\A'([01]*)'B\z/) |
||||
|
# FIXME: convert to FixNum? |
||||
|
|
||||
|
add("1.3.6.1.4.1.1466.115.121.1.7", "Boolean", :re=>/\A(TRUE|FALSE)\z/) do |
||||
|
def self.value_to_s(v) |
||||
|
return v if v.is_a?(string) |
||||
|
v ? "TRUE" : "FALSE" |
||||
|
end |
||||
|
def self.value_from_s(v) |
||||
|
v.upcase == "TRUE" |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
add("1.3.6.1.4.1.1466.115.121.1.8", "Certificate", :binary=>true, :nhr=>true) |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.9", "Certificate List", :binary=>true, :nhr=>true) |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.10", "Certificate Pair", :binary=>true, :nhr=>true) |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.11", "Country String", :re=>/\A[A-Z]{2}\z/i) |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.12", "Distinguished Name") |
||||
|
# FIXME: validate DN? |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.15", "Directory String") |
||||
|
# missed due to lack of interest: "DIT Content Rule Description" |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.22", "Facsimile Telephone Number") |
||||
|
add(" 1.3.6.1.4.1.1466.115.121.1.23", "Fax", :nhr=>true) |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.24", "Generalized Time") |
||||
|
# FIXME: Validate Generalized Time (find X.208) and convert to/from Ruby Time |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.26", "IA5 String") |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.27", "Integer", :re=>/\A\d+\z/) do |
||||
|
def self.value_from_s(v) |
||||
|
v.to_i |
||||
|
end |
||||
|
end |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.28", "JPEG", :nhr=>true) |
||||
|
MatchingRuleDescription = |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.30", "Matching Rule Description", :re=> |
||||
|
%r! \A \s* \( \s* |
||||
|
#{NUMERICOID} \s* |
||||
|
(?: NAME #{QDESCRS} )? |
||||
|
(?: DESC #{QDSTRING} )? |
||||
|
( OBSOLETE \s* )? |
||||
|
SYNTAX \s* #{NUMERICOID} \s* |
||||
|
\s* \) \s* \z !xu) |
||||
|
MatchingRuleUseDescription = |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.31", "Matching Rule Use Description", :re=> |
||||
|
%r! \A \s* \( \s* |
||||
|
#{NUMERICOID} \s* |
||||
|
(?: NAME #{QDESCRS} )? |
||||
|
(?: DESC #{QDSTRING} )? |
||||
|
( OBSOLETE \s* )? |
||||
|
APPLIES \s* #{OIDS} \s* |
||||
|
\s* \) \s* \z !xu) |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.33", "MHS OR Address") |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.34", "Name And Optional UID") |
||||
|
# missed due to lack of interest: "Name Form Description" |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.36", "Numeric String", :re=>/\A\d+\z/) |
||||
|
ObjectClassDescription = |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.37", "Object Class Description", :re=> |
||||
|
%r! \A \s* \( \s* |
||||
|
#{NUMERICOID} \s* |
||||
|
(?: NAME #{QDESCRS} )? |
||||
|
(?: DESC #{QDSTRING} )? |
||||
|
( OBSOLETE \s* )? |
||||
|
(?: SUP #{OIDS} )? |
||||
|
(?: ( ABSTRACT|STRUCTURAL|AUXILIARY ) \s* )? |
||||
|
(?: MUST #{OIDS} )? |
||||
|
(?: MAY #{OIDS} )? |
||||
|
\s* \) \s* \z !xu) |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.38", "OID", :re=>/\A#{WOID}\z/xu) |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.39", "Other Mailbox") |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.41", "Postal Address") do |
||||
|
def self.value_from_s(v) |
||||
|
v.split(/\$/) |
||||
|
end |
||||
|
def self.value_to_s(v) |
||||
|
return v.join("$") if v.is_a?(Array) |
||||
|
return v |
||||
|
end |
||||
|
end |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.43", "Presentation Address") |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.44", "Printable String") |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.50", "Telephone Number") |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.53", "UTC Time") |
||||
|
|
||||
|
LDAPSyntaxDescription = |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.54", "LDAP Syntax Description", :re=> |
||||
|
%r! \A \s* \( \s* |
||||
|
#{NUMERICOID} \s* |
||||
|
(?: DESC #{QDSTRING} )? |
||||
|
(?: X-BINARY-TRANSFER-REQUIRED \s* ' (TRUE|FALSE) ' \s* )? |
||||
|
(?: X-NOT-HUMAN-READABLE \s* ' (TRUE|FALSE) ' \s* )? |
||||
|
\s* \) \s* \z !xu) |
||||
|
|
||||
|
# Missed due to lack of interest: "DIT Structure Rule Description" |
||||
|
|
||||
|
# A few others from RFC2252 section 4.3.2 |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.4", "Audio", :nhr=>true) |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.40", "Octet String") |
||||
|
add("1.3.6.1.4.1.1466.115.121.1.58", "Substring Assertion") |
||||
|
end |
||||
|
|
||||
|
end # class Server |
||||
|
end # module LDAP |
||||
@ -0,0 +1,102 @@ |
|||||
|
require 'socket' |
||||
|
require 'fileutils' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
# Accept connections on a port, and for each one start a new thread |
||||
|
# and run the given block. Returns the Thread object for the listener. |
||||
|
# |
||||
|
# FIXME: |
||||
|
# - have a limit on total number of concurrent connects |
||||
|
# - have a limit on connections from a single IP, or from a /24 |
||||
|
# (to avoid the trivial DoS that the first limit creates) |
||||
|
# - ACL using source IP address (or perhaps that belongs in application) |
||||
|
# |
||||
|
# Options: |
||||
|
# :port=>port number [required] |
||||
|
# :bindaddr=>"IP address" |
||||
|
# :user=>"username" - drop privileges after bind |
||||
|
# :group=>"groupname" - ditto |
||||
|
# :logger=>object - implements << method |
||||
|
# :listen=>number - listen queue depth |
||||
|
# :nodelay=>true - set TCP_NODELAY option |
||||
|
|
||||
|
def self.tcpserver(opt, &blk) |
||||
|
if opt[:socket] |
||||
|
server = UNIXServer.new(opt[:socket]) |
||||
|
FileUtils.chmod(0777, opt[:socket]) |
||||
|
else |
||||
|
server = TCPServer.new(opt[:bindaddr] || "0.0.0.0", opt[:port]) |
||||
|
end |
||||
|
|
||||
|
# Drop privileges if requested |
||||
|
require 'etc' if opt[:group] or opt[:user] |
||||
|
Process.gid = Process.egid = Etc.getgrnam(opt[:group]).gid if opt[:group] |
||||
|
Process.uid = Process.euid = Etc.getpwnam(opt[:user]).uid if opt[:user] |
||||
|
|
||||
|
Process.gid = opt[:gid] if opt[:gid] |
||||
|
Process.uid = opt[:uid] if opt[:uid] |
||||
|
|
||||
|
if opt[:socket] |
||||
|
FileUtils.chown((opt[:user] || opt[:uid]), (opt[:group] || opt[:gid]), opt[:socket]) |
||||
|
end |
||||
|
|
||||
|
# Typically the O/S will buffer response data for 100ms before sending. |
||||
|
# If the response is sent as a single write() then there's no need for it. |
||||
|
if opt[:nodelay] |
||||
|
begin |
||||
|
server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) |
||||
|
rescue Exception |
||||
|
end |
||||
|
end |
||||
|
# set queue size for incoming connections (default is 5) |
||||
|
server.listen(opt[:listen]) if opt[:listen] |
||||
|
|
||||
|
Thread.new do |
||||
|
while true |
||||
|
begin |
||||
|
session = server.accept |
||||
|
# subtlety: copy 'session' into a block-local variable because |
||||
|
# it will change when the next session is accepted |
||||
|
Thread.new(session) do |s| |
||||
|
begin |
||||
|
s.instance_eval(&blk) |
||||
|
rescue Exception => e |
||||
|
opt[:logger].error(s.peeraddr[3]) {"#{e}: #{e.backtrace[0]}"} |
||||
|
ensure |
||||
|
s.close |
||||
|
end |
||||
|
end |
||||
|
rescue Interrupt |
||||
|
# This exception can be raised to shut the server down |
||||
|
server.close if server and not server.closed? |
||||
|
break |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
end # class Server |
||||
|
end # module LDAP |
||||
|
|
||||
|
if __FILE__ == $0 |
||||
|
# simple test |
||||
|
puts "Running a test POP3 server on port 1110" |
||||
|
t = LDAP::Server.tcpserver(:port=>1110) do |
||||
|
print "+OK I am a fake POP3 server\r\n" |
||||
|
while line = gets |
||||
|
case line |
||||
|
when /^quit/i |
||||
|
break |
||||
|
when /^crash/i |
||||
|
raise Errno::EPERM, "dammit!" |
||||
|
else |
||||
|
print "-ERR I don't understand #{line}" |
||||
|
end |
||||
|
end |
||||
|
print "+OK bye\r\n" |
||||
|
end |
||||
|
#sleep 10; t.raise Interrupt # uncomment to run for fixed time period |
||||
|
t.join |
||||
|
end |
||||
@ -0,0 +1,92 @@ |
|||||
|
require 'ldap/server/dn' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
class Trie |
||||
|
|
||||
|
# Trie or prefix tree suitable for storing LDAP paths |
||||
|
# Variables (wildcards) are supported |
||||
|
|
||||
|
class NodeNotFoundError < Error; end |
||||
|
|
||||
|
attr_accessor :parent, :value, :children |
||||
|
|
||||
|
# Create a new Trie. Use with a block |
||||
|
def initialize(parent = nil, value = nil) |
||||
|
@parent = parent |
||||
|
@value = value |
||||
|
@children = Hash.new |
||||
|
|
||||
|
yield self if block_given? |
||||
|
end |
||||
|
|
||||
|
# Insert a path (empty node) |
||||
|
def <<(dn) |
||||
|
insert(dn) |
||||
|
end |
||||
|
|
||||
|
# Insert a node with a value |
||||
|
def insert(dn, value = nil) |
||||
|
dn = LDAP::Server::DN.new(dn || '') if not dn.is_a? LDAP::Server::DN |
||||
|
dn.reverse_each do |component| |
||||
|
@children[component] = Trie.new(self) if @children[component].nil? |
||||
|
dn.dname.pop |
||||
|
if dn.any? |
||||
|
@children[component].insert dn, value |
||||
|
else |
||||
|
@children[component].value = value |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Looks up a node and returns its value or raises |
||||
|
# LDAP::Server::Trie::NodeNotFoundError if it's not in the tree |
||||
|
def lookup(dn) |
||||
|
dn = LDAP::Server::DN.new(dn || '') if not dn.is_a? LDAP::Server::DN |
||||
|
return @value if dn.dname.empty? |
||||
|
component = dn.dname.pop |
||||
|
@children.each do |key, value| |
||||
|
if key.keys.first == component.keys.first |
||||
|
if key.values.first.start_with?(':') or key.values.first == component.values.first |
||||
|
return value.lookup dn |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
raise NodeNotFoundError |
||||
|
end |
||||
|
|
||||
|
# Looks up a node and returns its value or the (non-nil) value of |
||||
|
# the nearest ancestor. |
||||
|
def match(dn, path = '') |
||||
|
dn = LDAP::Server::DN.new(dn || '') if not dn.is_a? LDAP::Server::DN |
||||
|
return path, @value if dn.dname.empty? |
||||
|
component = dn.dname.pop |
||||
|
@children.each do |key, value| |
||||
|
if key.keys.first == component.keys.first |
||||
|
if key.values.first.start_with?(':') or key.values.first == component.values.first |
||||
|
path.prepend ',' unless path.empty? |
||||
|
path.prepend "#{LDAP::Server::DN.join key}" |
||||
|
new_path, new_value = value.match dn, path |
||||
|
if new_value |
||||
|
return new_path, new_value |
||||
|
else |
||||
|
return (@value ? path : nil), @value |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
return path, @value |
||||
|
end |
||||
|
|
||||
|
def print_tree(prefix = '') |
||||
|
if @value |
||||
|
p "#{prefix}{{#{@value}}}" |
||||
|
end |
||||
|
@children.each do |key, value| |
||||
|
p "#{prefix}#{key.keys.first} => #{key.values.first}" |
||||
|
@children[key].print_tree("#{prefix} ") |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
end |
||||
@ -0,0 +1,88 @@ |
|||||
|
require 'ldap/server/result' |
||||
|
|
||||
|
module LDAP |
||||
|
class Server |
||||
|
|
||||
|
class Operation |
||||
|
|
||||
|
# Return true if connection is not authenticated |
||||
|
|
||||
|
def anonymous? |
||||
|
@connection.binddn.nil? |
||||
|
end |
||||
|
|
||||
|
# Split dn string into its component parts, returning |
||||
|
# [ {attr=>val}, {attr=>val}, ... ] |
||||
|
# |
||||
|
# This is pretty horrible legacy stuff from X500; see RFC2253 for the |
||||
|
# full gore. It's stupid that the LDAP protocol sends the DN in string |
||||
|
# form, rather than in ASN1 form (as it does with search filters, for |
||||
|
# example), even though the DN syntax is defined in terms of ASN1! |
||||
|
# |
||||
|
# Attribute names are downcased, but values are not. For any |
||||
|
# case-insensitive attributes it's up to you to downcase them. |
||||
|
# |
||||
|
# Note that only v2 clients should add extra space around the comma. |
||||
|
# This is accepted, and so is semicolon instead of comma, but the |
||||
|
# full RFC1779 backwards-compatibility rules (e.g. quoted values) |
||||
|
# are not implemented. |
||||
|
# |
||||
|
# I *think* these functions will work correctly with UTF8-encoded |
||||
|
# characters, given that a multibyte UTF8 character does not contain |
||||
|
# the bytes 00-7F and therefore we cannot confuse '\', '+' etc |
||||
|
|
||||
|
def self.split_dn(dn) |
||||
|
# convert \\ to \5c, \+ to \2b etc |
||||
|
dn.gsub!(/\\([ #,+"\\<>;])/) { |match| format "\\%02x", match[1].ord } |
||||
|
|
||||
|
# Now we know that \\ and \, do not exist, it's safe to split |
||||
|
parts = dn.split(/\s*[,;]\s*/) |
||||
|
|
||||
|
parts.collect do |part| |
||||
|
res = {} |
||||
|
|
||||
|
# Split each part into attr=val+attr=val |
||||
|
avs = part.split(/\+/) |
||||
|
|
||||
|
avs.each do |av| |
||||
|
# These should all be of form attr=value |
||||
|
unless av =~ /^([^=]+)=(.*)$/ |
||||
|
raise LDAP::ResultError::ProtocolError, "Bad DN component: #{av}" |
||||
|
end |
||||
|
attr, val = $1.downcase, $2 |
||||
|
# Now we can decode those bits |
||||
|
attr.gsub!(/\\([a-f0-9][a-f0-9])/i) { $1.hex.chr } |
||||
|
val.gsub!(/\\([a-f0-9][a-f0-9])/i) { $1.hex.chr } |
||||
|
res[attr] = val |
||||
|
end |
||||
|
res |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Reverse of split_dn. Join [elements...] |
||||
|
# where each element can be {attr=>val,...} or [[attr,val],...] |
||||
|
# or just [attr,val] |
||||
|
|
||||
|
def self.join_dn(elements) |
||||
|
dn = "" |
||||
|
elements.each do |elem| |
||||
|
av = "" |
||||
|
elem = [elem] if elem[0].is_a?(String) |
||||
|
elem.each do |attr,val| |
||||
|
av << "+" unless av == "" |
||||
|
|
||||
|
av << attr << "=" << |
||||
|
val.sub(/^([# ])/, '\\\\\\1'). |
||||
|
sub(/( )$/, '\\\\\\1'). |
||||
|
gsub(/([,+"\\<>;])/, '\\\\\\1') |
||||
|
end |
||||
|
dn << "," unless dn == "" |
||||
|
dn << av |
||||
|
end |
||||
|
dn |
||||
|
end |
||||
|
|
||||
|
end # class Operation |
||||
|
|
||||
|
end # class Server |
||||
|
end # module LDAP |
||||
@ -0,0 +1,5 @@ |
|||||
|
module LDAP #:nodoc: |
||||
|
class Server #:nodoc: |
||||
|
VERSION = '0.7.0' |
||||
|
end |
||||
|
end |
||||
@ -0,0 +1,27 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
lib = File.expand_path('../lib', __FILE__) |
||||
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) |
||||
|
require 'ldap/server/version' |
||||
|
|
||||
|
Gem::Specification.new do |s| |
||||
|
s.name = %q{ruby-ldapserver} |
||||
|
s.version = LDAP::Server::VERSION |
||||
|
|
||||
|
s.authors = ["Brian Candler", "Florian Dejonckheere", "Lars Kanis"] |
||||
|
s.description = %q{ruby-ldapserver is a lightweight, pure-Ruby skeleton for implementing LDAP server applications.} |
||||
|
s.email = ["B.Candler@pobox.com", "florian@floriandejonckheere.be", "lars@greiz-reinsdorf.de"] |
||||
|
s.files = `git ls-files`.split($/) |
||||
|
s.homepage = %q{https://github.com/larskanis/ruby-ldapserver} |
||||
|
s.rdoc_options = ["--main", "README.md"] |
||||
|
s.require_paths = ["lib"] |
||||
|
s.summary = %q{A pure-Ruby framework for building LDAP servers} |
||||
|
s.test_files = s.files.grep(%r{^(test|spec|features)/}) |
||||
|
|
||||
|
s.required_ruby_version = '>= 2.3' |
||||
|
|
||||
|
s.add_development_dependency 'bundler', '>= 1.3', '< 3.0' |
||||
|
s.add_development_dependency 'rake', '~> 13.0' |
||||
|
s.add_development_dependency 'net-ldap', '~> 0.10' |
||||
|
s.add_development_dependency 'rspec', '~> 3.1' |
||||
|
s.add_development_dependency 'test-unit', '~> 3.0' |
||||
|
end |
||||
@ -0,0 +1,39 @@ |
|||||
|
require 'spec_helper' |
||||
|
|
||||
|
require 'ldap/server/operation' |
||||
|
|
||||
|
describe LDAP::Server::Operation do |
||||
|
let(:server) { double 'server' } |
||||
|
let(:connection) { double "connection", opt: { schema: schema, server: server } } |
||||
|
let(:message_id) { 337 } |
||||
|
subject(:operation) { LDAP::Server::Operation.new connection, message_id } |
||||
|
|
||||
|
context 'on search' do |
||||
|
before do |
||||
|
operation.instance_variable_set :@attributes, attributes |
||||
|
operation.instance_variable_set :@rescount, 0 |
||||
|
end |
||||
|
|
||||
|
context 'with schema and wildcard attribute query' do |
||||
|
let(:schema) do |
||||
|
double('schema').tap do |schema| |
||||
|
allow(schema).to receive(:find_attrtype).and_return nil |
||||
|
allow(schema).to receive(:find_attrtype).with('attr')\ |
||||
|
.and_return double 'attr', usage: nil |
||||
|
end |
||||
|
end |
||||
|
let(:attributes) { %w(*) } |
||||
|
|
||||
|
describe '#send_SearchResultEntry' do |
||||
|
it 'correctly handles wildcard attribute' do |
||||
|
expect(connection).to receive(:write).twice do |message| |
||||
|
expect(message).to include 'val' |
||||
|
end |
||||
|
|
||||
|
operation.send_SearchResultEntry 'o=foo', 'attr' => 'val' |
||||
|
operation.send_SearchResultEntry 'o=bar', 'attr' => 'val' |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
end |
||||
@ -0,0 +1,582 @@ |
|||||
|
# OpenLDAP Core schema |
||||
|
# $OpenLDAP: pkg/ldap/servers/slapd/schema/core.schema,v 1.68.2.6 2005/01/20 17:01:18 kurt Exp $ |
||||
|
## This work is part of OpenLDAP Software <http://www.openldap.org/>. |
||||
|
## |
||||
|
## Copyright 1998-2005 The OpenLDAP Foundation. |
||||
|
## All rights reserved. |
||||
|
## |
||||
|
## Redistribution and use in source and binary forms, with or without |
||||
|
## modification, are permitted only as authorized by the OpenLDAP |
||||
|
## Public License. |
||||
|
## |
||||
|
## A copy of this license is available in the file LICENSE in the |
||||
|
## top-level directory of the distribution or, alternatively, at |
||||
|
## <http://www.OpenLDAP.org/license.html>. |
||||
|
# |
||||
|
## Portions Copyright (C) The Internet Society (1997-2003). |
||||
|
## All Rights Reserved. |
||||
|
## |
||||
|
## This document and translations of it may be copied and furnished to |
||||
|
## others, and derivative works that comment on or otherwise explain it |
||||
|
## or assist in its implementation may be prepared, copied, published |
||||
|
## and distributed, in whole or in part, without restriction of any |
||||
|
## kind, provided that the above copyright notice and this paragraph are |
||||
|
## included on all such copies and derivative works. However, this |
||||
|
## document itself may not be modified in any way, such as by removing |
||||
|
## the copyright notice or references to the Internet Society or other |
||||
|
## Internet organizations, except as needed for the purpose of |
||||
|
## developing Internet standards in which case the procedures for |
||||
|
## copyrights defined in the Internet Standards process must be |
||||
|
## followed, or as required to translate it into languages other than |
||||
|
## English. |
||||
|
## |
||||
|
## The limited permissions granted above are perpetual and will not be |
||||
|
## revoked by the Internet Society or its successors or assigns. |
||||
|
## |
||||
|
## This document and the information contained herein is provided on an |
||||
|
## "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING |
||||
|
## TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING |
||||
|
## BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION |
||||
|
## HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF |
||||
|
## MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. |
||||
|
|
||||
|
# |
||||
|
# |
||||
|
# Includes LDAPv3 schema items from: |
||||
|
# RFC 2252/2256 (LDAPv3) |
||||
|
# |
||||
|
# Select standard track schema items: |
||||
|
# RFC 1274 (uid/dc) |
||||
|
# RFC 2079 (URI) |
||||
|
# RFC 2247 (dc/dcObject) |
||||
|
# RFC 2587 (PKI) |
||||
|
# RFC 2589 (Dynamic Directory Services) |
||||
|
# |
||||
|
# Select informational schema items: |
||||
|
# RFC 2377 (uidObject) |
||||
|
|
||||
|
# |
||||
|
# Standard attribute types from RFC 2256 |
||||
|
# |
||||
|
|
||||
|
# system schema |
||||
|
#attributetype ( 2.5.4.0 NAME 'objectClass' |
||||
|
# DESC 'RFC2256: object classes of the entity' |
||||
|
# EQUALITY objectIdentifierMatch |
||||
|
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 ) |
||||
|
|
||||
|
# system schema |
||||
|
#attributetype ( 2.5.4.1 NAME ( 'aliasedObjectName' 'aliasedEntryName' ) |
||||
|
# DESC 'RFC2256: name of aliased object' |
||||
|
# EQUALITY distinguishedNameMatch |
||||
|
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE ) |
||||
|
|
||||
|
attributetype ( 2.5.4.2 NAME 'knowledgeInformation' |
||||
|
DESC 'RFC2256: knowledge information' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} ) |
||||
|
|
||||
|
# system schema |
||||
|
#attributetype ( 2.5.4.3 NAME ( 'cn' 'commonName' ) |
||||
|
# DESC 'RFC2256: common name(s) for which the entity is known by' |
||||
|
# SUP name ) |
||||
|
|
||||
|
attributetype ( 2.5.4.4 NAME ( 'sn' 'surname' ) |
||||
|
DESC 'RFC2256: last (family) name(s) for which the entity is known by' |
||||
|
SUP name ) |
||||
|
|
||||
|
attributetype ( 2.5.4.5 NAME 'serialNumber' |
||||
|
DESC 'RFC2256: serial number of the entity' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{64} ) |
||||
|
|
||||
|
attributetype ( 2.5.4.6 NAME ( 'c' 'countryName' ) |
||||
|
DESC 'RFC2256: ISO-3166 country 2-letter code' |
||||
|
EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.11 |
||||
|
SINGLE-VALUE ) |
||||
|
|
||||
|
attributetype ( 2.5.4.7 NAME ( 'l' 'localityName' ) |
||||
|
DESC 'RFC2256: locality which this object resides in' |
||||
|
SUP name ) |
||||
|
|
||||
|
attributetype ( 2.5.4.8 NAME ( 'st' 'stateOrProvinceName' ) |
||||
|
DESC 'RFC2256: state or province which this object resides in' |
||||
|
SUP name ) |
||||
|
|
||||
|
attributetype ( 2.5.4.9 NAME ( 'street' 'streetAddress' ) |
||||
|
DESC 'RFC2256: street address of this object' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} ) |
||||
|
|
||||
|
attributetype ( 2.5.4.10 NAME ( 'o' 'organizationName' ) |
||||
|
DESC 'RFC2256: organization this object belongs to' |
||||
|
SUP name ) |
||||
|
|
||||
|
attributetype ( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) |
||||
|
DESC 'RFC2256: organizational unit this object belongs to' |
||||
|
SUP name ) |
||||
|
|
||||
|
attributetype ( 2.5.4.12 NAME 'title' |
||||
|
DESC 'RFC2256: title associated with the entity' |
||||
|
SUP name ) |
||||
|
|
||||
|
attributetype ( 2.5.4.13 NAME 'description' |
||||
|
DESC 'RFC2256: descriptive information' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} ) |
||||
|
|
||||
|
# Obsoleted by enhancedSearchGuide |
||||
|
attributetype ( 2.5.4.14 NAME 'searchGuide' |
||||
|
DESC 'RFC2256: search guide, obsoleted by enhancedSearchGuide' |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.25 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.15 NAME 'businessCategory' |
||||
|
DESC 'RFC2256: business category' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} ) |
||||
|
|
||||
|
attributetype ( 2.5.4.16 NAME 'postalAddress' |
||||
|
DESC 'RFC2256: postal address' |
||||
|
EQUALITY caseIgnoreListMatch |
||||
|
SUBSTR caseIgnoreListSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.17 NAME 'postalCode' |
||||
|
DESC 'RFC2256: postal code' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{40} ) |
||||
|
|
||||
|
attributetype ( 2.5.4.18 NAME 'postOfficeBox' |
||||
|
DESC 'RFC2256: Post Office Box' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{40} ) |
||||
|
|
||||
|
attributetype ( 2.5.4.19 NAME 'physicalDeliveryOfficeName' |
||||
|
DESC 'RFC2256: Physical Delivery Office Name' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} ) |
||||
|
|
||||
|
attributetype ( 2.5.4.20 NAME 'telephoneNumber' |
||||
|
DESC 'RFC2256: Telephone Number' |
||||
|
EQUALITY telephoneNumberMatch |
||||
|
SUBSTR telephoneNumberSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.50{32} ) |
||||
|
|
||||
|
attributetype ( 2.5.4.21 NAME 'telexNumber' |
||||
|
DESC 'RFC2256: Telex Number' |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.52 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.22 NAME 'teletexTerminalIdentifier' |
||||
|
DESC 'RFC2256: Teletex Terminal Identifier' |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.51 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.23 NAME ( 'facsimileTelephoneNumber' 'fax' ) |
||||
|
DESC 'RFC2256: Facsimile (Fax) Telephone Number' |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.22 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.24 NAME 'x121Address' |
||||
|
DESC 'RFC2256: X.121 Address' |
||||
|
EQUALITY numericStringMatch |
||||
|
SUBSTR numericStringSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{15} ) |
||||
|
|
||||
|
attributetype ( 2.5.4.25 NAME 'internationaliSDNNumber' |
||||
|
DESC 'RFC2256: international ISDN number' |
||||
|
EQUALITY numericStringMatch |
||||
|
SUBSTR numericStringSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{16} ) |
||||
|
|
||||
|
attributetype ( 2.5.4.26 NAME 'registeredAddress' |
||||
|
DESC 'RFC2256: registered postal address' |
||||
|
SUP postalAddress |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.27 NAME 'destinationIndicator' |
||||
|
DESC 'RFC2256: destination indicator' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{128} ) |
||||
|
|
||||
|
attributetype ( 2.5.4.28 NAME 'preferredDeliveryMethod' |
||||
|
DESC 'RFC2256: preferred delivery method' |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.14 |
||||
|
SINGLE-VALUE ) |
||||
|
|
||||
|
attributetype ( 2.5.4.29 NAME 'presentationAddress' |
||||
|
DESC 'RFC2256: presentation address' |
||||
|
EQUALITY presentationAddressMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.43 |
||||
|
SINGLE-VALUE ) |
||||
|
|
||||
|
attributetype ( 2.5.4.30 NAME 'supportedApplicationContext' |
||||
|
DESC 'RFC2256: supported application context' |
||||
|
EQUALITY objectIdentifierMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.31 NAME 'member' |
||||
|
DESC 'RFC2256: member of a group' |
||||
|
SUP distinguishedName ) |
||||
|
|
||||
|
attributetype ( 2.5.4.32 NAME 'owner' |
||||
|
DESC 'RFC2256: owner (of the object)' |
||||
|
SUP distinguishedName ) |
||||
|
|
||||
|
attributetype ( 2.5.4.33 NAME 'roleOccupant' |
||||
|
DESC 'RFC2256: occupant of role' |
||||
|
SUP distinguishedName ) |
||||
|
|
||||
|
attributetype ( 2.5.4.34 NAME 'seeAlso' |
||||
|
DESC 'RFC2256: DN of related object' |
||||
|
SUP distinguishedName ) |
||||
|
|
||||
|
# system schema |
||||
|
#attributetype ( 2.5.4.35 NAME 'userPassword' |
||||
|
# DESC 'RFC2256/2307: password of user' |
||||
|
# EQUALITY octetStringMatch |
||||
|
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.40{128} ) |
||||
|
|
||||
|
# Must be transferred using ;binary |
||||
|
# with certificateExactMatch rule (per X.509) |
||||
|
attributetype ( 2.5.4.36 NAME 'userCertificate' |
||||
|
DESC 'RFC2256: X.509 user certificate, use ;binary' |
||||
|
EQUALITY certificateExactMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 ) |
||||
|
|
||||
|
# Must be transferred using ;binary |
||||
|
# with certificateExactMatch rule (per X.509) |
||||
|
attributetype ( 2.5.4.37 NAME 'cACertificate' |
||||
|
DESC 'RFC2256: X.509 CA certificate, use ;binary' |
||||
|
EQUALITY certificateExactMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 ) |
||||
|
|
||||
|
# Must be transferred using ;binary |
||||
|
attributetype ( 2.5.4.38 NAME 'authorityRevocationList' |
||||
|
DESC 'RFC2256: X.509 authority revocation list, use ;binary' |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 ) |
||||
|
|
||||
|
# Must be transferred using ;binary |
||||
|
attributetype ( 2.5.4.39 NAME 'certificateRevocationList' |
||||
|
DESC 'RFC2256: X.509 certificate revocation list, use ;binary' |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 ) |
||||
|
|
||||
|
# Must be stored and requested in the binary form |
||||
|
attributetype ( 2.5.4.40 NAME 'crossCertificatePair' |
||||
|
DESC 'RFC2256: X.509 cross certificate pair, use ;binary' |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.10 ) |
||||
|
|
||||
|
# 2.5.4.41 is defined above as it's used for subtyping |
||||
|
#attributetype ( 2.5.4.41 NAME 'name' |
||||
|
# EQUALITY caseIgnoreMatch |
||||
|
# SUBSTR caseIgnoreSubstringsMatch |
||||
|
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} ) |
||||
|
|
||||
|
attributetype ( 2.5.4.42 NAME ( 'givenName' 'gn' ) |
||||
|
DESC 'RFC2256: first name(s) for which the entity is known by' |
||||
|
SUP name ) |
||||
|
|
||||
|
attributetype ( 2.5.4.43 NAME 'initials' |
||||
|
DESC 'RFC2256: initials of some or all of names, but not the surname(s).' |
||||
|
SUP name ) |
||||
|
|
||||
|
attributetype ( 2.5.4.44 NAME 'generationQualifier' |
||||
|
DESC 'RFC2256: name qualifier indicating a generation' |
||||
|
SUP name ) |
||||
|
|
||||
|
attributetype ( 2.5.4.45 NAME 'x500UniqueIdentifier' |
||||
|
DESC 'RFC2256: X.500 unique identifier' |
||||
|
EQUALITY bitStringMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.46 NAME 'dnQualifier' |
||||
|
DESC 'RFC2256: DN qualifier' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
ORDERING caseIgnoreOrderingMatch |
||||
|
SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.47 NAME 'enhancedSearchGuide' |
||||
|
DESC 'RFC2256: enhanced search guide' |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.21 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.48 NAME 'protocolInformation' |
||||
|
DESC 'RFC2256: protocol information' |
||||
|
EQUALITY protocolInformationMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.42 ) |
||||
|
|
||||
|
# 2.5.4.49 is defined above as it's used for subtyping |
||||
|
#attributetype ( 2.5.4.49 NAME 'distinguishedName' |
||||
|
# EQUALITY distinguishedNameMatch |
||||
|
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.50 NAME 'uniqueMember' |
||||
|
DESC 'RFC2256: unique member of a group' |
||||
|
EQUALITY uniqueMemberMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.51 NAME 'houseIdentifier' |
||||
|
DESC 'RFC2256: house identifier' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} ) |
||||
|
|
||||
|
# Must be transferred using ;binary |
||||
|
attributetype ( 2.5.4.52 NAME 'supportedAlgorithms' |
||||
|
DESC 'RFC2256: supported algorithms' |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.49 ) |
||||
|
|
||||
|
# Must be transferred using ;binary |
||||
|
attributetype ( 2.5.4.53 NAME 'deltaRevocationList' |
||||
|
DESC 'RFC2256: delta revocation list; use ;binary' |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 ) |
||||
|
|
||||
|
attributetype ( 2.5.4.54 NAME 'dmdName' |
||||
|
DESC 'RFC2256: name of DMD' |
||||
|
SUP name ) |
||||
|
|
||||
|
|
||||
|
# Standard object classes from RFC2256 |
||||
|
|
||||
|
# system schema |
||||
|
#objectclass ( 2.5.6.1 NAME 'alias' |
||||
|
# DESC 'RFC2256: an alias' |
||||
|
# SUP top STRUCTURAL |
||||
|
# MUST aliasedObjectName ) |
||||
|
|
||||
|
objectclass ( 2.5.6.2 NAME 'country' |
||||
|
DESC 'RFC2256: a country' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST c |
||||
|
MAY ( searchGuide $ description ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.3 NAME 'locality' |
||||
|
DESC 'RFC2256: a locality' |
||||
|
SUP top STRUCTURAL |
||||
|
MAY ( street $ seeAlso $ searchGuide $ st $ l $ description ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.4 NAME 'organization' |
||||
|
DESC 'RFC2256: an organization' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST o |
||||
|
MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ |
||||
|
x121Address $ registeredAddress $ destinationIndicator $ |
||||
|
preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ |
||||
|
telephoneNumber $ internationaliSDNNumber $ |
||||
|
facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ |
||||
|
postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.5 NAME 'organizationalUnit' |
||||
|
DESC 'RFC2256: an organizational unit' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST ou |
||||
|
MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ |
||||
|
x121Address $ registeredAddress $ destinationIndicator $ |
||||
|
preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ |
||||
|
telephoneNumber $ internationaliSDNNumber $ |
||||
|
facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ |
||||
|
postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.6 NAME 'person' |
||||
|
DESC 'RFC2256: a person' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST ( sn $ cn ) |
||||
|
MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.7 NAME 'organizationalPerson' |
||||
|
DESC 'RFC2256: an organizational person' |
||||
|
SUP person STRUCTURAL |
||||
|
MAY ( title $ x121Address $ registeredAddress $ destinationIndicator $ |
||||
|
preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ |
||||
|
telephoneNumber $ internationaliSDNNumber $ |
||||
|
facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ |
||||
|
postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.8 NAME 'organizationalRole' |
||||
|
DESC 'RFC2256: an organizational role' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST cn |
||||
|
MAY ( x121Address $ registeredAddress $ destinationIndicator $ |
||||
|
preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ |
||||
|
telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ |
||||
|
seeAlso $ roleOccupant $ preferredDeliveryMethod $ street $ |
||||
|
postOfficeBox $ postalCode $ postalAddress $ |
||||
|
physicalDeliveryOfficeName $ ou $ st $ l $ description ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.9 NAME 'groupOfNames' |
||||
|
DESC 'RFC2256: a group of names (DNs)' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST ( member $ cn ) |
||||
|
MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.10 NAME 'residentialPerson' |
||||
|
DESC 'RFC2256: an residential person' |
||||
|
SUP person STRUCTURAL |
||||
|
MUST l |
||||
|
MAY ( businessCategory $ x121Address $ registeredAddress $ |
||||
|
destinationIndicator $ preferredDeliveryMethod $ telexNumber $ |
||||
|
teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ |
||||
|
facsimileTelephoneNumber $ preferredDeliveryMethod $ street $ |
||||
|
postOfficeBox $ postalCode $ postalAddress $ |
||||
|
physicalDeliveryOfficeName $ st $ l ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.11 NAME 'applicationProcess' |
||||
|
DESC 'RFC2256: an application process' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST cn |
||||
|
MAY ( seeAlso $ ou $ l $ description ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.12 NAME 'applicationEntity' |
||||
|
DESC 'RFC2256: an application entity' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST ( presentationAddress $ cn ) |
||||
|
MAY ( supportedApplicationContext $ seeAlso $ ou $ o $ l $ |
||||
|
description ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.13 NAME 'dSA' |
||||
|
DESC 'RFC2256: a directory system agent (a server)' |
||||
|
SUP applicationEntity STRUCTURAL |
||||
|
MAY knowledgeInformation ) |
||||
|
|
||||
|
objectclass ( 2.5.6.14 NAME 'device' |
||||
|
DESC 'RFC2256: a device' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST cn |
||||
|
MAY ( serialNumber $ seeAlso $ owner $ ou $ o $ l $ description ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.15 NAME 'strongAuthenticationUser' |
||||
|
DESC 'RFC2256: a strong authentication user' |
||||
|
SUP top AUXILIARY |
||||
|
MUST userCertificate ) |
||||
|
|
||||
|
objectclass ( 2.5.6.16 NAME 'certificationAuthority' |
||||
|
DESC 'RFC2256: a certificate authority' |
||||
|
SUP top AUXILIARY |
||||
|
MUST ( authorityRevocationList $ certificateRevocationList $ |
||||
|
cACertificate ) MAY crossCertificatePair ) |
||||
|
|
||||
|
objectclass ( 2.5.6.17 NAME 'groupOfUniqueNames' |
||||
|
DESC 'RFC2256: a group of unique names (DN and Unique Identifier)' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST ( uniqueMember $ cn ) |
||||
|
MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.18 NAME 'userSecurityInformation' |
||||
|
DESC 'RFC2256: a user security information' |
||||
|
SUP top AUXILIARY |
||||
|
MAY ( supportedAlgorithms ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.16.2 NAME 'certificationAuthority-V2' |
||||
|
SUP certificationAuthority |
||||
|
AUXILIARY MAY ( deltaRevocationList ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.19 NAME 'cRLDistributionPoint' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST ( cn ) |
||||
|
MAY ( certificateRevocationList $ authorityRevocationList $ |
||||
|
deltaRevocationList ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.20 NAME 'dmd' |
||||
|
SUP top STRUCTURAL |
||||
|
MUST ( dmdName ) |
||||
|
MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ |
||||
|
x121Address $ registeredAddress $ destinationIndicator $ |
||||
|
preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ |
||||
|
telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ |
||||
|
street $ postOfficeBox $ postalCode $ postalAddress $ |
||||
|
physicalDeliveryOfficeName $ st $ l $ description ) ) |
||||
|
|
||||
|
# |
||||
|
# Object Classes from RFC 2587 |
||||
|
# |
||||
|
objectclass ( 2.5.6.21 NAME 'pkiUser' |
||||
|
DESC 'RFC2587: a PKI user' |
||||
|
SUP top AUXILIARY |
||||
|
MAY userCertificate ) |
||||
|
|
||||
|
objectclass ( 2.5.6.22 NAME 'pkiCA' |
||||
|
DESC 'RFC2587: PKI certificate authority' |
||||
|
SUP top AUXILIARY |
||||
|
MAY ( authorityRevocationList $ certificateRevocationList $ |
||||
|
cACertificate $ crossCertificatePair ) ) |
||||
|
|
||||
|
objectclass ( 2.5.6.23 NAME 'deltaCRL' |
||||
|
DESC 'RFC2587: PKI user' |
||||
|
SUP top AUXILIARY |
||||
|
MAY deltaRevocationList ) |
||||
|
|
||||
|
# |
||||
|
# Standard Track URI label schema from RFC 2079 |
||||
|
# system schema |
||||
|
#attributetype ( 1.3.6.1.4.1.250.1.57 NAME 'labeledURI' |
||||
|
# DESC 'RFC2079: Uniform Resource Identifier with optional label' |
||||
|
# EQUALITY caseExactMatch |
||||
|
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) |
||||
|
|
||||
|
objectclass ( 1.3.6.1.4.1.250.3.15 NAME 'labeledURIObject' |
||||
|
DESC 'RFC2079: object that contains the URI attribute type' |
||||
|
SUP top AUXILIARY |
||||
|
MAY labeledURI ) |
||||
|
|
||||
|
# |
||||
|
# Derived from RFC 1274, but with new "short names" |
||||
|
# |
||||
|
attributetype ( 0.9.2342.19200300.100.1.1 |
||||
|
NAME ( 'uid' 'userid' ) |
||||
|
DESC 'RFC1274: user identifier' |
||||
|
EQUALITY caseIgnoreMatch |
||||
|
SUBSTR caseIgnoreSubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) |
||||
|
|
||||
|
attributetype ( 0.9.2342.19200300.100.1.3 |
||||
|
NAME ( 'mail' 'rfc822Mailbox' ) |
||||
|
DESC 'RFC1274: RFC822 Mailbox' |
||||
|
EQUALITY caseIgnoreIA5Match |
||||
|
SUBSTR caseIgnoreIA5SubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} ) |
||||
|
|
||||
|
objectclass ( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' |
||||
|
DESC 'RFC1274: simple security object' |
||||
|
SUP top AUXILIARY |
||||
|
MUST userPassword ) |
||||
|
|
||||
|
# RFC 1274 + RFC 2247 |
||||
|
attributetype ( 0.9.2342.19200300.100.1.25 |
||||
|
NAME ( 'dc' 'domainComponent' ) |
||||
|
DESC 'RFC1274/2247: domain component' |
||||
|
EQUALITY caseIgnoreIA5Match |
||||
|
SUBSTR caseIgnoreIA5SubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) |
||||
|
|
||||
|
# RFC 2247 |
||||
|
objectclass ( 1.3.6.1.4.1.1466.344 NAME 'dcObject' |
||||
|
DESC 'RFC2247: domain component object' |
||||
|
SUP top AUXILIARY MUST dc ) |
||||
|
|
||||
|
# RFC 2377 |
||||
|
objectclass ( 1.3.6.1.1.3.1 NAME 'uidObject' |
||||
|
DESC 'RFC2377: uid object' |
||||
|
SUP top AUXILIARY MUST uid ) |
||||
|
|
||||
|
# From COSINE Pilot |
||||
|
attributetype ( 0.9.2342.19200300.100.1.37 |
||||
|
NAME 'associatedDomain' |
||||
|
DESC 'RFC1274: domain associated with object' |
||||
|
EQUALITY caseIgnoreIA5Match |
||||
|
SUBSTR caseIgnoreIA5SubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) |
||||
|
|
||||
|
# RFC 2459 -- deprecated in favor of 'mail' (in cosine.schema) |
||||
|
attributetype ( 1.2.840.113549.1.9.1 |
||||
|
NAME ( 'email' 'emailAddress' 'pkcs9email' ) |
||||
|
DESC 'RFC2459: legacy attribute for email addresses in DNs' |
||||
|
EQUALITY caseIgnoreIA5Match |
||||
|
SUBSTR caseIgnoreIA5SubstringsMatch |
||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} ) |
||||
|
|
||||
@ -0,0 +1,149 @@ |
|||||
|
require File.dirname(__FILE__) + '/test_helper' |
||||
|
require 'ldap/server/dn' |
||||
|
|
||||
|
class TestLdapDn < Test::Unit::TestCase |
||||
|
def setup |
||||
|
@dn = LDAP::Server::DN.new("cn=Steve Kille,o=Isode Limited,o=Companies,c=GB") |
||||
|
end |
||||
|
|
||||
|
def test_find_first |
||||
|
assert_equal(nil, @dn.find_first("ou")) |
||||
|
assert_equal("Steve Kille", @dn.find_first("cn")) |
||||
|
assert_equal("Isode Limited", @dn.find_first("o")) |
||||
|
end |
||||
|
|
||||
|
def test_find_last |
||||
|
assert_equal(nil, @dn.find_last("ou")) |
||||
|
assert_equal("Steve Kille", @dn.find_last("cn")) |
||||
|
assert_equal("Companies", @dn.find_last("o")) |
||||
|
end |
||||
|
|
||||
|
def test_find |
||||
|
assert_equal([], @dn.find("ou")) |
||||
|
assert_equal(["Steve Kille"], @dn.find("cn")) |
||||
|
assert_equal(["Isode Limited", "Companies"], @dn.find("o")) |
||||
|
end |
||||
|
|
||||
|
def test_find_nth |
||||
|
assert_equal(nil, @dn.find_nth("ou", 0)) |
||||
|
assert_equal("Steve Kille", @dn.find_nth("cn", 0)) |
||||
|
assert_equal(nil, @dn.find_nth("cn", 1)) |
||||
|
assert_equal("Isode Limited", @dn.find_nth("o", 0)) |
||||
|
assert_equal("Companies", @dn.find_nth("o", 1)) |
||||
|
assert_equal(nil, @dn.find_nth("o", 2)) |
||||
|
end |
||||
|
|
||||
|
def test_start_with |
||||
|
assert @dn.start_with?("cn=Steve Kille") |
||||
|
assert @dn.start_with?("cn=Steve Kille, o=Isode Limited") |
||||
|
refute @dn.start_with?("cn=John Doe") |
||||
|
end |
||||
|
|
||||
|
def test_start_with_format |
||||
|
assert @dn.start_with_format?("cn=Steve Kille") |
||||
|
assert @dn.start_with_format?("cn=foo, o=bar") |
||||
|
refute @dn.start_with_format?("c=GB") |
||||
|
refute @dn.start_with_format?("c=BE") |
||||
|
end |
||||
|
|
||||
|
def test_end_with |
||||
|
assert @dn.end_with?("c=GB") |
||||
|
assert @dn.end_with?("o=Companies, c=GB") |
||||
|
refute @dn.end_with?("c=BE") |
||||
|
end |
||||
|
|
||||
|
def test_end_with_format |
||||
|
assert @dn.end_with_format?("c=GB") |
||||
|
assert @dn.end_with_format?("o=foo, c=bar") |
||||
|
refute @dn.end_with_format?("cn=Steve Kille") |
||||
|
refute @dn.end_with_format?("cn=foo") |
||||
|
end |
||||
|
|
||||
|
def test_equal |
||||
|
assert @dn.equal?("CN=Steve Kille, o=Isode Limited,O=Companies,c=GB") |
||||
|
refute @dn.equal?("cn=John Doe,o=Isode Limited,o=Companies,c=GB") |
||||
|
end |
||||
|
|
||||
|
def test_equal_format |
||||
|
assert @dn.equal_format?("cn=Steve Kille,o=Isode Limited,o=Companies,c=GB") |
||||
|
assert @dn.equal_format?("cn=STEVE KILLE,o=ISODE LIMITED,o=COMPANIES,c=GB") |
||||
|
assert @dn.equal_format?("cn=foo,o=bar,o=baz,c=bat") |
||||
|
assert @dn.equal_format?("CN=foo,O=bar,O=baz,C=bat") |
||||
|
refute @dn.equal_format?("cn=foo,o=Isode Limited,c=GB") |
||||
|
end |
||||
|
|
||||
|
def test_include |
||||
|
assert @dn.include?("cn=Steve Kille,o=Isode Limited,o=Companies,c=GB") |
||||
|
assert @dn.include?("cn=Steve Kille") |
||||
|
assert @dn.include?("o=Isode Limited") |
||||
|
assert @dn.include?("o=Isode Limited,o=Companies") |
||||
|
assert @dn.include?("c=GB") |
||||
|
|
||||
|
refute @dn.include?("cn=John Doe,o=Isode Limited,o=Companies,c=GB") |
||||
|
refute @dn.include?("cn=Steve Kille,o=Isode Limited,c=GB") |
||||
|
end |
||||
|
|
||||
|
def test_include_format |
||||
|
assert @dn.include_format?("cn=foo,o=bar,o=baz,c=bat") |
||||
|
assert @dn.include_format?("cn=foo") |
||||
|
assert @dn.include_format?("o=bar") |
||||
|
assert @dn.include_format?("o=bar,o=baz") |
||||
|
assert @dn.include_format?("c=bat") |
||||
|
|
||||
|
refute @dn.include_format?("cn=foo,o=bar,c=bat") |
||||
|
refute @dn.include_format?("cn=bar,c=bat") |
||||
|
end |
||||
|
|
||||
|
def test_parse |
||||
|
assert_equal @dn.parse("cn=:cn,o=:company,o=Companies,c=:country"), |
||||
|
{ |
||||
|
:cn => 'Steve Kille', |
||||
|
:company => 'Isode Limited', |
||||
|
:country => 'GB' |
||||
|
} |
||||
|
assert_equal @dn.parse("cn=:cn,o=:company,o=:company,c=:country"), |
||||
|
{ |
||||
|
:cn => 'Steve Kille', |
||||
|
:company => 'Companies', |
||||
|
:country => 'GB' |
||||
|
} |
||||
|
assert_empty @dn.parse("cn=Steve Kille,o=Isode Limited,o=Companies,c=GB") |
||||
|
|
||||
|
assert_equal @dn.parse("cn=:cn,cn=Steve Kille,o=Isode Limited,o=Companies,c=GB"), |
||||
|
{ |
||||
|
:cn => nil |
||||
|
} |
||||
|
|
||||
|
assert_empty @dn.parse("o=Foo,o=Companies,c=GB") |
||||
|
assert_empty @dn.parse("cn=:cn,o=Foo,o=Companies,c=GB") |
||||
|
|
||||
|
end |
||||
|
|
||||
|
def test_each |
||||
|
answer = [ |
||||
|
{ 'cn' => 'Steve Kille' }, |
||||
|
{ 'o' => 'Isode Limited' }, |
||||
|
{ 'o' => 'Companies' }, |
||||
|
{ 'c' => 'GB' } |
||||
|
] |
||||
|
i = 0 |
||||
|
@dn.each do |pair| |
||||
|
assert_equal pair, answer[i] |
||||
|
i += 1 |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def test_reverse_each |
||||
|
answer = [ |
||||
|
{ 'c' => 'GB' }, |
||||
|
{ 'o' => 'Companies' }, |
||||
|
{ 'o' => 'Isode Limited' }, |
||||
|
{ 'cn' => 'Steve Kille' } |
||||
|
] |
||||
|
i = 0 |
||||
|
@dn.reverse_each do |pair| |
||||
|
assert_equal pair, answer[i] |
||||
|
i += 1 |
||||
|
end |
||||
|
end |
||||
|
end |
||||
@ -0,0 +1,289 @@ |
|||||
|
require File.dirname(__FILE__) + '/test_helper' |
||||
|
|
||||
|
Thread.abort_on_exception = true |
||||
|
|
||||
|
# This test suite requires the ruby-ldap client library to be installed. |
||||
|
# |
||||
|
# Unfortunately, this library is not ruby thread-safe (it blocks until |
||||
|
# it gets a response). Hence we have to fork a child to perform the actual |
||||
|
# LDAP requests, which is nasty. However, it does give us a completely |
||||
|
# independent source of LDAP packets to try. |
||||
|
|
||||
|
require 'ldap/server/operation' |
||||
|
require 'ldap/server/server' |
||||
|
|
||||
|
require 'net/ldap' |
||||
|
|
||||
|
# We subclass the Operation class, overriding the methods to do what we need |
||||
|
|
||||
|
class MockOperation < LDAP::Server::Operation |
||||
|
def initialize(connection, messageId) |
||||
|
super(connection, messageId) |
||||
|
@@lastop = [:connect] |
||||
|
end |
||||
|
|
||||
|
def simple_bind(version, user, pass) |
||||
|
@@lastop = [:simple_bind, version.to_i, user, pass] |
||||
|
end |
||||
|
|
||||
|
def search(basedn, scope, deref, filter) |
||||
|
@@lastop = [:search, basedn, scope.to_i, deref.to_i, filter, @attributes] |
||||
|
send_SearchResultEntry("cn=foo", {"a"=>["1","2"], "b"=>"boing"}) |
||||
|
send_SearchResultEntry("cn=bar", {"a"=>["3","4","5"], "b"=>"wibble"}) |
||||
|
end |
||||
|
|
||||
|
def add(dn, av) |
||||
|
@@lastop = [:add, dn, av] |
||||
|
end |
||||
|
|
||||
|
def del(dn) |
||||
|
@@lastop = [:del, dn] |
||||
|
end |
||||
|
|
||||
|
def modify(dn, ops) |
||||
|
@@lastop = [:modify, dn, ops] |
||||
|
end |
||||
|
|
||||
|
def modifydn(dn, newrdn, deleteoldrdn, newSuperior) |
||||
|
@@lastop = [:modifydn, dn, newrdn, deleteoldrdn, newSuperior] |
||||
|
end |
||||
|
|
||||
|
def compare(dn, attr, val) |
||||
|
@@lastop = [:compare, dn, attr, val] |
||||
|
return val != "false" |
||||
|
end |
||||
|
|
||||
|
def self.lastop |
||||
|
@@lastop |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
class TestLdap < Test::Unit::TestCase |
||||
|
HOST = '127.0.0.1' |
||||
|
PORT = 1389 |
||||
|
|
||||
|
def start_client |
||||
|
in_ = Queue.new |
||||
|
out = Queue.new |
||||
|
Thread.new do |
||||
|
do_child(in_, out) |
||||
|
end |
||||
|
return in_, out |
||||
|
end |
||||
|
|
||||
|
def ensure_server_started |
||||
|
@serv || start_server |
||||
|
end |
||||
|
|
||||
|
def start_server(opts={}) |
||||
|
# back to a single process (the parent). Now we start our |
||||
|
# listener thread |
||||
|
@serv = LDAP::Server.new({ |
||||
|
:bindaddr => '127.0.0.1', |
||||
|
:port => PORT, |
||||
|
:nodelay => true, |
||||
|
:operation_class => MockOperation, |
||||
|
}.merge(opts)) |
||||
|
|
||||
|
@serv.run_tcpserver |
||||
|
end |
||||
|
|
||||
|
def setup |
||||
|
@client = nil |
||||
|
@serv = nil |
||||
|
end |
||||
|
|
||||
|
def teardown |
||||
|
if @serv |
||||
|
@serv.stop |
||||
|
@serv = nil |
||||
|
end |
||||
|
if @client |
||||
|
# @client.close |
||||
|
@client = nil |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def conn |
||||
|
ensure_server_started |
||||
|
@client ||= Net::LDAP.new(host: HOST, port: PORT) |
||||
|
end |
||||
|
|
||||
|
def test_bind2 |
||||
|
pend("net-ldap gem doesn't support protocol 2") |
||||
|
|
||||
|
# TODO: Net::LDAP only supports protocol 3 |
||||
|
conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 2) |
||||
|
conn.auth("foo","bar") |
||||
|
conn.bind |
||||
|
|
||||
|
assert_equal([:simple_bind, 2, "foo", "bar"], MockOperation.lastop) |
||||
|
# cannot bind any more; ldap client library says "already binded." (sic) |
||||
|
end |
||||
|
|
||||
|
def test_bind3 |
||||
|
conn.auth("foo","bar") |
||||
|
conn.bind |
||||
|
|
||||
|
assert_equal([:simple_bind, 3, "foo", "bar"], MockOperation.lastop) |
||||
|
# cannot bind any more; ldap client library says "already binded." (sic) |
||||
|
end |
||||
|
|
||||
|
def test_add |
||||
|
entry1 = { |
||||
|
objectclass: ['top', 'domain'], |
||||
|
o: ['TTSKY.NET'], |
||||
|
dc: ['localhost'], |
||||
|
} |
||||
|
conn.add(dn: "dc=localhost, dc=domain", attributes: entry1) |
||||
|
|
||||
|
assert_equal([:add, "dc=localhost, dc=domain", { |
||||
|
'objectclass'=>['top', 'domain'], |
||||
|
'o'=>['TTSKY.NET'], |
||||
|
'dc'=>['localhost'], |
||||
|
}], MockOperation.lastop) |
||||
|
|
||||
|
entry2 = { |
||||
|
objectclass: ['top', 'person'], |
||||
|
cn: ['Takaaki Tateishi'], |
||||
|
sn: ['ttate','Tateishi', "zero\000zero"], |
||||
|
} |
||||
|
conn.add(dn: "cn=Takaaki Tateishi, dc=localhost, dc=localdomain", attributes: entry2) |
||||
|
|
||||
|
assert_equal([:add, "cn=Takaaki Tateishi, dc=localhost, dc=localdomain", { |
||||
|
'objectclass'=>['top', 'person'], |
||||
|
'cn'=>['Takaaki Tateishi'], |
||||
|
'sn'=>['ttate','Tateishi',"zero\000zero"], |
||||
|
}], MockOperation.lastop) |
||||
|
end |
||||
|
|
||||
|
def test_del |
||||
|
conn.delete(dn: "cn=Takaaki-Tateishi, dc=localhost, dc=localdomain") |
||||
|
assert_equal([:del, "cn=Takaaki-Tateishi, dc=localhost, dc=localdomain"], MockOperation.lastop) |
||||
|
end |
||||
|
|
||||
|
def test_compare |
||||
|
pend("net-ldap gem doesn't support compare requests") |
||||
|
res = conn.compare("cn=Takaaki Tateishi, dc=localhost, dc=localdomain", |
||||
|
"cn", "Takaaki Tateishi") |
||||
|
|
||||
|
assert_equal([:compare, "cn=Takaaki Tateishi, dc=localhost, dc=localdomain", |
||||
|
"cn", "Takaaki Tateishi"], MockOperation.lastop) |
||||
|
assert res |
||||
|
|
||||
|
res = conn.compare("cn=Takaaki Tateishi, dc=localhost, dc=localdomain", |
||||
|
"cn", "false") |
||||
|
assert_equal([:compare, "cn=Takaaki Tateishi, dc=localhost, dc=localdomain", |
||||
|
"cn", "false"], MockOperation.lastop) |
||||
|
refute res |
||||
|
end |
||||
|
|
||||
|
def test_modrdn |
||||
|
conn.modify_rdn(olddn: "cn=Takaaki Tateishi, dc=localhost, dc=localdomain", |
||||
|
newrdn: "cn=Takaaki-Tateishi", |
||||
|
delete_attributes: true) |
||||
|
|
||||
|
assert_equal([:modifydn, "cn=Takaaki Tateishi, dc=localhost, dc=localdomain", |
||||
|
"cn=Takaaki-Tateishi", true, nil], MockOperation.lastop) |
||||
|
# FIXME: ruby-ldap doesn't support the four-argument form |
||||
|
end |
||||
|
|
||||
|
def test_modify |
||||
|
entry = [ |
||||
|
[:add, :objectclass, ['top', 'domain']], |
||||
|
[:delete, :o, []], |
||||
|
[:replace, :dc, ['localhost']], |
||||
|
] |
||||
|
conn.modify(dn: "dc=localhost, dc=domain", operations: entry) |
||||
|
|
||||
|
assert_equal([:modify, "dc=localhost, dc=domain", { |
||||
|
'objectclass' => [:add, 'top', 'domain'], |
||||
|
'o' => [:delete], |
||||
|
'dc' => [:replace, 'localhost'], |
||||
|
}], MockOperation.lastop) |
||||
|
end |
||||
|
|
||||
|
def test_search |
||||
|
res = [] |
||||
|
conn.search(base: "dc=localhost, dc=localdomain", |
||||
|
scope: Net::LDAP::SearchScope_WholeSubtree, |
||||
|
filter: "(objectclass=*)") do |e| |
||||
|
res << e.to_h |
||||
|
end |
||||
|
|
||||
|
assert_equal([:search, "dc=localhost, dc=localdomain", |
||||
|
LDAP::Server::WholeSubtree, |
||||
|
LDAP::Server::NeverDerefAliases, |
||||
|
[:true], []], MockOperation.lastop) |
||||
|
|
||||
|
exp = [ |
||||
|
{a: ["1","2"], b: ["boing"], dn: ["cn=foo"]}, |
||||
|
{a: ["3","4","5"], b: ["wibble"], dn: ["cn=bar"]}, |
||||
|
] |
||||
|
assert_equal exp, res |
||||
|
|
||||
|
res = [] |
||||
|
# FIXME: ruby-ldap doesn't seem to allow DEREF options to be set |
||||
|
conn.search(base: "dc=localhost, dc=localdomain", |
||||
|
scope: Net::LDAP::SearchScope_BaseObject, |
||||
|
filter: "(&(cn=foo)(objectclass=*)(|(!(sn=*))(ou>=baz)(o<=z)(cn=*and*er)))", |
||||
|
attributes: [:a, :b]) do |e| |
||||
|
res << e.to_h |
||||
|
end |
||||
|
|
||||
|
assert_equal([:search, "dc=localhost, dc=localdomain", |
||||
|
LDAP::Server::BaseObject, |
||||
|
LDAP::Server::NeverDerefAliases, |
||||
|
[:and, [:eq, "cn", nil, "foo"], |
||||
|
[:or, [:not, [:present, "sn"]], |
||||
|
[:ge, "ou", nil, "baz"], |
||||
|
[:le, "o", nil, "z"], |
||||
|
[:substrings, "cn", nil, nil, "and", "er"], |
||||
|
], |
||||
|
], ["a","b"]], MockOperation.lastop) |
||||
|
|
||||
|
assert_equal exp, res |
||||
|
end |
||||
|
|
||||
|
def test_search_with_range |
||||
|
res = [] |
||||
|
conn.search(base: "dc=localhost, dc=localdomain", |
||||
|
scope: Net::LDAP::SearchScope_BaseObject, |
||||
|
attributes: ["a;range=1-2", "b"]) do |e| |
||||
|
res << e.to_h |
||||
|
end |
||||
|
|
||||
|
assert_equal([:search, "dc=localhost, dc=localdomain", |
||||
|
LDAP::Server::BaseObject, |
||||
|
LDAP::Server::NeverDerefAliases, |
||||
|
[:true], ["a","b"]], MockOperation.lastop) |
||||
|
|
||||
|
exp = [ |
||||
|
{a: [], "a;range=1-*": ["2"], b: ["boing"], dn: ["cn=foo"]}, |
||||
|
{a: [], "a;range=1-2": ["4","5"], b: ["wibble"], dn: ["cn=bar"]}, |
||||
|
] |
||||
|
assert_equal exp, res |
||||
|
end |
||||
|
|
||||
|
def test_search_with_range_limit |
||||
|
start_server(attribute_range_limit: 2) |
||||
|
|
||||
|
res = [] |
||||
|
conn.search(base: "dc=localhost, dc=localdomain", |
||||
|
scope: Net::LDAP::SearchScope_WholeSubtree, |
||||
|
filter: "(objectclass=*)") do |e| |
||||
|
res << e.to_h |
||||
|
end |
||||
|
|
||||
|
assert_equal([:search, "dc=localhost, dc=localdomain", |
||||
|
LDAP::Server::WholeSubtree, |
||||
|
LDAP::Server::NeverDerefAliases, |
||||
|
[:true], []], MockOperation.lastop) |
||||
|
|
||||
|
exp = [ |
||||
|
{a: ["1","2"], b: ["boing"], dn: ["cn=foo"]}, |
||||
|
{a: [], "a;range=0-1": ["3","4"], b: ["wibble"], dn: ["cn=bar"]}, |
||||
|
] |
||||
|
assert_equal exp, res |
||||
|
end |
||||
|
end |
||||
@ -0,0 +1,107 @@ |
|||||
|
require File.dirname(__FILE__) + '/test_helper' |
||||
|
require 'ldap/server/filter' |
||||
|
|
||||
|
class FilterTest < Test::Unit::TestCase |
||||
|
|
||||
|
AV1 = { |
||||
|
"foo" => ["abc","def"], |
||||
|
"bar" => ["wibblespong"], |
||||
|
} |
||||
|
|
||||
|
def test_bad |
||||
|
assert_raises(LDAP::ResultError::OperationsError) { |
||||
|
LDAP::Server::Filter.run([:wibbly], AV1) |
||||
|
} |
||||
|
end |
||||
|
|
||||
|
def test_const |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:true], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:false], AV1)) |
||||
|
assert_equal(nil, LDAP::Server::Filter.run([:undef], AV1)) |
||||
|
end |
||||
|
|
||||
|
def test_present |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:present,"foo"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:present,"zog"], AV1)) |
||||
|
end |
||||
|
|
||||
|
def test_eq |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:eq,"foo",nil,"abc"], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:eq,"foo",nil,"def"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:eq,"foo",nil,"ghi"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:eq,"xyz",nil,"abc"], AV1)) |
||||
|
end |
||||
|
|
||||
|
def test_eq_case |
||||
|
c = LDAP::Server::MatchingRule.find('2.5.13.2') |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:eq,"foo",c,"ABC"], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:eq,"foo",c,"DeF"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:eq,"foo",c,"ghi"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:eq,"xyz",c,"abc"], AV1)) |
||||
|
end |
||||
|
|
||||
|
def test_not |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:not,[:eq,"foo",nil,"abc"]], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:not,[:eq,"foo",nil,"def"]], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:not,[:eq,"foo",nil,"ghi"]], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:not,[:eq,"xyz",nil,"abc"]], AV1)) |
||||
|
end |
||||
|
|
||||
|
def test_ge |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:ge,"foo",nil,"ccc"], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:ge,"foo",nil,"def"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:ge,"foo",nil,"deg"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:ge,"xyz",nil,"abc"], AV1)) |
||||
|
end |
||||
|
|
||||
|
def test_le |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:le,"foo",nil,"ccc"], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:le,"foo",nil,"abc"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:le,"foo",nil,"abb"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:le,"xyz",nil,"abc"], AV1)) |
||||
|
end |
||||
|
|
||||
|
def test_substrings |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:substrings,"foo",nil,"a",nil], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:substrings,"foo",nil,"def",nil], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:substrings,"foo",nil,"bc",nil], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:substrings,"foo",nil,"az",nil], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:substrings,"foo",nil,"",nil], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:substrings,"zzz",nil,"",nil], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:substrings,"foo",nil,nil,"a",nil], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:substrings,"foo",nil,nil,"e",nil], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:substrings,"foo",nil,nil,"ba",nil], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:substrings,"foo",nil,nil,"az",nil], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:substrings,"foo",nil,nil,"c"], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:substrings,"foo",nil,nil,"ef"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:substrings,"foo",nil,nil,"ab"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:substrings,"foo",nil,nil,"e"], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:substrings,"bar",nil,"wib","ong"], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:substrings,"bar",nil,"",""], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:substrings,"bar",nil,"wib","ble"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:substrings,"bar",nil,"sp","ong"], AV1)) |
||||
|
end |
||||
|
|
||||
|
def test_substr_case |
||||
|
c = LDAP::Server::MatchingRule.find('1.3.6.1.4.1.1466.109.114.3') |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:substrings,"bar",c,"WIB",nil], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:substrings,"bar",c,"WIB","lES","ong"], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:substrings,"bar",c,"SPONG",nil], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:substrings,"xyz",c,"wib",nil], AV1)) |
||||
|
end |
||||
|
|
||||
|
def test_and |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:and,[:true],[:true]], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:and,[:false],[:true]], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:and,[:true],[:false]], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:and,[:false],[:false]], AV1)) |
||||
|
end |
||||
|
|
||||
|
def test_or |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:or,[:true],[:true]], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:or,[:false],[:true]], AV1)) |
||||
|
assert_equal(true, LDAP::Server::Filter.run([:or,[:true],[:false]], AV1)) |
||||
|
assert_equal(false, LDAP::Server::Filter.run([:or,[:false],[:false]], AV1)) |
||||
|
end |
||||
|
|
||||
|
end |
||||
@ -0,0 +1,59 @@ |
|||||
|
require File.dirname(__FILE__) + '/test_helper' |
||||
|
require 'ldap/server/match' |
||||
|
|
||||
|
class MatchTest < Test::Unit::TestCase |
||||
|
|
||||
|
def test_caseIgnoreOrderingMatch |
||||
|
s = LDAP::Server::MatchingRule.find("2.5.13.3") |
||||
|
assert_equal(LDAP::Server::MatchingRule, s.class) |
||||
|
assert_equal("caseIgnoreOrderingMatch", s.name) |
||||
|
assert_equal("( 2.5.13.3 NAME 'caseIgnoreOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", s.to_def) |
||||
|
assert_equal(true, s.le(["foobar","wibble"], "ghi")) |
||||
|
assert_equal(true, s.le(["FOOBAR","WIBBLE"], "ghi")) |
||||
|
assert_equal(true, s.le(["foobar","wibble"], "GHI")) |
||||
|
assert_equal(true, s.le(["FOOBAR","WIBBLE"], "GHI")) |
||||
|
assert_equal(false, s.le(["foobar","wibble"], "fab")) |
||||
|
assert_equal(false, s.le(["FOOBAR","WIBBLE"], "fab")) |
||||
|
assert_equal(false, s.le(["foobar","wibble"], "FAB")) |
||||
|
assert_equal(false, s.le(["FOOBAR","WIBBLE"], "FAB")) |
||||
|
end |
||||
|
|
||||
|
def test_caseIgnoreSubstringsMatch |
||||
|
s = LDAP::Server::MatchingRule.find("2.5.13.4") |
||||
|
assert_equal(LDAP::Server::MatchingRule, s.class) |
||||
|
assert_equal("caseIgnoreSubstringsMatch", s.name) |
||||
|
assert_equal(true, s.substrings(["foobar","wibble"], nil, "oob", nil)) |
||||
|
assert_equal(true, s.substrings(["foobar","wibble"], nil, "foo", nil)) |
||||
|
assert_equal(true, s.substrings(["foobar","wibble"], nil, "bar", nil)) |
||||
|
assert_equal(true, s.substrings(["foobar","wibble"], "wib", nil)) |
||||
|
assert_equal(true, s.substrings(["foobar","wibble"], nil, "ar")) |
||||
|
assert_equal(true, s.substrings(["foobar","wibble"], "wib", "ble")) |
||||
|
assert_equal(true, s.substrings(["foobar","wibble"], nil, "oo", "bar")) |
||||
|
assert_equal(false, s.substrings(["foobar","wibble"], nil, "ooz", nil)) |
||||
|
assert_equal(false, s.substrings(["foobar","wibble"], nil, "foz", nil)) |
||||
|
assert_equal(false, s.substrings(["foobar","wibble"], nil, "zar", nil)) |
||||
|
assert_equal(false, s.substrings(["foobar","wibble"], "bar", nil)) |
||||
|
assert_equal(false, s.substrings(["foobar","wibble"], nil, "oob")) |
||||
|
assert_equal(false, s.substrings(["foobar","wibble"], "foo", "ble")) |
||||
|
assert_equal(false, s.substrings(["foobar","wibble"], "foo", "obar")) |
||||
|
end |
||||
|
|
||||
|
def test_unknown |
||||
|
s = LDAP::Server::MatchingRule.find("1.4.7.1") |
||||
|
assert_equal(nil, s) ## this may change to generate a default object |
||||
|
end |
||||
|
|
||||
|
def test_nil |
||||
|
s = LDAP::Server::MatchingRule.find(nil) |
||||
|
assert_equal(nil, s) |
||||
|
end |
||||
|
|
||||
|
def test_from_def |
||||
|
s = LDAP::Server::MatchingRule.from_def("( 1.2.3 NAME ( 'wibble' 'bibble' ) DESC 'foobar' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )") |
||||
|
assert_equal("1.2.3", s.oid) |
||||
|
assert_equal(['wibble','bibble'], s.names) |
||||
|
assert_equal('wibble', s.to_s) |
||||
|
assert_equal("foobar", s.desc) |
||||
|
assert_equal("IA5 String", s.syntax.desc) |
||||
|
end |
||||
|
end |
||||
@ -0,0 +1,113 @@ |
|||||
|
require File.dirname(__FILE__) + '/test_helper' |
||||
|
require 'ldap/server/schema' |
||||
|
require 'ldap/server/match' |
||||
|
|
||||
|
class SchemaTest < Test::Unit::TestCase |
||||
|
|
||||
|
def test_parse_attr |
||||
|
attr = <<ATTR |
||||
|
( 2.5.4.3 NAME 'cn' OBSOLETE EQUALITY 1.2.3 ORDERING 4.5.678 SUBSTR 9.1.1 SYNTAX 4.3.2{58} SINGLE-VALUE COLLECTIVE NO-USER-MODIFICATION USAGE userApplications ) |
||||
|
ATTR |
||||
|
a = LDAP::Server::Schema::AttributeType.new(attr) |
||||
|
assert_equal("2.5.4.3", a.oid) |
||||
|
assert_equal("cn", a.name) |
||||
|
assert_equal(["cn"], a.names) |
||||
|
assert(a.obsolete) |
||||
|
assert_equal("1.2.3", a.equality) |
||||
|
assert_equal("4.5.678", a.ordering) |
||||
|
assert_equal("9.1.1", a.substr) |
||||
|
assert_equal("4.3.2", a.syntax) |
||||
|
assert_equal(58, a.maxlen) |
||||
|
assert(a.singlevalue) |
||||
|
assert(a.collective) |
||||
|
assert(a.nousermod) |
||||
|
assert_equal(:userApplications, a.usage) |
||||
|
assert_equal(attr.chomp, a.to_def) |
||||
|
|
||||
|
attr = <<ATTR |
||||
|
( 2.5.4.3 NAME ( 'cn' 'commonName' ) DESC 'RFC2256: common name(s) for which the entity is known by' SUP name ) |
||||
|
ATTR |
||||
|
a = LDAP::Server::Schema::AttributeType.new(attr) |
||||
|
assert_equal("2.5.4.3", a.oid) |
||||
|
assert_equal("cn", a.name) |
||||
|
assert_equal(["cn", "commonName"], a.names) |
||||
|
assert_equal("RFC2256: common name(s) for which the entity is known by", a.desc) |
||||
|
assert(! a.obsolete) |
||||
|
assert_equal("name", a.sup) |
||||
|
assert(! a.singlevalue) |
||||
|
assert(! a.collective) |
||||
|
assert(! a.nousermod) |
||||
|
assert_equal(attr.chomp, a.to_def) |
||||
|
end |
||||
|
|
||||
|
def test_parse_objectclass |
||||
|
oc = <<OC |
||||
|
( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' DESC 'RFC1274: simple security object' SUP top AUXILIARY MUST userPassword ) |
||||
|
OC |
||||
|
a = LDAP::Server::Schema::ObjectClass.new(oc) |
||||
|
assert_equal("0.9.2342.19200300.100.4.19", a.oid) |
||||
|
assert_equal("simpleSecurityObject", a.name) |
||||
|
assert_equal(["simpleSecurityObject"], a.names) |
||||
|
assert(! a.obsolete) |
||||
|
assert_equal("RFC1274: simple security object", a.desc) |
||||
|
assert_equal(["top"], a.sup) |
||||
|
assert_equal(:auxiliary, a.struct) |
||||
|
assert_equal(["userPassword"], a.must) |
||||
|
assert_equal([], a.may) |
||||
|
assert_equal(oc.chomp, a.to_def) |
||||
|
|
||||
|
oc = <<OC |
||||
|
( 2.5.6.6 NAME 'person' DESC 'RFC2256: a person' SUP top STRUCTURAL MUST ( sn $ cn ) MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) ) |
||||
|
OC |
||||
|
a = LDAP::Server::Schema::ObjectClass.new(oc) |
||||
|
assert_equal("2.5.6.6", a.oid) |
||||
|
assert_equal("person", a.name) |
||||
|
assert_equal(["person"], a.names) |
||||
|
assert(! a.obsolete) |
||||
|
assert_equal("RFC2256: a person", a.desc) |
||||
|
assert_equal(["top"], a.sup) |
||||
|
assert_equal(:structural, a.struct) |
||||
|
assert_equal(["sn", "cn"], a.must) |
||||
|
assert_equal(["userPassword","telephoneNumber","seeAlso","description"], a.may) |
||||
|
assert_equal(oc.chomp, a.to_def) |
||||
|
end |
||||
|
|
||||
|
def test_loadschema |
||||
|
s = LDAP::Server::Schema.new |
||||
|
s.load_system |
||||
|
s.load_file(File.dirname(__FILE__) + "/core.schema") |
||||
|
s.resolve_oids |
||||
|
a = s.find_attrtype("objectclass") |
||||
|
assert_equal("objectClass", a.name) |
||||
|
a = s.find_attrtype("COMMONNAME") |
||||
|
assert_equal(LDAP::Server::Schema::AttributeType, a.class) |
||||
|
assert_equal("caseIgnoreMatch", a.equality.to_s) |
||||
|
assert_equal(LDAP::Server::MatchingRule, a.equality.class) |
||||
|
assert_equal("caseIgnoreSubstringsMatch", a.substr.to_s) |
||||
|
assert_equal(LDAP::Server::MatchingRule, a.substr.class) |
||||
|
assert_equal("1.3.6.1.4.1.1466.115.121.1.15", a.syntax.to_s) |
||||
|
assert_equal(LDAP::Server::Syntax, a.syntax.class) |
||||
|
assert_equal("cn", a.name) |
||||
|
a = s.find_attrtype("COUNTRYname") |
||||
|
assert_equal("c", a.name) |
||||
|
# I modified core.schema so that countryName has the appropriate syntax |
||||
|
assert(a.syntax.match("GB")) |
||||
|
assert(!a.syntax.match("ABC")) |
||||
|
end |
||||
|
|
||||
|
def test_backwards_api |
||||
|
s = LDAP::Server::Schema.new |
||||
|
s.load_system |
||||
|
assert_equal(['subschema','OpenLDAProotDSE','referral','alias','extensibleObject','top'].sort, |
||||
|
s.names('objectClasses').sort) |
||||
|
assert_equal(['dITStructureRules','nameForms','ditContentRules','objectClasses','attributeTypes','matchingRules','matchingRuleUse'], |
||||
|
s.attr('subschema', 'may')) |
||||
|
assert_equal([], s.attr('subschema', 'must')) |
||||
|
assert_equal(nil, s.attr('foo', 'must')) |
||||
|
assert_equal(['top'], s.sup('extensibleObject')) |
||||
|
end |
||||
|
|
||||
|
def test_validate |
||||
|
# FIXME |
||||
|
end |
||||
|
end |
||||
@ -0,0 +1,40 @@ |
|||||
|
require File.dirname(__FILE__) + '/test_helper' |
||||
|
require 'ldap/server/syntax' |
||||
|
|
||||
|
class SyntaxTest < Test::Unit::TestCase |
||||
|
|
||||
|
def test_integer |
||||
|
s = LDAP::Server::Syntax.find("1.3.6.1.4.1.1466.115.121.1.27") |
||||
|
assert_equal(LDAP::Server::Syntax, s.class) |
||||
|
assert_equal("Integer", s.desc) |
||||
|
assert_equal("( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'Integer' )", s.to_def) |
||||
|
assert(!s.nhr) |
||||
|
assert(s.match("123")) |
||||
|
assert(!s.match("12A")) |
||||
|
assert_equal(123, s.value_from_s("123")) |
||||
|
assert_equal("456", s.value_to_s(456)) |
||||
|
assert_equal("789", s.value_to_s("789")) |
||||
|
end |
||||
|
|
||||
|
def test_unknown |
||||
|
s = LDAP::Server::Syntax.find("1.4.7.1") |
||||
|
assert_equal(LDAP::Server::Syntax, s.class) |
||||
|
assert_equal("1.4.7.1", s.oid) |
||||
|
assert_equal("1.4.7.1", s.to_s) |
||||
|
assert_equal("( 1.4.7.1 )", s.to_def) |
||||
|
assert_equal("false", s.value_to_s(false)) # generic value_to_s |
||||
|
assert_equal("true", s.value_from_s("true")) # generic value_from_s |
||||
|
assert(s.match("123")) # match anything |
||||
|
end |
||||
|
|
||||
|
def test_nil |
||||
|
s = LDAP::Server::Syntax.find(nil) |
||||
|
assert_equal(nil, s) |
||||
|
end |
||||
|
|
||||
|
def test_from_def |
||||
|
s = LDAP::Server::Syntax.from_def("( 1.2.3 DESC 'foobar' )") |
||||
|
assert_equal("1.2.3", s.oid) |
||||
|
assert_equal("foobar", s.desc) |
||||
|
end |
||||
|
end |
||||
@ -0,0 +1,2 @@ |
|||||
|
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) |
||||
|
require 'test/unit' |
||||
@ -0,0 +1,60 @@ |
|||||
|
require File.dirname(__FILE__) + '/test_helper' |
||||
|
require 'ldap/server/trie' |
||||
|
|
||||
|
class TestTrie < Test::Unit::TestCase |
||||
|
|
||||
|
def test_trie_base |
||||
|
trie = LDAP::Server::Trie.new do |trie| |
||||
|
trie.insert 'ou=Users,dc=mydomain,dc=com,op=bind', 'UsersBindTree' |
||||
|
trie.insert 'ou=Users,dc=mydomain,dc=com,op=search', 'UsersSearchTree' |
||||
|
trie.insert 'ou=Groups,dc=mydomain,dc=com,op=search', 'GroupsSearchTree' |
||||
|
trie.insert 'dc=mydomain,dc=com,op=search', 'RootSearchValue' |
||||
|
trie.insert 'dc=com,op=search', '' |
||||
|
end |
||||
|
|
||||
|
assert_equal trie.lookup('ou=Users,dc=mydomain,dc=com,op=bind'), 'UsersBindTree' |
||||
|
assert_equal trie.lookup('ou=Users,dc=mydomain,dc=com,op=search'), 'UsersSearchTree' |
||||
|
assert_equal trie.lookup('ou=Groups,dc=mydomain,dc=com,op=search'), 'GroupsSearchTree' |
||||
|
assert_equal trie.lookup('dc=mydomain,dc=com,op=search'), 'RootSearchValue' |
||||
|
assert_equal trie.lookup('dc=com,op=search'), '' |
||||
|
assert_nil trie.lookup 'dc=mydomain,dc=com,op=bind' |
||||
|
assert_nil trie.lookup nil |
||||
|
|
||||
|
assert_raises LDAP::Server::Trie::NodeNotFoundError do |
||||
|
trie.lookup 'ou=DoesNotExist,dc=mydomain,dc=com,op=search' |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def test_trie_wildcard |
||||
|
trie = LDAP::Server::Trie.new do |trie| |
||||
|
trie.insert 'uid=:uid,ou=Users,dc=mydomain,dc=com', 'SpecificUsers' |
||||
|
trie.insert 'ou=Users,dc=mydomain,dc=com', 'Users' |
||||
|
trie.insert 'ou=Users,dc=:domain,dc=:tld', 'Domain' |
||||
|
end |
||||
|
|
||||
|
assert_equal trie.lookup('uid=john,ou=Users,dc=mydomain,dc=com'), 'SpecificUsers' |
||||
|
assert_equal trie.lookup('uid=jane,ou=Users,dc=mydomain,dc=com'), 'SpecificUsers' |
||||
|
assert_equal trie.lookup('ou=Users,dc=mydomain,dc=com'), 'Users' |
||||
|
assert_equal trie.lookup('ou=Users,dc=otherdomain,dc=net'), 'Domain' |
||||
|
end |
||||
|
|
||||
|
def test_trie_match |
||||
|
trie = LDAP::Server::Trie.new do |trie| |
||||
|
trie.insert 'uid=:uid,ou=Users,dc=mydomain,dc=com', 'SpecificUsers' |
||||
|
trie.insert 'ou=Users,dc=mydomain,dc=com', 'Users' |
||||
|
trie.insert 'dc=mydomain,dc=com', 'Domains' |
||||
|
end |
||||
|
|
||||
|
assert_equal trie.match('uid=john,ou=Users,dc=mydomain,dc=com'), |
||||
|
['uid=:uid,ou=Users,dc=mydomain,dc=com', 'SpecificUsers'] |
||||
|
assert_equal trie.match('cn=john,ou=Users,dc=mydomain,dc=com'), |
||||
|
['ou=Users,dc=mydomain,dc=com', 'Users'] |
||||
|
assert_equal trie.match('ou=Users,dc=mydomain,dc=com'), |
||||
|
['ou=Users,dc=mydomain,dc=com', 'Users'] |
||||
|
assert_equal trie.match('dc=mydomain,dc=com'), |
||||
|
['dc=mydomain,dc=com', 'Domains'] |
||||
|
assert_equal trie.match('dc=otherdomain,dc=com'), |
||||
|
[nil, nil] |
||||
|
end |
||||
|
|
||||
|
end |
||||
@ -0,0 +1,51 @@ |
|||||
|
require File.dirname(__FILE__) + '/test_helper' |
||||
|
require 'ldap/server/util' |
||||
|
|
||||
|
class TestLdapUtil < Test::Unit::TestCase |
||||
|
def test_split_dn |
||||
|
# examples from RFC 2253 |
||||
|
assert_equal( |
||||
|
[{"cn"=>"Steve Kille"},{"o"=>"Isode Limited"},{"c"=>"GB"}], |
||||
|
LDAP::Server::Operation.split_dn("CN=Steve Kille , O=Isode Limited;C=GB") |
||||
|
) |
||||
|
assert_equal( |
||||
|
[{"ou"=>"Sales","cn"=>"J. Smith"},{"o"=>"Widget Inc."},{"c"=>"US"}], |
||||
|
LDAP::Server::Operation.split_dn("OU=Sales+CN=J. Smith,O=Widget Inc.,C=US") |
||||
|
) |
||||
|
assert_equal( |
||||
|
[{"cn"=>"L. Eagle"},{"o"=>"Sue, Grabbit and Runn"},{"c"=>"GB"}], |
||||
|
LDAP::Server::Operation.split_dn("CN=L. Eagle,O=Sue\\, Grabbit and Runn,C=GB") |
||||
|
) |
||||
|
assert_equal( |
||||
|
[{"cn"=>"Before\rAfter"},{"o"=>"Test"},{"c"=>"GB"}], |
||||
|
LDAP::Server::Operation.split_dn("CN=Before\\0DAfter,O=Test,C=GB") |
||||
|
) |
||||
|
res = LDAP::Server::Operation.split_dn("SN=Lu\\C4\\8Di\\C4\\87") |
||||
|
assert_equal([{ "sn" => "Lu\xc4\x8di\xc4\x87".force_encoding('ascii-8bit') }], res) |
||||
|
|
||||
|
# Just for fun, let's try parsing it as UTF8 |
||||
|
chars = res[0]["sn"].force_encoding('utf-8').scan(/./u) |
||||
|
assert_equal(["L", "u", "\u010d", "i", "\u0107"], chars) |
||||
|
end |
||||
|
|
||||
|
def test_join_dn |
||||
|
# examples from RFC 2253 |
||||
|
assert_equal( |
||||
|
"cn=Steve Kille,o=Isode Limited,c=GB", |
||||
|
LDAP::Server::Operation.join_dn([{"cn"=>"Steve Kille"},{"o"=>"Isode Limited"},{"c"=>"GB"}]) |
||||
|
) |
||||
|
# These are equivalent |
||||
|
d1 = "ou=Sales+cn=J. Smith,o=Widget Inc.,c=US" |
||||
|
d2 = "cn=J. Smith+ou=Sales,o=Widget Inc.,c=US" |
||||
|
assert_equal(d1, |
||||
|
LDAP::Server::Operation.join_dn([[["ou","Sales"],["cn","J. Smith"]],[["o","Widget Inc."]],["c","US"]]) |
||||
|
) |
||||
|
r = LDAP::Server::Operation.join_dn([{"ou"=>"Sales","cn"=>"J. Smith"},{"o"=>"Widget Inc."},{"c"=>"US"}]) |
||||
|
assert(r == d1 || r == d2, "got #{r.inspect}, expected #{d1.inspect} or #{d2.inspect}") |
||||
|
assert_equal( |
||||
|
"cn=L. Eagle,o=Sue\\, Grabbit and Runn,c=GB", |
||||
|
LDAP::Server::Operation.join_dn([{"cn"=>"L. Eagle"},{"o"=>"Sue, Grabbit and Runn"},{"c"=>"GB"}]) |
||||
|
) |
||||
|
end |
||||
|
end |
||||
|
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue