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

#_inject_privacy_eager_block(opts) ⇒ Object



423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/sequel/plugins/privacy.rb', line 423

def _inject_privacy_eager_block(opts)
  original = opts[:eager_block]
  wrapped = proc do |ds|
    ds = original.call(ds) if original
    vc = Thread.current[DatasetMethods::EAGER_VC_KEY]
    if vc && T.unsafe(ds).model.respond_to?(:privacy_vc_key)
      T.unsafe(ds).for_vc(vc)
    else
      ds
    end
  end
  opts.merge(eager_block: wrapped)
end

#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



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/sequel/plugins/privacy.rb', line 398

def associate(type, name, opts = {}, &block)
  opts = _inject_privacy_eager_block(opts)

  # 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



203
204
205
206
207
208
209
210
211
212
# File 'lib/sequel/plugins/privacy.rb', line 203

def call(values)
  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

  super
end

#finalize_privacy!Object



362
363
364
# File 'lib/sequel/plugins/privacy.rb', line 362

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

#for_vc(vc) ⇒ Object



388
389
390
# File 'lib/sequel/plugins/privacy.rb', line 388

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

#policies(action, *policy_chain) ⇒ Object



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

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



250
251
252
253
254
255
256
257
# File 'lib/sequel/plugins/privacy.rb', line 250

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



230
231
232
# File 'lib/sequel/plugins/privacy.rb', line 230

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

#privacy_fieldsObject



224
225
226
# File 'lib/sequel/plugins/privacy.rb', line 224

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

#privacy_finalized?Boolean

Returns:

  • (Boolean)


235
236
237
# File 'lib/sequel/plugins/privacy.rb', line 235

def privacy_finalized?
  @privacy_finalized == true
end

#privacy_policiesObject



219
220
221
# File 'lib/sequel/plugins/privacy.rb', line 219

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



379
380
381
382
383
384
# File 'lib/sequel/plugins/privacy.rb', line 379

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



307
308
309
310
311
312
313
314
315
316
317
# File 'lib/sequel/plugins/privacy.rb', line 307

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



261
262
263
264
265
266
267
268
# File 'lib/sequel/plugins/privacy.rb', line 261

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



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/sequel/plugins/privacy.rb', line 272

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
      return original_method.bind(self).() if T.unsafe(self.class).allow_unsafe_access?

      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



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/sequel/plugins/privacy.rb', line 322

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