clickwrap 0.0.0 → 0.1.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.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +90 -0
  3. data/CHANGELOG.md +612 -0
  4. data/README.md +830 -1204
  5. data/SECURITY.md +33 -0
  6. data/app/assets/stylesheets/clickwrap.css +241 -0
  7. data/app/controllers/clickwrap/application_controller.rb +79 -0
  8. data/app/controllers/clickwrap/captures_controller.rb +145 -0
  9. data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
  10. data/app/controllers/clickwrap/receipts_controller.rb +115 -0
  11. data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
  12. data/app/helpers/clickwrap/engine_helper.rb +97 -0
  13. data/app/views/clickwrap/captures/show.html.erb +34 -0
  14. data/app/views/clickwrap/receipts/index.html.erb +38 -0
  15. data/app/views/clickwrap/receipts/show.html.erb +91 -0
  16. data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
  17. data/app/views/clickwrap/shared/_fields.html.erb +100 -0
  18. data/app/views/clickwrap/shared/_statement.html.erb +105 -0
  19. data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
  20. data/config/locales/en.yml +160 -0
  21. data/config/locales/es.yml +138 -0
  22. data/config/routes.rb +41 -0
  23. data/exe/clickwrap +374 -0
  24. data/guides/README.md +30 -0
  25. data/guides/accessibility.md +249 -0
  26. data/guides/consent-and-lifecycle.md +299 -0
  27. data/guides/integrating.md +610 -0
  28. data/guides/integrity.md +212 -0
  29. data/guides/migrating.md +335 -0
  30. data/guides/naming.md +320 -0
  31. data/guides/organizations.md +320 -0
  32. data/guides/receipts-and-verification.md +415 -0
  33. data/guides/request-evidence.md +512 -0
  34. data/guides/retention-and-legal-holds.md +438 -0
  35. data/lib/clickwrap/actor_proxy.rb +147 -0
  36. data/lib/clickwrap/anonymous_actor.rb +47 -0
  37. data/lib/clickwrap/authority.rb +174 -0
  38. data/lib/clickwrap/canonical_json.rb +216 -0
  39. data/lib/clickwrap/capture/event_builder.rb +220 -0
  40. data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
  41. data/lib/clickwrap/capture.rb +650 -0
  42. data/lib/clickwrap/configuration.rb +1129 -0
  43. data/lib/clickwrap/controller_helpers.rb +758 -0
  44. data/lib/clickwrap/current_state.rb +282 -0
  45. data/lib/clickwrap/digest.rb +125 -0
  46. data/lib/clickwrap/doctor.rb +418 -0
  47. data/lib/clickwrap/document_definition.rb +255 -0
  48. data/lib/clickwrap/document_renderer.rb +83 -0
  49. data/lib/clickwrap/document_renderers/markdown.rb +175 -0
  50. data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
  51. data/lib/clickwrap/dsl/policy_builder.rb +462 -0
  52. data/lib/clickwrap/dsl/retention_builder.rb +89 -0
  53. data/lib/clickwrap/durable_commit_callback.rb +37 -0
  54. data/lib/clickwrap/engine.rb +184 -0
  55. data/lib/clickwrap/errors.rb +181 -0
  56. data/lib/clickwrap/form_builder_extensions.rb +341 -0
  57. data/lib/clickwrap/front_matter.rb +67 -0
  58. data/lib/clickwrap/identifier.rb +112 -0
  59. data/lib/clickwrap/import/external_receipt.rb +241 -0
  60. data/lib/clickwrap/import/fine_print.rb +290 -0
  61. data/lib/clickwrap/import/legacy.rb +450 -0
  62. data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
  63. data/lib/clickwrap/integrity/anchor.rb +130 -0
  64. data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
  65. data/lib/clickwrap/integrity/attestor.rb +221 -0
  66. data/lib/clickwrap/integrity/chain.rb +313 -0
  67. data/lib/clickwrap/integrity/timestamp.rb +143 -0
  68. data/lib/clickwrap/ip_geolocation/location.rb +112 -0
  69. data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
  70. data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
  71. data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
  72. data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
  73. data/lib/clickwrap/ip_geolocation.rb +16 -0
  74. data/lib/clickwrap/lifecycle.rb +534 -0
  75. data/lib/clickwrap/linter.rb +382 -0
  76. data/lib/clickwrap/localized_text.rb +101 -0
  77. data/lib/clickwrap/macros.rb +203 -0
  78. data/lib/clickwrap/models/application_record.rb +20 -0
  79. data/lib/clickwrap/models/chain_head.rb +79 -0
  80. data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
  81. data/lib/clickwrap/models/disposition_plan.rb +208 -0
  82. data/lib/clickwrap/models/document.rb +46 -0
  83. data/lib/clickwrap/models/document_version.rb +163 -0
  84. data/lib/clickwrap/models/event.rb +743 -0
  85. data/lib/clickwrap/models/event_document.rb +79 -0
  86. data/lib/clickwrap/models/event_statement.rb +92 -0
  87. data/lib/clickwrap/models/external_action.rb +150 -0
  88. data/lib/clickwrap/models/integrity_attestation.rb +90 -0
  89. data/lib/clickwrap/models/legal_hold.rb +81 -0
  90. data/lib/clickwrap/models/policy_revision.rb +115 -0
  91. data/lib/clickwrap/models/presentation.rb +59 -0
  92. data/lib/clickwrap/models/receipt_access.rb +53 -0
  93. data/lib/clickwrap/models/recording_sequence.rb +21 -0
  94. data/lib/clickwrap/models/request_evidence.rb +378 -0
  95. data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
  96. data/lib/clickwrap/models/statement_state.rb +130 -0
  97. data/lib/clickwrap/pending_receipt.rb +177 -0
  98. data/lib/clickwrap/policy.rb +283 -0
  99. data/lib/clickwrap/presentation_manifest.rb +210 -0
  100. data/lib/clickwrap/presenter.rb +716 -0
  101. data/lib/clickwrap/privacy.rb +419 -0
  102. data/lib/clickwrap/protected_outcome.rb +120 -0
  103. data/lib/clickwrap/receipt.rb +606 -0
  104. data/lib/clickwrap/receipt_html.rb +235 -0
  105. data/lib/clickwrap/receipt_verifier.rb +978 -0
  106. data/lib/clickwrap/reference.rb +44 -0
  107. data/lib/clickwrap/registration.rb +236 -0
  108. data/lib/clickwrap/registry.rb +54 -0
  109. data/lib/clickwrap/remediation_token.rb +155 -0
  110. data/lib/clickwrap/request_evidence_extractor.rb +590 -0
  111. data/lib/clickwrap/request_evidence_policy.rb +261 -0
  112. data/lib/clickwrap/retention/applier.rb +231 -0
  113. data/lib/clickwrap/retention/disposition.rb +221 -0
  114. data/lib/clickwrap/retention/planner.rb +502 -0
  115. data/lib/clickwrap/retention_class.rb +97 -0
  116. data/lib/clickwrap/reviewed_text.rb +28 -0
  117. data/lib/clickwrap/schema_requirements.rb +196 -0
  118. data/lib/clickwrap/services/authorize_external_action.rb +149 -0
  119. data/lib/clickwrap/services/load_policies.rb +69 -0
  120. data/lib/clickwrap/services/publish_documents.rb +251 -0
  121. data/lib/clickwrap/services/validate_policy_references.rb +166 -0
  122. data/lib/clickwrap/statement.rb +248 -0
  123. data/lib/clickwrap/subject_fingerprint.rb +28 -0
  124. data/lib/clickwrap/submission.rb +169 -0
  125. data/lib/clickwrap/system_actor.rb +31 -0
  126. data/lib/clickwrap/test_helpers.rb +676 -0
  127. data/lib/clickwrap/testing.rb +211 -0
  128. data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
  129. data/lib/clickwrap/verification.rb +504 -0
  130. data/lib/clickwrap/version.rb +12 -1
  131. data/lib/clickwrap/view_helpers.rb +190 -0
  132. data/lib/clickwrap/vocabulary.rb +294 -0
  133. data/lib/clickwrap.rb +497 -7
  134. data/lib/generators/clickwrap/document_generator.rb +164 -0
  135. data/lib/generators/clickwrap/hardening_generator.rb +177 -0
  136. data/lib/generators/clickwrap/install_generator.rb +1287 -0
  137. data/lib/generators/clickwrap/link_generator.rb +56 -0
  138. data/lib/generators/clickwrap/policy_generator.rb +118 -0
  139. data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
  140. data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
  141. data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
  142. data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
  143. data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
  144. data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
  145. data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
  146. data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
  147. data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
  148. data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
  149. data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
  150. data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
  151. data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
  152. data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
  153. data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
  154. data/lib/generators/clickwrap/views_generator.rb +101 -0
  155. data/lib/tasks/clickwrap.rake +569 -0
  156. metadata +311 -16
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The external-action outbox — `rails generate clickwrap:install
4
+ # --with-external-actions`, or added later with the same command.
5
+ #
6
+ # Only `Clickwrap.authorize_external_action!` writes here, so an
7
+ # application that never crosses a system boundary inside a capture can
8
+ # never put a row in this table.
9
+ class CreateClickwrapExternalActionTables < ActiveRecord::Migration<%= migration_version %>
10
+ def change
11
+ primary_key_type, = primary_and_foreign_key_types
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # clickwrap_external_actions
15
+ #
16
+ # The outbox. A provider cannot join your database transaction, so an
17
+ # external handoff gets a pending authorization committed locally, an
18
+ # idempotency key, and an explicit resolution — succeeded, failed, or
19
+ # genuinely unknown. `unknown` is a first-class state because a timeout is
20
+ # not a failure, and treating it as one is how a second debit happens.
21
+ # ---------------------------------------------------------------------------
22
+ create_table :clickwrap_external_actions, id: primary_key_type do |t|
23
+ t.string :event_id, limit: 26, null: false
24
+ t.string :policy_key, null: false
25
+ t.string :idempotency_key, null: false
26
+ t.string :provider_name
27
+
28
+ t.string :state, null: false, default: "pending"
29
+ t.integer :attempt_count, null: false, default: 0
30
+ t.send(json_column_type, :provider_receipt)
31
+ t.text :failure_reason
32
+
33
+ t.datetime :requested_at, precision: 6, null: false
34
+ t.datetime :resolved_at, precision: 6
35
+
36
+ t.timestamps precision: 6
37
+ end
38
+
39
+ add_index :clickwrap_external_actions, :idempotency_key, unique: true,
40
+ name: "index_clickwrap_external_actions_on_idempotency_key"
41
+ add_index :clickwrap_external_actions, :event_id, unique: true,
42
+ name: "index_clickwrap_external_actions_on_event"
43
+ add_index :clickwrap_external_actions, [ :state, :requested_at ],
44
+ name: "index_clickwrap_external_actions_on_state"
45
+ add_clickwrap_check_constraint :clickwrap_external_actions,
46
+ "state IN (#{quoted_values(%w[pending succeeded failed unknown])})",
47
+ name: "chk_clickwrap_external_actions_state"
48
+
49
+
50
+ add_clickwrap_foreign_key :clickwrap_external_actions, :clickwrap_events,
51
+ column: :event_id, name: "fk_clickwrap_external_actions_event"
52
+ end
53
+
54
+ private
55
+
56
+ # Honor the host's configured primary key type (uuid vs bigint). Reads the
57
+ # same setting `rails g model` uses, so an app generated with
58
+ # `config.generators { |g| g.orm :active_record, primary_key_type: :uuid }`
59
+ # gets uuid clickwrap tables and uuid foreign keys, automatically.
60
+ #
61
+ # Note that clickwrap_events keeps a ULID string key regardless: its id is
62
+ # quoted verbatim in receipts and exports, so it has to be stable, sortable,
63
+ # and identical in every host.
64
+ def primary_and_foreign_key_types
65
+ config = Rails.configuration.generators
66
+ setting = config.options[config.orm][:primary_key_type]
67
+ primary_key_type = setting || :primary_key
68
+ foreign_key_type = setting || :bigint
69
+ [ primary_key_type, foreign_key_type ]
70
+ end
71
+
72
+ def json_column_type
73
+ return :jsonb if connection.adapter_name.downcase.match?(/postg/) # postgresql, postgis
74
+
75
+ :json
76
+ end
77
+
78
+ # MySQL 8+ doesn't allow default values on JSON columns. Returns an empty-hash
79
+ # default for SQLite/PostgreSQL, nil for MySQL. The models handle nil
80
+ # gracefully by defaulting to {} in their accessors.
81
+ def json_column_default
82
+ return nil if connection.adapter_name.downcase.match?(/mysql|trilogy/)
83
+
84
+ {}
85
+ end
86
+
87
+ # Same MySQL caveat as `json_column_default`, but for list-shaped columns.
88
+ def json_array_default
89
+ return nil if connection.adapter_name.downcase.match?(/mysql|trilogy/)
90
+
91
+ []
92
+ end
93
+
94
+ # Legal documents are routinely longer than MySQL's 64 KB TEXT limit, and a
95
+ # silently truncated agreement is the worst possible failure for this gem.
96
+ # `:mediumtext` maps to MEDIUMTEXT on MySQL (16 MB) and to ordinary TEXT
97
+ # everywhere else.
98
+ def text_column_type
99
+ return :mediumtext if connection.adapter_name.downcase.match?(/mysql|trilogy/)
100
+
101
+ :text
102
+ end
103
+
104
+ def quoted_values(values)
105
+ values.map { |value| connection.quote(value) }.join(", ")
106
+ end
107
+
108
+ # SQLite implements both foreign keys and check constraints by rebuilding a
109
+ # table. During a long install migration, Active Record can otherwise rebuild
110
+ # from a schema-cache entry captured before the indexes/columns immediately
111
+ # above were added. Refreshing around every rebuild keeps the SQLite result
112
+ # identical to PostgreSQL/MySQL instead of quietly resurrecting stale shape.
113
+ def add_clickwrap_check_constraint(table, expression, **options)
114
+ refresh_clickwrap_table_schema!(table)
115
+ add_check_constraint(table, expression, **options)
116
+ refresh_clickwrap_table_schema!(table)
117
+ end
118
+
119
+ def add_clickwrap_foreign_key(from_table, to_table, **options)
120
+ refresh_clickwrap_table_schema!(from_table)
121
+ add_foreign_key(from_table, to_table, **options)
122
+ refresh_clickwrap_table_schema!(from_table)
123
+ end
124
+
125
+ def refresh_clickwrap_table_schema!(table)
126
+ connection.schema_cache.clear_data_source_cache!(table.to_s)
127
+ end
128
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The higher integrity tiers — `rails generate clickwrap:install
4
+ # --with-integrity`, or added later with the same command.
5
+ #
6
+ # Per-tenant event chaining, external anchoring, and third-party
7
+ # timestamping are each off until the host brings an adapter
8
+ # (`config.chain_event_history_with`, `config.anchor_event_history_with`,
9
+ # `config.timestamp_receipts_with`). Until then neither table can receive a
10
+ # row.
11
+ class CreateClickwrapIntegrityTables < ActiveRecord::Migration<%= migration_version %>
12
+ def change
13
+ primary_key_type, = primary_and_foreign_key_types
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # clickwrap_chain_heads
17
+ #
18
+ # Optional tamper-evident chaining, off unless configured. The scope is per
19
+ # tenant or per aggregate, never one global chain: a single chain across
20
+ # unrelated tenants turns every capture into a queue behind every other one,
21
+ # and buys assurance nobody asked for at a cost everybody pays.
22
+ # ---------------------------------------------------------------------------
23
+ create_table :clickwrap_chain_heads, id: primary_key_type do |t|
24
+ t.string :chain_scope, null: false
25
+ t.string :last_event_id, limit: 26
26
+ t.string :last_event_digest
27
+ t.bigint :chain_sequence, null: false, default: 0
28
+
29
+ t.datetime :checkpointed_at, precision: 6
30
+ t.string :checkpoint_digest
31
+ t.string :anchor_reference
32
+ t.send(json_column_type, :anchor_receipt)
33
+
34
+ t.timestamps precision: 6
35
+ end
36
+
37
+ add_index :clickwrap_chain_heads, :chain_scope, unique: true,
38
+ name: "index_clickwrap_chain_heads_on_scope"
39
+ add_index :clickwrap_chain_heads, :last_event_id,
40
+ name: "index_clickwrap_chain_heads_on_last_event"
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # clickwrap_integrity_attestations
45
+ #
46
+ # One row per attempt to have an outside party vouch for an exact event
47
+ # digest, carrying the provider's own result and this gem's verification of
48
+ # it as separate facts. An unverified issue is recorded as unverified.
49
+ # ---------------------------------------------------------------------------
50
+ create_table :clickwrap_integrity_attestations, id: primary_key_type do |t|
51
+ t.string :event_id, limit: 26, null: false
52
+ t.string :kind, null: false
53
+ t.string :state, null: false
54
+ t.string :provider_name, null: false
55
+ t.string :subject_digest, null: false
56
+ t.string :chain_scope
57
+ t.bigint :chain_sequence
58
+ t.string :provider_reference
59
+ t.send(json_column_type, :provider_result, null: false)
60
+ t.send(json_column_type, :verification, null: false)
61
+ t.send(json_column_type, :adapter_capabilities, null: false)
62
+ t.string :attestation_digest, null: false
63
+ t.datetime :attempted_at, precision: 6, null: false
64
+ t.datetime :provider_reported_at, precision: 6
65
+ t.datetime :created_at, precision: 6, null: false
66
+ end
67
+
68
+ add_index :clickwrap_integrity_attestations, [ :event_id, :kind, :attempted_at ],
69
+ name: "index_clickwrap_integrity_attestations_on_event"
70
+ add_clickwrap_check_constraint :clickwrap_integrity_attestations,
71
+ "kind IN (#{quoted_values(%w[event_anchor third_party_timestamp])})",
72
+ name: "chk_clickwrap_attestations_kind"
73
+ add_clickwrap_check_constraint :clickwrap_integrity_attestations,
74
+ "state IN (#{quoted_values(%w[verified issued_unverified unavailable failed])})",
75
+ name: "chk_clickwrap_attestations_state"
76
+
77
+ add_clickwrap_foreign_key :clickwrap_chain_heads, :clickwrap_events,
78
+ column: :last_event_id, name: "fk_clickwrap_chain_heads_event"
79
+ add_clickwrap_foreign_key :clickwrap_integrity_attestations, :clickwrap_events,
80
+ column: :event_id, name: "fk_clickwrap_attestations_event"
81
+ end
82
+
83
+ private
84
+
85
+ # Honor the host's configured primary key type (uuid vs bigint). Reads the
86
+ # same setting `rails g model` uses, so an app generated with
87
+ # `config.generators { |g| g.orm :active_record, primary_key_type: :uuid }`
88
+ # gets uuid clickwrap tables and uuid foreign keys, automatically.
89
+ #
90
+ # Note that clickwrap_events keeps a ULID string key regardless: its id is
91
+ # quoted verbatim in receipts and exports, so it has to be stable, sortable,
92
+ # and identical in every host.
93
+ def primary_and_foreign_key_types
94
+ config = Rails.configuration.generators
95
+ setting = config.options[config.orm][:primary_key_type]
96
+ primary_key_type = setting || :primary_key
97
+ foreign_key_type = setting || :bigint
98
+ [ primary_key_type, foreign_key_type ]
99
+ end
100
+
101
+ def json_column_type
102
+ return :jsonb if connection.adapter_name.downcase.match?(/postg/) # postgresql, postgis
103
+
104
+ :json
105
+ end
106
+
107
+ # MySQL 8+ doesn't allow default values on JSON columns. Returns an empty-hash
108
+ # default for SQLite/PostgreSQL, nil for MySQL. The models handle nil
109
+ # gracefully by defaulting to {} in their accessors.
110
+ def json_column_default
111
+ return nil if connection.adapter_name.downcase.match?(/mysql|trilogy/)
112
+
113
+ {}
114
+ end
115
+
116
+ # Same MySQL caveat as `json_column_default`, but for list-shaped columns.
117
+ def json_array_default
118
+ return nil if connection.adapter_name.downcase.match?(/mysql|trilogy/)
119
+
120
+ []
121
+ end
122
+
123
+ # Legal documents are routinely longer than MySQL's 64 KB TEXT limit, and a
124
+ # silently truncated agreement is the worst possible failure for this gem.
125
+ # `:mediumtext` maps to MEDIUMTEXT on MySQL (16 MB) and to ordinary TEXT
126
+ # everywhere else.
127
+ def text_column_type
128
+ return :mediumtext if connection.adapter_name.downcase.match?(/mysql|trilogy/)
129
+
130
+ :text
131
+ end
132
+
133
+ def quoted_values(values)
134
+ values.map { |value| connection.quote(value) }.join(", ")
135
+ end
136
+
137
+ # SQLite implements both foreign keys and check constraints by rebuilding a
138
+ # table. During a long install migration, Active Record can otherwise rebuild
139
+ # from a schema-cache entry captured before the indexes/columns immediately
140
+ # above were added. Refreshing around every rebuild keeps the SQLite result
141
+ # identical to PostgreSQL/MySQL instead of quietly resurrecting stale shape.
142
+ def add_clickwrap_check_constraint(table, expression, **options)
143
+ refresh_clickwrap_table_schema!(table)
144
+ add_check_constraint(table, expression, **options)
145
+ refresh_clickwrap_table_schema!(table)
146
+ end
147
+
148
+ def add_clickwrap_foreign_key(from_table, to_table, **options)
149
+ refresh_clickwrap_table_schema!(from_table)
150
+ add_foreign_key(from_table, to_table, **options)
151
+ refresh_clickwrap_table_schema!(from_table)
152
+ end
153
+
154
+ def refresh_clickwrap_table_schema!(table)
155
+ connection.schema_cache.clear_data_source_cache!(table.to_s)
156
+ end
157
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Persisted pre-submit presentations — `rails generate clickwrap:install
4
+ # --with-persisted-presentations`, or added later with the same command.
5
+ #
6
+ # A default install writes nothing on GET, so this table can never receive
7
+ # a row until a policy declares
8
+ # `persist_presentations_before_submission_for`. It ships separately so an
9
+ # installation's schema says what that installation actually records.
10
+ class CreateClickwrapPresentationTables < ActiveRecord::Migration<%= migration_version %>
11
+ def change
12
+ primary_key_type, foreign_key_type = primary_and_foreign_key_types
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # clickwrap_presentations
16
+ #
17
+ # The default path signs the manifest into a short-lived token and writes
18
+ # NOTHING on GET — a render is not evidence, and a table row per page view
19
+ # would be both slow and more personal data than the job needs.
20
+ #
21
+ # Rows land here in two cases: a policy explicitly retains pre-submit
22
+ # presentations for a documented reason, and every successful capture, whose
23
+ # accepted manifest is persisted with the event. An abandoned GET is
24
+ # `presented_by_server` — never `accepted`, and never `seen_by_human`,
25
+ # because a server cannot know whether a human saw anything.
26
+ # ---------------------------------------------------------------------------
27
+ create_table :clickwrap_presentations, id: primary_key_type do |t|
28
+ t.string :policy_key, null: false
29
+ t.references :policy_revision, null: false, type: foreign_key_type, index: false,
30
+ foreign_key: { to_table: :clickwrap_policy_revisions }
31
+
32
+ t.string :nonce, null: false
33
+ t.send(json_column_type, :manifest, null: false)
34
+ t.string :manifest_digest, null: false
35
+
36
+ # Stable references drive every evidence query. These optional convenience
37
+ # pointers are strings so one polymorphic column can safely point at host
38
+ # models whose primary keys are integers, UUIDs, or domain strings.
39
+ t.string :actor_type
40
+ t.string :actor_id
41
+ t.string :actor_reference
42
+ t.string :registration_flow_id
43
+ t.string :represented_party_type
44
+ t.string :represented_party_id
45
+ t.string :represented_party_reference
46
+ t.string :tenant_key
47
+ t.string :subject_type
48
+ t.string :subject_id
49
+ t.string :subject_fingerprint
50
+
51
+ t.string :locale, null: false, default: "en"
52
+ t.string :capture_channel, null: false, default: "web_browser"
53
+ t.string :state, null: false, default: "presented_by_server"
54
+
55
+ t.datetime :issued_at, precision: 6, null: false
56
+ t.datetime :expires_at, precision: 6, null: false
57
+ t.datetime :submitted_at, precision: 6
58
+ t.datetime :retain_until, precision: 6
59
+
60
+ t.datetime :created_at, precision: 6, null: false
61
+ end
62
+
63
+ add_index :clickwrap_presentations, :nonce, unique: true,
64
+ name: "index_clickwrap_presentations_on_nonce"
65
+ add_index :clickwrap_presentations, [ :actor_type, :actor_id ],
66
+ name: "index_clickwrap_presentations_on_actor"
67
+ add_index :clickwrap_presentations, :retain_until,
68
+ name: "index_clickwrap_presentations_on_retain_until"
69
+ add_index :clickwrap_presentations, :expires_at,
70
+ name: "index_clickwrap_presentations_on_expires_at"
71
+ add_index :clickwrap_presentations, :actor_reference,
72
+ name: "index_clickwrap_presentations_on_actor_reference"
73
+ add_clickwrap_check_constraint :clickwrap_presentations,
74
+ "state IN (#{quoted_values(%w[presented_by_server accepted rejected expired])})",
75
+ name: "chk_clickwrap_presentations_state"
76
+ add_clickwrap_check_constraint :clickwrap_presentations,
77
+ "capture_channel IN (#{quoted_values(%w[web_browser native_app api_client operator background_job imported_provider system])})",
78
+ name: "chk_clickwrap_presentations_channel"
79
+
80
+ # `clickwrap_events.presentation_id` is created by the core migration and
81
+ # gains its foreign key here, with the table it points at.
82
+ add_clickwrap_foreign_key :clickwrap_events, :clickwrap_presentations,
83
+ column: :presentation_id, name: "fk_clickwrap_events_presentation"
84
+ end
85
+
86
+ private
87
+
88
+ # Honor the host's configured primary key type (uuid vs bigint). Reads the
89
+ # same setting `rails g model` uses, so an app generated with
90
+ # `config.generators { |g| g.orm :active_record, primary_key_type: :uuid }`
91
+ # gets uuid clickwrap tables and uuid foreign keys, automatically.
92
+ #
93
+ # Note that clickwrap_events keeps a ULID string key regardless: its id is
94
+ # quoted verbatim in receipts and exports, so it has to be stable, sortable,
95
+ # and identical in every host.
96
+ def primary_and_foreign_key_types
97
+ config = Rails.configuration.generators
98
+ setting = config.options[config.orm][:primary_key_type]
99
+ primary_key_type = setting || :primary_key
100
+ foreign_key_type = setting || :bigint
101
+ [ primary_key_type, foreign_key_type ]
102
+ end
103
+
104
+ def json_column_type
105
+ return :jsonb if connection.adapter_name.downcase.match?(/postg/) # postgresql, postgis
106
+
107
+ :json
108
+ end
109
+
110
+ # MySQL 8+ doesn't allow default values on JSON columns. Returns an empty-hash
111
+ # default for SQLite/PostgreSQL, nil for MySQL. The models handle nil
112
+ # gracefully by defaulting to {} in their accessors.
113
+ def json_column_default
114
+ return nil if connection.adapter_name.downcase.match?(/mysql|trilogy/)
115
+
116
+ {}
117
+ end
118
+
119
+ # Same MySQL caveat as `json_column_default`, but for list-shaped columns.
120
+ def json_array_default
121
+ return nil if connection.adapter_name.downcase.match?(/mysql|trilogy/)
122
+
123
+ []
124
+ end
125
+
126
+ # Legal documents are routinely longer than MySQL's 64 KB TEXT limit, and a
127
+ # silently truncated agreement is the worst possible failure for this gem.
128
+ # `:mediumtext` maps to MEDIUMTEXT on MySQL (16 MB) and to ordinary TEXT
129
+ # everywhere else.
130
+ def text_column_type
131
+ return :mediumtext if connection.adapter_name.downcase.match?(/mysql|trilogy/)
132
+
133
+ :text
134
+ end
135
+
136
+ def quoted_values(values)
137
+ values.map { |value| connection.quote(value) }.join(", ")
138
+ end
139
+
140
+ # SQLite implements both foreign keys and check constraints by rebuilding a
141
+ # table. During a long install migration, Active Record can otherwise rebuild
142
+ # from a schema-cache entry captured before the indexes/columns immediately
143
+ # above were added. Refreshing around every rebuild keeps the SQLite result
144
+ # identical to PostgreSQL/MySQL instead of quietly resurrecting stale shape.
145
+ def add_clickwrap_check_constraint(table, expression, **options)
146
+ refresh_clickwrap_table_schema!(table)
147
+ add_check_constraint(table, expression, **options)
148
+ refresh_clickwrap_table_schema!(table)
149
+ end
150
+
151
+ def add_clickwrap_foreign_key(from_table, to_table, **options)
152
+ refresh_clickwrap_table_schema!(from_table)
153
+ add_foreign_key(from_table, to_table, **options)
154
+ refresh_clickwrap_table_schema!(from_table)
155
+ end
156
+
157
+ def refresh_clickwrap_table_schema!(table)
158
+ connection.schema_cache.clear_data_source_cache!(table.to_s)
159
+ end
160
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The optional request-evidence annex — `rails generate clickwrap:install
4
+ # --with-request-evidence`, or added later with the same command.
5
+ #
6
+ # Nothing personal is recorded until a policy names a field with a purpose
7
+ # and a deletion rule, so on a default install this table can never receive
8
+ # a row. It ships separately for the same reason the fields are off: an
9
+ # application's schema should not imply it collects things it does not.
10
+ class CreateClickwrapRequestEvidenceTables < ActiveRecord::Migration<%= migration_version %>
11
+ def change
12
+ primary_key_type, = primary_and_foreign_key_types
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # clickwrap_request_evidence
16
+ #
17
+ # The optional annex: IP address, browser user-agent, and provider-estimated
18
+ # IP geolocation, none of it recorded unless a policy names the field.
19
+ #
20
+ # It is a separate table on purpose. Personal request evidence needs its own
21
+ # deletion schedule, and welding it into the core event payload would make a
22
+ # lawful deletion request either impossible or destructive of the historical
23
+ # record. Here it can be removed on its own clock while the agreement it
24
+ # accompanied stays intact and verifiable.
25
+ #
26
+ # Every IP-geolocation value carries its provenance in the same row.
27
+ # Coordinates without an accuracy radius, or a country without knowing which
28
+ # provider guessed it, would read as far more certain than they are.
29
+ # ---------------------------------------------------------------------------
30
+ create_table :clickwrap_request_evidence, id: primary_key_type do |t|
31
+ t.string :event_id, limit: 26, null: false
32
+ t.send(json_column_type, :authorized_fields, null: false)
33
+
34
+ t.text :ip_address_ciphertext
35
+ t.string :ip_address_reader_name
36
+ t.string :trusted_proxy_configuration_digest
37
+ t.datetime :ip_address_recorded_at, precision: 6
38
+ t.datetime :ip_address_delete_after, precision: 6
39
+ t.string :ip_address_retain_until_rule
40
+ t.datetime :ip_address_deleted_at, precision: 6
41
+ t.string :ip_address_unavailable_reason
42
+
43
+ t.text :browser_user_agent_ciphertext
44
+ t.boolean :browser_user_agent_was_client_supplied, null: false, default: true
45
+ t.datetime :browser_user_agent_recorded_at, precision: 6
46
+ t.datetime :browser_user_agent_delete_after, precision: 6
47
+ t.string :browser_user_agent_retain_until_rule
48
+ t.datetime :browser_user_agent_deleted_at, precision: 6
49
+ t.string :browser_user_agent_unavailable_reason
50
+
51
+ t.string :ip_geolocation_country_code
52
+ t.string :ip_geolocation_country_name
53
+ t.string :ip_geolocation_region_name
54
+ t.string :ip_geolocation_region_code
55
+ t.string :ip_geolocation_city_name
56
+ t.string :ip_geolocation_postal_code
57
+
58
+ # Coordinates are strings, not decimals, for two reasons that point the
59
+ # same way. A receipt serializes them as strings anyway, so storing the
60
+ # decimal and formatting it back would introduce a rounding step between
61
+ # what the provider said and what the evidence shows. And a string column
62
+ # can carry ciphertext, which is what lets `encrypt_recorded_ip_geolocation`
63
+ # actually apply to the most identifying field in this table rather than
64
+ # being a setting that quietly does nothing.
65
+ t.string :ip_geolocation_latitude
66
+ t.string :ip_geolocation_longitude
67
+
68
+ t.string :ip_geolocation_timezone
69
+ t.string :ip_geolocation_continent_code
70
+ t.string :ip_geolocation_metro_code
71
+
72
+ t.string :ip_geolocation_provider_name
73
+ t.string :ip_geolocation_provider_source
74
+ t.string :ip_geolocation_database_version
75
+ t.string :ip_geolocation_database_sha256
76
+ t.integer :ip_geolocation_accuracy_radius_in_kilometers
77
+ t.integer :ip_geolocation_accuracy_radius_confidence_percentage
78
+ t.boolean :ip_geolocation_was_estimated, null: false, default: true
79
+ t.boolean :ip_geolocation_source_was_verified_by_host, null: false, default: false
80
+ t.datetime :ip_geolocation_resolved_at, precision: 6
81
+ t.string :ip_geolocation_unavailable_reason
82
+ t.datetime :ip_geolocation_recorded_at, precision: 6
83
+ t.datetime :ip_geolocation_delete_after, precision: 6
84
+ t.string :ip_geolocation_retain_until_rule
85
+ t.datetime :ip_geolocation_deleted_at, precision: 6
86
+
87
+ t.datetime :created_at, precision: 6, null: false
88
+ end
89
+
90
+ add_index :clickwrap_request_evidence, :event_id, unique: true,
91
+ name: "index_clickwrap_request_evidence_on_event"
92
+ add_index :clickwrap_request_evidence, :ip_address_delete_after,
93
+ name: "index_clickwrap_request_evidence_on_ip_address_delete_after"
94
+ add_index :clickwrap_request_evidence, :browser_user_agent_delete_after,
95
+ name: "index_clickwrap_request_evidence_on_user_agent_delete_after"
96
+ add_index :clickwrap_request_evidence, :ip_geolocation_delete_after,
97
+ name: "index_clickwrap_request_evidence_on_geolocation_delete_after"
98
+
99
+
100
+ add_clickwrap_foreign_key :clickwrap_events, :clickwrap_request_evidence,
101
+ column: :request_evidence_id, name: "fk_clickwrap_events_request_evidence"
102
+ add_clickwrap_foreign_key :clickwrap_request_evidence, :clickwrap_events,
103
+ column: :event_id, name: "fk_clickwrap_request_evidence_event"
104
+ end
105
+
106
+ private
107
+
108
+ # Honor the host's configured primary key type (uuid vs bigint). Reads the
109
+ # same setting `rails g model` uses, so an app generated with
110
+ # `config.generators { |g| g.orm :active_record, primary_key_type: :uuid }`
111
+ # gets uuid clickwrap tables and uuid foreign keys, automatically.
112
+ #
113
+ # Note that clickwrap_events keeps a ULID string key regardless: its id is
114
+ # quoted verbatim in receipts and exports, so it has to be stable, sortable,
115
+ # and identical in every host.
116
+ def primary_and_foreign_key_types
117
+ config = Rails.configuration.generators
118
+ setting = config.options[config.orm][:primary_key_type]
119
+ primary_key_type = setting || :primary_key
120
+ foreign_key_type = setting || :bigint
121
+ [ primary_key_type, foreign_key_type ]
122
+ end
123
+
124
+ def json_column_type
125
+ return :jsonb if connection.adapter_name.downcase.match?(/postg/) # postgresql, postgis
126
+
127
+ :json
128
+ end
129
+
130
+ # MySQL 8+ doesn't allow default values on JSON columns. Returns an empty-hash
131
+ # default for SQLite/PostgreSQL, nil for MySQL. The models handle nil
132
+ # gracefully by defaulting to {} in their accessors.
133
+ def json_column_default
134
+ return nil if connection.adapter_name.downcase.match?(/mysql|trilogy/)
135
+
136
+ {}
137
+ end
138
+
139
+ # Same MySQL caveat as `json_column_default`, but for list-shaped columns.
140
+ def json_array_default
141
+ return nil if connection.adapter_name.downcase.match?(/mysql|trilogy/)
142
+
143
+ []
144
+ end
145
+
146
+ # Legal documents are routinely longer than MySQL's 64 KB TEXT limit, and a
147
+ # silently truncated agreement is the worst possible failure for this gem.
148
+ # `:mediumtext` maps to MEDIUMTEXT on MySQL (16 MB) and to ordinary TEXT
149
+ # everywhere else.
150
+ def text_column_type
151
+ return :mediumtext if connection.adapter_name.downcase.match?(/mysql|trilogy/)
152
+
153
+ :text
154
+ end
155
+
156
+ def quoted_values(values)
157
+ values.map { |value| connection.quote(value) }.join(", ")
158
+ end
159
+
160
+ # SQLite implements both foreign keys and check constraints by rebuilding a
161
+ # table. During a long install migration, Active Record can otherwise rebuild
162
+ # from a schema-cache entry captured before the indexes/columns immediately
163
+ # above were added. Refreshing around every rebuild keeps the SQLite result
164
+ # identical to PostgreSQL/MySQL instead of quietly resurrecting stale shape.
165
+ def add_clickwrap_check_constraint(table, expression, **options)
166
+ refresh_clickwrap_table_schema!(table)
167
+ add_check_constraint(table, expression, **options)
168
+ refresh_clickwrap_table_schema!(table)
169
+ end
170
+
171
+ def add_clickwrap_foreign_key(from_table, to_table, **options)
172
+ refresh_clickwrap_table_schema!(from_table)
173
+ add_foreign_key(from_table, to_table, **options)
174
+ refresh_clickwrap_table_schema!(from_table)
175
+ end
176
+
177
+ def refresh_clickwrap_table_schema!(table)
178
+ connection.schema_cache.clear_data_source_cache!(table.to_s)
179
+ end
180
+ end