Class: CommandTower::ApplicationController

Inherits:
ActionController::API
  • Object
show all
Includes:
Execution::HttpBoundary
Defined in:
app/controllers/command_tower/application_controller.rb

Constant Summary collapse

AUTHENTICATION_HEADER =
CommandTower::Jwt::AuthorizationHelper::AUTHENTICATION_HEADER
AUTHENTICATION_EXPIRE_HEADER =
CommandTower::Jwt::AuthorizationHelper::AUTHENTICATION_EXPIRE_HEADER
AUTHENTICATION_WITH_RESET =
CommandTower::Jwt::AuthorizationHelper::AUTHENTICATION_WITH_RESET

Instance Method Summary collapse

Instance Method Details

#authenticate_user!(bypass_email_validation: false) ⇒ Object

Authenticate user via the passed in header or cookie AUTHENTICATION_HEADER="Bearer value" or cookie (if enabled)



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'app/controllers/command_tower/application_controller.rb', line 20

def authenticate_user!(bypass_email_validation: false)
  # Extract and validate token (from header or cookie fallback)
  token_data = CommandTower::Jwt::AuthorizationHelper.extract_token(request)
  if token_data[:error]
    status = 401
    schema = CommandTower::Schema::Error::Base.new(status:, message: token_data[:message])
    render(json: schema.to_h, status:)
    return false
  end

  token = token_data[:token]
  token_source = token_data[:source]

  # CSRF validation for cookie-authenticated unsafe requests
  # Enforcement is centralized here because token_source is only reliably known in this shared filter
  if csrf_required?(request, token_source)
    csrf_validation = CommandTower::Jwt::CsrfHelper.validate(request)
    unless csrf_validation[:valid]
      status = 403
      schema = CommandTower::Schema::Error::Base.new(
        status: status.to_s,
        message: csrf_validation[:message]  # Stable error code: "csrf_missing" or "csrf_mismatch"
      )
      render(json: schema.to_h, status: status)
      return false
    end
  end

  with_reset = CommandTower::Jwt::AuthorizationHelper.get_with_reset_flag(request)
  result = CommandTower::Jwt::AuthenticateUser.(token:, bypass_email_validation:, with_reset:)
  if result.success?
    established = CommandTower::Impersonation::EstablishIdentity.call(
      actor: result.user,
      impersonation_session_id: result.impersonation_session_id
    )
    if established.expired?
      status = 401
      schema = CommandTower::Schema::Error::Base.new(
        status:,
        message: CommandTower::Errors::Auth::ImpersonationSessionExpiredError.new.message
      )
      render(json: schema.to_h, status:)
      return false
    end

    @current_user = established.user
    if with_reset
      # Use helper to set both headers and cookie (if enabled)
      CommandTower::Jwt::AuthorizationHelper.set_token(
        response,
        result.generated_token,
        expires_at: result.expires_at
      )

      # Handle CSRF cookie issuance/rotation for token reset path
      # CSRF cookie issuance rules:
      # - rotate_on_reset = true → force rotate (generate new token)
      # - rotate_on_reset = false → ensure exists only (create if missing, keep if exists)
      if CommandTower::Jwt::CsrfHelper.csrf_enabled?
        if CommandTower.config.jwt.cookie.csrf.rotate_on_reset
          CommandTower::Jwt::CsrfHelper.ensure_cookie(request, response, should_rotate: true)
        else
          # Ensure cookie exists (rotate-or-ensure model)
          CommandTower::Jwt::CsrfHelper.ensure_cookie(request, response, should_rotate: false)
        end
      end
    else
      # Only set expire header when not resetting
      response.set_header(AUTHENTICATION_EXPIRE_HEADER, result.expires_at)
    end
    true
  else
    # Check if this is an email validation failure (status 412)
    if result.status == 412
      status = 412
      schema = CommandTower::Schema::Error::EmailValidationRequired.new(
        status:,
        message: result.msg,
        meta: { email_validated: false }
      )
      # Do NOT clear cookie for 412 status - user needs to keep cookie to verify email
    else
      # If authentication failed and token came from cookie, clear the invalid cookie
      # Exception: Do not clear cookie for 412 status (email validation required)
      if token_source == :cookie
        CommandTower::Jwt::AuthorizationHelper.clear_cookie(response)
      end
      status = result.status || 401
      schema = CommandTower::Schema::Error::Base.new(status:, message: result.msg)
    end
    render(json: schema.to_h, status:)
    # Must return false so callbacks know to halt propagation
    false
  end
end

#authenticate_user_without_email_verification!Object

Authenticate user via the passed in header without validating email



118
119
120
# File 'app/controllers/command_tower/application_controller.rb', line 118

def authenticate_user_without_email_verification!
  authenticate_user!(bypass_email_validation: true)
end

#authorize_user!Object

After Authenticating user, see if the user needs authorization on the route



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'app/controllers/command_tower/application_controller.rb', line 124

def authorize_user!
  if current_user.nil?
    Rails.logger.error { "Current User is not defined. This means that authenticate_user! was not called" }
    status = 401
    schema = CommandTower::Schema::Error::Base.new(status:, message: "Bearer token missing")
    render(json: schema.to_h, status:)
    return false
  end
  result = CommandTower::Authorize::Validate.(user: current_user, controller: self.class, method: params[:action])

  if result.success?
    @current_user = result.user
    true
  else
    # Current user is not authorized for the current Controller#action
    status = 403
    schema = CommandTower::Schema::Error::Base.new(status:, message: result.msg)
    render(json: schema.to_h, status:)
    # Must return false so callbacks know to halt propagation
    false
  end
end

#current_userObject



147
148
149
# File 'app/controllers/command_tower/application_controller.rb', line 147

def current_user
  @current_user ||= nil
end

#safe_boolean(value:) ⇒ Object



11
12
13
14
15
# File 'app/controllers/command_tower/application_controller.rb', line 11

def safe_boolean(value:)
  return nil unless [true, false, "true", "false", "0", "1", 0, 1].include?(value)

  ActiveModel::Type::Boolean.new.cast(value)
end