connectors_sdk 8.3.0.0.pre.20220414T060419Z → 8.3.0.0.pre.20220510T144908Z

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/connectors_sdk/atlassian/config.rb +27 -0
  3. data/lib/connectors_sdk/atlassian/custom_client.rb +87 -0
  4. data/lib/connectors_sdk/base/adapter.rb +7 -8
  5. data/lib/connectors_sdk/base/authorization.rb +89 -0
  6. data/lib/connectors_sdk/base/custom_client.rb +0 -1
  7. data/lib/connectors_sdk/base/extractor.rb +3 -2
  8. data/lib/connectors_sdk/base/http_call_wrapper.rb +135 -0
  9. data/lib/connectors_sdk/base/registry.rb +5 -3
  10. data/lib/connectors_sdk/confluence/adapter.rb +216 -0
  11. data/lib/connectors_sdk/confluence/custom_client.rb +143 -0
  12. data/lib/connectors_sdk/confluence/extractor.rb +270 -0
  13. data/lib/connectors_sdk/confluence_cloud/authorization.rb +64 -0
  14. data/lib/connectors_sdk/confluence_cloud/custom_client.rb +61 -0
  15. data/lib/connectors_sdk/confluence_cloud/extractor.rb +59 -0
  16. data/lib/connectors_sdk/confluence_cloud/http_call_wrapper.rb +59 -0
  17. data/lib/connectors_sdk/helpers/atlassian_time_formatter.rb +10 -0
  18. data/lib/connectors_sdk/office365/adapter.rb +7 -7
  19. data/lib/connectors_sdk/office365/config.rb +1 -0
  20. data/lib/connectors_sdk/office365/custom_client.rb +31 -9
  21. data/lib/connectors_sdk/office365/extractor.rb +8 -8
  22. data/lib/connectors_sdk/share_point/adapter.rb +12 -12
  23. data/lib/connectors_sdk/share_point/authorization.rb +14 -62
  24. data/lib/connectors_sdk/share_point/extractor.rb +2 -2
  25. data/lib/connectors_sdk/share_point/http_call_wrapper.rb +24 -83
  26. data/lib/connectors_shared/exception_tracking.rb +4 -4
  27. data/lib/connectors_shared/extraction_utils.rb +109 -0
  28. data/lib/connectors_shared/middleware/basic_auth.rb +27 -0
  29. data/lib/connectors_shared/middleware/bearer_auth.rb +27 -0
  30. data/lib/connectors_shared/middleware/restrict_hostnames.rb +73 -0
  31. data/lib/connectors_shared/monitor.rb +3 -3
  32. data/lib/stubs/enterprise_search/exception_tracking.rb +43 -0
  33. metadata +22 -10
  34. data/lib/connectors_sdk/base/.config.rb.un~ +0 -0
  35. data/lib/connectors_sdk/base/.connectors.rb.un~ +0 -0
  36. data/lib/connectors_sdk/base/.registry.rb.un~ +0 -0
  37. data/lib/connectors_sdk/share_point/.http_call_wrapper.rb.un~ +0 -0
