ginjo-omniauth-slack 2.4.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
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