Class: Plutonium::Resource::Policy

Inherits:
ActionPolicy::Base
  • Object
show all
Defined in:
lib/plutonium/resource/policy.rb

Overview

Policy class to define permissions and attributes for a resource. This class provides methods to check permissions for various actions and to retrieve permitted attributes for these actions.

Direct Known Subclasses

Interaction::Async::RunPolicy

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.method_added(name) ⇒ Object

relation_scope is a class-level macro: it registers a scope handler named __scoping__active_record_relation__default, and never defines an instance method called relation_scope. So def relation_scope(relation) overrides nothing — apply_scope keeps dispatching to the inherited handler, the author's narrowing never runs, and verify_default_relation_scope_applied! is happy because the base handler did call default_relation_scope. Fail-open and silent, so catch it the moment the method is defined rather than at request time.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/plutonium/resource/policy.rb', line 31

def self.method_added(name)
  super

  return unless name == :relation_scope

  raise <<~MSG
    #{self} defines `relation_scope` as an instance method, which Plutonium never calls.
    `relation_scope` is a macro, so this scoping silently does nothing.

    Use the block form instead:

      relation_scope do |relation|
        default_relation_scope(relation).your_scope
      end
  MSG
end

Instance Method Details

#apply_scope(relation, type:, **options) ⇒ Object

Wraps apply_scope to verify default_relation_scope was called. This prevents accidental multi-tenancy leaks when overriding relation_scope.



50
51
52
53
54
55
# File 'lib/plutonium/resource/policy.rb', line 50

def apply_scope(relation, type:, **options)
  @_default_relation_scope_applied = false
  result = super
  verify_default_relation_scope_applied! if type == :active_record_relation
  result
end

#create?Boolean

Checks if the create action is permitted.

Returns:

  • (Boolean)

    false by default.



150
151
152
# File 'lib/plutonium/resource/policy.rb', line 150

def create?
  false
end

#default_relation_scope(relation) ⇒ ActiveRecord::Relation

Applies Plutonium's default scoping (parent or entity) to a relation.

This method MUST be called in any custom relation_scope to ensure proper parent/entity scoping. Failure to call it will raise an error.

Examples:

Overriding inherited scope while keeping default scoping

# Parent policy has custom filtering you want to replace
class AdminPostPolicy < PostPolicy
  relation_scope do |relation|
    # Replace inherited scope but keep Plutonium's parent/entity scoping
    default_relation_scope(relation)
  end
end

Adding filtering on top of default scoping

relation_scope do |relation|
  default_relation_scope(relation).where(published: true)
end

Parameters:

  • relation (ActiveRecord::Relation)

    The relation to scope

Returns:

  • (ActiveRecord::Relation)

    The scoped relation



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/plutonium/resource/policy.rb', line 92

def default_relation_scope(relation)
  @_default_relation_scope_applied = true

  if parent || parent_association
    unless parent && parent_association
      raise ArgumentError, "parent and parent_association must both be provided together"
    end

    # Parent association scoping (nested routes).
    #
    # The parent context is set on the policy for the whole request, so it
    # leaks into sibling lookups too — e.g. a SecureAssociation field on
    # the child's form authorizes an unrelated resource scope while
    # parent/parent_association are still set. Only apply parent scoping
    # when the relation actually corresponds to the parent's named
    # association; otherwise fall through to entity scoping so we don't
    # produce an incoherent (and silently empty) result.
    assoc_reflection = parent.class.reflect_on_association(parent_association)
    if assoc_reflection && relation.klass <= assoc_reflection.klass
      # The parent was already entity-scoped during authorization, so
      # children accessed through the parent don't need additional
      # entity scoping.
      if assoc_reflection.collection?
        # has_many: merge with the association's scope
        parent.public_send(parent_association).merge(relation)
      else
        # has_one: scope by foreign key
        relation.where(assoc_reflection.foreign_key => parent.id)
      end
    elsif entity_scope
      relation.associated_with(entity_scope)
    else
      relation
    end
  elsif entity_scope
    # Entity scoping (multi-tenancy)
    relation.associated_with(entity_scope)
  else
    relation
  end
end

#destroy?Boolean

Checks if the destroy action is permitted.

Returns:

  • (Boolean)

    Delegates to create?.



171
172
173
# File 'lib/plutonium/resource/policy.rb', line 171

def destroy?
  create?
end

#edit?Boolean

Checks if the edit action is permitted.

Returns:

  • (Boolean)

    Delegates to update?.



201
202
203
# File 'lib/plutonium/resource/policy.rb', line 201

def edit?
  update?
end

#export_csv?Boolean

Checks if CSV export is permitted.

Defaults to false so export is strictly opt-in. Enable it per resource by overriding to return true (or delegating to index?).

Returns:

  • (Boolean)

    false by default.



242
243
244
# File 'lib/plutonium/resource/policy.rb', line 242

def export_csv?
  false
end

#index?Boolean

Checks if the index action is permitted.

Returns:

  • (Boolean)

    Delegates to read?.



180
181
182
# File 'lib/plutonium/resource/policy.rb', line 180

def index?
  read?
end

#kanban_move?Boolean

Authorizes a kanban board move. Delegates to update? by default — override to allow board drags without granting full edit-form access.

Returns:

  • (Boolean)

    Delegates to update?.



209
210
211
# File 'lib/plutonium/resource/policy.rb', line 209

def kanban_move?
  update?
end

#new?Boolean

Checks if the new action is permitted.

Returns:

  • (Boolean)

    Delegates to create?.



187
188
189
# File 'lib/plutonium/resource/policy.rb', line 187

def new?
  create?
