Module: Sequel::Plugins::Privacy::ClassMethods

Extended by:
T::Helpers, T::Sig
Defined in:
lib/sequel/plugins/privacy.rb

Instance Method Summary collapse

Instance Method Details

#allow_unsafe_access!Object



178
179
180
181
# File 'lib/sequel/plugins/privacy.rb', line 178

def allow_unsafe_access!
  @allow_unsafe_access = T.let(true, T.nilable(T::Boolean))
  Sequel::Privacy.logger&.warn("#{self} allows unsafe access - migrate to use for_vc()")
end

#allow_unsafe_access?Boolean

Returns:

  • (Boolean)


184
185
186
# File 'lib/sequel/plugins/privacy.rb', line 184

def allow_unsafe_access?
  @allow_unsafe_access == true
end

#associate(type, name, opts = {}, &block) ⇒ Object



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/sequel/plugins/privacy.rb', line 410

def associate(type, name, opts = {}, &block)
  # Call original to create the association
  result = super

  # Wrap the association method with privacy checks
  case type
  when :many_to_one, :one_to_one
    _override_singular_association(name)
  when :one_to_many, :many_to_many
    _override_plural_association(name)
    # Check if there are already privacy policies defined for this association
    setup_association_privacy(name) if privacy_association_policies[name]
  end

  result
end

#call(values) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/sequel/plugins/privacy.rb', line 198

def call(values)
  # Check if we're in a VC context (thread-local set by for_vc)
  vc = Thread.current[privacy_vc_key]

  unless vc || allow_unsafe_access?
    Kernel.raise Sequel::Privacy::MissingViewerContext,
                 "#{self} requires a ViewerContext. Use #{self}.for_vc(vc) or call #{self}.allow_unsafe_access!"
  end

  # Create the instance via parent chain
  instance = super

  # Attach VC if present
  if vc && instance
    instance.instance_variable_set(:@viewer_context, vc)

    # During nested policy evaluation, return raw rows so the outer
    # policy can traverse data (e.g. checking membership) without
    # recursive :view filtering.
    return instance if Sequel::Privacy::Enforcer.in_policy_eval?

    unless T.cast(instance, InstanceMethods).allow?(vc, :view)
      Sequel::Privacy.logger&.debug { "Privacy denied :view on #{self}[#{instance.pk}]" }
      return nil
    end
  end

  instance
end

#finalize_privacy!Object



374
375
376
# File 'lib/sequel/plugins/privacy.rb', line 374

def finalize_privacy!
  @privacy_finalized = T.let(true, T.nilable(T::Boolean))
end

#for_vc(vc) ⇒ Object



400
401
402
# File 'lib/sequel/plugins/privacy.rb', line 400

def for_vc(vc)
  dataset.for_vc(vc)
end

#policies(action, *policy_chain) ⇒ Object



384
385
386
387
# File 'lib/sequel/plugins/privacy.rb', line 384

def policies(action, *policy_chain)
  Kernel.warn "DEPRECATED: #{self}.policies is deprecated. Use `privacy do; can :#{action}, ...; end` instead"
  register_policies(action, policy_chain)
end

#privacy(&block) ⇒ Object



264
265
266
267
268
269
270
271
# File 'lib/sequel/plugins/privacy.rb', line 264

def privacy(&block)
  if privacy_finalized?
    Kernel.raise Sequel::Privacy::PrivacyAlreadyFinalizedError, "Privacy already finalized for #{self}"
  end

  dsl = PrivacyDSL.new(self)
  dsl.instance_eval(&block)
end

#privacy_association_policiesObject



244
245
246
# File 'lib/sequel/plugins/privacy.rb', line 244

def privacy_association_policies
  @privacy_association_policies ||= T.let({}, T.nilable(T::Hash[Symbol, T::Hash[Symbol, T::Array[T.untyped]]]))
end

#privacy_fieldsObject



238
239
240
# File 'lib/sequel/plugins/privacy.rb', line 238

def privacy_fields
  @privacy_fields ||= T.let({}, T.nilable(T::Hash[Symbol, Symbol]))
end

#privacy_finalized?Boolean

Returns:

  • (Boolean)


249
250
251
# File 'lib/sequel/plugins/privacy.rb', line 249

def privacy_finalized?
  @privacy_finalized == true
end

#privacy_policiesObject



233
234
235
# File 'lib/sequel/plugins/privacy.rb', line 233

def privacy_policies
  @privacy_policies ||= T.let({}, T.nilable(T::Hash[Symbol, T::Array[T.untyped]]))
