Class: Clickwrap::DSL::PolicyBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/clickwrap/dsl/policy_builder.rb

Overview

The block passed to Clickwrap.policy.

Clickwrap.policy :signup do
agree_to :terms
acknowledge :privacy_notice
retain_with :ordinary_agreement_evidence
end

The verbs are deliberate. agree_to :terms and consent_to :marketing are different sentences because they are different acts with different lifecycles, and a developer choosing between them is doing the one piece of thinking this gem cannot do for them.

Constant Summary collapse

STATEMENT_OPTIONS =
%i[
  document
  statement
  label
  link_label
  choices
  purpose
  withdrawal_path
  valid_for
  requires
  subject_fingerprint_with
  subject_fingerprint_version
  record_protected_outcome_with
  protected_outcome_version
  optional
  require_an_explicit_choice
  one_time
  require_current_version
].freeze
DEFAULT_RETENTION_SETTING_NAMES =
{
  ip_address: :delete_recorded_ip_addresses_after,
  browser_user_agent: :delete_recorded_browser_user_agents_after,
  ip_geolocation: :delete_recorded_ip_geolocation_after
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(key) ⇒ PolicyBuilder

Returns a new instance of PolicyBuilder.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/clickwrap/dsl/policy_builder.rb', line 43

def initialize(key)
  @key = key.to_s
  @statements = []
  @retention_class_key = nil
  @request_evidence = {}
  @ip_geolocation_fields = {}
  @persist_presentations_for = nil
  @persist_presentations_because = nil
  @capture_channels = nil
  @locales = nil
  @tenant_scope = "optional"
  @options = {}
  @authority_rule = nil
  @ip_geolocation_resolver_name = nil
  @review_request_evidence_configuration_on = nil
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *_arguments, **_options) ⇒ Object (private)

Raises:



453
454
455
456
457
# File 'lib/clickwrap/dsl/policy_builder.rb', line 453

def method_missing(name, *_arguments, **_options)
  raise DefinitionError,
        "Policy #{@key} calls unknown DSL method `#{name}`. Check the spelling; " \
        "Clickwrap never ignores policy declarations."
end

Instance Method Details

#acknowledge(statement_key, **options) ⇒ Object

Affirmative receipt or awareness of a notice or risk. This is not permission, and it is not consent: a privacy notice is information the person is entitled to, not something they agree to.



70
71
72
# File 'lib/clickwrap/dsl/policy_builder.rb', line 70

def acknowledge(statement_key, **options)
  add_statement(statement_key, "acknowledgment", options)
end

#agree_to(statement_key, **options) ⇒ Object

Assent to contractual terms.



63
64
65
# File 'lib/clickwrap/dsl/policy_builder.rb', line 63

def agree_to(statement_key, **options)
  add_statement(statement_key, "agreement", options)
end

#attest(statement_key, **options) ⇒ Object

An operational fact affirmed by an authorized actor, usually an operator rather than an end user.



89
90
91
# File 'lib/clickwrap/dsl/policy_builder.rb', line 89

def attest(statement_key, **options)
  add_statement(statement_key, "attestation", options)
end

#authorize(statement_key, **options) ⇒ Object

Narrow permission bound to one protected action. This is the difference between "the user once accepted something" and "this exact evidence authorized this exact operation".



96
97
98
# File 'lib/clickwrap/dsl/policy_builder.rb', line 96

def authorize(statement_key, **options)
  add_statement(statement_key, "authorization", options)
end

#compileObject

--- Compilation ----------------------------------------------------------



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/clickwrap/dsl/policy_builder.rb', line 293

def compile
  Policy.new(
    key: @key,
    statements: @statements,
    retention_class_key: @retention_class_key,
    request_evidence: build_request_evidence_policy,
    persist_presentations_for: @persist_presentations_for,
    persist_presentations_because: @persist_presentations_because,
    capture_channels: @capture_channels,
    locales: @locales,
    tenant_scope: @tenant_scope,
    authority_rule: @authority_rule,
    options: @options
  )
end

Purpose-specific permission, where the host has decided consent is the right basis for this processing. Clickwrap does not make that decision; it makes the grant, the withdrawal, and the scope demonstrable.



77
78
79
# File 'lib/clickwrap/dsl/policy_builder.rb', line 77

def consent_to(statement_key, **options)
  add_statement(statement_key, "consent", options)
end

#declare(statement_key, **options) ⇒ Object

A factual statement made by the actor. A declaration can expire without implying it was false when it was made.



83
84
85
# File 'lib/clickwrap/dsl/policy_builder.rb', line 83

def declare(statement_key, **options)
  add_statement(statement_key, "declaration", options)
end

#do_not_record_browser_user_agentObject



238
239
240
# File 'lib/clickwrap/dsl/policy_builder.rb', line 238

def do_not_record_browser_user_agent
  @request_evidence[:browser_user_agent] = RequestEvidencePolicy::NOT_RECORDED
end

#do_not_record_ip_addressObject

A policy-level refusal wins over an application-wide default. This is a named method rather than record: false: a privacy-reducing decision should read unambiguously in review and must not be confused with an omitted option.