@@ -0,0 +1,143 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'active_support/json'
10
+ require 'active_support/core_ext/array'
11
+ require 'active_support/core_ext/object'
12
+ require 'hashie/mash'
13
+
14
+ require 'connectors_sdk/atlassian/custom_client'
15
+
16
+ module ConnectorsSdk
17
+ module Confluence
18
+ class CustomClient < ConnectorsSdk::Atlassian::CustomClient
19
+ SEARCH_ENDPOINT = 'rest/api/search'
20
+ CONTENT_SEARCH_ENDPOINT = 'rest/api/content/search'
21
+ CONTENT_EXPAND_FIELDS = %w(body.export_view history.lastUpdated ancestors space children.comment.body.export_view container).freeze
22
+
23
+ def content(space:, types: [], start_at: 0, order_field_and_direction: 'created asc', updated_after: nil, next_value: nil)
24
+ search_helper(
25
+ :endpoint => CONTENT_SEARCH_ENDPOINT,
26
+ :space => space,
27
+ :types => types,
28
+ :start_at => start_at,
29
+ :order_field_and_direction => order_field_and_direction,
30
+ :updated_after => updated_after,
31
+ :next_value => next_value
32
+ )
33
+ end
34
+
35
+ def content_by_id(content_id, include_permissions: false, expand_fields: CONTENT_EXPAND_FIELDS)
36
+ endpoint = "rest/api/content/#{Faraday::Utils.escape(content_id)}"
37
+ if include_permissions
38
+ expand_fields = expand_fields.dup
39
+ expand_fields << 'restrictions.read.restrictions.user'
40
+ expand_fields << 'restrictions.read.restrictions.group'
41
+ end
42
+ response = begin
43
+ parse_and_raise_if_necessary!(get(endpoint, :status => 'any', :expand => expand_fields.join(',')))
44
+ rescue ContentConvertibleError
45
+ # Confluence has a bug when trying to expand `container` for certain items:
46
+ # https://jira.atlassian.com/browse/CONFSERVER-40475
47
+ Connectors::Stats.increment('custom_client.confluence.error.content_convertible')
48
+ parse_and_raise_if_necessary!(get(endpoint, :status => 'any', :expand => (expand_fields - ['container']).join(',')))
49
+ end
50
+ Hashie::Mash.new(response)
51
+ end
52
+
53
+ def spaces(start_at: 0, limit: 50, space_keys: nil, include_permissions: false)
54
+ params = {
55
+ :start => start_at,
56
+ :limit => limit
57
+ }
58
+ params[:spaceKey] = space_keys if space_keys.present?
59
+ params[:expand] = 'permissions' if include_permissions
60
+ response = get('rest/api/space', params)
61
+ Hashie::Mash.new(parse_and_raise_if_necessary!(response))
62
+ end
63
+
64
+ def search(
65
+ space: nil,
66
+ start_at: 0,
67
+ limit: 50,
68
+ types: [],
69
+ expand_fields: [],
70
+ order_field_and_direction: 'created asc',
71
+ updated_after: nil
72
+ )
73
+ search_helper(
74
+ :endpoint => SEARCH_ENDPOINT,
75
+ :space => space,
76
+ :start_at => start_at,
77
+ :limit => limit,
78
+ :types => types,
79
+ :expand_fields => expand_fields,
80
+ :order_field_and_direction => order_field_and_direction,
81
+ :updated_after => updated_after
82
+ )
83
+ end
84
+
85
+ def content_search(cql, expand: [], limit: 25)
86
+ response = get(CONTENT_SEARCH_ENDPOINT, :cql => cql, :expand => expand.join(','), :limit => limit)
87
+ Hashie::Mash.new(parse_and_raise_if_necessary!(response))
88
+ end
89
+
90
+ def me
91
+ response = get('rest/api/user/current')
92
+ Hashie::Mash.new(parse_and_raise_if_necessary!(response))
93
+ end
94
+
95
+ private
96
+
97
+ def search_helper(
98
+ endpoint:,
99
+ space: nil,
100
+ start_at: 0,
101
+ limit: 50,
102
+ types: [],
103
+ expand_fields: [],
104
+ order_field_and_direction: 'created asc',
105
+ updated_after: nil,
106
+ next_value: nil
107
+ )
108
+
109
+ response =
110
+ if next_value.present?
111
+ get(next_value.reverse.chomp('/').reverse)
112
+ else
113
+ params = {
114
+ :cql => generate_cql(:space => space, :types => types, :order_field_and_direction => order_field_and_direction, :updated_after => updated_after),
115
+ :start => start_at,
116
+ :expand => expand_fields.join(','),
117
+ :limit => limit
118
+ }
119
+
120
+ get(endpoint, params)
121
+ end
122
+
123
+ Hashie::Mash.new(parse_and_raise_if_necessary!(response))
124
+ end
125
+
126
+ def generate_cql(space: nil, types: nil, order_field_and_direction: nil, updated_after: nil)
127
+ query_conditions = []
128
+ query_conditions << "space=\"#{space}\"" if space
129
+ query_conditions << "type in (#{types.join(',')})" if types.any?
130
+ query_conditions << "lastmodified > \"#{format_date(updated_after)}\"" if updated_after.present?
131
+
132
+ query_parts = [query_conditions.join(' AND ')]
133
+ query_parts << "order by #{order_field_and_direction}" if order_field_and_direction
134
+
135
+ query_parts.join(' ').strip
136
+ end
137
+
138
+ def format_date(date)
139
+ DateTime.parse(date).strftime('%Y-%m-%d %H:%M')
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,270 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'connectors_sdk/atlassian/custom_client'
10
+ require 'connectors_sdk/confluence/adapter'
11
+ require 'connectors_sdk/base/extractor'
12
+
13
+ module ConnectorsSdk
14
+ module Confluence
15
+ class Extractor < ConnectorsSdk::Base::Extractor
16
+ CONTENT_OFFSET_CURSOR_KEY = 'content'
17
+ CONTENT_NEXT_CURSOR_KEY = 'next'
18
+ CONTENT_MODIFIED_SINCE_NEXT_CURSOR_KEY = 'modified_since_next'
19
+ CONTENT_MODIFIED_SINCE_CURSOR_KEY = 'content_modified_at'
20
+
21
+ ConnectorsSdk::Base::Extractor::TRANSIENT_SERVER_ERROR_CLASSES << Atlassian::CustomClient::ServiceUnavailableError
22
+
23
+ def yield_document_changes(modified_since: nil, break_after_page: false)
24
+ @space_permissions_cache = {}
25
+ @content_restriction_cache = {}
26
+ yield_spaces do |space|
27
+ yield_single_document_change(:identifier => "Confluence Space: #{space&.fetch(:key)} (#{space&.webui})") do
28
+ permissions = config.index_permissions ? get_space_permissions(space) : []
29
+ yield :create_or_update, Confluence::Adapter.es_document_from_confluence_space(space, content_base_url, permissions)
30
+ end
31
+
32
+ yield_content_for_space(
33
+ :space => space[:key],
34
+ :types => %w(page blogpost attachment),
35
+ :modified_since => modified_since
36
+ ) do |content|
37
+ restrictions = config.index_permissions ? get_content_restrictions(content) : []
38
+ if content.type == 'attachment'
39
+ document = Confluence::Adapter.es_document_from_confluence_attachment(content, content_base_url, restrictions)
40
+ download_args = download_args_and_proc(
41
+ id: document.fetch(:id),
42
+ name: content.title,
43
+ size: content.extensions.fileSize,
44
+ download_args: { content: content }
45
+ ) do |args|
46
+ download(args)
47
+ end
48
+ yield :create_or_update, document, download_args
49
+ else
50
+ yield :create_or_update, Confluence::Adapter.es_document_from_confluence_content(content, content_base_url, restrictions)
51
+ end
52
+ end
53
+
54
+ if break_after_page
55
+ @completed = true
56
+ break
57
+ end
58
+ end
59
+ end
60
+
61
+ def yield_deleted_ids(ids)
62
+ id_groups = ids.group_by do |id|
63
+ if Confluence::Adapter.es_id_is_confluence_space_id?(id)
64
+ :space
65
+ elsif Confluence::Adapter.es_id_is_confluence_content_id?(id)
66
+ :content
67
+ elsif Confluence::Adapter.es_id_is_confluence_attachment_id?(id)
68
+ :attachment
69
+ else
70
+ :unknown
71
+ end
72
+ end
73
+
74
+ %i(space content attachment).each do |group|
75
+ confluence_ids = Array(id_groups[group]).map { |id| Confluence::Adapter.public_send("es_id_to_confluence_#{group}_id", id) }
76
+ get_ids_for_deleted(confluence_ids, group).each do |deleted_id|
77
+ yield Confluence::Adapter.public_send("confluence_#{group}_id_to_es_id", deleted_id)
78
+ end
79
+ end
80
+ end
81
+
82
+ def download(item)
83
+ content = item[:content]
84
+ client.download("#{content._links.base}#{content._links.download}").body
85
+ end
86
+
87
+ private
88
+
89
+ def content_base_url
90
+ 'https://workplace-search.atlassian.net/wiki'
91
+ end
92
+
93
+ def yield_spaces
94
+ @space_cursor ||= 0
95
+ loop do
96
+ response = client.spaces(:start_at => @space_cursor, :include_permissions => config.index_permissions)
97
+ response.results.each do |space|
98
+ yield space
99
+ @space_cursor += 1
100
+ end
101
+ break unless should_continue_looping?(response)
102
+ log_info("Requesting more spaces with cursor: #{@space_cursor}")
103
+ end
104
+ end
105
+
106
+ def yield_content_for_space(space:, types:, modified_since:)
107
+ loop do
108
+ response = client.content(
109
+ :space => space,
110
+ :types => types,
111
+ :order_field_and_direction => modified_since ? 'lastmodified asc' : 'created asc',
112
+ cursoring_param(:modified_since => modified_since, :space => space) => cursoring_value(:modified_since => modified_since, :space => space)
113
+ )
114
+
115
+ response.results.each do |result|
116
+ yield_single_document_change(:identifier => "Confluence ID: #{result&.id} (#{result&.webui})") do
117
+ content = client.content_by_id(result.id, :include_permissions => config.index_permissions)
118
+ yield content if content.status == 'current'
119
+ end
120
+ end
121
+ update_content_cursors(space, response, modified_since)
122
+
123
+ break unless should_continue_looping?(response)
124
+ log_info("Requesting more content for space #{space} with cursor: #{cursoring_param(:modified_since => modified_since, :space => space)} #{cursoring_value(:modified_since => modified_since, :space => space)}")
125
+ end
126
+ end
127
+
128
+ def next_cursor_key(modified_since:)
129
+ modified_since ? CONTENT_MODIFIED_SINCE_NEXT_CURSOR_KEY : CONTENT_NEXT_CURSOR_KEY
130
+ end
131
+
132
+ def cursoring_param(modified_since:, space:)
133
+ return :next_value if config.cursors.dig(next_cursor_key(:modified_since => modified_since), space).present?
134
+
135
+ modified_since ? :updated_after : :start_at
136
+ end
137
+
138
+ def cursoring_value(modified_since:, space:)
139
+ next_value = config.cursors.dig(next_cursor_key(:modified_since => modified_since), space)
140
+ return next_value if next_value.present?
141
+
142
+ get_content_cursors(space, modified_since)[space]
143
+ end
144
+
145
+ def update_content_cursors(space, response, modified_since)
146
+ if response._links&.next.present?
147
+ config.cursors[next_cursor_key(:modified_since => modified_since)] ||= {}
148
+ config.cursors[next_cursor_key(:modified_since => modified_since)][space] = response._links.next
149
+ end
150
+
151
+ if response.results && modified_since
152
+ updated_cursor = response.results.last&.history&.lastUpdated&.when
153
+ config.cursors[CONTENT_MODIFIED_SINCE_CURSOR_KEY][space] = updated_cursor if updated_cursor
154
+ else
155
+ config.cursors[CONTENT_OFFSET_CURSOR_KEY][space] += response.results.size
156
+ end
157
+ end
158
+
159
+ def get_content_cursors(space, modified_since)
160
+ modified_since ? get_content_modified_since_cursors(space, modified_since) : get_content_offset_cursors(space)
161
+ end
162
+
163
+ def get_content_offset_cursors(space)
164
+ config.cursors[CONTENT_OFFSET_CURSOR_KEY] ||= {}
165
+ config.cursors[CONTENT_OFFSET_CURSOR_KEY][space] ||= 0
166
+ config.cursors[CONTENT_OFFSET_CURSOR_KEY]
167
+ end
168
+
169
+ def get_content_modified_since_cursors(space, modified_since)
170
+ config.cursors[CONTENT_MODIFIED_SINCE_CURSOR_KEY] ||= {}
171
+ config.cursors[CONTENT_MODIFIED_SINCE_CURSOR_KEY][space] ||= modified_since.to_s
172
+ config.cursors[CONTENT_MODIFIED_SINCE_CURSOR_KEY]
173
+ end
174
+
175
+ def should_continue_looping?(response)
176
+ response.results&.size.to_i > 0 && response._links.next.present?
177
+ end
178
+
179
+ def get_ids_for_deleted(search_ids, group)
180
+ return [] if search_ids.empty?
181
+
182
+ response, id_sym =
183
+ if group == :space
184
+ [client.spaces(:space_keys => search_ids, :limit => search_ids.size), :key]
185
+ else
186
+ [client.content_search("id in (#{search_ids.join(',')})", :limit => search_ids.size), :id]
187
+ end
188
+ found_ids = response.results.map { |result| result.fetch(id_sym) }.map(&:to_s)
189
+ search_ids - found_ids
190
+ end
191
+
192
+ def get_space_permissions(space)
193
+ space_permissions = space.permissions&.select { |permission| %w(read administer).include?(permission.operation.operation) }
194
+ if space_permissions.nil? || space_permissions.any?(&:anonymousAccess)
195
+ @space_permissions_cache[space.fetch('key')] = []
196
+ return []
197
+ end
198
+
199
+ subjects = space_permissions.flat_map(&:subjects)
200
+ permissions = [
201
+ subjects.select { |subject| subject.has_key?(:user) }.map { |subject| subject.user.results.map { |user| "user:#{user.accountId}" } },
202
+ subjects.select { |subject| subject.has_key?(:group) }.map { |subject| subject.group.results.map { |group| "group:#{group.name}" } }
203
+ ].flatten.uniq
204
+ @space_permissions_cache[space.fetch('key')] = permissions
205
+ end
206
+
207
+ def get_user_spaces(user_id)
208
+ (@space_permissions_cache || {}).select { |_, permissions| "user:#{user_id}".in?(permissions) }.keys.uniq
209
+ end
210
+
211
+ def get_group_spaces(group_name)
212
+ (@space_permissions_cache || {}).select { |_, permissions| "group:#{group_name}".in?(permissions) }.keys.uniq
213
+ end
214
+
215
+ # get_content_restrictions returns the final restriction of the content, taking into consideration inherited restrictions
216
+ # if the content is an attachment, the final restriction will will be either space permissions
217
+ # or (in case they aren't empty) the intersection of
218
+ # 1. its restriction
219
+ # 2. its container's restriction
220
+ # 3. its container's ancestors' restriction
221
+ # if the content is a page or blog post, the final restriction will be either space permissions
222
+ # or (in case they aren't empty) the intersection of
223
+ # 1. its restrictions
224
+ # 2. its ancestors' restrictions
225
+ def get_content_restrictions(content)
226
+ restrictions = []
227
+ restrictions << extract_restrictions(content.restrictions&.read&.restrictions)
228
+
229
+ # space permissions should always be available in cache
230
+ space_key = content.space&.fetch('key')
231
+ space_restrictions = (@space_permissions_cache[space_key] || [])
232
+
233
+ ancestors = content.ancestors || []
234
+
235
+ if content.type == 'attachment'
236
+ if content.container&.type == 'global'
237
+ log_info("Skipping ancestor restrictions as it is a space and should already be cached: [#{content.id}] in [#{content.container&.type}_#{content.container&.id}]")
238
+ else
239
+ container = client.content_by_id(content.container&.id, :include_permissions => true)
240
+ @content_restriction_cache[container.id] ||= extract_restrictions(container.restrictions&.read&.restrictions)
241
+ restrictions << @content_restriction_cache[container.id]
242
+ ancestors = container.ancestors
243
+ end
244
+ end
245
+
246
+ ancestors.each do |ancestor|
247
+ restrictions << get_restrictions_by_content_id(ancestor.id)
248
+ end
249
+
250
+ combined_restrictions = restrictions.select(&:any?).reduce(:&)
251
+ @content_restriction_cache[content.id] = combined_restrictions || []
252
+ (combined_restrictions || space_restrictions).map { |restriction| "#{space_key}/#{restriction}" }
253
+ end
254
+
255
+ def get_restrictions_by_content_id(content_id)
256
+ @content_restriction_cache[content_id] ||= begin
257
+ content = client.content_by_id(content_id, :include_permissions => true)
258
+ extract_restrictions(content.restrictions&.read&.restrictions)
259
+ end
260
+ end
261
+
262
+ def extract_restrictions(restrictions)
263
+ [
264
+ restrictions&.user&.results&.map { |user| "user:#{user.accountId}" },
265
+ restrictions&.group&.results&.map { |group| "group:#{group.name}" }
266
+ ].flatten.compact.uniq
267
+ end
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,64 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'connectors_sdk/base/authorization'
10
+
11
+ module ConnectorsSdk
12
+ module ConfluenceCloud
13
+ class Authorization < ConnectorsSdk::Base::Authorization
14
+ class << self
15
+ def access_token(params)
16
+ tokens = super
17
+ tokens.merge(:cloud_id => fetch_cloud_id(tokens['access_token'], params[:external_connector_base_url]))
18
+ end
19
+
20
+ def oauth_scope
21
+ %w[
22
+ offline_access
23
+
24
+ read:confluence-content.all
25
+ read:confluence-content.summary
26
+ read:confluence-props
27
+ read:confluence-space.summary
28
+ read:confluence-user
29
+ readonly:content.attachment:confluence
30
+ search:confluence
31
+ ]
32
+ end
33
+
34
+ private
35
+
36
+ def authorization_url
37
+ 'https://auth.atlassian.com/authorize'
38
+ end
39
+
40
+ def token_credential_uri
41
+ 'https://auth.atlassian.com/oauth/token'
42
+ end
43
+
44
+ def additional_parameters
45
+ { :prompt => 'consent', :audience => 'api.atlassian.com' }
46
+ end
47
+
48
+ def fetch_cloud_id(access_token, base_url)
49
+ response = HTTPClient.new.get(
50
+ 'https://api.atlassian.com/oauth/token/accessible-resources',
51
+ nil,
52
+ 'Accept' => 'application/json',
53
+ 'Authorization' => "Bearer #{access_token}"
54
+ )
55
+ raise 'unable to fetch cloud id' unless HTTP::Status.successful?(response.status)
56
+ json = JSON.parse(response.body)
57
+
58
+ site = json.find { |sites| sites['url'] == base_url } || {}
59
+ site.fetch('id') { raise 'unable to fetch cloud id' }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,61 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'connectors_sdk/confluence/custom_client'
10
+
11
+ module ConnectorsSdk
12
+ module ConfluenceCloud
13
+ class CustomClient < ConnectorsSdk::Confluence::CustomClient
14
+ def user_groups(account_id, limit: 200, start: 0)
15
+ size = Float::INFINITY
16
+
17
+ groups = []
18
+
19
+ while start + limit < size
20
+ params = {
21
+ :start => start,
22
+ :limit => limit,
23
+ :accountId => account_id
24
+ }
25
+ response = get('rest/api/user/memberof', params)
26
+ result = Hashie::Mash.new(parse_and_raise_if_necessary!(response))
27
+ size = result.size
28
+ start += limit
29
+ groups.concat(result.results)
30
+ end
31
+
32
+ groups
33
+ end
34
+
35
+ def user(account_id, expand: [])
36
+ params = {
37
+ :accountId => account_id
38
+ }
39
+ if expand.present?
40
+ params[:expand] = case expand
41
+ when Array
42
+ expand.join(',')
43
+ when String
44
+ expand
45
+ else
46
+ expand.to_s
47
+ end
48
+ end
49
+ response = get('rest/api/user', params)
50
+ Hashie::Mash.new(parse_and_raise_if_necessary!(response))
51
+ rescue ConnectorsSdk::Atlassian::CustomClient::ClientError => e
52
+ if e.status_code == 404
53
+ ConnectorsShared::Logger.warn("Could not find a user with account id #{account_id}")
54
+ nil
55
+ else
56
+ raise
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,59 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'connectors_sdk/confluence/extractor'
10
+
11
+ module ConnectorsSdk
12
+ module ConfluenceCloud
13
+ class Extractor < ConnectorsSdk::Confluence::Extractor
14
+
15
+ def yield_permissions(source_user_id)
16
+ # yield empty permissions if the user is suspended or deleted
17
+ user = client.user(source_user_id, :expand => 'operations')
18
+ if user.nil? || user.operations.blank?
19
+ yield [] and return
20
+ end
21
+
22
+ # refresh space permissions if not initialized
23
+ if @space_permissions_cache.nil?
24
+ @space_permissions_cache = {}
25
+ yield_spaces do |space|
26
+ if config.index_permissions
27
+ get_space_permissions(space)
28
+ end
29
+ end
30
+ end
31
+
32
+ direct_spaces = get_user_spaces(source_user_id)
33
+ indirect_spaces = []
34
+
35
+ group_permissions = []
36
+ client.user_groups(source_user_id).each do |group|
37
+ group_name = group.name
38
+ group_spaces = get_group_spaces(group_name)
39
+ indirect_spaces << group_spaces
40
+ group_permissions << "group:#{group_name}"
41
+ end
42
+
43
+ total_user_spaces = indirect_spaces.flatten.concat(direct_spaces).uniq
44
+ user_permissions = ["user:#{source_user_id}"]
45
+ .concat(group_permissions)
46
+ .product(total_user_spaces)
47
+ .collect { |permission, space| "#{space}/#{permission}" }
48
+
49
+ yield user_permissions.flatten.uniq.sort
50
+ end
51
+
52
+ def download(item)
53
+ content = item[:content]
54
+ parent_id = content.dig('container', 'id')
55
+ client.download("#{client.base_url}/wiki/rest/api/content/#{parent_id}/child/attachment/#{content['id']}/download").body
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,59 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'connectors_sdk/atlassian/config'
10
+ require 'connectors_sdk/confluence_cloud/extractor'
11
+ require 'connectors_sdk/confluence_cloud/authorization'
12
+ require 'connectors_sdk/confluence_cloud/custom_client'
13
+ require 'connectors_sdk/base/http_call_wrapper'
14
+
15
+ module ConnectorsSdk
16
+ module ConfluenceCloud
17
+ class HttpCallWrapper < ConnectorsSdk::Base::HttpCallWrapper
18
+ SERVICE_TYPE = 'confluence_cloud'
19
+
20
+ def name
21
+ 'Confluence Cloud'
22
+ end
23
+
24
+ def service_type
25
+ SERVICE_TYPE
26
+ end
27
+
28
+ private
29
+
30
+ def extractor_class
31
+ ConnectorsSdk::ConfluenceCloud::Extractor
32
+ end
33
+
34
+ def authorization
35
+ ConnectorsSdk::ConfluenceCloud::Authorization
36
+ end
37
+
38
+ def client(params)
39
+ ConnectorsSdk::ConfluenceCloud::CustomClient.new(:base_url => base_url(params[:cloud_id]), :access_token => params[:access_token])
40
+ end
41
+
42
+ def custom_client_error
43
+ ConnectorsSdk::Atlassian::CustomClient::ClientError
44
+ end
45
+
46
+ def config(params)
47
+ ConnectorsSdk::Atlassian::Config.new(:base_url => base_url(params[:cloud_id]), :cursors => params.fetch(:cursors, {}) || {})
48
+ end
49
+
50
+ def health_check(params)
51
+ client(params).me
52
+ end
53
+
54
+ def base_url(cloud_id)
55
+ "https://api.atlassian.com/ex/confluence/#{cloud_id}"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,10 @@
1
+ module ConnectorsSdk
2
+ module Helpers
3
+ module AtlassianTimeFormatter
4
+ def format_time(time)
5
+ time = Time.parse(time) if time.is_a?(String)
6
+ time.strftime('%Y-%m-%d %H:%M')
7
+ end
8
+ end
9
+ end
10
+ end