gooddata 0.6.53 → 0.6.54
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 +5 -5
- data/.flayignore +6 -0
- data/.gitignore +1 -0
- data/.pronto.yml +3 -0
- data/.rspec +2 -0
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +18 -0
- data/CONTRIBUTING.md +14 -1
- data/DEPENDENCIES.md +324 -253
- data/Dockerfile.jruby +5 -7
- data/Dockerfile.ruby +8 -8
- data/Rakefile +24 -0
- data/ci.rake +47 -0
- data/docker-compose.yml +34 -0
- data/gooddata.gemspec +8 -2
- data/lib/gooddata/bricks/middleware/restforce_middleware.rb +0 -3
- data/lib/gooddata/helpers/data_helper.rb +10 -7
- data/lib/gooddata/helpers/global_helpers_params.rb +8 -3
- data/lib/gooddata/lcm/actions/apply_custom_maql.rb +2 -1
- data/lib/gooddata/lcm/actions/associate_clients.rb +10 -1
- data/lib/gooddata/lcm/actions/collect_client_projects.rb +78 -0
- data/lib/gooddata/lcm/actions/collect_clients.rb +20 -6
- data/lib/gooddata/lcm/actions/collect_data_product.rb +62 -0
- data/lib/gooddata/lcm/actions/collect_dynamic_schedule_params.rb +62 -0
- data/lib/gooddata/lcm/actions/{collect_attrs.rb → collect_ldm_objects.rb} +3 -3
- data/lib/gooddata/lcm/actions/collect_meta.rb +6 -3
- data/lib/gooddata/lcm/actions/collect_segment_clients.rb +2 -1
- data/lib/gooddata/lcm/actions/collect_segments.rb +6 -7
- data/lib/gooddata/lcm/actions/collect_tagged_objects.rb +7 -4
- data/lib/gooddata/lcm/actions/create_segment_masters.rb +7 -3
- data/lib/gooddata/lcm/actions/ensure_data_product.rb +53 -0
- data/lib/gooddata/lcm/actions/ensure_technical_users_domain.rb +6 -2
- data/lib/gooddata/lcm/actions/ensure_technical_users_project.rb +30 -18
- data/lib/gooddata/lcm/actions/execute_schedules.rb +128 -0
- data/lib/gooddata/lcm/actions/provision_clients.rb +32 -21
- data/lib/gooddata/lcm/actions/purge_clients.rb +25 -39
- data/lib/gooddata/lcm/actions/rename_existing_client_projects.rb +70 -0
- data/lib/gooddata/lcm/actions/segments_filter.rb +6 -0
- data/lib/gooddata/lcm/actions/synchronize_cas.rb +11 -0
- data/lib/gooddata/lcm/actions/synchronize_clients.rb +2 -1
- data/lib/gooddata/lcm/actions/synchronize_etls_in_segment.rb +34 -15
- data/lib/gooddata/lcm/actions/synchronize_ldm.rb +10 -1
- data/lib/gooddata/lcm/actions/synchronize_new_segments.rb +2 -1
- data/lib/gooddata/lcm/actions/synchronize_processes.rb +4 -7
- data/lib/gooddata/lcm/actions/synchronize_tag_objects.rb +8 -5
- data/lib/gooddata/lcm/actions/synchronize_user_filters.rb +224 -0
- data/lib/gooddata/lcm/actions/synchronize_user_groups.rb +53 -0
- data/lib/gooddata/lcm/actions/synchronize_users.rb +324 -0
- data/lib/gooddata/lcm/dsl/type_dsl.rb +1 -0
- data/lib/gooddata/lcm/helpers/check_helper.rb +4 -0
- data/lib/gooddata/lcm/helpers/tags_helper.rb +4 -3
- data/lib/gooddata/lcm/lcm2.rb +33 -1
- data/lib/gooddata/lcm/types/complex/segment.rb +3 -0
- data/lib/gooddata/lcm/types/complex/update_preference.rb +8 -2
- data/lib/gooddata/lcm/types/special/array.rb +1 -3
- data/lib/gooddata/lcm/types/special/enum.rb +1 -3
- data/lib/gooddata/mixins/md_id_to_uri.rb +0 -1
- data/lib/gooddata/mixins/md_json.rb +2 -2
- data/lib/gooddata/models/blueprint/project_blueprint.rb +15 -0
- data/lib/gooddata/models/blueprint/to_wire.rb +1 -0
- data/lib/gooddata/models/client.rb +21 -9
- data/lib/gooddata/models/data_product.rb +149 -0
- data/lib/gooddata/models/domain.rb +26 -72
- data/lib/gooddata/models/from_wire.rb +2 -0
- data/lib/gooddata/models/metadata/report.rb +9 -3
- data/lib/gooddata/models/metadata/report_definition.rb +2 -2
- data/lib/gooddata/models/model.rb +1 -1
- data/lib/gooddata/models/process.rb +4 -0
- data/lib/gooddata/models/project.rb +58 -35
- data/lib/gooddata/models/project_creator.rb +13 -0
- data/lib/gooddata/models/segment.rb +63 -16
- data/lib/gooddata/models/style_setting.rb +2 -15
- data/lib/gooddata/models/user_group.rb +2 -0
- data/lib/gooddata/rest/connection.rb +32 -9
- data/lib/gooddata/rest/object_factory.rb +0 -25
- data/lib/gooddata/version.rb +1 -1
- data/spec/data/blueprints/invalid_blueprint.json +2 -2
- data/spec/data/blueprints/test_project_model_spec.json +1 -1
- data/spec/data/dynamic_schedule_params_table.csv +7 -0
- data/spec/data/workspace_table.csv +3 -3
- data/spec/environment/staging.rb +3 -3
- data/spec/integration/ads_output_stage_spec.rb +0 -10
- data/spec/integration/clients_spec.rb +1 -1
- data/spec/{unit → integration}/commands/command_projects_spec.rb +0 -0
- data/spec/{unit → integration}/core/connection_spec.rb +0 -0
- data/spec/{unit → integration}/core/logging_spec.rb +0 -0
- data/spec/{unit → integration}/core/project_spec.rb +0 -0
- data/spec/integration/date_dim_switch_spec.rb +13 -0
- data/spec/integration/full_process_schedule_spec.rb +2 -2
- data/spec/integration/helpers_spec.rb +16 -0
- data/spec/integration/lcm_spec.rb +12 -2
- data/spec/integration/mixins/id_to_uri_spec.rb +44 -0
- data/spec/integration/models/data_product_spec.rb +71 -0
- data/spec/{unit → integration}/models/domain_spec.rb +2 -2
- data/spec/{unit → integration}/models/invitation_spec.rb +0 -0
- data/spec/{unit → integration}/models/membership_spec.rb +0 -0
- data/spec/{unit → integration}/models/params_spec.rb +0 -0
- data/spec/{unit → integration}/models/profile_spec.rb +0 -0
- data/spec/{unit → integration}/models/project_role_spec.rb +0 -0
- data/spec/integration/models/project_spec.rb +225 -0
- data/spec/{unit → integration}/models/schedule_spec.rb +0 -0
- data/spec/{unit → integration}/models/unit_project_spec.rb +0 -0
- data/spec/integration/project_spec.rb +40 -5
- data/spec/integration/segments_spec.rb +27 -26
- data/spec/integration/user_filters_spec.rb +1 -1
- data/spec/spec_helper.rb +15 -19
- data/spec/unit/actions/associate_clients_spec.rb +47 -0
- data/spec/unit/actions/collect_client_projects_spec.rb +47 -0
- data/spec/unit/actions/collect_clients_spec.rb +27 -0
- data/spec/unit/actions/collect_data_product_spec.rb +64 -0
- data/spec/unit/actions/collect_dynamic_schedule_params_spec.rb +56 -0
- data/spec/unit/actions/collect_meta_spec.rb +4 -4
- data/spec/unit/actions/collect_segment_clients_spec.rb +44 -3
- data/spec/unit/actions/collect_tagged_objects_spec.rb +20 -4
- data/spec/unit/actions/create_segment_masters_spec.rb +64 -0
- data/spec/unit/actions/ensure_data_product_spec.rb +38 -0
- data/spec/unit/actions/ensure_technical_users_domain_spec.rb +51 -0
- data/spec/unit/actions/ensure_technical_users_project_spec.rb +72 -0
- data/spec/unit/actions/execute_schedules_spec.rb +94 -0
- data/spec/unit/actions/provision_clients_spec.rb +45 -0
- data/spec/unit/actions/purge_clients_spec.rb +47 -0
- data/spec/unit/actions/rename_existing_client_projects_spec.rb +54 -0
- data/spec/unit/actions/segments_filter_spec.rb +46 -0
- data/spec/unit/actions/shared_examples_for_user_actions.rb +10 -0
- data/spec/unit/actions/synchronize_cas_spec.rb +58 -0
- data/spec/unit/actions/synchronize_etls_in_segment_spec.rb +174 -13
- data/spec/unit/actions/synchronize_ldm_spec.rb +57 -0
- data/spec/unit/actions/synchronize_user_filters_spec.rb +142 -0
- data/spec/unit/actions/synchronize_user_groups_spec.rb +49 -0
- data/spec/unit/actions/synchronize_users_spec.rb +76 -0
- data/spec/unit/helpers/data_helper_spec.rb +17 -0
- data/spec/unit/helpers/global_helpers_spec.rb +16 -0
- data/spec/unit/helpers_spec.rb +0 -6
- data/spec/unit/models/blueprint/project_blueprint_spec.rb +21 -4
- data/spec/unit/models/project_creator_spec.rb +16 -0
- data/spec/unit/models/project_spec.rb +66 -197
- metadata +202 -100
- data/PULL_REQUEST_TEMPLATE.md +0 -5
- data/lib/gooddata/bricks/middleware/params_inspect_middleware.rb +0 -21
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2010-2017 GoodData Corporation. All rights reserved.
|
|
4
|
+
# This source code is licensed under the BSD-style license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
|
|
7
|
+
require_relative 'base_action'
|
|
8
|
+
|
|
9
|
+
module GoodData
|
|
10
|
+
module LCM2
|
|
11
|
+
class SynchronizeUserFilters < BaseAction
|
|
12
|
+
DESCRIPTION = 'Synchronizes User Permissions Between Projects'
|
|
13
|
+
|
|
14
|
+
PARAMS = define_params(self) do
|
|
15
|
+
description 'Client Used For Connecting To GD'
|
|
16
|
+
param :gdc_gd_client, instance_of(Type::GdClientType), required: true
|
|
17
|
+
|
|
18
|
+
description 'Input Source'
|
|
19
|
+
param :input_source, instance_of(Type::HashType), required: true
|
|
20
|
+
|
|
21
|
+
description 'Synchronization Mode (e.g. sync_one_project_based_on_pid)'
|
|
22
|
+
param :sync_mode, instance_of(Type::StringType), required: false
|
|
23
|
+
|
|
24
|
+
description 'Column That Contains Target Project IDs'
|
|
25
|
+
param :multiple_projects_column, instance_of(Type::StringType), required: false
|
|
26
|
+
|
|
27
|
+
description 'Filters Config'
|
|
28
|
+
param :filters_config, instance_of(Type::HashType), required: true
|
|
29
|
+
|
|
30
|
+
description 'Input Source Contains CSV Headers?'
|
|
31
|
+
param :csv_headers, instance_of(Type::StringType), required: false
|
|
32
|
+
|
|
33
|
+
description 'Restrict If Missing Values In Input Source'
|
|
34
|
+
param :restrict_if_missing_all_values, instance_of(Type::StringType), required: false
|
|
35
|
+
|
|
36
|
+
description 'Ignore Missing Values In Input Source'
|
|
37
|
+
param :ignore_missing_values, instance_of(Type::StringType), required: false
|
|
38
|
+
|
|
39
|
+
description 'Do Not Touch Filters That Are Not Mentioned'
|
|
40
|
+
param :do_not_touch_filters_that_are_not_mentioned, instance_of(Type::StringType), required: false
|
|
41
|
+
|
|
42
|
+
description 'Restricts synchronization to specified segments'
|
|
43
|
+
param :segments_filter, array_of(instance_of(Type::StringType)), required: false
|
|
44
|
+
|
|
45
|
+
# gdc_project/gdc_project_id, required: true
|
|
46
|
+
# organization/domain, required: true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
def call(params)
|
|
51
|
+
client = params.gdc_gd_client
|
|
52
|
+
domain_name = params.organization || params.domain
|
|
53
|
+
domain = client.domain(domain_name) if domain_name
|
|
54
|
+
project = client.projects(params.gdc_project) || client.projects(params.gdc_project_id)
|
|
55
|
+
data_product = params.data_product
|
|
56
|
+
|
|
57
|
+
data_source = GoodData::Helpers::DataSource.new(params.input_source)
|
|
58
|
+
|
|
59
|
+
config = params.filters_config
|
|
60
|
+
fail 'User filters brick requires configuration how the filter should be setup. For this use the param "filters_config"' if config.blank?
|
|
61
|
+
symbolized_config = GoodData::Helpers.deep_dup(config)
|
|
62
|
+
symbolized_config = GoodData::Helpers.symbolize_keys(symbolized_config)
|
|
63
|
+
symbolized_config[:labels] = symbolized_config[:labels].map { |l| GoodData::Helpers.symbolize_keys(l) }
|
|
64
|
+
headers_in_options = params.csv_headers == 'false' || true
|
|
65
|
+
|
|
66
|
+
mode = params.sync_mode || 'sync_project'
|
|
67
|
+
filters = []
|
|
68
|
+
|
|
69
|
+
csv_with_headers = if GoodData::UserFilterBuilder.row_based?(symbolized_config)
|
|
70
|
+
false
|
|
71
|
+
else
|
|
72
|
+
headers_in_options
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
multiple_projects_column = params.multiple_projects_column
|
|
76
|
+
unless multiple_projects_column
|
|
77
|
+
client_modes = %w(sync_domain_client_workspaces sync_one_project_based_on_custom_id sync_multiple_projects_based_on_custom_id)
|
|
78
|
+
multiple_projects_column = if client_modes.include?(mode)
|
|
79
|
+
'client_id'
|
|
80
|
+
else
|
|
81
|
+
'project_id'
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
run_params = {
|
|
86
|
+
restrict_if_missing_all_values: params.restrict_if_missing_all_values == 'true',
|
|
87
|
+
ignore_missing_values: params.ignore_missing_values == 'true',
|
|
88
|
+
do_not_touch_filters_that_are_not_mentioned: params.do_not_touch_filters_that_are_not_mentioned == 'true',
|
|
89
|
+
domain: domain,
|
|
90
|
+
dry_run: false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
puts "Synchronizing in mode \"#{mode}\""
|
|
94
|
+
case mode
|
|
95
|
+
when 'sync_project'
|
|
96
|
+
CSV.foreach(File.open(data_source.realize(params), 'r:UTF-8'), headers: csv_with_headers, return_headers: false, encoding: 'utf-8') do |row|
|
|
97
|
+
filters << row
|
|
98
|
+
end
|
|
99
|
+
filters_to_load = GoodData::UserFilterBuilder.get_filters(filters, symbolized_config)
|
|
100
|
+
puts "Synchronizing #{filters_to_load.count} filters"
|
|
101
|
+
project.add_data_permissions(filters_to_load, run_params)
|
|
102
|
+
when 'sync_one_project_based_on_pid'
|
|
103
|
+
CSV.foreach(File.open(data_source.realize(params), 'r:UTF-8'), headers: csv_with_headers, return_headers: false, encoding: 'utf-8') do |row|
|
|
104
|
+
filters << row if row[multiple_projects_column] == project.pid
|
|
105
|
+
end
|
|
106
|
+
filters_to_load = GoodData::UserFilterBuilder.get_filters(filters, symbolized_config)
|
|
107
|
+
puts "Synchronizing #{filters_to_load.count} filters"
|
|
108
|
+
project.add_data_permissions(filters_to_load, run_params)
|
|
109
|
+
when 'sync_multiple_projects_based_on_pid'
|
|
110
|
+
CSV.foreach(File.open(data_source.realize(params), 'r:UTF-8'), headers: csv_with_headers, return_headers: false, encoding: 'utf-8') do |row|
|
|
111
|
+
filters << row.to_hash
|
|
112
|
+
end
|
|
113
|
+
filters.group_by { |u| u[multiple_projects_column] }.flat_map do |project_id, new_filters|
|
|
114
|
+
fail "Project id cannot be empty" if project_id.blank?
|
|
115
|
+
project = client.projects(project_id)
|
|
116
|
+
filters_to_load = GoodData::UserFilterBuilder.get_filters(new_filters, symbolized_config)
|
|
117
|
+
puts "Synchronizing #{filters_to_load.count} filters in project #{project.pid}"
|
|
118
|
+
project.add_data_permissions(filters_to_load, run_params)
|
|
119
|
+
end
|
|
120
|
+
when 'sync_one_project_based_on_custom_id'
|
|
121
|
+
md = project.metadata
|
|
122
|
+
goodot_id = md['GOODOT_CUSTOM_PROJECT_ID'].to_s
|
|
123
|
+
|
|
124
|
+
client = domain.clients(:all, data_product).find { |c| c.project_uri == project.uri }
|
|
125
|
+
if goodot_id.empty? && client.nil?
|
|
126
|
+
fail "Project \"#{project.pid}\" metadata does not contain key GOODOT_CUSTOM_PROJECT_ID neither is it mapped \
|
|
127
|
+
to a client_id in LCM metadata. We are unable to get the values for user filters."
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
unless goodot_id.empty? || client.nil? || (goodot_id == client.id)
|
|
131
|
+
fail "GOODOT_CUSTOM_PROJECT_ID metadata key is provided for project \"#{project.pid}\" but doesn't match \
|
|
132
|
+
client id assigned to the project in LCM metadata. Please resolve the conflict."
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
filter_value = goodot_id.empty? ? client.id : goodot_id
|
|
136
|
+
|
|
137
|
+
filepath = File.open(data_source.realize(params), 'r:UTF-8')
|
|
138
|
+
CSV.foreach(filepath, headers: csv_with_headers, return_headers: false, encoding: 'utf-8') do |row|
|
|
139
|
+
client_id = row[multiple_projects_column].to_s
|
|
140
|
+
filters << row if client_id == filter_value
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
if filters.empty?
|
|
144
|
+
params.gdc_logger.warn "Project \"#{project.pid}\" does not match with any client ids in input source (both GOODOT_CUSTOM_PROJECT_ID and SEGMENT/CLIENT). \
|
|
145
|
+
Unable to get the value to filter users."
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
filters_to_load = GoodData::UserFilterBuilder.get_filters(filters, symbolized_config)
|
|
149
|
+
puts "Synchronizing #{filters_to_load.count} filters"
|
|
150
|
+
project.add_data_permissions(filters_to_load, run_params)
|
|
151
|
+
when 'sync_multiple_projects_based_on_custom_id'
|
|
152
|
+
CSV.foreach(File.open(data_source.realize(params), 'r:UTF-8'), headers: csv_with_headers, return_headers: false, encoding: 'utf-8') do |row|
|
|
153
|
+
filters << row.to_hash
|
|
154
|
+
end
|
|
155
|
+
filters.group_by { |u| u[multiple_projects_column] }.flat_map do |client_id, new_filters|
|
|
156
|
+
fail "Client id cannot be empty" if client_id.blank?
|
|
157
|
+
project = domain.clients(client_id, data_product).project
|
|
158
|
+
fail "Client #{client_id} does not have project." unless project
|
|
159
|
+
filters_to_load = GoodData::UserFilterBuilder.get_filters(new_filters, symbolized_config)
|
|
160
|
+
puts "Synchronizing #{filters_to_load.count} filters in project #{project.pid} of client #{client_id}"
|
|
161
|
+
project.add_data_permissions(filters_to_load, run_params)
|
|
162
|
+
end
|
|
163
|
+
when 'sync_domain_client_workspaces'
|
|
164
|
+
CSV.foreach(File.open(data_source.realize(params), 'r:UTF-8'), headers: csv_with_headers, return_headers: false, encoding: 'utf-8') do |row|
|
|
165
|
+
filters << row.to_hash
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
domain_clients = domain.clients(:all, data_product)
|
|
169
|
+
params.segments_filter ||= params.segments_filter
|
|
170
|
+
if params.segments_filter
|
|
171
|
+
segments_filter = params.segments_filter.map { |seg| "/gdc/domains/#{domain.name}/segments/#{seg}" }
|
|
172
|
+
domain_clients.select! { |c| segments_filter.include?(c.segment_uri) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
working_client_ids = []
|
|
176
|
+
|
|
177
|
+
filters.group_by { |u| u[multiple_projects_column] }.flat_map do |client_id, new_filters|
|
|
178
|
+
fail "Client id cannot be empty" if client_id.blank?
|
|
179
|
+
c = domain.clients(client_id, data_product)
|
|
180
|
+
if params.segments_filter && !segments_filter.include?(c.segment_uri)
|
|
181
|
+
puts "Client #{client_id} is outside segments_filter #{params.segments_filter}"
|
|
182
|
+
next
|
|
183
|
+
end
|
|
184
|
+
project = c.project
|
|
185
|
+
fail "Client #{client_id} does not have project." unless project
|
|
186
|
+
working_client_ids << client_id
|
|
187
|
+
filters_to_load = GoodData::UserFilterBuilder.get_filters(new_filters, symbolized_config)
|
|
188
|
+
puts "Synchronizing #{filters_to_load.count} filters in project #{project.pid} of client #{client_id}"
|
|
189
|
+
project.add_data_permissions(filters_to_load, run_params)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
results = []
|
|
193
|
+
unless run_params[:do_not_touch_filters_that_are_not_mentioned]
|
|
194
|
+
domain_clients.each do |c|
|
|
195
|
+
next if working_client_ids.include?(c.client_id)
|
|
196
|
+
begin
|
|
197
|
+
project = c.project
|
|
198
|
+
rescue => e
|
|
199
|
+
puts "Error when accessing project of client #{c.client_id}. Error: #{e}"
|
|
200
|
+
next
|
|
201
|
+
end
|
|
202
|
+
unless project
|
|
203
|
+
puts "Client #{c.client_id} has no project."
|
|
204
|
+
next
|
|
205
|
+
end
|
|
206
|
+
if project.deleted?
|
|
207
|
+
puts "Project #{project.pid} of client #{c.client_id} is deleted."
|
|
208
|
+
next
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
puts "Delete all filters in project #{project.pid} of client #{c.client_id}"
|
|
212
|
+
results << project.add_data_permissions([], run_params)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
{
|
|
217
|
+
results: results
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2010-2017 GoodData Corporation. All rights reserved.
|
|
4
|
+
# This source code is licensed under the BSD-style license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
|
|
7
|
+
require_relative 'base_action'
|
|
8
|
+
require 'thread_safe'
|
|
9
|
+
|
|
10
|
+
module GoodData
|
|
11
|
+
module LCM2
|
|
12
|
+
class SynchronizeUserGroups < BaseAction
|
|
13
|
+
DESCRIPTION = 'Synchronize User Groups'
|
|
14
|
+
|
|
15
|
+
PARAMS = define_params(self) do
|
|
16
|
+
description 'Client Used for Connecting to GD'
|
|
17
|
+
param :gdc_gd_client, instance_of(Type::GdClientType), required: true
|
|
18
|
+
|
|
19
|
+
description 'Development Client Used for Connecting to GD'
|
|
20
|
+
param :development_client, instance_of(Type::GdClientType), required: true
|
|
21
|
+
|
|
22
|
+
description 'Synchronization Info'
|
|
23
|
+
param :synchronize, array_of(instance_of(Type::SynchronizationInfoType)), required: true, generated: true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def call(params)
|
|
28
|
+
results = ThreadSafe::Array.new
|
|
29
|
+
|
|
30
|
+
client = params.gdc_gd_client
|
|
31
|
+
development_client = params.development_client
|
|
32
|
+
|
|
33
|
+
params.synchronize.peach do |info|
|
|
34
|
+
from_project = info.from
|
|
35
|
+
to_projects = info.to
|
|
36
|
+
|
|
37
|
+
from = development_client.projects(from_project) || fail("Invalid 'from' project specified - '#{from_project}'")
|
|
38
|
+
|
|
39
|
+
to_projects.peach do |entry|
|
|
40
|
+
pid = entry[:pid]
|
|
41
|
+
to_project = client.projects(pid) || fail("Invalid 'to' project specified - '#{pid}'")
|
|
42
|
+
|
|
43
|
+
params.gdc_logger.info "Transferring User Groups, from project: '#{from.title}', PID: '#{from.pid}', to project: '#{to_project.title}', PID: '#{to_project.pid}'"
|
|
44
|
+
results += GoodData::Project.transfer_user_groups(from, to_project)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
results.uniq
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2010-2017 GoodData Corporation. All rights reserved.
|
|
4
|
+
# This source code is licensed under the BSD-style license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
|
|
7
|
+
require_relative 'base_action'
|
|
8
|
+
|
|
9
|
+
module GoodData
|
|
10
|
+
module LCM2
|
|
11
|
+
class SynchronizeUsers < BaseAction
|
|
12
|
+
DESCRIPTION = 'Synchronizes Users Between Projects'
|
|
13
|
+
|
|
14
|
+
PARAMS = define_params(self) do
|
|
15
|
+
description 'Client Used For Connecting To GD'
|
|
16
|
+
param :gdc_gd_client, instance_of(Type::GdClientType), required: true
|
|
17
|
+
|
|
18
|
+
description 'Input Source'
|
|
19
|
+
param :input_source, instance_of(Type::HashType), required: true
|
|
20
|
+
|
|
21
|
+
description 'Synchronization Mode (e.g. sync_one_project_based_on_pid)'
|
|
22
|
+
param :sync_mode, instance_of(Type::StringType), required: false, default: 'sync_domain_and_project'
|
|
23
|
+
|
|
24
|
+
description 'Column That Contains Target Project IDs'
|
|
25
|
+
param :multiple_projects_column, instance_of(Type::StringType), required: false
|
|
26
|
+
|
|
27
|
+
# gdc_project/gdc_project_id, required: true
|
|
28
|
+
# organization/domain, required: true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
MODES = %w(
|
|
33
|
+
add_to_organization
|
|
34
|
+
sync_project
|
|
35
|
+
sync_domain_and_project
|
|
36
|
+
sync_multiple_projects_based_on_pid
|
|
37
|
+
sync_one_project_based_on_pid
|
|
38
|
+
sync_one_project_based_on_custom_id
|
|
39
|
+
sync_multiple_projects_based_on_custom_id
|
|
40
|
+
sync_domain_client_workspaces
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def version
|
|
44
|
+
'0.0.1'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def call(params)
|
|
48
|
+
client = params.gdc_gd_client
|
|
49
|
+
domain_name = params.organization || params.domain
|
|
50
|
+
project = client.projects(params.gdc_project) || client.projects(params.gdc_project_id)
|
|
51
|
+
data_source = GoodData::Helpers::DataSource.new(params.input_source)
|
|
52
|
+
data_product = params.data_product
|
|
53
|
+
mode = params.sync_mode
|
|
54
|
+
unless mode.nil? || MODES.include?(mode)
|
|
55
|
+
fail "The parameter \"sync_mode\" has to have one of the values #{MODES.map(&:to_s).join(', ')} or has to be empty."
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
whitelists = Set.new(params.whitelists || []) + Set.new((params.regexp_whitelists || []).map { |r| /#{r}/ }) + Set.new([client.user.login])
|
|
59
|
+
|
|
60
|
+
[domain_name, data_source].each do |param|
|
|
61
|
+
fail param + ' is required in the block parameters.' unless param
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
domain = client.domain(domain_name)
|
|
65
|
+
|
|
66
|
+
ignore_failures = GoodData::Helpers.to_boolean(params.ignore_failures)
|
|
67
|
+
remove_users_from_project = GoodData::Helpers.to_boolean(params.remove_users_from_project)
|
|
68
|
+
do_not_touch_users_that_are_not_mentioned = GoodData::Helpers.to_boolean(params.do_not_touch_users_that_are_not_mentioned)
|
|
69
|
+
create_non_existing_user_groups = GoodData::Helpers.to_boolean(params.create_non_existing_user_groups || true)
|
|
70
|
+
|
|
71
|
+
new_users = load_data(params, data_source).compact
|
|
72
|
+
|
|
73
|
+
# There are several scenarios we want to provide with this brick
|
|
74
|
+
# 1) Sync only domain
|
|
75
|
+
# 2) Sync both domain and project
|
|
76
|
+
# 3) Sync multiple projects. Sync them by using one file. The file has to
|
|
77
|
+
# contain additional column that contains the PID of the project so the
|
|
78
|
+
# process can partition the users correctly. The column is configurable
|
|
79
|
+
# 4) Sync one project the users are filtered based on a column in the data
|
|
80
|
+
# that should contain pid of the project
|
|
81
|
+
# 5) Sync one project. The users are filtered form a given file based on the
|
|
82
|
+
# value in the file. The value is compared against the value
|
|
83
|
+
# GOODOT_CUSTOM_PROJECT_ID that is saved in project metadata. This is
|
|
84
|
+
# aiming at solving the problem that the customer cannot give us the
|
|
85
|
+
# value of a project id in the data since he does not know it upfront
|
|
86
|
+
# and we cannot influence its value.
|
|
87
|
+
results = case mode
|
|
88
|
+
when 'add_to_organization'
|
|
89
|
+
domain.create_users(new_users.uniq { |u| u[:login] || u[:email] })
|
|
90
|
+
when 'sync_project'
|
|
91
|
+
project.import_users(new_users,
|
|
92
|
+
domain: domain,
|
|
93
|
+
whitelists: whitelists,
|
|
94
|
+
ignore_failures: ignore_failures,
|
|
95
|
+
remove_users_from_project: remove_users_from_project,
|
|
96
|
+
do_not_touch_users_that_are_not_mentioned: do_not_touch_users_that_are_not_mentioned,
|
|
97
|
+
create_non_existing_user_groups: create_non_existing_user_groups)
|
|
98
|
+
when 'sync_multiple_projects_based_on_pid'
|
|
99
|
+
new_users.group_by { |u| u[:pid] }.flat_map do |project_id, users|
|
|
100
|
+
begin
|
|
101
|
+
project = client.projects(project_id)
|
|
102
|
+
fail "You (user executing the script - #{client.user.login}) is not admin in project \"#{project_id}\"." unless project.am_i_admin?
|
|
103
|
+
project.import_users(users,
|
|
104
|
+
domain: domain,
|
|
105
|
+
whitelists: whitelists,
|
|
106
|
+
ignore_failures: ignore_failures,
|
|
107
|
+
remove_users_from_project: remove_users_from_project,
|
|
108
|
+
do_not_touch_users_that_are_not_mentioned: do_not_touch_users_that_are_not_mentioned,
|
|
109
|
+
create_non_existing_user_groups: create_non_existing_user_groups)
|
|
110
|
+
rescue RestClient::ResourceNotFound
|
|
111
|
+
fail "Project \"#{project_id}\" was not found. Please check your project ids in the source file"
|
|
112
|
+
rescue RestClient::Gone
|
|
113
|
+
fail "Seems like you (user executing the script - #{client.user.login}) do not have access to project \"#{project_id}\""
|
|
114
|
+
rescue RestClient::Forbidden
|
|
115
|
+
fail "User #{client.user.login} is not enabled within project \"#{project_id}\""
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
when 'sync_one_project_based_on_pid'
|
|
119
|
+
filtered_users = new_users.select { |u| u[:pid] == project.pid }
|
|
120
|
+
project.import_users(filtered_users,
|
|
121
|
+
domain: domain,
|
|
122
|
+
whitelists: whitelists,
|
|
123
|
+
ignore_failures: ignore_failures,
|
|
124
|
+
remove_users_from_project: remove_users_from_project,
|
|
125
|
+
do_not_touch_users_that_are_not_mentioned: do_not_touch_users_that_are_not_mentioned,
|
|
126
|
+
create_non_existing_user_groups: create_non_existing_user_groups)
|
|
127
|
+
when 'sync_one_project_based_on_custom_id'
|
|
128
|
+
md = project.metadata
|
|
129
|
+
goodot_id = md['GOODOT_CUSTOM_PROJECT_ID'].to_s
|
|
130
|
+
|
|
131
|
+
filtered_users = new_users.select do |u|
|
|
132
|
+
fail "Column for determining the project assignement is empty for \"#{u[:login]}\"" if u[:pid].blank?
|
|
133
|
+
client_id = u[:pid].to_s
|
|
134
|
+
(goodot_id && client_id == goodot_id) || domain.clients(client_id, data_product).project_uri == project.uri
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if filtered_users.empty?
|
|
138
|
+
fail "Project \"#{project.pid}\" does not match with any client ids in input source (both GOODOT_CUSTOM_PROJECT_ID and SEGMENT/CLIENT). \
|
|
139
|
+
We are unable to get the value to filter users."
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
puts "Project #{project.pid} will receive #{filtered_users.count} from #{new_users.count} users"
|
|
143
|
+
project.import_users(filtered_users,
|
|
144
|
+
domain: domain,
|
|
145
|
+
whitelists: whitelists,
|
|
146
|
+
ignore_failures: ignore_failures,
|
|
147
|
+
remove_users_from_project: remove_users_from_project,
|
|
148
|
+
do_not_touch_users_that_are_not_mentioned: do_not_touch_users_that_are_not_mentioned,
|
|
149
|
+
create_non_existing_user_groups: create_non_existing_user_groups)
|
|
150
|
+
when 'sync_multiple_projects_based_on_custom_id'
|
|
151
|
+
new_users.group_by { |u| u[:pid] }.flat_map do |client_id, users|
|
|
152
|
+
fail "Client id cannot be empty" if client_id.blank?
|
|
153
|
+
begin
|
|
154
|
+
project = domain.clients(client_id, data_product).project
|
|
155
|
+
rescue RestClient::BadRequest => e
|
|
156
|
+
raise e unless /does not exist in data product/ =~ e.response
|
|
157
|
+
fail "The client \"#{client_id}\" does not exist in data product \"#{data_product.data_product_id}\""
|
|
158
|
+
end
|
|
159
|
+
fail "Client #{client_id} does not have project." unless project
|
|
160
|
+
puts "Project #{project.pid} of client #{client_id} will receive #{users.count} users"
|
|
161
|
+
project.import_users(users,
|
|
162
|
+
domain: domain,
|
|
163
|
+
whitelists: whitelists,
|
|
164
|
+
ignore_failures: ignore_failures,
|
|
165
|
+
remove_users_from_project: remove_users_from_project,
|
|
166
|
+
do_not_touch_users_that_are_not_mentioned: do_not_touch_users_that_are_not_mentioned,
|
|
167
|
+
create_non_existing_user_groups: create_non_existing_user_groups)
|
|
168
|
+
end
|
|
169
|
+
when 'sync_domain_client_workspaces'
|
|
170
|
+
domain_clients = domain.clients(:all, data_product)
|
|
171
|
+
working_client_ids = []
|
|
172
|
+
res = []
|
|
173
|
+
res += new_users.group_by { |u| u[:pid] }.flat_map do |client_id, users|
|
|
174
|
+
fail "Client id cannot be empty" if client_id.blank?
|
|
175
|
+
c = domain.clients(client_id, data_product)
|
|
176
|
+
project = c.project
|
|
177
|
+
fail "Client #{client_id} does not have project." unless project
|
|
178
|
+
working_client_ids << client_id.to_s
|
|
179
|
+
puts "Project #{project.pid} of client #{client_id} will receive #{users.count} users"
|
|
180
|
+
project.import_users(users,
|
|
181
|
+
domain: domain,
|
|
182
|
+
whitelists: whitelists,
|
|
183
|
+
ignore_failures: ignore_failures,
|
|
184
|
+
remove_users_from_project: remove_users_from_project,
|
|
185
|
+
do_not_touch_users_that_are_not_mentioned: do_not_touch_users_that_are_not_mentioned,
|
|
186
|
+
create_non_existing_user_groups: create_non_existing_user_groups)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
params.gdc_logger.debug("Working client ids are: #{working_client_ids.join(', ')}")
|
|
190
|
+
|
|
191
|
+
unless do_not_touch_users_that_are_not_mentioned
|
|
192
|
+
domain_clients.each do |c|
|
|
193
|
+
next if working_client_ids.include?(c.client_id.to_s)
|
|
194
|
+
begin
|
|
195
|
+
project = c.project
|
|
196
|
+
rescue => e
|
|
197
|
+
puts "Error when accessing project of client #{c.client_id}. Error: #{e}"
|
|
198
|
+
next
|
|
199
|
+
end
|
|
200
|
+
unless project
|
|
201
|
+
puts "Client #{c.client_id} has no project."
|
|
202
|
+
next
|
|
203
|
+
end
|
|
204
|
+
if project.deleted?
|
|
205
|
+
puts "Project #{project.pid} of client #{c.client_id} is deleted."
|
|
206
|
+
next
|
|
207
|
+
end
|
|
208
|
+
puts "Synchronizing all users in project #{project.pid} of client #{c.client_id}"
|
|
209
|
+
res += project.import_users([],
|
|
210
|
+
domain: domain,
|
|
211
|
+
whitelists: whitelists,
|
|
212
|
+
ignore_failures: ignore_failures,
|
|
213
|
+
remove_users_from_project: remove_users_from_project,
|
|
214
|
+
do_not_touch_users_that_are_not_mentioned: do_not_touch_users_that_are_not_mentioned,
|
|
215
|
+
create_non_existing_user_groups: create_non_existing_user_groups)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
res
|
|
220
|
+
when 'sync_domain_and_project'
|
|
221
|
+
domain.create_users(new_users, ignore_failures: ignore_failures)
|
|
222
|
+
project.import_users(new_users,
|
|
223
|
+
domain: domain,
|
|
224
|
+
whitelists: whitelists,
|
|
225
|
+
ignore_failures: ignore_failures,
|
|
226
|
+
remove_users_from_project: remove_users_from_project,
|
|
227
|
+
do_not_touch_users_that_are_not_mentioned: do_not_touch_users_that_are_not_mentioned,
|
|
228
|
+
create_non_existing_user_groups: create_non_existing_user_groups)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
results.compact!
|
|
232
|
+
counts = results.group_by { |r| r[:type] }.map { |g, r| [g, r.count] }
|
|
233
|
+
counts.each do |category, count|
|
|
234
|
+
puts "There were #{count} events of type #{category}"
|
|
235
|
+
end
|
|
236
|
+
errors = results.select { |r| r[:type] == :error || r[:type] == :failed }
|
|
237
|
+
return if errors.empty?
|
|
238
|
+
|
|
239
|
+
puts 'Printing 10 first errors'
|
|
240
|
+
puts '========================'
|
|
241
|
+
pp errors.take(10)
|
|
242
|
+
fail 'There was an error syncing users'
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def load_data(params, data_source)
|
|
246
|
+
first_name_column = params.first_name_column || 'first_name'
|
|
247
|
+
last_name_column = params.last_name_column || 'last_name'
|
|
248
|
+
login_column = params.login_column || 'login'
|
|
249
|
+
password_column = params.password_column || 'password'
|
|
250
|
+
email_column = params.email_column || 'email'
|
|
251
|
+
role_column = params.role_column || 'role'
|
|
252
|
+
sso_provider_column = params.sso_provider_column || 'sso_provider'
|
|
253
|
+
authentication_modes_column = params.authentication_modes_column || 'authentication_modes'
|
|
254
|
+
user_groups_column = params.user_groups_column || 'user_groups'
|
|
255
|
+
language_column = params.language_column || 'language'
|
|
256
|
+
company_column = params.company_column || 'company'
|
|
257
|
+
position_column = params.position_column || 'position'
|
|
258
|
+
country_column = params.country_column || 'country'
|
|
259
|
+
phone_column = params.phone_column || 'phone'
|
|
260
|
+
ip_whitelist_column = params.ip_whitelist_column || 'ip_whitelist'
|
|
261
|
+
mode = params.sync_mode
|
|
262
|
+
|
|
263
|
+
sso_provider = params.sso_provider
|
|
264
|
+
authentication_modes = params.authentication_modes || []
|
|
265
|
+
|
|
266
|
+
multiple_projects_column = params.multiple_projects_column
|
|
267
|
+
unless multiple_projects_column
|
|
268
|
+
client_modes = %w(sync_domain_client_workspaces sync_one_project_based_on_custom_id sync_multiple_projects_based_on_custom_id)
|
|
269
|
+
multiple_projects_column = if client_modes.include?(mode)
|
|
270
|
+
'client_id'
|
|
271
|
+
else
|
|
272
|
+
'project_id'
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
dwh = params.ads_client
|
|
277
|
+
if dwh
|
|
278
|
+
data = dwh.execute_select(params.input_source.query)
|
|
279
|
+
else
|
|
280
|
+
tmp = File.open(data_source.realize(params), 'r:UTF-8')
|
|
281
|
+
data = CSV.read(tmp, headers: true)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
data.map do |row|
|
|
285
|
+
params.gdc_logger.debug("Processing row: #{row}")
|
|
286
|
+
|
|
287
|
+
modes = if authentication_modes.empty?
|
|
288
|
+
row[authentication_modes_column] || row[authentication_modes_column.to_sym] || []
|
|
289
|
+
else
|
|
290
|
+
authentication_modes
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
modes = modes.split(',').map(&:strip).map { |x| x.to_s.upcase } unless modes.is_a? Array
|
|
294
|
+
|
|
295
|
+
user_group = row[user_groups_column] || row[user_groups_column.to_sym]
|
|
296
|
+
user_group = user_group.split(',').map(&:strip) if user_group
|
|
297
|
+
|
|
298
|
+
ip_whitelist = row[ip_whitelist_column] || row[ip_whitelist_column.to_sym]
|
|
299
|
+
ip_whitelist = ip_whitelist.split(',').map(&:strip) if ip_whitelist
|
|
300
|
+
|
|
301
|
+
{
|
|
302
|
+
:first_name => row[first_name_column] || row[first_name_column.to_sym],
|
|
303
|
+
:last_name => row[last_name_column] || row[last_name_column.to_sym],
|
|
304
|
+
:login => row[login_column] || row[login_column.to_sym],
|
|
305
|
+
:password => row[password_column] || row[password_column.to_sym],
|
|
306
|
+
:email => row[email_column] || row[login_column] || row[email_column.to_sym] || row[login_column.to_sym],
|
|
307
|
+
:role => row[role_column] || row[role_column.to_sym],
|
|
308
|
+
:sso_provider => sso_provider || row[sso_provider_column] || row[sso_provider_column.to_sym],
|
|
309
|
+
:authentication_modes => modes,
|
|
310
|
+
:user_group => user_group,
|
|
311
|
+
:pid => multiple_projects_column.nil? ? nil : (row[multiple_projects_column] || row[multiple_projects_column.to_sym]),
|
|
312
|
+
:language => row[language_column] || row[language_column.to_sym],
|
|
313
|
+
:company => row[company_column] || row[company_column.to_sym],
|
|
314
|
+
:position => row[position_column] || row[position_column.to_sym],
|
|
315
|
+
:country => row[country_column] || row[country_column.to_sym],
|
|
316
|
+
:phone => row[phone_column] || row[phone_column.to_sym],
|
|
317
|
+
:ip_whitelist => ip_whitelist
|
|
318
|
+
}
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|