Skip to content

Commit

Permalink
mask all ssn fields (#4279)
Browse files Browse the repository at this point in the history
* complete initial pass on new ssn field reqs

* add slight modification to new family policy method

* add event listener for accessibility

* complete spec for new endpoints

* remove unnecessary changes from enroll system config file

* adds ssn masks to my household section

* bearer failure

* bearer failure

* bearer failure

* fix failing spec in family policy

* remove console.logs

* make env var true on failing spec

* bearer failure

* readd bearer fix

* remove excessive line breaks

* remove unnecessary helper_method in family_members controller

* complete first pass at applicant ssn field masking

* remove ssn display in all admin tables

* making brief switch to update broker permissions

* complete initial pass on new ssn field reqs

* add event listener for accessibility

* remove unnecessary changes from enroll system config file

* adds ssn masks to my household section

* bearer failure

* bearer failure

* bearer failure

* bearer failure

* readd bearer fix

* remove excessive line breaks

* remove unnecessary helper_method in family_members controller

* complete first pass at applicant ssn field masking

* add addtl permissions and update specs

* remove unnecessary line break

* finalize permission details

* complete first iteration of ssn fields

* remove changes for testing drop enrollment members

* remove comment

* reformat explanatory comment

* remove unused attrs from ssn_form_presenter

* update dependent form input

* remove unused id from edit enrollment partial

* plan shopping options page fixes (#4274)

plan shopping options fixes

* dont have secondary modal for dependent table (#4273)

* don't open secondary modal when in comparison modal

* forgot to commit a file

* rubocop

* fix spec by undoing rubocop suggestions

* enrollment submitted logic fix

* more aptc logic

* aptc calculator adjust amount applied (#4286)

adjust the max amount of aptc to account for if the aptc is higher than the premium

* aptc calc results should have round values (#4288)

* fix rounding issue

* fix rounding

* use default cancel translation sting (#4290)

* fix issue with failing cucumber

* remove console log in coffee file

* remove family_helper changes

* remove changes with unemployment form

* add translations and update w/feedback from comments

* remove extra row column in table

* add admin css file

* small changes for rubocop

* reversed logic on column list

---------

Co-authored-by: Kristin Merbach <[email protected]>
Co-authored-by: kristinmerbach <[email protected]>
  • Loading branch information
3 people authored and bbodine1 committed Aug 29, 2024
1 parent 2510da9 commit ea789d8
Show file tree
Hide file tree
Showing 37 changed files with 793 additions and 111 deletions.
11 changes: 10 additions & 1 deletion app/controllers/insured/consumer_roles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def update
mec_check(@person.hbx_id) if EnrollRegistry.feature_enabled?(:mec_check) && @person.send(:mec_check_eligible?)
@shop_coverage_result = EnrollRegistry.feature_enabled?(:shop_coverage_check) ? (check_shop_coverage.success? && check_shop_coverage.success.present?) : nil
@consumer_role.skip_consumer_role_callbacks = true
valid_params = {"skip_person_updated_event_callback" => true, "skip_lawful_presence_determination_callbacks" => true}.merge(params.require(:person).permit(*person_parameters_list))
valid_params = {"skip_person_updated_event_callback" => true, "skip_lawful_presence_determination_callbacks" => true}.merge(params.require(:person).permit(*existing_personal_parameters_list))

if update_vlp_documents(@consumer_role, 'person') && @consumer_role.update_by_person(valid_params)
@consumer_role.update_attribute(:is_applying_coverage, params[:person][:is_applying_coverage]) unless params[:person][:is_applying_coverage].nil?
Expand Down Expand Up @@ -473,6 +473,15 @@ def sanitize_contact_method
params.dig("person", "consumer_role_attributes").merge!("contact_method" => ConsumerRole::CONTACT_METHOD_MAPPING[contact_method])
end

def existing_personal_parameters_list
if EnrollRegistry.feature_enabled?(:mask_ssn_ui_fields)
# NOTE: with this update, the only place ssn/dob/no_ssn can be updated is the edit ssn dob feature, which is restricted to admin
person_parameters_list - [:dob, :ssn, :no_ssn]
else
person_parameters_list
end
end

def person_parameters_list
[
{ :addresses_attributes => [:kind, :address_1, :address_2, :city, :state, :zip, :county, :id, :_destroy] },
Expand Down
22 changes: 20 additions & 2 deletions app/controllers/insured/family_members_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,24 @@ def show
end
end

# NOTE: ensure that current user is either
# 1.) hbx admin, or
# 2.) the current user is the primary for the family AND the subject is a member of that family
def show_ssn
@family = Family.find(params[:family_id]) if params[:family_id]
authorize @family, :can_show_ssn?
@person = Person.find(params[:id]) unless @person.id == params[:id]

if @family.person_is_family_member?(@person)
payload = number_to_ssn(@person.ssn)
render json: { payload: payload, status: 200 }
else
render json: { message: "Unauthorized" }, status: 401
end
rescue Pundit::NotAuthorizedError, Mongoid::Errors::DocumentNotFound
render json: { message: "Unauthorized" }, status: 401
end

def edit
authorize @family, :edit?

Expand Down Expand Up @@ -301,8 +319,8 @@ def set_type_role_and_family
end
@family = @person.primary_family
when 'consumer'
@consumer_role = @person.consumer_role
@family = @consumer_role.person.primary_family
@consumer_role = @person&.consumer_role
@family = @consumer_role&.person&.primary_family
end

@family = Family.find(params[:family_id]) if params[:family_id]
Expand Down
2 changes: 1 addition & 1 deletion app/domain/operations/update_dob_ssn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def update_person(person, params, current_user, ssn_require)
if params[:person][:ssn].blank?
if ssn_require
dont_update_ssn = true
else
elsif params[:person][:ssn] == '' # meaning they have deliberately been set to blank vs. not updating them
person.unset(:encrypted_ssn)
end
else
Expand Down
13 changes: 12 additions & 1 deletion app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,18 @@ def number_to_ssn(number)
def number_to_obscured_ssn(number)
return unless number
number_to_ssn(number)
number.to_s.gsub!(/\w{3}-\w{2}/, '***-**')
if EnrollRegistry.feature_enabled?(:mask_ssn_ui_fields)
'●●●●●●●●●'
else
number.to_s.gsub!(/\w{3}-\w{2}/, '***-**')
end
end

def organize_ssn_params(form_object, family_member_id = nil)
return unless form_object.ssn

presenter = ::Presenters::SsnFormPresenter.new(form_object, family_member_id)
presenter.sanitize_ssn_params
end

# Formats a number into a nine-digit US Federal Entity Identification Number string (nn-nnnnnnn)
Expand Down
91 changes: 91 additions & 0 deletions app/javascript/controllers/person_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Controller } from "stimulus"
import axios from 'axios'

export default class extends Controller {
connect() {
document.addEventListener("keydown", this.handleKeyDown);
}

disconnect() {
document.removeEventListener("keydown", this.handleKeyDown);
}

handleKeyDown(event) {
event.preventDefault();
if (event.key === 'Enter' || event.key === ' ') {
event.target.click();
}
}

showSsn(event) {
event.stopImmediatePropagation();
let target = event.target;
let personId = target.getAttribute('data-id');
let familyId = target.getAttribute('data-family-id');

if (personId == 'temp') {
this.showSsnInput(personId);
} else {
axios({
method: 'GET',
url: `/insured/family_members/${personId}/show_ssn`,
params: {
family_id: familyId
},
headers: {
'X-CSRF-Token': document.querySelector("meta[name=csrf-token]").content
}
}).then((response) => {
if (response.data.status == 200) {
let payload = response.data.payload;
this.populateHtmlElement(personId, payload);

this.showSsnInput(personId);
} else {
console.log("Unauthorized.");
}
}).catch(() => {
console.log('Error retrieving info');
})
}
}

populateHtmlElement(personId, payload) {
let ssnInputElement = document.querySelector(`.ssn-input-${personId}`);

if (ssnInputElement.tagName === 'Input') {
ssnInputElement.value = payload;
} else {
ssnInputElement.textContent = payload;
}
}

hideSsn(event) {
const target = event.target;
const personId = target.getAttribute('data-id');

document.querySelector(`.ssn-input-${personId}`).classList.add('hidden');
document.querySelector(`.ssn-facade-${personId}`).classList.remove('hidden');
document.querySelector(`.ssn-eye-on-${personId}`).classList.add('hidden');
document.querySelector(`.ssn-eye-off-${personId}`).classList.remove('hidden');

document.querySelector(`.ssn-eye-off-${personId}`).focus();
const ssnInput = document.querySelector(`.ssn-input-${personId}`);
if (ssnInput.getAttribute('data-admin-can-enable') !== null) {
ssnInput.disabled = true;
}
}

showSsnInput(personId) {
document.querySelector(`.ssn-input-${personId}`).classList.remove('hidden');
document.querySelector(`.ssn-facade-${personId}`).classList.add('hidden');
document.querySelector(`.ssn-eye-on-${personId}`).classList.remove('hidden');
document.querySelector(`.ssn-eye-off-${personId}`).classList.add('hidden');

document.querySelector(`.ssn-eye-on-${personId}`).focus();
const ssnInput = document.querySelector(`.ssn-input-${personId}`);
if (ssnInput.getAttribute('data-admin-can-enable') !== null) {
ssnInput.disabled = false;
}
}
}
14 changes: 14 additions & 0 deletions app/javascript/css/admin_ssn.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.ssn-eye-off, .ssn-eye-on {
cursor: pointer;
}

.admin-ssn-block {
display: flex;
align-items: center;
justify-content: flex-start;

.ssn-facade {
// this is a magic number that makes placeholders the same width as the numbers
letter-spacing: 0.055em;
}
}
49 changes: 47 additions & 2 deletions app/javascript/css/forms.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
#personal_info {
input:not([type='checkbox']):not([type='radio']):not(.ssn-input),
select,
.input {
width: 180px;
}

.no-ssn-container {
width: 180px;

label {
font-weight: normal;
}
}
}

#new_user {
#pass_strength,
#pass_match {
Expand Down Expand Up @@ -69,7 +85,8 @@ dt.required:after,

input:not([type='submit']),
select,
.input {
.input,
#ssn-input-container {
box-sizing: border-box;
height: 36px;
padding: 6px 8px;
Expand All @@ -90,9 +107,37 @@ select,
}
}

input.fake-disabled-input:not([type='submit']):disabled {
#ssn-input-container {
width: 180px;
display: flex;
align-items: center;
justify-content: space-between;

.ssn-input {
padding: 0px;
border: unset;
width: 120px;
height: 34px;

&:hover {
border-color: unset;
}
}

img {
cursor: pointer;
}
}

input.fake-disabled-input:not([type='submit']):disabled,
#ssn-input-container.div-disabled {
background: var(--grey-030);
color: var(--default-font-color);

.ssn-input {
background: inherit;
color: inherit;
}
}

input[type='checkbox'],
Expand Down
20 changes: 0 additions & 20 deletions app/javascript/css/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -178,26 +178,6 @@ dl.parent.weight-n {
}
}

.tax_credit_field_container {
height: 44px !important;
}

#personal_info {
input:not([type='checkbox']):not([type='radio']),
select,
.input {
width: 180px;
}

.no-ssn-container {
width: 180px;

label {
font-weight: normal;
}
}
}

.ethnicity-container {
label {
font-weight: 400;
Expand Down
7 changes: 7 additions & 0 deletions app/javascript/packs/effective_table_add_ons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Application } from "stimulus";
import { definitionsFromContext } from "stimulus/webpack-helpers";
import '../css/admin_ssn.scss';

const application = Application.start();
const context = require.context("controllers", false, /person_controller.js/);
application.load(definitionsFromContext(context));
1 change: 0 additions & 1 deletion app/models/effective/datatables/family_data_table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ class FamilyDataTable < Effective::MongoidDatatable
resume_enrollment_exchanges_agents_path(person_id: row.primary_applicant.person.id)
)
}, :filter => false, :sortable => false
table_column :ssn, :label => 'SSN', :proc => proc { |row| truncate(number_to_obscured_ssn(row.primary_applicant.person.ssn)) }, :filter => false, :sortable => false
table_column :dob, :label => 'DOB', :proc => proc { |row| format_date(row.primary_applicant.person.dob)}, :filter => false, :sortable => false
table_column :hbx_id, :label => 'HBX ID', :proc => proc { |row| row.primary_applicant.person.hbx_id }, :filter => false, :sortable => false
table_column :external_app_id, :label => 'External ID', :proc => proc { |row| row.external_app_id }, :filter => false, :sortable => false if EnrollRegistry[:display_external_id_in_family_datatable].enabled?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ class IdentityVerificationDataTable < Effective::MongoidDatatable

