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.
@@ -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: []