anoubis_sso_client 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d6d6fc2d74ea7bf5a7702de2638af217e68f7095567ea9a179de4b139a01b5cd
4
+ data.tar.gz: c5978605ce5ec97d9e8bc5e1f632a15f36cb8a6ce7573b6ce8694adb67b79123
5
+ SHA512:
6
+ metadata.gz: 658c4bf239c8dcab773f3551a93d6ca1649ea2b0ab5a95ecdadce60ec5ee14bbc59586b56aec6cb92b4e3c96983a405f3d0b23d592c2f538896aceae3dbf56fe
7
+ data.tar.gz: a01020e0880338c9a014466c3ead470b499505c3216fc45a67f62a56b224e40e1f9d0833f01168436a522b25126e77aea0f1756dde56c881cdd54676b811b32b
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Andrey Ryabov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # AnoubisSsoClient
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "anoubis_sso_client"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install anoubis_sso_client
22
+ ```
23
+
24
+ ## Configuration parameters
25
+
26
+ This configuration parameters can be placed at files config/application.rb for global configuration or config/environments/<environment>.rb for custom environment configuration.
27
+
28
+ ```ruby
29
+ config.anoubis_sso_server = 'https://sso.example.com/' # Full URL of SSO server (*required)
30
+ config.anoubis_sso_user_model = 'AnoubisSsoClient::User'# Used User model. (By default used AnoubisSsoServer::User model) (*optional)
31
+ config.anoubis_sso_menu_model = 'AnoubisSsoClient::Menu'# Used Menu model. (By default used AnoubisSsoServer::Menu model) (*optional)
32
+ config.anoubis_sso_group_model = 'AnoubisSsoClient::Group'# Used Group model. (By default used AnoubisSsoServer::Group model) (*optional)
33
+ config.anoubis_sso_group_menu_model = 'AnoubisSsoClient::GroupMenu'# Used GroupMenu model. (By default used AnoubisSsoServer::GroupMenu model) (*optional)
34
+ config.anoubis_sso_group_user_model = 'AnoubisSsoClient::GroupUser'# Used GroupMenu model. (By default used AnoubisSsoServer::GroupUser model) (*optional)
35
+ ```
36
+
37
+ ## Contributing
38
+ Contribution directions go here.
39
+
40
+ ## License
41
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
13
+
14
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
15
+ load 'rails/tasks/engine.rake'
@@ -0,0 +1,516 @@
1
+ ##
2
+ # Common functions for all controllers
3
+ module AnoubisSsoClient::ApplicationCommon
4
+ ## Returns [Anoubis::Etc::Base] global system parameters
5
+ attr_accessor :etc
6
+
7
+ ## Returns main SSO server URL.
8
+ attr_accessor :sso_server
9
+
10
+ ## Returns SSO JWK data URL
11
+ attr_accessor :sso_jwk_data_url
12
+
13
+ ## Returns SSO userinfo URL
14
+ attr_accessor :sso_userinfo_url
15
+
16
+ ## Returns Hash of current user
17
+ attr_accessor :current_user
18
+
19
+ ## Returns Hash of menu for current user
20
+ attr_accessor :current_menu
21
+
22
+ ##
23
+ # Returns main SSO server URL. Link should be defined in Rails.configuration.anoubis.sso_server configuration parameter
24
+ # @return [String] link to SSO server
25
+ def sso_server
26
+ @sso_server ||= get_sso_server
27
+ end
28
+
29
+ private def get_sso_server
30
+ begin
31
+ value = Rails.configuration.anoubis_sso_server
32
+ rescue StandardError
33
+ value = ''
34
+ render json: { error: 'Please setup Rails.configuration.anoubis_sso_server configuration variable' }
35
+ end
36
+
37
+ value
38
+ end
39
+
40
+ ##
41
+ # Returns SSO Menu model.
42
+ # Can be redefined in Rails.application configuration_anoubis_sso_menu_model configuration parameter.
43
+ # By default returns {AnoubisSsoClient::Menu} model class
44
+ # @return [Class] Menu model class
45
+ def menu_model
46
+ begin
47
+ value = Object.const_get Rails.configuration.anoubis_sso_menu_model
48
+ rescue StandardError
49
+ value = AnoubisSsoClient::Menu
50
+ end
51
+
52
+ value
53
+ end
54
+
55
+ ##
56
+ # Returns SSO Group model.
57
+ # Can be redefined in Rails.application configuration_anoubis_sso_group_model configuration parameter.
58
+ # By default returns {AnoubisSsoClient::Group} model class
59
+ # @return [Class] Group model class
60
+ def group_model
61
+ begin
62
+ value = Object.const_get Rails.configuration.anoubis_sso_group_model
63
+ rescue StandardError
64
+ value = AnoubisSsoClient::Group
65
+ end
66
+
67
+ value
68
+ end
69
+
70
+ ##
71
+ # Returns SSO GroupMenu model.
72
+ # Can be redefined in Rails.application configuration_anoubis_sso_group_menu_model configuration parameter.
73
+ # By default returns {AnoubisSsoClient::GroupMenu} model class
74
+ # @return [Class] GroupMenu model class
75
+ def group_menu_model
76
+ begin
77
+ value = Object.const_get Rails.configuration.anoubis_sso_group_menu_model
78
+ rescue StandardError
79
+ value = AnoubisSsoClient::GroupMenu
80
+ end
81
+
82
+ value
83
+ end
84
+
85
+ ##
86
+ # Returns SSO GroupUser model.
87
+ # Can be redefined in Rails.application configuration_anoubis_sso_group_user_model configuration parameter.
88
+ # By default returns {AnoubisSsoClient::GroupUser} model class
89
+ # @return [Class] GroupUser model class
90
+ def group_user_model
91
+ begin
92
+ value = Object.const_get Rails.configuration.anoubis_sso_group_user_model
93
+ rescue StandardError
94
+ value = AnoubisSsoClient::GroupUser
95
+ end
96
+
97
+ value
98
+ end
99
+
100
+ ##
101
+ # Returns SSO User model.
102
+ # Can be redefined in Rails.application configuration_anoubis_sso_user_model configuration parameter.
103
+ # By default returns {AnoubisSsoClient::User} model class
104
+ # @return [Class] User model class
105
+ def user_model
106
+ begin
107
+ value = Object.const_get Rails.configuration.anoubis_sso_user_model
108
+ rescue
109
+ value = AnoubisSsoClient::User
110
+ end
111
+
112
+ value
113
+ end
114
+
115
+ ##
116
+ # Action fires before any other actions
117
+ def after_anoubis_initialization
118
+ self.current_user = nil
119
+ self.current_menu = nil
120
+ self.sso_jwk_data_url = nil
121
+ self.sso_userinfo_url = nil
122
+ if defined? params
123
+ self.etc = Anoubis::Etc::Base.new({ params: params })
124
+ else
125
+ self.etc = Anoubis::Etc::Base.new
126
+ end
127
+
128
+ if access_allowed?
129
+ options request.method.to_s.upcase
130
+ else
131
+ render_error_exit({ error: I18n.t('anoubis.errors.access_not_allowed') })
132
+ return
133
+ end
134
+
135
+ if authenticate?
136
+ if authentication
137
+ if check_menu_access?
138
+ allow = false
139
+ if current_menu.key? params[:controller].to_sym
140
+ etc.menu = Anoubis::Etc::Menu.new current_menu[params[:controller].to_sym]
141
+ allow = true unless current_menu[params[:controller].to_sym][:access] == 'disable'
142
+ end
143
+ unless allow
144
+ render_error_exit({ error: I18n.t('anoubis.errors.access_not_allowed') })
145
+ return
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ after_sso_client_initialization
152
+ end
153
+
154
+ ##
155
+ # Procedure fires after initializes all parameters of {AnoubisSsoClient::ApplicationController}
156
+ def after_sso_client_initialization
157
+ puts etc.inspect
158
+ end
159
+
160
+ ##
161
+ # Check for site access. By default return true.
162
+ def access_allowed?
163
+ true
164
+ end
165
+
166
+ ##
167
+ # Gracefully terminate script execution with code 422 (Unprocessable entity). And JSON data
168
+ # @param data [Hash] Resulting data
169
+ # @option data [Integer] :code resulting error code
170
+ # @option data [String] :error resulting error message
171
+ def render_error_exit(data = {})
172
+ result = {
173
+ result: -1,
174
+ message: I18n.t('anoubis.error')
175
+ }
176
+
177
+ result[:result] = data[:code] if data.has_key? :code
178
+ result[:message] = data[:error] if data.has_key? :error
179
+
180
+
181
+ render json: result, status: :unprocessable_entity
182
+
183
+ begin
184
+ exit
185
+ rescue SystemExit => e
186
+ puts result[:message]
187
+ end
188
+ end
189
+
190
+ ##
191
+ # Checks if needed user authentication.
192
+ # @return [Boolean] if true, then user must be authenticated. By default application do not need authorization.
193
+ def authenticate?
194
+ true
195
+ end
196
+
197
+ ##
198
+ # Procedure authenticates user in the system
199
+ def authentication
200
+ session = get_oauth_session
201
+
202
+ unless session
203
+ render_error_exit code: -2, error: I18n.t('anoubis.errors.session_expired')
204
+ return
205
+ end
206
+
207
+ self.current_user = session[:user]
208
+ self.current_menu = session[:menu]
209
+ end
210
+
211
+ ##
212
+ # Check if menu access required
213
+ # @return [Boolean] menu access requirements
214
+ def check_menu_access?
215
+ if controller_name == 'index'
216
+ if action_name == 'login' || action_name == 'menu' || action_name == 'logout'
217
+ return false
218
+ end
219
+ end
220
+ return true
221
+ end
222
+
223
+ ##
224
+ # Return OAUTH session for current request. Session name gets from cookies. If session present but it's timeout was expired, then session regenerated.
225
+ def get_oauth_session
226
+ begin
227
+ session = JSON.parse(redis.get("#{redis_prefix}session:#{token}"),{ symbolize_names: true })
228
+ rescue
229
+ session = nil
230
+ end
231
+
232
+ return session if session
233
+
234
+ puts 'get_oauth_session'
235
+
236
+ jwt = check_sso_token
237
+
238
+ puts "JWT #{jwt}"
239
+
240
+ return nil unless jwt
241
+
242
+ ttl = jwt['exp'] - Time.now.utc.to_i
243
+
244
+ puts "Time: #{Time.now.to_i} -> #{ttl}"
245
+
246
+ return nil if ttl <= 0
247
+
248
+ session = {
249
+ user: {},
250
+ menu: {}
251
+ }
252
+
253
+ user_data = load_user_from_sso_server
254
+
255
+ puts "User data: #{user_data}"
256
+
257
+ return nil unless user_data
258
+ return nil if user_data.key? :error
259
+
260
+ c_user = user_model.where(sso_uuid: user_data[:public]).first
261
+ c_user = user_model.create(sso_uuid: user_data[:public]) unless c_user
262
+ c_user.update_user_data(user_data)
263
+ c_user.save if c_user.changed?
264
+ session[:user] = c_user.session_data
265
+ session[:menu] = load_full_menu(c_user.id)
266
+
267
+ puts session.inspect
268
+
269
+ redis.set("#{redis_prefix}session:#{token}", session.to_json, ex: ttl)
270
+
271
+ session
272
+ end
273
+
274
+ ##
275
+ # Load full menu from database
276
+ # @param [Integer] user_id - User ID
277
+ def load_full_menu(user_id)
278
+ query = <<-SQL
279
+ SELECT `t`.*
280
+ FROM
281
+ (
282
+ SELECT `t2`.`id`, `t2`.`mode`, `t2`.`action`, `t2`.`title_locale`, `t2`.`page_title_locale`, `t2`.`short_title_locale`,
283
+ `t2`.`position`, `t2`.`tab`, `t2`.`menu_id`, `t2`.`state`, MAX(`t2`.`access`) AS `access`,
284
+ `t2`.`user_id`, `t2`.`parent_mode`
285
+ FROM (
286
+ SELECT `menus`.`id`, `menus`.`id` AS `menu_id`, `menus`.`mode`, `menus`.`action`, `menus`.`title_locale`, `menus`.`page_title_locale`,
287
+ `menus`.`short_title_locale`, `menus`.`position`, `menus`.`tab`, `menus`.`menu_id` AS `parent_menu_id`, `menus`.`state`,
288
+ `group_menus`.`access`, `group_users`.`user_id`, `parent_menu`.`mode` AS `parent_mode`
289
+ FROM (`menus`, `group_menus`, `groups`, `group_users`)
290
+ LEFT JOIN `menus` AS `parent_menu` ON `menus`.`menu_id` = `parent_menu`.`id`
291
+ WHERE `menus`.`id` = `group_menus`.`menu_id` AND `menus`.`status` = 0 AND `group_menus`.`group_id` = `groups`.`id` AND
292
+ `groups`.`id` = `group_users`.`group_id` AND `group_users`.`user_id` = #{user_id}
293
+ ) AS `t2`
294
+ GROUP BY `t2`.`id`, `t2`.`mode`, `t2`.`action`, `t2`.`title_locale`, `t2`.`page_title_locale`, `t2`.`short_title_locale`,
295
+ `t2`.`position`, `t2`.`tab`, `t2`.`menu_id`, `t2`.`state`, `t2`.`user_id`, `t2`.`parent_mode`
296
+ ) AS `t`
297
+ ORDER BY `t`.`tab`, `t`.`position`
298
+ SQL
299
+
300
+ result = {}
301
+ group_menu_model.find_by_sql(query).each do |data|
302
+ result[data.mode.to_sym] = {
303
+ mode: data.mode,
304
+ title: data.title,
305
+ page_title: data.page_title,
306
+ short_title: data.short_title,
307
+ position: data.position,
308
+ tab: data.tab,
309
+ action: data.action,
310
+ access: data.access,
311
+ state: menu_model.states.invert[data.state],
312
+ parent: data.parent_mode
313
+ }
314
+ #self.output[:data].push menu_id[data.id.to_s.to_sym]
315
+ end
316
+
317
+ result
318
+ end
319
+
320
+ ##
321
+ # Get current token based on HTTP Authorization
322
+ # @return [String] current token
323
+ def token
324
+ return params[:oauth_token] if params.key? :oauth_token
325
+ request.env.fetch('HTTP_AUTHORIZATION', '').scan(/Bearer (.*)$/).flatten.last
326
+ end
327
+
328
+ ##
329
+ # Validate SSO token
330
+ def check_sso_token
331
+ puts 'check_sso_token'
332
+ jwt = jwt_decode token
333
+
334
+ puts "JWT #{jwt}"
335
+
336
+ return nil unless jwt
337
+ return nil unless jwt.key? :payload
338
+ return nil unless jwt[:payload].key? 'iss'
339
+
340
+ puts "ISS #{jwt[:payload]['iss']} -> #{sso_server}"
341
+ puts jwt[:payload]['iss'].index(sso_server)
342
+
343
+ return nil if jwt[:payload]['iss'].index(sso_server) == nil
344
+ return nil if jwt[:payload]['iss'].index(sso_server) != 0
345
+
346
+ begin
347
+ iss = JSON.parse(redis.get("#{redis_prefix}iss:#{jwt[:payload]['iss']}"),{ symbolize_names: true })
348
+ rescue StandardError
349
+ iss = nil
350
+ end
351
+
352
+ puts "ISS1 #{iss}"
353
+
354
+ unless iss
355
+ begin
356
+ response = RestClient.get "#{jwt[:payload]['iss']}.well-known/openid-configuration", { accept: :json }
357
+ rescue StandardError
358
+ return nil
359
+ end
360
+
361
+ begin
362
+ iss = JSON.parse(response.body, { symbolize_names: true })
363
+ rescue StandardError
364
+ return nil
365
+ end
366
+
367
+ redis.set("#{redis_prefix}iss:#{jwt[:payload]['iss']}", iss.to_json, ex: 86400)
368
+ end
369
+
370
+ puts "ISS2 #{iss}"
371
+ return nil unless iss.key? :jwks_uri
372
+ self.sso_jwk_data_url = iss[:jwks_uri]
373
+ self.sso_userinfo_url = iss[:userinfo_endpoint]
374
+
375
+ jwk = jwk_key(jwt[:header]['kid'])
376
+
377
+ puts "JWK #{jwk}"
378
+
379
+ return nil unless jwk
380
+
381
+ begin
382
+ public_key = JWT::JWK::RSA.import(jwk).public_key
383
+ rescue StandardError
384
+ return nil
385
+ end
386
+
387
+ begin
388
+ jwt_v = JWT.decode token, public_key, true, { algorithm: jwk[:alg] }
389
+ rescue StandardError => e
390
+ puts e
391
+ return nil
392
+ end
393
+
394
+ jwt_v[0]
395
+ end
396
+
397
+ ##
398
+ # Decode JWT token
399
+ # @param token [String] selected token
400
+ # @return [Hash] Encoded JWT token
401
+ def jwt_decode(token)
402
+ begin
403
+ jwt = JWT.decode token, nil, false
404
+ rescue StandardError => e
405
+ puts e
406
+ return nil
407
+ end
408
+
409
+ #puts jwt
410
+
411
+ return nil if jwt.count != 2
412
+
413
+ payload = nil
414
+ payload = jwt[0] if jwt[0].key? 'aud'
415
+ payload = jwt[1] if jwt[1].key? 'aud'
416
+
417
+ header = nil
418
+ header = jwt[0] if jwt[0].key? 'alg'
419
+ header = jwt[1] if jwt[1].key? 'alg'
420
+
421
+ return nil unless payload || header
422
+
423
+ return nil if Time.now.utc.to_i > payload['exp']
424
+
425
+ {
426
+ header: header,
427
+ payload: payload
428
+ }
429
+ end
430
+
431
+ ##
432
+ # Receives JWK key
433
+ # @param [String] key - public key identifier
434
+ # @return [Hash] - JWK selected key
435
+ def jwk_key(key)
436
+ puts "jwk_key #{key}"
437
+ jwk = jwk_data
438
+
439
+ return nil unless jwk
440
+
441
+ jwk[:keys].each do |item|
442
+ return item if item[:kid] == key
443
+ end
444
+
445
+ nil
446
+ end
447
+
448
+ ##
449
+ # Load JWK keys from cache or server.
450
+ # @return [Hash] JWK loaded from cache or server
451
+ def jwk_data
452
+ puts "jwk_data"
453
+ puts "#{redis_prefix}jwk"
454
+ jwk = redis.get("#{redis_prefix}jwk")
455
+
456
+ if jwk
457
+ begin
458
+ return JSON.parse(jwk,{ symbolize_names: true })
459
+ rescue StandardError
460
+ return nil
461
+ end
462
+ end
463
+
464
+ jwk = load_jwk_data_from_sso_server
465
+
466
+ return nil unless jwk
467
+
468
+ redis.set("#{redis_prefix}jwk", jwk.to_json, ex: 3600)
469
+
470
+ jwk
471
+ end
472
+
473
+ ##
474
+ # Load JWK keys from server according by OAUTH specification.
475
+ # @return [Object] returns JWK loaded from server
476
+ def load_jwk_data_from_sso_server
477
+ puts 'load_jwk_data_from_sso_server'
478
+ puts sso_jwk_data_url
479
+ begin
480
+ response = RestClient.get sso_jwk_data_url
481
+ rescue StandardError
482
+ return nil
483
+ end
484
+
485
+ begin
486
+ data = JSON.parse(response.body, { symbolize_names: true })
487
+ rescue StandardError
488
+ return nil
489
+ end
490
+
491
+ data[:update] = Time.now
492
+
493
+ data
494
+ end
495
+
496
+ ##
497
+ # Loads user data from SSO server and returns it.
498
+ # @return [Hash] User data
499
+ def load_user_from_sso_server
500
+ puts 'load_user_from_sso_server'
501
+ puts sso_userinfo_url
502
+ begin
503
+ response = RestClient.get sso_userinfo_url, { authorization: "Bearer #{token}" }
504
+ rescue StandardError
505
+ return nil
506
+ end
507
+
508
+ begin
509
+ result = JSON.parse(response.body, { symbolize_names: true })
510
+ rescue StandardError
511
+ return nil
512
+ end
513
+
514
+ result
515
+ end
516
+ end
@@ -0,0 +1,5 @@
1
+ ##
2
+ # Main application class inherited from {https://www.rubydoc.info/gems/anoubis/Anoubis/ApplicationController Anoubis::ApplicationController}
3
+ class AnoubisSsoClient::ApplicationController < Anoubis::ApplicationController
4
+ include AnoubisSsoClient::ApplicationCommon
5
+ end
@@ -0,0 +1,5 @@
1
+ ##
2
+ # Data controller class inherited from {https://www.rubydoc.info/gems/anoubis/Anoubis/DataController Anoubis::DataController}
3
+ class AnoubisSsoClient::DataController < Anoubis::DataController
4
+ include AnoubisSsoClient::ApplicationCommon
5
+ end
@@ -0,0 +1,30 @@
1
+ ##
2
+ # Index controller class. Output system actions
3
+ class AnoubisSsoClient::IndexController < AnoubisSsoClient::ApplicationController
4
+
5
+ ##
6
+ # Output allowed menu items
7
+ def menu
8
+ result = {
9
+ result: 0,
10
+ message: I18n.t('anoubis.success'),
11
+ menu: []
12
+ }
13
+
14
+ if current_menu
15
+ current_menu.each_value do |dat|
16
+ result[:menu].push dat
17
+ end
18
+ end
19
+
20
+ before_menu_output result
21
+
22
+ render json: result
23
+ end
24
+
25
+ ##
26
+ # Callback for change menu output
27
+ def before_menu_output(result)
28
+
29
+ end
30
+ end