Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
module Api
module Internal
module TwoFactorAuthentication
class PivCacController < ApplicationController
include CsrfTokenConcern
include ReauthenticationRequiredConcern

before_action :render_unauthorized, unless: :recently_authenticated_2fa?

after_action :add_csrf_token_header_to_response

respond_to :json

def update
result = ::TwoFactorAuthentication::PivCacUpdateForm.new(
user: current_user,
configuration_id: params[:id],
).submit(name: params[:name])

analytics.piv_cac_update_name_submitted(**result.to_h)

if result.success?
render json: { success: true }
else
render json: { success: false, error: result.first_error_message }, status: :bad_request
end
end

def destroy
result = ::TwoFactorAuthentication::PivCacDeleteForm.new(
user: current_user,
configuration_id: params[:id],
).submit

analytics.piv_cac_delete_submitted(**result.to_h)

if result.success?
create_user_event(:piv_cac_disabled)
revoke_remember_device(current_user)
deliver_push_notification
render json: { success: true }
else
render json: { success: false, error: result.first_error_message }, status: :bad_request
end
end

private

def deliver_push_notification
event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user)
PushNotification::HttpPush.deliver(event)
end

def render_unauthorized
render json: { error: 'Unauthorized' }, status: :unauthorized
end
end
end
end
end
76 changes: 76 additions & 0 deletions app/controllers/users/piv_cac_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
module Users
class PivCacController < ApplicationController
include ReauthenticationRequiredConcern

before_action :confirm_two_factor_authenticated
before_action :confirm_recently_authenticated_2fa
before_action :set_form
before_action :validate_configuration_exists
before_action :set_presenter

def edit; end

def update
result = form.submit(name: params.dig(:form, :name))

analytics.piv_cac_update_name_submitted(**result.to_h)

if result.success?
flash[:success] = presenter.rename_success_alert_text
redirect_to account_path
else
flash.now[:error] = result.first_error_message
render :edit
end
end

def destroy
result = form.submit

analytics.piv_cac_delete_submitted(**result.to_h)

if result.success?
create_user_event(:piv_cac_disabled)
revoke_remember_device(current_user)
deliver_push_notification

flash[:success] = presenter.delete_success_alert_text
redirect_to account_path
else
flash[:error] = result.first_error_message
redirect_to edit_piv_cac_path(id: params[:id])
end
end

private

def deliver_push_notification
event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user)
PushNotification::HttpPush.deliver(event)
end

def form
@form ||= form_class.new(user: current_user, configuration_id: params[:id])
end

def presenter
@presenter ||= TwoFactorAuthentication::PivCacEditPresenter.new
end

alias_method :set_form, :form
alias_method :set_presenter, :presenter

def form_class
case action_name
when 'edit', 'update'
TwoFactorAuthentication::PivCacUpdateForm
when 'destroy'
TwoFactorAuthentication::PivCacDeleteForm
end
end

def validate_configuration_exists
render_not_found if form.configuration.blank?
end
end
end
57 changes: 57 additions & 0 deletions app/forms/two_factor_authentication/piv_cac_delete_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module TwoFactorAuthentication
class PivCacDeleteForm
include ActiveModel::Model
include ActionView::Helpers::TranslationHelper

attr_reader :user, :configuration_id

validate :validate_configuration_exists
validate :validate_has_multiple_mfa

def initialize(user:, configuration_id:)
@user = user
@configuration_id = configuration_id
end

def submit
success = valid?

configuration.destroy if success

FormResponse.new(
success:,
errors:,
extra: extra_analytics_attributes,
serialize_error_details_only: true,
)
end

def configuration
@configuration ||= user.piv_cac_configurations.find_by(id: configuration_id)
end

private

def validate_configuration_exists
return if configuration.present?
errors.add(
:configuration_id,
:configuration_not_found,
message: t('errors.manage_authenticator.internal_error'),
)
end

def validate_has_multiple_mfa
return if !configuration || MfaPolicy.new(user).multiple_factors_enabled?
errors.add(
:configuration_id,
:only_method,
message: t('errors.manage_authenticator.remove_only_method_error'),
)
end

def extra_analytics_attributes
{ configuration_id: configuration_id }
end
end
end
70 changes: 70 additions & 0 deletions app/forms/two_factor_authentication/piv_cac_update_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
module TwoFactorAuthentication
class PivCacUpdateForm
include ActiveModel::Model
include ActionView::Helpers::TranslationHelper

attr_reader :user, :configuration_id

validate :validate_configuration_exists
validate :validate_unique_name

def initialize(user:, configuration_id:)
@user = user
@configuration_id = configuration_id
end

def submit(name:)
@name = name

success = valid?
if valid?
configuration.name = name
success = configuration.valid?
errors.merge!(configuration.errors)
configuration.save if success
end

