ldumbd 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/LICENSE +19 -0
- data/NEWS +8 -0
- data/README.md +75 -0
- data/Rakefile +25 -0
- data/TODO.org +4 -0
- data/bin/ldumbd +43 -0
- data/config.yml.sample +11 -0
- data/db/migrations/001_create_users.rb +12 -0
- data/db/migrations/002_create_groups.rb +9 -0
- data/db/migrations/003_add_user_group_id.rb +20 -0
- data/db/migrations/004_create_groups_users.rb +10 -0
- data/db/migrations/005_set_uid_and_gid_start_value.rb +59 -0
- data/ldumbd.gemspec +42 -0
- data/lib/ldumbd/filter_converter.rb +190 -0
- data/lib/ldumbd/group.rb +3 -0
- data/lib/ldumbd/ldap_tree.rb +44 -0
- data/lib/ldumbd/operation.rb +61 -0
- data/lib/ldumbd/preforkserver.rb +64 -0
- data/lib/ldumbd/table_map.rb +89 -0
- data/lib/ldumbd/tree_object.rb +67 -0
- data/lib/ldumbd/user.rb +3 -0
- data/lib/ldumbd/version.rb +3 -0
- data/spec/filter_converter_spec.rb +154 -0
- data/spec/operation_spec.rb +274 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/table_map_spec.rb +45 -0
- metadata +209 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2014 Sebastian Boehm
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/NEWS
ADDED
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
ldumbd
|
2
|
+
======
|
3
|
+
|
4
|
+
A simple, self-contained LDAP server with a database back end.
|
5
|
+
|
6
|
+
Documentation
|
7
|
+
-------------
|
8
|
+
|
9
|
+
Ldumbd is a simple, self-contained read-only LDAP server that uses PostgreSQL, MySQL/MariaDB or SQLite as a back end.
|
10
|
+
|
11
|
+
Ldumbd is designed primarily to act as an LDAP gateway to a simple SQL user database for use with the `nss-pam-ldapd` Name Service Switch (NSS) module.
|
12
|
+
|
13
|
+
Limitations
|
14
|
+
-----------
|
15
|
+
|
16
|
+
At the moment, ldumbd has no support for any of the following:
|
17
|
+
|
18
|
+
- LDAP schemas
|
19
|
+
- LDAP binds
|
20
|
+
- any request type other than search requests
|
21
|
+
- "approximately equal" operators in search filters
|
22
|
+
|
23
|
+
Installation: Debian Wheezy
|
24
|
+
---------------------------
|
25
|
+
|
26
|
+
sudo -i
|
27
|
+
export LDUMBD_DIR=/var/lib/ldumbd
|
28
|
+
mkdir -p ${LDUMBD_DIR}
|
29
|
+
groupadd -r ldumbd
|
30
|
+
useradd -r -s /bin/false -g ldumbd -d ${LDUMBD_DIR} ldumbd
|
31
|
+
chown ldumbd:ldumbd ${LDUMBD_DIR}
|
32
|
+
chmod 700 ${LDUMBD_DIR}
|
33
|
+
gem install ldumbd
|
34
|
+
export MIGRATIONS=$(dirname $(gem contents ldumbd | grep migrations/001))
|
35
|
+
|
36
|
+
Database setup: SQLite
|
37
|
+
----------------------
|
38
|
+
|
39
|
+
aptitude install libsqlite3-dev
|
40
|
+
gem install sqlite3
|
41
|
+
sudo -u ldumbd sequel -m ${MIGRATIONS} sqlite://${LDUMBD_DIR}/ldumbd.sqlite3
|
42
|
+
|
43
|
+
Database setup: PostgreSQL
|
44
|
+
--------------------------
|
45
|
+
|
46
|
+
aptitude install postgresql libpq-dev
|
47
|
+
sudo -u postgres createuser ldumbd
|
48
|
+
sudo -u postgres createdb -O ldumbd ldumbd
|
49
|
+
gem install pg
|
50
|
+
sudo -u ldumbd sequel -m ${MIGRATIONS} postgres:///ldumbd
|
51
|
+
|
52
|
+
Database setup: MySQL/MariaDB
|
53
|
+
-----------------------------
|
54
|
+
|
55
|
+
export DB_PASSWORD='secret'
|
56
|
+
aptitude install mysql-server libmysqlclient-dev
|
57
|
+
cat <<EOS | mysql -u root -p
|
58
|
+
CREATE DATABASE ldumbd;
|
59
|
+
CREATE USER 'ldumbd'@'localhost' IDENTIFIED BY 'secret';
|
60
|
+
GRANT ALL PRIVILEGES ON ldumbd.* TO 'ldumbd'@'localhost';
|
61
|
+
EOS
|
62
|
+
gem install mysql2
|
63
|
+
sequel -m ${MIGRATIONS} "mysql2://ldumbd:${DB_PASSWORD}@localhost/ldumbd"
|
64
|
+
|
65
|
+
Running ldumbd
|
66
|
+
--------------
|
67
|
+
|
68
|
+
cp ${LDUMBD_DIR}/config.yml.sample /etc/ldumbd.yml
|
69
|
+
$EDITOR /etc/ldumbd.yml
|
70
|
+
ldumbd /etc/ldumbd.yml
|
71
|
+
|
72
|
+
Copyright
|
73
|
+
---------
|
74
|
+
|
75
|
+
Copyright (c) 2014 Sebastian Boehm. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
MIGRATIONS = 'db/migrations'
|
4
|
+
DATABASE_URL = ENV['DATABASE_URL'] || 'sqlite://ldumbd-test.sqlite3'
|
5
|
+
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
t.pattern = 'spec/**/*_spec.rb'
|
8
|
+
end
|
9
|
+
|
10
|
+
namespace :db do
|
11
|
+
desc 'Run database migrations'
|
12
|
+
task :migrate do
|
13
|
+
sh "bundle exec sequel -m #{MIGRATIONS} #{DATABASE_URL}"
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Drop all tables'
|
17
|
+
task :nuke do
|
18
|
+
nuke_all_tables = 'DB.tables.each {|t| DB.drop_table?(t) }'
|
19
|
+
sh "bundle exec sequel -m #{MIGRATIONS} -M 0 #{DATABASE_URL}"
|
20
|
+
sh "bundle exec sequel -c '#{nuke_all_tables}' #{DATABASE_URL}"
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Reset database'
|
24
|
+
task :reset => [:nuke, :migrate]
|
25
|
+
end
|
data/TODO.org
ADDED
data/bin/ldumbd
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'sequel'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
require 'ldumbd/operation'
|
7
|
+
require 'ldumbd/ldap_tree'
|
8
|
+
require 'ldumbd/preforkserver'
|
9
|
+
|
10
|
+
if ARGV.size != 1
|
11
|
+
puts 'usage: ldumbd LDUMBD_CONFIG'
|
12
|
+
exit false
|
13
|
+
end
|
14
|
+
|
15
|
+
begin
|
16
|
+
debug = ENV['DEBUG'] == '1'
|
17
|
+
config = YAML.load(File.read(ARGV[0]))
|
18
|
+
DB = Sequel.connect(config['database'])
|
19
|
+
require 'ldumbd/user'
|
20
|
+
require 'ldumbd/group'
|
21
|
+
DB.disconnect
|
22
|
+
|
23
|
+
ldap_tree = Ldumbd::LdapTree.new(config['basedn'])
|
24
|
+
|
25
|
+
puts "ldumbd #{Ldumbd::VERSION} starting..."
|
26
|
+
server = LDAP::Server.new(bindaddr: config['bind_address'],
|
27
|
+
port: config['port'],
|
28
|
+
nodelay: true,
|
29
|
+
listen: 10,
|
30
|
+
num_processes: 10,
|
31
|
+
operation_class: Ldumbd::Operation,
|
32
|
+
operation_args: ldap_tree,
|
33
|
+
user: config['user'],
|
34
|
+
group: config['group'],
|
35
|
+
debug: debug)
|
36
|
+
server.run_preforkserver
|
37
|
+
trap('INT') { puts 'caught SIGINT, shutting down'; exit }
|
38
|
+
puts "prefork ok, resuming normal operation"
|
39
|
+
server.join
|
40
|
+
rescue => e
|
41
|
+
$stderr.puts e.message
|
42
|
+
$stderr.puts e.backtrace if debug
|
43
|
+
end
|
data/config.yml.sample
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#bind_address: 0.0.0.0
|
2
|
+
#port: 1389
|
3
|
+
#user: ldumbd
|
4
|
+
#group: ldumbd
|
5
|
+
basedn: dc=example,dc=org
|
6
|
+
|
7
|
+
# database: postgres:///ldumbd
|
8
|
+
|
9
|
+
# database: postgres://user:password@host/database
|
10
|
+
# database: mysql2://user:password@host:port/database
|
11
|
+
# database: sqlite:///path/to/database.sqlite3
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_table(:users) do
|
4
|
+
primary_key :id
|
5
|
+
String :name, size: 32, null: false, unique: true
|
6
|
+
String :realname, size: 64, null: false, default: ''
|
7
|
+
String :shell, size: 32, null: false, default: '/bin/false'
|
8
|
+
String :homedir, size: 128, null: false
|
9
|
+
index :name
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
nogroup_gid = 65534
|
3
|
+
|
4
|
+
up do
|
5
|
+
self[:groups].insert(id: nogroup_gid, name: 'nogroup')
|
6
|
+
alter_table(:users) do
|
7
|
+
add_foreign_key :group_id, :groups, null: false, default: nogroup_gid
|
8
|
+
# MySQL automatically adds indexes for foreign key columns
|
9
|
+
add_index :group_id unless DB.database_type == :mysql
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
down do
|
14
|
+
alter_table(:users) do
|
15
|
+
drop_index :group_id unless DB.database_type == :mysql
|
16
|
+
drop_foreign_key :group_id
|
17
|
+
end
|
18
|
+
self[:groups].where(id: nogroup_gid).delete
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
def set_min_max_value_commands(dbtype, min, max)
|
2
|
+
case dbtype
|
3
|
+
when :postgres
|
4
|
+
options = "start #{min} restart #{min} minvalue #{min} maxvalue #{max}"
|
5
|
+
["alter sequence users_id_seq #{options};",
|
6
|
+
"alter sequence groups_id_seq #{options};"]
|
7
|
+
when :sqlite
|
8
|
+
id = min-1
|
9
|
+
name = '#dummyname#'
|
10
|
+
dir = '#dummydir#'
|
11
|
+
["insert into users(id, name, homedir) values(#{id}, '#{name}', '#{dir}');",
|
12
|
+
"delete from users where id = #{id};",
|
13
|
+
"insert into groups(id, name) values(#{id}, '#{name}');",
|
14
|
+
"delete from groups where id = #{id};"]
|
15
|
+
when :mysql
|
16
|
+
["alter table users auto_increment = #{min};",
|
17
|
+
"alter table groups auto_increment = #{min};"]
|
18
|
+
else
|
19
|
+
raise Sequel::Error, "database type '#{dbtype}' not supported"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def reset_min_max_value_commands(dbtype, next_uid, next_gid)
|
24
|
+
case dbtype
|
25
|
+
when :postgres
|
26
|
+
["alter sequence users_id_seq start #{next_uid} restart #{next_uid} \
|
27
|
+
no minvalue no maxvalue;",
|
28
|
+
"alter sequence groups_id_seq start #{next_gid} restart #{next_gid} \
|
29
|
+
no minvalue no maxvalue;"]
|
30
|
+
when :sqlite
|
31
|
+
["delete from sqlite_sequence where name = 'users';",
|
32
|
+
"delete from sqlite_sequence where name = 'groups';"]
|
33
|
+
when :mysql
|
34
|
+
["alter table users auto_increment = #{next_uid};",
|
35
|
+
"alter table groups auto_increment = #{next_gid};"]
|
36
|
+
else
|
37
|
+
raise Sequel::Error, "database type '#{dbtype}' not supported"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
Sequel.migration do
|
42
|
+
dbtype = DB.database_type
|
43
|
+
min_id = 100_000
|
44
|
+
max_id = 200_000
|
45
|
+
|
46
|
+
up do
|
47
|
+
set_min_max_value_commands(dbtype, min_id, max_id).each do |command|
|
48
|
+
run command
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
down do
|
53
|
+
next_uid = self[:users].max(:id).to_i + 1
|
54
|
+
next_gid = self[:groups].max(:id).to_i + 1
|
55
|
+
reset_min_max_value_commands(dbtype, next_uid, next_gid).each do |command|
|
56
|
+
run command
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/ldumbd.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'ldumbd/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'ldumbd'
|
8
|
+
spec.version = Ldumbd::VERSION
|
9
|
+
spec.author = 'Sebastian Boehm'
|
10
|
+
spec.email = 'sebastian@sometimesfood.org'
|
11
|
+
|
12
|
+
spec.summary = %q{A simple, self-contained LDAP server}
|
13
|
+
spec.homepage = 'https://github.com/sometimesfood/ldumbd'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.description = <<EOS
|
16
|
+
Ldumbd is a simple, self-contained read-only LDAP server that uses
|
17
|
+
PostgreSQL, MySQL/MariaDB or SQLite as a back end.
|
18
|
+
EOS
|
19
|
+
|
20
|
+
spec.files = `git ls-files -z`.split("\0") &
|
21
|
+
Dir['config.yml.sample',
|
22
|
+
'Gemfile',
|
23
|
+
'ldumbd.gemspec',
|
24
|
+
'LICENSE',
|
25
|
+
'NEWS',
|
26
|
+
'Rakefile',
|
27
|
+
'README.md',
|
28
|
+
'TODO.org',
|
29
|
+
'{bin,lib,db,spec}/**/*']
|
30
|
+
spec.executables = ['ldumbd']
|
31
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
32
|
+
|
33
|
+
spec.add_dependency 'ruby-ldapserver', '~> 0.5.0'
|
34
|
+
spec.add_dependency 'sequel', '~> 4.7.0'
|
35
|
+
|
36
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3.9'
|
37
|
+
spec.add_development_dependency 'pg', '~> 0.17.1'
|
38
|
+
spec.add_development_dependency 'mysql2', '~> 0.3.15'
|
39
|
+
spec.add_development_dependency 'rake', '~> 10.0.4'
|
40
|
+
spec.add_development_dependency 'net-ldap', '~> 0.3.1'
|
41
|
+
spec.add_development_dependency 'minitest', '~> 5.3.0'
|
42
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'ldumbd/table_map'
|
2
|
+
|
3
|
+
module Ldumbd
|
4
|
+
class FilterConverter
|
5
|
+
# Public: Converts a parsed LDAP filter to a Sequel dataset filter.
|
6
|
+
#
|
7
|
+
# base_model: The dataset's base model.
|
8
|
+
# filter: A parsed LDAP filter.
|
9
|
+
#
|
10
|
+
# Examples
|
11
|
+
#
|
12
|
+
# base_model = User
|
13
|
+
# filter = [:and,
|
14
|
+
# [:eq, 'uid', nil, 'john'],
|
15
|
+
# [:ge, 'uidNumber', nil, '1000']]
|
16
|
+
# base_model.where(Ldumbd::FilterConverter.filter_to_sequel(base_model,
|
17
|
+
# filter)).sql
|
18
|
+
# # => "SELECT * FROM \"users\" WHERE ((\"users\".\"name\" = 'john') AND (\"users\".\"id\" >= '1000'))"
|
19
|
+
#
|
20
|
+
# Returns a Sequel dataset filter.
|
21
|
+
def self.filter_to_sequel(base_model, filter)
|
22
|
+
filter = filter.dup
|
23
|
+
op = filter.shift
|
24
|
+
case op
|
25
|
+
when :and, :or, :not
|
26
|
+
nested_filter_to_sequel(base_model, filter, op)
|
27
|
+
when :true, :false, :eq, :ge, :le, :present, :substrings
|
28
|
+
simple_filter_to_sequel(base_model, filter, op)
|
29
|
+
else
|
30
|
+
raise 'not implemented yet'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
# Internal: Transforms a nested filter expression into a
|
36
|
+
# Sequel::SQL::BooleanExpression
|
37
|
+
#
|
38
|
+
# base_model: The dataset's base model.
|
39
|
+
# filters: An array of parsed LDAP filters.
|
40
|
+
# op: The type of nested filter, i.e. :or, :and or :not.
|
41
|
+
#
|
42
|
+
# Examples
|
43
|
+
#
|
44
|
+
# base_model = User
|
45
|
+
# filters = [[:eq, 'uid', nil, 'john'],
|
46
|
+
# [:ge, 'uidNumber', nil, '1000']]
|
47
|
+
# op = :and
|
48
|
+
# query = Ldumbd::FilterConverter.nested_filter_to_sequel(base_model,
|
49
|
+
# filters,
|
50
|
+
# op)
|
51
|
+
# base_model.where(query).sql
|
52
|
+
# # => "SELECT * FROM \"users\" WHERE ((\"users\".\"name\" = 'john') AND (\"users\".\"id\" >= '1000'))"
|
53
|
+
def self.nested_filter_to_sequel(base_model, filters, op)
|
54
|
+
case op
|
55
|
+
when :and
|
56
|
+
Sequel.&(*filters.map { |f| filter_to_sequel(base_model, f) })
|
57
|
+
when :or
|
58
|
+
Sequel.|(*filters.map { |f| filter_to_sequel(base_model, f) })
|
59
|
+
when :not
|
60
|
+
raise ArgumentError, 'Too many :not arguments' unless filters.size == 1
|
61
|
+
subfilter = filters[0]
|
62
|
+
# ... id NOT IN (SELECT id FROM ...)
|
63
|
+
Sequel.~(id: base_model.where(filter_to_sequel(base_model,
|
64
|
+
subfilter)).select(:id))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Internal: Transforms a non-nested filter expression into a sequel
|
69
|
+
# dataset filter.
|
70
|
+
#
|
71
|
+
# base_model: The dataset's base model.
|
72
|
+
# filter: A parsed LDAP filter without a filter operator.
|
73
|
+
# op: The filter type, e.g. :eq, :le, :ge, ...
|
74
|
+
#
|
75
|
+
# Examples
|
76
|
+
#
|
77
|
+
# base_model = User
|
78
|
+
# filter = ['uid', nil, 'john']
|
79
|
+
# op = :eq
|
80
|
+
# query = Ldumbd::FilterConverter.simple_filter_to_sequel(base_model,
|
81
|
+
# filter,
|
82
|
+
# op)
|
83
|
+
# base_model.where(query).sql
|
84
|
+
# # => "SELECT * FROM \"users\" WHERE (\"users\".\"name\" = 'john')"
|
85
|
+
def self.simple_filter_to_sequel(base_model, filter, op)
|
86
|
+
key, value = extract_key_value(base_model, filter, op)
|
87
|
+
return { key => value } if key == :users
|
88
|
+
return oc_match?(base_model, op, value) if key == :object_class
|
89
|
+
|
90
|
+
case op
|
91
|
+
when :true
|
92
|
+
true
|
93
|
+
when :false
|
94
|
+
false
|
95
|
+
when :eq
|
96
|
+
# TODO: no case insensitive search support yet
|
97
|
+
# User.where(Sequel.function(:lower, :name) => v.downcase)
|
98
|
+
{ key => value }
|
99
|
+
when :le
|
100
|
+
Sequel.expr(key) <= Sequel.expr(value)
|
101
|
+
when :ge
|
102
|
+
Sequel.expr(key) >= Sequel.expr(value)
|
103
|
+
when :present
|
104
|
+
!key.nil?
|
105
|
+
when :substrings
|
106
|
+
Sequel.like(key, value)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Internal: Extracts keys and values or Sequel filters for use in
|
111
|
+
# simple filters.
|
112
|
+
#
|
113
|
+
# model: The dataset's model.
|
114
|
+
# filter: A parsed LDAP filter without a filter operator.
|
115
|
+
# op: The filter type, e.g. :eq, :le, :ge, ...
|
116
|
+
#
|
117
|
+
# Examples
|
118
|
+
#
|
119
|
+
# model = User
|
120
|
+
# filter = ['uid', nil, 'john']
|
121
|
+
# op = :eq
|
122
|
+
# Ldumbd::FilterConverter.extract_key_value(model, filter, op)
|
123
|
+
# # => [:users__name, "john"]
|
124
|
+
def self.extract_key_value(model, filter, op)
|
125
|
+
ldap_value = filter[2..-1] || []
|
126
|
+
key = extract_key(model, filter)
|
127
|
+
|
128
|
+
value = if key == :users
|
129
|
+
User.where(filter_to_sequel(User, [op, 'uid', nil, ldap_value]))
|
130
|
+
elsif op == :substrings
|
131
|
+
ldap_value.join('%')
|
132
|
+
else
|
133
|
+
ldap_value[0]
|
134
|
+
end
|
135
|
+
|
136
|
+
return key, value
|
137
|
+
end
|
138
|
+
|
139
|
+
# Internal: Extracts LDAP keys from parsed LDAP filters and
|
140
|
+
# converts them to SQL attributes.
|
141
|
+
#
|
142
|
+
# model: The dataset model.
|
143
|
+
# filter: A parsed LDAP filter without a filter operator.
|
144
|
+
#
|
145
|
+
# Examples
|
146
|
+
#
|
147
|
+
# model = User
|
148
|
+
# filter = ['uid', nil, 'john']
|
149
|
+
# Ldumbd::FilterConverter.extract_key(model, filter)
|
150
|
+
# # => :users__name
|
151
|
+
def self.extract_key(model, filter)
|
152
|
+
ldap_key = filter[0]
|
153
|
+
if ldap_key == 'objectClass'
|
154
|
+
# object classes are not saved in the database
|
155
|
+
:object_class
|
156
|
+
else
|
157
|
+
TableMap.db_key(model, ldap_key)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Internal: Checks whether a model matches a target object class.
|
162
|
+
#
|
163
|
+
# model: The model.
|
164
|
+
# op: An LDAP filter operator, i.e. :eq, :le, :ge or :substrings.
|
165
|
+
# target_oc: The target object class.
|
166
|
+
#
|
167
|
+
# Examples
|
168
|
+
#
|
169
|
+
# Ldumbd::FilterConverter.oc_match?(User, :ge, 'posixA')
|
170
|
+
# # => true
|
171
|
+
#
|
172
|
+
# Ldumbd::FilterConverter.oc_match?(Group, :eq, 'nonexistent')
|
173
|
+
# # => false
|
174
|
+
def self.oc_match?(model, op, target_oc)
|
175
|
+
object_classes = TableMap.object_classes(model)
|
176
|
+
|
177
|
+
case op
|
178
|
+
when :eq
|
179
|
+
object_classes.any? { |oc| oc == target_oc }
|
180
|
+
when :le
|
181
|
+
object_classes.any? { |oc| oc <= target_oc }
|
182
|
+
when :ge
|
183
|
+
object_classes.any? { |oc| oc >= target_oc }
|
184
|
+
when :substrings
|
185
|
+
target_oc_re = /\A#{Regexp.escape(target_oc).gsub('%', '.*')}\Z/
|
186
|
+
object_classes.any? { |oc| oc =~ target_oc_re }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
data/lib/ldumbd/group.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'ldumbd/tree_object'
|
2
|
+
|
3
|
+
module Ldumbd
|
4
|
+
class LdapTree
|
5
|
+
def find_by_dn(dn)
|
6
|
+
object = @tree[dn]
|
7
|
+
unless object
|
8
|
+
object = TreeObject.by_dn(@basedn, dn)
|
9
|
+
end
|
10
|
+
object or raise LDAP::ResultError::NoSuchObject
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(basedn)
|
14
|
+
@basedn = basedn
|
15
|
+
/\Adc=(?<dc>[^,]*)/ =~ basedn
|
16
|
+
root = TreeObject.new({ 'dn_prefix' => '',
|
17
|
+
'dc' => dc,
|
18
|
+
'objectClass' => ['top', 'domain']})
|
19
|
+
people = TreeObject.new({ 'dn_prefix' => 'ou=People',
|
20
|
+
'ou' => 'People',
|
21
|
+
'objectClass' => ['top',
|
22
|
+
'organizationalUnit'] })
|
23
|
+
groups = TreeObject.new({ 'dn_prefix' => 'ou=Groups',
|
24
|
+
'ou' => 'Groups',
|
25
|
+
'objectClass' => ['top',
|
26
|
+
'organizationalUnit'] })
|
27
|
+
root.children << people
|
28
|
+
root.children << groups
|
29
|
+
people.children << User
|
30
|
+
groups.children << Group
|
31
|
+
|
32
|
+
@tree = Hash[
|
33
|
+
[root, people, groups].map do |object|
|
34
|
+
dn_prefix = object.attributes['dn_prefix']
|
35
|
+
[dn(dn_prefix), object]
|
36
|
+
end
|
37
|
+
]
|
38
|
+
end
|
39
|
+
|
40
|
+
def dn(dn_prefix)
|
41
|
+
[dn_prefix, @basedn].reject(&:empty?).join(',')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'ldap/server'
|
2
|
+
|
3
|
+
module Ldumbd
|
4
|
+
class Operation < LDAP::Server::Operation
|
5
|
+
def initialize(connection, messageID, ldap_tree)
|
6
|
+
super(connection, messageID)
|
7
|
+
@ldap_tree = ldap_tree
|
8
|
+
end
|
9
|
+
|
10
|
+
def search(basedn, scope, deref, filter)
|
11
|
+
search_results(basedn, scope, deref, filter) do |ldap_object|
|
12
|
+
dn = @ldap_tree.dn(ldap_object['dn_prefix'])
|
13
|
+
object = ldap_object.reject { |k, v| k == 'dn_prefix' }
|
14
|
+
send_SearchResultEntry(dn, object)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def search_results(basedn, scope, deref, filter, &block)
|
20
|
+
case scope
|
21
|
+
when LDAP::Server::BaseObject
|
22
|
+
search_results_baseobject(basedn, filter, &block)
|
23
|
+
when LDAP::Server::SingleLevel
|
24
|
+
search_results_singlelevel(basedn, filter, &block)
|
25
|
+
when LDAP::Server::WholeSubtree
|
26
|
+
search_results_subtree(basedn, filter, &block)
|
27
|
+
else
|
28
|
+
raise LDAP::ResultError::UnwillingToPerform, 'Invalid search scope'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def search_results_baseobject(basedn, filter, &block)
|
33
|
+
# the base object only
|
34
|
+
object = @ldap_tree.find_by_dn(basedn)
|
35
|
+
if object.matches_filter?(filter) && block_given?
|
36
|
+
block.call(TableMap.sequel_to_ldap_object(object.attributes))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def search_results_singlelevel(basedn, filter, &block)
|
41
|
+
# objects immediately subordinate to the base object; does not
|
42
|
+
# include the base object itself.
|
43
|
+
object = @ldap_tree.find_by_dn(basedn)
|
44
|
+
object.each_child(filter) do |r|
|
45
|
+
block.call(TableMap.sequel_to_ldap_object(r)) if block_given?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def search_results_subtree(basedn, filter, &block)
|
50
|
+
# base object and the entire subtree, also includes the base
|
51
|
+
# object itself.
|
52
|
+
object = @ldap_tree.find_by_dn(basedn)
|
53
|
+
if object.matches_filter?(filter) && block_given?
|
54
|
+
block.call(TableMap.sequel_to_ldap_object(object.attributes))
|
55
|
+
end
|
56
|
+
object.each_child(filter, true) do |r|
|
57
|
+
block.call(TableMap.sequel_to_ldap_object(r)) if block_given?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end # Operation
|
61
|
+
end # Ldumbd
|