obscured-doorman 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +28 -0
  3. data/.github/dependabot.yml +11 -0
  4. data/.github/workflows/publish.yml +44 -0
  5. data/.gitignore +4 -0
  6. data/.rubocop.yml +14 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/.simplecov +6 -0
  10. data/.travis.yml +17 -0
  11. data/CHANGELOG.md +31 -0
  12. data/Gemfile +8 -0
  13. data/Gemfile.lock +144 -0
  14. data/README.md +115 -0
  15. data/lib/obscured-doorman.rb +69 -0
  16. data/lib/obscured-doorman/base.rb +203 -0
  17. data/lib/obscured-doorman/configuration.rb +123 -0
  18. data/lib/obscured-doorman/errors.rb +44 -0
  19. data/lib/obscured-doorman/helpers.rb +66 -0
  20. data/lib/obscured-doorman/loggable.rb +51 -0
  21. data/lib/obscured-doorman/mailer.rb +46 -0
  22. data/lib/obscured-doorman/messages.rb +30 -0
  23. data/lib/obscured-doorman/models/token.rb +57 -0
  24. data/lib/obscured-doorman/models/user.rb +160 -0
  25. data/lib/obscured-doorman/providers/base/configuration.rb +69 -0
  26. data/lib/obscured-doorman/providers/bitbucket.rb +79 -0
  27. data/lib/obscured-doorman/providers/bitbucket/access_token.rb +27 -0
  28. data/lib/obscured-doorman/providers/bitbucket/configuration.rb +38 -0
  29. data/lib/obscured-doorman/providers/bitbucket/messages.rb +13 -0
  30. data/lib/obscured-doorman/providers/bitbucket/strategy.rb +53 -0
  31. data/lib/obscured-doorman/providers/github.rb +78 -0
  32. data/lib/obscured-doorman/providers/github/access_token.rb +23 -0
  33. data/lib/obscured-doorman/providers/github/configuration.rb +38 -0
  34. data/lib/obscured-doorman/providers/github/messages.rb +13 -0
  35. data/lib/obscured-doorman/providers/github/strategy.rb +53 -0
  36. data/lib/obscured-doorman/strategies/forgot_password.rb +157 -0
  37. data/lib/obscured-doorman/strategies/password.rb +38 -0
  38. data/lib/obscured-doorman/strategies/remember_me.rb +54 -0
  39. data/lib/obscured-doorman/utilities/roles.rb +11 -0
  40. data/lib/obscured-doorman/utilities/types.rb +14 -0
  41. data/lib/obscured-doorman/version.rb +7 -0
  42. data/obscured-doorman.gemspec +42 -0
  43. data/spec/config/mongoid.yml +11 -0
  44. data/spec/doorman_spec.rb +203 -0
  45. data/spec/errors_spec.rb +11 -0
  46. data/spec/factories/token_factory.rb +8 -0
  47. data/spec/factories/user_factory.rb +12 -0
  48. data/spec/helpers/application_helper.rb +52 -0
  49. data/spec/helpers/request_helper.rb +53 -0
  50. data/spec/loggable_spec.rb +27 -0
  51. data/spec/mailer_spec.rb +26 -0
  52. data/spec/matchers/time.rb +7 -0
  53. data/spec/setup.rb +58 -0
  54. data/spec/token_spec.rb +62 -0
  55. data/spec/user_spec.rb +151 -0
  56. metadata +361 -0
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bcrypt'
4
+ require 'geocoder'
5
+ require 'haml'
6
+ require 'mail'
7
+ require 'mongoid'
8
+ require 'sinatra'
9
+ require 'sinatra/contrib'
10
+ require 'sinatra/flash'
11
+ require 'sinatra/partial'
12
+ require 'rack'
13
+ require 'rack/contrib'
14
+ require 'rack/contrib/cookies'
15
+ require 'rest-client'
16
+ require 'warden'
17
+
18
+ require 'obscured-doorman/loggable'
19
+ require 'obscured-doorman/configuration'
20
+ require 'obscured-doorman/errors'
21
+ require 'obscured-doorman/providers/bitbucket'
22
+ require 'obscured-doorman/providers/github'
23
+ require 'obscured-doorman/strategies/password'
24
+ require 'obscured-doorman/strategies/forgot_password'
25
+ require 'obscured-doorman/strategies/remember_me'
26
+ require 'obscured-doorman/utilities/roles'
27
+ require 'obscured-doorman/utilities/types'
28
+ require 'obscured-doorman/helpers'
29
+ require 'obscured-doorman/mailer'
30
+ require 'obscured-doorman/messages'
31
+ require 'obscured-doorman/version'
32
+ require 'obscured-doorman/base'
33
+
34
+ module Obscured
35
+ module Doorman
36
+ extend Loggable
37
+
38
+ class << self
39
+ # Configuration Object (instance of Obscured::Doorman::Configuration)
40
+ attr_writer :configuration
41
+
42
+ ##
43
+ # Configuration options should be set by passing a hash:
44
+ #
45
+ # Obscured::Doorman.setup do |cfg|
46
+ # cfg.confirmation = false,
47
+ # cfg.registration = true,
48
+ # cfg.smtp_domain = 'domain.tld',
49
+ # cfg.smtp_username = 'username',
50
+ # cfg.smtp_password = 'password',
51
+ # end
52
+ #
53
+ def setup
54
+ yield(configuration)
55
+
56
+ require 'obscured-doorman/models/user'
57
+ require 'obscured-doorman/models/token'
58
+ end
59
+
60
+ def configuration
61
+ @configuration ||= Configuration.new
62
+ end
63
+
64
+ def default_configuration
65
+ configuration.defaults
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Obscured
4
+ module Doorman
5
+ class Warden::SessionSerializer
6
+ def serialize(user)
7
+ user.id
8
+ end
9
+
10
+ def deserialize(id)
11
+ User.find(id)
12
+ end
13
+ end
14
+
15
+ module Base
16
+ module Helpers
17
+ # Generates a flash message by trying to fetch a default message,
18
+ # if that fails just pass the message
19
+ def notify(type, message)
20
+ message = Doorman::MESSAGES[message] if message.is_a?(Symbol)
21
+ flash[type] = message
22
+ end
23
+
24
+ # Generates a url for confirm account or reset password
25
+ def token_link(action, token)
26
+ "http://#{env['HTTP_HOST']}/doorman/#{action}/#{token}"
27
+ end
28
+ end
29
+
30
+ def self.registered(app)
31
+ app.helpers Doorman::Base::Helpers
32
+ app.helpers Doorman::Helpers
33
+
34
+ # Enable Sessions
35
+ app.set :sessions, true unless defined?(Rack::Session::Cookie)
36
+
37
+ app.use Warden::Manager do |config|
38
+ config.scope_defaults :default, action: '/doorman/unauthenticated'
39
+
40
+ config.failure_app = lambda { |_env|
41
+ notify :error, Doorman[:auth_required]
42
+ [302, { 'Location' => Doorman.configuration.paths[:login] }, ['']]
43
+ }
44
+ end
45
+
46
+ Warden::Manager.before_failure do |env, _opts|
47
+ # Because authentication failure can happen on any request but
48
+ # we handle it only under "post '/doorman/unauthenticated'",
49
+ # we need o change request to POST
50
+ env['REQUEST_METHOD'] = 'POST'
51
+ # And we need to do the following to work with Rack::MethodOverride
52
+ env.each do |key, _value|
53
+ env[key]['_method'] = 'post' if key == 'rack.request.form_hash'
54
+ end
55
+ end
56
+ Warden::Strategies.add(:password, Doorman::Strategies::Password)
57
+
58
+ app.post '/doorman/unauthenticated' do
59
+ status 401
60
+ session[:return_to] = env['warden.options'][:attempted_path] if session[:return_to].nil?
61
+ redirect(Doorman.configuration.paths[:login])
62
+ end
63
+
64
+ app.get '/doorman/register/?' do
65
+ redirect(Doorman.configuration.paths[:success]) if authenticated?
66
+
67
+ unless Doorman.configuration.registration
68
+ notify :error, :signup_disabled
69
+ redirect(Doorman.configuration.paths[:login])
70
+ end
71
+
72
+ haml :register
73
+ end
74
+
75
+ app.post '/doorman/register' do
76
+ redirect(Doorman.configuration.paths[:success]) if authenticated?
77
+
78
+ unless Doorman.configuration[:registration]
79
+ notify :error, :signup_disabled
80
+ redirect(Doorman.configuration.paths[:login])
81
+ end
82
+
83
+ begin
84
+ if User.registered?(params[:user][:username])
85
+ notify :error, :register_account_exists
86
+ redirect(Doorman.configuration.paths[:login])
87
+ end
88
+
89
+ user = User.make(
90
+ username: params[:user][:username],
91
+ password: params[:user][:password],
92
+ confirmed: !Doorman.configuration[:confirmation]
93
+ )
94
+ user.name = {
95
+ first_name: params[:user][:first_name],
96
+ last_name: params[:user][:last_name]
97
+ }
98
+ user.save
99
+
100
+ if Doorman.configuration[:confirmation]
101
+ token = user.confirm if Doorman.configuration[:confirmation]
102
+
103
+ if File.exist?('views/doorman/templates/account_activation.haml')
104
+ template = haml :'/templates/account_activation', layout: false, locals: {
105
+ user: user.username,
106
+ link: token_link('confirm', token.token)
107
+ }
108
+ Doorman::Mailer.new(
109
+ to: user.username,
110
+ subject: 'Account activation request',
111
+ text: "You have to activate your account (#{user.username}) before using this service. " + token_link('confirm', token.token),
112
+ html: template
113
+ ).deliver!
114
+ else
115
+ Doorman.logger.warn "Template not found (views/doorman/templates/account_activation.haml), account activation at #{token_link('confirm', token.token)}"
116
+ end
117
+ end
118
+
119
+ # Login when registration is completed
120
+ warden.authenticate(:password)
121
+
122
+ # Set cookie
123
+ cookies[:email] = params[:user][:username]
124
+
125
+ notify :success, :signup_success
126
+ redirect(Doorman.configuration.paths[:success])
127
+ rescue => e
128
+ notify :error, e.message
129
+ redirect(Doorman.configuration.paths[:login])
130
+ end
131
+ end
132
+
133
+ app.get '/doorman/confirm/:token/?' do
134
+ redirect(Doorman.configuration.paths[:success]) if authenticated?
135
+
136
+ if params[:token].nil? || params[:token].empty?
137
+ notify :error, :token_not_found
138
+ redirect(back)
139
+ end
140
+
141
+ token = Token.where(token: params[:token]).first
142
+ if token.nil? && !token&.type.eql?(:confirm)
143
+ notify :error, :token_not_found
144
+ redirect(back)
145
+ end
146
+ if token&.used?
147
+ notify :error, :token_used
148
+ redirect(back)
149
+ end
150
+
151
+ user = token&.user
152
+ if user.nil?
153
+ notify :error, :confirm_no_user
154
+ redirect(Doorman.config.paths[:login])
155
+ end
156
+
157
+ user&.confirm!
158
+ notify :success, :confirm_success
159
+ redirect(Doorman.configuration.paths[:login])
160
+ end
161
+
162
+ app.get '/doorman/login/?' do
163
+ redirect(Doorman.configuration.paths[:success]) if authenticated?
164
+
165
+ email = cookies[:email]
166
+ email = params[:email] if email.nil?
167
+
168
+ haml :login, locals: { email: email }
169
+ end
170
+
171
+ app.post '/doorman/login' do
172
+ warden.authenticate(:password)
173
+
174
+ # Set cookie
175
+ cookies[:email] = params[:user][:username]
176
+
177
+ # Notify if there are any messages from Warden.
178
+ notify :error, warden.message unless warden.message.blank?
179
+
180
+ redirect(Doorman.configuration.use_referrer && session[:return_to] ? session.delete(:return_to) : Doorman.configuration.paths[:success])
181
+ end
182
+
183
+ app.get '/doorman/logout/?' do
184
+ warden.logout(:default)
185
+
186
+ notify :success, :logout_success
187
+ redirect(Doorman.configuration.paths[:login])
188
+ end
189
+ end
190
+ end
191
+
192
+ class Middleware < Sinatra::Base
193
+ helpers Sinatra::Cookies
194
+ register Sinatra::Flash
195
+ register Sinatra::Partial
196
+ register Strategies::ForgotPassword
197
+ register Strategies::RememberMe
198
+ register Doorman::Base
199
+ register Doorman::Providers::Bitbucket
200
+ register Doorman::Providers::GitHub
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Obscured
4
+ module Doorman
5
+ class Configuration
6
+ def self.config_option(name)
7
+ define_method(name) do
8
+ read_value(name)
9
+ end
10
+
11
+ define_method("#{name}=") do |value|
12
+ set_value(name, value)
13
+ end
14
+ end
15
+
16
+ #def self.proc_config_option(name)
17
+ # define_method(name) do |&block|
18
+ # set_value(name, block) unless block.nil?
19
+ # read_value(name)
20
+ # end
21
+ #
22
+ # define_method("#{name}=") do |value|
23
+ # set_value(name, value)
24
+ # end
25
+ #end
26
+
27
+ # Set log level
28
+ config_option :log_level
29
+
30
+ # Enables/disables user confirmation
31
+ config_option :confirmation
32
+ # Enables/disables user registration
33
+ config_option :registration
34
+ # Enables/disables the usage of referrer redirection
35
+ config_option :use_referrer
36
+
37
+ # Remember me cookie name
38
+ config_option :remember_cookie
39
+ # Remember me for x-days
40
+ config_option :remember_for
41
+
42
+ # Database name
43
+ config_option :db_name
44
+ # Database client
45
+ config_option :db_client
46
+
47
+ # SMTP Domain
48
+ config_option :smtp_domain
49
+ # SMTP Server
50
+ config_option :smtp_server
51
+ # SMTP Server PSort
52
+ config_option :smtp_port
53
+ # SMTP Sender Username
54
+ config_option :smtp_username
55
+ # SMTP Sender Password
56
+ config_option :smtp_password
57
+
58
+ # Authentication Providers
59
+ config_option :providers
60
+
61
+ # Authentication Providers
62
+ config_option :paths
63
+
64
+ attr_reader :defaults
65
+
66
+ def initialize
67
+ @config_values = {}
68
+
69
+ # set default attribute values
70
+ @defaults = _defaults
71
+ end
72
+
73
+ def [](key)
74
+ read_value(key)
75
+ end
76
+
77
+ def []=(key, value)
78
+ set_value(key, value)
79
+ end
80
+
81
+ private
82
+
83
+ def read_value(name)
84
+ if @config_values.key?(name)
85
+ @config_values[name]
86
+ else
87
+ @defaults.send(name)
88
+ end
89
+ end
90
+
91
+ def set_value(name, value)
92
+ @config_values[name] = value
93
+ end
94
+
95
+ def _defaults
96
+ OpenStruct.new(
97
+ log_level: Logger::DEBUG,
98
+ confirmation: false,
99
+ registration: false,
100
+ use_referrer: true,
101
+ remember_cookie: 'sinatra.doorman.remember',
102
+ remember_for: 30,
103
+ db_name: 'doorman',
104
+ db_client: :doorman,
105
+ smtp_domain: 'doorman.local',
106
+ smtp_server: '127.0.0.1',
107
+ smtp_port: 587,
108
+ smtp_username: nil,
109
+ smtp_password: nil,
110
+ providers: [],
111
+ paths: {
112
+ success: '/home',
113
+ login: '/doorman/login',
114
+ logout: '/doorman/logout',
115
+ forgot: '/doorman/forgot',
116
+ reset: '/doorman/reset',
117
+ register: '/doorman/register'
118
+ }
119
+ )
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Obscured
4
+ module Doorman
5
+ class Error < StandardError
6
+ attr_reader :code
7
+ attr_reader :field
8
+ attr_reader :error
9
+
10
+ ERRORS = {
11
+ invalid_api_method: 'No method parameter was supplied',
12
+ unspecified_error: 'Unspecified error',
13
+ already_exists: '{what}',
14
+ does_not_exist: '{what}',
15
+ does_not_match: '{what}',
16
+ account: '{what}',
17
+ invalid_date: 'Cannot parse {what} from: {date}',
18
+ invalid_type: '{what}',
19
+ not_active: 'Not active',
20
+ required_field_missing: 'Required field {field} is missing'
21
+ }.freeze
22
+
23
+ def initialize(code, params = {})
24
+ field = params.delete(:field)
25
+ error = params.delete(:error)
26
+
27
+ super(parse(code, params))
28
+ @code = code || :unspecified_error
29
+ @field = field || :unspecified_field
30
+ @error = error
31
+ end
32
+
33
+ private
34
+
35
+ def parse(code, params = {})
36
+ message = ERRORS[code]
37
+ params.each_pair do |key, value|
38
+ message = message.sub("{#{key}}", value)
39
+ end
40
+ message
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Obscured
4
+ module Doorman
5
+ module Helpers
6
+ # The main accessor to the warden middleware
7
+ def warden
8
+ request.env['warden']
9
+ end
10
+
11
+ # Check the current session is authenticated to a given scope
12
+ def authenticated?(scope = nil)
13
+ scope ? warden.authenticated?(scope: scope) : warden.authenticated?
14
+ end
15
+ alias logged_in? authenticated?
16
+
17
+ # Authenticate a user against defined strategies
18
+ def authenticate(*args)
19
+ warden.authenticate!(*args)
20
+ end
21
+ alias login authenticate
22
+
23
+ # Return session info
24
+ #
25
+ # @param [Symbol] scope the scope to retrieve session info for
26
+ def session_info(scope = nil)
27
+ scope ? warden.session(scope) : scope
28
+ end
29
+
30
+ # Terminate the current session
31
+ #
32
+ # @param [Symbol] scopes the session scope to terminate
33
+ def logout(scopes = nil)
34
+ scopes ? warden.logout(scopes) : warden.logout(warden.config.default_scope)
35
+ end
36
+
37
+ # Access the user from the current session
38
+ #
39
+ # @param [Symbol] scope for the logged in user
40
+ def user(scope = nil)
41
+ scope ? warden.user(scope) : warden.user
42
+ end
43
+ alias current_user user
44
+
45
+ # Require authorization for an action
46
+ #
47
+ # @param [String] failure_path path to redirect to if user is unauthenticated
48
+ def authorize!(failure_path = nil)
49
+ unless authenticated?
50
+ session[:return_to] = request.path if Doorman.configuration.use_referrer
51
+ redirect(failure_path || Doorman.configuration.paths[:login])
52
+ end
53
+ end
54
+
55
+ # Require authorization for example ajax calls, returns 403 is not authenticated
56
+ #
57
+ # @param [Symbol] format
58
+ def authorized?(format = :json)
59
+ unless authenticated?
60
+ halt 403, { 'Content-Type' => 'application/json' }, { message: 'Unauthorized' }.to_json if format == :json
61
+ halt 403
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end