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,6 @@
1
+ module ApiGuardian
2
+ module Errors
3
+ class InvalidContentTypeError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module ApiGuardian
2
+ module Errors
3
+ class InvalidPermissionNameError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module ApiGuardian
2
+ module Errors
3
+ class InvalidRequestBodyError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module ApiGuardian
2
+ module Errors
3
+ class InvalidRequestResourceIdError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module ApiGuardian
2
+ module Errors
3
+ class InvalidRequestResourceTypeError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module ApiGuardian
2
+ module Errors
3
+ class InvalidUpdateActionError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module ApiGuardian
2
+ module Errors
3
+ class ResetTokenExpiredError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module ApiGuardian
2
+ module Errors
3
+ class ResetTokenUserMismatchError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,65 @@
1
+ module ApiGuardian
2
+ module Policies
3
+ class ApplicationPolicy
4
+ attr_reader :user, :record
5
+
6
+ def initialize(user, record)
7
+ @user = user
8
+ @record = record
9
+ end
10
+
11
+ def index?
12
+ false
13
+ end
14
+
15
+ def show?
16
+ user.can?(["#{resource_name}:read", "#{resource_name}:manage"])
17
+ end
18
+
19
+ def create?
20
+ user.can?(["#{resource_name}:create", "#{resource_name}:manage"])
21
+ end
22
+
23
+ def new?
24
+ create?
25
+ end
26
+
27
+ def update?
28
+ user.can?(["#{resource_name}:update", "#{resource_name}:manage"])
29
+ end
30
+
31
+ def edit?
32
+ update?
33
+ end
34
+
35
+ def destroy?
36
+ user.can?(["#{resource_name}:delete", "#{resource_name}:manage"])
37
+ end
38
+
39
+ def scope
40
+ Pundit.policy_scope!(user, record.class)
41
+ end
42
+
43
+ class Scope
44
+ attr_reader :user, :scope
45
+
46
+ def initialize(user, scope)
47
+ @user = user
48
+ @scope = scope
49
+ end
50
+
51
+ def resolve
52
+ scope
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ def resource_name
59
+ return record.new.class.name.demodulize.downcase if record.respond_to? :new
60
+
61
+ record.class.name.demodulize.downcase
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,15 @@
1
+ module ApiGuardian
2
+ module Policies
3
+ class PermissionPolicy < ApplicationPolicy
4
+ class Scope < Scope
5
+ def resolve
6
+ if user.can?(['permission:read', 'permission:manage'])
7
+ scope
8
+ else
9
+ fail Pundit::NotAuthorizedError
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module ApiGuardian
2
+ module Policies
3
+ class RolePolicy < ApplicationPolicy
4
+ class Scope < Scope
5
+ def resolve
6
+ if user.can?(['role:read', 'role:manage'])
7
+ scope
8
+ else
9
+ fail Pundit::NotAuthorizedError
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ module ApiGuardian
2
+ module Policies
3
+ class UserPolicy < ApplicationPolicy
4
+ class Scope < Scope
5
+ def resolve
6
+ if user.can?(['user:read', 'user:manage'])
7
+ scope.includes(role: [role_permissions: [:permission]])
8
+ else
9
+ fail Pundit::NotAuthorizedError
10
+ end
11
+ end
12
+ end
13
+
14
+ def show?
15
+ user.can?(['user:read', 'user:manage']) || record.id == user.id
16
+ end
17
+
18
+ def update?
19
+ user.can?(['user:update', 'user:manage']) || record.id == user.id
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ module ApiGuardian
2
+ module Stores
3
+ class Base
4
+ @@instance = nil
5
+
6
+ delegate :new, to: :resource_class
7
+
8
+ def initialize(scope = nil)
9
+ @scope = scope
10
+ @@instance = self
11
+ end
12
+
13
+ def all
14
+ @scope.all
15
+ end
16
+
17
+ def paginate(page = 1, per_page = 25)
18
+ @scope.page(page).per(per_page)
19
+ end
20
+
21
+ def find(id)
22
+ record = resource_class.find(id)
23
+ fail ActiveRecord::RecordNotFound unless record
24
+ record
25
+ end
26
+
27
+ def save(resource)
28
+ resource.save!
29
+ end
30
+
31
+ def create(attributes)
32
+ resource = resource_class.new(attributes)
33
+ fail ActiveRecord::RecordInvalid.new(resource), '' unless resource.valid?
34
+ save(resource)
35
+ resource
36
+ end
37
+
38
+ def update(resource, attributes)
39
+ resource.update_attributes!(attributes)
40
+ end
41
+
42
+ def destroy(resource)
43
+ resource.destroy!
44
+ end
45
+
46
+ protected
47
+
48
+ def resource_class
49
+ @resource_class ||= self.class.name.gsub('Stores::', '').gsub('Store', '').classify.constantize
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,6 @@
1
+ module ApiGuardian
2
+ module Stores
3
+ class PermissionStore < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module ApiGuardian
2
+ module Stores
3
+ class RoleStore < Base
4
+ def self.default_role
5
+ Role.default_role
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,86 @@
1
+ # TODO: How can we remove dependency on .new?
2
+ module ApiGuardian
3
+ module Stores
4
+ class UserStore < Base
5
+ def find_by_email(email)
6
+ User.find_by_email(email)
7
+ end
8
+
9
+ def find_by_reset_password_token(token)
10
+ User.find_by_reset_password_token(token)
11
+ end
12
+
13
+ def create(attributes)
14
+ attributes[:role_id] = ApiGuardian::Stores::RoleStore.default_role.id
15
+ attributes[:email_confirmed_at] = DateTime.now.utc
16
+ attributes[:active] = true
17
+ super attributes
18
+ end
19
+
20
+ def self.register(attributes)
21
+ instance = new(nil)
22
+
23
+ attributes[:role_id] = ApiGuardian::Stores::RoleStore.default_role.id
24
+ attributes[:active] = false
25
+
26
+ # create user
27
+ user = instance.new(attributes)
28
+ fail ActiveRecord::RecordInvalid.new(user), '' unless user.valid?
29
+ instance.save(user)
30
+
31
+ # TODO: put user created event onto queue
32
+
33
+ user
34
+ end
35
+
36
+ def self.reset_password(email)
37
+ instance = new(nil)
38
+
39
+ user = instance.find_by_email(email)
40
+
41
+ if user
42
+ user.reset_password_token = SecureRandom.hex(64)
43
+ user.reset_password_sent_at = DateTime.now.utc
44
+ user.save
45
+
46
+ # TODO: email password reset
47
+ return true
48
+ end
49
+
50
+ false
51
+ end
52
+
53
+ def self.complete_reset_password(attributes)
54
+ instance = new(nil)
55
+ # Find user by token
56
+ user = instance.find_by_reset_password_token(attributes[:token])
57
+
58
+ if user
59
+ # Validate submitted email matches token
60
+ fail ApiGuardian::Errors::ResetTokenUserMismatchError, attributes[:email] unless user.email == attributes[:email]
61
+
62
+ # Check that it hasn't expired
63
+ fail ApiGuardian::Errors::ResetTokenExpiredError, '' unless user.reset_password_token_valid?
64
+
65
+ # Validate password
66
+ if attributes.fetch(:password, nil).blank?
67
+ user.errors.add(:password, :blank)
68
+ fail ActiveRecord::RecordInvalid.new(user), ''
69
+ end
70
+ user.assign_attributes(attributes.slice(:password, :password_confirmation))
71
+ user.save! # This will fail if it is invalid
72
+
73
+ # Done
74
+ user.reset_password_token = nil
75
+ user.reset_password_sent_at = nil
76
+ user.save
77
+
78
+ # TODO: send password changed confirmation email
79
+
80
+ return true
81
+ end
82
+ false
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,3 @@
1
+ module ApiGuardian
2
+ VERSION = "0.1.0.pre"
3
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ rails generate install Thing
6
+
7
+ This will create:
8
+ what/will/it/create
@@ -0,0 +1,19 @@
1
+ module ApiGuardian
2
+ class InstallGenerator < Rails::Generators::Base
3
+ source_root File.expand_path('../templates', __FILE__)
4
+
5
+ desc 'Creates an ApiGuardian initializer and copy locale files to your application.'
6
+
7
+ def copy_initializer
8
+ template 'api_guardian.rb', 'config/initializers/api_guardian.rb'
9
+ end
10
+
11
+ def add_routes
12
+ route 'mount ApiGuardian::Engine => \'/auth\''
13
+ end
14
+
15
+ def show_readme
16
+ readme 'README' if behavior == :invoke
17
+ end
18
+ end
19
+ end
@@ -0,0 +1 @@
1
+ Do stuff with ApiGuardian
@@ -0,0 +1,5 @@
1
+ ApiGuardian.setup do |config|
2
+ # In order to change the base user class, you'd need to uncomment this line and
3
+ # enter your own class name. Your class will need to extend ApiGuardian::User.
4
+ # config.user_class = 'User'
5
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :api_guardian do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,114 @@
1
+ module Test
2
+ class Dummy
3
+ include ControllerConcernTestHelpers
4
+ include ApiGuardian::Concerns::ApiErrors::Handler
5
+ end
6
+ end
7
+
8
+ describe ApiGuardian::Concerns::ApiErrors::Handler, type: :request do
9
+ let(:dummy_class) { Test::Dummy.new }
10
+
11
+ # Methods
12
+ describe 'methods' do
13
+ describe '#doorkeeper_unauthorized_render_options' do
14
+ it 'returns an error hash' do
15
+ expect_any_instance_of(Test::Dummy).to receive(:construct_error).with(
16
+ 401, 'not_authenticated', 'Not Authenticated', 'You must be logged in.'
17
+ ).and_return('foo')
18
+ result = dummy_class.doorkeeper_unauthorized_render_options ''
19
+
20
+ expect(result).to eq(json: { errors: ['foo'] })
21
+ end
22
+ end
23
+
24
+ describe '#api_error_handler' do
25
+ it 'handles Pundit::NotAuthorizedError' do
26
+ expect_any_instance_of(Test::Dummy).to receive(:render_error).with(
27
+ 403, 'not_authorized', 'Not Authorized', 'You are not authorized to perform this action.'
28
+ )
29
+ expect { dummy_class.api_error_handler(Pundit::NotAuthorizedError.new) }.not_to raise_error
30
+ end
31
+
32
+ it 'handles ActionController::ParameterMissing' do
33
+ expect_any_instance_of(Test::Dummy).to receive(:render_error).with(
34
+ 400, 'malformed_request', 'Malformed Request', 'param is missing or the value is empty: test'
35
+ )
36
+ expect { dummy_class.api_error_handler(ActionController::ParameterMissing.new('test')) }.not_to raise_error
37
+ end
38
+
39
+ it 'handles ActiveRecord::RecordInvalid' do
40
+ end
41
+
42
+ it 'handles ActiveRecord::RecordNotFound' do
43
+ allow_any_instance_of(ActionDispatch::Request).to receive(:original_url).and_return('test.com')
44
+ expect_any_instance_of(Test::Dummy).to receive(:render_error).with(
45
+ 404, 'not_found', 'Not Found', 'Resource or endpoint missing: test.com'
46
+ )
47
+ expect { dummy_class.api_error_handler(ActiveRecord::RecordNotFound.new('test')) }.not_to raise_error
48
+ end
49
+
50
+ it 'handles InvalidContentTypeError' do
51
+ expect_any_instance_of(Test::Dummy).to receive(:render_error).with(
52
+ 415, 'invalid_content_type', 'Invalid Content Type', 'Supported content types are: application/vnd.api+json'
53
+ )
54
+ expect { dummy_class.api_error_handler(ApiGuardian::Errors::InvalidContentTypeError.new('')) }.not_to raise_error
55
+ end
56
+
57
+ it 'handles InvalidRequestBodyError' do
58
+ expect_any_instance_of(Test::Dummy).to receive(:render_error).with(
59
+ 400, 'invalid_request_body', 'Invalid Request Body', 'The \'test\' property is required.'
60
+ )
61
+ expect { dummy_class.api_error_handler(ApiGuardian::Errors::InvalidRequestBodyError.new('test')) }.not_to raise_error
62
+ end
63
+
64
+ it 'handles InvalidRequestResourceTypeError' do
65
+ expect_any_instance_of(Test::Dummy).to receive(:render_error).with(
66
+ 400, 'invalid_request_resource_type', 'Invalid Request Resource Type',
67
+ 'Expected \'type\' property to be \'test\' for this resource.'
68
+ )
69
+ expect { dummy_class.api_error_handler(ApiGuardian::Errors::InvalidRequestResourceTypeError.new('test')) }.not_to raise_error
70
+ end
71
+
72
+ it 'handles InvalidRequestResourceIdError' do
73
+ allow_any_instance_of(ActionController::Parameters).to receive(:fetch).with(:id, nil).and_return('test')
74
+ expect_any_instance_of(Test::Dummy).to receive(:render_error).with(
75
+ 400, 'invalid_request_resource_id', 'Invalid Request Resource ID',
76
+ 'Request \'id\' property does not match \'id\' of URI. Provided: value, Expected: test'
77
+ )
78
+ expect { dummy_class.api_error_handler(ApiGuardian::Errors::InvalidRequestResourceIdError.new('value')) }.not_to raise_error
79
+ end
80
+
81
+ it 'handles InvalidUpdateActionError' do
82
+ expect_any_instance_of(Test::Dummy).to receive(:render_error).with(
83
+ 405, 'method_not_allowed', 'Method Not Allowed',
84
+ 'Resource update action expects PATCH method.'
85
+ )
86
+ expect { dummy_class.api_error_handler(ApiGuardian::Errors::InvalidUpdateActionError.new('')) }.not_to raise_error
87
+ end
88
+
89
+ it 'handles ResetTokenUserMismatchError' do
90
+ expect_any_instance_of(Test::Dummy).to receive(:render_error).with(
91
+ 403, 'reset_token_mismatch', 'Reset Token Mismatch',
92
+ 'Reset token is not valid for the supplied email address.'
93
+ )
94
+ expect { dummy_class.api_error_handler(ApiGuardian::Errors::ResetTokenUserMismatchError.new('')) }.not_to raise_error
95
+ end
96
+
97
+ it 'handles ResetTokenExpiredError' do
98
+ expect_any_instance_of(Test::Dummy).to receive(:render_error).with(
99
+ 403, 'reset_token_expired', 'Reset Token Expired',
100
+ 'This reset token has expired. Tokens are valid for 24 hours.'
101
+ )
102
+ expect { dummy_class.api_error_handler(ApiGuardian::Errors::ResetTokenExpiredError.new('')) }.not_to raise_error
103
+ end
104
+
105
+ it 'handles generic errors' do
106
+ exception = StandardError.new('')
107
+ expect_any_instance_of(Test::Dummy).to receive(:render_error).with(
108
+ 500, nil, nil, nil, exception
109
+ )
110
+ expect { dummy_class.api_error_handler(exception) }.not_to raise_error
111
+ end
112
+ end
113
+ end
114
+ end