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.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +59 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/command_tower/admin_controller.rb +104 -0
  6. data/app/controllers/command_tower/application_controller.rb +81 -0
  7. data/app/controllers/command_tower/auth/plain_text_controller.rb +132 -0
  8. data/app/controllers/command_tower/inbox/message_blast_controller.rb +89 -0
  9. data/app/controllers/command_tower/inbox/message_controller.rb +79 -0
  10. data/app/controllers/command_tower/user_controller.rb +49 -0
  11. data/app/controllers/command_tower/username_controller.rb +26 -0
  12. data/app/helpers/command_tower/application_helper.rb +4 -0
  13. data/app/helpers/command_tower/schema_helper.rb +29 -0
  14. data/app/jobs/command_tower/application_job.rb +4 -0
  15. data/app/mailers/command_tower/application_mailer.rb +8 -0
  16. data/app/mailers/command_tower/email_verification_mailer.rb +12 -0
  17. data/app/models/command_tower/application_record.rb +45 -0
  18. data/app/models/message.rb +30 -0
  19. data/app/models/message_blast.rb +27 -0
  20. data/app/models/user.rb +61 -0
  21. data/app/models/user_secret.rb +72 -0
  22. data/app/services/command_tower/README.md +49 -0
  23. data/app/services/command_tower/argument_validation/README.md +192 -0
  24. data/app/services/command_tower/argument_validation/class_methods.rb +178 -0
  25. data/app/services/command_tower/argument_validation/instance_methods.rb +148 -0
  26. data/app/services/command_tower/argument_validation.rb +11 -0
  27. data/app/services/command_tower/authorize/validate.rb +49 -0
  28. data/app/services/command_tower/inbox_service/blast/delete.rb +23 -0
  29. data/app/services/command_tower/inbox_service/blast/metadata.rb +26 -0
  30. data/app/services/command_tower/inbox_service/blast/new_user_blaster.rb +24 -0
  31. data/app/services/command_tower/inbox_service/blast/retrieve.rb +30 -0
  32. data/app/services/command_tower/inbox_service/blast/upsert.rb +67 -0
  33. data/app/services/command_tower/inbox_service/message/metadata.rb +35 -0
  34. data/app/services/command_tower/inbox_service/message/modify.rb +44 -0
  35. data/app/services/command_tower/inbox_service/message/retrieve.rb +36 -0
  36. data/app/services/command_tower/inbox_service/message/send.rb +33 -0
  37. data/app/services/command_tower/jwt/authenticate_user.rb +86 -0
  38. data/app/services/command_tower/jwt/decode.rb +21 -0
  39. data/app/services/command_tower/jwt/encode.rb +15 -0
  40. data/app/services/command_tower/jwt/login_create.rb +21 -0
  41. data/app/services/command_tower/jwt/time_delay_token.rb +17 -0
  42. data/app/services/command_tower/login_strategy/plain_text/create.rb +43 -0
  43. data/app/services/command_tower/login_strategy/plain_text/email_verification/generate.rb +29 -0
  44. data/app/services/command_tower/login_strategy/plain_text/email_verification/required.rb +20 -0
  45. data/app/services/command_tower/login_strategy/plain_text/email_verification/send.rb +23 -0
  46. data/app/services/command_tower/login_strategy/plain_text/email_verification/verify.rb +24 -0
  47. data/app/services/command_tower/login_strategy/plain_text/login.rb +50 -0
  48. data/app/services/command_tower/secrets/cleanse.rb +14 -0
  49. data/app/services/command_tower/secrets/generate.rb +62 -0
  50. data/app/services/command_tower/secrets/verify.rb +27 -0
  51. data/app/services/command_tower/secrets.rb +15 -0
  52. data/app/services/command_tower/service_base.rb +89 -0
  53. data/app/services/command_tower/service_logging.rb +41 -0
  54. data/app/services/command_tower/user_attributes/modify.rb +68 -0
  55. data/app/services/command_tower/user_attributes/roles.rb +27 -0
  56. data/app/services/command_tower/username/available.rb +64 -0
  57. data/app/views/command_tower/email_verification_mailer/verify_email.html.erb +26 -0
  58. data/config/routes.rb +55 -0
  59. data/db/migrate/20241117043720_create_command_tower_users.rb +42 -0
  60. data/db/migrate/20241204065708_create_command_tower_user_secrets.rb +16 -0
  61. data/db/migrate/20250223023306_create_command_tower_messages.rb +12 -0
  62. data/db/migrate/20250223023313_create_command_tower_message_blasts.rb +14 -0
  63. data/lib/command_tower/authorization/default.yml +42 -0
  64. data/lib/command_tower/authorization/entity.rb +101 -0
  65. data/lib/command_tower/authorization/role.rb +101 -0
  66. data/lib/command_tower/authorization.rb +85 -0
  67. data/lib/command_tower/configuration/admin/config.rb +18 -0
  68. data/lib/command_tower/configuration/application/config.rb +40 -0
  69. data/lib/command_tower/configuration/authorization/config.rb +24 -0
  70. data/lib/command_tower/configuration/base.rb +11 -0
  71. data/lib/command_tower/configuration/config.rb +77 -0
  72. data/lib/command_tower/configuration/email/config.rb +87 -0
  73. data/lib/command_tower/configuration/jwt/config.rb +22 -0
  74. data/lib/command_tower/configuration/login/config.rb +18 -0
  75. data/lib/command_tower/configuration/login/strategy/plain_text/config.rb +57 -0
  76. data/lib/command_tower/configuration/login/strategy/plain_text/email_verify.rb +50 -0
  77. data/lib/command_tower/configuration/login/strategy/plain_text/lockable.rb +27 -0
  78. data/lib/command_tower/configuration/otp/config.rb +54 -0
  79. data/lib/command_tower/configuration/user/config.rb +56 -0
  80. data/lib/command_tower/configuration/username/check.rb +31 -0
  81. data/lib/command_tower/configuration/username/config.rb +41 -0
  82. data/lib/command_tower/engine.rb +53 -0
  83. data/lib/command_tower/error.rb +5 -0
  84. data/lib/command_tower/schema/admin/users.rb +15 -0
  85. data/lib/command_tower/schema/error/base.rb +15 -0
  86. data/lib/command_tower/schema/error/invalid_argument.rb +15 -0
  87. data/lib/command_tower/schema/error/invalid_argument_response.rb +17 -0
  88. data/lib/command_tower/schema/inbox/blast_request.rb +15 -0
  89. data/lib/command_tower/schema/inbox/blast_response.rb +16 -0
  90. data/lib/command_tower/schema/inbox/message_blast_entity.rb +16 -0
  91. data/lib/command_tower/schema/inbox/message_blast_metadata.rb +16 -0
  92. data/lib/command_tower/schema/inbox/message_entity.rb +14 -0
  93. data/lib/command_tower/schema/inbox/metadata.rb +18 -0
  94. data/lib/command_tower/schema/inbox/modified.rb +13 -0
  95. data/lib/command_tower/schema/page.rb +14 -0
  96. data/lib/command_tower/schema/plain_text/create_user_request.rb +18 -0
  97. data/lib/command_tower/schema/plain_text/create_user_response.rb +17 -0
  98. data/lib/command_tower/schema/plain_text/email_verify_request.rb +11 -0
  99. data/lib/command_tower/schema/plain_text/email_verify_response.rb +11 -0
  100. data/lib/command_tower/schema/plain_text/email_verify_send_request.rb +9 -0
  101. data/lib/command_tower/schema/plain_text/email_verify_send_response.rb +11 -0
  102. data/lib/command_tower/schema/plain_text/login_request.rb +15 -0
  103. data/lib/command_tower/schema/plain_text/login_response.rb +13 -0
  104. data/lib/command_tower/schema/user.rb +28 -0
  105. data/lib/command_tower/schema.rb +38 -0
  106. data/lib/command_tower/spec_helper.rb +19 -0
  107. data/lib/command_tower/version.rb +5 -0
  108. data/lib/command_tower.rb +33 -0
  109. data/lib/generators/api_engine_base/configure/USAGE +8 -0
  110. data/lib/generators/api_engine_base/configure/configure_generator.rb +12 -0
  111. data/lib/tasks/auto_annotate_models.rake +60 -0
  112. 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