socialcast 1.2.3 → 1.3.0
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 +8 -8
- data/.travis.yml +1 -2
- data/bin/socialcast +2 -2
- data/lib/socialcast.rb +7 -15
- data/lib/socialcast/command_line/cli.rb +161 -0
- data/lib/socialcast/command_line/message.rb +18 -0
- data/lib/socialcast/command_line/provision.rb +330 -0
- data/lib/socialcast/command_line/version.rb +5 -0
- data/socialcast.gemspec +13 -10
- data/spec/fixtures/fake_attribute_map.rb +8 -6
- data/spec/fixtures/ldap_with_account_type_without_roles.yml +44 -0
- data/spec/fixtures/ldap_with_class_ldap_attribute.yml +47 -0
- data/spec/fixtures/ldap_with_connection_mapping.yml +52 -0
- data/spec/fixtures/ldap_with_connection_permission_mapping.yml +59 -0
- data/spec/fixtures/ldap_with_custom_attributes.yml +50 -0
- data/spec/fixtures/ldap_with_manager_attribute.yml +6 -3
- data/spec/fixtures/ldap_with_multiple_connection_mappings.yml +51 -0
- data/spec/fixtures/ldap_with_multiple_connection_permission_mappings.yml +75 -0
- data/spec/fixtures/ldap_with_plugin_mapping.yml +1 -1
- data/spec/fixtures/ldap_with_roles_without_account_type.yml +48 -0
- data/spec/fixtures/ldap_with_unique_identifier.yml +50 -0
- data/spec/fixtures/ldap_without_account_type_or_roles.yml +42 -0
- data/spec/{cli_spec.rb → socialcast/command_line/cli_spec.rb} +94 -82
- data/spec/socialcast/command_line/provision_spec.rb +497 -0
- data/spec/spec_helper.rb +2 -1
- metadata +103 -26
- data/lib/ext/array_ext.rb +0 -11
- data/lib/ext/string_ext.rb +0 -13
- data/lib/socialcast/cli.rb +0 -339
- data/lib/socialcast/message.rb +0 -17
- data/lib/socialcast/net_ldap_ext.rb +0 -87
- data/lib/socialcast/version.rb +0 -3
- 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
|
-
|
|
4
|
+
NGRhZWZlNmVkZGY1ODEwZTcyNTJkMmYyZmE4ZjQ3Yzc4ZWIwNDIyNw==
|
|
5
5
|
data.tar.gz: !binary |-
|
|
6
|
-
|
|
6
|
+
MDMzZGZiZjdmN2NkOTk5NzZjYTU2MzY3YzIxZDQxZWNkMmQxNWMwNw==
|
|
7
7
|
SHA512:
|
|
8
8
|
metadata.gz: !binary |-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
MmEzZWUwYjZlZDk1ZGM3NWRkYjNlYWRiMWU3NGIxZTg3ZWEzOTg2NGRiZGFk
|
|
10
|
+
NDk4MGQ0NWI1OGNmZWMzYTEwZWJmZDg0NjI3OGRlZmViNmI0YjAxNjQ2NGU4
|
|
11
|
+
M2I1NmZmYTllYWRlMjkwZmY2YTY2ZjgyZDdmZTYwYjY3ZTFhNzI=
|
|
12
12
|
data.tar.gz: !binary |-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
MTJmNDA3MTQ1ZDdkMDhhNDMyOWNiNjcyZTRhN2UzOWU2YjEzMDJmY2RiNDk3
|
|
14
|
+
YzZjMzMzNDdmYWYxMzllMmM0MGUxZTFiNTRiNzViZDVlZGRmZTQ1YjA4ZmRm
|
|
15
|
+
YjJiMzVjMzlmOGVlNDEzYzEzYTNhMTlkYzg5MGVlMmEwZTkyMTk=
|
data/.travis.yml
CHANGED
data/bin/socialcast
CHANGED
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
|
-
|
|
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(
|
|
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
|