datatable do
table_column :name, :label => 'Name', :proc => proc { |row| link_to_with_noopener_noreferrer(h(row.full_name), resume_enrollment_exchanges_agents_path(person_id: row.id)) }, :filter => false, :sortable => false
table_column :ssn, :label => 'SSN', :proc => proc { |row| truncate(number_to_obscured_ssn(row.ssn))}, :filter => false, :sortable => false
unless EnrollRegistry.feature_enabled?(:mask_ssn_ui_fields) # rubocop:disable Style/IfUnlessModifier --> not disabling this rule will break the code or result in more rubocop errors, and will be remedied when feature flag is removed
table_column :ssn, :label => 'SSN', :proc => proc { |row| truncate(number_to_obscured_ssn(row.ssn))}, :filter => false, :sortable => false
end
table_column :dob, :label => 'DOB', :proc => proc { |row| row.dob }, :filter => false, :sortable => false
table_column :hbx_id, :label => 'HBX ID', :proc => proc { |row| row.hbx_id }, :filter => false, :sortable => false
table_column :count, :label => 'Count', :width => '100px', :proc => proc { |row| row.primary_family.active_family_members.size }, :filter => false, :sortable => false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ def load_verification_type_columns
table_column :name, :label => 'Name', :proc => proc { |row|
link_to_with_noopener_noreferrer(h(row.primary_applicant.person.full_name), resume_enrollment_exchanges_agents_path(person_id: row.primary_applicant.person.id))
}, :filter => false, :sortable => true
table_column :ssn, :label => 'SSN', :proc => proc { |row| truncate(number_to_obscured_ssn(row.primary_applicant.person.ssn)) }, :filter => false, :sortable => false
unless EnrollRegistry.feature_disabled?(:mask_ssn_ui_fields) # rubocop:disable Style/IfUnlessModifier --> not disabling this rule will break the code or result in more rubocop errors, and will be remedied when feature flag is removed
table_column :ssn, :label => 'SSN', :proc => proc { |row| truncate(number_to_obscured_ssn(row.primary_applicant.person.ssn)) }, :filter => false, :sortable => false
end
table_column :dob, :label => 'DOB', :proc => proc { |row| format_date(row.primary_applicant.person.dob)}, :filter => false, :sortable => false
table_column :hbx_id, :label => 'HBX ID', :proc => proc { |row| row.primary_applicant.person.hbx_id }, :filter => false, :sortable => false
table_column :count, :label => 'Count', :width => '100px', :proc => proc { |row| row.active_family_members.size }, :filter => false, :sortable => false
Expand All @@ -37,7 +39,9 @@ def load_eligibility_determination_columns
table_column :name, :label => 'Name', :proc => proc { |row|
link_to_with_noopener_noreferrer(eligibility_primary_name(row), resume_enrollment_exchanges_agents_path(person_id: eligibility_primary_family_member(row).person_id))
}, :filter => false, :sortable => true
table_column :ssn, :label => 'SSN', :proc => proc { |row| truncate(number_to_obscured_ssn(eligibility_primary_ssn(row))) }, :filter => false, :sortable => false
unless EnrollRegistry.feature_enabled?(:mask_ssn_ui_fields) # rubocop:disable Style/IfUnlessModifier --> not disabling this rule will break the code or result in more rubocop errors, and will be remedied when feature flag is removed
table_column :ssn, :label => 'SSN', :proc => proc { |row| truncate(number_to_obscured_ssn(eligibility_primary_ssn(row))) }, :filter => false, :sortable => false
end
table_column :dob, :label => 'DOB', :proc => proc { |row| format_date(eligibility_primary_family_member(row).dob)}, :filter => false, :sortable => false
table_column :hbx_id, :label => 'HBX ID', :proc => proc { |row| eligibility_primary_family_member(row).hbx_id }, :filter => false, :sortable => false
table_column :count, :label => 'Count', :width => '100px', :proc => proc { |row| eligibility_enrolled_family_members(row).count }, :filter => false, :sortable => false
Expand Down
18 changes: 10 additions & 8 deletions app/models/effective/datatables/user_account_datatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ class UserAccountDatatable < Effective::MongoidDatatable
include Config::SiteModelConcern

