socialcast 1.2.3 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +8 -8
  2. data/.travis.yml +1 -2
  3. data/bin/socialcast +2 -2
  4. data/lib/socialcast.rb +7 -15
  5. data/lib/socialcast/command_line/cli.rb +161 -0
  6. data/lib/socialcast/command_line/message.rb +18 -0
  7. data/lib/socialcast/command_line/provision.rb +330 -0
  8. data/lib/socialcast/command_line/version.rb +5 -0
  9. data/socialcast.gemspec +13 -10
  10. data/spec/fixtures/fake_attribute_map.rb +8 -6
  11. data/spec/fixtures/ldap_with_account_type_without_roles.yml +44 -0
  12. data/spec/fixtures/ldap_with_class_ldap_attribute.yml +47 -0
  13. data/spec/fixtures/ldap_with_connection_mapping.yml +52 -0
  14. data/spec/fixtures/ldap_with_connection_permission_mapping.yml +59 -0
  15. data/spec/fixtures/ldap_with_custom_attributes.yml +50 -0
  16. data/spec/fixtures/ldap_with_manager_attribute.yml +6 -3
  17. data/spec/fixtures/ldap_with_multiple_connection_mappings.yml +51 -0
  18. data/spec/fixtures/ldap_with_multiple_connection_permission_mappings.yml +75 -0
  19. data/spec/fixtures/ldap_with_plugin_mapping.yml +1 -1
  20. data/spec/fixtures/ldap_with_roles_without_account_type.yml +48 -0
  21. data/spec/fixtures/ldap_with_unique_identifier.yml +50 -0
  22. data/spec/fixtures/ldap_without_account_type_or_roles.yml +42 -0
  23. data/spec/{cli_spec.rb → socialcast/command_line/cli_spec.rb} +94 -82
  24. data/spec/socialcast/command_line/provision_spec.rb +497 -0
  25. data/spec/spec_helper.rb +2 -1
  26. metadata +103 -26
  27. data/lib/ext/array_ext.rb +0 -11
  28. data/lib/ext/string_ext.rb +0 -13
  29. data/lib/socialcast/cli.rb +0 -339
  30. data/lib/socialcast/message.rb +0 -17
  31. data/lib/socialcast/net_ldap_ext.rb +0 -87
  32. data/lib/socialcast/version.rb +0 -3
  33. data/spec/net_ldap_ext_spec.rb +0 -107
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- Y2Q0N2I4MGQyYzQ1MzBlYmExMmY5NjcyZDQ2YzNmMWZkYTNmZWFlZg==
4
+ NGRhZWZlNmVkZGY1ODEwZTcyNTJkMmYyZmE4ZjQ3Yzc4ZWIwNDIyNw==
5
5
  data.tar.gz: !binary |-
6
- N2Q1OWJjZmNhYTE4MmRlNzg5MmFmZGIxNTg4M2YyZTdjZjIyMzlhYw==
6
+ MDMzZGZiZjdmN2NkOTk5NzZjYTU2MzY3YzIxZDQxZWNkMmQxNWMwNw==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- OGE0ZWRiY2IwMGNlNjQ2NzE2MTc4NzE4ZWQ4NmFiNzEzZWMxNjc2MWNmOWFj
10
- ZGU1YzZjZjM5ZmE1ZTgzMjI2OWI5ZWQzODk0YzQyY2FhOTdlMWFmMTIxYjRi
11
- Y2RjNWM2OWYyYmJhMzEyMWI5ODI4MzJiMjM4MmUyMDBhNTRhMmY=
9
+ MmEzZWUwYjZlZDk1ZGM3NWRkYjNlYWRiMWU3NGIxZTg3ZWEzOTg2NGRiZGFk
10
+ NDk4MGQ0NWI1OGNmZWMzYTEwZWJmZDg0NjI3OGRlZmViNmI0YjAxNjQ2NGU4
11
+ M2I1NmZmYTllYWRlMjkwZmY2YTY2ZjgyZDdmZTYwYjY3ZTFhNzI=
12
12
  data.tar.gz: !binary |-
