class RailsAiAdmin::Authorization::Access
Intersects host decisions with declared exposure on every use.
Constants
- VIEWS
Attributes
Public Class Methods
Source
# File lib/rails_ai_admin/authorization/access.rb, line 9 def initialize(context, resource) raise AccessDenied, "UNAUTHENTICATED" unless context.is_a?(Context) @context, @resource = context, resource @adapter = RailsAiAdmin.configuration.authorization_adapter.constantize.new end
Public Instance Methods
Source
# File lib/rails_ai_admin/authorization/access.rb, line 210 def attachment(record, field) macro = field.scalar_type == :files ? :has_many_attached : :has_one_attached reflection = record.class.respond_to?(:reflect_on_attachment) && record.class.reflect_on_attachment(field.column) raise ConfigurationError, "#{field.scalar_type} fields must name a #{macro} attachment" unless reflection && reflection.macro == macro ActiveRecord::Base.while_preventing_writes { record.public_send(field.column) } end
The Active Storage proxy for a file (has_one_attached) or files (has_many_attached) field.
Source
# File lib/rails_ai_admin/authorization/access.rb, line 44 def canonical_id?(id) return id.positive? if id.is_a?(Integer) return false unless id.is_a?(String) && id.valid_encoding? && id.bytesize.between?(1, 150) && !id.match?(/[[:cntrl:]]|[[:space:]]/) case resource.model_class.type_for_attribute(resource.model_class.primary_key).type when :integer then id.match?(/\A[1-9][0-9]{0,18}\z/) when :uuid then id.match?(/\A[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\z/) else true end end
“1abc”, “01” and “ 1” would otherwise be cast to record 1 (RAA-022). Pages address a record only by the primary key’s canonical text; the trusted Runner API still canonicalizes through the scoped lookup.
Source
# File lib/rails_ai_admin/authorization/access.rb, line 56 def fields(action, record = nil, operation: nil, view: nil) view = write_view(operation, view) if action == :write granted = permission_fields(action, record, operation: operation, view: view) action == :read && view ? granted.select { |field| field.visible_on?(view) } : granted end
Source
# File lib/rails_ai_admin/authorization/access.rb, line 31 def find!(id, action: :show, lock: false) scoped = relation scoped = scoped.lock if lock record = scoped.find(id) authorize!(action, record) record rescue ActiveRecord::RecordNotFound raise AccessDenied end
Source
# File lib/rails_ai_admin/authorization/access.rb, line 74 def named_scopes declared = resource.scopes return [] if declared.empty? names = @adapter.respond_to?(:named_scopes) ? @adapter.named_scopes(context: context, resource: resource) : [] unless names.is_a?(Array) && names.all? { |name| name.is_a?(String) || name.is_a?(Symbol) } raise ConfigurationError, "Named scope grants must be an array of names" end names = names.map(&:to_sym) declared.select { |definition| names.include?(definition.name) } end
Declared named scopes intersected with the current independent grant, in declaration order. This performs no domain query.
Source
# File lib/rails_ai_admin/authorization/access.rb, line 146 def operation_fields(operation, record = nil) definition = RailsAiAdmin.operation_registry.fetch(resource, operation) return [] unless definition.custom? names = @adapter.respond_to?(:operation_fields) ? @adapter.operation_fields(context: context, resource: resource, operation: operation.to_sym, record: record) : [] raise ConfigurationError, "Operation fields must be an array" unless names.is_a?(Array) names = names.map(&:to_sym) definition.handler_class.input.select { |field| names.include?(field.name) } end
Source
# File lib/rails_ai_admin/authorization/access.rb, line 64 def project(record, view: nil, only: nil) authorize!(:show, record) granted = permission_fields(:read, record, view: view) visible = view ? granted.select { |field| field.visible_on?(view) } : granted visible = visible.select { |field| only.include?(field.name) } if only Fields::Projection.new(self, record, granted).values(fields: visible) end
only: optional field names to compute (for example index columns plus the title); it can narrow but never add to the granted, visible fields.
Source
# File lib/rails_ai_admin/authorization/access.rb, line 113 def read_attribute(record, field) raise AccessDenied if field.hidden? || field.scalar_type == :password raise ConfigurationError, "Computed fields do not invoke model methods" if field.virtual? return read_file(record, field) if field.scalar_type == :file return read_files(record, field) if field.scalar_type == :files return read_rich_text(record, field) if field.scalar_type == :rich_text return read_references(record, field) if field.scalar_type == :references name = field.column primary_key = record.class.primary_key id_alias = name == "id" && primary_key.is_a?(String) && record.has_attribute?(primary_key) unless record.has_attribute?(name) || id_alias raise ConfigurationError, "Scalar fields must name model attributes" end value = ActiveRecord::Base.while_preventing_writes { record.public_send(name) } # References travel as opaque key text, matching their input contract. field.scalar_type == :belongs_to && !value.nil? ? value.to_s : value rescue ActiveRecord::ReadOnlyError raise ConfigurationError, "Scalar attribute getters must not write", cause: nil end
Source
# File lib/rails_ai_admin/authorization/access.rb, line 103 def read_value(record, field, view: nil) unless resource.fields.any? { |candidate| candidate.equal?(field) } raise ConfigurationError, "Read fields must belong to the current resource" end authorize!(:show, record) granted = permission_fields(:read, record, view: view) raise AccessDenied if view && !field.visible_on?(view) Fields::Projection.new(self, record, granted).value(field) end
Scalar declarations cannot turn arbitrary model methods into read actions. Host overrides of real attribute getters remain trusted application code.
Source
# File lib/rails_ai_admin/authorization/access.rb, line 134 def record_label(record, values: nil, view: :show) fallback = "Record ##{record.id}" authorize!(:show, record) field = fields(:read, record, view: view).find { |candidate| candidate.name == resource.title } return fallback unless field return fallback if values && !values.key?(field.name.to_s) value = values ? values.fetch(field.name.to_s) : read_value(record, field, view: view) field.format(value, time_zone: context.time_zone).truncate(120) rescue AccessDenied fallback end
Shared title contract for native pages, fixed selections and job entries.
Source
# File lib/rails_ai_admin/authorization/access.rb, line 20 def relation relation = @adapter.scope(context: context, resource: resource, relation: resource.model_class.all) unless relation.is_a?(ActiveRecord::Relation) && relation.klass == resource.model_class raise ConfigurationError, "Policy scope must return the resource's ActiveRecord relation" end if relation.limit_value || relation.offset_value || relation.select_values.any? || relation.group_values.any? || !relation.having_clause.empty? || !relation.from_clause.empty? raise ConfigurationError, "Policy scope must be a complete filter relation without limit, offset, select, group, having or from" end relation end
Source
# File lib/rails_ai_admin/authorization/access.rb, line 203 def stored_attachments(record, field) attachment(record, field) ::ActiveStorage::Attachment.where(record_type: record.class.polymorphic_name, record_id: record.id, name: field.column).order(:id) end
The saved attachment rows of a files field, oldest first. Pending attachments of an unsaved change are not included.