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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +7 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +28 -0
- data/README.md +122 -0
- data/Rakefile +10 -0
- data/adap.gemspec +29 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docker-compose.yml +21 -0
- data/dockerfile/Dockerfile +13 -0
- data/dockerfile/README.md +14 -0
- data/dockerfile/entrypoint.sh +0 -0
- data/lib/adap.rb +7 -0
- data/lib/adap/adap.rb +471 -0
- data/lib/adap/version.rb +3 -0
- metadata +101 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/adap.gemspec
ADDED
@@ -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
|
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/docker-compose.yml
ADDED
@@ -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
|
data/lib/adap.rb
ADDED
data/lib/adap/adap.rb
ADDED
@@ -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
|
+
|
data/lib/adap/version.rb
ADDED
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: []
|