end

#permitted_associationsArray<Symbol>

Returns the permitted associations.

Returns:

  • (Array<Symbol>)

    An empty array by default.



316
317
318
# File 'lib/plutonium/resource/policy.rb', line 316

def permitted_associations
  []
end

#permitted_attributes_for_createArray<Symbol>

Returns the permitted attributes for the create action.

Returns:

  • (Array<Symbol>)

    The permitted attributes.



251
252
253
254
255
256
# File 'lib/plutonium/resource/policy.rb', line 251

def permitted_attributes_for_create
  autodetect_permitted_fields(:permitted_attributes_for_create) - [
    resource_class.primary_key.to_sym, # primary_key
    :created_at, :updated_at # timestamps
  ]
end

#permitted_attributes_for_editArray<Symbol>

Returns the permitted attributes for the edit action.

Returns:

  • (Array<Symbol>)

    Delegates to permitted_attributes_for_update.



309
310
311
# File 'lib/plutonium/resource/policy.rb', line 309

def permitted_attributes_for_edit
  permitted_attributes_for_update
end

#permitted_attributes_for_exportArray<Symbol>

Returns the attributes included in an export (e.g. CSV columns).

Format-agnostic on purpose (named _export, not _export_csv) so a future export format can reuse the same column set. Defaults to the index columns; override to tailor the exported columns.

Returns:

  • (Array<Symbol>)

    Delegates to permitted_attributes_for_index.



295
296
297
# File 'lib/plutonium/resource/policy.rb', line 295

def permitted_attributes_for_export
  permitted_attributes_for_index
end

#permitted_attributes_for_indexArray<Symbol>

Returns the permitted attributes for the index action.

Returns:

  • (Array<Symbol>)

    Delegates to permitted_attributes_for_read.



277
278
279
# File 'lib/plutonium/resource/policy.rb', line 277

def permitted_attributes_for_index
  permitted_attributes_for_read
end

#permitted_attributes_for_newArray<Symbol>

Returns the permitted attributes for the new action.

Returns:

  • (Array<Symbol>)

    Delegates to permitted_attributes_for_create.



302
303
304
# File 'lib/plutonium/resource/policy.rb', line 302

def permitted_attributes_for_new
  permitted_attributes_for_create
end

#permitted_attributes_for_readArray<Symbol>

Returns the permitted attributes for the read action.

Returns:

  • (Array<Symbol>)

    The permitted attributes.



261
262
263
# File 'lib/plutonium/resource/policy.rb', line 261

def permitted_attributes_for_read
  autodetect_permitted_fields(:permitted_attributes_for_read)
end

#permitted_attributes_for_showArray<Symbol>

Returns the permitted attributes for the show action.

Returns:

  • (Array<Symbol>)

    Delegates to permitted_attributes_for_read.



284
285
286
# File 'lib/plutonium/resource/policy.rb', line 284

def permitted_attributes_for_show
  permitted_attributes_for_read
end

#permitted_attributes_for_updateArray<Symbol>

Returns the permitted attributes for the update action.

Returns:

  • (Array<Symbol>)

    Delegates to permitted_attributes_for_create.



268
269
270
# File 'lib/plutonium/resource/policy.rb', line 268

def permitted_attributes_for_update
  permitted_attributes_for_create
end

#read?Boolean

Checks if the read action is permitted.

Returns:

  • (Boolean)

    false by default.



157
158
159
# File 'lib/plutonium/resource/policy.rb', line 157

def read?
  false
end

#reposition?Boolean

Authorizes a drag-reorder drop on a table or grid. Delegates to update? by default — override to allow (or forbid) reordering independently of full edit-form access.

Returns:

  • (Boolean)

    Delegates to update?.



218
219
220
# File 'lib/plutonium/resource/policy.rb', line 218

def reposition?
  update?
end

#search?Boolean

Checks if record search is permitted.

Returns:

  • (Boolean)

    Delegates to index?.



225
226
227
# File 'lib/plutonium/resource/policy.rb', line 225

def search?
  index?
end

#send_with_report(method) ⇒ Object

Sends a method and raises an error if the method is not implemented.

Parameters:

  • method (Symbol)

    The method to send.



137
138
139
140
141
142
143
# File 'lib/plutonium/resource/policy.rb', line 137

def send_with_report(method)
  unless respond_to?(method)
    raise NotImplementedError, "#{self.class.name} does not implement the required #{method}"
  end

  public_send(method)
end

#show?Boolean

Checks if the show action is permitted.

Returns:

  • (Boolean)

    Delegates to read?.



194
195
196
# File 'lib/plutonium/resource/policy.rb', line 194

def show?
  read?
end

#skip_default_relation_scope!Object

Explicitly skip the default relation scope verification.

Call this when you intentionally want to bypass parent/entity scoping. This should be rare - consider using a separate portal instead.

Examples:

Skipping default scoping (use sparingly)

relation_scope do |relation|
  skip_default_relation_scope!
  relation.where(featured: true)  # No parent/entity scoping
end


67
68
69
# File 'lib/plutonium/resource/policy.rb', line 67

def skip_default_relation_scope!
  @_default_relation_scope_applied = true
end

#typeahead?Boolean

Checks if typeahead/autocomplete queries are permitted.

Returns:

  • (Boolean)

    Delegates to index?.



232
233
234
# File 'lib/plutonium/resource/policy.rb', line 232

def typeahead?
  index?
end

#update?Boolean

Checks if the update action is permitted.

Returns:

  • (Boolean)

    Delegates to create?.



164
165
166
# File 'lib/plutonium/resource/policy.rb', line 164

def update?
  create?
end