orfeas_pam_dsl 0.6.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +93 -0
- data/README.md +405 -102
- data/Rakefile +19 -0
- data/lib/pam_dsl/consent.rb +143 -13
- data/lib/pam_dsl/core_ext.rb +67 -0
- data/lib/pam_dsl/enforcement.rb +104 -0
- data/lib/pam_dsl/field.rb +18 -2
- data/lib/pam_dsl/gdpr_compliance.rb +47 -21
- data/lib/pam_dsl/pii_detector.rb +67 -31
- data/lib/pam_dsl/pii_masker.rb +1 -0
- data/lib/pam_dsl/policy.rb +133 -19
- data/lib/pam_dsl/policy_generator.rb +81 -134
- data/lib/pam_dsl/purpose.rb +89 -1
- data/lib/pam_dsl/reporter.rb +77 -41
- data/lib/pam_dsl/tasks/privacy.rake +9 -4
- data/lib/pam_dsl/version.rb +1 -1
- data/lib/pam_dsl.rb +32 -4
- metadata +10 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 491fcb9f14e0d6b8902e18855face0c4f7fac3e59984e3b994f645f8127f3b91
|
|
4
|
+
data.tar.gz: 982d327acf820378e65f1c3c39f18dcb4056efb7aa9c5c465b30c1f8b02fd9e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cff013a4a07623d006613bfcad540c13f237a23e61be2324ef542e744c52b091b113dd6c7a653ee18d7ac0c683873e473063f87db702649fe5c52b3a7615f13d
|
|
7
|
+
data.tar.gz: 9303fb69a0822272ad2b6e9156d8d92f8a142e01d49052b2dd551b24f885c6c849a9c15a0a628846d0838b4c7647bff5b19b7261291648c1f79aba9f67834d76
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,99 @@ All notable changes to PAM DSL will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.0] - 2026-10-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`online_identifier` is a declarable field type** — the detector had a pattern by that name
|
|
11
|
+
but `Field::PII_TYPES` lacked it, so a policy could not declare such a field. It is a
|
|
12
|
+
non-special, non-sensitive type. (The detector still reports login-name columns as
|
|
13
|
+
`identifier`.)
|
|
14
|
+
- **The policy generators keep an existing policy file** — `generate:policy` and
|
|
15
|
+
`generate:from_models` raise `PolicyGenerator::FileExistsError` instead of overwriting
|
|
16
|
+
`config/initializers/pam_dsl_policy.rb`; `FORCE=1` on the rake tasks (`force: true` in Ruby)
|
|
17
|
+
replaces it.
|
|
18
|
+
- **Article 30 register from declarations, not placeholders** — `article_30_report` printed
|
|
19
|
+
"Internal staff" as every purpose's recipients, "No international transfers", a list of
|
|
20
|
+
asserted security measures (TLS, role-based access, PaperTrail), a per-purpose retention that
|
|
21
|
+
was always the default (it passed a field where a model name belongs), and a data-subject
|
|
22
|
+
rights section of "[Configure implementation path]" that Article 30 does not ask for. Purposes
|
|
23
|
+
now declare `data_subjects`, `recipients` and `transfer to:, safeguard:` (or `no_transfers!`),
|
|
24
|
+
and policies `security_measures` (Art. 30(1)(c), (d), (e), (g)); the register prints them, a
|
|
25
|
+
retention schedule from the policy's rules (Art. 30(1)(f)), and a completeness section listing
|
|
26
|
+
each undeclared item with its clause (`Policy#article_30_gaps`). The JSON export carries the
|
|
27
|
+
same, under `not_declared` for the gaps. Durations read "1 year", not "1 years".
|
|
28
|
+
- **One PII dictionary; detection fixed on a foreign codebase** — `PolicyGenerator` kept its own
|
|
29
|
+
exact-name patterns and exclusions, separate from `PIIDetector`'s; on Solidus 4.7 the
|
|
30
|
+
generator found 6 of 26 personal columns and flagged an order's state machine as an address.
|
|
31
|
+
The generator now detects with `PIIDetector` (its `PII_PATTERNS` and `EXCLUDE_PATTERNS` are
|
|
32
|
+
gone), scans each table's real columns (a column the model ignores is reported, and the
|
|
33
|
+
generated policy says the application cannot see or erase it), and counts a bare `name` only
|
|
34
|
+
in a model that holds other personal data (a product's or a payment plan's name is not a
|
|
35
|
+
person's). The detector gained `address1`/`address2`, `zipcode`/`postcode`, `state_name`, any
|
|
36
|
+
`*_ip`, `last_digits`/`last4`, `login`/`username` (an `:identifier`), `bic`/`swift`/account
|
|
37
|
+
numbers, `birth_date`, and a person's account and session tokens and credentials (password
|
|
38
|
+
hashes and salts, remember/confirmation/reset/unlock/guest tokens), which the `encrypted_` and
|
|
39
|
+
`_token` exclusions used to drop; names match name forms and the names of people in a role
|
|
40
|
+
(`debtor_name`, `approver_name`), not every `*_name`. New exclusions: foreign keys (`*_id`,
|
|
41
|
+
except identifiers such as `vat_id`, `customer_national_id`), `*_iso`, `*_message`, `*_reason`,
|
|
42
|
+
`*_notes`; a bare `state` and `rest_income_budget` are no longer personal. On Solidus: 24 of
|
|
43
|
+
26; on the Aegean testbed: 29 fields (27 personal) where the old generator reported 18; on the
|
|
44
|
+
labelled evaluation dataset: recall 99.1% (was 97.4%), precision 98.3% (was 98.2%).
|
|
45
|
+
- **Access recorder** (`PamDsl.access_recorder`, `Enforcement::Access`): every
|
|
46
|
+
`validate_access!` call, whatever its outcome, is passed to the recorder before it returns or
|
|
47
|
+
raises, as an `Access` (policy, purpose, legal basis, fields, subject, outcome `:granted`,
|
|
48
|
+
`:audited` or `:denied`, the violations found, time). The recorder answers `record?(policy)`
|
|
49
|
+
and `call(access)`; an error it raises propagates, so an access it cannot record does not go
|
|
50
|
+
ahead. One per process, set by the host (Lyra's access log); `PamDsl.reset!` leaves it in place.
|
|
51
|
+
- **Enforcement modes** (`PamDsl.enforcement_mode`, `PamDsl::Enforcement`): `:strict`, the
|
|
52
|
+
default and the behaviour so far, blocks an invalid access by raising its typed exception;
|
|
53
|
+
`:audit` lets it through, records **every** violation found (not only the first), logs each
|
|
54
|
+
(`PamDsl.logger`: `Rails.logger` under Rails, standard error otherwise), passes each to the
|
|
55
|
+
`PamDsl.on_violation { |violation| ... }` handlers as an `Enforcement::Violation` (policy,
|
|
56
|
+
purpose, fields, subject, error class, message, time), and makes `validate_access!` return
|
|
57
|
+
`false`. A policy can override the global mode (`enforcement :audit` in its definition). In
|
|
58
|
+
strict mode the first violation raised is the same, in the same order, as before.
|
|
59
|
+
`Policy#access_violations` returns them all without raising. `PamDsl.reset!` also resets the
|
|
60
|
+
mode and the handlers.
|
|
61
|
+
### Changed
|
|
62
|
+
- **Ruby 4.0 or later is required** (`required_ruby_version >= 4.0`; none was declared before).
|
|
63
|
+
### Fixed
|
|
64
|
+
- `PamDsl.reporter` dropped the `output:` option, so reports always went to standard output.
|
|
65
|
+
|
|
66
|
+
## [0.8.0] - 2026-06-15
|
|
67
|
+
|
|
68
|
+
The first release to RubyGems since 0.6.0, and the version the PAM article (Requirements
|
|
69
|
+
Engineering, 2026) cites. It also contains 0.7.0 (below), which was never published.
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
- **ActiveSupport is now optional.** `pam_dsl` previously declared a hard runtime dependency on `activesupport` and unconditionally `require`d it. It now loads ActiveSupport only if present and otherwise falls back to a minimal standard-library polyfill (`lib/pam_dsl/core_ext.rb`), so the gem is self-contained when used standalone (outside Rails). `activesupport` moved from a runtime to a development dependency (still exercised by the test suite). The ActiveSupport surface the gem uses is small: `Numeric` duration helpers (`.years`/`.months`/`.weeks`/`.days`, `.ago`/`.from_now`), `Time.current`, `String#underscore`/`#titleize`, and `Object#present?`.
|
|
73
|
+
- The polyfill durations use a **fixed-length approximation** (30-day month, 365-day year), matching the gem's report tooling. When ActiveSupport is present its calendar-aware durations are used instead, unchanged.
|
|
74
|
+
- `Reporter#format_duration` now matches `PamDsl::DURATION_CLASS` (`ActiveSupport::Duration` when available, else `Numeric`) instead of referencing `ActiveSupport::Duration` directly, so it works under both paths.
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
- `PAM_DSL_FORCE_POLYFILL` environment variable forces the stdlib polyfill even when ActiveSupport is installed, so both code paths can be tested.
|
|
78
|
+
- Rake tasks `test:polyfill` (run the suite forcing the polyfill) and `test:both` (run it under both ActiveSupport and the polyfill). The same 425 tests pass under both paths; `test_duration_formatting` was made tolerant of the 30-day month boundary (the only assertion that differs between the calendar and fixed-length duration models).
|
|
79
|
+
|
|
80
|
+
## [0.7.0] - 2026-06-02 (never published; part of 0.8.0)
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
- `ConsentRecord` class: full four-state lifecycle (P0 Pending, P1 Granted, P2 Expired, P3 Withdrawn) matching the formal consent state diagram (paper §4.2). `expires_at` is stored on the record at grant time so `state` derives `:expired` autonomously without external requirement lookup. `grant!` (P0→P1) and `withdraw!` (P1→P3) enforce valid transitions; P2 and P3 are absorbing.
|
|
84
|
+
- `ConsentStore#request` creates a Pending record (P0); replaces absorbing records to support re-consent flow
|
|
85
|
+
- `ConsentPolicy#request_consent` exposes the Pending flow at the policy level
|
|
86
|
+
- `ConsentStore` class: runtime consent state store (the $CS$ component of the formal runtime context); indexed by `[purpose, subject]` pairs; supports `grant`, `withdraw`, `record_for`, `granted?`
|
|
87
|
+
- `ConsentPolicy#grant_consent(purpose:, subject:, granted_at:)` and `#withdraw_consent(purpose:, subject:)` for populating the consent store
|
|
88
|
+
- `ConsentPolicy#validate!(purpose, subject:)` now performs per-subject consent lookup against the store, implementing `consent_active(u, s, C)` from Definition 5
|
|
89
|
+
- `Policy#validate_access!` signature changed to `(field_names, purpose_name, subject:)` — subject identifier is now required, eliminating the caller-supplied boolean anti-pattern
|
|
90
|
+
- `UndeclaredPurposeError` and `PurposeFieldMismatchError` exception classes — completes the five-class violation taxonomy from Definition 4 (`InvalidFieldError`, `UndeclaredPurposeError`, `PurposeFieldMismatchError`, `ConsentRequiredError`, `SensitivityViolationError`)
|
|
91
|
+
- `Policy#validate_access!` now calls `get_field` before `allowed?` so undeclared fields raise `InvalidFieldError` and purpose-field mismatches raise `PurposeFieldMismatchError`, making violation classes unambiguous
|
|
92
|
+
- `Purpose#lia_documented!` DSL method and `lia_documented?` predicate — records that a Legitimate Interests Assessment has been conducted for an Art. 6(1)(f) purpose
|
|
93
|
+
- `Policy#lia_compliance_gaps` — returns all `:legitimate_interests` purposes lacking a documented LIA; surfaces Definition 2's basis_ok condition as a compliance query rather than an enforcement gate (per paper §3.2: LIA is a human-judgment obligation, not automatable)
|
|
94
|
+
- `Purpose#art9_basis(*bases)` DSL method for declaring GDPR Art. 9(2) sub-clauses on a purpose; multiple calls accumulate
|
|
95
|
+
- `Purpose#art9_basis?` predicate returning true when at least one Art. 9(2) basis is declared
|
|
96
|
+
- `SensitivityViolationError` exception class (Definition 4, Sensitivity Violation class)
|
|
97
|
+
- `Field::SPECIAL_CATEGORY_TYPES` constant (`:health`, `:biometric`) and `Field#special_category?` predicate — identify GDPR Article 9 special-category data by **type**, independently of the `:restricted` sensitivity (Article 32 risk) level
|
|
98
|
+
- `Policy#validate_access!` enforces Definition 1 Condition 5 by **data type**: raises `SensitivityViolationError` when any requested field is an Article 9 special-category type (`special_category?`) and the purpose declares no Art. 9(2) basis. This deliberately decouples the Art. 9(2) requirement from the `:restricted` risk tier, so high-risk-but-ordinary data (e.g. financial or national-identifier fields marked `:restricted`) no longer spuriously requires an Art. 9(2) basis, while a special-category type triggers the check at any sensitivity level.
|
|
99
|
+
|
|
7
100
|
## [0.6.0] - 2026-01-05
|
|
8
101
|
|
|
9
102
|
### Added
|