13
- M2Q1ZjdjODVlY2I1MmRmOGVjMmE0ZjAzMzkxYWU0YTI3OWY1OGVjODNjZWI1
14
- ZWFhNTAyYzU5ODAyNTdjMjAwZTNiNGFkMDYxNjNhYmI4YjQ5MjVlZDBhNzcz
15
- NWQ1NzdjODQ2MjcwMzQ0MTVkNWI5Y2UwMTk3YjkxZjZjZTRmZmQ=
13
+ MTJmNDA3MTQ1ZDdkMDhhNDMyOWNiNjcyZTRhN2UzOWU2YjEzMDJmY2RiNDk3
14
+ YzZjMzMzNDdmYWYxMzllMmM0MGUxZTFiNTRiNzViZDVlZGRmZTQ1YjA4ZmRm
15
+ YjJiMzVjMzlmOGVlNDEzYzEzYTNhMTlkYzg5MGVlMmEwZTkyMTk=
data/.travis.yml CHANGED
@@ -1,6 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.8.7
4
3
  - 1.9.3
5
- - ree
6
4
  - 2.0.0
5
+ - 2.1.1
data/bin/socialcast CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  $:.unshift(File.expand_path('../../lib', __FILE__))
4
- require 'socialcast/cli'
4
+ require 'socialcast/command_line/cli'
5
5
 
6
- Socialcast::CLI.start
6
+ Socialcast::CommandLine::CLI.start
data/lib/socialcast.rb CHANGED
@@ -1,40 +1,32 @@
1
1
  require 'yaml'
2
2
  require 'fileutils'
3
- require File.join(File.dirname(__FILE__), 'ext', 'array_ext') unless Array.respond_to?(:wrap)
4
- require File.join(File.dirname(__FILE__), 'ext', 'string_ext')
5
3
 
6
4
  module Socialcast
7
- class << self
8
- def config_dir
5
+ module CommandLine
6
+ def self.config_dir
9
7
  config_dir = File.expand_path '~/.socialcast'
10
8
  FileUtils.mkdir config_dir, :mode => 0700 unless File.exist?(config_dir)
11
9
  config_dir
12
10
  end
13
- def credentials_file
11
+ def self.credentials_file
14
12
  File.join config_dir, 'credentials.yml'
15
13
  end
16
- def credentials
14
+ def self.credentials
17
15
  fail 'Unknown Socialcast credentials. Run `socialcast authenticate` to initialize' unless File.exist?(credentials_file)
18
16
  @@credentials ||= YAML.load_file(credentials_file)
19
17
  end
20
- def credentials=(options)
18
+ def self.credentials=(options)
21
19
  File.open(credentials_file, "w") do |f|
22
20
  f.write(options.to_yaml)
23
21
  end
24
22
  File.chmod 0600, credentials_file
25
23
  end
26
- def basic_auth_options
27
- {:user => credentials[:user], :password => credentials[:password]}
28
- end
29
- def default_options
30
- basic_auth_options
31
- end
32
24
  # configure restclient for api call
33
- def resource_for_path(path, options = {}, debug = true)
25
+ def self.resource_for_path(path, options = {}, debug = true)
34
26
  RestClient.log = Logger.new(STDOUT) if debug
35
27
  RestClient.proxy = credentials[:proxy] if credentials[:proxy]
36
28
  url = ['https://', credentials[:domain], path].join
37
- RestClient::Resource.new url, options.merge(default_options)
29
+ RestClient::Resource.new url, options.merge({ :user => credentials[:user], :password => credentials[:password] })
38
30
  end
39
31
  end
40
32
  end
