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