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.
Files changed (139) hide show
  1. checksums.yaml +5 -5
  2. data/.flayignore +6 -0
  3. data/.gitignore +1 -0
  4. data/.pronto.yml +3 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +4 -1
  7. data/CHANGELOG.md +18 -0
  8. data/CONTRIBUTING.md +14 -1
  9. data/DEPENDENCIES.md +324 -253
  10. data/Dockerfile.jruby +5 -7
  11. data/Dockerfile.ruby +8 -8
  12. data/Rakefile +24 -0
  13. data/ci.rake +47 -0
  14. data/docker-compose.yml +34 -0
  15. data/gooddata.gemspec +8 -2
  16. data/lib/gooddata/bricks/middleware/restforce_middleware.rb +0 -3
  17. data/lib/gooddata/helpers/data_helper.rb +10 -7
  18. data/lib/gooddata/helpers/global_helpers_params.rb +8 -3
  19. data/lib/gooddata/lcm/actions/apply_custom_maql.rb +2 -1
  20. data/lib/gooddata/lcm/actions/associate_clients.rb +10 -1
  21. data/lib/gooddata/lcm/actions/collect_client_projects.rb +78 -0
  22. data/lib/gooddata/lcm/actions/collect_clients.rb +20 -6
  23. data/lib/gooddata/lcm/actions/collect_data_product.rb +62 -0
  24. data/lib/gooddata/lcm/actions/collect_dynamic_schedule_params.rb +62 -0
  25. data/lib/gooddata/lcm/actions/{collect_attrs.rb → collect_ldm_objects.rb} +3 -3
  26. data/lib/gooddata/lcm/actions/collect_meta.rb +6 -3
  27. data/lib/gooddata/lcm/actions/collect_segment_clients.rb +2 -1
  28. data/lib/gooddata/lcm/actions/collect_segments.rb +6 -7
  29. data/lib/gooddata/lcm/actions/collect_tagged_objects.rb +7 -4
  30. data/lib/gooddata/lcm/actions/create_segment_masters.rb +7 -3
  31. data/lib/gooddata/lcm/actions/ensure_data_product.rb +53 -0
  32. data/lib/gooddata/lcm/actions/ensure_technical_users_domain.rb +6 -2
  33. data/lib/gooddata/lcm/actions/ensure_technical_users_project.rb +30 -18
  34. data/lib/gooddata/lcm/actions/execute_schedules.rb +128 -0
  35. data/lib/gooddata/lcm/actions/provision_clients.rb +32 -21
  36. data/lib/gooddata/lcm/actions/purge_clients.rb +25 -39
  37. data/lib/gooddata/lcm/actions/rename_existing_client_projects.rb +70 -0
  38. data/lib/gooddata/lcm/actions/segments_filter.rb +6 -0
  39. data/lib/gooddata/lcm/actions/synchronize_cas.rb +11 -0
  40. data/lib/gooddata/lcm/actions/synchronize_clients.rb +2 -1
  41. data/lib/gooddata/lcm/actions/synchronize_etls_in_segment.rb +34 -15
  42. data/lib/gooddata/lcm/actions/synchronize_ldm.rb +10 -1
  43. data/lib/gooddata/lcm/actions/synchronize_new_segments.rb +2 -1
  44. data/lib/gooddata/lcm/actions/synchronize_processes.rb +4 -7
  45. data/lib/gooddata/lcm/actions/synchronize_tag_objects.rb +8 -5
  46. data/lib/gooddata/lcm/actions/synchronize_user_filters.rb +224 -0
  47. data/lib/gooddata/lcm/actions/synchronize_user_groups.rb +53 -0
  48. data/lib/gooddata/lcm/actions/synchronize_users.rb +324 -0
  49. data/lib/gooddata/lcm/dsl/type_dsl.rb +1 -0
  50. data/lib/gooddata/lcm/helpers/check_helper.rb +4 -0
  51. data/lib/gooddata/lcm/helpers/tags_helper.rb +4 -3
  52. data/lib/gooddata/lcm/lcm2.rb +33 -1
  53. data/lib/gooddata/lcm/types/complex/segment.rb +3 -0
  54. data/lib/gooddata/lcm/types/complex/update_preference.rb +8 -2
  55. data/lib/gooddata/lcm/types/special/array.rb +1 -3
  56. data/lib/gooddata/lcm/types/special/enum.rb +1 -3
  57. data/lib/gooddata/mixins/md_id_to_uri.rb +0 -1
  58. data/lib/gooddata/mixins/md_json.rb +2 -2
  59. data/lib/gooddata/models/blueprint/project_blueprint.rb +15 -0
  60. data/lib/gooddata/models/blueprint/to_wire.rb +1 -0
  61. data/lib/gooddata/models/client.rb +21 -9
  62. data/lib/gooddata/models/data_product.rb +149 -0
  63. data/lib/gooddata/models/domain.rb +26 -72
  64. data/lib/gooddata/models/from_wire.rb +2 -0
  65. data/lib/gooddata/models/metadata/report.rb +9 -3
  66. data/lib/gooddata/models/metadata/report_definition.rb +2 -2
  67. data/lib/gooddata/models/model.rb +1 -1
  68. data/lib/gooddata/models/process.rb +4 -0
  69. data/lib/gooddata/models/project.rb +58 -35
  70. data/lib/gooddata/models/project_creator.rb +13 -0
  71. data/lib/gooddata/models/segment.rb +63 -16
  72. data/lib/gooddata/models/style_setting.rb +2 -15
  73. data/lib/gooddata/models/user_group.rb +2 -0
  74. data/lib/gooddata/rest/connection.rb +32 -9
  75. data/lib/gooddata/rest/object_factory.rb +0 -25
  76. data/lib/gooddata/version.rb +1 -1
  77. data/spec/data/blueprints/invalid_blueprint.json +2 -2
  78. data/spec/data/blueprints/test_project_model_spec.json +1 -1
  79. data/spec/data/dynamic_schedule_params_table.csv +7 -0
  80. data/spec/data/workspace_table.csv +3 -3
  81. data/spec/environment/staging.rb +3 -3
  82. data/spec/integration/ads_output_stage_spec.rb +0 -10
  83. data/spec/integration/clients_spec.rb +1 -1
  84. data/spec/{unit → integration}/commands/command_projects_spec.rb +0 -0
  85. data/spec/{unit → integration}/core/connection_spec.rb +0 -0
  86. data/spec/{unit → integration}/core/logging_spec.rb +0 -0
  87. data/spec/{unit → integration}/core/project_spec.rb +0 -0
  88. data/spec/integration/date_dim_switch_spec.rb +13 -0
  89. data/spec/integration/full_process_schedule_spec.rb +2 -2
  90. data/spec/integration/helpers_spec.rb +16 -0
  91. data/spec/integration/lcm_spec.rb +12 -2
  92. data/spec/integration/mixins/id_to_uri_spec.rb +44 -0
  93. data/spec/integration/models/data_product_spec.rb +71 -0
  94. data/spec/{unit → integration}/models/domain_spec.rb +2 -2
  95. data/spec/{unit → integration}/models/invitation_spec.rb +0 -0
  96. data/spec/{unit → integration}/models/membership_spec.rb +0 -0
  97. data/spec/{unit → integration}/models/params_spec.rb +0 -0
  98. data/spec/{unit → integration}/models/profile_spec.rb +0 -0
  99. data/spec/{unit → integration}/models/project_role_spec.rb +0 -0
  100. data/spec/integration/models/project_spec.rb +225 -0
  101. data/spec/{unit → integration}/models/schedule_spec.rb +0 -0
  102. data/spec/{unit → integration}/models/unit_project_spec.rb +0 -0
  103. data/spec/integration/project_spec.rb +40 -5
  104. data/spec/integration/segments_spec.rb +27 -26
  105. data/spec/integration/user_filters_spec.rb +1 -1
  106. data/spec/spec_helper.rb +15 -19
  107. data/spec/unit/actions/associate_clients_spec.rb +47 -0
  108. data/spec/unit/actions/collect_client_projects_spec.rb +47 -0
  109. data/spec/unit/actions/collect_clients_spec.rb +27 -0
  110. data/spec/unit/actions/collect_data_product_spec.rb +64 -0
  111. data/spec/unit/actions/collect_dynamic_schedule_params_spec.rb +56 -0
  112. data/spec/unit/actions/collect_meta_spec.rb +4 -4
  113. data/spec/unit/actions/collect_segment_clients_spec.rb +44 -3
  114. data/spec/unit/actions/collect_tagged_objects_spec.rb +20 -4
  115. data/spec/unit/actions/create_segment_masters_spec.rb +64 -0
  116. data/spec/unit/actions/ensure_data_product_spec.rb +38 -0
  117. data/spec/unit/actions/ensure_technical_users_domain_spec.rb +51 -0
  118. data/spec/unit/actions/ensure_technical_users_project_spec.rb +72 -0
  119. data/spec/unit/actions/execute_schedules_spec.rb +94 -0
  120. data/spec/unit/actions/provision_clients_spec.rb +45 -0
  121. data/spec/unit/actions/purge_clients_spec.rb +47 -0
  122. data/spec/unit/actions/rename_existing_client_projects_spec.rb +54 -0
  123. data/spec/unit/actions/segments_filter_spec.rb +46 -0
  124. data/spec/unit/actions/shared_examples_for_user_actions.rb +10 -0
  125. data/spec/unit/actions/synchronize_cas_spec.rb +58 -0
  126. data/spec/unit/actions/synchronize_etls_in_segment_spec.rb +174 -13
  127. data/spec/unit/actions/synchronize_ldm_spec.rb +57 -0
  128. data/spec/unit/actions/synchronize_user_filters_spec.rb +142 -0
  129. data/spec/unit/actions/synchronize_user_groups_spec.rb +49 -0
  130. data/spec/unit/actions/synchronize_users_spec.rb +76 -0
  131. data/spec/unit/helpers/data_helper_spec.rb +17 -0
  132. data/spec/unit/helpers/global_helpers_spec.rb +16 -0
  133. data/spec/unit/helpers_spec.rb +0 -6
  134. data/spec/unit/models/blueprint/project_blueprint_spec.rb +21 -4
  135. data/spec/unit/models/project_creator_spec.rb +16 -0
  136. data/spec/unit/models/project_spec.rb +66 -197
  137. metadata +202 -100
  138. data/PULL_REQUEST_TEMPLATE.md +0 -5
  139. 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