@@ -0,0 +1,161 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rubygems'
4
+
5
+ require "thor"
6
+ require 'json'
7
+ require 'rest_client'
8
+ require 'highline'
9
+ require 'socialcast'
10
+ require 'socialcast/command_line/message'
11
+ require 'socialcast/command_line/provision'
12
+
13
+ require 'logger'
14
+ require 'fileutils'
15
+
16
+ # uncomment to debug HTTP traffic
17
+ # class ActiveResource::Connection
18
+ # def configure_http(http)
19
+ # http = apply_ssl_options(http)
20
+ # # Net::HTTP timeouts default to 60 seconds.
21
+ # if @timeout
22
+ # http.open_timeout = @timeout
23
+ # http.read_timeout = @timeout
24
+ # end
25
+ # http.set_debug_output STDOUT
26
+ # http
27
+ # end
28
+ # end
29
+
30
+ module Socialcast
31
+ module CommandLine
32
+ class CLI < Thor
33
+ include Thor::Actions
34
+
35
+ method_option :trace, :type => :boolean, :aliases => '-v'
36
+ def initialize(*args); super(*args) end
37
+
38
+ desc "authenticate", "Authenticate using your Socialcast credentials"
39
+ method_option :user, :type => :string, :aliases => '-u', :desc => 'email address for the authenticated user'
40
+ method_option :password, :type => :string, :aliases => '-p', :desc => 'password for the authenticated user'
41
+ method_option :domain, :type => :string, :default => 'api.socialcast.com', :desc => 'Socialcast community domain'
42
+ method_option :proxy, :type => :string, :desc => 'HTTP proxy options for connecting to Socialcast server'
43
+ def authenticate
44
+ user = options[:user] || ask('Socialcast username: ')
45
+ password = options[:password] || HighLine.new.ask("Socialcast password: ") { |q| q.echo = false }.to_s
46
+ domain = options[:domain]
47
+
48
+ url = ['https://', domain, '/api/authentication'].join
49
+ say "Authenticating #{user} to #{url}"
50
+ params = {:email => user, :password => password }
51
+ RestClient.log = Logger.new(STDOUT) if options[:trace]
52
+ RestClient.proxy = options[:proxy] if options[:proxy]
53
+ resource = RestClient::Resource.new url
54
+ response = resource.post params, :accept => :json
55
+ say "API response: #{response.body.to_s}" if options[:trace]
56
+ communities = JSON.parse(response.body.to_s)['communities']
57
+ domain = communities.detect {|c| c['domain'] == domain} ? domain : communities.first['domain']
58
+
59
+ Socialcast::CommandLine.credentials = {:user => user, :password => password, :domain => domain, :proxy => options[:proxy]}
60
+ say "Authentication successful for #{domain}"
61
+ end
62
+
63
+ desc "share MESSAGE", "Posts a new message into socialcast"
64
+ method_option :url, :type => :string, :desc => '(optional) url to associate to the message'
65
+ method_option :message_type, :type => :string, :desc => '(optional) force an alternate message_type'
66
+ method_option :attachments, :type => :array, :default => []
67
+ method_option :group_id, :type => :numeric, :desc => "(optional) ID of group to post into"
68
+ def share(message = nil)
69
+ message ||= $stdin.read_nonblock(100_000) rescue nil
70
+
71
+ attachment_ids = []
72
+ options[:attachments].each do |path|
73
+ Dir[File.expand_path(path)].each do |attachment|
74
+ say "Uploading attachment #{attachment}..."
75
+ uploader = Socialcast::CommandLine.resource_for_path '/api/attachments', {}, options[:trace]
76
+ uploader.post({:attachment => File.new(attachment)}, {:accept => :json}) do |response, request, result|
77
+ if response.code == 201
78
+ attachment_ids << JSON.parse(response.body)['attachment']['id']
79
+ else
80
+ say "Error uploading attachment: #{response.body}"
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ ActiveResource::Base.logger = Logger.new(STDOUT) if options[:trace]
87
+ Socialcast::CommandLine::Message.configure_from_credentials
88
+ Socialcast::CommandLine::Message.create :body => message, :url => options[:url], :message_type => options[:message_type], :attachment_ids => attachment_ids, :group_id => options[:group_id]
89
+
90
+ say "Message has been shared"
91
+ end
92
+
93
+ desc 'provision', 'provision users from ldap compatible user repository'
94
+ method_option :config, :default => 'ldap.yml', :aliases => '-c', :desc => 'Path to ldap config file'
95
+ method_option :output, :default => Socialcast::CommandLine::Provision::DEFAULT_OUTPUT_FILE, :aliases => '-o', :desc => 'Name of the output file'
96
+ method_option :setup, :type => :boolean, :desc => 'Create an example ldap config file and exit'
97
+ method_option :delete_users_file, :type => :boolean, :desc => 'Delete the output file'
98
+ method_option :test, :type => :boolean, :desc => 'Do not persist changes'
99
+ method_option :skip_emails, :type => :boolean, :desc => 'Do not send signup emails to users'
100
+ method_option :force, :type => :boolean, :aliases => '-f', :default => false, :desc => 'Proceed with provisioning even if no users are found, which would deactivate all users in the community'
101
+ method_option :sanity_check, :type => :boolean, :default => false, :desc => 'Double check that users marked for termination really no longer exist'
102
+ method_option :plugins, :type => :array, :desc => "Pass in an array of plugins. Can be either the gem require or the absolute path to a ruby file"
103
+ def provision
104
+ config = ldap_config options
105
+ load_plugins options
106
+
107
+ Socialcast::CommandLine::Provision.new(config, options).provision
108
+
109
+ rescue Socialcast::CommandLine::Provision::ProvisionError => e
110
+ Kernel.abort e.message
111
+ end
112
+
113
+ desc 'sync_photos', 'Upload default avatar photos from LDAP repository'
114
+ method_option :config, :default => 'ldap.yml', :aliases => '-c'
115
+ def sync_photos
116
+ config = ldap_config options
117
+
118
+ Socialcast::CommandLine::Provision.new(config).sync_photos
119
+ end
120
+
121
+ no_tasks do
122
+ def load_plugins(options)
123
+ Array.wrap(options[:plugins]).each do |plugin|
124
+ begin
125
+ require plugin
126
+ rescue LoadError => e
127
+ fail "Unable to load #{plugin}: #{e}"
128
+ end
129
+ end
130
+ end
131
+
132
+ def ldap_config(options)
133
+ config_file = File.expand_path options[:config]
134
+
135
+ if options[:setup]
136
+ create_file config_file do
137
+ File.read File.join(File.dirname(__FILE__), '..', '..', '..', 'config', 'ldap.yml')
138
+ end
139
+ say "Created config file: #{config_file}"
140
+ Kernel.exit 0
141
+ end
142
+
143
+ fail "Unable to load configuration file: #{config_file}" unless File.exists?(config_file)
144
+ say "Using configuration file: #{config_file}"
145
+ config = YAML.load_file config_file
146
+
147
+ mappings = config.fetch 'mappings', {}
148
+ required_mappings = %w{email first_name last_name}
149
+ required_mappings.each do |field|
150
+ unless mappings.has_key? field
151
+ fail "Missing required mapping: #{field}"
152
+ end
153
+ end
154
+
155
+ config
156
+ end
157
+
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,18 @@
1
+ require 'active_resource'
2
+
3
+ ActiveResource::Base.include_root_in_json = true
4
+
5
+ module Socialcast
6
+ module CommandLine
7
+ class Message < ActiveResource::Base
8
+ headers['Accept'] = 'application/json'
9
+
10
+ def self.configure_from_credentials
11
+ Socialcast::CommandLine::Message.site = ['https://', Socialcast::CommandLine.credentials[:domain], '/api'].join
12
+ Socialcast::CommandLine::Message.proxy = Socialcast::CommandLine.credentials[:proxy] if Socialcast::CommandLine.credentials[:proxy]
13
+ Socialcast::CommandLine::Message.user = Socialcast::CommandLine.credentials[:user]
14
+ Socialcast::CommandLine::Message.password = Socialcast::CommandLine.credentials[:password]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,330 @@
1
+ require 'net/ldap'
2
+
3
+ require 'zlib'
4
+ require 'builder'
5
+ require 'set'
6
+ require 'fileutils'
7
+
8
+ module Socialcast
9
+ module CommandLine
10
+ class Provision
11
+ DEFAULT_OUTPUT_FILE = 'users.xml.gz'
12
+
13
+ class ProvisionError < StandardError; end
14
+
15
+ def initialize(ldap_config, options = {})
16
+ @ldap_config = ldap_config.dup
17
+ @options = options.dup
18
+
19
+ @options[:output] ||= DEFAULT_OUTPUT_FILE
20
+ end
21
+
22
+ def each_user_hash
23
+ each_ldap_entry do |ldap, entry, attr_mappings, perm_mappings|
24
+ yield build_user_hash_from_mappings(ldap, entry, attr_mappings, perm_mappings)
25
+ end
26
+ end
27
+
28
+ def provision
29
+ http_config = @ldap_config.fetch 'http', {}
30
+
31
+ user_whitelist = Set.new
32
+ output_file = File.join Dir.pwd, @options[:output]
33
+
34
+ Zlib::GzipWriter.open(output_file) do |gz|
35
+ xml = Builder::XmlMarkup.new(:target => gz, :indent => 1)
36
+ xml.instruct!
37
+ xml.export do |export|
38
+ export.users(:type => "array") do |users|
39
+ each_user_hash do |user_hash|
40
+ users << user_hash.to_xml(:skip_instruct => true, :root => 'user')
41
+ user_whitelist << [user_hash['contact_info']['email'], user_hash['unique_identifier'], user_hash['employee_number']]
42
+ end
43
+ end # users
44
+ end # export
45
+ end # gzip
46
+
47
+ if @options[:sanity_check]
48
+ puts "Sanity checking users currently marked as needing to be terminated"
49
+ each_ldap_connection do |ldap_connection_name, connection, ldap|
50
+ attr_mappings = attribute_mappings(ldap_connection_name)
51
+ (current_socialcast_users(http_config) - user_whitelist).each do |user_identifiers|
52
+ combined_filters = []
53
+ ['email', 'unique_identifier', 'employee_number'].each_with_index do |identifier, index|
54
+ combined_filters << ((attr_mappings[identifier].blank? || user_identifiers[index].nil?) ? nil : Net::LDAP::Filter.eq(attr_mappings[identifier], user_identifiers[index]))
55
+ end
56
+ combined_filters.compact!
57
+ filter = ((combined_filters.size > 1) ? '(|%s)' : '%s') % combined_filters.join(' ')
58
+ filter = Net::LDAP::Filter.construct(filter) & Net::LDAP::Filter.construct(connection["filter"])
59
+ ldap_result = ldap.search(:return_result => true, :base => connection["basedn"], :filter => filter, :attributes => ldap_search_attributes(ldap_connection_name))
60
+ raise ProvisionError.new "Found user marked for termination that should not be terminated: #{user_identifiers}" unless ldap_result.blank?
61
+ end
62
+ end
63
+ end
64
+
65
+ if user_whitelist.empty? && !@options[:force]
66
+ raise ProvisionError.new "Skipping upload to Socialcast since no users were found"
67
+ else
68
+ puts "Uploading dataset to Socialcast..."
69
+ resource = Socialcast::CommandLine.resource_for_path '/api/users/provision', http_config
70
+ begin
71
+ File.open(output_file, 'r') do |file|
72
+ request_params = {:file => file}
73
+ request_params[:skip_emails] = 'true' if (@ldap_config['options']["skip_emails"] || @options[:skip_emails])
74
+ request_params[:test] = 'true' if (@ldap_config['options']["test"] || @options[:test])
75
+ resource.post request_params, :accept => :json
76
+ end
77
+ rescue RestClient::Unauthorized => e
78
+ raise ProvisionError.new "Authenticated user either does not have administration privileges or the community is not configured to allow provisioning. Please contact Socialcast support to if you need help." if e.http_code == 401
79
+ end
80
+ puts "Finished"
81
+ end
82
+ File.delete(output_file) if (@ldap_config['options']['delete_users_file'] || @options[:delete_users_file])
83
+ end
84
+
85
+ def sync_photos
86
+ http_config = @ldap_config.fetch 'http', {}
87
+
88
+ @ldap_config["connections"].keys.each do |ldap_connection_name|
89
+ attribute_mappings(ldap_connection_name).fetch('profile_photo')
90
+ end
91
+
92
+ search_users_resource = Socialcast::CommandLine.resource_for_path '/api/users/search', http_config
93
+
94
+ each_ldap_entry do |ldap, entry, attr_mappings, _|
95
+ email = grab(entry, attr_mappings['email'])
96
+ if profile_photo_data = grab(entry, attr_mappings['profile_photo'])
97
+ profile_photo_data = profile_photo_data.force_encoding('binary')
98
+
99
+ user_search_response = search_users_resource.get(:params => { :q => email, :per_page => 1 }, :accept => :json)
100
+ user_info = JSON.parse(user_search_response)['users'].first
101
+ if user_info && user_info['avatars'] && user_info['avatars']['is_system_default']
102
+ puts "Uploading photo for #{email}"
103
+
104
+ user_resource = Socialcast::CommandLine.resource_for_path "/api/users/#{user_info['id']}", http_config
105
+ content_type = case profile_photo_data
106
+ when Regexp.new("\AGIF8", nil, 'n')
107
+ 'gif'
108
+ when Regexp.new('\A\x89PNG', nil, 'n')
109
+ 'png'
110
+ when Regexp.new("\A\xff\xd8\xff\xe0\x00\x10JFIF", nil, 'n'), Regexp.new("\A\xff\xd8\xff\xe1(.*){2}Exif", nil, 'n')
111
+ 'jpg'
112
+ else
113
+ puts "Skipping photo for #{email}: unknown image format (supports .gif, .png, .jpg)"
114
+ next
115
+ end
116
+
117
+ tempfile = Tempfile.new(["photo_upload", ".#{content_type}"])
118
+ tempfile.write(profile_photo_data)
119
+ tempfile.rewind
120
+ begin
121
+ user_resource.put({ :user => { :profile_photo => { :data => tempfile } } })
122
+ ensure
123
+ tempfile.unlink
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def dereference_mail(entry, ldap_connection, dn_field, mail_attribute)
133
+ dn = grab(entry, dn_field)
134
+ ldap_connection.search(:base => dn, :scope => Net::LDAP::SearchScope_BaseObject) do |manager_entry|
135
+ return grab(manager_entry, mail_attribute)
136
+ end
137
+ end
138
+
139
+ def build_user_hash_from_mappings(ldap_connection, entry, attr_mappings, perm_mappings)
140
+ user_hash = HashWithIndifferentAccess.new
141
+ primary_attributes = %w{unique_identifier first_name last_name employee_number}
142
+ primary_attributes.each do |attribute|
143
+ next unless attr_mappings.has_key?(attribute)
144
+ user_hash[attribute] = grab(entry, attr_mappings[attribute])
145
+ end
146
+
147
+ contact_attributes = %w{email location cell_phone office_phone}
148
+ user_hash['contact_info'] = {}
149
+ contact_attributes.each do |attribute|
150
+ next unless attr_mappings.has_key?(attribute)
151
+ user_hash['contact_info'][attribute] = grab(entry, attr_mappings[attribute])
152
+ end
153
+
154
+ custom_attributes = attr_mappings.keys - (primary_attributes + contact_attributes)
155
+
156
+ user_hash['custom_fields'] = []
157
+ custom_attributes.each do |attribute|
158
+ if attribute == 'manager'
159
+ user_hash['custom_fields'] << { 'id' => 'manager_email', 'label' => 'manager_email', 'value' => dereference_mail(entry, ldap_connection, attr_mappings[attribute], attr_mappings['email']) }
160
+ else
161
+ user_hash['custom_fields'] << { 'id' => attribute, 'label' => attribute, 'value' => grab(entry, attr_mappings[attribute]) }
162
+ end
163
+ end
164
+
165
+ membership_attribute = perm_mappings.fetch 'attribute_name', 'memberof'
166
+ memberships = entry[membership_attribute]
167
+ external_ldap_groups = Array.wrap(perm_mappings.fetch('account_types', {})['external'])
168
+ if external_ldap_groups.any? { |external_ldap_group| memberships.include?(external_ldap_group) }
169
+ user_hash['account_type'] = 'external'
170
+ else
171
+ user_hash['account_type'] = 'member'
172
+ if permission_roles_mappings = perm_mappings['roles']
173
+ user_hash['roles'] = []
174
+ permission_roles_mappings.each_pair do |socialcast_role, ldap_groups|
175
+ Array.wrap(ldap_groups).each do |ldap_group|
176
+ if memberships.include?(ldap_group)
177
+ user_hash['roles'] << socialcast_role
178
+ break
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ user_hash
186
+ end
187
+
188
+
189
+ def each_ldap_entry(&block)
190
+ count = 0
191
+
192
+ each_ldap_connection do |ldap_connection_name, connection, ldap|
193
+ attr_mappings = attribute_mappings(ldap_connection_name)
194
+ perm_mappings = permission_mappings(ldap_connection_name)
195
+ ldap.search(:return_result => false, :filter => connection["filter"], :base => connection["basedn"], :attributes => ldap_search_attributes(ldap_connection_name)) do |entry|
196
+ if grab(entry, attr_mappings["email"]).present? || (attr_mappings.has_key?("unique_identifier") && grab(entry, attr_mappings["unique_identifier"]).present?)
197
+ yield ldap, entry, attr_mappings, perm_mappings
198
+ end
199
+
200
+ count += 1
201
+ puts "Scanned #{count} users" if ((count % 100) == 0)
202
+ end
203
+ end
204
+ puts "Finished scanning #{count} users"
205
+ end
206
+
207
+
208
+ def each_ldap_connection
209
+ @ldap_config["connections"].each_pair do |ldap_connection_name, connection|
210
+ puts "Connecting to #{ldap_connection_name} at #{[connection["host"], connection["port"]].join(':')}"
211
+ ldap = create_ldap_instance(connection)
212
+ puts "Searching base DN: #{connection["basedn"]} with filter: #{connection["filter"]}"
213
+ yield ldap_connection_name, connection, ldap
214
+ end
215
+ end
216
+
217
+ def create_ldap_instance(connection)
218
+ ldap = Net::LDAP.new :host => connection["host"], :port => connection["port"], :base => connection["basedn"]
219
+ ldap.encryption connection['encryption'].to_sym if connection['encryption']
220
+ ldap.auth connection["username"], connection["password"]
221
+ ldap
222
+ end
223
+
224
+ def ldap_search_attributes(ldap_connection_name)
225
+ attr_mappings = attribute_mappings(ldap_connection_name)
226
+ perm_mappings = permission_mappings(ldap_connection_name)
227
+
228
+ membership_attribute = perm_mappings.fetch 'attribute_name', 'memberof'
229
+ attributes = attr_mappings.values.map do |mapping_value|
230
+ value = begin
231
+ mapping_value.camelize.constantize
232
+ rescue NameError
233
+ mapping_value
234
+ end
235
+
236
+ case value
237
+ when Hash
238
+ dup_mapping_value = value.dup
239
+ dup_mapping_value.delete("value")
240
+ dup_mapping_value.values
241
+ when String
242
+ value
243
+ when Class, Module
244
+ if value.respond_to?(:attributes)
245
+ value.attributes
246
+ else
247
+ mapping_value
248
+ end
249
+ end
250
+ end.flatten
251
+ attributes << membership_attribute
252
+ end
253
+
254
+ def current_socialcast_users(http_config)
255
+ current_socialcast_list = Set.new
256
+ request_params = {:per_page => 500}
257
+ request_params[:page] = 1
258
+ resource = create_socialcast_user_index_request(http_config, request_params)
259
+ loop do
260
+ response = resource.get :accept => :json
261
+ result = JSON.parse(response)
262
+ users = result["users"]
263
+ break if users.blank?
264
+ request_params[:page] += 1
265
+ resource = create_socialcast_user_index_request(http_config, request_params)
266
+ users.each do |user|
267
+ current_socialcast_list << [user['contact_info']['email'], user['company_login'], user['employee_number']]
268
+ end
269
+ end
270
+ current_socialcast_list
271
+ end
272
+
273
+ def create_socialcast_user_index_request(http_config, request_params)
274
+ path_template = "/api/users?per_page=%{per_page}&page=%{page}"
275
+ Socialcast::CommandLine.resource_for_path((path_template % request_params), http_config)
276
+ end
277
+
278
+ def attribute_mappings(connection_name)
279
+ @attribute_mappings ||= {}
280
+
281
+ unless @attribute_mappings[connection_name]
282
+ @attribute_mappings[connection_name] = @ldap_config['connections'][connection_name].fetch 'mappings', nil
283
+ @attribute_mappings[connection_name] ||= @ldap_config.fetch 'mappings', {}
284
+ end
285
+
286
+ @attribute_mappings[connection_name]
287
+ end
288
+
289
+ def permission_mappings(connection_name)
290
+ @permission_mappings ||= {}
291
+
292
+ unless @permission_mappings[connection_name]
293
+ @permission_mappings[connection_name] = @ldap_config['connections'][connection_name].fetch 'permission_mappings', nil
294
+ @permission_mappings[connection_name] ||= @ldap_config.fetch 'permission_mappings', {}
295
+ end
296
+
297
+ @permission_mappings[connection_name]
298
+ end
299
+
300
+ # grab a *single* value of an attribute
301
+ # abstracts away ldap multivalue attributes
302
+ def grab(entry, attribute)
303
+ const_attribute = begin
304
+ attribute.camelize.constantize
305
+ rescue NameError
306
+ attribute
307
+ end
308
+
309
+ case const_attribute
310
+ when Hash
311
+ dup_attribute = const_attribute.dup
312
+ value = dup_attribute.delete("value")
313
+ sprintf value, Hash[dup_attribute.map { |k, v| [k, grab(entry, v)] }].symbolize_keys
314
+ when String
315
+ normalize_ldap_value(entry, attribute)
316
+ when Class, Module
317
+ if const_attribute.respond_to?(:run)
318
+ const_attribute.run(entry)
319
+ else
320
+ normalize_ldap_value(entry, attribute)
321
+ end
322
+ end
323
+ end
324
+
325
+ def normalize_ldap_value(entry, attribute)
326
+ Array.wrap(entry[attribute]).compact.first
327
+ end
328
+ end
329
+ end
330
+ end