FormResponse.new(
success:,
errors:,
extra: extra_analytics_attributes,
serialize_error_details_only: true,
)
end

def name
return @name if defined?(@name)
@name = configuration&.name
end

def configuration
@configuration ||= user.piv_cac_configurations.find_by(id: configuration_id)
end

private

def validate_configuration_exists
return if configuration.present?

errors.add(
:configuration_id,
:configuration_not_found,
message: t('errors.manage_authenticator.internal_error'),
)
end

def validate_unique_name
return unless user.piv_cac_configurations.where.not(id: configuration_id).find_by(name:)

errors.add(
:name,
:duplicate,
message: t('errors.manage_authenticator.unique_name_error'),
)
end

def extra_analytics_attributes
{ configuration_id: configuration_id }
end
end
end
31 changes: 31 additions & 0 deletions app/presenters/two_factor_authentication/piv_cac_edit_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module TwoFactorAuthentication
class PivCacEditPresenter
include ActionView::Helpers::TranslationHelper

def initialize; end

def heading
t('two_factor_authentication.piv_cac.edit_heading')
end

def nickname_field_label
t('two_factor_authentication.piv_cac.nickname')
end

def rename_button_label
t('two_factor_authentication.piv_cac.change_nickname')
end

def delete_button_label
t('two_factor_authentication.piv_cac.delete')
end

def rename_success_alert_text
t('two_factor_authentication.piv_cac.renamed')
end

def delete_success_alert_text
t('two_factor_authentication.piv_cac.deleted')
end
end
end
38 changes: 38 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3830,6 +3830,25 @@ def phone_input_country_changed(country_code:, **extra)
track_event(:phone_input_country_changed, country_code:, **extra)
end

# @param [Boolean] success
# @param [Hash] error_details
# @param [Integer] configuration_id
# Tracks when user attempts to delete a PIV/CAC configuraton
def piv_cac_delete_submitted(
success:,
configuration_id:,
error_details: nil,
**extra
)
track_event(
:piv_cac_delete_submitted,
success:,
error_details:,
configuration_id:,
**extra,
)
end

# @identity.idp.previous_event_name User Registration: piv cac disabled
# @identity.idp.previous_event_name PIV CAC disabled
# Tracks when user's piv cac is disabled
Expand Down Expand Up @@ -3866,6 +3885,25 @@ def piv_cac_setup_visited(in_account_creation_flow:, **extra)
)
end

# @param [Boolean] success
# @param [Hash] error_details
# @param [Integer] configuration_id
# Tracks when user submits a name change for a PIV/CAC configuraton
def piv_cac_update_name_submitted(
success:,
configuration_id:,
error_details: nil,
**extra
)
track_event(
:piv_cac_update_name_submitted,
success:,
error_details:,
configuration_id:,
**extra,
)
end

# @param [String] redirect_url URL user was directed to
# @param [String, nil] step which step
# @param [String, nil] location which part of a step, if applicable
Expand Down
31 changes: 16 additions & 15 deletions app/views/accounts/_piv_cac.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
<%= t('headings.account.federal_employee_id') %>
</h2>

<div class="border-bottom border-primary-light">
<% MfaContext.new(current_user).piv_cac_configurations.each do |piv_cac_configuration| %>
<div class="grid-row padding-1 border-top border-left border-right border-primary-light">
<div class="grid-col-8">
<div class="grid-col-12 tablet:grid-col-6">
<%= piv_cac_configuration.name %>
</div>
</div>
<% if MfaPolicy.new(current_user).multiple_factors_enabled? %>
<div class="grid-col-4 text-right">
<%= render 'accounts/actions/disable_piv_cac', id: piv_cac_configuration.id %>
</div>
<% end %>
</div>
<div role="list">
<% MfaContext.new(current_user).piv_cac_configurations.each do |configuration| %>
<%= render ManageableAuthenticatorComponent.new(
configuration:,
user_session:,
manage_url: edit_piv_cac_path(id: configuration.id),
manage_api_url: api_internal_two_factor_authentication_piv_cac_path(id: configuration.id),
custom_strings: {
deleted: t('two_factor_authentication.piv_cac.deleted'),
renamed: t('two_factor_authentication.piv_cac.renamed'),
manage_accessible_label: t('two_factor_authentication.piv_cac.manage_accessible_label'),
},
role: 'list-item',
) %>
<% end %>
</div>

Expand All @@ -25,6 +25,7 @@
link_to(setup_piv_cac_url, **tag_options, &block)
end,
icon: :add,
class: 'usa-button usa-button--outline margin-top-2',
outline: true,
class: 'margin-top-2',
).with_content(t('account.index.piv_cac_add')) %>
<% end %>
Loading