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,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower
|
4
|
+
module InboxService
|
5
|
+
module Message
|
6
|
+
class Send < CommandTower::ServiceBase
|
7
|
+
validate :user, is_a: User, required: true
|
8
|
+
validate :text, is_a: String, required: true
|
9
|
+
validate :title, is_a: String, required: true
|
10
|
+
validate :message_blast, is_a: ::MessageBlast, required: false
|
11
|
+
validate :pushed, is_one: [true, false], default: false
|
12
|
+
|
13
|
+
def call
|
14
|
+
message = create_message!
|
15
|
+
context.message = message
|
16
|
+
end
|
17
|
+
|
18
|
+
def push_notification!
|
19
|
+
# TODO: Push notifications
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_message!
|
23
|
+
::Message.create!(
|
24
|
+
user:,
|
25
|
+
text: ,
|
26
|
+
title: ,
|
27
|
+
message_blast:,
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::Jwt
|
4
|
+
class AuthenticateUser < CommandTower::ServiceBase
|
5
|
+
|
6
|
+
validate :token, is_a: String, required: true, sensitive: true
|
7
|
+
validate :bypass_email_validation, is_one: [true, false], default: false
|
8
|
+
validate :with_reset, is_one: [true, false], default: false
|
9
|
+
|
10
|
+
def call
|
11
|
+
result = Decode.(token:)
|
12
|
+
|
13
|
+
if result.failure?
|
14
|
+
context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
|
15
|
+
end
|
16
|
+
payload = result.payload
|
17
|
+
|
18
|
+
expires_at = validate_generated_at!(generated_at: payload[:generated_at])
|
19
|
+
|
20
|
+
user = User.find(payload[:user_id]) rescue nil
|
21
|
+
if user.nil?
|
22
|
+
log_warn("user_id [#{payload[:user_id]}] was not found. Cannot Continue")
|
23
|
+
context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
|
24
|
+
end
|
25
|
+
|
26
|
+
if user.verifier_token == payload[:verifier_token]
|
27
|
+
context.user = user
|
28
|
+
else
|
29
|
+
context.fail!(msg: "Unauthorized Access. Token is no longer valid")
|
30
|
+
end
|
31
|
+
|
32
|
+
email_validation_required!(user:)
|
33
|
+
|
34
|
+
if with_reset
|
35
|
+
context.generated_token = CommandTower::Jwt::LoginCreate.(user:).token
|
36
|
+
expires_at = CommandTower.config.jwt.ttl.from_now.to_time
|
37
|
+
end
|
38
|
+
|
39
|
+
context.expires_at = expires_at.to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def validate_generated_at!(generated_at:)
|
43
|
+
if generated_at.nil?
|
44
|
+
log_warn("generated_at payload is missing from the JWT token. Cannot continue")
|
45
|
+
context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
|
46
|
+
end
|
47
|
+
|
48
|
+
expires_time = begin
|
49
|
+
time = Time.at(generated_at)
|
50
|
+
time + CommandTower.config.jwt.ttl
|
51
|
+
rescue
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
if expires_time.nil?
|
56
|
+
log_warn("generated_at payload cannot be parsed. Cannot continue")
|
57
|
+
context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
|
58
|
+
end
|
59
|
+
|
60
|
+
if expires_time < Time.now
|
61
|
+
log_warn("generated_at is no longer valid. Must request new token")
|
62
|
+
context.fail!(msg: "Unauthorized Access. Invalid Authorization token")
|
63
|
+
end
|
64
|
+
|
65
|
+
expires_time
|
66
|
+
end
|
67
|
+
|
68
|
+
def email_validation_required!(user:)
|
69
|
+
return unless CommandTower.config.login.plain_text.email_verify?
|
70
|
+
|
71
|
+
if bypass_email_validation
|
72
|
+
log_info("Bypassing email validation without checking if user should be able to continue")
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
return if user.email_validated
|
77
|
+
|
78
|
+
log_info("User's email is not yet validated.")
|
79
|
+
result = CommandTower::LoginStrategy::PlainText::EmailVerification::Required.(user:)
|
80
|
+
|
81
|
+
if result.required
|
82
|
+
context.fail!(msg: "User's Email must be validated before they can continue")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jwt"
|
4
|
+
|
5
|
+
module CommandTower::Jwt
|
6
|
+
class Decode < CommandTower::ServiceBase
|
7
|
+
|
8
|
+
validate :token, is_a: String, required: true, sensitive: true
|
9
|
+
|
10
|
+
def call
|
11
|
+
data = JWT.decode(token, CommandTower.config.jwt.hmac_secret, true, { algorithm: "HS256" })
|
12
|
+
|
13
|
+
context.payload = data[0].with_indifferent_access
|
14
|
+
context.headers = data[1].with_indifferent_access
|
15
|
+
rescue JWT::DecodeError => e
|
16
|
+
log_error(e)
|
17
|
+
|
18
|
+
context.fail!(msg: "Invalid Token")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jwt"
|
4
|
+
|
5
|
+
module CommandTower::Jwt
|
6
|
+
class Encode < CommandTower::ServiceBase
|
7
|
+
|
8
|
+
validate :payload, is_a: Hash, required: true
|
9
|
+
validate :header, is_a: Hash, required: false
|
10
|
+
|
11
|
+
def call
|
12
|
+
context.token = JWT.encode(payload, CommandTower.config.jwt.hmac_secret, "HS256", header || {})
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::Jwt
|
4
|
+
class LoginCreate < CommandTower::ServiceBase
|
5
|
+
on_argument_validation :fail_early
|
6
|
+
|
7
|
+
validate :user, is_a: User, required: true
|
8
|
+
|
9
|
+
def call
|
10
|
+
context.token = Encode.(payload:).token
|
11
|
+
end
|
12
|
+
|
13
|
+
def payload
|
14
|
+
{
|
15
|
+
generated_at: Time.now.to_i,
|
16
|
+
user_id: user.id,
|
17
|
+
verifier_token: user.retreive_verifier_token!,
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::Jwt
|
4
|
+
class TimeDelayToken < CommandTower::ServiceBase
|
5
|
+
on_argument_validation :fail_early
|
6
|
+
|
7
|
+
validate :expires_in, is_a: ActiveSupport::Duration, required: true
|
8
|
+
|
9
|
+
def call
|
10
|
+
context.token = Encode.(payload:).token
|
11
|
+
end
|
12
|
+
|
13
|
+
def payload
|
14
|
+
{ expires_in: expires_in }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::LoginStrategy::PlainText
|
4
|
+
class Create < CommandTower::ServiceBase
|
5
|
+
on_argument_validation :fail_early
|
6
|
+
|
7
|
+
EASY_GETTER = CommandTower.config.login.plain_text
|
8
|
+
|
9
|
+
validate :first_name, is_a: String, required: true
|
10
|
+
validate :last_name, is_a: String, required: true
|
11
|
+
validate :username, is_a: String, required: true
|
12
|
+
validate :email, length: true, lt: EASY_GETTER.email_length_max, gt: EASY_GETTER.email_length_min, is_a: String, required: true, sensitive: true
|
13
|
+
validate :password, length: true, lt: EASY_GETTER.password_length_max, gt: EASY_GETTER.password_length_min, is_a: String, required: true, sensitive: true
|
14
|
+
validate :password_confirmation, is_a: String, required: true, sensitive: true
|
15
|
+
|
16
|
+
def call
|
17
|
+
unless email =~ URI::MailTo::EMAIL_REGEXP
|
18
|
+
inline_argument_failure!(errors: { email: "Invalid email address" })
|
19
|
+
end
|
20
|
+
|
21
|
+
username_validity = CommandTower::Username::Available.(username:)
|
22
|
+
if !username_validity.valid
|
23
|
+
inline_argument_failure!(errors: { username: "Username is invalid. #{CommandTower.config.username.username_failure_message}" })
|
24
|
+
end
|
25
|
+
|
26
|
+
user = User.new(
|
27
|
+
first_name:,
|
28
|
+
last_name:,
|
29
|
+
username:,
|
30
|
+
email:,
|
31
|
+
password:,
|
32
|
+
password_confirmation:,
|
33
|
+
)
|
34
|
+
|
35
|
+
if user.save
|
36
|
+
context.user = user
|
37
|
+
CommandTower::InboxService::Blast::NewUserBlaster.(user:)
|
38
|
+
else
|
39
|
+
inline_argument_failure!(errors: user.errors)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::LoginStrategy::PlainText::EmailVerification
|
4
|
+
class Generate < CommandTower::ServiceBase
|
5
|
+
validate :user, is_a: User, required: true
|
6
|
+
|
7
|
+
def call
|
8
|
+
result = CommandTower::Secrets::Generate.(
|
9
|
+
user:,
|
10
|
+
secret_length: email_verify.verify_code_length,
|
11
|
+
reason: CommandTower::Secrets::EMAIL_VERIFICIATION,
|
12
|
+
use_count_max: 1,
|
13
|
+
death_time: email_verify.verify_code_link_valid_for,
|
14
|
+
type: CommandTower::Secrets::NUMERIC,
|
15
|
+
cleanse: true,
|
16
|
+
)
|
17
|
+
|
18
|
+
if result.failure?
|
19
|
+
context.fail!(msg: "Secret Generation is not available at this time")
|
20
|
+
end
|
21
|
+
|
22
|
+
context.secret = result.secret
|
23
|
+
end
|
24
|
+
|
25
|
+
def email_verify
|
26
|
+
CommandTower.config.login.plain_text.email_verify
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::LoginStrategy::PlainText::EmailVerification
|
4
|
+
class Required < CommandTower::ServiceBase
|
5
|
+
validate :user, is_a: User, required: true
|
6
|
+
|
7
|
+
def call
|
8
|
+
context.reqired_after_time = reqired_after_time
|
9
|
+
context.required = Time.now > reqired_after_time
|
10
|
+
end
|
11
|
+
|
12
|
+
def reqired_after_time
|
13
|
+
user.created_at + email_verify.verify_email_required_within
|
14
|
+
end
|
15
|
+
|
16
|
+
def email_verify
|
17
|
+
CommandTower.config.login.plain_text.email_verify
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::LoginStrategy::PlainText::EmailVerification
|
4
|
+
class Send < CommandTower::ServiceBase
|
5
|
+
on_argument_validation :fail_early
|
6
|
+
|
7
|
+
validate :user, is_a: User, required: true
|
8
|
+
|
9
|
+
def call
|
10
|
+
result = Generate.(user:)
|
11
|
+
if result.failure?
|
12
|
+
context.fail!(msg: result.msg)
|
13
|
+
end
|
14
|
+
|
15
|
+
begin
|
16
|
+
CommandTower::EmailVerificationMailer.verify_email(user.email, user, result.secret).deliver
|
17
|
+
rescue StandardError => e
|
18
|
+
log_error("Failed to send message to [#{user.id}]: #{e.message}")
|
19
|
+
context.fail!(msg: "Unable to send email. Please try again later", status: 500)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::LoginStrategy::PlainText::EmailVerification
|
4
|
+
class Verify < CommandTower::ServiceBase
|
5
|
+
on_argument_validation :fail_early
|
6
|
+
|
7
|
+
validate :user, is_a: User, required: true
|
8
|
+
validate :code, is_a: String, required: true
|
9
|
+
|
10
|
+
def call
|
11
|
+
result = CommandTower::Secrets::Verify.(secret: code, reason: CommandTower::Secrets::EMAIL_VERIFICIATION)
|
12
|
+
if result.failure?
|
13
|
+
inline_argument_failure!(errors: { code: "Incorrect verification code provided" })
|
14
|
+
end
|
15
|
+
|
16
|
+
if result.user != user
|
17
|
+
log_warn("Yikes! The logged in user does not match the correct code. Kick them back out and do not verify")
|
18
|
+
inline_argument_failure!(errors: { code: "Incorrect verification code provided" })
|
19
|
+
end
|
20
|
+
|
21
|
+
user.update(email_validated: true)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::LoginStrategy::PlainText
|
4
|
+
class Login < CommandTower::ServiceBase
|
5
|
+
on_argument_validation :fail_early
|
6
|
+
|
7
|
+
one_of(:login_key, required: true) do
|
8
|
+
validate :username, is_a: String, sensitive: true
|
9
|
+
validate :email, is_a: String, sensitive: true
|
10
|
+
end
|
11
|
+
validate :password, is_a: String, required: true, sensitive: true
|
12
|
+
|
13
|
+
def call
|
14
|
+
if user.nil?
|
15
|
+
log_warn("Login attempted with [#{login_key_key}] => [#{login_key}]. Resource not found")
|
16
|
+
credential_mismatch!
|
17
|
+
end
|
18
|
+
|
19
|
+
if user.authenticate(password)
|
20
|
+
user.successful_login += 1
|
21
|
+
user.password_consecutive_fail = 0
|
22
|
+
user.save
|
23
|
+
else
|
24
|
+
user.password_consecutive_fail += 1
|
25
|
+
user.save
|
26
|
+
log_warn("Valid #{login_key_key}. Incorrect password. Consecutive Password failures: #{user.password_consecutive_fail}")
|
27
|
+
credential_mismatch!
|
28
|
+
end
|
29
|
+
|
30
|
+
context.user = user
|
31
|
+
|
32
|
+
result = CommandTower::Jwt::LoginCreate.(user:)
|
33
|
+
if result.failure?
|
34
|
+
context.fail!(msg: "Failed to generate Authorization. Please Try again")
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
context.token = result.token
|
39
|
+
end
|
40
|
+
|
41
|
+
def credential_mismatch!
|
42
|
+
msg = "Unauthorized Access. Incorrect Credentials"
|
43
|
+
inline_argument_failure!(errors: { login_key_key => msg, password: msg })
|
44
|
+
end
|
45
|
+
|
46
|
+
def user
|
47
|
+
@user ||= User.where(login_key_key => login_key).first
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::Secrets
|
4
|
+
class Cleanse < CommandTower::ServiceBase
|
5
|
+
validate :user, is_a: User, required: true
|
6
|
+
validate :reason, is_one: ALLOWED_SECRET_REASONS, required: true
|
7
|
+
|
8
|
+
def call
|
9
|
+
secrets = UserSecret.where(user:, reason:)
|
10
|
+
count = secrets.delete_all
|
11
|
+
log_info("Cleansed #{count} #{reason} secret(s) from the store for user [#{user.id}]")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::Secrets
|
4
|
+
class Generate < CommandTower::ServiceBase
|
5
|
+
MAX_RETRY = 10
|
6
|
+
|
7
|
+
validate :user, is_a: User, required: true
|
8
|
+
validate :secret_length, is_a: Integer, gte: 4, lte: 64, required: true
|
9
|
+
validate :reason, is_one: ALLOWED_SECRET_REASONS, required: true
|
10
|
+
validate :type, is_one: ALLOWED_SECRET_TYPES, default: DEFAULT_SECRET_TYPE
|
11
|
+
validate :extra, is_a: String, length: true, lt: 256
|
12
|
+
validate :cleanse, is_one: [true, false], default: false
|
13
|
+
at_least_one(:death, required: true) do
|
14
|
+
validate :death_time, is_a: ActiveSupport::Duration, gte: 10.seconds
|
15
|
+
validate :use_count_max, is_a: Integer, gte: 1
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
if cleanse && @attempts.nil?
|
20
|
+
# if this fails ... so be it
|
21
|
+
Cleanse.(user:, reason:)
|
22
|
+
end
|
23
|
+
|
24
|
+
@attempts ||= 1
|
25
|
+
record = UserSecret.create!(**db_params)
|
26
|
+
|
27
|
+
context.record = record
|
28
|
+
context.secret = record.secret
|
29
|
+
rescue ActiveRecord::RecordNotUnique => e
|
30
|
+
if @attempts < MAX_RETRY
|
31
|
+
@attempts += 1
|
32
|
+
log_warn("Duplicate Secret was generated. Attempting to retry: #{@attempts} of #{MAX_RETRY}")
|
33
|
+
retry
|
34
|
+
else
|
35
|
+
log_error("Duplicate Secret was generated. Exhausted Max attempts of #{MAX_RETRY}.")
|
36
|
+
context.fail!(msg: "Failed to generate Secret. Cannot Continue")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def db_params
|
41
|
+
{
|
42
|
+
death_time: death_time&.from_now,
|
43
|
+
use_count_max:,
|
44
|
+
extra:,
|
45
|
+
reason:,
|
46
|
+
secret: generate_secret,
|
47
|
+
user:,
|
48
|
+
}.compact
|
49
|
+
end
|
50
|
+
|
51
|
+
def generate_secret
|
52
|
+
case type
|
53
|
+
when :numeric
|
54
|
+
secret_length.times.map { SecureRandom.rand(0...10) }.join
|
55
|
+
when :alphanumeric, :hex
|
56
|
+
SecureRandom.public_send(type, secret_length)
|
57
|
+
when :uuid
|
58
|
+
SecureRandom.public_send(type)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::Secrets
|
4
|
+
class Verify < CommandTower::ServiceBase
|
5
|
+
validate :secret, is_a: String, required: true, sensitive: true
|
6
|
+
validate :reason, is_one: ALLOWED_SECRET_REASONS, required: true
|
7
|
+
validate :access_count, is_one: [true, false], default: false
|
8
|
+
|
9
|
+
def call
|
10
|
+
record = UserSecret.find_record(secret:, reason:, access_count:)
|
11
|
+
|
12
|
+
if record[:found] == false
|
13
|
+
context.fail!(record:, msg: "Secret not found")
|
14
|
+
end
|
15
|
+
|
16
|
+
if record[:valid] == false
|
17
|
+
if CommandTower.config.delete_secret_after_invalid
|
18
|
+
record[:record].destroy
|
19
|
+
end
|
20
|
+
|
21
|
+
context.fail!(record:, msg: "Secret is invalid. #{record[:record].invalid_reason.join(" ")}")
|
22
|
+
end
|
23
|
+
|
24
|
+
context.user = record[:user]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::Secrets
|
4
|
+
ALLOWED_SECRET_REASONS = [
|
5
|
+
EMAIL_VERIFICIATION = :email_verification,
|
6
|
+
SSO = :sso,
|
7
|
+
]
|
8
|
+
|
9
|
+
ALLOWED_SECRET_TYPES = [
|
10
|
+
DEFAULT_SECRET_TYPE = ALPHANUMERIC = :alphanumeric,
|
11
|
+
HEX = :hex,
|
12
|
+
NUMERIC = :numeric,
|
13
|
+
UUID = :uuid,
|
14
|
+
]
|
15
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactor"
|
4
|
+
|
5
|
+
class CommandTower::ServiceBase
|
6
|
+
class ServiceBaseError < CommandTower::Error; end;
|
7
|
+
class ValidationError < ServiceBaseError; end;
|
8
|
+
class ConfigurationError < ServiceBaseError; end;
|
9
|
+
|
10
|
+
class NameConflictError < CommandTower::Error; end
|
11
|
+
class DefaultValueError < CommandTower::Error; end
|
12
|
+
class OneOfError < CommandTower::Error; end
|
13
|
+
class NestedOneOfError < CommandTower::Error; end
|
14
|
+
class ArgumentValidationError < CommandTower::Error; end
|
15
|
+
|
16
|
+
class KeyValidationError < CommandTower::ServiceBase::ValidationError; end
|
17
|
+
class CompositionValidationError < CommandTower::ServiceBase::ValidationError; end
|
18
|
+
|
19
|
+
include Interactor
|
20
|
+
include CommandTower::ServiceLogging
|
21
|
+
include CommandTower::ArgumentValidation
|
22
|
+
|
23
|
+
ON_ARGUMENT_VALIDATION = [
|
24
|
+
DEFAULT_VALIDATION = :raise,
|
25
|
+
:fail_early,
|
26
|
+
:log,
|
27
|
+
]
|
28
|
+
|
29
|
+
def self.inherited(subclass)
|
30
|
+
# Add the base logging to the subclass.
|
31
|
+
# Since this is done at inheritance time it should always be the first and last hook to run.
|
32
|
+
subclass.around(:service_base_logging)
|
33
|
+
subclass.around(:internal_validate)
|
34
|
+
subclass.after(:sanitize_params)
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate!
|
38
|
+
# overload from child
|
39
|
+
end
|
40
|
+
|
41
|
+
def internal_validate(interactor)
|
42
|
+
# call validate that is overridden from child
|
43
|
+
begin
|
44
|
+
validate! # custom validations defined on the child class
|
45
|
+
run_validations! # ArgumentValidation's based on defined settings on child
|
46
|
+
rescue StandardError => e
|
47
|
+
log_error("Error during validation. #{e.message}")
|
48
|
+
raise
|
49
|
+
end
|
50
|
+
|
51
|
+
# call interactor
|
52
|
+
interactor.call
|
53
|
+
end
|
54
|
+
|
55
|
+
def service_base_logging(interactor)
|
56
|
+
beginning_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
57
|
+
|
58
|
+
# Pre processing stats
|
59
|
+
log_info("Start")
|
60
|
+
|
61
|
+
# Run the job!
|
62
|
+
interactor.call
|
63
|
+
|
64
|
+
# Set status for use in ensure block
|
65
|
+
status = :complete
|
66
|
+
|
67
|
+
# Capture Interactor::Failure for logging purposes, then reraise
|
68
|
+
rescue ::Interactor::Failure
|
69
|
+
# set status for use in ensure block
|
70
|
+
status = :failure
|
71
|
+
|
72
|
+
# Re-raise to let the core Interactor handle this
|
73
|
+
raise
|
74
|
+
# Capture exception explicitly for logging purposes, then reraise
|
75
|
+
rescue ::Exception => e
|
76
|
+
# set status for use in ensure block
|
77
|
+
status = :error
|
78
|
+
|
79
|
+
# Log error
|
80
|
+
log_error("Error #{e.class.name}")
|
81
|
+
|
82
|
+
raise
|
83
|
+
ensure
|
84
|
+
# Always log how long it took along with a status
|
85
|
+
finished_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
86
|
+
elapsed = ((finished_time - beginning_time) * 1000).round(2)
|
87
|
+
log_info("Finished with [#{status}]...elapsed #{elapsed}ms")
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandTower::ServiceLogging
|
4
|
+
def log_info(msg)
|
5
|
+
log(level: :info, msg:)
|
6
|
+
end
|
7
|
+
|
8
|
+
def log_warn(msg)
|
9
|
+
log(level: :warn, msg:)
|
10
|
+
end
|
11
|
+
|
12
|
+
def log_error(msg)
|
13
|
+
log(level: :error, msg:)
|
14
|
+
end
|
15
|
+
|
16
|
+
def log(level:, msg:)
|
17
|
+
logger.public_send(level, aletered_message(msg))
|
18
|
+
rescue StandardError
|
19
|
+
Rails.logger.public_send(level, aletered_message(msg))
|
20
|
+
end
|
21
|
+
|
22
|
+
def aletered_message(msg)
|
23
|
+
"#{log_prefix}: #{msg}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def logger
|
27
|
+
defined?(context) ? context.logger : Rails.logger
|
28
|
+
end
|
29
|
+
|
30
|
+
def log_prefix
|
31
|
+
"[#{class_name}-#{service_id}]"
|
32
|
+
end
|
33
|
+
|
34
|
+
def class_name
|
35
|
+
self.class.name
|
36
|
+
end
|
37
|
+
|
38
|
+
def service_id
|
39
|
+
@service_id ||= SecureRandom.alphanumeric(10)
|
40
|
+
end
|
41
|
+
end
|