api_guardian 0.1.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +125 -0
- data/Rakefile +30 -0
- data/app/controllers/api_guardian/api_controller.rb +112 -0
- data/app/controllers/api_guardian/application_controller.rb +11 -0
- data/app/controllers/api_guardian/permissions_controller.rb +7 -0
- data/app/controllers/api_guardian/registration_controller.rb +38 -0
- data/app/controllers/api_guardian/roles_controller.rb +19 -0
- data/app/controllers/api_guardian/users_controller.rb +20 -0
- data/app/models/api_guardian/permission.rb +14 -0
- data/app/models/api_guardian/role.rb +97 -0
- data/app/models/api_guardian/role_permission.rb +8 -0
- data/app/models/api_guardian/user.rb +23 -0
- data/app/serializers/api_guardian/permission_serializer.rb +7 -0
- data/app/serializers/api_guardian/role_serializer.rb +7 -0
- data/app/serializers/api_guardian/user_serializer.rb +10 -0
- data/config/initializers/api_guardian.rb +10 -0
- data/config/initializers/doorkeeper.rb +143 -0
- data/config/routes.rb +20 -0
- data/db/migrate/20151117191338_api_guardian_enable_uuid_extension.rb +5 -0
- data/db/migrate/20151117191911_create_api_guardian_roles.rb +9 -0
- data/db/migrate/20151117195618_create_api_guardian_users.rb +25 -0
- data/db/migrate/20151117212826_create_api_guardian_permissions.rb +10 -0
- data/db/migrate/20151117213145_create_api_guardian_role_permissions.rb +11 -0
- data/db/migrate/20151117225238_create_doorkeeper_tables.rb +42 -0
- data/db/seeds.rb +32 -0
- data/lib/api_guardian.rb +80 -0
- data/lib/api_guardian/concerns/api_errors/handler.rb +145 -0
- data/lib/api_guardian/concerns/api_errors/renderer.rb +45 -0
- data/lib/api_guardian/concerns/api_request/validator.rb +66 -0
- data/lib/api_guardian/configuration.rb +171 -0
- data/lib/api_guardian/engine.rb +23 -0
- data/lib/api_guardian/errors/invalid_content_type_error.rb +6 -0
- data/lib/api_guardian/errors/invalid_permission_name_error.rb +6 -0
- data/lib/api_guardian/errors/invalid_request_body_error.rb +6 -0
- data/lib/api_guardian/errors/invalid_request_resource_id_error.rb +6 -0
- data/lib/api_guardian/errors/invalid_request_resource_type_error.rb +6 -0
- data/lib/api_guardian/errors/invalid_update_action_error.rb +6 -0
- data/lib/api_guardian/errors/reset_token_expired_error.rb +6 -0
- data/lib/api_guardian/errors/reset_token_user_mismatch_error.rb +6 -0
- data/lib/api_guardian/policies/application_policy.rb +65 -0
- data/lib/api_guardian/policies/permission_policy.rb +15 -0
- data/lib/api_guardian/policies/role_policy.rb +15 -0
- data/lib/api_guardian/policies/user_policy.rb +23 -0
- data/lib/api_guardian/stores/base.rb +53 -0
- data/lib/api_guardian/stores/permission_store.rb +6 -0
- data/lib/api_guardian/stores/role_store.rb +9 -0
- data/lib/api_guardian/stores/user_store.rb +86 -0
- data/lib/api_guardian/version.rb +3 -0
- data/lib/generators/api_guardian/install/USAGE +8 -0
- data/lib/generators/api_guardian/install/install_generator.rb +19 -0
- data/lib/generators/api_guardian/install/templates/README +1 -0
- data/lib/generators/api_guardian/install/templates/api_guardian.rb +5 -0
- data/lib/tasks/api_guardian_tasks.rake +4 -0
- data/spec/concerns/api_errors/handler_spec.rb +114 -0
- data/spec/concerns/api_request/validator_spec.rb +102 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +25 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +13 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/db/schema.rb +104 -0
- data/spec/dummy/log/test.log +5031 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/permissions.rb +6 -0
- data/spec/factories/role_permissions.rb +6 -0
- data/spec/factories/roles.rb +24 -0
- data/spec/factories/users.rb +11 -0
- data/spec/models/permission_spec.rb +28 -0
- data/spec/models/role_permission_spec.rb +27 -0
- data/spec/models/role_spec.rb +209 -0
- data/spec/models/user_spec.rb +44 -0
- data/spec/policies/application_policy_spec.rb +118 -0
- data/spec/policies/permission_policy_spec.rb +28 -0
- data/spec/policies/role_policy_spec.rb +28 -0
- data/spec/policies/user_policy_spec.rb +29 -0
- data/spec/requests/permissions_controller_spec.rb +19 -0
- data/spec/requests/registration_controller_spec.rb +151 -0
- data/spec/requests/roles_controller_spec.rb +75 -0
- data/spec/requests/users_controller_spec.rb +75 -0
- data/spec/spec_helper.rb +138 -0
- data/spec/stores/base_spec.rb +113 -0
- data/spec/stores/permission_store_spec.rb +2 -0
- data/spec/stores/role_store_spec.rb +12 -0
- data/spec/stores/user_store_spec.rb +144 -0
- data/spec/support/controller_concern_test_helpers.rb +21 -0
- data/spec/support/matchers.rb +37 -0
- data/spec/support/request_helpers.rb +111 -0
- metadata +508 -0
@@ -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,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,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,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,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
|