api_guardian 0.1.0.pre

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.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +125 -0
  4. data/Rakefile +30 -0
  5. data/app/controllers/api_guardian/api_controller.rb +112 -0
  6. data/app/controllers/api_guardian/application_controller.rb +11 -0
  7. data/app/controllers/api_guardian/permissions_controller.rb +7 -0
  8. data/app/controllers/api_guardian/registration_controller.rb +38 -0
  9. data/app/controllers/api_guardian/roles_controller.rb +19 -0
  10. data/app/controllers/api_guardian/users_controller.rb +20 -0
  11. data/app/models/api_guardian/permission.rb +14 -0
  12. data/app/models/api_guardian/role.rb +97 -0
  13. data/app/models/api_guardian/role_permission.rb +8 -0
  14. data/app/models/api_guardian/user.rb +23 -0
  15. data/app/serializers/api_guardian/permission_serializer.rb +7 -0
  16. data/app/serializers/api_guardian/role_serializer.rb +7 -0
  17. data/app/serializers/api_guardian/user_serializer.rb +10 -0
  18. data/config/initializers/api_guardian.rb +10 -0
  19. data/config/initializers/doorkeeper.rb +143 -0
  20. data/config/routes.rb +20 -0
  21. data/db/migrate/20151117191338_api_guardian_enable_uuid_extension.rb +5 -0
  22. data/db/migrate/20151117191911_create_api_guardian_roles.rb +9 -0
  23. data/db/migrate/20151117195618_create_api_guardian_users.rb +25 -0
  24. data/db/migrate/20151117212826_create_api_guardian_permissions.rb +10 -0
  25. data/db/migrate/20151117213145_create_api_guardian_role_permissions.rb +11 -0
  26. data/db/migrate/20151117225238_create_doorkeeper_tables.rb +42 -0
  27. data/db/seeds.rb +32 -0
  28. data/lib/api_guardian.rb +80 -0
  29. data/lib/api_guardian/concerns/api_errors/handler.rb +145 -0
  30. data/lib/api_guardian/concerns/api_errors/renderer.rb +45 -0
  31. data/lib/api_guardian/concerns/api_request/validator.rb +66 -0
  32. data/lib/api_guardian/configuration.rb +171 -0
  33. data/lib/api_guardian/engine.rb +23 -0
  34. data/lib/api_guardian/errors/invalid_content_type_error.rb +6 -0
  35. data/lib/api_guardian/errors/invalid_permission_name_error.rb +6 -0
  36. data/lib/api_guardian/errors/invalid_request_body_error.rb +6 -0
  37. data/lib/api_guardian/errors/invalid_request_resource_id_error.rb +6 -0
  38. data/lib/api_guardian/errors/invalid_request_resource_type_error.rb +6 -0
  39. data/lib/api_guardian/errors/invalid_update_action_error.rb +6 -0
  40. data/lib/api_guardian/errors/reset_token_expired_error.rb +6 -0
  41. data/lib/api_guardian/errors/reset_token_user_mismatch_error.rb +6 -0
  42. data/lib/api_guardian/policies/application_policy.rb +65 -0
  43. data/lib/api_guardian/policies/permission_policy.rb +15 -0
  44. data/lib/api_guardian/policies/role_policy.rb +15 -0
  45. data/lib/api_guardian/policies/user_policy.rb +23 -0
  46. data/lib/api_guardian/stores/base.rb +53 -0
  47. data/lib/api_guardian/stores/permission_store.rb +6 -0
  48. data/lib/api_guardian/stores/role_store.rb +9 -0
  49. data/lib/api_guardian/stores/user_store.rb +86 -0
  50. data/lib/api_guardian/version.rb +3 -0
  51. data/lib/generators/api_guardian/install/USAGE +8 -0
  52. data/lib/generators/api_guardian/install/install_generator.rb +19 -0
  53. data/lib/generators/api_guardian/install/templates/README +1 -0
  54. data/lib/generators/api_guardian/install/templates/api_guardian.rb +5 -0
  55. data/lib/tasks/api_guardian_tasks.rake +4 -0
  56. data/spec/concerns/api_errors/handler_spec.rb +114 -0
  57. data/spec/concerns/api_request/validator_spec.rb +102 -0
  58. data/spec/dummy/README.rdoc +28 -0
  59. data/spec/dummy/Rakefile +6 -0
  60. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  61. data/spec/dummy/bin/bundle +3 -0
  62. data/spec/dummy/bin/rails +4 -0
  63. data/spec/dummy/bin/rake +4 -0
  64. data/spec/dummy/bin/setup +29 -0
  65. data/spec/dummy/config.ru +4 -0
  66. data/spec/dummy/config/application.rb +25 -0
  67. data/spec/dummy/config/boot.rb +5 -0
  68. data/spec/dummy/config/database.yml +13 -0
  69. data/spec/dummy/config/environment.rb +5 -0
  70. data/spec/dummy/config/environments/development.rb +41 -0
  71. data/spec/dummy/config/environments/production.rb +79 -0
  72. data/spec/dummy/config/environments/test.rb +42 -0
  73. data/spec/dummy/config/initializers/assets.rb +11 -0
  74. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  75. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  76. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  77. data/spec/dummy/config/initializers/inflections.rb +16 -0
  78. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  79. data/spec/dummy/config/initializers/session_store.rb +3 -0
  80. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  81. data/spec/dummy/config/locales/en.yml +23 -0
  82. data/spec/dummy/config/routes.rb +3 -0
  83. data/spec/dummy/config/secrets.yml +22 -0
  84. data/spec/dummy/db/schema.rb +104 -0
  85. data/spec/dummy/log/test.log +5031 -0
  86. data/spec/dummy/public/404.html +67 -0
  87. data/spec/dummy/public/422.html +67 -0
  88. data/spec/dummy/public/500.html +66 -0
  89. data/spec/dummy/public/favicon.ico +0 -0
  90. data/spec/factories/permissions.rb +6 -0
  91. data/spec/factories/role_permissions.rb +6 -0
  92. data/spec/factories/roles.rb +24 -0
  93. data/spec/factories/users.rb +11 -0
  94. data/spec/models/permission_spec.rb +28 -0
  95. data/spec/models/role_permission_spec.rb +27 -0
  96. data/spec/models/role_spec.rb +209 -0
  97. data/spec/models/user_spec.rb +44 -0
  98. data/spec/policies/application_policy_spec.rb +118 -0
  99. data/spec/policies/permission_policy_spec.rb +28 -0
  100. data/spec/policies/role_policy_spec.rb +28 -0
  101. data/spec/policies/user_policy_spec.rb +29 -0
  102. data/spec/requests/permissions_controller_spec.rb +19 -0
  103. data/spec/requests/registration_controller_spec.rb +151 -0
  104. data/spec/requests/roles_controller_spec.rb +75 -0
  105. data/spec/requests/users_controller_spec.rb +75 -0
  106. data/spec/spec_helper.rb +138 -0
  107. data/spec/stores/base_spec.rb +113 -0
  108. data/spec/stores/permission_store_spec.rb +2 -0
  109. data/spec/stores/role_store_spec.rb +12 -0
  110. data/spec/stores/user_store_spec.rb +144 -0
  111. data/spec/support/controller_concern_test_helpers.rb +21 -0
  112. data/spec/support/matchers.rb +37 -0
  113. data/spec/support/request_helpers.rb +111 -0
  114. metadata +508 -0
