diff --git a/config/locales/en.yml b/config/locales/en.yml index eb94de5c..74b2c095 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -32,9 +32,11 @@ en: description: | The name of the BigQuery airbyte dataset we're writing to. default: ENV['BIGQUERY_AIRBYTE_DATASET'] - bigquery_hidden_policy_tag: - description: The name of the BigQuery hidden policy tag applied to PII in raw airbyte tables. - default: ENV['BIGQUERY_HIDDEN_POLICY_TAG'] + bigquery_policy_tags: + description: | + The BigQuery policy tags JSON with multiple policy tags to be + applied to hidden and sensitive data in raw airbyte tables. + default: ENV['BIGQUERY_POLICY_TAGS'] bigquery_retries: description: | Passed directly to the retries: option on the BigQuery client diff --git a/lib/dfe/analytics.rb b/lib/dfe/analytics.rb index 88ee03aa..ee7260ac 100644 --- a/lib/dfe/analytics.rb +++ b/lib/dfe/analytics.rb @@ -165,7 +165,10 @@ def self.extract_model_attributes(model, attributes = nil) table_name = model.class.table_name.to_sym exportable_attrs = (allowlist[table_name].presence || []).map(&:to_sym) - hidden_pii_attrs = (hidden_pii[table_name].presence || []).map(&:to_sym) + hidden_pii_attrs = (hidden_pii[table_name].presence || []).map do |field| + field = field.keys.first.to_sym if field.is_a?(Hash) + field.to_sym + end exportable_hidden_pii_attrs = exportable_attrs & hidden_pii_attrs # Exclude hidden pii attributes from allowed_attributes diff --git a/lib/dfe/analytics/big_query_api.rb b/lib/dfe/analytics/big_query_api.rb index 6a2239c1..ec48b907 100644 --- a/lib/dfe/analytics/big_query_api.rb +++ b/lib/dfe/analytics/big_query_api.rb @@ -9,20 +9,9 @@ class BigQueryApi RETRY_INITIAL_BASE_INTERVAL = 15 RETRY_MAX_INTERVAL = 60 RETRY_INTERVAL_MULTIPLIER = 2 - BIGQUERY_MANDATORY_CONFIG = %i[ - bigquery_project_id - bigquery_table_name - bigquery_dataset - azure_client_id - azure_token_path - azure_scope - gcp_scope - google_cloud_credentials - ].freeze - BIGQUERY_AIRBYTE_MANDATORY_CONFIG = %i[ - bigquery_airbyte_dataset - bigquery_hidden_policy_tag - ].freeze + BIGQUERY_MANDATORY_CONFIG = + %i[bigquery_project_id bigquery_table_name bigquery_dataset azure_client_id azure_token_path azure_scope gcp_scope google_cloud_credentials].freeze + BIGQUERY_AIRBYTE_MANDATORY_CONFIG = %i[bigquery_airbyte_dataset].freeze def self.client @client ||= begin @@ -81,8 +70,8 @@ def self.error_message_for(response) "DfE::Analytics BigQuery API insert error for #{response.insert_errors.length} event(s):\n#{message}" end - def self.apply_policy_tags(dataset, tables, policy_tag) - tables.each do |table_name, column_names| + def self.apply_policy_tags(dataset, tables, policy_tags) + tables.each do |table_name, columns| begin table = client.get_table( DfE::Analytics.config.bigquery_project_id, @@ -96,7 +85,17 @@ def self.apply_policy_tags(dataset, tables, policy_tag) end updated_fields = table.schema.fields.map do |field| - field.policy_tags = Google::Apis::BigqueryV2::TableFieldSchema::PolicyTags.new(names: [policy_tag]) if column_names.include?(field.name) + column = columns.find do |column_config| + column_name = column_config.is_a?(Hash) ? column_config.keys.first : column_config + column_name.to_s == field.name + end + + if column + policy_tag_key = column.is_a?(Hash) ? column.values.first.to_sym : :hidden + + field.policy_tags = Google::Apis::BigqueryV2::TableFieldSchema::PolicyTags.new(names: [policy_tags.symbolize_keys.fetch(policy_tag_key)]) + end + field end diff --git a/lib/dfe/analytics/config.rb b/lib/dfe/analytics/config.rb index 53a343b7..b1f5fa4c 100644 --- a/lib/dfe/analytics/config.rb +++ b/lib/dfe/analytics/config.rb @@ -16,6 +16,7 @@ module Config bigquery_airbyte_dataset bigquery_api_json_key bigquery_hidden_policy_tag + bigquery_policy_tags bigquery_retries bigquery_timeout enable_analytics @@ -56,6 +57,9 @@ def self.configure(config) config.bigquery_airbyte_dataset ||= ENV.fetch('BIGQUERY_AIRBYTE_DATASET', nil) config.bigquery_api_json_key ||= ENV.fetch('BIGQUERY_API_JSON_KEY', nil) config.bigquery_hidden_policy_tag ||= ENV.fetch('BIGQUERY_HIDDEN_POLICY_TAG', nil) + config.bigquery_policy_tags ||= + JSON.parse(ENV.fetch('BIGQUERY_POLICY_TAGS', '{}')).transform_keys(&:underscore).symbolize_keys + config.bigquery_policy_tags ||= { hidden: config.bigquery_hidden_policy_tag } if config.bigquery_hidden_policy_tag.present? config.bigquery_retries ||= 3 config.bigquery_timeout ||= 120 config.environment ||= ENV.fetch('RAILS_ENV', 'development') diff --git a/lib/dfe/analytics/jobs/big_query_apply_policy_tags_job.rb b/lib/dfe/analytics/jobs/big_query_apply_policy_tags_job.rb index 4a134b7d..e13b32e0 100644 --- a/lib/dfe/analytics/jobs/big_query_apply_policy_tags_job.rb +++ b/lib/dfe/analytics/jobs/big_query_apply_policy_tags_job.rb @@ -3,25 +3,25 @@ module DfE module Analytics module Jobs - # Applies BigQuery hidden policy tags to PII fields in the airbyte tables + # Applies BigQuery policy tags to hidden and sensitive fields in the airbyte tables class BigQueryApplyPolicyTagsJob < AnalyticsJob - def self.do(dataset:, tables:, policy_tag:, delay_in_minutes: 0) + def self.do(dataset:, tables:, policy_tags:, delay_in_minutes: 0) if delay_in_minutes.zero? - perform_later(dataset, tables, policy_tag) + perform_later(dataset, tables, policy_tags) else time_to_run = Time.zone.now + delay_in_minutes.minutes - set(wait_until: time_to_run).perform_later(dataset, tables, policy_tag) + set(wait_until: time_to_run).perform_later(dataset, tables, policy_tags) end end - def perform(dataset, tables, policy_tag) + def perform(dataset, tables, policy_tags) unless DfE::Analytics.airbyte_enabled? Rails.logger.warn('DfE::Analytics::BigQueryApplyPolicyTags.perform called but airbyte is disabled. Please check DfE::Analytics.airbyte_enabled? before applying policy tags in BigQuery') return end - DfE::Analytics::BigQueryApi.apply_policy_tags(dataset, tables, policy_tag) + DfE::Analytics::BigQueryApi.apply_policy_tags(dataset, tables, policy_tags) end end end diff --git a/lib/dfe/analytics/services.rb b/lib/dfe/analytics/services.rb index 9c6f7e0f..b1d8bf8e 100644 --- a/lib/dfe/analytics/services.rb +++ b/lib/dfe/analytics/services.rb @@ -3,7 +3,6 @@ require 'dfe/analytics/shared/service_pattern' require 'dfe/analytics/services/apply_airbyte_final_tables_policy_tags' -require 'dfe/analytics/services/apply_airbyte_internal_tables_policy_tags' require 'dfe/analytics/services/checksum_calculator' require 'dfe/analytics/services/entity_table_checks' require 'dfe/analytics/services/generic_checksum_calculator' diff --git a/lib/dfe/analytics/services/apply_airbyte_final_tables_policy_tags.rb b/lib/dfe/analytics/services/apply_airbyte_final_tables_policy_tags.rb index e7804b0c..e2552c4f 100644 --- a/lib/dfe/analytics/services/apply_airbyte_final_tables_policy_tags.rb +++ b/lib/dfe/analytics/services/apply_airbyte_final_tables_policy_tags.rb @@ -3,7 +3,7 @@ module DfE module Analytics module Services - # Apply hidden policy tags to the final airbyte table columns in the hidden pii config + # Apply policy tags to the final airbyte table columns configured as hidden or sensitive class ApplyAirbyteFinalTablesPolicyTags include ServicePattern @@ -16,7 +16,7 @@ def call delay_in_minutes: @delay_in_minutes, dataset: DfE::Analytics.config.bigquery_airbyte_dataset, tables: DfE::Analytics.hidden_pii, - policy_tag: DfE::Analytics.config.bigquery_hidden_policy_tag + policy_tags: DfE::Analytics.config.bigquery_policy_tags ) end end diff --git a/lib/dfe/analytics/services/apply_airbyte_internal_tables_policy_tags.rb b/lib/dfe/analytics/services/apply_airbyte_internal_tables_policy_tags.rb deleted file mode 100644 index bb72eea6..00000000 --- a/lib/dfe/analytics/services/apply_airbyte_internal_tables_policy_tags.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -module DfE - module Analytics - module Services - # Apply hidden policy tags to the internal airbyte table columns containing any data - class ApplyAirbyteInternalTablesPolicyTags - include ServicePattern - - AIRBYTE_INTERNAL_TABLE_DATA_COLUMN = '_airbyte_data' - - def initialize(delay_in_minutes: 0) - @delay_in_minutes = delay_in_minutes - end - - def call - DfE::Analytics::Jobs::BigQueryApplyPolicyTagsJob.do( - delay_in_minutes: @delay_in_minutes, - dataset: DfE::Analytics.config.airbyte_internal_dataset, - tables: internal_airbyte_tables, - policy_tag: DfE::Analytics.config.bigquery_hidden_policy_tag - ) - end - - private - - def internal_airbyte_tables - DfE::Analytics.allowlist.keys.each_with_object({}) do |table, mem| - airbyte_internal_table_name = "#{DfE::Analytics.config.airbyte_internal_dataset}_raw__stream_#{table}" - - mem[airbyte_internal_table_name] = AIRBYTE_INTERNAL_TABLE_DATA_COLUMN - end - end - end - end - end -end diff --git a/lib/dfe/analytics/tasks/big_query_apply_policy_tags.rake b/lib/dfe/analytics/tasks/big_query_apply_policy_tags.rake index cae683af..f91060f3 100644 --- a/lib/dfe/analytics/tasks/big_query_apply_policy_tags.rake +++ b/lib/dfe/analytics/tasks/big_query_apply_policy_tags.rake @@ -6,10 +6,6 @@ namespace :dfe do puts "Calling DfE::Analytics::Services::ApplyAirbyteFinalTablesPolicyTags.call(delay_in_minutes: #{delay})" DfE::Analytics::Services::ApplyAirbyteFinalTablesPolicyTags.call(delay_in_minutes: delay) - - # Internal tables no longer created by airbyte - remove following code following testing - # puts "Calling DfE::Analytics::Services::ApplyAirbyteInternalTablesPolicyTags.call(delay_in_minutes: #{delay})" - # DfE::Analytics::Services::ApplyAirbyteInternalTablesPolicyTags.call(delay_in_minutes: delay) end end end diff --git a/spec/dfe/analytics/big_query_api_spec.rb b/spec/dfe/analytics/big_query_api_spec.rb index bd6d9d93..f512ae08 100644 --- a/spec/dfe/analytics/big_query_api_spec.rb +++ b/spec/dfe/analytics/big_query_api_spec.rb @@ -87,8 +87,22 @@ describe '.apply_policy_tags' do let(:dataset) { 'airbyte_dataset' } - let(:tables) { { users: %w[email name] } } - let(:policy_tag) { 'projects/my-project/locations/eu/taxonomies/123/policyTags/abc' } + + let(:hidden_policy_tag) do + 'projects/my-project/locations/eu/taxonomies/123/policyTags/hidden' + end + + let(:sensitive_hidden_policy_tag) do + 'projects/my-project/locations/eu/taxonomies/123/policyTags/sensitive-hidden' + end + + let(:policy_tags) do + { + hidden: hidden_policy_tag, + sensitive_hidden: sensitive_hidden_policy_tag + } + end + let(:fields) do [ instance_double(Google::Apis::BigqueryV2::TableFieldSchema, name: 'email'), @@ -96,46 +110,109 @@ instance_double(Google::Apis::BigqueryV2::TableFieldSchema, name: 'created_at') ] end - let(:schema) { instance_double(Google::Apis::BigqueryV2::TableSchema, fields: fields) } - let(:table) { instance_double(Google::Apis::BigqueryV2::Table, schema: schema) } + + let(:schema) do + instance_double( + Google::Apis::BigqueryV2::TableSchema, + fields: fields + ) + end + + let(:table) do + instance_double( + Google::Apis::BigqueryV2::Table, + schema: schema + ) + end before do - allow(fields[0]).to receive(:policy_tags=) - allow(fields[1]).to receive(:policy_tags=) - allow(fields[2]).to receive(:policy_tags=) + fields.each do |field| + allow(field).to receive(:policy_tags=) + end allow(client).to receive(:get_table).and_return(table) allow(client).to receive(:patch_table) end - it 'sets policy tags on specified columns only' do - with_analytics_config(test_dummy_config) do - described_class.apply_policy_tags(dataset, tables, policy_tag) + context 'when no policy tag is specified for the column' do + let(:tables) do + { + users: %w[email name] + } + end + + it 'applies the hidden policy tag by default' do + with_analytics_config(test_dummy_config) do + described_class.apply_policy_tags(dataset, tables, policy_tags) - expect(fields[0]).to have_received(:policy_tags=) - expect(fields[1]).to have_received(:policy_tags=) - expect(fields[2]).not_to have_received(:policy_tags=) + expect(fields[0]).to have_received(:policy_tags=).with( + an_object_having_attributes(names: [hidden_policy_tag]) + ) + + expect(fields[1]).to have_received(:policy_tags=).with( + an_object_having_attributes(names: [hidden_policy_tag]) + ) + + expect(fields[2]).not_to have_received(:policy_tags=) + end + end + end + + context 'when a policy tag is specified for a column' do + let(:tables) do + { + users: [ + { 'email' => 'sensitive_hidden' }, + 'name' + ] + } + end + + it 'applies the specified policy tag to that column' do + with_analytics_config(test_dummy_config) do + described_class.apply_policy_tags(dataset, tables, policy_tags) + + expect(fields[0]).to have_received(:policy_tags=).with( + an_object_having_attributes(names: [sensitive_hidden_policy_tag]) + ) + + expect(fields[1]).to have_received(:policy_tags=).with( + an_object_having_attributes(names: [hidden_policy_tag]) + ) + + expect(fields[2]).not_to have_received(:policy_tags=) + end end end it 'raises and logs error if get_table fails' do + tables = { users: %w[email name] } + with_analytics_config(test_dummy_config) do - allow(client).to receive(:get_table).and_raise(Google::Apis::ClientError.new('not found')) + allow(client) + .to receive(:get_table) + .and_raise(Google::Apis::ClientError.new('not found')) expect(Rails.logger).to receive(:error).with(/Failed to retrieve table/) + expect do - described_class.apply_policy_tags(dataset, tables, policy_tag) + described_class.apply_policy_tags(dataset, tables, policy_tags) end.to raise_error(DfE::Analytics::BigQueryApi::PolicyTagError) end end it 'raises and logs error if patch_table fails' do + tables = { users: %w[email name] } + with_analytics_config(test_dummy_config) do - allow(client).to receive(:patch_table).and_raise(Google::Apis::ClientError.new('invalid')) + allow(client) + .to receive(:patch_table) + .and_raise(Google::Apis::ClientError.new('invalid')) expect(Rails.logger).to receive(:error).with(/Failed to update table/) + expect do - described_class.apply_policy_tags(dataset, tables, policy_tag) + described_class.apply_policy_tags(dataset, tables, policy_tags) end.to raise_error(DfE::Analytics::BigQueryApi::PolicyTagError) end end diff --git a/spec/dfe/analytics/config_spec.rb b/spec/dfe/analytics/config_spec.rb index fac68020..1250e6fc 100644 --- a/spec/dfe/analytics/config_spec.rb +++ b/spec/dfe/analytics/config_spec.rb @@ -41,6 +41,7 @@ allow(ENV).to receive(:fetch).with('BIGQUERY_AIRBYTE_DATASET', nil).and_return('my_airbyte_dataset') allow(ENV).to receive(:fetch).with('BIGQUERY_API_JSON_KEY', nil).and_return(nil) allow(ENV).to receive(:fetch).with('BIGQUERY_HIDDEN_POLICY_TAG', nil).and_return('my_policy_tag') + allow(ENV).to receive(:fetch).with('BIGQUERY_POLICY_TAGS', '{}').and_return('{"key":"val"}') allow(ENV).to receive(:fetch).with('BIGQUERY_MAINTENANCE_WINDOW', nil).and_return(nil) allow(ENV).to receive(:fetch).with('RAILS_ENV', 'development').and_return('test') allow(ENV).to receive(:fetch).with('AIRBYTE_INTERNAL_DATASET', 'airbyte_internal').and_return('my_airbyte_internal_dataset') diff --git a/spec/dfe/analytics/jobs/big_query_apply_policy_tag_job_spec.rb b/spec/dfe/analytics/jobs/big_query_apply_policy_tag_job_spec.rb index a8e16c81..c4f9ad47 100644 --- a/spec/dfe/analytics/jobs/big_query_apply_policy_tag_job_spec.rb +++ b/spec/dfe/analytics/jobs/big_query_apply_policy_tag_job_spec.rb @@ -4,12 +4,16 @@ describe '.do' do let(:dataset) { 'airbyte_dataset' } let(:tables) { { users: %w[email name] } } - let(:policy_tag) { 'projects/my-project/locations/eu/taxonomies/123/policyTags/abc' } + let(:policy_tags) do + { + hidden: 'projects/my-project/locations/eu/taxonomies/123/policyTags/abc' + } + end context 'when delay_in_minutes is 0' do it 'enqueues the job immediately' do - expect(described_class).to receive(:perform_later).with(dataset, tables, policy_tag) - described_class.do(delay_in_minutes: 0, dataset: dataset, tables: tables, policy_tag: policy_tag) + expect(described_class).to receive(:perform_later).with(dataset, tables, policy_tags) + described_class.do(delay_in_minutes: 0, dataset: dataset, tables: tables, policy_tags: policy_tags) end end @@ -23,9 +27,9 @@ .and_return(described_class) expect(described_class) .to receive(:perform_later) - .with(dataset, tables, policy_tag) + .with(dataset, tables, policy_tags) - described_class.do(delay_in_minutes: 10, dataset: dataset, tables: tables, policy_tag: policy_tag) + described_class.do(delay_in_minutes: 10, dataset: dataset, tables: tables, policy_tags: policy_tags) end end end @@ -36,7 +40,11 @@ let(:dataset) { 'airbyte_dataset' } let(:tables) { { users: %w[email name] } } - let(:policy_tag) { 'projects/my-project/locations/eu/taxonomies/123/policyTags/abc' } + let(:policy_tags) do + { + hidden: 'projects/my-project/locations/eu/taxonomies/123/policyTags/abc' + } + end context 'when airbyte is disabled' do before do @@ -47,7 +55,7 @@ expect(Rails.logger).to receive(:warn).with(/airbyte is disabled/) expect(DfE::Analytics::BigQueryApi).not_to receive(:apply_policy_tags) - job.perform(dataset, tables, policy_tag) + job.perform(dataset, tables, policy_tags) end end @@ -56,12 +64,12 @@ allow(DfE::Analytics).to receive(:airbyte_enabled?).and_return(true) end - it 'calls apply_policy_tags with dataset, tables, and policy_tag' do + it 'calls apply_policy_tags with dataset, tables, and policy_tags' do expect(DfE::Analytics::BigQueryApi) .to receive(:apply_policy_tags) - .with(dataset, tables, policy_tag) + .with(dataset, tables, policy_tags) - job.perform(dataset, tables, policy_tag) + job.perform(dataset, tables, policy_tags) end end end diff --git a/spec/dfe/analytics/services/apply_airbyte_final_tables_policy_tags_spec.rb b/spec/dfe/analytics/services/apply_airbyte_final_tables_policy_tags_spec.rb index 4aa98476..181eed93 100644 --- a/spec/dfe/analytics/services/apply_airbyte_final_tables_policy_tags_spec.rb +++ b/spec/dfe/analytics/services/apply_airbyte_final_tables_policy_tags_spec.rb @@ -3,11 +3,17 @@ RSpec.describe DfE::Analytics::Services::ApplyAirbyteFinalTablesPolicyTags do let(:delay_in_minutes) { 10 } + let(:policy_tags) do + { + hidden: 'projects/my-project/locations/eu/taxonomies/123/policyTags/abc' + } + end + let(:config_double) do instance_double( 'DfE::Analytics.config', bigquery_airbyte_dataset: 'airbyte_dataset', - bigquery_hidden_policy_tag: 'projects/my-project/locations/eu/taxonomies/123/policyTags/abc' + bigquery_policy_tags: policy_tags ) end @@ -29,7 +35,7 @@ delay_in_minutes: delay_in_minutes, dataset: 'airbyte_dataset', tables: hidden_pii, - policy_tag: 'projects/my-project/locations/eu/taxonomies/123/policyTags/abc' + policy_tags: policy_tags ) end @@ -43,7 +49,7 @@ delay_in_minutes: 0, dataset: 'airbyte_dataset', tables: hidden_pii, - policy_tag: 'projects/my-project/locations/eu/taxonomies/123/policyTags/abc' + policy_tags: policy_tags ) end end diff --git a/spec/dfe/analytics/services/apply_airbyte_internal_tables_policy_tags_spec.rb b/spec/dfe/analytics/services/apply_airbyte_internal_tables_policy_tags_spec.rb deleted file mode 100644 index c6d784a9..00000000 --- a/spec/dfe/analytics/services/apply_airbyte_internal_tables_policy_tags_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe DfE::Analytics::Services::ApplyAirbyteInternalTablesPolicyTags do - let(:delay_in_minutes) { 5 } - - let(:config_double) do - instance_double( - 'DfE::Analytics.config', - airbyte_internal_dataset: 'internal_dataset', - bigquery_hidden_policy_tag: 'projects/my-project/locations/eu/taxonomies/123/policyTags/xyz' - ) - end - - let(:allowlist) do - { - academic_cycles: %w[id start_date], - teachers: %w[id name email] - } - end - - let(:expected_internal_tables) do - { - 'internal_dataset_raw__stream_academic_cycles' => '_airbyte_data', - 'internal_dataset_raw__stream_teachers' => '_airbyte_data' - } - end - - before do - allow(DfE::Analytics).to receive(:config).and_return(config_double) - allow(DfE::Analytics).to receive(:allowlist).and_return(allowlist) - allow(DfE::Analytics::Jobs::BigQueryApplyPolicyTagsJob).to receive(:do) - end - - describe '.call' do - it 'calls BigQueryApplyPolicyTagsJob.do with internal dataset and correct table structure' do - described_class.call(delay_in_minutes: delay_in_minutes) - - expect(DfE::Analytics::Jobs::BigQueryApplyPolicyTagsJob).to have_received(:do).with( - delay_in_minutes: delay_in_minutes, - dataset: 'internal_dataset', - tables: expected_internal_tables, - policy_tag: 'projects/my-project/locations/eu/taxonomies/123/policyTags/xyz' - ) - end - - context 'when delay_in_minutes is 0' do - let(:delay_in_minutes) { 0 } - - it 'calls the job with delay 0' do - described_class.call(delay_in_minutes: delay_in_minutes) - - expect(DfE::Analytics::Jobs::BigQueryApplyPolicyTagsJob).to have_received(:do).with( - delay_in_minutes: 0, - dataset: 'internal_dataset', - tables: expected_internal_tables, - policy_tag: 'projects/my-project/locations/eu/taxonomies/123/policyTags/xyz' - ) - end - end - end -end diff --git a/spec/dfe/analytics_spec.rb b/spec/dfe/analytics_spec.rb index 087947a0..7a11dd73 100644 --- a/spec/dfe/analytics_spec.rb +++ b/spec/dfe/analytics_spec.rb @@ -299,6 +299,20 @@ expect(result[:hidden_data]).to be_nil.or be_empty expect { DfE::Analytics.extract_model_attributes(candidate) }.not_to raise_error end + + context 'when custom policy tags are provided' do + before do + allow(DfE::Analytics).to receive(:hidden_pii).and_return({ + Candidate.table_name.to_sym => ['hidden_data', { 'age' => 'policy_tag' }] + }) + end + + it 'correctly handles field with custom policy tag' do + result = described_class.extract_model_attributes(candidate) + + expect(result[:hidden_data]['age']).to eq(50) + end + end end describe '.parse_maintenance_window' do