rhino_project_core 0.20.0.beta.18
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +28 -0
- data/Rakefile +34 -0
- data/app/assets/stripe_flow.png +0 -0
- data/app/controllers/concerns/rhino/authenticated.rb +18 -0
- data/app/controllers/concerns/rhino/error_handling.rb +58 -0
- data/app/controllers/concerns/rhino/paper_trail_whodunnit.rb +11 -0
- data/app/controllers/concerns/rhino/permit.rb +38 -0
- data/app/controllers/concerns/rhino/set_current_user.rb +13 -0
- data/app/controllers/rhino/account_controller.rb +34 -0
- data/app/controllers/rhino/active_model_extension_controller.rb +52 -0
- data/app/controllers/rhino/base_controller.rb +23 -0
- data/app/controllers/rhino/crud_controller.rb +57 -0
- data/app/controllers/rhino/simple_controller.rb +11 -0
- data/app/controllers/rhino/simple_stream_controller.rb +12 -0
- data/app/helpers/rhino/omniauth_helper.rb +67 -0
- data/app/helpers/rhino/policy_helper.rb +42 -0
- data/app/helpers/rhino/segment_helper.rb +62 -0
- data/app/models/rhino/account.rb +13 -0
- data/app/models/rhino/current.rb +7 -0
- data/app/models/rhino/user.rb +44 -0
- data/app/overrides/active_record/autosave_association_override.rb +18 -0
- data/app/overrides/active_record/delegated_type_override.rb +14 -0
- data/app/overrides/activestorage/direct_uploads_controller_override.rb +23 -0
- data/app/overrides/activestorage/redirect_controller_override.rb +21 -0
- data/app/overrides/activestorage/redirect_representation_controller_override.rb +21 -0
- data/app/overrides/devise_token_auth/confirmations_controller_override.rb +14 -0
- data/app/overrides/devise_token_auth/omniauth_callbacks_controller_override.rb +45 -0
- data/app/overrides/devise_token_auth/passwords_controller_override.rb +9 -0
- data/app/overrides/devise_token_auth/registrations_controller_override.rb +20 -0
- data/app/overrides/devise_token_auth/sessions_controller_override.rb +26 -0
- data/app/overrides/devise_token_auth/token_validations_controller_override.rb +18 -0
- data/app/policies/rhino/account_policy.rb +27 -0
- data/app/policies/rhino/active_storage_attachment_policy.rb +16 -0
- data/app/policies/rhino/admin_policy.rb +20 -0
- data/app/policies/rhino/base_policy.rb +72 -0
- data/app/policies/rhino/crud_policy.rb +109 -0
- data/app/policies/rhino/editor_policy.rb +12 -0
- data/app/policies/rhino/global_policy.rb +8 -0
- data/app/policies/rhino/resource_info_policy.rb +9 -0
- data/app/policies/rhino/user_policy.rb +20 -0
- data/app/policies/rhino/viewer_policy.rb +19 -0
- data/app/resources/rhino/info_graph.rb +41 -0
- data/app/resources/rhino/open_api_info.rb +108 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20180101000000_devise_token_auth_create_users.rb +54 -0
- data/db/migrate/20180622142754_add_allow_change_password_to_users.rb +5 -0
- data/db/migrate/20191217010224_add_approved_to_users.rb +7 -0
- data/db/migrate/20200503182019_change_tokens_to_json_b.rb +9 -0
- data/lib/commands/rhino_command.rb +59 -0
- data/lib/generators/rhino/dev/setup/setup_generator.rb +175 -0
- data/lib/generators/rhino/dev/setup/templates/env.client.tt +4 -0
- data/lib/generators/rhino/dev/setup/templates/env.server.tt +35 -0
- data/lib/generators/rhino/dev/setup/templates/prepare-commit-msg +17 -0
- data/lib/generators/rhino/install/install_generator.rb +24 -0
- data/lib/generators/rhino/install/templates/account.rb +4 -0
- data/lib/generators/rhino/install/templates/rhino.rb +24 -0
- data/lib/generators/rhino/install/templates/user.rb +4 -0
- data/lib/generators/rhino/module/module_generator.rb +93 -0
- data/lib/generators/rhino/module/templates/engine.rb.tt +19 -0
- data/lib/generators/rhino/module/templates/install_generator.rb.tt +12 -0
- data/lib/generators/rhino/module/templates/module_tasks.rake.tt +17 -0
- data/lib/generators/rhino/policy/policy_generator.rb +33 -0
- data/lib/generators/rhino/policy/templates/policy.rb.tt +46 -0
- data/lib/generators/test_unit/rhino_policy_generator.rb +13 -0
- data/lib/generators/test_unit/templates/policy_test.rb.tt +57 -0
- data/lib/rhino/engine.rb +140 -0
- data/lib/rhino/omniauth/strategies/azure_o_auth2.rb +16 -0
- data/lib/rhino/resource/active_model_extension/backing_store/google_sheet.rb +89 -0
- data/lib/rhino/resource/active_model_extension/backing_store.rb +33 -0
- data/lib/rhino/resource/active_model_extension/describe.rb +38 -0
- data/lib/rhino/resource/active_model_extension/params.rb +70 -0
- data/lib/rhino/resource/active_model_extension/properties.rb +229 -0
- data/lib/rhino/resource/active_model_extension/reference.rb +50 -0
- data/lib/rhino/resource/active_model_extension/routing.rb +15 -0
- data/lib/rhino/resource/active_model_extension/serialization.rb +16 -0
- data/lib/rhino/resource/active_model_extension.rb +38 -0
- data/lib/rhino/resource/active_record_extension/describe.rb +44 -0
- data/lib/rhino/resource/active_record_extension/params.rb +213 -0
- data/lib/rhino/resource/active_record_extension/properties.rb +85 -0
- data/lib/rhino/resource/active_record_extension/properties_describe.rb +226 -0
- data/lib/rhino/resource/active_record_extension/reference.rb +50 -0
- data/lib/rhino/resource/active_record_extension/routing.rb +21 -0
- data/lib/rhino/resource/active_record_extension/search.rb +23 -0
- data/lib/rhino/resource/active_record_extension/serialization.rb +16 -0
- data/lib/rhino/resource/active_record_extension.rb +30 -0
- data/lib/rhino/resource/active_record_tree.rb +50 -0
- data/lib/rhino/resource/active_storage_extension.rb +41 -0
- data/lib/rhino/resource/describe.rb +19 -0
- data/lib/rhino/resource/owner.rb +172 -0
- data/lib/rhino/resource/params.rb +31 -0
- data/lib/rhino/resource/properties.rb +192 -0
- data/lib/rhino/resource/reference.rb +31 -0
- data/lib/rhino/resource/routing.rb +107 -0
- data/lib/rhino/resource/serialization.rb +13 -0
- data/lib/rhino/resource/sieves.rb +36 -0
- data/lib/rhino/resource.rb +54 -0
- data/lib/rhino/sieve/filter.rb +149 -0
- data/lib/rhino/sieve/helpers.rb +11 -0
- data/lib/rhino/sieve/limit.rb +20 -0
- data/lib/rhino/sieve/offset.rb +16 -0
- data/lib/rhino/sieve/order.rb +143 -0
- data/lib/rhino/sieve/search.rb +20 -0
- data/lib/rhino/sieve.rb +158 -0
- data/lib/rhino/test_case/controller.rb +134 -0
- data/lib/rhino/test_case/override.rb +19 -0
- data/lib/rhino/test_case/policy.rb +76 -0
- data/lib/rhino/test_case.rb +10 -0
- data/lib/rhino/version.rb +17 -0
- data/lib/rhino.rb +129 -0
- data/lib/tasks/rhino.rake +38 -0
- data/lib/tasks/rhino_dev.rake +17 -0
- data/lib/validators/country_validator.rb +11 -0
- data/lib/validators/email_validator.rb +8 -0
- data/lib/validators/ipv4_validator.rb +10 -0
- data/lib/validators/mac_address_validator.rb +9 -0
- metadata +178 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# https://stackoverflow.com/questions/4470108/when-monkey-patching-an-instance-method-can-you-call-the-overridden-method-from/4471202
|
4
|
+
# Mixin Prepending
|
5
|
+
module ActiveStorage::DirectUploadsController::Extensions
|
6
|
+
def create
|
7
|
+
authorize ActiveStorage::Attachment
|
8
|
+
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ActiveStorage::DirectUploadsController
|
14
|
+
include DeviseTokenAuth::Concerns::SetUserByToken
|
15
|
+
include Pundit
|
16
|
+
|
17
|
+
include Rhino::ErrorHandling
|
18
|
+
include Rhino::Authenticated
|
19
|
+
|
20
|
+
after_action :verify_authorized
|
21
|
+
|
22
|
+
prepend ActiveStorage::DirectUploadsController::Extensions
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage::Blobs::RedirectController::Extensions
|
4
|
+
def show
|
5
|
+
authorize ActiveStorage::Attachment
|
6
|
+
|
7
|
+
super
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class ActiveStorage::Blobs::RedirectController
|
12
|
+
include DeviseTokenAuth::Concerns::SetUserByToken
|
13
|
+
include Pundit
|
14
|
+
|
15
|
+
include Rhino::ErrorHandling
|
16
|
+
include Rhino::Authenticated
|
17
|
+
|
18
|
+
after_action :verify_authorized
|
19
|
+
|
20
|
+
prepend ActiveStorage::Blobs::RedirectController::Extensions
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage::Representations::RedirectController::Extensions
|
4
|
+
def show
|
5
|
+
authorize ActiveStorage::Attachment
|
6
|
+
|
7
|
+
super
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class ActiveStorage::Representations::RedirectController
|
12
|
+
include DeviseTokenAuth::Concerns::SetUserByToken
|
13
|
+
include Pundit
|
14
|
+
|
15
|
+
include Rhino::ErrorHandling
|
16
|
+
include Rhino::Authenticated
|
17
|
+
|
18
|
+
after_action :verify_authorized
|
19
|
+
|
20
|
+
prepend ActiveStorage::Representations::RedirectController::Extensions
|
21
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeviseTokenAuth::ConfirmationsController::Extensions
|
4
|
+
# devise_token_auth raises an error if the confirmation token can't be found
|
5
|
+
def show
|
6
|
+
super
|
7
|
+
rescue StandardError
|
8
|
+
redirect_to ENV["FRONT_END_URL"]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class DeviseTokenAuth::ConfirmationsController
|
13
|
+
prepend DeviseTokenAuth::ConfirmationsController::Extensions
|
14
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# https://stackoverflow.com/questions/4470108/when-monkey-patching-an-instance-method-can-you-call-the-overridden-method-from/4471202
|
4
|
+
# Mixin Prepending
|
5
|
+
module DeviseTokenAuth::OmniauthCallbacksController::Extensions
|
6
|
+
include RhinoOrganizations::Concerns::CreateOrganization
|
7
|
+
|
8
|
+
def redirect_callbacks
|
9
|
+
# derive target redirect route from 'resource_class' param, which was set
|
10
|
+
# before authentication.
|
11
|
+
devise_mapping = get_devise_mapping
|
12
|
+
redirect_route = get_redirect_route(devise_mapping)
|
13
|
+
|
14
|
+
# preserve omniauth info for success route. ignore 'extra' in twitter
|
15
|
+
# and 'credentials' in others auth response, like azure_oauth2, to avoid CookieOverflow.
|
16
|
+
|
17
|
+
session["dta.omniauth.auth"] = request.env["omniauth.auth"].except("extra").except("credentials")
|
18
|
+
session["dta.omniauth.params"] = request.env["omniauth.params"]
|
19
|
+
|
20
|
+
redirect_to redirect_route
|
21
|
+
end
|
22
|
+
|
23
|
+
def omniauth_success
|
24
|
+
super do |resource|
|
25
|
+
if Rhino.resources.include?("Organization") && @oauth_registration
|
26
|
+
# Create if this is the first time we've seen the user
|
27
|
+
create_organization(resource)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# set a server cookie if configured
|
32
|
+
if DeviseTokenAuth.cookie_enabled
|
33
|
+
auth_header = @resource.build_auth_header(@token.token, @token.client)
|
34
|
+
cookies[DeviseTokenAuth.cookie_name] = DeviseTokenAuth.cookie_attributes.merge(value: auth_header.to_json)
|
35
|
+
end
|
36
|
+
rescue ActiveRecord::RecordInvalid
|
37
|
+
# Technically this could be something else, but this is the most common
|
38
|
+
# devise_token_auth calls save! which is why we catch it here
|
39
|
+
render_data_or_redirect("authFailure", error: "Email address in use")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class DeviseTokenAuth::OmniauthCallbacksController
|
44
|
+
prepend DeviseTokenAuth::OmniauthCallbacksController::Extensions
|
45
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DeviseTokenAuth::PasswordsController
|
4
|
+
# If there is an error preparing to edit the password, redirect to frontend
|
5
|
+
# This would normally be an expired password token
|
6
|
+
def render_edit_error
|
7
|
+
redirect_to "#{ENV['FRONT_END_URL']}/auth/reset-password/expired"
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# https://stackoverflow.com/questions/4470108/when-monkey-patching-an-instance-method-can-you-call-the-overridden-method-from/4471202
|
4
|
+
# Mixin Prepending
|
5
|
+
module DeviseTokenAuth::RegistrationsController::Extensions
|
6
|
+
include RhinoOrganizations::Concerns::CreateOrganization
|
7
|
+
|
8
|
+
def create
|
9
|
+
return render_error(401, "signup is not allowed") unless Rhino.allow_signup
|
10
|
+
|
11
|
+
super do |resource|
|
12
|
+
# Create if this is the first time we've seen the user
|
13
|
+
create_organization(resource) if Rhino.resources.include?("Organization")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class DeviseTokenAuth::RegistrationsController
|
19
|
+
prepend DeviseTokenAuth::RegistrationsController::Extensions
|
20
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeviseTokenAuth::SessionsController::Extensions
|
4
|
+
def render_destroy_error
|
5
|
+
if DeviseTokenAuth.cookie_enabled
|
6
|
+
# If a cookie is set with a domain specified then it must be deleted with that domain specified
|
7
|
+
# See https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html
|
8
|
+
cookies.delete(DeviseTokenAuth.cookie_name, domain: DeviseTokenAuth.cookie_attributes[:domain])
|
9
|
+
end
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def create
|
14
|
+
super
|
15
|
+
Rhino::SegmentHelper.track("Signed In", @resource) unless @resource.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def destroy
|
19
|
+
Rhino::SegmentHelper.track("Signed Out", @resource) unless @resource.nil?
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class DeviseTokenAuth::SessionsController
|
25
|
+
prepend DeviseTokenAuth::SessionsController::Extensions
|
26
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# https://stackoverflow.com/questions/4470108/when-monkey-patching-an-instance-method-can-you-call-the-overridden-method-from/4471202
|
4
|
+
# Mixin Prepending
|
5
|
+
module DeviseTokenAuth::TokenValidationsController::Extensions
|
6
|
+
def render_validate_token_error
|
7
|
+
if DeviseTokenAuth.cookie_enabled
|
8
|
+
# If a cookie is set with a domain specified then it must be deleted with that domain specified
|
9
|
+
# See https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html
|
10
|
+
cookies.delete(DeviseTokenAuth.cookie_name, domain: DeviseTokenAuth.cookie_attributes[:domain])
|
11
|
+
end
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class DeviseTokenAuth::TokenValidationsController
|
17
|
+
prepend DeviseTokenAuth::TokenValidationsController::Extensions
|
18
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
class AccountPolicy < ::Rhino::BasePolicy
|
5
|
+
# Show and update only for this user
|
6
|
+
# It should only ever be current user, but this is a safety check
|
7
|
+
|
8
|
+
def show?
|
9
|
+
authorize_action(auth_owned?)
|
10
|
+
end
|
11
|
+
|
12
|
+
def update?
|
13
|
+
authorize_action(auth_owned?)
|
14
|
+
end
|
15
|
+
|
16
|
+
class Scope < ::Rhino::BasePolicy::Scope
|
17
|
+
def resolve
|
18
|
+
scope.where(id: auth_owner&.id)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def auth_owned?
|
24
|
+
record.is_a?(User) && record&.id == auth_owner&.id
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
class ActiveStorageAttachmentPolicy < ::Rhino::BasePolicy
|
5
|
+
def create?
|
6
|
+
authorize_action(true)
|
7
|
+
end
|
8
|
+
|
9
|
+
def show?
|
10
|
+
authorize_action(true)
|
11
|
+
end
|
12
|
+
|
13
|
+
class Scope < ::Rhino::BasePolicy::Scope
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
class AdminPolicy < ::Rhino::ViewerPolicy
|
5
|
+
def create?
|
6
|
+
authorize_action(true)
|
7
|
+
end
|
8
|
+
|
9
|
+
def update?
|
10
|
+
authorize_action(true)
|
11
|
+
end
|
12
|
+
|
13
|
+
def destroy?
|
14
|
+
authorize_action(true)
|
15
|
+
end
|
16
|
+
|
17
|
+
class Scope < ::Rhino::ViewerPolicy::Scope
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Base policy allows no actions by default
|
4
|
+
# Permitted attributes are the default not empty set though
|
5
|
+
module Rhino
|
6
|
+
class BasePolicy
|
7
|
+
include ActiveSupport::Callbacks
|
8
|
+
|
9
|
+
# Do not authorize if any before callbacks return false
|
10
|
+
define_callbacks :authorize_action, terminator: ->(_target, result_lambda) { result_lambda.call == false }
|
11
|
+
|
12
|
+
attr_reader :auth_owner, :record
|
13
|
+
|
14
|
+
def initialize(auth_owner, record)
|
15
|
+
@auth_owner = auth_owner
|
16
|
+
@record = record
|
17
|
+
end
|
18
|
+
|
19
|
+
# Authorize the action with a default permission
|
20
|
+
# Ensure the callbacks are run
|
21
|
+
def authorize_action(permission)
|
22
|
+
run_callbacks :authorize_action do
|
23
|
+
permission
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def index?
|
28
|
+
authorize_action(false)
|
29
|
+
end
|
30
|
+
|
31
|
+
def show?
|
32
|
+
authorize_action(false)
|
33
|
+
end
|
34
|
+
|
35
|
+
def create?
|
36
|
+
authorize_action(false)
|
37
|
+
end
|
38
|
+
|
39
|
+
def update?
|
40
|
+
authorize_action(false)
|
41
|
+
end
|
42
|
+
|
43
|
+
def destroy?
|
44
|
+
authorize_action(false)
|
45
|
+
end
|
46
|
+
|
47
|
+
def permitted_attributes_for_create
|
48
|
+
record.create_params
|
49
|
+
end
|
50
|
+
|
51
|
+
def permitted_attributes_for_show
|
52
|
+
record.show_params
|
53
|
+
end
|
54
|
+
|
55
|
+
def permitted_attributes_for_update
|
56
|
+
record.update_params
|
57
|
+
end
|
58
|
+
|
59
|
+
class Scope
|
60
|
+
attr_reader :auth_owner, :scope
|
61
|
+
|
62
|
+
def initialize(auth_owner, scope)
|
63
|
+
@auth_owner = auth_owner
|
64
|
+
@scope = scope
|
65
|
+
end
|
66
|
+
|
67
|
+
def resolve
|
68
|
+
scope.none
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_policy"
|
4
|
+
|
5
|
+
module Rhino
|
6
|
+
# CrudPolicy finds the role for the auth owner and then uses that role
|
7
|
+
# to look up a corresponding policy.
|
8
|
+
#
|
9
|
+
# Authorization for an action, scoping and permitted params are then
|
10
|
+
# delegated to that policy
|
11
|
+
class CrudPolicy < ::Rhino::BasePolicy
|
12
|
+
def check_action(action)
|
13
|
+
# If any role allows the action, return true
|
14
|
+
# There should only be multiple roles in the case of index because we
|
15
|
+
# can't trace to a specific owner for the ActiveRecord class
|
16
|
+
# FIXME: Make sure this is ok for create - ie the ownership is enforced/checked
|
17
|
+
Rhino.base_owner.roles_for_auth(auth_owner, record).each do |role, _base_owner_array|
|
18
|
+
policy_class = Rhino::PolicyHelper.find_policy(role, record)
|
19
|
+
next unless policy_class
|
20
|
+
|
21
|
+
return true if policy_class.new(auth_owner, record).send(action)
|
22
|
+
end
|
23
|
+
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def permitted_attributes(action)
|
28
|
+
# There should only be one match because record should be a instance not a class
|
29
|
+
# for show/create/update
|
30
|
+
Rhino.base_owner.roles_for_auth(auth_owner, record).each do |role, _base_owner_array|
|
31
|
+
policy_class = Rhino::PolicyHelper.find_policy(role, record)
|
32
|
+
|
33
|
+
return policy_class.new(auth_owner, record).send("permitted_attributes_for_#{action}") if policy_class
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return nothing if we didn't find a policy
|
37
|
+
[]
|
38
|
+
end
|
39
|
+
|
40
|
+
def index?
|
41
|
+
authorize_action(check_action(:index?))
|
42
|
+
end
|
43
|
+
|
44
|
+
def show?
|
45
|
+
authorize_action(check_action(:show?))
|
46
|
+
end
|
47
|
+
|
48
|
+
def create?
|
49
|
+
authorize_action(check_action(:create?))
|
50
|
+
end
|
51
|
+
|
52
|
+
def update?
|
53
|
+
authorize_action(check_action(:update?))
|
54
|
+
end
|
55
|
+
|
56
|
+
def destroy?
|
57
|
+
authorize_action(check_action(:destroy?))
|
58
|
+
end
|
59
|
+
|
60
|
+
def permitted_attributes_for_create
|
61
|
+
permitted_attributes(:create)
|
62
|
+
end
|
63
|
+
|
64
|
+
def permitted_attributes_for_show
|
65
|
+
permitted_attributes(:show)
|
66
|
+
end
|
67
|
+
|
68
|
+
def permitted_attributes_for_update
|
69
|
+
permitted_attributes(:update)
|
70
|
+
end
|
71
|
+
|
72
|
+
class Scope < ::Rhino::BasePolicy::Scope
|
73
|
+
# rubocop:todo Metrics/MethodLength
|
74
|
+
def resolve # rubocop:disable Metrics/AbcSize,
|
75
|
+
role_scopes = []
|
76
|
+
|
77
|
+
# Get every role for the auth owner
|
78
|
+
Rhino.base_owner.roles_for_auth(auth_owner).each do |role, base_owner_array|
|
79
|
+
base_owner_array.each do |base_owner|
|
80
|
+
scope_class = Rhino::PolicyHelper.find_policy_scope(role, scope)
|
81
|
+
next unless scope_class
|
82
|
+
|
83
|
+
# Collect all the role based scopes
|
84
|
+
# Use the scope for this role and join/select the base owner
|
85
|
+
scope_instance = scope_class.new(auth_owner, scope)
|
86
|
+
# NUB-367 and NUB-392
|
87
|
+
# default scopes with includes and order can cause problems
|
88
|
+
scope_instance = scope_instance.resolve.unscope(:includes, :order)
|
89
|
+
role_scopes << scope_instance.select(tnpk(scope)).joins(scope.joins_for_base_owner).where(tnpk(Rhino.base_owner) => base_owner.id)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Select with present? because scope.none with produce empty sql string
|
94
|
+
role_scopes = role_scopes.map(&:to_sql).compact_blank
|
95
|
+
return scope.none unless role_scopes.present?
|
96
|
+
|
97
|
+
# UNION all the role based scopes
|
98
|
+
# The front end needs to filter per base owner as appropriate
|
99
|
+
scope.where("#{tnpk(scope)} in (#{role_scopes.join(' UNION ')})")
|
100
|
+
end
|
101
|
+
# rubocop:enable Metrics/MethodLength
|
102
|
+
|
103
|
+
private
|
104
|
+
def tnpk(scope)
|
105
|
+
"#{scope.table_name}.#{scope.primary_key}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
class UserPolicy < ::Rhino::ViewerPolicy
|
5
|
+
class Scope < ::Rhino::ViewerPolicy::Scope
|
6
|
+
# We allow other users in the org to view the user
|
7
|
+
def resolve # rubocop:disable Metrics/AbcSize
|
8
|
+
return scope.none unless auth_owner
|
9
|
+
|
10
|
+
base_owner_pk = "#{Rhino.base_owner.table_name}.#{Rhino.base_owner.primary_key}"
|
11
|
+
|
12
|
+
# Base owners for the user
|
13
|
+
base_owners = scope.joins(scope.joins_for_base_owner).where(id: auth_owner.id).pluck(base_owner_pk)
|
14
|
+
|
15
|
+
# Users related to the base owners
|
16
|
+
scope.joins(scope.joins_for_base_owner).where(base_owner_pk => base_owners).distinct
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
class ViewerPolicy < ::Rhino::BasePolicy
|
5
|
+
def index?
|
6
|
+
authorize_action(true)
|
7
|
+
end
|
8
|
+
|
9
|
+
def show?
|
10
|
+
authorize_action(true)
|
11
|
+
end
|
12
|
+
|
13
|
+
class Scope < ::Rhino::BasePolicy::Scope
|
14
|
+
def resolve
|
15
|
+
scope.all
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rgl/implicit"
|
4
|
+
require "rgl/dot"
|
5
|
+
|
6
|
+
module Rhino
|
7
|
+
class InfoGraph
|
8
|
+
include Rhino::Resource
|
9
|
+
|
10
|
+
rhino_owner_global
|
11
|
+
|
12
|
+
rhino_routing only: [:index], path: "info/graph"
|
13
|
+
rhino_controller :simple_stream
|
14
|
+
rhino_policy :resource_info
|
15
|
+
|
16
|
+
def self.describe
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
FILENAME = "rhino-resource-graph"
|
21
|
+
FILETYPE = "png"
|
22
|
+
|
23
|
+
def self.build_graph
|
24
|
+
RGL::ImplicitGraph.new do |g|
|
25
|
+
# Add every rhino resource
|
26
|
+
g.vertex_iterator { |c| Rhino.resource_classes.each(&c) }
|
27
|
+
|
28
|
+
# If its owned by another resoource, link it
|
29
|
+
g.adjacent_iterator { |v, c| c.call(v.resource_owned_by.to_s.camelize.constantize) if v&.resource_owned_by && !v.global_owner? }
|
30
|
+
|
31
|
+
g.directed = true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.index
|
36
|
+
build_graph.write_to_graphic_file(FILETYPE, FILENAME)
|
37
|
+
|
38
|
+
{ file: [FILENAME, FILETYPE].join("."), type: "image/png", disposition: "inline" }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
class OpenApiInfo
|
5
|
+
include Rhino::Resource
|
6
|
+
|
7
|
+
rhino_owner_global
|
8
|
+
|
9
|
+
rhino_routing only: [:index], path: 'info/openapi'
|
10
|
+
rhino_controller :simple
|
11
|
+
rhino_policy :resource_info
|
12
|
+
|
13
|
+
def self.describe
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.index
|
18
|
+
JSON.pretty_generate({
|
19
|
+
openapi: '3.0.3',
|
20
|
+
components: {
|
21
|
+
schemas: describe_schemas
|
22
|
+
},
|
23
|
+
paths: describe_paths,
|
24
|
+
info: describe_info
|
25
|
+
})
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.describe_schemas
|
29
|
+
Rhino.resource_classes.index_with(&:describe).compact.transform_keys { |r| r.model_name.singular }
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.describe_parameter(param)
|
33
|
+
{
|
34
|
+
name: param,
|
35
|
+
in: "path",
|
36
|
+
required: true
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# https://gist.github.com/bantic/5688232#gistcomment-3274704
|
41
|
+
RESPONSE_CODES = [200, 400, 403, 422].freeze
|
42
|
+
SUCCESS_CODES = [200].freeze
|
43
|
+
def self.describe_path_content(klass, code)
|
44
|
+
{
|
45
|
+
"application/json":
|
46
|
+
({ schema: { "$ref": "#/components/schemas/#{klass.model_name.singular}" } } if SUCCESS_CODES.include?(code)) || {}
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.describe_path_responses(klass)
|
51
|
+
RESPONSE_CODES.index_with do |code|
|
52
|
+
{
|
53
|
+
description: Rack::Utils::HTTP_STATUS_CODES[code],
|
54
|
+
content: describe_path_content(klass, code)
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.describe_path(path) # rubocop:todo Metrics/AbcSize
|
60
|
+
new_path = path.index_by { |p| p[:verb].downcase.to_sym }
|
61
|
+
new_path.transform_values! do |verb_values|
|
62
|
+
klass = verb_values[:rhino_resource].constantize
|
63
|
+
|
64
|
+
verb_info = {
|
65
|
+
operationId: "#{klass.model_name.singular}-#{verb_values[:action]}",
|
66
|
+
parameters: verb_values[:required_names].map { |param| describe_parameter(param) },
|
67
|
+
responses: describe_path_responses(klass),
|
68
|
+
tags: [klass.model_name.singular]
|
69
|
+
}
|
70
|
+
|
71
|
+
verb_info
|
72
|
+
end
|
73
|
+
|
74
|
+
new_path
|
75
|
+
end
|
76
|
+
|
77
|
+
PATH_IGNORES = ["Rhino::OpenApiInfo", "Rhino::InfoGraph"].freeze
|
78
|
+
def self.describe_paths # rubocop:todo Metrics/AbcSize
|
79
|
+
routes = Rails.application.routes.routes
|
80
|
+
|
81
|
+
# Extract the path information we need
|
82
|
+
paths = routes.map { |r| r.defaults.merge(verb: r.verb, path: r.path.spec.to_s, required_names: r.path.required_names) }
|
83
|
+
paths = paths.group_by { |r| r[:path] }
|
84
|
+
|
85
|
+
# Get rid of non-rhino paths
|
86
|
+
# Rhino paths have rhino_resource set as an attribute when the routes are created
|
87
|
+
paths.transform_values! { |v| v.select { |p| p[:rhino_resource].present? && PATH_IGNORES.exclude?(p[:rhino_resource]) } }
|
88
|
+
|
89
|
+
# Remove empty hashes
|
90
|
+
paths.compact_blank!
|
91
|
+
|
92
|
+
# For each path, list the verbs it supports
|
93
|
+
paths.transform_values! do |path|
|
94
|
+
describe_path(path)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.describe_info
|
99
|
+
{
|
100
|
+
title: "#{Rails.application.class.module_parent_name} API",
|
101
|
+
version: '0.0.0',
|
102
|
+
"x-rhino": {
|
103
|
+
modules: Rhino.registered_modules
|
104
|
+
}
|
105
|
+
}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|