Module: Familia::Features::Relationships::PermissionManagement::ClassMethods

Defined in:
lib/familia/features/relationships/permission_management.rb

Overview

Relationships::ClassMethods

Instance Method Summary collapse

Instance Method Details

#permission_tracking(field_name = :permissions) ⇒ Object

Enable permission tracking for this class

Examples:

class Document < Familia::Horreum
  permission_tracking :user_permissions
end

Parameters:

  • field_name (Symbol) (defaults to: :permissions)

    Name of the hash field to store permissions



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/familia/features/relationships/permission_management.rb', line 37

def permission_tracking(field_name = :permissions)
  # Define a hashkey for storing per-user permissions
  hashkey field_name

  # Grant permissions to a user for this object
  #
  # @param user [Object] User or user identifier
  # @param permissions [Array<Symbol>] Permissions to grant
  #
  # @example
  #   document.grant(user, :read, :write, :edit)
  define_method :grant do |user, *permissions|
    user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s

    # Get current score from any sorted set this object belongs to
    # For simplicity, we'll create a new timestamp-based score
    current_time = Time.now
    new_score = ScoreEncoding.encode_score(current_time, permissions)

    # Store permission bits in hash for quick lookup
    decoded = ScoreEncoding.decode_score(new_score)
    send(field_name)[user_key] = decoded[:permissions]
  end

  # Revoke permissions from a user for this object
  #
  # @param user [Object] User or user identifier
  # @param permissions [Array<Symbol>] Permissions to revoke
  #
  # @example
  #   document.revoke(user, :write, :edit)
  define_method :revoke do |user, *permissions|
    user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
    current_bits = send(field_name)[user_key].to_i

    # Remove the specified permission bits
    new_bits = permissions.reduce(current_bits) do |acc, perm|
      acc & ~(ScoreEncoding::PERMISSION_FLAGS[perm] || 0)
    end

    if new_bits.zero?
      send(field_name).remove_field(user_key)
    else
      send(field_name)[user_key] = new_bits
    end
  end

  # Check if user has specific permissions for this object
  #
  # @param user [Object] User or user identifier
  # @param permissions [Array<Symbol>] Permissions to check
  # @return [Boolean] True if user has all specified permissions
  #
  # @example
  #   document.can?(user, :read, :write)  #=> true
  define_method :can? do |user, *permissions|
    user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
    bits = send(field_name)[user_key].to_i

    permissions.all? do |perm|
      flag = ScoreEncoding::PERMISSION_FLAGS[perm]
      flag && bits.anybits?(flag)
    end
  end

  # Get all permissions for a user on this object
  #
  # @param user [Object] User or user identifier
  # @return [Array<Symbol>] Array of permission symbols
  #
  # @example
  #   document.permissions_for(user)  #=> [:read, :write, :edit]
  define_method :permissions_for do |user|
    user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
    bits = send(field_name)[user_key].to_i
    ScoreEncoding.decode_permission_flags(bits)
  end

  # Add permissions to existing user permissions
  #
  # @param user [Object] User or user identifier
  # @param permissions [Array<Symbol>] Permissions to add
  #
  # @example
  #   document.add_permission(user, :delete, :transfer)
  define_method :add_permission do |user, *permissions|
    user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
    current_bits = send(field_name)[user_key].to_i

    # Add the specified permission bits
    new_bits = permissions.reduce(current_bits) do |acc, perm|
      acc | (ScoreEncoding::PERMISSION_FLAGS[perm] || 0)
    end

    send(field_name)[user_key] = new_bits
  end

  # Set exact permissions for a user (replaces existing)
  #
  # @param user [Object] User or user identifier
  # @param permissions [Array<Symbol>] Permissions to set
  #
  # @example
  #   document.set_permissions(user, :read, :write)
  define_method :set_permissions do |user, *permissions|
    user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s

    if permissions.empty?
      send(field_name).remove_field(user_key)
    else
      permission_bits = permissions.reduce(0) do |acc, perm|
        acc | (ScoreEncoding::PERMISSION_FLAGS[perm] || 0)
      end
      send(field_name)[user_key] = permission_bits
    end
  end

  # Get all users and their permissions for this object
  #
  # @return [Hash] Hash mapping user keys to permission arrays
  #
  # @example
  #   document.all_permissions
  #   #=> { "user123" => [:read, :write], "user456" => [:read] }
  define_method :all_permissions do
    permissions_hash = send(field_name).hgetall
    permissions_hash.transform_values do |bits|
      ScoreEncoding.decode_permission_flags(bits.to_i)
    end
  end

  # Remove all permissions for all users on this object
  #
  # @example
  #   document.clear_all_permissions
  define_method :clear_all_permissions do
    send(field_name).clear
  end

  # === Two-Stage Filtering Methods ===

  # Stage 1: Redis pre-filtering via zset membership
  define_method :accessible_items do |collection_key|
    self.class.dbclient.zrange(collection_key, 0, -1, with_scores: true)
  end

  # Stage 2: Broad categorical filtering on small sets
  define_method :items_by_permission do |collection_key, category = :readable|
    items_with_scores = accessible_items(collection_key)

    # Operating on ~20-100 items, not millions
    filtered = items_with_scores.select do |(_member, score)|
      ScoreEncoding.category?(score, category)
    end

    filtered.map(&:first) # Return just the members
  end

  # Bulk permission check for UI rendering
  define_method :permission_matrix do |collection_key|
    items_with_scores = accessible_items(collection_key)

    {
      total: items_with_scores.size,
      viewable: items_with_scores.count { |(_, s)| ScoreEncoding.category?(s, :readable) },
      editable: items_with_scores.count { |(_, s)| ScoreEncoding.category?(s, :content_editor) },
      administrative: items_with_scores.count { |(_, s)| ScoreEncoding.category?(s, :administrator) }
    }
  end

  # Efficient "can perform any administrative action?" check
  # Note: Currently checks if this object has admin privileges in the collection.
  # The user parameter is reserved for future user-specific permission checking.
  define_method :admin_access? do |_user, collection_key|
    score = self.class.dbclient.zscore(collection_key, identifier)
    return false unless score

    ScoreEncoding.category?(score, :administrator)
  end

  # === Categorical Permission Methods ===

  # Check permission category for user
  #
  # @param user [Object] User object to check category for
  # @param category [Symbol] Category to check (:readable, :content_editor, etc.)
  # @return [Boolean] True if user meets the category requirements
  # @example Check if user has content editor permissions
  #   document.category?(user, :content_editor)  #=> true
  define_method :category? do |user, category|
    user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
    bits = send(field_name)[user_key].to_i
    ScoreEncoding.meets_category?(bits, category)
  end

  # Get permission tier for user
  #
  # @param user [Object] User object to get tier for
  # @return [Symbol] Permission tier (:administrator, :content_editor, :viewer, :none)
  # @example Get user's permission tier
  #   document.permission_tier_for(user)  #=> :content_editor
  define_method :permission_tier_for do |user|
    user_key = user.respond_to?(:identifier) ? user.identifier : user.to_s
    bits = send(field_name)[user_key].to_i

    # Create a temporary score to use ScoreEncoding.permission_tier
    temp_score = ScoreEncoding.encode_score(Time.now, bits)
    ScoreEncoding.permission_tier(temp_score)
  end

  # Get users by permission category
  #
  # @param category [Symbol] Category to filter by
  # @return [Array<String>] Array of user keys with the specified category
  # @example Get all content editors
  #   document.users_by_category(:content_editor)  #=> ["user123", "user456"]
  define_method :users_by_category do |category|
    permissions_hash = send(field_name).hgetall
    permissions_hash.select do |_user_key, bits|
      ScoreEncoding.meets_category?(bits.to_i, category)
    end.keys
  end
end