224
225
226
# File 'lib/clickwrap/dsl/policy_builder.rb', line 224

def do_not_record_ip_address
  @request_evidence[:ip_address] = RequestEvidencePolicy::NOT_RECORDED
end

#do_not_record_ip_geolocationObject



278
279
280
281
282
# File 'lib/clickwrap/dsl/policy_builder.rb', line 278

def do_not_record_ip_geolocation
  @request_evidence[:ip_geolocation] = RequestEvidencePolicy::NOT_RECORDED
  @ip_geolocation_fields = {}
  @ip_geolocation_resolver_name = nil
end

#only_capture_from(*channels) ⇒ Object

Restrict which capture channels this policy accepts. By default all are allowed and the channel is recorded; a policy that should only ever be completed in a browser can say so.



120
121
122
# File 'lib/clickwrap/dsl/policy_builder.rb', line 120

def only_capture_from(*channels)
  @capture_channels = channels.flatten.map(&:to_s)
end

#only_present_in(*locales) ⇒ Object

Restrict the policy to locales it can actually present. A required legal statement should not fall back to a language nobody chose.



126
127
128
# File 'lib/clickwrap/dsl/policy_builder.rb', line 126

def only_present_in(*locales)
  @locales = locales.flatten.map(&:to_s)
end

#permit_acting_for(*represented_party_types, using: :host, when_actor_is_at_least: nil, when_actor_has_permission: nil, including_when_this_action_creates_the_represented_party: false, **unknown_options) ⇒ Object

Allow an actor to act for a represented party (an employee signing for an organization, a guardian, a service account). The receipt keeps the authenticated principal, the asserted actor, and the represented party as three separate facts. Clickwrap does not decide whether the authority is sufficient.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/clickwrap/dsl/policy_builder.rb', line 157

def permit_acting_for(*represented_party_types, using: :host,
                      when_actor_is_at_least: nil, when_actor_has_permission: nil,
                      including_when_this_action_creates_the_represented_party: false,
                      **unknown_options)
  refuse_unknown_options!("permit_acting_for", unknown_options)
  if represented_party_types.compact.all? { |type| type.to_s.strip.empty? }
    raise DefinitionError,
          "permit_acting_for needs at least one represented-party class name. " \
          "Name every type this policy permits so it cannot authorize an unexpected kind of record."
  end

  @options[:permit_acting_for] = true
  @authority_rule = AuthorityRule.new(
    represented_party_types: represented_party_types,
    adapter_name: using,
    minimum_role: when_actor_is_at_least,
    required_permission: when_actor_has_permission,
    allow_represented_party_creation:
      including_when_this_action_creates_the_represented_party
  )
end

#permit_acting_for_organization(when_actor_is_at_least: nil, when_actor_has_permission: nil, including_when_this_action_creates_the_organization: false, **unknown_options) ⇒ Object

One-line integration with https://github.com/rameerez/organizations. The User remains the human actor; Organizations::Organization is the represented party. A policy must name at least one reviewed authority criterion rather than silently treating every member as able to bind it.

permit_acting_for_organization when_actor_is_at_least: :admin


186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/clickwrap/dsl/policy_builder.rb', line 186

def permit_acting_for_organization(when_actor_is_at_least: nil,
                                   when_actor_has_permission: nil,
                                   including_when_this_action_creates_the_organization: false,
                                   **unknown_options)
  refuse_unknown_options!("permit_acting_for_organization", unknown_options)
  if when_actor_is_at_least.nil? && when_actor_has_permission.nil?
    raise DefinitionError,
          "permit_acting_for_organization needs `when_actor_is_at_least:` or " \
          "`when_actor_has_permission:`. Organization membership alone does not establish " \
          "legal authority to bind the organization."
  end

  permit_acting_for(
    "Organizations::Organization",
    using: :organizations_membership,
    when_actor_is_at_least:,
    when_actor_has_permission:,
    including_when_this_action_creates_the_represented_party:
      including_when_this_action_creates_the_organization
  )
end

#permit_exemptions(because: nil, **unknown_options) ⇒ Object

Allow explicitly recorded system exemptions for this policy. Even when allowed, an exemption never answers agreed_to? — it answers exempted_from?. There is no "missing checkbox means system account" inference anywhere in this gem.



146
147
148
149
150
# File 'lib/clickwrap/dsl/policy_builder.rb', line 146

def permit_exemptions(because: nil, **unknown_options)
  refuse_unknown_options!("permit_exemptions", unknown_options)
  @options[:permit_exemptions] = true
  @options[:permit_exemptions_because] = because
end

#persist_presentations_before_submission_for(duration, because: nil, **unknown_options) ⇒ Object

Keep the presentation manifest for renders that were never submitted. The default path writes nothing on GET; this trades that for a record of display attempts, which some high-assurance flows want. An abandoned GET is labeled presented_by_server — never accepted, and never seen_by_human.



111
112
113
114
115
# File 'lib/clickwrap/dsl/policy_builder.rb', line 111

