adap 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ee6e3514fa3c73373536ae3998baa47a70c1a2e528274d7a0b78f49a6a24188d
4
+ data.tar.gz: 226aed513502e093d6b53bbe3e959ce56b123ac5292643e0497a45d50556694d
5
+ SHA512:
6
+ metadata.gz: 55950748cc4a1c3fb4fd2c6cea48a432760808b26e42773937beb6786771842c433f2e9f62b5923bc529acc35a98eb1d17c8df5a5ab7ef92a330f9045114f404
7
+ data.tar.gz: d3254d935e294f91dd803ba4128cdec7e29d0e9fe34cee6dfaaf9895ec819a7b2299d7def2ac23e102bf4fa4850aedefd636e9e63a87fba9d4c9b0c008133b1e
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ adap-*.gem
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.5
7
+ before_install: gem install bundler -v 2.0.2
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in adap.gemspec
4
+ gemspec
5
+
6
+ gem "unix-crypt", "~> 1.3"
7
+
8
+ gem "net-ldap", "~> 0.16.2"
9
+
10
+ gem "mocha", "~> 1.10"
@@ -0,0 +1,28 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ adap (0.0.7)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ minitest (5.14.0)
10
+ mocha (1.11.2)
11
+ net-ldap (0.16.2)
12
+ rake (10.5.0)
13
+ unix-crypt (1.3.0)
14
+
15
+ PLATFORMS
16
+ ruby
17
+
18
+ DEPENDENCIES
19
+ adap!
20
+ bundler (~> 2.0)
21
+ minitest (~> 5.0)
22
+ mocha (~> 1.10)
23
+ net-ldap (~> 0.16.2)
24
+ rake (~> 10.0)
25
+ unix-crypt (~> 1.3)
26
+
27
+ BUNDLED WITH
28
+ 2.1.4
@@ -0,0 +1,122 @@
1
+ # Adap
2
+ Adap is a program that synchronize user data on Samba Active Directory (AD) to LDAP.
3
+
4
+ ## Installation
5
+
6
+ Add this line to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem 'adap'
10
+ ```
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install adap
19
+
20
+ ## Usage
21
+
22
+ To build this modules, run the command like below.
23
+
24
+ ```
25
+ gem build adap.gemspec
26
+ ```
27
+
28
+ Then include this module and use it like below.
29
+
30
+ ```ruby
31
+ require "adap"
32
+
33
+ adap = Adap.new({
34
+ :ad_host => "localhost", # Host name or IP of your Active Directory(AD)
35
+ :ad_binddn => "CN=Administrator,CN=Users,DC=mysite,DC=example,DC=com", # Bind dn of your AD
36
+ :ad_basedn => "CN=Users,DC=mysite,DC=example,DC=com", # Base dn of your AD
37
+ :ad_password => "ad_secret", # Password of your AD's bind dn
38
+ :ldap_host => "ldap_server", # Host name or IP of your LDAP
39
+ :ldap_binddn => "uid=Administrator,ou=Users,dc=mysite,dc=example,dc=com", # Bind dn of your LDAP
40
+ :ldap_basedn => "dc=mysite,dc=example,dc=com", # Base dn of your LDAP
41
+ :ldap_password => "ldap_secret" # Password of your LDAP's bind dn
42
+ })
43
+
44
+ # This operation will synchronize a user taro-suzuki to LDAP from AD
45
+ adap.sync_user("taro-suzuki")
46
+ ```
47
+
48
+ ## Requirements and limitations
49
+
50
+ This program has some requirements and limitations like below.
51
+
52
+ ### Not all attributes are synchronized
53
+
54
+ Data synchronized to LDAP from AD are limited such as dn, cn uid and uidNumber etc.
55
+ These attributes are enough to authenticate users to login to Unix-like systems that used an LDAP for authenticating users.
56
+
57
+ ### AD must be set not to require strong auth
58
+
59
+ AD must allow setting `ldap server require strong auth = no` for getting user data.
60
+
61
+ * smb.conf of your AD
62
+ ```
63
+ ldap server require strong auth = no
64
+ ```
65
+
66
+ This program will fail to get user data from AD if you did not allow this setting.
67
+
68
+ ### AD must allow CryptSHA256 or CryptSHA512 to store password and they have to be same as a storing method in LDAP
69
+
70
+ AD must allow storing password as CryptSHA256 or CryptSHA512 by setting smb.conf like below.
71
+
72
+ * your AD's smb.conf
73
+ ```
74
+ password hash userPassword schemes = CryptSHA256 CryptSHA512
75
+ ```
76
+
77
+ And LDAP have to be configured to store password as sha256(CryptSHA256) or sha512(CryptSHA512).
78
+
79
+ For example, you use OpneLDAP, you have to set configuration like below when you store password as sha256.
80
+
81
+ ```
82
+ $ ldapmodify -Y EXTERNAL -H ldapi:/// << 'EOF'
83
+ dn: cn=config
84
+ add: olcPasswordHash
85
+ olcPasswordHash: {CRYPT}
86
+ -
87
+ add: olcPasswordCryptSaltFormat
88
+ olcPasswordCryptSaltFormat: $5$%.16s
89
+ EOF
90
+ ```
91
+
92
+ This instruction allows us to save password as sha256 with a salt that length is 16 characters.
93
+ Or you can store user's password as sha512 with a salt that length is 16 characters like below.
94
+
95
+ ```
96
+ $ ldapmodify -Y EXTERNAL -H ldapi:/// << 'EOF'
97
+ dn: cn=config
98
+ add: olcPasswordHash
99
+ olcPasswordHash: {CRYPT}
100
+ -
101
+ add: olcPasswordCryptSaltFormat
102
+ olcPasswordCryptSaltFormat: $6$%.16s
103
+ EOF
104
+ ```
105
+
106
+ ### This program must be located in AD server
107
+
108
+ This program must be located in AD server because samba-tool on AD only support getting hashed password only from `ldapi://` or `tdb://`.
109
+
110
+ ### This program only supports syncing user data. Syncing group data does not support yet
111
+
112
+ Syncing group data might be supported and implemented if some people demand it.
113
+
114
+ ## Development
115
+
116
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
117
+
118
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
119
+
120
+ ## Contributing
121
+
122
+ Bug reports and pull requests are welcome on GitHub at https://github.com/TsutomuNakamura/adap.
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,29 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "adap/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "adap"
7
+ spec.version = ModAdap::VERSION
8
+ spec.authors = ["Tsutomu Nakamura"]
9
+ spec.email = ["tsuna.0x00@gmail.com"]
10
+
11
+ spec.summary = %q{LDAP migration tool from AD to NT schema}
12
+ spec.description = %q{This tool migrate AD LDAP entries to NT LDAP entries.}
13
+ spec.homepage = "https://github.com/TsutomuNakamura/adap"
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_development_dependency "bundler", "~> 2.0"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "minitest", "~> 5.0"
29
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "adap"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,21 @@
1
+ version: '3'
2
+
3
+ services:
4
+ ruby:
5
+ container_name: ad-ruby
6
+ image: tsutomu/adap:docker_bundle_v2.1.2
7
+ hostname: ruby
8
+ depends_on:
9
+ - nt
10
+ entrypoint:
11
+ - /opt/entrypoint.sh
12
+ volumes:
13
+ - .:/opt/app
14
+ privileged: true
15
+
16
+ nt:
17
+ container_name: nt
18
+ hostname: nt
19
+ image: tsutomu/docker-samba-nt
20
+ privileged: true
21
+
@@ -0,0 +1,13 @@
1
+ FROM tsutomu/docker-samba-ad
2
+ LABEL maintainer "Tsutomu Nakamura<tsuna.0x00@gmail.com>"
3
+
4
+ RUN DEBIAN_FRONTEND=noninteractive apt-get update \
5
+ && DEBIAN_FRONTEND=noninteractive apt-get -y install \
6
+ curl ruby-full ruby-bundler \
7
+ && gem update --system \
8
+ && rm -rf /usr/share/rubygems-integration/all/specifications/rake-* \
9
+ && apt-get clean
10
+
11
+ ENV LC_ALL=C.UTF-8
12
+ ENV LANG=C.UTF-8
13
+
@@ -0,0 +1,14 @@
1
+ # For testing
2
+ after the container up, you can test connectivity by executing command like below.
3
+
4
+ ```
5
+ ldapsearch -x -LLL -o 'ldif-wrap=no' -h ad -w "p@ssword0" \
6
+ -D "CN=Administrator,CN=Users,DC=mysite,DC=example,DC=com" \
7
+ -b "CN=Users,DC=mysite,DC=example,DC=com" \
8
+ '(objectCategory=CN=Person,CN=Schema,CN=Configuration,DC=mysite,DC=example,DC=com)'
9
+ ```
10
+
11
+ ```
12
+ samba-tool user getpassword taro-suzuki --attribute virtualCryptSHA512
13
+ ```
14
+
File without changes
@@ -0,0 +1,7 @@
1
+ require "adap/version"
2
+ require "adap/adap.rb"
3
+
4
+ module ModAdap
5
+ class Error < StandardError; end
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,471 @@
1
+ require 'net-ldap'
2
+
3
+ class Adap
4
+
5
+ # :unixhomedirectory and :homedirectory are the attributes that has same meaning between AD and LDAP.
6
+ USER_REQUIRED_ATTRIBUTES = [:cn, :sn, :uid, :uidnumber, :gidnumber, :displayname, :loginshell, :gecos, :givenname, :unixhomedirectory, :homedirectory]
7
+ #USER_REQUIRED_ATTRIBUTES = ['cn', 'sn', 'uid', 'uidNumber', 'gidNumber', 'homeDirectory', 'loginShell', 'gecos', 'givenName']
8
+ GROUP_OF_USER_REQUIRED_ATTRIBUTES = [:objectclass, :gidnumber, :cn, :description, :memberuid]
9
+
10
+ #
11
+ # params {
12
+ # :ad_host required IP or hostname of AD.
13
+ # :ad_port optional (default:389) Port of AD host.
14
+ # :ad_binddn required Binddn of AD.
15
+ # :ad_basedn required Basedn of AD users.
16
+ # :ad_password optional (default:(empty)) Password of AD with :ad_binddn.
17
+ # :ldap_host required IP or hostname of NT.
18
+ # :ldap_port optional (default:389) Port of NT host.
19
+ # :ldap_binddn required Binddn of NT.
20
+ # :ldap_basedn required Basedn of NT users.
21
+ # :ldap_password optional (default:(same as :ad_password)) Password of NT with :ldap_binddn
22
+ # :password_hash_algorithm (default:virtualCryptSHA512) Password hash algorithm. It must be same between AD and LDAP, and only support virtualCryptSHA512 or virtualCryptSHA256 now.
23
+ # }
24
+ #
25
+ def initialize(params)
26
+ raise "Initialize Adap was failed. params must not be nil" if params == nil
27
+ #raise 'Adap requires keys of parameter "ad_host" "ad_binddn" "ad_basedn"' \
28
+ [:ad_host, :ad_binddn, :ad_basedn, :ldap_host, :ldap_binddn, :ldap_basedn].each { |k|
29
+ raise 'Adap requires keys in params ":ad_host", ":ad_binddn", ":ad_basedn", ":ldap_host", ":ldap_binddn", ":ldap_basedn"' if !params.key?(k)
30
+ }
31
+
32
+ @ad_host = params[:ad_host]
33
+ @ad_port = (params[:ad_port] ? params[:ad_port] : 389)
34
+ @ad_binddn = params[:ad_binddn]
35
+ @ad_basedn = params[:ad_basedn]
36
+ @ad_auth = (params.has_key?(:ad_password) ? { :method => :simple, :username => @ad_binddn, :password => params[:ad_password] } : nil)
37
+ @ldap_host = params[:ldap_host]
38
+ @ldap_port = (params[:ldap_port] ? params[:ldap_port] : 389)
39
+ @ldap_binddn = params[:ldap_binddn]
40
+ @ldap_basedn = params[:ldap_basedn]
41
+ @ldap_user_basedn = params[:ldap_user_basedn]
42
+ @ldap_auth = (params.has_key?(:ldap_password) ? { :method => :simple, :username => @ldap_binddn, :password => params[:ldap_password] } : nil )
43
+ @password_hash_algorithm = (params[:password_hash_algorithm] ? params[:password_hash_algorithm] : 'virtualCryptSHA512')
44
+
45
+ @ad_client = Adap::get_ad_client_instance(@ad_host, @ad_port, @ad_auth)
46
+ @ldap_client = Adap::get_ldap_client_instance(@ldap_host, @ldap_port, @ldap_auth)
47
+ end
48
+
49
+ def self.get_ad_client_instance(ad_host, ad_port, ad_auth)
50
+ Net::LDAP.new(:host => ad_host, :port => ad_port, :auth => ad_auth)
51
+ end
52
+
53
+ def self.get_ldap_client_instance(ldap_host, ldap_port, ldap_auth)
54
+ Net::LDAP.new(:host => ldap_host, :port => ldap_port, :auth => ldap_auth)
55
+ end
56
+
57
+ def get_ad_dn(username)
58
+ "CN=#{username},CN=Users,#{@ad_basedn}"
59
+ end
60
+
61
+ def get_ldap_dn(username)
62
+ "uid=#{username},ou=Users,#{@ldap_basedn}"
63
+ end
64
+
65
+ def create_ldap_attributes(entry)
66
+ attributes = {
67
+ :objectclass => ["top", "person", "organizationalPerson", "inetOrgPerson", "posixAccount", "shadowAccount"]
68
+ }
69
+
70
+ entry.each do |attribute, values|
71
+ # Change string to lower case symbols to compare each attributes correctly
72
+ attribute = attribute.downcase.to_sym
73
+
74
+ if USER_REQUIRED_ATTRIBUTES.include?(attribute) then
75
+ if attribute == :unixhomedirectory then
76
+ attributes[:homedirectory] = values
77
+ else
78
+ attributes[attribute] = values
79
+ end
80
+ end
81
+ end
82
+
83
+ attributes
84
+ end
85
+
86
+ def get_password(username)
87
+ password = get_raw_password(username, @password_hash_algorithm)
88
+
89
+ if password == nil || password.empty?
90
+ raise "Failed to get password of #{username} from AD. Did you enabled AD password option virtualCryptSHA512 and/or virtualCryptSHA256?"
91
+ end
92
+ password = password.chomp
93
+
94
+ password
95
+ end
96
+
97
+ def get_raw_password(username, algo)
98
+ `samba-tool user getpassword #{username} --attribute #{algo} 2> /dev/null | grep -E '^virtualCrypt' -A 1 | tr -d ' \n' | cut -d ':' -f 2`
99
+ end
100
+
101
+ def sync_user(uid)
102
+ ad_entry = nil
103
+ ldap_entry = nil
104
+ ad_dn = get_ad_dn(uid)
105
+ ldap_dn = get_ldap_dn(uid)
106
+
107
+ # dn: CN=user-name,CN=Users,DC=mysite,DC=example,DC=com
108
+ @ad_client.search(:base => ad_dn) do |entry|
109
+ ad_entry = entry
110
+ end
111
+ ret_code = @ad_client.get_operation_result.code
112
+
113
+ return {
114
+ :code => ret_code,
115
+ :operations => nil,
116
+ :message => "Failed to get a user #{ad_dn} from AD - " + @ad_client.get_operation_result.error_message
117
+ } if ret_code != 0 && ret_code != 32
118
+
119
+ @ldap_client.search(:base => ldap_dn) do |entry|
120
+ ldap_entry = entry
121
+ end
122
+ ret_code = @ldap_client.get_operation_result.code
123
+
124
+ return {
125
+ :code => ret_code,
126
+ :operations => nil,
127
+ :message => "Failed to get a user #{ldap_dn} from LDAP - " + @ldap_client.get_operation_result.error_message
128
+ } if ret_code != 0 && ret_code != 32
129
+
130
+ ret = nil
131
+ if !ad_entry.nil? and ldap_entry.nil? then
132
+ ret = add_user(ldap_dn, ad_entry, get_password(uid))
133
+ elsif ad_entry.nil? and !ldap_entry.nil? then
134
+ ret = delete_user(ldap_dn)
135
+ elsif !ad_entry.nil? and !ldap_entry.nil? then
136
+ ret = modify_user(ldap_dn, ad_entry, ldap_entry, get_password(uid))
137
+ else
138
+ # ad_entry.nil? and ldap_entry.nil? then
139
+ return {:code => 0, :operations => nil, :message => "There are not any data of #{uid} to sync."}
140
+ end
141
+
142
+ return ret if ret[:code] != 0
143
+
144
+ # Sync groups belonging the user next if syncing data of the user has succeeded.
145
+ ret_sync_group = sync_group_of_user(uid, get_primary_gidnumber(ad_entry))
146
+
147
+ return {
148
+ :code => ret_sync_group[:code],
149
+ :operations => ret[:operations].concat(ret_sync_group[:operations]),
150
+ :message => (
151
+ ret_sync_group[:code] == 0 ?
152
+ nil : "Syncing a user #{uid} has succeeded but syncing its groups have failed. Message: " + ret_sync_group[:message]
153
+ )
154
+ }
155
+ end
156
+
157
+ def add_user(ldap_user_dn, ad_entry, password)
158
+ attributes = create_ldap_attributes(ad_entry)
159
+
160
+ @ldap_client.add(
161
+ :dn => ldap_user_dn,
162
+ :attributes => attributes
163
+ )
164
+ ret_code = @ldap_client.get_operation_result.code
165
+
166
+ return {
167
+ :code => ret_code,
168
+ :operations => [:add_user],
169
+ :message => "Failed to add a user #{ldap_user_dn} in add_user() - " + @ldap_client.get_operation_result.error_message
170
+ } if ret_code != 0
171
+
172
+ @ldap_client.modify(
173
+ :dn => ldap_user_dn,
174
+ :operations => [
175
+ [:add, :userPassword, password]
176
+ ]
177
+ )
178
+ ret_code = @ldap_client.get_operation_result.code
179
+
180
+ return {
181
+ :code => ret_code,
182
+ :operations => [:add_user],
183
+ :message => "Failed to modify a user #{ldap_user_dn} in add_user() - " + @ldap_client.get_operation_result.error_message
184
+ } if ret_code != 0
185
+
186
+ return {:code => ret_code, :operations => [:add_user], :message => nil}
187
+ end
188
+
189
+ def modify_user(ldap_user_dn, ad_entry, ldap_entry, password)
190
+ # An attribute objectClass will not be sync because it assumed already added by add_user() function or another method in LDAP.
191
+ operations = create_modify_operations(ad_entry, ldap_entry, password)
192
+
193
+ @ldap_client.modify(
194
+ :dn => ldap_user_dn,
195
+ :operations => operations
196
+ )
197
+ ret_code = @ldap_client.get_operation_result.code
198
+
199
+ return {
200
+ :code => ret_code,
201
+ :operations => [:modify_user],
202
+ :message => "Failed to modify a user #{ldap_user_dn} in modify_user() - " + @ldap_client.get_operation_result.error_message
203
+ } if ret_code != 0
204
+
205
+ return {:code => ret_code, :operations => [:modify_user], :message => nil}
206
+ end
207
+
208
+ def create_modify_operations(ad_entry, ldap_entry, password)
209
+ operations = []
210
+
211
+ ad_entry.each do |key, value|
212
+ ad_key_sym = key.downcase.to_sym
213
+ ldap_key = (key != :unixhomedirectory ? key : :homedirectory)
214
+ ldap_key_sym = ldap_key.downcase.to_sym
215
+
216
+ if USER_REQUIRED_ATTRIBUTES.include?(ad_key_sym)
217
+ next if value == ldap_entry[ldap_key]
218
+ operations.push((ldap_entry[ldap_key] != nil ? [:replace, ldap_key_sym, value] : [:add, ldap_key_sym, value]))
219
+ end
220
+ end
221
+
222
+ ldap_entry.each do |key, value|
223
+ ldap_key_sym = key.downcase.to_sym
224
+ ad_key = (key != :homedirectory ? key : :unixhomedirectory)
225
+
226
+ if USER_REQUIRED_ATTRIBUTES.include?(ldap_key_sym)
227
+ operations.push([:delete, ldap_key_sym, nil]) if ad_entry[ad_key] == nil
228
+ end
229
+ end
230
+
231
+ # AD does not have password as simple ldap attribute.
232
+ # So password will always be updated for this reason.
233
+ operations.push([:replace, :userpassword, password])
234
+
235
+ operations
236
+ end
237
+
238
+ def delete_user(ldap_user_dn)
239
+ @ldap_client.delete(:dn => ldap_user_dn)
240
+ ret_code = @ldap_client.get_operation_result.code
241
+
242
+ return {
243
+ :code => ret_code,
244
+ :operations => [:delete_user],
245
+ :message => "Failed to delete a user #{ldap_user_dn} in delete_user() - " + @ldap_client.get_operation_result.error_message
246
+ } if ret_code != 0
247
+
248
+ return {:code => ret_code, :operations => [:delete_user], :message => nil}
249
+ end
250
+
251
+ def sync_group_of_user(uid, primary_gid_number)
252
+ ad_group_map = {}
253
+ ldap_group_map = {}
254
+
255
+ # Creating AD ldapsearch filter
256
+
257
+ ad_filter = if primary_gid_number == nil then
258
+ Net::LDAP::Filter.construct(
259
+ "(&(objectCategory=CN=Group,CN=Schema,CN=Configuration,#{@ad_basedn})(member=CN=#{uid},CN=Users,#{@ad_basedn}))")
260
+ else
261
+ Net::LDAP::Filter.construct(
262
+ "(&(objectCategory=CN=Group,CN=Schema,CN=Configuration,#{@ad_basedn})(|(member=CN=#{uid},CN=Users,#{@ad_basedn})(gidNumber=#{primary_gid_number})))")
263
+ end
264
+
265
+ # Get groups from AD
266
+ # entry = {
267
+ # :gidnumber => xxx,
268
+ # }
269
+ #
270
+ @ad_client.search(:base => @ad_basedn, :filter => ad_filter) do |entry|
271
+ ad_group_map[entry[:name].first] = {:gidnumber => entry[:gidnumber]}
272
+ #ad_group_map[entry[:name]] = nil
273
+ end
274
+ ret_code = @ad_client.get_operation_result.code
275
+
276
+ return {
277
+ :code => ret_code,
278
+ :operations => [:search_groups_from_ad],
279
+ :message => "Failed to get groups of a user #{uid} from AD to sync them. " + @ad_client.get_operation_result.error_message
280
+ } if ret_code != 0 && ret_code != 32
281
+
282
+ # Create LDAP ldapsearch filter
283
+ ldap_filter = Net::LDAP::Filter.construct("(memberUid=#{uid})")
284
+
285
+ # Get groups from LDAP
286
+ @ldap_client.search(:base => "ou=Groups," + @ldap_basedn, :filter => ldap_filter) do |entry|
287
+ # gidnumber is not necessary for LDAP entry
288
+ ldap_group_map[entry[:cn].first] = nil
289
+ end
290
+ ret_code = @ldap_client.get_operation_result.code
291
+
292
+ return {
293
+ :code => ret_code,
294
+ :operations => [:search_groups_from_ldap],
295
+ :message => "Failed to get groups of a user #{uid} from LDAP to sync them. " + @ldap_client.get_operation_result.error_message
296
+ } if ret_code != 0
297
+
298
+ # Comparing name of AD's entry and cn of LDAP's entry
299
+ operation_pool = create_sync_group_of_user_operation(ad_group_map, ldap_group_map, uid)
300
+ ret = do_sync_group_of_user_operation(operation_pool)
301
+
302
+ return {
303
+ :code => ret[:code],
304
+ :operations => [:modify_group_of_user],
305
+ :message => (ret[:code] == 0 ? nil: ret[:message])
306
+ }
307
+ end
308
+
309
+ # {
310
+ # "cn=foo,ou=Groups,dc=mysite,dc=example,dc=com": {
311
+ # :cn => "foo",
312
+ # :gidnumber => xxx,
313
+ # :operations => [[:add, :memberuid, uid]]
314
+ # }
315
+ # "cn=bar,ou=Groups,dc=mysite,dc=example,dc=com": {
316
+ # :cn => "bar",
317
+ # :gidnumber => yyy,
318
+ # :operations => [[:delete, :memberuid, uid]]
319
+ # }
320
+ # }
321
+ def create_sync_group_of_user_operation(ad_group_map, ldap_group_map, uid)
322
+ operation_pool = {}
323
+
324
+ ad_group_map.each_key do |key|
325
+ dn = "cn=#{key},ou=Groups,#{@ldap_basedn}"
326
+ # Convert AD entries to LDAP entries to create operation to update LDAP data.
327
+ operation_pool[dn] = {
328
+ :cn => key,
329
+ :gidnumber => ad_group_map[key][:gidnumber],
330
+ :operations => [[:add, :memberuid, uid]]
331
+ } if !ldap_group_map.has_key?(key)
332
+ end
333
+
334
+ ldap_group_map.each_key do |key|
335
+ operation_pool["cn=#{key},ou=Groups,#{@ldap_basedn}"] = {
336
+ # :cn and :gidnumber are not necessary
337
+ :operations => [[:delete, :memberuid, uid]]
338
+ } if !ad_group_map.has_key?(key)
339
+ end
340
+
341
+ operation_pool
342
+ end
343
+
344
+ def do_sync_group_of_user_operation(operation_pool)
345
+ return {
346
+ :code => 0,
347
+ :operations => nil,
348
+ :message => "There are not any groups of user to sync"
349
+ } if operation_pool.length == 0
350
+
351
+ # ex)
352
+ # entry_key = "cn=bar,ou=Groups,dc=mysite,dc=example,dc=com"
353
+ operation_pool.each_key do |entry_key|
354
+
355
+ # ex)
356
+ # entry = {
357
+ # :cn => "bar",
358
+ # :gidnumber => 1000,
359
+ # :operations => [[:add, :memberuid, uid]] # or [[:delete, :memberuid, uid]]
360
+ # }
361
+ entry = operation_pool[entry_key]
362
+
363
+ if entry[:operations].first.first == :add then
364
+ ret = add_group_if_not_existed(entry_key, entry)
365
+ return ret if ret[:code] != 0
366
+ end
367
+ # The operation will be like...
368
+ # [[:add, :memberuid, "username"]] or [[:delete, :memberuid, "username"]]
369
+
370
+ @ldap_client.modify({
371
+ :dn => entry_key,
372
+ :operations => entry[:operations]
373
+ })
374
+ ret_code = @ldap_client.get_operation_result.code
375
+
376
+ return {
377
+ :code => ret_code,
378
+ :operations => [:modify_group_of_user],
379
+ :message => "Failed to modify group \"#{entry_key}\" of user #{entry[:cn]}. " + @ldap_client.get_operation_result.error_message
380
+ } if ret_code != 0
381
+
382
+ if entry[:operations].first.first == :delete then
383
+ ret = delete_group_if_existed_as_empty(entry_key)
384
+ return ret if ret[:code] != 0
385
+ end
386
+ end
387
+
388
+ return {:code => 0, :operations => [:modify_group_of_user], :message => nil}
389
+ end
390
+
391
+ def add_group_if_not_existed(group_dn, entry)
392
+ @ldap_client.search(:base => group_dn)
393
+ ret_code = @ldap_client.get_operation_result.code
394
+
395
+ # The group already existed
396
+ return {:code => 0, :operations => nil, :message => nil} if ret_code == 0
397
+
398
+ # Failed to query ldapsearch for some reason
399
+ return {
400
+ :code => ret_code,
401
+ :operations => nil,
402
+ :message => "Failed to search LDAP in add_group_if_not_existed(). " + @ldap_client.get_operation_result.error_message
403
+ } if ret_code != 32
404
+
405
+ attributes = {:objectclass => ["top", "posixGroup"]}
406
+ attributes[:gidnumber] = entry[:gidnumber] if entry[:gidnumber] != nil
407
+ attributes[:cn] = entry[:cn] if entry[:cn] != nil
408
+
409
+ @ldap_client.add(
410
+ :dn => group_dn,
411
+ :attributes => attributes
412
+ )
413
+ ret_code = @ldap_client.get_operation_result.code
414
+
415
+ return {
416
+ :code => ret_code,
417
+ :operations => [:add_group],
418
+ :message => (ret_code == 0 ? nil : "Failed to add a group in add_group_if_not_existed(). " + @ldap_client.get_operation_result.error_message)
419
+ }
420
+ end
421
+
422
+ def delete_group_if_existed_as_empty(group_dn)
423
+ is_no_memberuid = false
424
+ # Check whether the group has memberuid
425
+ @ldap_client.search(:base => group_dn, :filter => "(!(memberUid=*))") do |e|
426
+ is_no_memberuid = true
427
+ end
428
+
429
+ ret_code = @ldap_client.get_operation_result.code
430
+ return {:code => 0, :operations => nil, :message => nil} \
431
+ if (ret_code == 0 && is_no_memberuid == false) || ret_code == 32
432
+
433
+ return {
434
+ :code => ret_code,
435
+ :operations => nil,
436
+ :message => "Failed to search group in delete_group_if_existed_as_empty(). " + @ldap_client.get_operation_result.error_message
437
+ } if ret_code != 0
438
+
439
+ @ldap_client.delete(:dn => group_dn)
440
+ ret_code = @ldap_client.get_operation_result.code
441
+
442
+ return {
443
+ :code => ret_code,
444
+ :operations => [:delete_group],
445
+ :message => (ret_code == 0 ? nil: "Failed to delete a group in delete_group_if_existed_as_empty(). " + @ldap_client.get_operation_result.error_message)
446
+ }
447
+ end
448
+
449
+ def get_primary_gidnumber(entry)
450
+ return nil if entry == nil
451
+
452
+ if entry[:primarygroupid] == nil then
453
+ ad_result = get_primary_gidnumber_from_ad(entry[:uid].first)
454
+ return ad_result
455
+ end
456
+
457
+ return entry[:primarygroupid].first
458
+ end
459
+
460
+ def get_primary_gidnumber_from_ad(uid)
461
+ return nil if uid ==nil
462
+
463
+ @ad_client.search(:base => "CN=#{uid},CN=Users,#{@ad_basedn}") do |entry|
464
+ primary_gid = entry[:gidnumber].first
465
+ end
466
+
467
+ return primary_gid
468
+ end
469
+
470
+ end
471
+
@@ -0,0 +1,3 @@
1
+ module ModAdap
2
+ VERSION = "0.0.12"
3
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.12
5
+ platform: ruby
6
+ authors:
7
+ - Tsutomu Nakamura
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-01-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ description: This tool migrate AD LDAP entries to NT LDAP entries.
56
+ email:
57
+ - tsuna.0x00@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - Gemfile.lock
66
+ - README.md
67
+ - Rakefile
68
+ - adap.gemspec
69
+ - bin/console
70
+ - bin/setup
71
+ - docker-compose.yml
72
+ - dockerfile/Dockerfile
73
+ - dockerfile/README.md
74
+ - dockerfile/entrypoint.sh
75
+ - lib/adap.rb
76
+ - lib/adap/adap.rb
77
+ - lib/adap/version.rb
78
+ homepage: https://github.com/TsutomuNakamura/adap
79
+ licenses: []
80
+ metadata:
81
+ homepage_uri: https://github.com/TsutomuNakamura/adap
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 3.1.2
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: LDAP migration tool from AD to NT schema
101
+ test_files: []