connectors_sdk 8.2.0.0 → 8.3.0.0.pre.20220517T144653Z

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 (35) 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 +1 -2
  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 +265 -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 +25 -64
  21. data/lib/connectors_sdk/office365/extractor.rb +18 -34
  22. data/lib/connectors_sdk/share_point/adapter.rb +24 -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/job_status.rb +18 -0
  29. data/lib/connectors_shared/middleware/basic_auth.rb +27 -0
  30. data/lib/connectors_shared/middleware/bearer_auth.rb +27 -0
  31. data/lib/connectors_shared/middleware/restrict_hostnames.rb +73 -0
  32. data/lib/connectors_shared/monitor.rb +3 -3
  33. data/lib/connectors_shared.rb +1 -0
  34. data/lib/stubs/enterprise_search/exception_tracking.rb +43 -0
  35. metadata +23 -5
@@ -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,265 @@
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)
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
+ end
54
+ end
55
+
56
+ def yield_deleted_ids(ids)
57
+ id_groups = ids.group_by do |id|
58
+ if Confluence::Adapter.es_id_is_confluence_space_id?(id)
59
+ :space
60
+ elsif Confluence::Adapter.es_id_is_confluence_content_id?(id)
61
+ :content
62
+ elsif Confluence::Adapter.es_id_is_confluence_attachment_id?(id)
63
+ :attachment
64
+ else
65
+ :unknown
66
+ end
67
+ end
68
+
69
+ %i(space content attachment).each do |group|
70
+ confluence_ids = Array(id_groups[group]).map { |id| Confluence::Adapter.public_send("es_id_to_confluence_#{group}_id", id) }
71
+ get_ids_for_deleted(confluence_ids, group).each do |deleted_id|
72
+ yield Confluence::Adapter.public_send("confluence_#{group}_id_to_es_id", deleted_id)
73
+ end
74
+ end
75
+ end
76
+
77
+ def download(item)
78
+ content = item[:content]
79
+ client.download("#{content._links.base}#{content._links.download}").body
80
+ end
81
+
82
+ private
83
+
84
+ def content_base_url
85
+ 'https://workplace-search.atlassian.net/wiki'
86
+ end
87
+
88
+ def yield_spaces
89
+ @space_cursor ||= 0
90
+ loop do
91
+ response = client.spaces(:start_at => @space_cursor, :include_permissions => config.index_permissions)
92
+ response.results.each do |space|
93
+ yield space
94
+ @space_cursor += 1
95
+ end
96
+ break unless should_continue_looping?(response)
97
+ log_info("Requesting more spaces with cursor: #{@space_cursor}")
98
+ end
99
+ end
100
+
101
+ def yield_content_for_space(space:, types:, modified_since:)
102
+ loop do
103
+ response = client.content(
104
+ :space => space,
105
+ :types => types,
106
+ :order_field_and_direction => modified_since ? 'lastmodified asc' : 'created asc',
107
+ cursoring_param(:modified_since => modified_since, :space => space) => cursoring_value(:modified_since => modified_since, :space => space)
108
+ )
109
+
110
+ response.results.each do |result|
111
+ yield_single_document_change(:identifier => "Confluence ID: #{result&.id} (#{result&.webui})") do
112
+ content = client.content_by_id(result.id, :include_permissions => config.index_permissions)
113
+ yield content if content.status == 'current'
114
+ end
115
+ end
116
+ update_content_cursors(space, response, modified_since)
117
+
118
+ break unless should_continue_looping?(response)
119
+ 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)}")
120
+ end
121
+ end
122
+
123
+ def next_cursor_key(modified_since:)
124
+ modified_since ? CONTENT_MODIFIED_SINCE_NEXT_CURSOR_KEY : CONTENT_NEXT_CURSOR_KEY
125
+ end
126
+
127
+ def cursoring_param(modified_since:, space:)
128
+ return :next_value if config.cursors.dig(next_cursor_key(:modified_since => modified_since), space).present?
129
+
130
+ modified_since ? :updated_after : :start_at
131
+ end
132
+
133
+ def cursoring_value(modified_since:, space:)
134
+ next_value = config.cursors.dig(next_cursor_key(:modified_since => modified_since), space)
135
+ return next_value if next_value.present?
136
+
137
+ get_content_cursors(space, modified_since)[space]
138
+ end
139
+
140
+ def update_content_cursors(space, response, modified_since)
141
+ if response._links&.next.present?
142
+ config.cursors[next_cursor_key(:modified_since => modified_since)] ||= {}
143
+ config.cursors[next_cursor_key(:modified_since => modified_since)][space] = response._links.next
144
+ end
145
+
146
+ if response.results && modified_since
147
+ updated_cursor = response.results.last&.history&.lastUpdated&.when
148
+ config.cursors[CONTENT_MODIFIED_SINCE_CURSOR_KEY][space] = updated_cursor if updated_cursor
149
+ else
150
+ config.cursors[CONTENT_OFFSET_CURSOR_KEY][space] += response.results.size
151
+ end
152
+ end
153
+
154
+ def get_content_cursors(space, modified_since)
155
+ modified_since ? get_content_modified_since_cursors(space, modified_since) : get_content_offset_cursors(space)
156
+ end
157
+
158
+ def get_content_offset_cursors(space)
159
+ config.cursors[CONTENT_OFFSET_CURSOR_KEY] ||= {}
160
+ config.cursors[CONTENT_OFFSET_CURSOR_KEY][space] ||= 0
161
+ config.cursors[CONTENT_OFFSET_CURSOR_KEY]
162
+ end
163
+
164
+ def get_content_modified_since_cursors(space, modified_since)
165
+ config.cursors[CONTENT_MODIFIED_SINCE_CURSOR_KEY] ||= {}
166
+ config.cursors[CONTENT_MODIFIED_SINCE_CURSOR_KEY][space] ||= modified_since.to_s
167
+ config.cursors[CONTENT_MODIFIED_SINCE_CURSOR_KEY]
168
+ end
169
+
170
+ def should_continue_looping?(response)
171
+ response.results&.size.to_i > 0 && response._links.next.present?
172
+ end
173
+
174
+ def get_ids_for_deleted(search_ids, group)
175
+ return [] if search_ids.empty?
176
+
177
+ response, id_sym =
178
+ if group == :space
179
+ [client.spaces(:space_keys => search_ids, :limit => search_ids.size), :key]
180
+ else
181
+ [client.content_search("id in (#{search_ids.join(',')})", :limit => search_ids.size), :id]
182
+ end
183
+ found_ids = response.results.map { |result| result.fetch(id_sym) }.map(&:to_s)
184
+ search_ids - found_ids
185
+ end
186
+
187
+ def get_space_permissions(space)
188
+ space_permissions = space.permissions&.select { |permission| %w(read administer).include?(permission.operation.operation) }
189
+ if space_permissions.nil? || space_permissions.any?(&:anonymousAccess)
190
+ @space_permissions_cache[space.fetch('key')] = []
191
+ return []
192
+ end
193
+
194
+ subjects = space_permissions.flat_map(&:subjects)
195
+ permissions = [
196
+ subjects.select { |subject| subject.has_key?(:user) }.map { |subject| subject.user.results.map { |user| "user:#{user.accountId}" } },
197
+ subjects.select { |subject| subject.has_key?(:group) }.map { |subject| subject.group.results.map { |group| "group:#{group.name}" } }
198
+ ].flatten.uniq
199
+ @space_permissions_cache[space.fetch('key')] = permissions
200
+ end
201
+
202
+ def get_user_spaces(user_id)
203
+ (@space_permissions_cache || {}).select { |_, permissions| "user:#{user_id}".in?(permissions) }.keys.uniq
204
+ end
205
+
206
+ def get_group_spaces(group_name)
207
+ (@space_permissions_cache || {}).select { |_, permissions| "group:#{group_name}".in?(permissions) }.keys.uniq
208
+ end
209
+
210
+ # get_content_restrictions returns the final restriction of the content, taking into consideration inherited restrictions
211
+ # if the content is an attachment, the final restriction will will be either space permissions
212
+ # or (in case they aren't empty) the intersection of
213
+ # 1. its restriction
214
+ # 2. its container's restriction
215
+ # 3. its container's ancestors' restriction
216
+ # if the content is a page or blog post, the final restriction will be either space permissions
217
+ # or (in case they aren't empty) the intersection of
218
+ # 1. its restrictions
219
+ # 2. its ancestors' restrictions
220
+ def get_content_restrictions(content)
221
+ restrictions = []
222
+ restrictions << extract_restrictions(content.restrictions&.read&.restrictions)
223
+
224
+ # space permissions should always be available in cache
225
+ space_key = content.space&.fetch('key')
226
+ space_restrictions = (@space_permissions_cache[space_key] || [])
227
+
228
+ ancestors = content.ancestors || []
229
+
230
+ if content.type == 'attachment'
231
+ if content.container&.type == 'global'
232
+ log_info("Skipping ancestor restrictions as it is a space and should already be cached: [#{content.id}] in [#{content.container&.type}_#{content.container&.id}]")
233
+ else
234
+ container = client.content_by_id(content.container&.id, :include_permissions => true)
235
+ @content_restriction_cache[container.id] ||= extract_restrictions(container.restrictions&.read&.restrictions)
236
+ restrictions << @content_restriction_cache[container.id]
237
+ ancestors = container.ancestors
238
+ end
239
+ end
240
+
241
+ ancestors.each do |ancestor|
242
+ restrictions << get_restrictions_by_content_id(ancestor.id)
243
+ end
244
+
245
+ combined_restrictions = restrictions.select(&:any?).reduce(:&)
246
+ @content_restriction_cache[content.id] = combined_restrictions || []
247
+ (combined_restrictions || space_restrictions).map { |restriction| "#{space_key}/#{restriction}" }
248
+ end
249
+
250
+ def get_restrictions_by_content_id(content_id)
251
+ @content_restriction_cache[content_id] ||= begin
252
+ content = client.content_by_id(content_id, :include_permissions => true)
253
+ extract_restrictions(content.restrictions&.read&.restrictions)
254
+ end
255
+ end
256
+
257
+ def extract_restrictions(restrictions)
258
+ [
259
+ restrictions&.user&.results&.map { |user| "user:#{user.accountId}" },
260
+ restrictions&.group&.results&.map { |group| "group:#{group.name}" }
261
+ ].flatten.compact.uniq
262
+ end
263
+ end
264
+ end
265
+ 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