orfeas_pam_dsl 0.6.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.
@@ -0,0 +1,558 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PamDsl
4
+ # Generates PAM DSL policy files with sensible defaults
5
+ #
6
+ # Can generate a basic policy template or scan ActiveRecord models
7
+ # to detect PII fields and generate a policy based on them.
8
+ #
9
+ # @example Generate basic policy
10
+ # generator = PamDsl::PolicyGenerator.new("my_app")
11
+ # generator.generate # Creates config/initializers/pam_dsl_policy.rb
12
+ #
13
+ # @example Generate from models
14
+ # generator = PamDsl::PolicyGenerator.new("my_app")
15
+ # generator.generate_from_models # Scans models for PII
16
+ #
17
+ class PolicyGenerator
18
+ # Patterns that should be excluded (timestamps, counts, amounts, flags, etc.)
19
+ EXCLUDE_PATTERNS = [
20
+ /_at\z/i, # Timestamps: created_at, updated_at, email_sent_at, cancelled_at
21
+ /_on\z/i, # Date fields: published_on, expires_on
22
+ /_count\z/i, # Counters: login_count, failed_attempts_count
23
+ /_amount\z/i, # Amounts: vat_amount, total_amount, discount_amount
24
+ /_total\z/i, # Totals: grand_total, subtotal
25
+ /_reason\z/i, # Reason text: cancellation_reason, rejection_reason
26
+ /_notes?\z/i, # Notes: admin_notes, internal_note
27
+ /_status\z/i, # Status flags: payment_status, order_status
28
+ /_type\z/i, # Type flags: payment_type, user_type
29
+ /_id\z/i, # Foreign keys: user_id, order_id
30
+ /_uuid\z/i, # UUIDs: order_uuid
31
+ /_code\z/i, # Codes: country_code, currency_code (but not postal_code)
32
+ /\A(id|uuid)\z/i, # Primary keys
33
+ /\Ais_/i, # Boolean flags: is_active, is_verified
34
+ /\Ahas_/i, # Boolean flags: has_consent
35
+ /_enabled\z/i, # Flags: two_factor_enabled
36
+ /_verified\z/i, # Flags: email_verified
37
+ /_confirmed\z/i, # Flags: payment_confirmed
38
+ /encrypted_/i, # Already encrypted: encrypted_password
39
+ /_digest\z/i, # Hashes: password_digest
40
+ /_token\z/i, # Tokens: reset_token, auth_token
41
+ /_hash\z/i, # Hashes: password_hash
42
+ ].freeze
43
+
44
+ # Common PII field patterns and their types
45
+ # More specific patterns to reduce false positives
46
+ PII_PATTERNS = {
47
+ # Email - must be the actual email field, not email_sent_at, email_verified, etc.
48
+ /\A(email|e_mail|mail_address|user_email|contact_email|billing_email)\z/i => { type: :email, sensitivity: :confidential },
49
+
50
+ # Names - exact matches only
51
+ /\A(first_?name|given_?name|fname)\z/i => { type: :name, sensitivity: :internal },
52
+ /\A(last_?name|surname|family_?name|lname)\z/i => { type: :name, sensitivity: :internal },
53
+ /\A(full_?name|display_?name)\z/i => { type: :name, sensitivity: :internal },
54
+ /\A(father_?name|fathername|mother_?name|parent_?name)\z/i => { type: :name, sensitivity: :internal },
55
+ /\A(name|firstname|lastname)\z/i => { type: :name, sensitivity: :internal },
56
+
57
+ # Phone - exact field names only
58
+ /\A(phone|telephone|mobile|cell|fax|phone_number|mobile_number|cell_phone)\z/i => { type: :phone, sensitivity: :confidential },
59
+
60
+ # Address - exact matches
61
+ /\A(address|street|city|state|province|country|postal|zip|postcode|postal_code|zip_code)\z/i => { type: :address, sensitivity: :confidential },
62
+ /\A(address_line_?\d?|street_address|billing_address|shipping_address|home_address|work_address)\z/i => { type: :address, sensitivity: :confidential },
63
+
64
+ # Financial - exact matches for account identifiers
65
+ /\A(iban|swift|bic|bank_account|account_number|routing_number)\z/i => { type: :financial, sensitivity: :restricted },
66
+ /\A(credit_card|card_number|card_number_first\d|card_number_last\d|cvv|cvc)\z/i => { type: :credit_card, sensitivity: :restricted },
67
+
68
+ # Tax/VAT identifiers - the actual number, not amounts
69
+ # AFM = Greek tax identification number (ΑΦΜ - Αριθμός Φορολογικού Μητρώου)
70
+ /\A(vat_number|vat_id|tax_id|tin|tax_number|vat_reg_number|afm)\z/i => { type: :identifier, sensitivity: :restricted },
71
+
72
+ # Personal identifiers - exact matches
73
+ /\A(ssn|social_security|social_security_number|national_id|passport|passport_number|driver_license|drivers_license|license_number)\z/i => { type: :ssn, sensitivity: :restricted },
74
+ /\A(dob|date_of_birth|birth_date|birthday|birthdate)\z/i => { type: :date_of_birth, sensitivity: :confidential },
75
+
76
+ # Technical - IP addresses
77
+ /\A(ip|ip_address|remote_ip|client_ip|source_ip)\z/i => { type: :ip_address, sensitivity: :internal },
78
+
79
+ # Health - exact matches to avoid false positives
80
+ /\A(health_condition|medical_record|diagnosis|prescription|medical_history)\z/i => { type: :health, sensitivity: :restricted },
81
+
82
+ # Biometric - exact matches
83
+ /\A(fingerprint|face_id|biometric|biometric_data|retina_scan)\z/i => { type: :biometric, sensitivity: :restricted },
84
+
85
+ # Location - exact matches for coordinates
86
+ /\A(latitude|longitude|lat|lng|geo_location|gps_coordinates)\z/i => { type: :location, sensitivity: :confidential },
87
+ /\A(location)\z/i => { type: :location, sensitivity: :confidential },
88
+
89
+ # Credentials and tokens - security sensitive (usually excluded from auto-detection)
90
+ # These patterns are for manual matching when tokens are explicitly included
91
+ /\A(encrypted_password|password_salt|password_digest)\z/i => { type: :credential, sensitivity: :restricted },
92
+ /\A(api_key|spree_api_key|secret_key|access_key)\z/i => { type: :credential, sensitivity: :restricted },
93
+ /\A(reset_password_token|remember_token|confirmation_token|unlock_token)\z/i => { type: :token, sensitivity: :restricted },
94
+ /\A(authentication_token|auth_token|session_token|bearer_token)\z/i => { type: :token, sensitivity: :restricted },
95
+ /\A(guest_token|persistence_token|perishable_token)\z/i => { type: :token, sensitivity: :restricted },
96
+ /\A(gateway_customer_profile_id|gateway_payment_profile_id)\z/i => { type: :payment_token, sensitivity: :restricted }
97
+ }.freeze
98
+
99
+ # Common purposes with sensible defaults
100
+ DEFAULT_PURPOSES = {
101
+ service_delivery: {
102
+ description: "Core service delivery and functionality",
103
+ basis: :contract,
104
+ requires: [:email, :name]
105
+ },
106
+ account_management: {
107
+ description: "User account creation and management",
108
+ basis: :contract,
109
+ requires: [:email]
110
+ },
111
+ communication: {
112
+ description: "Sending transactional and service-related communications",
113
+ basis: :contract,
114
+ requires: [:email]
115
+ },
116
+ billing: {
117
+ description: "Processing payments and generating invoices",
118
+ basis: :contract,
119
+ requires: [:email, :address]
120
+ },
121
+ legal_compliance: {
122
+ description: "Compliance with legal and regulatory requirements",
123
+ basis: :legal_obligation,
124
+ requires: [:email, :address]
125
+ },
126
+ audit_trail: {
127
+ description: "Maintaining security and audit logs",
128
+ basis: :legal_obligation,
129
+ requires: [:ip_address]
130
+ },
131
+ analytics: {
132
+ description: "Analyzing usage patterns to improve services",
133
+ basis: :legitimate_interests,
134
+ requires: []
135
+ },
136
+ marketing: {
137
+ description: "Marketing communications and promotions",
138
+ basis: :consent,
139
+ requires: [:email]
140
+ }
141
+ }.freeze
142
+
143
+ attr_reader :name, :output_path
144
+
145
+ def initialize(name, output_path: nil)
146
+ @name = name.to_s.underscore.to_sym
147
+ @output_path = output_path || default_output_path
148
+ end
149
+
150
+ # Generate a basic policy template
151
+ def generate
152
+ content = generate_basic_policy
153
+ write_file(content)
154
+ print_summary
155
+ end
156
+
157
+ # Generate policy by scanning ActiveRecord models
158
+ def generate_from_models
159
+ detected_fields = scan_models
160
+ content = generate_policy_from_fields(detected_fields)
161
+ write_file(content)
162
+ print_summary(detected_fields)
163
+ end
164
+
165
+ private
166
+
167
+ def default_output_path
168
+ if defined?(Rails)
169
+ Rails.root.join("config", "initializers", "pam_dsl_policy.rb")
170
+ else
171
+ "pam_dsl_policy.rb"
172
+ end
173
+ end
174
+
175
+ def generate_basic_policy
176
+ <<~RUBY
177
+ # frozen_string_literal: true
178
+
179
+ # PAM DSL Privacy Policy for #{@name.to_s.titleize}
180
+ #
181
+ # This file defines PII fields, processing purposes, retention rules,
182
+ # and consent requirements for GDPR compliance.
183
+ #
184
+ # Generated: #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}
185
+ #
186
+ # Documentation: https://github.com/mpantel/lyra-engine/tree/main/gems/pam_dsl
187
+
188
+ PamDsl.define_policy :#{@name} do
189
+ # ─────────────────────────────────────────────────────────────────────────
190
+ # PII FIELD DEFINITIONS
191
+ # ─────────────────────────────────────────────────────────────────────────
192
+ #
193
+ # Sensitivity levels:
194
+ # :public - Publicly accessible
195
+ # :internal - Internal use only (low risk)
196
+ # :confidential - Sensitive, requires protection
197
+ # :restricted - Highly restricted (financial, health, etc.)
198
+
199
+ # Names
200
+ field :first_name, type: :name, sensitivity: :internal
201
+ field :last_name, type: :name, sensitivity: :internal
202
+
203
+ # Contact
204
+ field :email, type: :email, sensitivity: :confidential do
205
+ transform :display do |value|
206
+ value&.gsub(/(.{2})(.*)(@.*)/) { "\#{$1}" + "*" * $2.length + "\#{$3}" }
207
+ end
208
+ transform :log do |_value|
209
+ "[EMAIL]"
210
+ end
211
+ end
212
+
213
+ field :phone, type: :phone, sensitivity: :confidential do
214
+ transform :display do |value|
215
+ value ? "\#{value[0..3]}****\#{value[-2..]}" : nil
216
+ end
217
+ transform :log do |_value|
218
+ "[PHONE]"
219
+ end
220
+ end
221
+
222
+ # Address
223
+ field :address, type: :address, sensitivity: :confidential
224
+
225
+ # Technical
226
+ field :ip_address, type: :ip_address, sensitivity: :internal
227
+
228
+ # ─────────────────────────────────────────────────────────────────────────
229
+ # PROCESSING PURPOSES
230
+ # ─────────────────────────────────────────────────────────────────────────
231
+ #
232
+ # Legal bases (GDPR Article 6):
233
+ # :consent - Data subject has given consent
234
+ # :contract - Processing necessary for contract
235
+ # :legal_obligation - Compliance with legal obligation
236
+ # :vital_interests - Protection of vital interests
237
+ # :public_task - Task in public interest
238
+ # :legitimate_interests - Legitimate interests
239
+
240
+ purpose :service_delivery do
241
+ describe "Core service delivery and functionality"
242
+ basis :contract
243
+ requires :email, :first_name, :last_name
244
+ end
245
+
246
+ purpose :communication do
247
+ describe "Sending transactional and service-related communications"
248
+ basis :contract
249
+ requires :email
250
+ end
251
+
252
+ purpose :audit_trail do
253
+ describe "Maintaining security and audit logs"
254
+ basis :legal_obligation
255
+ requires :ip_address, :email
256
+ end
257
+
258
+ # Uncomment if you need marketing with consent
259
+ # purpose :marketing do
260
+ # describe "Marketing communications and promotions"
261
+ # basis :consent
262
+ # requires :email
263
+ # end
264
+
265
+ # ─────────────────────────────────────────────────────────────────────────
266
+ # RETENTION RULES
267
+ # ─────────────────────────────────────────────────────────────────────────
268
+
269
+ retention do
270
+ default 7.years
271
+
272
+ # Add model-specific retention rules
273
+ # for_model "User" do
274
+ # keep_for 7.years
275
+ # on_expiry :anonymize
276
+ # end
277
+
278
+ # for_model "Transaction" do
279
+ # keep_for 10.years # Financial records
280
+ # end
281
+ end
282
+
283
+ # ─────────────────────────────────────────────────────────────────────────
284
+ # CONSENT REQUIREMENTS
285
+ # ─────────────────────────────────────────────────────────────────────────
286
+
287
+ consent do
288
+ # Uncomment if you have marketing purpose
289
+ # for_purpose :marketing do
290
+ # required!
291
+ # granular!
292
+ # withdrawable!
293
+ # expires_in 2.years
294
+ # describe "We'll send you product updates and promotional offers"
295
+ # end
296
+ end
297
+ end
298
+
299
+ # ─────────────────────────────────────────────────────────────────────────
300
+ # RAILS CONFIGURATION
301
+ # ─────────────────────────────────────────────────────────────────────────
302
+
303
+ Rails.application.config.pam_dsl.default_policy = :#{@name}
304
+ Rails.application.config.pam_dsl.organization = "Your Organization Name"
305
+ Rails.application.config.pam_dsl.dpo_contact = "dpo@example.com"
306
+ RUBY
307
+ end
308
+
309
+ def scan_models
310
+ detected = {}
311
+
312
+ if defined?(ActiveRecord::Base)
313
+ # Get all ActiveRecord models
314
+ Rails.application.eager_load! if defined?(Rails) && Rails.application
315
+
316
+ ActiveRecord::Base.descendants.each do |model|
317
+ next if model.abstract_class?
318
+ next if model.name.start_with?("ActiveRecord::")
319
+ next unless model.table_exists?
320
+
321
+ model.column_names.each do |column|
322
+ # Skip excluded patterns (timestamps, amounts, flags, etc.)
323
+ next if excluded_field?(column)
324
+
325
+ PII_PATTERNS.each do |pattern, config|
326
+ if column.match?(pattern)
327
+ detected[column.to_sym] ||= config.merge(models: [])
328
+ detected[column.to_sym][:models] << model.name unless detected[column.to_sym][:models].include?(model.name)
329
+ end
330
+ end
331
+ end
332
+ end
333
+ end
334
+
335
+ detected
336
+ end
337
+
338
+ def excluded_field?(column)
339
+ EXCLUDE_PATTERNS.any? { |pattern| column.match?(pattern) }
340
+ end
341
+
342
+ def generate_policy_from_fields(detected_fields)
343
+ fields_code = generate_fields_code(detected_fields)
344
+ purposes_code = generate_purposes_code(detected_fields)
345
+ retention_code = generate_retention_code(detected_fields)
346
+
347
+ <<~RUBY
348
+ # frozen_string_literal: true
349
+
350
+ # PAM DSL Privacy Policy for #{@name.to_s.titleize}
351
+ #
352
+ # Auto-generated from ActiveRecord models
353
+ # Generated: #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}
354
+ #
355
+ # Detected PII fields: #{detected_fields.keys.count}
356
+ # Models scanned: #{detected_fields.values.flat_map { |v| v[:models] }.uniq.count}
357
+
358
+ PamDsl.define_policy :#{@name} do
359
+ # ─────────────────────────────────────────────────────────────────────────
360
+ # PII FIELD DEFINITIONS (Auto-detected)
361
+ # ─────────────────────────────────────────────────────────────────────────
362
+
363
+ #{fields_code}
364
+
365
+ # ─────────────────────────────────────────────────────────────────────────
366
+ # PROCESSING PURPOSES
367
+ # ─────────────────────────────────────────────────────────────────────────
368
+
369
+ #{purposes_code}
370
+
371
+ # ─────────────────────────────────────────────────────────────────────────
372
+ # RETENTION RULES
373
+ # ─────────────────────────────────────────────────────────────────────────
374
+
375
+ #{retention_code}
376
+ end
377
+
378
+ # Configuration
379
+ Rails.application.config.pam_dsl.default_policy = :#{@name}
380
+ Rails.application.config.pam_dsl.organization = "Your Organization Name"
381
+ Rails.application.config.pam_dsl.dpo_contact = "dpo@example.com"
382
+ RUBY
383
+ end
384
+
385
+ def generate_fields_code(detected_fields)
386
+ lines = []
387
+
388
+ detected_fields.sort_by { |name, _| name }.each do |name, config|
389
+ models_comment = "# Found in: #{config[:models].join(', ')}"
390
+ field_def = " field :#{name}, type: :#{config[:type]}, sensitivity: :#{config[:sensitivity]}"
391
+
392
+ # Add masking transforms for sensitive fields
393
+ if config[:sensitivity] == :confidential || config[:sensitivity] == :restricted
394
+ lines << models_comment
395
+ lines << "#{field_def} do"
396
+ lines << generate_transform_code(name, config[:type])
397
+ lines << " end"
398
+ lines << ""
399
+ else
400
+ lines << models_comment
401
+ lines << field_def
402
+ end
403
+ end
404
+
405
+ lines.join("\n")
406
+ end
407
+
408
+ def generate_transform_code(name, type)
409
+ indent = " "
410
+ case type
411
+ when :email
412
+ lines = []
413
+ lines << "#{indent}transform :display do |value|"
414
+ lines << '#{indent} value&.gsub(/(.{2})(.*)(@.*)/) { "#' + '{$1}" + \'*\' * $2.length + "#' + '{$3}" }'
415
+ lines << "#{indent}end"
416
+ lines << "#{indent}transform :log do |_value|"
417
+ lines << '#{indent} "[EMAIL]"'
418
+ lines << "#{indent}end"
419
+ lines.map { |l| l.gsub('#{indent}', indent) }.join("\n")
420
+ when :phone
421
+ lines = []
422
+ lines << "#{indent}transform :display do |value|"
423
+ lines << '#{indent} value ? "#' + '{value[0..3]}****#' + '{value[-2..]}" : nil'
424
+ lines << "#{indent}end"
425
+ lines << "#{indent}transform :log do |_value|"
426
+ lines << '#{indent} "[PHONE]"'
427
+ lines << "#{indent}end"
428
+ lines.map { |l| l.gsub('#{indent}', indent) }.join("\n")
429
+ when :identifier, :ssn, :credit_card
430
+ type_label = type.to_s.upcase
431
+ lines = []
432
+ lines << "#{indent}transform :display do |value|"
433
+ lines << '#{indent} value ? "#' + '{value[0..2]}*****#' + '{value[-2..]}" : nil'
434
+ lines << "#{indent}end"
435
+ lines << "#{indent}transform :log do |_value|"
436
+ lines << "#{indent} \"[#{type_label}]\""
437
+ lines << "#{indent}end"
438
+ lines.map { |l| l.gsub('#{indent}', indent) }.join("\n")
439
+ when :credential
440
+ lines = []
441
+ lines << "#{indent}transform :display do |_value|"
442
+ lines << "#{indent} \"[HIDDEN]\""
443
+ lines << "#{indent}end"
444
+ lines << "#{indent}transform :log do |_value|"
445
+ lines << "#{indent} \"[CREDENTIAL]\""
446
+ lines << "#{indent}end"
447
+ lines.join("\n")
448
+ when :token, :payment_token
449
+ type_label = type == :payment_token ? "PAYMENT_TOKEN" : "TOKEN"
450
+ lines = []
451
+ lines << "#{indent}transform :display do |value|"
452
+ lines << "#{indent} value ? \"\#{value[0..3]}...\" : nil"
453
+ lines << "#{indent}end"
454
+ lines << "#{indent}transform :log do |_value|"
455
+ lines << "#{indent} \"[#{type_label}]\""
456
+ lines << "#{indent}end"
457
+ lines.join("\n")
458
+ else
459
+ lines = []
460
+ lines << "#{indent}transform :log do |_value|"
461
+ lines << '#{indent} "[REDACTED]"'
462
+ lines << "#{indent}end"
463
+ lines.map { |l| l.gsub('#{indent}', indent) }.join("\n")
464
+ end
465
+ end
466
+
467
+ def generate_purposes_code(detected_fields)
468
+ # Determine which purposes make sense based on detected fields
469
+ field_names = detected_fields.keys
470
+
471
+ purposes = []
472
+
473
+ # Service delivery if we have names/emails
474
+ if field_names.any? { |f| f.to_s.include?("name") || f.to_s.include?("email") }
475
+ purposes << <<~RUBY
476
+ purpose :service_delivery do
477
+ describe "Core service delivery and functionality"
478
+ basis :contract
479
+ requires #{([:email] & field_names).map { |f| ":#{f}" }.join(", ")}
480
+ end
481
+ RUBY
482
+ end
483
+
484
+ # Billing if we have financial fields
485
+ if field_names.any? { |f| f.to_s.include?("address") || f.to_s.include?("vat") }
486
+ purposes << <<~RUBY
487
+ purpose :billing do
488
+ describe "Processing payments and generating invoices"
489
+ basis :contract
490
+ requires #{([:email, :address, :vat_number] & field_names).map { |f| ":#{f}" }.join(", ")}
491
+ end
492
+ RUBY
493
+ end
494
+
495
+ # Audit trail if we have IP address
496
+ if field_names.include?(:ip_address)
497
+ purposes << <<~RUBY
498
+ purpose :audit_trail do
499
+ describe "Maintaining security and audit logs"
500
+ basis :legal_obligation
501
+ requires :ip_address
502
+ end
503
+ RUBY
504
+ end
505
+
506
+ purposes.join("\n")
507
+ end
508
+
509
+ def generate_retention_code(detected_fields)
510
+ models = detected_fields.values.flat_map { |v| v[:models] }.uniq
511
+
512
+ lines = [" retention do", " default 7.years"]
513
+
514
+ models.sort.each do |model|
515
+ # Financial models get longer retention
516
+ if model.match?(/payment|transaction|invoice|order/i)
517
+ lines << ""
518
+ lines << " for_model \"#{model}\" do"
519
+ lines << " keep_for 10.years # Financial records"
520
+ lines << " end"
521
+ end
522
+ end
523
+
524
+ lines << " end"
525
+ lines.join("\n")
526
+ end
527
+
528
+ def write_file(content)
529
+ FileUtils.mkdir_p(File.dirname(@output_path))
530
+ File.write(@output_path, content)
531
+ end
532
+
533
+ def print_summary(detected_fields = nil)
534
+ puts "\n" + "=" * 60
535
+ puts " PAM DSL Policy Generated"
536
+ puts "=" * 60
537
+ puts "Policy name: #{@name}"
538
+ puts "Output file: #{@output_path}"
539
+
540
+ if detected_fields
541
+ puts "\nDetected PII fields: #{detected_fields.keys.count}"
542
+ puts "Models scanned: #{detected_fields.values.flat_map { |v| v[:models] }.uniq.count}"
543
+
544
+ puts "\nFields by sensitivity:"
545
+ detected_fields.group_by { |_, v| v[:sensitivity] }.each do |sens, fields|
546
+ puts " #{sens}: #{fields.count} (#{fields.map(&:first).join(', ')})"
547
+ end
548
+ end
549
+
550
+ puts "\nNext steps:"
551
+ puts "1. Review and customize the generated policy"
552
+ puts "2. Update organization name and DPO contact"
553
+ puts "3. Add model-specific retention rules"
554
+ puts "4. Test with: rake privacy:policy"
555
+ puts "=" * 60 + "\n"
556
+ end
557
+ end
558
+ end
@@ -0,0 +1,78 @@
1
+ module PamDsl
2
+ # Represents a processing purpose
3
+ class Purpose
4
+ attr_reader :name, :description, :legal_basis, :required_fields, :optional_fields, :metadata
5
+
6
+ LEGAL_BASES = [
7
+ :consent, # Article 6(1)(a) - Data subject has given consent
8
+ :contract, # Article 6(1)(b) - Processing necessary for contract
9
+ :legal_obligation, # Article 6(1)(c) - Compliance with legal obligation
10
+ :vital_interests, # Article 6(1)(d) - Protection of vital interests
11
+ :public_task, # Article 6(1)(e) - Task in public interest
12
+ :legitimate_interests # Article 6(1)(f) - Legitimate interests
13
+ ].freeze
14
+
15
+ def initialize(name)
16
+ @name = name.to_sym
17
+ @description = ""
18
+ @legal_basis = :consent
19
+ @required_fields = []
20
+ @optional_fields = []
21
+ @metadata = {}
22
+ end
23
+
24
+ # Set description
25
+ def describe(text)
26
+ @description = text
27
+ self
28
+ end
29
+
30
+ # Set legal basis
31
+ def basis(legal_basis)
32
+ legal_basis_sym = legal_basis.to_sym
33
+ unless LEGAL_BASES.include?(legal_basis_sym)
34
+ raise Error, "Invalid legal basis: #{legal_basis}. Must be one of #{LEGAL_BASES.join(', ')}"
35
+ end
36
+ @legal_basis = legal_basis_sym
37
+ self
38
+ end
39
+
40
+ # Define required fields
41
+ def requires(*field_names)
42
+ @required_fields.concat(field_names.map(&:to_sym))
43
+ self
44
+ end
45
+
46
+ # Define optional fields
47
+ def optionally(*field_names)
48
+ @optional_fields.concat(field_names.map(&:to_sym))
49
+ self
50
+ end
51
+
52
+ # Add metadata
53
+ def meta(key, value)
54
+ @metadata[key] = value
55
+ self
56
+ end
57
+
58
+ # Check if purpose requires consent
59
+ def requires_consent?
60
+ @legal_basis == :consent
61
+ end
62
+
63
+ # Get all fields (required + optional)
64
+ def all_fields
65
+ (@required_fields + @optional_fields).uniq
66
+ end
67
+
68
+ # Check if a field is required for this purpose
69
+ def requires_field?(field_name)
70
+ @required_fields.include?(field_name.to_sym)
71
+ end
72
+
73
+ # Check if a field is allowed for this purpose
74
+ def allows_field?(field_name)
75
+ all_fields.include?(field_name.to_sym)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module PamDsl
6
+ class Railtie < Rails::Railtie
7
+ railtie_name :pam_dsl
8
+
9
+ # Load rake tasks
10
+ rake_tasks do
11
+ load File.expand_path("tasks/privacy.rake", __dir__)
12
+ end
13
+
14
+ # Configuration for Rails apps
15
+ config.pam_dsl = ActiveSupport::OrderedOptions.new
16
+ config.pam_dsl.default_policy = nil
17
+ config.pam_dsl.organization = "Organization Name"
18
+ config.pam_dsl.dpo_contact = "dpo@example.com"
19
+
20
+ # Make configuration available
21
+ initializer "pam_dsl.configuration" do |app|
22
+ PamDsl.rails_config = app.config.pam_dsl
23
+ end
24
+ end
25
+ end