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

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 (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