end

#privacy_vc_keyObject



190
191
192
# File 'lib/sequel/plugins/privacy.rb', line 190

def privacy_vc_key
  :"#{self}_privacy_vc"
end

#protect_field(field, policy: nil) ⇒ Object



391
392
393
394
395
396
# File 'lib/sequel/plugins/privacy.rb', line 391

def protect_field(field, policy: nil)
  Kernel.warn "DEPRECATED: #{self}.protect_field is deprecated. Use `privacy do; field :#{field}, ...; end` instead"
  policy_name = policy || :"view_#{field}"
  # Need to also register the policy if not already defined
  register_protected_field(field, policy_name)
end

#register_association_policies(assoc_name, action, policies, defer_setup: false) ⇒ Object



319
320
321
322
323
324
325
326
327
328
329
# File 'lib/sequel/plugins/privacy.rb', line 319

def register_association_policies(assoc_name, action, policies, defer_setup: false)
  Kernel.raise "Privacy policies have been finalized for #{self}" if privacy_finalized?

  privacy_association_policies[assoc_name] ||= {}
  assoc_hash = T.must(privacy_association_policies[assoc_name])
  assoc_hash[action] ||= []
  T.must(assoc_hash[action]).concat(policies)

  # Set up the association method overrides if the association exists (unless deferred)
  setup_association_privacy(assoc_name) if !defer_setup && association_reflection(assoc_name)
end

#register_policies(action, policies) ⇒ Object



275
276
277
278
279
280
281
282
# File 'lib/sequel/plugins/privacy.rb', line 275

def register_policies(action, policies)
  if privacy_finalized?
    Kernel.raise Sequel::Privacy::PrivacyAlreadyFinalizedError, "Privacy already finalized for #{self}"
  end

  privacy_policies[action] ||= []
  T.must(privacy_policies[action]).concat(policies)
end

#register_protected_field(field, policy_name) ⇒ Object



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/sequel/plugins/privacy.rb', line 286

def register_protected_field(field, policy_name)
  if privacy_finalized?
    Kernel.raise Sequel::Privacy::PrivacyAlreadyFinalizedError, "Privacy already finalized for #{self}"
  end

  privacy_fields[field] = policy_name

  # Store original method
  original_method = instance_method(field)

  # Override the field getter
  define_method(field) do
    # During nested policy evaluation, return raw value without
    # checking the field's view policy.
    return original_method.bind(self).() if Sequel::Privacy::Enforcer.in_policy_eval?

    vc = instance_variable_get(:@viewer_context)

    unless vc
      Kernel.raise Sequel::Privacy::MissingViewerContext,
                   "#{self.class}##{field} requires a ViewerContext"
    end

    value = original_method.bind(self).()
    return unless T.cast(self, InstanceMethods).allow?(vc, policy_name)

    value
  end
end

#setup_association_privacy(assoc_name) ⇒ Object



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/sequel/plugins/privacy.rb', line 334

def setup_association_privacy(assoc_name)
  assoc_policies = privacy_association_policies[assoc_name]
  return unless assoc_policies

  reflection = association_reflection(assoc_name)
  return unless reflection

  # Track which associations have been wrapped to avoid double-wrapping
  @_wrapped_associations ||= T.let({}, T.nilable(T::Hash[Symbol, T::Boolean]))
  return if @_wrapped_associations[assoc_name]

  @_wrapped_associations[assoc_name] = true

  # Determine the singular name for method naming
  # For many_to_many :members, methods are add_member, remove_member
  # For one_to_many :memberships, methods are add_membership, remove_membership
  singular_name = reflection[:name].to_s.chomp('s').to_sym

  # Wrap add_* method if :add policy exists
  add_policies = assoc_policies[:add]
  if add_policies && method_defined?(:"add_#{singular_name}")
    _wrap_association_add(assoc_name, singular_name, add_policies)
  end

  # Wrap remove_* method if :remove policy exists
  remove_policies = assoc_policies[:remove]
  if remove_policies && method_defined?(:"remove_#{singular_name}")
    _wrap_association_remove(assoc_name, singular_name, remove_policies)
  end

  # Wrap remove_all_* method if :remove_all policy exists
  remove_all_policies = assoc_policies[:remove_all]
  return unless remove_all_policies && method_defined?(:"remove_all_#{reflection[:name]}")

  _wrap_association_remove_all(assoc_name, reflection[:name], remove_all_policies)
end