datatable do
table_column :name, :label => 'USERNAME', :proc => Proc.new { |row| row.oim_id }, :filter => false, :sortable => true
table_column :ssn, :label => 'SSN', :proc => Proc.new { |row| truncate(number_to_obscured_ssn(row.person.ssn)) if row.person.present? }, :filter => false, :sortable => false
table_column :dob, :label => 'DOB', :proc => Proc.new { |row| format_date(row.person.dob) if row.person.present?}, :filter => false, :sortable => false
table_column :hbx_id, :label => 'HBX ID', :proc => Proc.new { |row| row.person.hbx_id if row.person.present?}, :filter => false, :sortable => false
table_column :email, :label => 'USER EMAIL', :proc => Proc.new { |row| row.email }, :filter => false, :sortable => false
table_column :status, :label => 'Status', :proc => Proc.new { |row| status(row) }, :filter => false, :sortable => false
table_column :name, :label => 'USERNAME', :proc => proc { |row| row.oim_id }, :filter => false, :sortable => true
unless EnrollRegistry.feature_enabled?(:mask_ssn_ui_fields)
table_column :ssn, :label => 'SSN', :proc => proc { |row| truncate(number_to_obscured_ssn(row.person.ssn)) if row.person.present? }, :filter => false, :sortable => false
end
table_column :dob, :label => 'DOB', :proc => proc { |row| format_date(row.person.dob) if row.person.present?}, :filter => false, :sortable => false
table_column :hbx_id, :label => 'HBX ID', :proc => proc { |row| row.person.hbx_id if row.person.present?}, :filter => false, :sortable => false
table_column :email, :label => 'USER EMAIL', :proc => proc { |row| row.email }, :filter => false, :sortable => false
table_column :status, :label => 'Status', :proc => proc { |row| status(row) }, :filter => false, :sortable => false
table_column :role_type, :label => 'Role Type', :proc => proc { |row| all_roles(row) }, :filter => false, :sortable => false
table_column :permission, :label => 'Permission level', :proc => Proc.new { |row| permission_type(row) }, :filter => false, :sortable => false
table_column :actions, :width => '50px', :proc => Proc.new { |row|
table_column :permission, :label => 'Permission level', :proc => proc { |row| permission_type(row) }, :filter => false, :sortable => false
table_column :actions, :width => '50px', :proc => proc { |row|
dropdown = []
# Link Structure: ['Link Name', link_path(:params), 'link_type'], link_type can be 'ajax', 'static', or 'disabled'
current_user_permission = current_user&.person&.hbx_staff_role&.permission
Expand Down
Loading

0 comments on commit ea789d8

Please sign in to comment.