def persist_presentations_before_submission_for(duration, because: nil, **unknown_options)
  refuse_unknown_options!("persist_presentations_before_submission_for", unknown_options)
  @persist_presentations_for = duration
  @persist_presentations_because = because
end

#record_browser_user_agent(encrypted: nil, delete_after: nil, retain_until: nil, fail_if_unavailable: false, because: nil, legal_basis_reference: nil, **unknown_options) ⇒ Object



228
229
230
231
232
233
234
235
236
# File 'lib/clickwrap/dsl/policy_builder.rb', line 228

def record_browser_user_agent(encrypted: nil, delete_after: nil, retain_until: nil,
                              fail_if_unavailable: false, because: nil,
                              legal_basis_reference: nil, **unknown_options)
  refuse_unknown_options!("record_browser_user_agent", unknown_options)
  @request_evidence[:browser_user_agent] = RequestEvidencePolicy::Setting.new(
    record: true, encrypted:, delete_after:, retain_until:, fail_if_unavailable:,
    because:, legal_basis_reference:
  )
end

#record_ip_address(encrypted: nil, delete_after: nil, retain_until: nil, fail_if_unavailable: false, because: nil, legal_basis_reference: nil, **unknown_options) ⇒ Object

--- Request evidence -----------------------------------------------------



210
211
212
213
214
215
216
217
218
# File 'lib/clickwrap/dsl/policy_builder.rb', line 210

def record_ip_address(encrypted: nil, delete_after: nil, retain_until: nil,
                      fail_if_unavailable: false, because: nil, legal_basis_reference: nil,
                      **unknown_options)
  refuse_unknown_options!("record_ip_address", unknown_options)
  @request_evidence[:ip_address] = RequestEvidencePolicy::Setting.new(
    record: true, encrypted:, delete_after:, retain_until:, fail_if_unavailable:,
    because:, legal_basis_reference:
  )
end

#record_ip_geolocation(country: false, region: false, city: false, postal_code: false, latitude_and_longitude: false, timezone: false, continent: false, metro_code: false, accuracy_radius_in_kilometers: false, using: nil, encrypted: nil, delete_after: nil, retain_until: nil, fail_if_unavailable: false, because: nil, legal_basis_reference: nil, data_protection_impact_assessment_reference: nil, **unknown_options) ⇒ Object

Each IP-geolocation data field is named separately, because each one is a separate decision about what to keep about a person's network context. latitude_and_longitude is one coupled choice: half a coordinate is not a result. Whatever is enabled, the provider name, source, estimated status, resolution time, and any accuracy or database provenance the resolver supplies are stored with it automatically — a policy cannot keep the coordinates and drop the uncertainty needed to read them.



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/clickwrap/dsl/policy_builder.rb', line 250

def record_ip_geolocation(country: false, region: false, city: false, postal_code: false,
                          latitude_and_longitude: false, timezone: false, continent: false,
                          metro_code: false, accuracy_radius_in_kilometers: false,
                          using: nil, encrypted: nil, delete_after: nil, retain_until: nil,
                          fail_if_unavailable: false, because: nil,
                          legal_basis_reference: nil,
                          data_protection_impact_assessment_reference: nil,
                          **unknown_options)
  refuse_unknown_options!("record_ip_geolocation", unknown_options)
  @ip_geolocation_fields = {
    "country" => country,
    "region" => region,
    "city" => city,
    "postal_code" => postal_code,
    "latitude_and_longitude" => latitude_and_longitude,
    "timezone" => timezone,
    "continent" => continent,
    "metro_code" => metro_code,
    "accuracy_radius_in_kilometers" => accuracy_radius_in_kilometers
  }
  @ip_geolocation_resolver_name = using

  @request_evidence[:ip_geolocation] = RequestEvidencePolicy::Setting.new(
    record: true, encrypted:, delete_after:, retain_until:, fail_if_unavailable:,
    because:, legal_basis_reference:, data_protection_impact_assessment_reference:
  )
end

#retain_with(retention_class_key) ⇒ Object

--- Policy-level settings ------------------------------------------------



102
103
104
# File 'lib/clickwrap/dsl/policy_builder.rb', line 102

def retain_with(retention_class_key)
  @retention_class_key = retention_class_key.to_s
end

#review_request_evidence_configuration_on(date) ⇒ Object

A date by which someone should look at this policy's request-evidence configuration again. clickwrap:doctor reports policies that collect personal data without one, and policies whose date has passed.



287
288
289
# File 'lib/clickwrap/dsl/policy_builder.rb', line 287

def review_request_evidence_configuration_on(date)
  @review_request_evidence_configuration_on = date
end

#tenant_is(scope) ⇒ Object

Declares whether this policy is personal, tenant-bound, or deliberately usable in either context. The same policy-level decision is applied to presentation, capture, and verification, so ambient organization state cannot appear on only one side of a signed submission.

tenant_is :not_applicable # personal evidence; ignore ambient tenant
tenant_is :required       # every call must resolve a tenant
tenant_is :optional       # either context is deliberate (the default)


138
139
140
# File 'lib/clickwrap/dsl/policy_builder.rb', line 138

def tenant_is(scope)
  @tenant_scope = scope.to_s
end