command_tower 0.3.0
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +59 -0
- data/Rakefile +32 -0
- data/app/controllers/command_tower/admin_controller.rb +104 -0
- data/app/controllers/command_tower/application_controller.rb +81 -0
- data/app/controllers/command_tower/auth/plain_text_controller.rb +132 -0
- data/app/controllers/command_tower/inbox/message_blast_controller.rb +89 -0
- data/app/controllers/command_tower/inbox/message_controller.rb +79 -0
- data/app/controllers/command_tower/user_controller.rb +49 -0
- data/app/controllers/command_tower/username_controller.rb +26 -0
- data/app/helpers/command_tower/application_helper.rb +4 -0
- data/app/helpers/command_tower/schema_helper.rb +29 -0
- data/app/jobs/command_tower/application_job.rb +4 -0
- data/app/mailers/command_tower/application_mailer.rb +8 -0
- data/app/mailers/command_tower/email_verification_mailer.rb +12 -0
- data/app/models/command_tower/application_record.rb +45 -0
- data/app/models/message.rb +30 -0
- data/app/models/message_blast.rb +27 -0
- data/app/models/user.rb +61 -0
- data/app/models/user_secret.rb +72 -0
- data/app/services/command_tower/README.md +49 -0
- data/app/services/command_tower/argument_validation/README.md +192 -0
- data/app/services/command_tower/argument_validation/class_methods.rb +178 -0
- data/app/services/command_tower/argument_validation/instance_methods.rb +148 -0
- data/app/services/command_tower/argument_validation.rb +11 -0
- data/app/services/command_tower/authorize/validate.rb +49 -0
- data/app/services/command_tower/inbox_service/blast/delete.rb +23 -0
- data/app/services/command_tower/inbox_service/blast/metadata.rb +26 -0
- data/app/services/command_tower/inbox_service/blast/new_user_blaster.rb +24 -0
- data/app/services/command_tower/inbox_service/blast/retrieve.rb +30 -0
- data/app/services/command_tower/inbox_service/blast/upsert.rb +67 -0
- data/app/services/command_tower/inbox_service/message/metadata.rb +35 -0
- data/app/services/command_tower/inbox_service/message/modify.rb +44 -0
- data/app/services/command_tower/inbox_service/message/retrieve.rb +36 -0
- data/app/services/command_tower/inbox_service/message/send.rb +33 -0
- data/app/services/command_tower/jwt/authenticate_user.rb +86 -0
- data/app/services/command_tower/jwt/decode.rb +21 -0
- data/app/services/command_tower/jwt/encode.rb +15 -0
- data/app/services/command_tower/jwt/login_create.rb +21 -0
- data/app/services/command_tower/jwt/time_delay_token.rb +17 -0
- data/app/services/command_tower/login_strategy/plain_text/create.rb +43 -0
- data/app/services/command_tower/login_strategy/plain_text/email_verification/generate.rb +29 -0
- data/app/services/command_tower/login_strategy/plain_text/email_verification/required.rb +20 -0
- data/app/services/command_tower/login_strategy/plain_text/email_verification/send.rb +23 -0
- data/app/services/command_tower/login_strategy/plain_text/email_verification/verify.rb +24 -0
- data/app/services/command_tower/login_strategy/plain_text/login.rb +50 -0
- data/app/services/command_tower/secrets/cleanse.rb +14 -0
- data/app/services/command_tower/secrets/generate.rb +62 -0
- data/app/services/command_tower/secrets/verify.rb +27 -0
- data/app/services/command_tower/secrets.rb +15 -0
- data/app/services/command_tower/service_base.rb +89 -0
- data/app/services/command_tower/service_logging.rb +41 -0
- data/app/services/command_tower/user_attributes/modify.rb +68 -0
- data/app/services/command_tower/user_attributes/roles.rb +27 -0
- data/app/services/command_tower/username/available.rb +64 -0
- data/app/views/command_tower/email_verification_mailer/verify_email.html.erb +26 -0
- data/config/routes.rb +55 -0
- data/db/migrate/20241117043720_create_command_tower_users.rb +42 -0
- data/db/migrate/20241204065708_create_command_tower_user_secrets.rb +16 -0
- data/db/migrate/20250223023306_create_command_tower_messages.rb +12 -0
- data/db/migrate/20250223023313_create_command_tower_message_blasts.rb +14 -0
- data/lib/command_tower/authorization/default.yml +42 -0
- data/lib/command_tower/authorization/entity.rb +101 -0
- data/lib/command_tower/authorization/role.rb +101 -0
- data/lib/command_tower/authorization.rb +85 -0
- data/lib/command_tower/configuration/admin/config.rb +18 -0
- data/lib/command_tower/configuration/application/config.rb +40 -0
- data/lib/command_tower/configuration/authorization/config.rb +24 -0
- data/lib/command_tower/configuration/base.rb +11 -0
- data/lib/command_tower/configuration/config.rb +77 -0
- data/lib/command_tower/configuration/email/config.rb +87 -0
- data/lib/command_tower/configuration/jwt/config.rb +22 -0
- data/lib/command_tower/configuration/login/config.rb +18 -0
- data/lib/command_tower/configuration/login/strategy/plain_text/config.rb +57 -0
- data/lib/command_tower/configuration/login/strategy/plain_text/email_verify.rb +50 -0
- data/lib/command_tower/configuration/login/strategy/plain_text/lockable.rb +27 -0
- data/lib/command_tower/configuration/otp/config.rb +54 -0
- data/lib/command_tower/configuration/user/config.rb +56 -0
- data/lib/command_tower/configuration/username/check.rb +31 -0
- data/lib/command_tower/configuration/username/config.rb +41 -0
- data/lib/command_tower/engine.rb +53 -0
- data/lib/command_tower/error.rb +5 -0
- data/lib/command_tower/schema/admin/users.rb +15 -0
- data/lib/command_tower/schema/error/base.rb +15 -0
- data/lib/command_tower/schema/error/invalid_argument.rb +15 -0
- data/lib/command_tower/schema/error/invalid_argument_response.rb +17 -0
- data/lib/command_tower/schema/inbox/blast_request.rb +15 -0
- data/lib/command_tower/schema/inbox/blast_response.rb +16 -0
- data/lib/command_tower/schema/inbox/message_blast_entity.rb +16 -0
- data/lib/command_tower/schema/inbox/message_blast_metadata.rb +16 -0
- data/lib/command_tower/schema/inbox/message_entity.rb +14 -0
- data/lib/command_tower/schema/inbox/metadata.rb +18 -0
- data/lib/command_tower/schema/inbox/modified.rb +13 -0
- data/lib/command_tower/schema/page.rb +14 -0
- data/lib/command_tower/schema/plain_text/create_user_request.rb +18 -0
- data/lib/command_tower/schema/plain_text/create_user_response.rb +17 -0
- data/lib/command_tower/schema/plain_text/email_verify_request.rb +11 -0
- data/lib/command_tower/schema/plain_text/email_verify_response.rb +11 -0
- data/lib/command_tower/schema/plain_text/email_verify_send_request.rb +9 -0
- data/lib/command_tower/schema/plain_text/email_verify_send_response.rb +11 -0
- data/lib/command_tower/schema/plain_text/login_request.rb +15 -0
- data/lib/command_tower/schema/plain_text/login_response.rb +13 -0
- data/lib/command_tower/schema/user.rb +28 -0
- data/lib/command_tower/schema.rb +38 -0
- data/lib/command_tower/spec_helper.rb +19 -0
- data/lib/command_tower/version.rb +5 -0
- data/lib/command_tower.rb +33 -0
- data/lib/generators/api_engine_base/configure/USAGE +8 -0
- data/lib/generators/api_engine_base/configure/configure_generator.rb +12 -0
- data/lib/tasks/auto_annotate_models.rake +60 -0
- metadata +255 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::UserAttributes
|
4
|
+
class Modify < CommandTower::ServiceBase
|
5
|
+
on_argument_validation :fail_early
|
6
|
+
DEFAULT = {
|
7
|
+
verifier_token: [true, false]
|
8
|
+
}
|
9
|
+
validate :user, is_a: User, required: true
|
10
|
+
validate :admin_user, is_a: User, required: false
|
11
|
+
|
12
|
+
# Gets assigned during configuration phase via
|
13
|
+
# lib/command_tower/configuration/user/config.rb
|
14
|
+
def self.assign!
|
15
|
+
attributes = CommandTower.config.user.default_attributes_for_change + CommandTower.config.user.additional_attributes_for_change
|
16
|
+
one_of(:modify_attribute, required: true) do
|
17
|
+
attributes.uniq.each do |attribute|
|
18
|
+
if metadata = User.attribute_to_type_mapping[attribute]
|
19
|
+
arguments = {}
|
20
|
+
if default = DEFAULT[attribute.to_sym]
|
21
|
+
arguments[:is_one] = default
|
22
|
+
else
|
23
|
+
if allowed_types = metadata[:allowed_types]
|
24
|
+
arguments[:is_one] = allowed_types
|
25
|
+
else
|
26
|
+
arguments[:is_a] = metadata[:ruby_type]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
validate(attribute, **arguments)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def call
|
37
|
+
case modify_attribute_key
|
38
|
+
when :email
|
39
|
+
unless email =~ URI::MailTo::EMAIL_REGEXP
|
40
|
+
inline_argument_failure!(errors: { email: "Invalid email address" })
|
41
|
+
end
|
42
|
+
when :username
|
43
|
+
username_validity = CommandTower::Username::Available.(username:)
|
44
|
+
unless username_validity.valid
|
45
|
+
inline_argument_failure!(errors: { username: "Username is invalid. #{CommandTower.config.username.username_failure_message}" })
|
46
|
+
end
|
47
|
+
when :verifier_token
|
48
|
+
if verifier_token
|
49
|
+
verifier_token!
|
50
|
+
else
|
51
|
+
inline_argument_failure!(errors: { verifier_token: "verifier_token is invalid. Expected [true] when value present" })
|
52
|
+
end
|
53
|
+
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
update!
|
58
|
+
end
|
59
|
+
|
60
|
+
def verifier_token!
|
61
|
+
user.reset_verifier_token!
|
62
|
+
end
|
63
|
+
|
64
|
+
def update!
|
65
|
+
user.update!(modify_attribute_key => modify_attribute)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::UserAttributes
|
4
|
+
class Roles < CommandTower::ServiceBase
|
5
|
+
on_argument_validation :fail_early
|
6
|
+
|
7
|
+
validate :user, is_a: User, required: true
|
8
|
+
validate :admin_user, is_a: User, required: true
|
9
|
+
validate :roles, is_a: Array, required: true
|
10
|
+
|
11
|
+
def call
|
12
|
+
if valid_roles?
|
13
|
+
user.update!(roles:)
|
14
|
+
return true
|
15
|
+
end
|
16
|
+
|
17
|
+
inline_argument_failure!(errors: { roles: "Invalid roles provided" })
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid_roles?
|
21
|
+
return true if roles.empty?
|
22
|
+
|
23
|
+
available_roles = CommandTower::Authorization::Role.roles.keys
|
24
|
+
roles.all? { available_roles.include?(_1) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::Username
|
4
|
+
class Available < CommandTower::ServiceBase
|
5
|
+
on_argument_validation :fail_early
|
6
|
+
|
7
|
+
REFRESH_KEY = "username.refresh_after"
|
8
|
+
|
9
|
+
validate :username, is_a: String, required: true
|
10
|
+
validate :force_query, is_a: [TrueClass, FalseClass]
|
11
|
+
|
12
|
+
def initialize(*)
|
13
|
+
super
|
14
|
+
|
15
|
+
@mutex = Mutex.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
populate_local_cache! if refresh?
|
20
|
+
|
21
|
+
context.available = available?
|
22
|
+
context.valid = valid?
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid?
|
26
|
+
return false if username.length < CommandTower.config.username.username_length_min
|
27
|
+
return false if username.length > CommandTower.config.username.username_length_max
|
28
|
+
|
29
|
+
!!username[CommandTower.config.username.username_regex]
|
30
|
+
end
|
31
|
+
|
32
|
+
def available?
|
33
|
+
!realtime.local_cache.exist?(username)
|
34
|
+
end
|
35
|
+
|
36
|
+
# this is a very terrible cache design at scale
|
37
|
+
# If we can use Redis, a bloom filter would be great
|
38
|
+
def populate_local_cache!
|
39
|
+
@mutex.synchronize do
|
40
|
+
values = User.pluck(:username).map { [_1, 1] }.to_h rescue {}
|
41
|
+
realtime.local_cache.write_multi(values)
|
42
|
+
realtime.local_cache.write(REFRESH_KEY, realtime.local_cache_ttl.from_now)
|
43
|
+
|
44
|
+
values
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def refresh?
|
49
|
+
return true if force_query
|
50
|
+
|
51
|
+
refresh_by = realtime.local_cache.read(REFRESH_KEY)
|
52
|
+
return true if refresh_by.nil?
|
53
|
+
|
54
|
+
time = Time.at(refresh_by) rescue nil
|
55
|
+
return true if time.nil?
|
56
|
+
|
57
|
+
time < Time.now
|
58
|
+
end
|
59
|
+
|
60
|
+
def realtime
|
61
|
+
CommandTower.config.username.realtime_username_check
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<p>Hello <%= @user.full_name %>!</p>
|
2
|
+
|
3
|
+
<p>
|
4
|
+
Welcome to <%= CommandTower.config.app.communication_name %>.
|
5
|
+
</p>
|
6
|
+
<p>
|
7
|
+
Please provide the following verification code to confirm your email address.
|
8
|
+
</p>
|
9
|
+
<p></p>
|
10
|
+
<p></p>
|
11
|
+
<p>
|
12
|
+
<h1>
|
13
|
+
<%= @code %>
|
14
|
+
</h1>
|
15
|
+
</p>
|
16
|
+
|
17
|
+
<p></p>
|
18
|
+
<p></p>
|
19
|
+
|
20
|
+
<p>
|
21
|
+
Best wishes, <%= CommandTower.config.app.communication_name %> team
|
22
|
+
</p>
|
23
|
+
<p></p>
|
24
|
+
<p></p>
|
25
|
+
|
26
|
+
Visit us at <a href="<%= CommandTower.config.app.composed_url %>" target="_blank"><%= CommandTower.config.app.composed_url %></a>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
Rails.application.routes.draw do
|
2
|
+
append_to_ass = "command_tower"
|
3
|
+
|
4
|
+
constraints(->(_req) { CommandTower.config.username.realtime_username_check? }) do
|
5
|
+
scope "username" do
|
6
|
+
get "/available/:username", to: "command_tower/username#username_availability", as: :"#{append_to_ass}_username_availability_get"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
scope "auth" do
|
11
|
+
constraints(->(_req) { CommandTower.config.login.plain_text.enable? }) do
|
12
|
+
post "/login", to: "command_tower/auth/plain_text#login_post", as: :"#{append_to_ass}_auth_login_post"
|
13
|
+
post "/create", to: "command_tower/auth/plain_text#create_post", as: :"#{append_to_ass}_auth_create_post"
|
14
|
+
|
15
|
+
constraints(->(_req) { CommandTower.config.login.plain_text.email_verify? }) do
|
16
|
+
scope "email" do
|
17
|
+
post "/verify", to: "command_tower/auth/plain_text#email_verify_post", as: :"#{append_to_ass}_auth_email_verification"
|
18
|
+
post "/send", to: "command_tower/auth/plain_text#email_verify_resend_post", as: :"#{append_to_ass}_auth_email_verification_send"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
scope "user" do
|
25
|
+
get "/", to: "command_tower/user#show", as: :"#{append_to_ass}_user_show_get"
|
26
|
+
post "/modify", to: "command_tower/user#modify", as: :"#{append_to_ass}_user_modify_post"
|
27
|
+
end
|
28
|
+
|
29
|
+
scope "inbox" do
|
30
|
+
scope "messages" do
|
31
|
+
get "/", to: "command_tower/inbox/message#metadata", as: :"#{append_to_ass}_inbox_metadata"
|
32
|
+
|
33
|
+
get "/:id", to: "command_tower/inbox/message#message", as: :"#{append_to_ass}_inbox_message"
|
34
|
+
delete "/:id", to: "command_tower/inbox/message#delete", as: :"#{append_to_ass}_inbox_message_del"
|
35
|
+
|
36
|
+
post "/ack", to: "command_tower/inbox/message#ack", as: :"#{append_to_ass}_inbox_ack"
|
37
|
+
post "/delete", to: "command_tower/inbox/message#delete", as: :"#{append_to_ass}_inbox_delete"
|
38
|
+
end
|
39
|
+
|
40
|
+
scope "blast" do
|
41
|
+
get "/", to: "command_tower/inbox/message_blast#metadata", as: :"#{append_to_ass}_blast_metadata"
|
42
|
+
post "/", to: "command_tower/inbox/message_blast#create", as: :"#{append_to_ass}_blast_create"
|
43
|
+
|
44
|
+
get "/:id", to: "command_tower/inbox/message_blast#blast", as: :"#{append_to_ass}_blast_blast"
|
45
|
+
patch "/:id", to: "command_tower/inbox/message_blast#modify", as: :"#{append_to_ass}_blast_modify"
|
46
|
+
delete "/:id", to: "command_tower/inbox/message_blast#delete", as: :"#{append_to_ass}_blast_delete"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
scope "admin" do
|
51
|
+
get "/", to: "command_tower/admin#show", as: :"#{append_to_ass}_admin_show_get"
|
52
|
+
post "/modify", to: "command_tower/admin#modify", as: :"#{append_to_ass}_admin_modify_post"
|
53
|
+
post "/modify/role", to: "command_tower/admin#modify_role", as: :"#{append_to_ass}_admin_modify_role_post"
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class CreateCommandTowerUsers < ActiveRecord::Migration[7.2]
|
2
|
+
def change
|
3
|
+
create_table :users do |t|
|
4
|
+
t.string :first_name, null: false, default: ""
|
5
|
+
t.string :last_name, null: false, default: ""
|
6
|
+
t.string :username
|
7
|
+
|
8
|
+
t.string :last_known_timezone
|
9
|
+
t.timestamp :last_known_timezone_update
|
10
|
+
|
11
|
+
t.integer :successful_login, default: 0
|
12
|
+
t.string :last_login_strategy
|
13
|
+
t.datetime :last_login
|
14
|
+
|
15
|
+
t.string :roles, default: ""
|
16
|
+
|
17
|
+
###
|
18
|
+
# Database token to verify JWT
|
19
|
+
# Token will allow JWT values to expire/reset all devices
|
20
|
+
t.string :verifier_token
|
21
|
+
t.datetime :verifier_token_last_reset
|
22
|
+
|
23
|
+
# Login Strategy: PlainText
|
24
|
+
t.string :email, null: false, default: ""
|
25
|
+
t.boolean :email_validated, default: false
|
26
|
+
t.integer :password_consecutive_fail, default: 0
|
27
|
+
t.string :password_digest, null: false, default: ""
|
28
|
+
t.string :recovery_password_digest, null: false, default: ""
|
29
|
+
|
30
|
+
# OTP strategy
|
31
|
+
# t.string :otp_secret
|
32
|
+
# t.string :otp_temp_secret
|
33
|
+
# t.integer :otp_consumed_timestep
|
34
|
+
# t.boolean :otp_enabled, default: false
|
35
|
+
# t.text :otp_backup_codes
|
36
|
+
|
37
|
+
t.timestamps
|
38
|
+
end
|
39
|
+
|
40
|
+
add_index :users, :username, unique: true
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreateCommandTowerUserSecrets < ActiveRecord::Migration[7.2]
|
2
|
+
def change
|
3
|
+
create_table :user_secrets do |t|
|
4
|
+
t.references :user, null: false, foreign_key: true
|
5
|
+
t.integer :use_count, default: 0
|
6
|
+
t.integer :use_count_max
|
7
|
+
t.string :reason
|
8
|
+
t.string :extra
|
9
|
+
t.string :secret
|
10
|
+
t.datetime :death_time
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
add_index :user_secrets, :secret, unique: true
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateCommandTowerMessages < ActiveRecord::Migration[7.2]
|
2
|
+
def change
|
3
|
+
create_table :messages do |t|
|
4
|
+
t.timestamps
|
5
|
+
t.references :user, null: false, foreign_key: true # Required
|
6
|
+
t.text :text
|
7
|
+
t.string :title
|
8
|
+
t.boolean :viewed, default: false
|
9
|
+
t.boolean :pushed, default: false
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateCommandTowerMessageBlasts < ActiveRecord::Migration[7.2]
|
2
|
+
def change
|
3
|
+
create_table :message_blasts do |t|
|
4
|
+
t.timestamps
|
5
|
+
t.references :user, null: false, foreign_key: true # Required
|
6
|
+
t.text :text
|
7
|
+
t.string :title
|
8
|
+
t.boolean :existing_users, default: false
|
9
|
+
t.boolean :new_users, default: false
|
10
|
+
end
|
11
|
+
|
12
|
+
add_reference :messages, :message_blast, foreign_key: true, null: true
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
---
|
2
|
+
groups:
|
3
|
+
owner:
|
4
|
+
description: The owner of the application will have full access to all components
|
5
|
+
entities: true
|
6
|
+
admin:
|
7
|
+
description: |
|
8
|
+
This group defines permissions for Admin Read and Write operations. Users with this role will have
|
9
|
+
the ability to view and update other users states.
|
10
|
+
entities:
|
11
|
+
- admin
|
12
|
+
- message-blast
|
13
|
+
admin-without-impersonation:
|
14
|
+
description: |
|
15
|
+
This group defines permissions for Admin Read and Write operations. Users with this role will have
|
16
|
+
the ability to view and update other users states. However, impersonation is not permitted with this role
|
17
|
+
entities:
|
18
|
+
- admin-without-impersonate
|
19
|
+
- message-blast
|
20
|
+
admin-read-only:
|
21
|
+
description: |
|
22
|
+
This group defines permissions for Admin Read interface only.
|
23
|
+
entities:
|
24
|
+
- read-admin
|
25
|
+
- message-blast-read-only
|
26
|
+
entities:
|
27
|
+
- name: message-blast
|
28
|
+
controller: CommandTower::Inbox::MessageBlastController
|
29
|
+
- name: message-blast-read-only
|
30
|
+
controller: CommandTower::Inbox::MessageBlastController
|
31
|
+
only: metadata
|
32
|
+
- name: read-admin
|
33
|
+
controller: CommandTower::AdminController
|
34
|
+
only: show
|
35
|
+
- name: admin
|
36
|
+
controller: CommandTower::AdminController
|
37
|
+
- name: admin-without-impersonate
|
38
|
+
controller: CommandTower::AdminController
|
39
|
+
except: impersonate
|
40
|
+
|
41
|
+
|
42
|
+
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower
|
4
|
+
module Authorization
|
5
|
+
class Entity
|
6
|
+
class << self
|
7
|
+
def create_entity(name:, controller:, only: nil, except: nil)
|
8
|
+
if entities[name]
|
9
|
+
Rails.logger.warn("Warning: Authorization entity #{name} duplicated. Only the most recent one will persist")
|
10
|
+
end
|
11
|
+
|
12
|
+
entities[name] = new(name:, controller:, only:, except:)
|
13
|
+
|
14
|
+
entities[name]
|
15
|
+
end
|
16
|
+
|
17
|
+
def entities
|
18
|
+
@entities ||= ActiveSupport::HashWithIndifferentAccess.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def entities_reset!
|
22
|
+
@entities = ActiveSupport::HashWithIndifferentAccess.new
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :name, :controller, :only, :except
|
27
|
+
def initialize(name:, controller:, only: nil, except: nil)
|
28
|
+
@controller = controller
|
29
|
+
@except = except.nil? ? nil : Array(except).map(&:to_sym)
|
30
|
+
@only = only.nil? ? nil : Array(only).map(&:to_sym)
|
31
|
+
|
32
|
+
validate!
|
33
|
+
end
|
34
|
+
|
35
|
+
def humanize
|
36
|
+
"name:[#{name}]; controller:[#{controller}]; only:[#{only}]; except:[#{except}]"
|
37
|
+
end
|
38
|
+
|
39
|
+
# controller will be the class object
|
40
|
+
# method will be the string of the route method
|
41
|
+
def matches?(controller:, method:)
|
42
|
+
# Return early if the controller does not match the existing entity controller
|
43
|
+
return nil if @controller != controller
|
44
|
+
|
45
|
+
# We are in the correct controller
|
46
|
+
|
47
|
+
# if inclusions are not present, the check is on the entire contoller and we can return true
|
48
|
+
if only.nil? && except.nil?
|
49
|
+
return true
|
50
|
+
end
|
51
|
+
|
52
|
+
## `only` or `except` is present at this point
|
53
|
+
if only
|
54
|
+
# If method is included in only, accept otherwise return reject
|
55
|
+
return only.include?(method.to_sym)
|
56
|
+
else
|
57
|
+
# If method is included in except, reject otherwise return accept
|
58
|
+
return !except.include?(method.to_sym)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# This is a custom method that can get overridden by a child class for custom
|
63
|
+
# authorization logic beyond grouping
|
64
|
+
def authorized?(user:)
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def validate!
|
71
|
+
if @only && @except
|
72
|
+
raise Error, "kwargs `only` and `except` passed in. At most 1 can be passed in."
|
73
|
+
end
|
74
|
+
|
75
|
+
validate_controller!
|
76
|
+
validate_methods!(@only, :only)
|
77
|
+
validate_methods!(@except, :except)
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_controller!
|
81
|
+
return true if Class === @controller
|
82
|
+
|
83
|
+
@controller = @controller.constantize
|
84
|
+
rescue NameError => e
|
85
|
+
raise Error, "Controller [#{@controller}] was not found. Please validate spelling or ensure it is loaded earlier"
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate_methods!(array_of_methods, string)
|
89
|
+
return if array_of_methods.nil?
|
90
|
+
|
91
|
+
missing_methods = array_of_methods.select do |method|
|
92
|
+
!@controller.instance_methods.include?(method)
|
93
|
+
end
|
94
|
+
|
95
|
+
return true if missing_methods.empty?
|
96
|
+
|
97
|
+
raise Error, "#{string} parameter is invalid. Controller [#{@controller}] is missing methods:[#{missing_methods}]"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower
|
4
|
+
module Authorization
|
5
|
+
class Role
|
6
|
+
class << self
|
7
|
+
def create_role(name:, description:, entities: nil, allow_everything: false)
|
8
|
+
if roles[name]
|
9
|
+
raise Error, "Role [#{name}] already exists. Must use different name"
|
10
|
+
end
|
11
|
+
|
12
|
+
if allow_everything
|
13
|
+
Rails.logger.info { "Authorization Role: #{name} is granted authorization to all roles" }
|
14
|
+
else
|
15
|
+
unless Array(entities).all? { Entity === _1 }
|
16
|
+
raise Error, "Parameter :entities must include objects of or inherited by CommandTower::Authorization::Entity"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
roles[name] = new(name:, description:, entities:, allow_everything:)
|
21
|
+
# A role is `intended` to be immutable (attr_reader)
|
22
|
+
# Once the role is defined it will not get changed
|
23
|
+
# After it is created, add the mapping to the source of truth list of mapped method names to their controllers
|
24
|
+
CommandTower::Authorization.add_mapping!(role: roles[name])
|
25
|
+
|
26
|
+
roles[name]
|
27
|
+
end
|
28
|
+
|
29
|
+
def roles
|
30
|
+
@roles ||= ActiveSupport::HashWithIndifferentAccess.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def roles_reset!
|
34
|
+
@roles = ActiveSupport::HashWithIndifferentAccess.new
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :entities, :name, :description, :allow_everything
|
39
|
+
def initialize(name:, description:, entities:, allow_everything: false)
|
40
|
+
@name = name
|
41
|
+
@entities = Array(entities)
|
42
|
+
@description = description
|
43
|
+
@allow_everything = allow_everything
|
44
|
+
end
|
45
|
+
|
46
|
+
def authorized?(controller:, method:, user:)
|
47
|
+
return_value = { role: name, description: }
|
48
|
+
return return_value.merge(authorized: true, reason: "#{name} allows all authorizations") if allow_everything
|
49
|
+
|
50
|
+
matched_controllers = controller_entity_mapping[controller]
|
51
|
+
# if Role does not match any of the controllers
|
52
|
+
# explicitly return nil here to ensure upstream knows this role does not care about the route
|
53
|
+
return return_value.merge(authorized: nil, reason: "#{name} does not match") if matched_controllers.nil?
|
54
|
+
|
55
|
+
rejected_entities = matched_controllers.map do |entity|
|
56
|
+
case entity.matches?(controller:, method:)
|
57
|
+
when false, nil
|
58
|
+
{ authorized: false, entity: entity.name, controller:, readable: entity.humanize, status: "Rejected by inclusion" }
|
59
|
+
when true
|
60
|
+
# Entity matches all inclusions
|
61
|
+
if entity.authorized?(user:)
|
62
|
+
# Do nothing! Entity has authorized the user
|
63
|
+
else
|
64
|
+
{ authorized: false, entity: entity.name, controller:, readable: entity.humanize, status: "Rejected via custom Entity Authorization" }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end.compact
|
68
|
+
|
69
|
+
# If there were no entities that rejected authorization, return authorized
|
70
|
+
return return_value.merge(authorized: true, reason: "All entities approve authorization") if rejected_entities.empty?
|
71
|
+
|
72
|
+
return_value.merge(authorized: false, reason: "Subset of Entities Rejected authorization", rejected_entities:)
|
73
|
+
end
|
74
|
+
|
75
|
+
def guards
|
76
|
+
mapping = {}
|
77
|
+
controller_entity_mapping.each do |controller, entities|
|
78
|
+
mapping[controller] ||= []
|
79
|
+
entities.map do |entity|
|
80
|
+
if entity.only
|
81
|
+
# We only care about these methods on the controller
|
82
|
+
mapping[controller] += entity.only
|
83
|
+
elsif entity.except
|
84
|
+
# We care about all methods on the controller except these
|
85
|
+
mapping[controller] += controller.instance_methods(false) - entity.except
|
86
|
+
else
|
87
|
+
# We care about all methods on the controller
|
88
|
+
mapping[controller] += controller.instance_methods(false)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
mapping
|
94
|
+
end
|
95
|
+
|
96
|
+
def controller_entity_mapping
|
97
|
+
@controller_entity_mapping ||= @entities.group_by(&:controller)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "set"
|
5
|
+
require "command_tower/error"
|
6
|
+
require "command_tower/authorization/entity"
|
7
|
+
require "command_tower/authorization/role"
|
8
|
+
|
9
|
+
module CommandTower
|
10
|
+
module Authorization
|
11
|
+
module_function
|
12
|
+
|
13
|
+
class Error < CommandTower::Error; end
|
14
|
+
|
15
|
+
def mapped_controllers
|
16
|
+
@mapped_controllers ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_mapping!(role:)
|
20
|
+
role.guards.each do |controller, methods|
|
21
|
+
mapped_controllers[controller] ||= Set.new
|
22
|
+
mapped_controllers[controller] += methods
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def mapped_controllers_reset!
|
27
|
+
@mapped_controllers = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_defined!
|
31
|
+
provision_rbac_default!
|
32
|
+
provision_rbac_user_defined!
|
33
|
+
end
|
34
|
+
|
35
|
+
def provision_rbac_user_defined!
|
36
|
+
path = CommandTower.config.authorization.rbac_group_path
|
37
|
+
rbac_configuration = load_yaml(path)
|
38
|
+
provision_rbac_via_yaml(rbac_configuration)
|
39
|
+
end
|
40
|
+
|
41
|
+
def provision_rbac_default!
|
42
|
+
path = CommandTower::Engine.root.join("lib", "command_tower", "authorization", "default.yml")
|
43
|
+
rbac_configuration = load_yaml(path)
|
44
|
+
provision_rbac_via_yaml(rbac_configuration)
|
45
|
+
end
|
46
|
+
|
47
|
+
def load_yaml(path)
|
48
|
+
return nil unless File.exist?(path)
|
49
|
+
|
50
|
+
YAML.load_file(path)
|
51
|
+
end
|
52
|
+
|
53
|
+
def provision_rbac_via_yaml(rbac_configuration)
|
54
|
+
return if rbac_configuration.nil?
|
55
|
+
|
56
|
+
rbac_configuration["entities"].each do |entity|
|
57
|
+
CommandTower::Authorization::Entity.create_entity(
|
58
|
+
name: entity["name"],
|
59
|
+
controller: entity["controller"],
|
60
|
+
only: entity["only"],
|
61
|
+
except: entity["except"],
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
rbac_configuration["groups"].each do |name, metadata|
|
66
|
+
entities = nil
|
67
|
+
allow_everything = false
|
68
|
+
description = metadata["description"]
|
69
|
+
|
70
|
+
if metadata["entities"] == true
|
71
|
+
allow_everything = true
|
72
|
+
else
|
73
|
+
entities = CommandTower::Authorization::Entity.entities.map { |k, v| v if metadata["entities"].include?(k) }.compact
|
74
|
+
end
|
75
|
+
|
76
|
+
CommandTower::Authorization::Role.create_role(
|
77
|
+
name:,
|
78
|
+
entities:,
|
79
|
+
description:,
|
80
|
+
allow_everything:,
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "class_composer"
|
4
|
+
|
5
|
+
module CommandTower
|
6
|
+
module Configuration
|
7
|
+
module Admin
|
8
|
+
class Config
|
9
|
+
include ClassComposer::Generator
|
10
|
+
|
11
|
+
add_composer :enable,
|
12
|
+
desc: "Allow Admin Capabilities for the application. By default, this is enabled",
|
13
|
+
allowed: [FalseClass, TrueClass],
|
14
|
+
default: true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|