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.
- checksums.yaml +4 -4
- data/lib/connectors_sdk/atlassian/config.rb +27 -0
- data/lib/connectors_sdk/atlassian/custom_client.rb +87 -0
- data/lib/connectors_sdk/base/adapter.rb +7 -8
- data/lib/connectors_sdk/base/authorization.rb +89 -0
- data/lib/connectors_sdk/base/custom_client.rb +0 -1
- data/lib/connectors_sdk/base/extractor.rb +3 -2
- data/lib/connectors_sdk/base/http_call_wrapper.rb +135 -0
- data/lib/connectors_sdk/base/registry.rb +5 -3
- data/lib/connectors_sdk/confluence/adapter.rb +216 -0
- data/lib/connectors_sdk/confluence/custom_client.rb +143 -0
- data/lib/connectors_sdk/confluence/extractor.rb +270 -0
- data/lib/connectors_sdk/confluence_cloud/authorization.rb +64 -0
- data/lib/connectors_sdk/confluence_cloud/custom_client.rb +61 -0
- data/lib/connectors_sdk/confluence_cloud/extractor.rb +59 -0
- data/lib/connectors_sdk/confluence_cloud/http_call_wrapper.rb +59 -0
- data/lib/connectors_sdk/helpers/atlassian_time_formatter.rb +10 -0
- data/lib/connectors_sdk/office365/adapter.rb +7 -7
- data/lib/connectors_sdk/office365/config.rb +1 -0
- data/lib/connectors_sdk/office365/custom_client.rb +31 -9
- data/lib/connectors_sdk/office365/extractor.rb +8 -8
- data/lib/connectors_sdk/share_point/adapter.rb +12 -12
- data/lib/connectors_sdk/share_point/authorization.rb +14 -62
- data/lib/connectors_sdk/share_point/extractor.rb +2 -2
- data/lib/connectors_sdk/share_point/http_call_wrapper.rb +24 -83
- data/lib/connectors_shared/exception_tracking.rb +4 -4
- data/lib/connectors_shared/extraction_utils.rb +109 -0
- data/lib/connectors_shared/middleware/basic_auth.rb +27 -0
- data/lib/connectors_shared/middleware/bearer_auth.rb +27 -0
- data/lib/connectors_shared/middleware/restrict_hostnames.rb +73 -0
- data/lib/connectors_shared/monitor.rb +3 -3
- data/lib/stubs/enterprise_search/exception_tracking.rb +43 -0
- metadata +22 -10
- data/lib/connectors_sdk/base/.config.rb.un~ +0 -0
- data/lib/connectors_sdk/base/.connectors.rb.un~ +0 -0
- data/lib/connectors_sdk/base/.registry.rb.un~ +0 -0
- 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
|