api_guardian 0.1.0.pre

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