@@ -0,0 +1,145 @@
1
+ require 'active_support/concern'
2
+
3
+ # TODO: Break this into further modules to decrease complexity
4
+
5
+ module ApiGuardian
6
+ module Concerns
7
+ module ApiErrors
8
+ module Handler
9
+ extend ActiveSupport::Concern
10
+ include ApiErrors::Renderer
11
+
12
+ included do
13
+ def api_error_handler(exception)
14
+ if exception.is_a? Pundit::NotAuthorizedError
15
+ user_not_authorized
16
+ elsif exception.is_a? ActionController::ParameterMissing
17
+ malformed_request(exception)
18
+ elsif exception.is_a? ActiveRecord::RecordInvalid
19
+ record_invalid(exception)
20
+ elsif exception.is_a? ActiveRecord::RecordNotFound
21
+ render_not_found
22
+ elsif exception.is_a? ApiGuardian::Errors::InvalidContentTypeError
23
+ invalid_content_type
24
+ elsif exception.is_a? ApiGuardian::Errors::InvalidRequestBodyError
25
+ invalid_request_body(exception)
26
+ elsif exception.is_a? ApiGuardian::Errors::InvalidRequestResourceTypeError
27
+ invalid_request_resource_type(exception)
28
+ elsif exception.is_a? ApiGuardian::Errors::InvalidRequestResourceIdError
29
+ invalid_request_resource_id(exception)
30
+ elsif exception.is_a? ApiGuardian::Errors::InvalidUpdateActionError
31
+ invalid_update_action
32
+ elsif exception.is_a? ApiGuardian::Errors::ResetTokenUserMismatchError
33
+ reset_token_mismatch
34
+ elsif exception.is_a? ApiGuardian::Errors::ResetTokenExpiredError
35
+ reset_token_expired
36
+ else
37
+ generic_error_handler(exception)
38
+ end
39
+ end
40
+
41
+ def doorkeeper_unauthorized_render_options(_)
42
+ error = construct_error(
43
+ 401, 'not_authenticated', 'Not Authenticated',
44
+ 'You must be logged in.'
45
+ )
46
+ { json: { errors: [error] } }
47
+ end
48
+
49
+ protected
50
+
51
+ def user_not_authorized
52
+ render_error(
53
+ 403, 'not_authorized', 'Not Authorized',
54
+ 'You are not authorized to perform this action.'
55
+ )
56
+ end
57
+
58
+ def malformed_request(exception)
59
+ render_error(
60
+ 400, 'malformed_request', 'Malformed Request',
61
+ exception.message
62
+ )
63
+ end
64
+
65
+ def record_invalid(exception)
66
+ formatted_errors = []
67
+ used_fields = []
68
+ record = exception.record
69
+ record.errors.each do |error|
70
+ next if used_fields.include? error.to_s
71
+ formatted_error = {
72
+ field: error.to_s,
73
+ detail: record.errors[error][0]
74
+ }
75
+ formatted_errors.push formatted_error
76
+ used_fields.push error.to_s
77
+ end
78
+ formatted_errors = formatted_errors.sort_by { |k| k[:field] }
79
+ render_error(422, 'unprocessable_entity', 'Unprocessable Entity', formatted_errors)
80
+ end
81
+
82
+ def render_not_found
83
+ render_error(
84
+ 404, 'not_found', 'Not Found', 'Resource or endpoint missing: ' +
85
+ request.original_url
86
+ )
87
+ end
88
+
89
+ def generic_error_handler(exception)
90
+ render_error(500, nil, nil, nil, exception)
91
+ end
92
+
93
+ def invalid_content_type
94
+ render_error(
95
+ 415, 'invalid_content_type', 'Invalid Content Type',
96
+ 'Supported content types are: application/vnd.api+json'
97
+ )
98
+ end
99
+
100
+ def invalid_request_body(exception)
101
+ render_error(
102
+ 400, 'invalid_request_body', 'Invalid Request Body',
103
+ "The '#{exception.message}' property is required."
104
+ )
105
+ end
106
+
107
+ def invalid_request_resource_type(exception)
108
+ render_error(
109
+ 400, 'invalid_request_resource_type', 'Invalid Request Resource Type',
110
+ "Expected 'type' property to be '#{exception.message}' for this resource."
111
+ )
112
+ end
113
+
114
+ def invalid_request_resource_id(exception)
115
+ render_error(
116
+ 400, 'invalid_request_resource_id', 'Invalid Request Resource ID',
117
+ "Request 'id' property does not match 'id' of URI. Provided: #{exception.message}, Expected: #{params[:id]}"
118
+ )
119
+ end
120
+
121
+ def invalid_update_action
122
+ render_error(
123
+ 405, 'method_not_allowed', 'Method Not Allowed',
124
+ 'Resource update action expects PATCH method.'
125
+ )
126
+ end
127
+
128
+ def reset_token_mismatch
129
+ render_error(
130
+ 403, 'reset_token_mismatch', 'Reset Token Mismatch',
131
+ 'Reset token is not valid for the supplied email address.'
132
+ )
133
+ end
134
+
135
+ def reset_token_expired
136
+ render_error(
137
+ 403, 'reset_token_expired', 'Reset Token Expired',
138
+ 'This reset token has expired. Tokens are valid for 24 hours.'
139
+ )
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,45 @@
1
+ require 'active_support/concern'
2
+
3
+ module ApiGuardian
4
+ module Concerns
5
+ module ApiErrors
6
+ module Renderer
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ def render_error(status, code, title, detail, exception = nil)
11
+ error = construct_error status, code, title, detail
12
+ if Rails.env.production?
13
+ render json: { errors: [error] }, status: status
14
+ else
15
+ if exception
16
+ render json: { errors: [error], exception: exception.class.name,
17
+ message: exception.message, trace: exception.backtrace[0, 10] },
18
+ status: status
19
+ else
20
+ render json: { errors: [error] }, status: status
21
+ end
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def construct_error(status, code, title, detail)
28
+ # TODO: Create error log here
29
+ {
30
+ id: SecureRandom.uuid,
31
+ code: code || 'unknown',
32
+ status: status.to_s || '500',
33
+ title: title || 'Unknown',
34
+ detail: detail || 'An unknown error has occurred and has been logged.'
35
+ }
36
+ end
37
+
38
+ def render(**_)
39
+ return super if defined?(super)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,66 @@
1
+ require 'active_support/concern'
2
+
3
+ module ApiGuardian
4
+ module Concerns
5
+ module ApiRequest
6
+ module Validator
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ def validate_api_request
11
+ validate_content_type
12
+
13
+ # Make sure we conform to json-api request spec
14
+ case request.method
15
+ when 'POST'
16
+ validate_post_request
17
+ when 'PATCH'
18
+ validate_patch_request
19
+ end
20
+
21
+ if action_name == 'update' && request.method != 'PATCH'
22
+ fail ApiGuardian::Errors::InvalidUpdateActionError, request.method
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ def validate_content_type
29
+ if request.body.read != '' && request.headers['Content-Type'] != 'application/vnd.api+json'
30
+ fail ApiGuardian::Errors::InvalidContentTypeError, "Invalid content type #{request.headers['Content-Type']}"
31
+ end
32
+ end
33
+
34
+ def validate_post_request
35
+ validate_request_type
36
+ end
37
+
38
+ def validate_patch_request
39
+ validate_request_id
40
+ validate_request_type
41
+ end
42
+
43
+ def validate_request_id
44
+ top_params = params.fetch(:data)
45
+ fail ApiGuardian::Errors::InvalidRequestBodyError, 'id' unless top_params.fetch(:id, nil)
46
+
47
+ expected_request_id = params[:id]
48
+ request_id = top_params.fetch(:id, nil)
49
+
50
+ fail ApiGuardian::Errors::InvalidRequestResourceIdError, request_id unless request_id == expected_request_id
51
+ end
52
+
53
+ def validate_request_type
54
+ top_params = params.fetch(:data)
55
+ fail ApiGuardian::Errors::InvalidRequestBodyError, 'type' unless top_params.fetch(:type, nil)
56
+
57
+ expected_request_type = resource_name.pluralize.downcase
58
+ request_type = top_params.fetch(:type, nil)
59
+
60
+ fail ApiGuardian::Errors::InvalidRequestResourceTypeError, expected_request_type unless request_type == expected_request_type
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,171 @@
1
+ module ApiGuardian
2
+ class Configuration
3
+ # Controls whether the sign up route is enabled.
4
+ # Defaults to `true`. Set to `false` to disable user creation routes.
5
+ # The setting is ignored if routes are disabled.
6
+ # @param [Boolean] value
7
+ # @return [Boolean]
8
+ attr_writer :allow_sign_up
9
+
10
+ # The domain to use for the clearance remember token cookie.
11
+ # Defaults to `nil`, which causes the cookie domain to default to the
12
+ # domain of the request. For more, see
13
+ # [RFC6265](http://tools.ietf.org/html/rfc6265#section-5.2.3).
14
+ # @return [String]
15
+ attr_accessor :cookie_domain
16
+
17
+ # A lambda called to set the remember token cookie expires attribute.
18
+ # The lambda accepts the collection of cookies as an argument which
19
+ # allows for changing the expiration according to those cookies.
20
+ # This could be used, for example, to set a session cookie unless
21
+ # a `remember_me` cookie was also present. By default, cookie expiration
22
+ # is one year. For more on cookie expiration see
23
+ # [RFC6265](http://tools.ietf.org/html/rfc6265#section-5.2.1).
24
+ # @return [Lambda]
25
+ attr_accessor :cookie_expiration
26
+
27
+ # The name of Clearance's remember token cookie.
28
+ # Defaults to `remember_token`.
29
+ # @return [String]
30
+ attr_accessor :cookie_name
31
+
32
+ # Controls which paths the remember token cookie is valid for.
33
+ # Defaults to `"/"` for the entire domain. For more, see
34
+ # [RFC6265](http://tools.ietf.org/html/rfc6265#section-5.1.4).
35
+ # @return [String]
36
+ attr_accessor :cookie_path
37
+
38
+ # Controls whether the HttpOnly flag should be set on the remember token
39
+ # cookie. Defaults to `false`. If `true`, the cookie will not be made
40
+ # available to JavaScript. For more see
41
+ # [RFC6265](http://tools.ietf.org/html/rfc6265#section-5.2.6).
42
+ # @return [Boolean]
43
+ attr_accessor :httponly
44
+
45
+ # Controls the address the password reset email is sent from.
46
+ # Defaults to reply@example.com.
47
+ # @return [String]
48
+ attr_accessor :mailer_sender
49
+
50
+ # The password strategy to use when authenticating and setting passwords.
51
+ # Defaults to {Clearance::PasswordStrategies::BCrypt}.
52
+ # @return [Module #authenticated? #password=]
53
+ attr_accessor :password_strategy
54
+
55
+ # The default path Clearance will redirect signed in users to.
56
+ # Defaults to `"/"`. This can often be overridden for specific scenarios by
57
+ # overriding controller methods that rely on it.
58
+ # @return [String]
59
+ attr_accessor :redirect_url
60
+
61
+ # Set to `false` to disable Clearance's built-in routes.
62
+ # Defaults to `true`. When set to false, your app is responsible for all
63
+ # routes. You can dump a copy of Clearance's default routes with
64
+ # `rails generate clearance:routes`.
65
+ # @return [Boolean]
66
+ attr_writer :routes
67
+
68
+ # Controls the secure setting on the remember token cookie.
69
+ # Defaults to `false`. When set, the browser will only send the
70
+ # cookie to the server over HTTPS. You should set this value to true in
71
+ # live environments to prevent session hijacking. For more, see
72
+ # [RFC6265](http://tools.ietf.org/html/rfc6265#section-5.2.5).
73
+ # @return [Boolean]
74
+ attr_accessor :secure_cookie
75
+
76
+ # The array of sign in guards to run when signing a user in.
77
+ # Defaults to an empty array. Sign in guards respond to `call` and are
78
+ # initialized with a session and the current stack. Each guard can decide
79
+ # to fail the sign in, yield to the next guard, or allow the sign in.
80
+ # @return [Array<#call>]
81
+ attr_accessor :sign_in_guards
82
+
83
+ # The ActiveRecord class that represents users in your application.
84
+ # Defualts to `::User`.
85
+ # @return [ActiveRecord::Base]
86
+ attr_accessor :user_model
87
+
88
+ def initialize
89
+ @allow_sign_up = true
90
+ @cookie_expiration = ->(cookies) { 1.year.from_now.utc }
91
+ @cookie_domain = nil
92
+ @cookie_path = '/'
93
+ @cookie_name = "remember_token"
94
+ @httponly = false
95
+ @mailer_sender = 'reply@example.com'
96
+ @redirect_url = '/'
97
+ @routes = true
98
+ @secure_cookie = false
99
+ @sign_in_guards = []
100
+ end
101
+
102
+ def user_model
103
+ @user_model ||= ::User
104
+ end
105
+
106
+ # Is the user sign up route enabled?
107
+ # @return [Boolean]
108
+ def allow_sign_up?
109
+ @allow_sign_up
110
+ end
111
+
112
+ # Specifies which controller actions are allowed for user resources.
113
+ # This will be `[:create]` is `allow_sign_up` is true (the default), and
114
+ # empty otherwise.
115
+ # @return [Array<Symbol>]
116
+ def user_actions
117
+ if allow_sign_up?
118
+ [:create]
119
+ else
120
+ []
121
+ end
122
+ end
123
+
124
+ # The name of foreign key parameter for the configured user model.
125
+ # This is derived from the `model_name` of the `user_model` setting.
126
+ # In the default configuration, this is `user_id`.
127
+ # @return [Symbol]
128
+ def user_id_parameter
129
+ "#{user_model.model_name.singular}_id".to_sym
130
+ end
131
+
132
+ # @return [Boolean] are Clearance's built-in routes enabled?
133
+ def routes_enabled?
134
+ @routes
135
+ end
136
+
137
+ # Reloads the clearance user model class.
138
+ # This is called from the Clearance engine to reload the configured
139
+ # user class during each request while in development mode, but only once
140
+ # in production.
141
+ #
142
+ # @api private
143
+ def reload_user_model
144
+ if @user_model.present?
145
+ @user_model = @user_model.to_s.constantize
146
+ end
147
+ end
148
+ end
149
+
150
+ # @return [Clearance::Configuration] Clearance's current configuration
151
+ def self.configuration
152
+ @configuration ||= Configuration.new
153
+ end
154
+
155
+ # Set Clearance's configuration
156
+ # @param config [Clearance::Configuration]
157
+ def self.configuration=(config)
158
+ @configuration = config
159
+ end
160
+
161
+ # Modify Clearance's current configuration
162
+ # @yieldparam [Clearance::Configuration] config current Clearance config
163
+ # ```
164
+ # Clearance.configure do |config|
165
+ # config.routes = false
166
+ # end
167
+ # ```
168
+ def self.configure
169
+ yield configuration
170
+ end
171
+ end
@@ -0,0 +1,23 @@
1
+ require 'doorkeeper'
2
+
3
+ module ApiGuardian
4
+ Doorkeeper = ::Doorkeeper
5
+
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace ApiGuardian
8
+
9
+ config.generators do |g|
10
+ g.test_framework :rspec, fixture: false
11
+ g.fixture_replacement :factory_girl, dir: 'spec/factories'
12
+ g.assets false
13
+ g.helper false
14
+ end
15
+
16
+ config.middleware.insert_before 0, "Rack::Cors" do
17
+ allow do
18
+ origins '*'
19
+ resource '*', :headers => :any, :methods => [:get, :post, :options]
20
+ end
21
+ end
22
+ end
23
+ end