ldap-group-manager 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+ gemspec
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.
@@ -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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative('../lib/app.rb')
3
+ AdGear::Infrastructure::GroupManager::App.start(ARGV)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: []