Repository navigation
Conversation
Upstreamed from application-service (IX-3471), where IBANs had to be encrypted in three shapes: a plain string column, a raw partner payload in a jsonb column and a single key inside another jsonb column. Rails' encrypts covers the first shape only, so the service grew a small DSL on top of Active Record Encryption for the other two. encrypts_json_entirely wraps a whole JSON column in encrypts while still reading it back as a HashWithIndifferentAccess. encrypts_json_attrs encrypts only the leaves at the given JSONPath expressions, so the remaining keys stay queryable, and where_encrypted_json finds rows by the deterministic ciphertext of such a leaf through jsonb_path_query. Both build on NxtSupport::IndifferentJsonType, which also decodes the double encoded rows that indifferently_accessible_json_attrs writes to json columns, so services can switch a column over without a data migration. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The shorthand that prefixed a bare key path with "$." was a second syntax to document and test, and it decided between the two by looking at the first character, so a key that starts with "$" would have been read as a path expression. One syntax with no special case is worth the two extra characters per call site. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
nsommer
commented
Sep 16, 2026
nsommer
left a comment
Member
Author
There was a problem hiding this comment.
Inline comments from a standards pass; nothing blocking.
Name the two raise specs after the reason, cover the JSON::ParserError branch in IndifferentJsonType, use a per-attribute support_unencrypted_data instead of toggling the global config in a spec, and mention the jsonpath dependency in the changelog. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Specs that prove a column is encrypted keep repeating the same steps: a hand-written SELECT for the raw value, a database specific JSON function to dig out a leaf, a call to the encryptor and a check that the plaintext is absent. raw_column_value, be_encrypted and be_encrypted_at fold that into assertions that read the same on sqlite and PostgreSQL. The two matchers deliberately do not overlap. A wholly encrypted column is one envelope and only be_encrypted fits it. A partially encrypted column is still a JSON document, so be_encrypted fails on it and be_encrypted_at speaks about its leaves. A path that matches nothing fails in both directions so a typo cannot pass silently. The file is opt in through require 'nxt_support/rspec' and is not loaded by the gem itself, so RSpec stays a development dependency. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Member
Author
nsommer
requested review from
AkihikoITOH,
a-berg-gs,
bigzoo,
chiibis,
mitnal,
mridulmohan1 and
turhaug
September 16, 2026 18:24
a-berg-gs
reviewed
Sep 29, 2026
|
|
||
| ##### Migrating from `indifferently_accessible_json_attrs` | ||
|
|
||
| `indifferently_accessible_json_attrs` on a `json` or `jsonb` column stores the value double encoded, as a JSON string literal that contains JSON. `NxtSupport::IndifferentJsonType` (and with it both encryption variants) decodes such rows transparently, so a column can be switched over without a data migration. Re-saving the records encrypts them; with `config.active_record.encryption.support_unencrypted_data = true` unencrypted rows stay readable in the meantime when using `encrypts_json_entirely`. |
There was a problem hiding this comment.
Calling save! on an unchanged record would not be enough, right? We would need to run something like record.update_columns(data: record.data).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

References
Upstreamed from application-service IX-3471 (application-service#3480)
Problem
Rails'
encryptscovers plain columns only. Encrypting a whole JSON column, or single fields inside one, needs a small DSL on top of Active Record Encryption that every service storing personal data in JSON columns can use.Solution
encrypts_json_entirely:encryptson top ofNxtSupport::IndifferentJsonType.encrypts_json_attrs: encrypts only the leaves at the given JSONPath expressions, the rest stays queryable.where_encrypted_jsonfinds rows by deterministic ciphertext viajsonb_path_query(PostgreSQL only).NxtSupport::IndifferentJsonTypealso reads the double encoded rowsindifferently_accessible_json_attrswrites to json columns, so a column can be switched without a data migration.NxtSupport::RSpec::Encryption(opt in viarequire 'nxt_support/rspec'):raw_column_value,be_encrypted,be_encrypted_at('$.iban').jsonpath. Paths must be full JSONPath expressions; the application-service shorthand (ibanfor$.iban) is dropped.See the README for details.
🤖 Generated with Claude Code