ginjo-omniauth-slack 2.4.1 → 2.5.0

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.
data/Rakefile CHANGED
@@ -3,6 +3,7 @@ require "rake/testtask"
3
3
 
4
4
  Rake::TestTask.new do |task|
5
5
  task.libs << "test"
6
+ #task.test_files = FileList['test/*test.rb']
6
7
  end
7
8
 
8
9
  task :default => :test
@@ -0,0 +1,57 @@
1
+ require 'omniauth-slack/refinements'
2
+ require 'logger'
3
+
4
+ # Appends 'TRACE' option to Logger levels.
5
+ sev_label_new = Logger::SEV_LABEL.dup
6
+ sev_label_new << 'TRACE'
7
+ Logger::Severity::TRACE = (sev_label_new.size - 1)
8
+ Logger.send(:remove_const, :SEV_LABEL)
9
+ Logger::SEV_LABEL = sev_label_new.freeze
10
+
11
+ module OmniAuth
12
+ module Slack
13
+ module Debug
14
+ #using ObjectRefinements
15
+ LOG_ALL = %w(1 true yes all debug)
16
+ LOG_NONE = %w(0 false no none nil nill null)
17
+
18
+ include CallerMethodName
19
+
20
+ def self.included(other)
21
+ other.send(:include, CallerMethodName)
22
+ other.send(:extend, Extensions)
23
+ end
24
+
25
+ module Extensions
26
+ def debug(method_name = nil, klass=nil, &block)
27
+ method_name ||= caller_method_name
28
+ klass ||= self
29
+ filter = ENV['OMNIAUTH_SLACK_DEBUG']
30
+ return if filter.nil? || filter.to_s=='' || LOG_NONE.include?(filter.to_s.downcase)
31
+ klass = case klass
32
+ when Class; klass.name
33
+ when Module; klass.name
34
+ when String; klass
35
+ else klass.to_s
36
+ end
37
+ klass_name = klass.to_s.split('::').last.to_s
38
+ log_text = yield
39
+ full_text = "(#{klass_name} #{method_name}) #{log_text}" #{Thread.current.object_id}
40
+
41
+ if filter && !LOG_ALL.include?(filter.to_s.downcase)
42
+ regexp = filter.is_a?(Regexp) ? filter : Regexp.new(filter.to_s, true)
43
+ return unless full_text[regexp]
44
+ end
45
+
46
+ OmniAuth.logger.log(Logger::TRACE, full_text)
47
+ end
48
+ end
49
+
50
+ def debug(method_name=nil, klass=nil, &block)
51
+ method_name ||= caller_method_name
52
+ self.class.debug(method_name, klass, &block)
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,382 @@
1
+ # :markup: tomdoc
2
+
3
+ require 'oauth2/access_token'
4
+ require 'omniauth-slack/refinements'
5
+ require 'omniauth-slack/debug'
6
+
7
+ module OmniAuth
8
+ module Slack
9
+ using StringRefinements
10
+ using OAuth2Refinements
11
+
12
+ module OAuth2
13
+
14
+ # This is an enhanced subclass of OAuth2::AccessToken.
15
+ # It handles Slack-specific data and behavior, and it also adds
16
+ # a scope-query method has_scope?
17
+ #
18
+ # The AccessToken object is built with the hash returned from the get-token API request
19
+ # or is passed in manually via AccessToken.from_hash() or OmniAuth::Slack::build_access_token.
20
+ #
21
+ # The original hash from the API can always be found at AccessToken#params.
22
+ # As a convenience, you can call '[]' on the params hash by sending
23
+ # the method and args directly to the access-token object.
24
+ #
25
+ # my_token['ok'] --> true
26
+ # my_token['app_id'] --> A012345678
27
+ #
28
+ # The AccessToken instance provides getter methods for top-level data points
29
+ # of the returned token hash. See define_getters call below for more convenience methods.
30
+ #
31
+ # See Slack's documentation for the different types of tokens available.
32
+ #
33
+ # * https://api.slack.com/methods/oauth.access
34
+ # * https://api.slack.com/methods/oauth.v2.access
35
+ #
36
+ class AccessToken < ::OAuth2::AccessToken
37
+ include OmniAuth::Slack::Debug
38
+
39
+ # Creates simple getter methods to pull specific data from the raw token hash.
40
+ def self.define_getters(ary_of_words)
41
+ ary_of_words.each do |word|
42
+ obj, atrb = word.split('_')
43
+ define_method(word) do
44
+ rslt = (
45
+ params[word] ||
46
+ params && params[obj] && params[obj][atrb]
47
+ )
48
+ debug { "Simple getter '#{word}' rslt: #{rslt}" }
49
+ rslt
50
+ end
51
+ end
52
+ end
53
+
54
+ define_getters %w(
55
+ app_id
56
+ authorizing_user
57
+ enterprise
58
+ installer_user
59
+ scope
60
+ team
61
+ team_id
62
+ team_name
63
+ team_domain
64
+ user
65
+ )
66
+
67
+ # Check's the token hash 'ok' field.
68
+ #
69
+ # Returns true or false, representing the success status of the token response.
70
+ #
71
+ def ok?
72
+ params['ok'] == true ||
73
+ params['ok'].to_s[/true/i] ||
74
+ false
75
+ end
76
+
77
+ # Intercepts super to return nil instead of an empty string.
78
+ #
79
+ # Returns the token string or nil.
80
+ #
81
+ def token
82
+ rslt = super
83
+ rslt.to_s == '' ? nil : rslt
84
+ end
85
+
86
+ # Inspects the token and determines token type.
87
+ #
88
+ # Returns a string representing token type.
89
+ #
90
+ def token_type
91
+ params['token_type'] ||
92
+ case
93
+ when
94
+ params['token_type'] == 'user' ||
95
+ @token.to_s[/xoxp/]; 'user'
96
+ when
97
+ params['token_type'] == 'bot' ||
98
+ @token.to_s[/xoxb/]; 'bot'
99
+ when
100
+ params['token_type'] == 'app' ||
101
+ @token.to_s[/xoxa/]; 'app'
102
+ when
103
+ @token.to_s[/xoxr/]; 'refresh'
104
+ end
105
+ end
106
+
107
+ # Compares given token type with actual token_type.
108
+ #
109
+ # Returns true if given type matches actual token_type, otherwise false.
110
+ #
111
+ def token_type?(*_type)
112
+ #debug{"'#{_type}'"}
113
+ [_type].flatten.any? do |t|
114
+ token_type.to_s == t.to_s
115
+ end || false
116
+ end
117
+
118
+ # Converts 'authed_user' hash (of Slack v2 oauth flow) to AccessToken object.
119
+ #
120
+ # Returns an AccessToken instance or nil.
121
+ #
122
+ def user_token
123
+ @user_token ||= (
124
+ if token_type?('user')
125
+ self
126
+ elsif params['authed_user']
127
+ rslt = self.class.from_hash(client, params['authed_user']).tap do |t|
128
+ t.params['token_type'] = 'user'
129
+ t.params['team_id'] = team_id
130
+ end
131
+ end
132
+ )
133
+ end
134
+ alias_method :authed_user, :user_token
135
+
136
+ # Gets the AccessToken person-user-id if it exists.
137
+ #
138
+ # Returns string or nil.
139
+ #
140
+ def user_id
141
+ rslt = (
142
+ # classic token.
143
+ params['user_id'] ||
144
+ # from sub-token in 'authed_user'
145
+ params['authed_user'].to_h['id'] ||
146
+ # workspace-app token with attached user.
147
+ params['user'].to_h['id'] ||
148
+ # workspace-app token with authorizing user.
149
+ params['authorizing_user'].to_h['user_id'] ||
150
+ # workspace-app token with installer user.
151
+ params['installer_user'].to_h['user_id'] ||
152
+ # user-id from authed_user hash.
153
+ params['id'] #||
154
+ # v2 api bot token, as a last resort.
155
+ #params['bot_user_id']
156
+ )
157
+ debug { rslt }
158
+ rslt
159
+ end
160
+
161
+ # Gets the AccessToken unique user-team-id combo, if it can be determined.
162
+ #
163
+ # Returns string or nil.
164
+ #
165
+ def uid
166
+ rslt = (user_id && team_id) ? "#{user_id}-#{team_id}" : nil
167
+ debug { rslt }
168
+ rslt
169
+ end
170
+
171
+ # Gets user_name from wherever it can be found in the returned token,
172
+ # regardless of what type of token is returned.
173
+ #
174
+ # Returns string or nil.
175
+ #
176
+ def user_name
177
+ params['user_name'] ||
178
+ # from sub-token in 'authed_user'
179
+ params['authed_user'].to_h['name'] ||
180
+ # workspace-app token with attached user.
181
+ params['user'].to_h['name'] ||
182
+ # from authed_user hash.
183
+ params['name'] ||
184
+ # workspace token with authorizing user.
185
+ params['authorizing_user'].to_h['name'] ||
186
+ # workspace token with installer user.
187
+ params['installer_user'].to_h['name'] ||
188
+ # more workspace token possibilities.
189
+ to_auth_hash.deep_find('nickname') ||
190
+ to_auth_hash.deep_find('real_name')
191
+ end
192
+
193
+ # Gets bot_user_id if it exists.
194
+ #
195
+ # Returns string or nil.
196
+ #
197
+ def bot_user_id
198
+ params['bot_user_id']
199
+ end
200
+
201
+ # Gets the app_user_id if it exists.
202
+ #
203
+ # Returns string or nil.
204
+ #
205
+ def app_user_id
206
+ params['app_user_id']
207
+ end
208
+
209
+ # Experimental, converts this AccessToken instance to an AuthHash object.
210
+ #
211
+ # Returns OmniAuth::Slack::AuthHash instance.
212
+ #
213
+ def to_auth_hash
214
+ Module.const_get('::OmniAuth::Slack::AuthHash').new(params)
215
+ end
216
+
217
+ # Compiles scopes awarded to this AccessToken.
218
+ #
219
+ # Returns hash of scope arrays where *key* is scope section
220
+ # and *value* is Array of scopes.
221
+ #
222
+ def all_scopes
223
+ @all_scopes ||= (
224
+ case
225
+ when ! params['scope'].to_s.empty?
226
+ {'classic' => params['scope'].words}
227
+ when params['scopes'].to_h.any?
228
+ params['scopes']
229
+ end
230
+ )
231
+ end
232
+ alias_method :scopes, :all_scopes
233
+
234
+ # Match a given set of scopes against this token's awarded scopes,
235
+ # classic and workspace token compatible.
236
+ #
237
+ # If the scope-query is a string, it will be interpreted as a Slack Classic App
238
+ # scope string {classic: scope-query-string}, even if the token is a v2 token.
239
+ #
240
+ # The keywords need to be symbols, so any hash passed as an argument
241
+ # (or as the entire set of args) should have symbolized keys!
242
+ #
243
+ # freeform_array - [*Array, nil] default: [], array of scope query hashes or string(s).
244
+ #
245
+ # :query - [Hash, Array, nil] default: nil, a single scope-query Hash (or Array of Hashes).
246
+ #
247
+ # :logic - [String, Symbol] default: 'or' [:or | :and] logic for the scope-query.
248
+ # Applies to a single query hash.
249
+ # The reverse logic is applied to an array of query hashes.
250
+ #
251
+ # :user - [String] (nil) default: nil, user_id of the Slack user to query against.
252
+ # Leave blank for non-user queries.
253
+ #
254
+ # :base - [Hash] default: nil, a set of scopes to query against
255
+ # defaults to the awarded scopes on this token.
256
+ #
257
+ # freeform_hash - [**Hash] default: {}, interpreted as single scope query hash.
258
+ #
259
+ def has_scope?(*freeform_array, query: nil, logic:'or', user:nil, base:nil, **freeform_hash)
260
+ #OmniAuth.logger.debug({freeform_array:freeform_array, freeform_hash:freeform_hash, query:query, logic:logic, user:user, base:base})
261
+ debug{{freeform_array:freeform_array, freeform_hash:freeform_hash, query:query, logic:logic, user:user, base:base}}
262
+
263
+ query ||= case
264
+ #when simple_string; {classic: simple_string}
265
+ when freeform_array.any?; freeform_array
266
+ when freeform_hash.any?; freeform_hash
267
+ end
268
+ return unless query
269
+
270
+ query = [query].flatten if query.is_a?(Array) || query.is_a?(String)
271
+
272
+ user ||= user_id
273
+ debug{"using user '#{user}' and query '#{query}'"}
274
+
275
+ is_identity_query = case query
276
+ when Hash
277
+ query.keys.detect{|k| k.to_s == 'identity'}
278
+ when Array
279
+ query.detect{ |q| q.is_a?(Hash) && q.keys.detect{|k| k.to_s == 'identity'} }
280
+ end
281
+
282
+ base ||= case
283
+ when user && is_identity_query
284
+ #debug{"calling all_scopes(user=#{user}) to build base-scopes"}
285
+ all_scopes(user)
286
+ else
287
+ #debug{"calling all_scopes to build base-scopes"}
288
+ all_scopes
289
+ end
290
+
291
+ #debug{{freeform_array:freeform_array, freeform_hash:freeform_hash, query:query, logic:logic, user:user, base:base}}
292
+ self.class.has_scope?(scope_query:query, scope_base:base, logic:logic)
293
+ end
294
+
295
+ # Matches the given scope_query against the given scope_base, with the given logic.
296
+ #
297
+ # This is classic and workspace token compatible.
298
+ #
299
+ # keywords - All arguments are keyword arguments:
300
+ #
301
+ # :scope_query - [Hash, Array of hashes] default: {}.
302
+ # If scope_query is a string, it will be interpreted as {classic: scope-query-string}.
303
+ #
304
+ # key - Symbol of scope type, can be:
305
+ # [app_home|team|channel|group|mpim|im|identity|classic].
306
+ #
307
+ # value - Array or String of individual scopes.
308
+ #
309
+ # :scope_base - [Hash] defaul: {}, represents the set of scopes to query against.
310
+ #
311
+ # :logic - [String, Symbol] default: or. One of [and|or].
312
+ # Applies to a single query hash.
313
+ # The reverse logic is applied to an array of query hashes.
314
+ #
315
+ # Examples
316
+ #
317
+ # has_scope?(scope_query: {channel: 'channels:read chat:write'})
318
+ # has_scope?(scope_query: [{identity:'uers:read', channel:'chat:write'}, {app_home:'chat:write'}], logic:'and')
319
+ # has_scope?(scope_query: 'identity:users identity:team identity:avatar')
320
+ #
321
+ def self.has_scope?(scope_query:{}, scope_base:{}, logic:'or')
322
+ debug{"class-level scope_query '#{scope_query}' scope_base '#{scope_base}' logic '#{logic}'"}
323
+ _scope_query = scope_query.is_a?(String) ? {classic: scope_query} : scope_query
324
+ _scope_query = [_scope_query].flatten
325
+
326
+ # Converts array of unknown strings to uniform hash of classic:[array-of-scope-strings].
327
+ if _scope_query.is_a?(Array)
328
+ new_query = []
329
+ classic_array = []
330
+ _scope_query.each_with_index do |q,n|
331
+ if q.is_a?(String)
332
+ classic_array.concat(q.words)
333
+ debug{"building classic_array with words from string '#{q.words}' to give: #{classic_array}"}
334
+ else
335
+ new_query << _scope_query[n]
336
+ end
337
+ end
338
+ if classic_array.any?
339
+ new_query.unshift({classic: classic_array.flatten.uniq})
340
+ end
341
+ _scope_query = new_query
342
+ end
343
+
344
+ _scope_base = scope_base
345
+ raise "scope_base must be a hash" unless (_scope_base.is_a?(Hash) || _scope_base.respond_to?(:to_h))
346
+
347
+ out=false
348
+
349
+ _logic = case
350
+ when logic.to_s.downcase == 'or'; {outter: 'all?', inner: 'any?'}
351
+ when logic.to_s.downcase == 'and'; {outter: 'any?', inner: 'all?'}
352
+ else {outter: 'all?', inner: 'any?'}
353
+ end
354
+ debug{"_logic #{_logic.inspect}"}
355
+ debug{"_scope_query #{_scope_query}"}
356
+
357
+ _scope_query.send(_logic[:outter]) do |query|
358
+ debug{"outter query: #{_scope_query.inspect}"}
359
+
360
+ query.send(_logic[:inner]) do |section, scopes|
361
+ test_scopes = case
362
+ when scopes.is_a?(String); scopes.words
363
+ when scopes.is_a?(Array); scopes
364
+ else raise "Scope data must be a string or array of strings, like this {team: 'chat:write,team:read', channels: ['channels:read', 'chat:write']}"
365
+ end
366
+
367
+ test_scopes.send(_logic[:inner]) do |scope|
368
+ debug{"inner query section: #{section.to_s}, scope: #{scope}"}
369
+ out = _scope_base.to_h[section.to_s].to_a.include?(scope.to_s)
370
+ end
371
+ end
372
+
373
+ end # scope_query.send outter-query
374
+ debug{"output: #{out}"}
375
+ return out
376
+
377
+ end # self.has_scope?
378
+
379
+ end # AccessToken
380
+ end
381
+ end
382
+ end
@@ -0,0 +1,95 @@
1
+ require 'oauth2/client'
2
+ require 'oauth2/response'
3
+ require 'omniauth'
4
+ require 'omniauth-slack/debug'
5
+ require 'omniauth-slack/oauth2/access_token'
6
+ require 'omniauth-slack/omniauth/auth_hash'
7
+
8
+ module OmniAuth
9
+ module Slack
10
+ module OAuth2
11
+ class Client < ::OAuth2::Client
12
+
13
+ include OmniAuth::Slack::Debug
14
+
15
+ #using StringRefinements
16
+ #using OAuth2Refinements
17
+
18
+ # If this is an array, request history will be stored.
19
+ # Only store request history if each Client instance is relatively short-lived.
20
+ #
21
+ # From your app, you can set this:
22
+ # OmniAuth::Slack::OAuth2::Client::HISTORY_DEFAULT ||= []
23
+ #
24
+ # Then, in your authorization callback action, you can direct
25
+ # the OAuth2::Client request history to the AuthHash#['extra']['raw_info']:
26
+ # @auth_hash = env['omniauth.auth']
27
+ # @access_token = env['omniauth.strategy'].access_token
28
+ # @access_token.client.history = @auth_hash.extra.raw_info
29
+ #
30
+ # TODO: The above seems a little messy. Maybe use a proc
31
+ # to rediredct Client request history to wherever.
32
+ # Or maybe don't offer any history storage at all.
33
+ #
34
+ HISTORY_DEFAULT=nil
35
+ SUBDOMAIN_DEFAULT=nil
36
+
37
+ attr_accessor :logger, :history, :subdomain
38
+
39
+ def initialize(*args, **options)
40
+ debug{"args: #{args}"}
41
+ super
42
+ self.logger = OmniAuth.logger
43
+ self.history ||= (options[:history] || HISTORY_DEFAULT)
44
+ # Moved 'dup' to it's own line, cuz we can't dup nil in older ruby version.
45
+ self.history && self.history = self.history.dup
46
+ self.subdomain ||= options[:subdomain] || SUBDOMAIN_DEFAULT
47
+ end
48
+
49
+ # Wraps OAuth2::Client#get_token to pass in the omniauth-slack AccessToken class.
50
+ def get_token(params, access_token_opts = {}, access_token_class = OmniAuth::Slack::OAuth2::AccessToken) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
51
+ debug{"params #{params}, access_token_opts #{access_token_opts}"}
52
+ rslt = super(params, access_token_opts, access_token_class)
53
+ debug{"Client #{self} built AccessToken #{rslt}"}
54
+ rslt
55
+ end
56
+
57
+ # Logs each API request and stores the API result in History array (if exists).
58
+ #
59
+ # Storage can be disabled by setting client_options: {history: false}.
60
+ # Storage can be enabled by setting client_options: {history: Array.new}.
61
+ # Storage is enabled by default, when client is created from Strategy.
62
+ #
63
+ #
64
+ def request(*args)
65
+ logger.debug "(slack) API request '#{args[0..1]}'." # in thread '#{Thread.current.object_id}'." # by Client '#{self}'
66
+ debug{"API request args #{args}"}
67
+ request_output = super(*args)
68
+ uri = args[1].to_s.gsub(/^.*\/([^\/]+)/, '\1') # use single-quote or double-back-slash for replacement.
69
+ if history.is_a?(Array)
70
+ debug{"Saving response to history object #{history.object_id}"}
71
+ history << OmniAuth::Slack::AuthHash.new(
72
+ {api_call: uri.to_s, time: Time.now, response: request_output}
73
+ )
74
+ end
75
+ #debug{"API response (#{args[0..1]}) #{request_output.class}"}
76
+ debug{"API response #{request_output.response.env.body}"}
77
+ request_output
78
+ end
79
+
80
+ # Wraps #site to insert custom subdomain for API calls.
81
+ def site(*args)
82
+ if !@subdomain.to_s.empty?
83
+ site_uri = URI.parse(super)
84
+ site_uri.host = "#{@subdomain}.#{site_uri.host}"
85
+ logger.debug "(slack) Oauth site uri with custom team_domain #{site_uri}"
86
+ site_uri.to_s
87
+ else
88
+ super
89
+ end
90
+ end
91
+
92
+ end
93
+ end
94
+ end
95
+ end