ldap-group-manager 1.3.1
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/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +93 -0
- data/bin/ldap-group-manager +3 -0
- data/lib/app.rb +135 -0
- data/lib/config.rb +115 -0
- data/lib/ldap.rb +192 -0
- data/lib/logging.rb +29 -0
- data/lib/utils.rb +139 -0
- data/lib/version.rb +13 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9b343d86dae66287a19d9af4d598ab564ade5c2a918483ff2f28ea0b40109ce8
|
4
|
+
data.tar.gz: f64b800326c9d86dd01fe71b5d12fda1f52396057ceb84834b49bba68433c0f5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a55c41e7b7cc6af29bf0700c72ab3f4360894adfd5ec6e6f0f31ebf4e86d5ba1bcf94fb6bca9a751348b34cc0382cde93ed568257b92876c39e28ae86ea1b817
|
7
|
+
data.tar.gz: 6fd1873161c2ea3411503e87a55bf16eebc64782d37242f45b820ecd83f1cf445e5444ea528d82d2b041ced16747edf8137dc870088781909e8518020cac0da6
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2018 AdGear Technologies Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# ldap-group-manager
|
2
|
+
|
3
|
+
A Ruby helper to maintain LDAP group membership as flat yaml files. The goal is
|
4
|
+
to enable group membership to be managed through Github's pull requests
|
5
|
+
mechanisms by our CI systems.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
```shell
|
10
|
+
gem install ldap-group-manager
|
11
|
+
```
|
12
|
+
|
13
|
+
## Configuration
|
14
|
+
|
15
|
+
Just declare the following environment variables.
|
16
|
+
|
17
|
+
| Variable | Usage |
|
18
|
+
|----------------|----------------------------------------------------|
|
19
|
+
| AG_LDAP_HOST | Points to the ldaps server |
|
20
|
+
| AG_LOCAL_STATE | Points to the yaml definitions of your groups |
|
21
|
+
| AG_PASSWORD | The binder's password |
|
22
|
+
| AG_TREEBASE | The base of your search tree |
|
23
|
+
| AG_USER_DN | The full DN of the user to use as a binder to ldap |
|
24
|
+
|
25
|
+
## Commands
|
26
|
+
|
27
|
+
### diff
|
28
|
+
|
29
|
+
Displays the actions to be executed to bring remote in sync with local.
|
30
|
+
|
31
|
+
```shell
|
32
|
+
$ ldap-group-manager diff --verify-users
|
33
|
+
[2018-09-07T16:01:00.001-04:00] INFO: Compiling all local groups
|
34
|
+
[2018-09-07T16:02:00.002-04:00] INFO: Compiling local users
|
35
|
+
[2018-09-07T16:03:00.003-04:00] INFO: Verifying 67 local users against remote
|
36
|
+
[2018-09-07T16:04:00.004-04:00] INFO: Compiling remote groups
|
37
|
+
[2018-09-07T16:05:00.005-04:00] INFO: Operations to perform
|
38
|
+
{
|
39
|
+
:operations => {
|
40
|
+
:create => [],
|
41
|
+
:modify => [
|
42
|
+
[0] {
|
43
|
+
:cn => "yul",
|
44
|
+
:attrib => :description,
|
45
|
+
:value => [
|
46
|
+
[0] "Montreal"
|
47
|
+
]
|
48
|
+
},
|
49
|
+
[1] {
|
50
|
+
:cn => "yul.managers",
|
51
|
+
:attrib => :member,
|
52
|
+
:value => [
|
53
|
+
[0] "Keyser Soze",
|
54
|
+
[1] "Walter White",
|
55
|
+
[2] "Victor Von Doom"
|
56
|
+
]
|
57
|
+
}
|
58
|
+
],
|
59
|
+
:delete => [
|
60
|
+
[0] "yul.qa"
|
61
|
+
]
|
62
|
+
}
|
63
|
+
}
|
64
|
+
```
|
65
|
+
|
66
|
+
### apply
|
67
|
+
|
68
|
+
Applies the changes required to bring the remote state in sync with local.
|
69
|
+
|
70
|
+
```shell
|
71
|
+
ldap-group-manager apply
|
72
|
+
```
|
73
|
+
|
74
|
+
### Global flags
|
75
|
+
|
76
|
+
- `--verify-users` ensures every locally declared user exists on the
|
77
|
+
remote server. It takes forever to do... be forewarned.
|
78
|
+
|
79
|
+
## Contributing
|
80
|
+
|
81
|
+
1. Fork it!
|
82
|
+
2. Create your feature branch: `git checkout -b my-new-feature`
|
83
|
+
3. Commit your changes: `git commit -am 'Add some feature'`
|
84
|
+
4. Push to the branch: `git push origin my-new-feature`
|
85
|
+
5. Submit a pull request :D
|
86
|
+
|
87
|
+
## Credits
|
88
|
+
|
89
|
+
Alexis Vanier
|
90
|
+
|
91
|
+
## License
|
92
|
+
|
93
|
+
Check LICENSE file.
|
data/lib/app.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative('config')
|
4
|
+
require_relative('logging')
|
5
|
+
require_relative('ldap')
|
6
|
+
require_relative('utils')
|
7
|
+
require_relative('version')
|
8
|
+
require('thor')
|
9
|
+
|
10
|
+
# AdGear
|
11
|
+
# Top level container
|
12
|
+
# @since 0.1.0
|
13
|
+
module AdGear
|
14
|
+
# Infrastructure
|
15
|
+
# Container within the AdGear space for infrastructure related tools.
|
16
|
+
# @since 0.1.0
|
17
|
+
module Infrastructure
|
18
|
+
# GroupManager
|
19
|
+
# A Ruby gem that pushes group memberships to LDAP.
|
20
|
+
# @since 0.1.0
|
21
|
+
module GroupManager
|
22
|
+
# App
|
23
|
+
# The top of stack abstraction for this application.
|
24
|
+
# Read through the code to check the sequence of events.
|
25
|
+
# @since 0.1.0
|
26
|
+
class App < Thor
|
27
|
+
include AdGear::Infrastructure::GroupManager::Config
|
28
|
+
include AdGear::Infrastructure::GroupManager::Logging
|
29
|
+
include AdGear::Infrastructure::GroupManager::LDAP
|
30
|
+
include AdGear::Infrastructure::GroupManager::Utils
|
31
|
+
include AdGear::Infrastructure::GroupManager::Version
|
32
|
+
|
33
|
+
package_name 'groupmanager'
|
34
|
+
|
35
|
+
map %w[--version -v] => :print_version
|
36
|
+
|
37
|
+
# Displays the gem's version when invoked in the CLI.
|
38
|
+
# @since 0.1.0
|
39
|
+
desc '--version, -v', 'print the version'
|
40
|
+
def print_version
|
41
|
+
puts GEM_VERSION
|
42
|
+
end
|
43
|
+
|
44
|
+
class_option :verify_users, desc: 'Verifies that locally defined users exist remotely', type: :boolean
|
45
|
+
|
46
|
+
desc 'diff', 'displays the difference between local and remote'
|
47
|
+
# Displays the difference between local and remote.
|
48
|
+
# @since 0.1.0
|
49
|
+
def diff
|
50
|
+
# get all local groups
|
51
|
+
Log.info('Compiling all local groups')
|
52
|
+
local_groups = Config.list_all_groups
|
53
|
+
local_groups.each { |i| local_groups[i[0]] = Utils.symbolify_all_keys(i[1]) }
|
54
|
+
local_groups = Utils.sort_member(local_groups)
|
55
|
+
Log.debug(msg: 'local groups', local_groups: local_groups)
|
56
|
+
|
57
|
+
Log.info('Compiling local users')
|
58
|
+
users = Config.list_users
|
59
|
+
Log.debug(msg: 'users', users: users)
|
60
|
+
|
61
|
+
# get all local users and check if they exist remotely
|
62
|
+
if options[:verify_users]
|
63
|
+
Log.info("Verifying #{users.length} local users against remote")
|
64
|
+
users.each { |dn| LDAP.user_exists?(dn) ? Log.debug("#{dn} exists") : raise("#{dn} does not exist") }
|
65
|
+
end
|
66
|
+
|
67
|
+
# get all remote groups
|
68
|
+
Log.info('Compiling remote groups')
|
69
|
+
remote_groups = LDAP.list_all_groups
|
70
|
+
remote_groups = Utils.symbolify_all_keys(remote_groups)
|
71
|
+
remote_groups = Utils.sort_member(remote_groups)
|
72
|
+
Log.debug(msg: 'remote groups', remote_groups: remote_groups)
|
73
|
+
|
74
|
+
ops_to_perform = Utils.create_ops_list(local_groups, remote_groups)
|
75
|
+
Log.info(msg: 'Operations to perform', operations: ops_to_perform)
|
76
|
+
ops_to_perform
|
77
|
+
end
|
78
|
+
|
79
|
+
desc 'apply', 'applies changes to remote'
|
80
|
+
# Applies remote changes
|
81
|
+
# @since 0.1.0
|
82
|
+
def apply
|
83
|
+
ops_to_perform = diff
|
84
|
+
Log.info("Creating #{ops_to_perform[:create].length} new entities")
|
85
|
+
ops_to_perform[:create].each do |cn|
|
86
|
+
LDAP.set_item(
|
87
|
+
:create,
|
88
|
+
["cn=#{cn}", Utils.find_ou(cn), GLOBAL_CONFIG[:treebase]].join(', ')
|
89
|
+
)
|
90
|
+
end
|
91
|
+
if ops_to_perform[:create].any? && ops_to_perform[:modify].any?
|
92
|
+
sleep_time = GLOBAL_CONFIG[:settle_sleep]
|
93
|
+
sleep sleep_time
|
94
|
+
Log.info("Waiting #{sleep_time} seconds for ldap to propagate changes after object creation")
|
95
|
+
end
|
96
|
+
|
97
|
+
Log.info("Applying #{ops_to_perform[:modify].length} modifications to existing items")
|
98
|
+
ops_to_perform[:modify].each do |i|
|
99
|
+
LDAP.set_item(
|
100
|
+
:modify, ["cn=#{i[:cn]}",
|
101
|
+
Utils.find_ou(i[:cn]),
|
102
|
+
GLOBAL_CONFIG[:treebase]].join(', '), i[:attrib], i[:value]
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
if ops_to_perform[:delete]
|
107
|
+
Log.info("Removing #{ops_to_perform[:delete].length} deprecated items")
|
108
|
+
|
109
|
+
items_to_delete = ops_to_perform[:delete].map do |i|
|
110
|
+
treebase = GLOBAL_CONFIG[:treebase]
|
111
|
+
|
112
|
+
filter = Net::LDAP::Filter.construct("CN=#{i}*")
|
113
|
+
|
114
|
+
target = nil
|
115
|
+
Binder.search(base: treebase, filter: filter).each do |entry|
|
116
|
+
next unless /^CN=#{i},OU=/ =~ entry.dn
|
117
|
+
target = entry.dn
|
118
|
+
end
|
119
|
+
target
|
120
|
+
end
|
121
|
+
|
122
|
+
items_to_delete.each do |i|
|
123
|
+
LDAP.delete_item(i)
|
124
|
+
Log.debug(Binder.get_operation_result)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
Log.info('done')
|
129
|
+
|
130
|
+
exit(0)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/lib/config.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# The global config instance.
|
2
|
+
# @since 0.1.0
|
3
|
+
module AdGear
|
4
|
+
module Infrastructure
|
5
|
+
module GroupManager
|
6
|
+
module Config
|
7
|
+
require('yaml')
|
8
|
+
require_relative('logging')
|
9
|
+
|
10
|
+
include AdGear::Infrastructure::GroupManager::Logging
|
11
|
+
|
12
|
+
# The global config instance
|
13
|
+
# rubocop:disable Style/MutableConstant
|
14
|
+
GLOBAL_CONFIG = {
|
15
|
+
data: {
|
16
|
+
locations: {},
|
17
|
+
organizational: {},
|
18
|
+
functional: {},
|
19
|
+
permissions: {}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
GLOBAL_CONFIG[:user_dn] = ENV['AG_USER_DN']
|
24
|
+
GLOBAL_CONFIG[:password] = ENV['AG_PASSWORD']
|
25
|
+
GLOBAL_CONFIG[:ldap_host] = ENV['AG_LDAP_HOST']
|
26
|
+
GLOBAL_CONFIG[:treebase] = ENV['AG_TREEBASE']
|
27
|
+
GLOBAL_CONFIG[:local_state] = ENV['AG_LOCAL_STATE'] || Dir.pwd
|
28
|
+
GLOBAL_CONFIG[:settle_sleep] = Integer(ENV['AG_SETTLE_SLEEP'] || 15)
|
29
|
+
|
30
|
+
GLOBAL_CONFIG.freeze
|
31
|
+
# rubocop:enable Style/MutableConstant
|
32
|
+
|
33
|
+
config_files = Dir.glob(File.join(GLOBAL_CONFIG[:local_state], '**/*.{yaml,yml}'))
|
34
|
+
Log.trace(config_files)
|
35
|
+
Log.fatal('No configuration files detected') if config_files.empty?
|
36
|
+
|
37
|
+
config_files.each do |file|
|
38
|
+
Log.debug("loading #{file}")
|
39
|
+
parts = file.split('/').reject(&:empty?)
|
40
|
+
target = parts[parts.length - 2]
|
41
|
+
Log.debug("target: #{target}")
|
42
|
+
data = YAML.safe_load(File.read(file)) || {}
|
43
|
+
GLOBAL_CONFIG[:data][target.to_sym].merge!(data)
|
44
|
+
|
45
|
+
# This block sanitizes the schema of incoming configuration to ignore any unknown keys
|
46
|
+
# It's the _"next best thing"_ short of casting the individual items onto a schema. ;_;
|
47
|
+
GLOBAL_CONFIG[:data].each do |type, _|
|
48
|
+
Log.debug("type" => type)
|
49
|
+
GLOBAL_CONFIG[:data][type].each do |group, _|
|
50
|
+
Log.debug("group" => group)
|
51
|
+
Log.debug(GLOBAL_CONFIG[:data][type][group])
|
52
|
+
unknown_keys = GLOBAL_CONFIG[:data][type][group].keys.reject { |k| [ 'description', 'member' ].include?(k) }
|
53
|
+
|
54
|
+
Log.debug(unknown_keys)
|
55
|
+
unknown_keys.each do |k|
|
56
|
+
GLOBAL_CONFIG[:data][type][group].delete(k)
|
57
|
+
Log.debug("deleted #{type}.#{group}.#{k}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module_function
|
64
|
+
|
65
|
+
# Lists all the groups
|
66
|
+
# @since 0.1.0
|
67
|
+
def list_all_groups
|
68
|
+
newobj = {}
|
69
|
+
GLOBAL_CONFIG[:data].each do |_k, v|
|
70
|
+
newobj.merge!(v)
|
71
|
+
end
|
72
|
+
newobj
|
73
|
+
end
|
74
|
+
|
75
|
+
# List all organizational groups defined in local configuration.
|
76
|
+
# @since 0.1.0
|
77
|
+
def list_org_groups
|
78
|
+
GLOBAL_CONFIG[:data][:organizational]
|
79
|
+
end
|
80
|
+
|
81
|
+
# List all organizational groups defined in local configuration.
|
82
|
+
# @since 0.1.0
|
83
|
+
def list_perm_groups
|
84
|
+
GLOBAL_CONFIG[:data][:permissions]
|
85
|
+
end
|
86
|
+
|
87
|
+
# List all funcitonal groups defined in local configuration
|
88
|
+
# @since 1.0.0
|
89
|
+
def list_func_groups
|
90
|
+
GLOBAL_CONFIG[:data][:functional]
|
91
|
+
end
|
92
|
+
|
93
|
+
# List all location groups defined in local configuration.
|
94
|
+
# @since 0.1.0
|
95
|
+
def list_locations
|
96
|
+
GLOBAL_CONFIG[:data][:locations]
|
97
|
+
end
|
98
|
+
|
99
|
+
# List all users defined in local configuration.
|
100
|
+
# @since 0.1.0
|
101
|
+
def list_users
|
102
|
+
users = []
|
103
|
+
list_all_groups.each do |_k, v|
|
104
|
+
next if v.nil?
|
105
|
+
next unless v.key?('member')
|
106
|
+
next if v['member'].nil?
|
107
|
+
|
108
|
+
users += v['member']
|
109
|
+
end
|
110
|
+
users = users.uniq.sort - list_all_groups.keys
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/ldap.rb
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The global ldap instance. Uses the <code>net-ldap</code>.
|
4
|
+
# @since 0.1.0
|
5
|
+
module AdGear::Infrastructure::GroupManager::LDAP
|
6
|
+
require('net-ldap')
|
7
|
+
require_relative('./config')
|
8
|
+
require_relative('./logging')
|
9
|
+
require_relative('./utils')
|
10
|
+
|
11
|
+
include AdGear::Infrastructure::GroupManager::Config
|
12
|
+
include AdGear::Infrastructure::GroupManager::Logging
|
13
|
+
include AdGear::Infrastructure::GroupManager::Utils
|
14
|
+
|
15
|
+
Binder = Net::LDAP.new host: GLOBAL_CONFIG[:ldap_host],
|
16
|
+
port: 636,
|
17
|
+
encryption: {
|
18
|
+
method: :simple_tls,
|
19
|
+
tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
|
20
|
+
},
|
21
|
+
auth: {
|
22
|
+
method: :simple,
|
23
|
+
username: GLOBAL_CONFIG[:user_dn],
|
24
|
+
password: GLOBAL_CONFIG[:password]
|
25
|
+
}
|
26
|
+
|
27
|
+
module_function
|
28
|
+
|
29
|
+
# Fetches a given item by CN.
|
30
|
+
# @since 0.1.0
|
31
|
+
def get_item(cn, location)
|
32
|
+
treebase = GLOBAL_CONFIG[:treebase]
|
33
|
+
|
34
|
+
filter_string = ["distinguishedName=CN=#{cn}"]
|
35
|
+
filter_string << location if location
|
36
|
+
filter_string << treebase
|
37
|
+
filter_string = filter_string.join(', ')
|
38
|
+
|
39
|
+
filter = Net::LDAP::Filter.construct("(#{filter_string})")
|
40
|
+
|
41
|
+
hash = {}
|
42
|
+
Log.debug("Filter: #{filter}")
|
43
|
+
|
44
|
+
Binder.search(base: treebase, filter: filter) do |entry|
|
45
|
+
entry.each { |var| hash[var.to_s.delete('@')] = entry.public_send(var) }
|
46
|
+
Log.debug(msg: 'Found this!', data: hash)
|
47
|
+
end
|
48
|
+
hash
|
49
|
+
end
|
50
|
+
|
51
|
+
# Verifies that a given user exists.
|
52
|
+
# @since 0.1.0
|
53
|
+
def user_exists?(dn)
|
54
|
+
Log.debug("Verifying if user #{dn} exists")
|
55
|
+
treebase = GLOBAL_CONFIG[:treebase]
|
56
|
+
|
57
|
+
filter_string = ["distinguishedName=CN=#{dn}"]
|
58
|
+
filter_string << 'OU=Keycloak Users'
|
59
|
+
filter_string << treebase
|
60
|
+
|
61
|
+
filter = Net::LDAP::Filter.construct("(#{filter_string.join(', ')})")
|
62
|
+
result = Binder.search(base: treebase, filter: filter)
|
63
|
+
Log.trace("Dumping query inspect", data: result.inspect)
|
64
|
+
Log.trace("Dumping operation result", data: Binder.get_operation_result)
|
65
|
+
result.kind_of?(Array) && result.any?
|
66
|
+
end
|
67
|
+
|
68
|
+
# Modifies or creates an item.
|
69
|
+
# This is shoddily written and modify and create should be split.
|
70
|
+
# @since 0.1.0
|
71
|
+
def set_item(action, dn, attrib = nil, val = nil)
|
72
|
+
if action == :modify
|
73
|
+
if attrib == :member && !val.nil?
|
74
|
+
val.map! do |m|
|
75
|
+
[
|
76
|
+
"cn=#{m}",
|
77
|
+
AdGear::Infrastructure::GroupManager::Utils.find_ou(m),
|
78
|
+
GLOBAL_CONFIG[:treebase]
|
79
|
+
].join(', ')
|
80
|
+
end
|
81
|
+
Binder.replace_attribute(dn, attrib, val)
|
82
|
+
elsif val.nil?
|
83
|
+
Binder.replace_attribute(dn, attrib, [])
|
84
|
+
else
|
85
|
+
Binder.replace_attribute(dn, attrib, val)
|
86
|
+
end
|
87
|
+
Log.debug(msg: 'Trying to set attribute', result: Binder.get_operation_result, dn: dn, key: attrib, value: val)
|
88
|
+
elsif action == :create
|
89
|
+
base_attributes = {
|
90
|
+
samaccountname: dn.split(',').first.gsub(/cn=/i, ''),
|
91
|
+
objectclass: %w[top group]
|
92
|
+
}
|
93
|
+
|
94
|
+
Binder.add(dn: dn, attributes: base_attributes)
|
95
|
+
result = Binder.get_operation_result
|
96
|
+
Log.debug(msg: 'Trying to add item', result: result, dn: dn)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Deletes an item. Kerplow!
|
101
|
+
# @since 0.1.0
|
102
|
+
def delete_item(dn)
|
103
|
+
Binder.delete(dn: dn)
|
104
|
+
result = Binder.get_operation_result
|
105
|
+
Log.debug(msg: 'Trying to delete item', result: result, dn: dn)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Lists organizational units in the remote instance.
|
109
|
+
# @since 0.1.0
|
110
|
+
def list_organizational_units
|
111
|
+
treebase = GLOBAL_CONFIG[:treebase]
|
112
|
+
|
113
|
+
result = []
|
114
|
+
|
115
|
+
filter = Net::LDAP::Filter.construct('(objectCategory=organizationalUnit)')
|
116
|
+
Binder.search(base: treebase, filter: filter) do |entry|
|
117
|
+
result.push(entry.dn) if entry.dn.match?(/OU=Keycloak Groups/)
|
118
|
+
end
|
119
|
+
result.empty? ? raise('No valid OUs found') : result
|
120
|
+
end
|
121
|
+
|
122
|
+
# Lists all groups in the remote instance.
|
123
|
+
# @since 0.1.0
|
124
|
+
def list_groups(treebase)
|
125
|
+
filter = Net::LDAP::Filter.construct('(objectClass=group)')
|
126
|
+
|
127
|
+
result = {}
|
128
|
+
|
129
|
+
Log.debug(msg: 'base and filter', base: treebase, filter: filter.to_s)
|
130
|
+
|
131
|
+
Binder.search(base: treebase, filter: filter) do |entry|
|
132
|
+
Log.trace('Dumping binder entry', entry)
|
133
|
+
|
134
|
+
obj = {}
|
135
|
+
|
136
|
+
entry.each do |k, v|
|
137
|
+
next unless [:member].include?(k)
|
138
|
+
Log.trace('dumping key, values', k: k, v: v)
|
139
|
+
obj[k] = v.map { |p| extract_cn(p) }.sort
|
140
|
+
end
|
141
|
+
|
142
|
+
obj[:description] = entry.description if entry.respond_to?(:description)
|
143
|
+
|
144
|
+
result[extract_cn(entry.dn)] = obj unless entry.dn.nil?
|
145
|
+
end
|
146
|
+
|
147
|
+
Binder.get_operation_result
|
148
|
+
Log.error("No results for #{treebase}") if result.empty?
|
149
|
+
result
|
150
|
+
end
|
151
|
+
|
152
|
+
# Lists all organizational groups in the remote instance.
|
153
|
+
# @since 0.1.0
|
154
|
+
def list_org_groups
|
155
|
+
list_groups(list_organizational_units.select { |g| g.match?(/^OU=Organizational/) }.first)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Lists all functional groups in the remote instance.
|
159
|
+
# @since 1.0.0
|
160
|
+
def list_func_groups
|
161
|
+
list_groups(list_organizational_units.select { |g| g.match?(/^OU=Functional/) }.first)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Lists all permissions groups in the remote instance.
|
165
|
+
# @since 0.1.0
|
166
|
+
def list_perm_groups
|
167
|
+
list_groups(list_organizational_units.select { |g| g.match?(/^OU=Permission/) }.first)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Lists all location groups in the remote instance.
|
171
|
+
# @since 0.1.0
|
172
|
+
def list_locations
|
173
|
+
list_groups(list_organizational_units.select { |g| g.match?(/^OU=Location/) }.first)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Lists all groups groups in the remote instance.
|
177
|
+
# @since 0.1.0
|
178
|
+
def list_all_groups
|
179
|
+
groups = {}
|
180
|
+
groups.merge!(list_org_groups)
|
181
|
+
groups.merge!(list_func_groups)
|
182
|
+
groups.merge!(list_perm_groups)
|
183
|
+
groups.merge!(list_locations)
|
184
|
+
groups
|
185
|
+
end
|
186
|
+
|
187
|
+
# Extracts the CN out of a full DN.
|
188
|
+
# @since 0.1.0
|
189
|
+
def extract_cn(dn)
|
190
|
+
dn.split(',').select { |p| p.match(/^CN=/) }.first.gsub('CN=', '')
|
191
|
+
end
|
192
|
+
end
|
data/lib/logging.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The global logging instance. Uses the <code>Ougai</code> JSON structured
|
4
|
+
# logger.
|
5
|
+
# @since 0.1.0
|
6
|
+
module AdGear::Infrastructure::GroupManager::Logging
|
7
|
+
require('ougai')
|
8
|
+
|
9
|
+
# The global logging instance
|
10
|
+
if ENV['AG_LOG_FORMAT'] =~ /json/i
|
11
|
+
Log = Ougai::Logger.new(STDOUT)
|
12
|
+
else
|
13
|
+
Log = Ougai::Logger.new(STDERR)
|
14
|
+
Log.formatter = Ougai::Formatters::Readable.new
|
15
|
+
end
|
16
|
+
|
17
|
+
Log.level = ENV['LOG_LEVEL'] || 'info'
|
18
|
+
|
19
|
+
module_function
|
20
|
+
|
21
|
+
# A helper method that allows to log an error and exit.
|
22
|
+
# @param [Array] args passes all arguments, as is, to Ougai.
|
23
|
+
# @return [nil] no return.
|
24
|
+
# @since 0.1.0
|
25
|
+
def fatal(*args)
|
26
|
+
Log.error(*args)
|
27
|
+
exit(1)
|
28
|
+
end
|
29
|
+
end
|
data/lib/utils.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The global utils container. Used for storing common stuff.
|
4
|
+
# @since 0.1.0
|
5
|
+
module AdGear::Infrastructure::GroupManager::Utils
|
6
|
+
require_relative('./config')
|
7
|
+
require_relative('./logging')
|
8
|
+
|
9
|
+
include AdGear::Infrastructure::GroupManager::Config
|
10
|
+
include AdGear::Infrastructure::GroupManager::Logging
|
11
|
+
|
12
|
+
module_function
|
13
|
+
|
14
|
+
# A helper method that converts all symbol keys to string keys.
|
15
|
+
# @since 0.1.0
|
16
|
+
def stringify_all_keys(hash)
|
17
|
+
stringified_hash = {}
|
18
|
+
hash.each do |k, v|
|
19
|
+
stringified_hash[k.to_s] = v.is_a?(Hash) ? stringify_all_keys(v) : v
|
20
|
+
end
|
21
|
+
stringified_hash
|
22
|
+
end
|
23
|
+
|
24
|
+
# A helper method that converts all string keys to symbol keys.
|
25
|
+
# @since 0.1.0
|
26
|
+
def symbolify_all_keys(hash)
|
27
|
+
Log.debug(msg: 'symbolifiying', data: hash)
|
28
|
+
symbolified = {}
|
29
|
+
hash.keys.each do |k|
|
30
|
+
newkey = %w[member description].include?(k) ? k.to_sym : k
|
31
|
+
symbolified[newkey] = hash[k].is_a?(Hash) ? symbolify_all_keys(hash[k]) : hash[k]
|
32
|
+
end
|
33
|
+
symbolified
|
34
|
+
end
|
35
|
+
|
36
|
+
# I don't remember what this does
|
37
|
+
# @since 0.1.0
|
38
|
+
def diff_op_exist?(ops, type, target)
|
39
|
+
ops.select { |op| op[0] == type && op[1] == target }.any?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Checks if the reverse operation exists
|
43
|
+
# @since 0.1.0
|
44
|
+
def duplicate?(ops, item)
|
45
|
+
opposite = case item[0]
|
46
|
+
when '-'
|
47
|
+
'+'
|
48
|
+
when '+'
|
49
|
+
'-'
|
50
|
+
end
|
51
|
+
ops.select { |op| op[0] == opposite && op[1] == item[1] && op[2] == item[2] }.any?
|
52
|
+
end
|
53
|
+
|
54
|
+
# Finds in which OU a given CN can be found.
|
55
|
+
# @since 0.1.0
|
56
|
+
def find_ou(cn)
|
57
|
+
if GLOBAL_CONFIG[:data][:organizational].key?(cn)
|
58
|
+
'OU=Organizational, OU=Keycloak Groups'
|
59
|
+
elsif GLOBAL_CONFIG[:data][:functional].key?(cn)
|
60
|
+
'OU=Functional, OU=Keycloak Groups'
|
61
|
+
elsif GLOBAL_CONFIG[:data][:permissions].key?(cn)
|
62
|
+
'OU=Permission, OU=Keycloak Groups'
|
63
|
+
elsif GLOBAL_CONFIG[:data][:locations].key?(cn)
|
64
|
+
'OU=Location, OU=Keycloak Groups'
|
65
|
+
else
|
66
|
+
'OU=Keycloak Users'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Sorts the member array bundle of groups.
|
71
|
+
# @since 0.1.0
|
72
|
+
def sort_member(all_groups)
|
73
|
+
ordered_groups = {}
|
74
|
+
all_groups.keys.sort.each do |group|
|
75
|
+
ordered_groups[group] = {}
|
76
|
+
begin
|
77
|
+
all_groups[group].sort.map { |k, v| ordered_groups[group][k] = v }
|
78
|
+
rescue => e
|
79
|
+
Log.debug(group)
|
80
|
+
Log.debug(all_groups[group])
|
81
|
+
Log.error(e)
|
82
|
+
exit(1)
|
83
|
+
end
|
84
|
+
unless !all_groups[group].key?(:member) || all_groups[group][:member].nil?
|
85
|
+
ordered_groups[group].delete(:member)
|
86
|
+
ordered_groups[group][:member] = all_groups[group][:member].sort
|
87
|
+
end
|
88
|
+
end
|
89
|
+
ordered_groups
|
90
|
+
end
|
91
|
+
|
92
|
+
# Creates the list of operations to perform to synchronize local with remote.
|
93
|
+
# @since 0.1.0
|
94
|
+
def create_ops_list(local_groups, remote_groups)
|
95
|
+
operations = {
|
96
|
+
create: [],
|
97
|
+
modify: [],
|
98
|
+
delete: []
|
99
|
+
}
|
100
|
+
|
101
|
+
local_groups.each do |gr, vals|
|
102
|
+
# if remote group exists, diff each attribute
|
103
|
+
if remote_groups.key?(gr)
|
104
|
+
vals.keys.each do |key|
|
105
|
+
message = {
|
106
|
+
msg: "diffing local and remote instances of #{gr}",
|
107
|
+
cn: gr,
|
108
|
+
attrib: key,
|
109
|
+
local: local_groups[gr][key],
|
110
|
+
remote: remote_groups.dig(gr, key),
|
111
|
+
require_change: compare_attributes(local_groups[gr][key], remote_groups.dig(gr, key))
|
112
|
+
}
|
113
|
+
Log.debug(message) if message[:require_change]
|
114
|
+
if compare_attributes(local_groups[gr][key], remote_groups.dig(gr, key))
|
115
|
+
operations[:modify] << { cn: gr, attrib: key, value: local_groups[gr][key] }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
else
|
119
|
+
# if remote group doesn't exist create key, then modify
|
120
|
+
operations[:create] << gr
|
121
|
+
vals.keys.each do |key|
|
122
|
+
operations[:modify] << { cn: gr, attrib: key, value: local_groups[gr][key] }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# delete remote groups that aren't locally described
|
128
|
+
(remote_groups.keys - local_groups.keys).each { |gr| operations[:delete] << gr }
|
129
|
+
|
130
|
+
# return stuff to do
|
131
|
+
operations
|
132
|
+
end
|
133
|
+
|
134
|
+
# A helper method that compares two objects, accounting for LDAP idiosyncracies.
|
135
|
+
# @since 0.1.0
|
136
|
+
def compare_attributes(local, remote)
|
137
|
+
(local == [] ? nil : local) != remote
|
138
|
+
end
|
139
|
+
end
|
data/lib/version.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Holds the verison of the gem.
|
4
|
+
module AdGear
|
5
|
+
module Infrastructure
|
6
|
+
module GroupManager
|
7
|
+
module Version
|
8
|
+
# The global constant holding the version of the gem.
|
9
|
+
GEM_VERSION = '1.3.1'.freeze
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ldap-group-manager
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.3.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexis Vanier
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-08-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: net-ldap
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.16.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.16.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ougai
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: thor
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.20.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.20.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: awesome_print
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
executables:
|
72
|
+
- ldap-group-manager
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- Gemfile
|
77
|
+
- LICENSE
|
78
|
+
- README.md
|
79
|
+
- bin/ldap-group-manager
|
80
|
+
- lib/app.rb
|
81
|
+
- lib/config.rb
|
82
|
+
- lib/ldap.rb
|
83
|
+
- lib/logging.rb
|
84
|
+
- lib/utils.rb
|
85
|
+
- lib/version.rb
|
86
|
+
homepage: https://www.github.com/adgear/ldap-group-manager
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - "~>"
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 2.7.0
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubygems_version: 3.1.4
|
106
|
+
signing_key:
|
107
|
+
specification_version: 4
|
108
|
+
summary: Manage ldap group membership as flat files
|
109
|
+
test_files: []
|