Class: Labimotion::KlassShare

Inherits:
ApplicationRecord
  • Object
show all
Defined in:
lib/labimotion/models/klass_share.rb

Overview

One user's right on one template (template-sharing.md §5). The owner row (permission_level 30) is the transferable right; the klass's created_by column stays immutable provenance. Deliberately NOT acts_as_paranoid, and the klass models carry no dependent: towards it: deleting a template must leave its shares alone, so a restored template comes back owned. Wiping the rows on delete would make restore itself a privilege escalation — the template would return owner-less and fall open to the legacy designer-wide gate.

Constant Summary collapse

LEVELS =

Strictly nested ladder (requested ⊂ viewer ⊂ editor ⊂ owner), which is what makes >= threshold checks valid. Spaced by ten so a future level is additive; never renumber — the one-owner-per-klass partial index freezes permission_level = 30 into the schema.

requested is the odd one: it grants nothing. It is how an access request is stored — a row rather than a message, because the last hop of the request loop lands on the Designer, which polls no message list, so a message nobody sees is no notification at all. As a row it survives acknowledgement, granting and rejecting become ordinary state changes on it, and the unique (klass_type, klass_id, shared_with_id) index dedupes repeat asks for free. 0 >= 10 is false, so every threshold check refuses it without a special case.

{ requested: 0, viewer: 10, editor: 20, owner: 30 }.freeze
SHAREABLE_USER_TYPES =

Which account types may hold a share row. Admin is in the list because both seeding paths have to reach the same verdict: the seed migration is raw SQL over created_by and does write an owner row naming an administrator, while this validation used to refuse the runtime self-grant for the same actor and leave the template owner-less — the worse of the two, since owner-less falls back to the permanent legacy gate where any designer of the family may edit and delete it. Not hypothetical: the backfill_klass_created_columns migration fills a NULL created_by with the first system administrator, so a real population of templates is admin-created. Group and DeviceDeprecated stay out: a group is not an individual, and ELN reserves Group for device management.

%w[Person Admin].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.key_of(klass) ⇒ Object



77
78
79
# File 'lib/labimotion/models/klass_share.rb', line 77

def self.key_of(klass)
  "#{klass.class.name.split('::').last}:#{klass.id}"
end

.owner_row_for(klass) ⇒ Object



81
82
83
84
# File 'lib/labimotion/models/klass_share.rb', line 81

def self.owner_row_for(klass)
  for_klass(klass.class.name.split('::').last, klass.id)
    .find_by(permission_level: LEVELS[:owner])
end

.seed_owner!(klass, user_id) ⇒ Object

Writes the owner self-grant a fresh klass gets on every create path — bookkeeping, not enforcement, so it is unconditional but must never break the create it rides on: a host that has not migrated yet simply skips it (log only), and a concurrent duplicate means the owner row already exists, which is the goal state.



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/labimotion/models/klass_share.rb', line 90

def self.seed_owner!(klass, user_id)
  return unless table_exists?
  return if owner_row_for(klass).present?

  create!(klass_type: klass.class.name.split('::').last, klass_id: klass.id,
          shared_with_id: user_id, created_by: user_id,
          permission_level: LEVELS[:owner])
rescue ActiveRecord::RecordNotUnique
  nil
rescue ActiveRecord::RecordInvalid => e
  # An Admin creator no longer lands here — SHAREABLE_USER_TYPES admits it, which is the
  # point of the allowlist. What still can: a `created_by` naming an excluded type or an
  # account that no longer exists at all. The klass then stays owner-less on the legacy
  # gate, logged, rather than taking the create down with it.
  Labimotion.log_exception(e)
  nil
end

Instance Method Details

#levelObject

The integer, whatever the reader: the AR enum getter answers the label string, a bare test double answers the raw integer.



68
69
70
71
# File 'lib/labimotion/models/klass_share.rb', line 68

def level
  raw = permission_level
  raw.is_a?(Integer) ? raw : LEVELS.fetch(raw.to_sym)
end