Class: LcpRuby::Authorization::PermissionEvaluator

Inherits:
Object
  • Object
show all
Defined in:
lib/lcp_ruby/authorization/permission_evaluator.rb

Constant Summary collapse

ACTION_ALIASES =
{
  "edit" => "update",
  "new" => "create"
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(permission_definition, user, model_name) ⇒ PermissionEvaluator

Returns a new instance of PermissionEvaluator.



6
7
8
9
10
11
12
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 6

def initialize(permission_definition, user, model_name)
  @permission_definition = permission_definition
  @user = user
  @model_name = model_name
  @roles = resolve_roles(user)
  @effective_config = permission_definition.merged_role_config_for(@roles)
end

Instance Attribute Details

#effective_configObject (readonly)

Returns the value of attribute effective_config.



4
5
6
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 4

def effective_config
  @effective_config
end

#model_nameObject (readonly)

Returns the value of attribute model_name.



4
5
6
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 4

def model_name
  @model_name
end

#permission_definitionObject (readonly)

Returns the value of attribute permission_definition.



4
5
6
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 4

def permission_definition
  @permission_definition
end

#rolesObject (readonly)

Returns the value of attribute roles.



4
5
6
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 4

def roles
  @roles
end

#userObject (readonly)

Returns the value of attribute user.



4
5
6
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 4

def user
  @user
end

Instance Method Details

#apply_scope(base_relation) ⇒ Object



139
140
141
142
143
144
145
146
147
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 139

def apply_scope(base_relation)
  scope_config = permission_definition.merged_effective_scope_for(roles)
  return base_relation if scope_config.nil? || scope_config == "all"

  filtered = ScopeBuilder.new(scope_config, user).apply(base_relation)
  model_class = base_relation.respond_to?(:klass) ? base_relation.klass : base_relation
  hint = IncludesHint.build(scope_config, model_class, roles)
  hint ? filtered.includes(*Array.wrap(hint)) : filtered
end

#can?(action) ⇒ Boolean

Returns:

  • (Boolean)


19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 19

def can?(action)
  crud_list = effective_config["crud"]
  return false unless crud_list

  resolved = ACTION_ALIASES[action.to_s] || action.to_s
  granted = crud_list.include?(resolved)

  ActiveSupport::Notifications.instrument("permission.lcp_ruby", {
    model: model_name.to_s,
    result: granted ? "granted" : "denied"
  })

  granted
end

#can_access_presenter?(presenter_name) ⇒ Boolean

Returns:

  • (Boolean)


132
133
134
135
136
137
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 132

def can_access_presenter?(presenter_name)
  presenters = effective_config["presenters"]
  return true if presenters.nil? || presenters == "all"

  Array(presenters).map(&:to_s).include?(presenter_name.to_s)
end

#can_execute_action?(action_name) ⇒ Boolean

Returns:

  • (Boolean)


118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 118

def can_execute_action?(action_name)
  actions_config = effective_config["actions"]
  return true if actions_config == "all"
  return false unless actions_config.is_a?(Hash)

  denied = Array(actions_config["denied"]).map(&:to_s)
  return false if denied.include?(action_name.to_s)

  allowed = actions_config["allowed"]
  return true if allowed == "all"

  Array(allowed).map(&:to_s).include?(action_name.to_s)
end

#can_for_record?(action, record) ⇒ Boolean

Returns:

  • (Boolean)


34
35
36
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
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 34

def can_for_record?(action, record)
  return false unless can?(action)

  resolved = ACTION_ALIASES[action.to_s] || action.to_s

  # Inherited record check: walk inherits_from chain BEFORE evaluating
  # local record_rules. Parent is always asked :show? — visibility is
  # the inheritance semantic; CRUD on the child is governed locally.
  # Skipped for :create because the parent record does not yet exist.
  return false unless inherited_record_check(record, resolved)

  # Check local record-level rules
  permission_definition.record_rules.each do |rule|
    rule = rule.transform_keys(&:to_s) if rule.is_a?(Hash)
    condition = rule["condition"]
    unless condition.is_a?(Hash)
      raise ConditionError,
        "record rule '#{rule['name']}' has invalid condition: expected Hash, got #{condition.class}"
    end
    next unless ConditionEvaluator.evaluate_any(record, condition, context: { current_user: @user })

    denied = (rule.dig("effect", "deny_crud") || []).map(&:to_s)
    except_roles = (rule.dig("effect", "except_roles") || []).map(&:to_s)

    if denied.include?(resolved) && (roles & except_roles).empty?
      return false
    end
  end

  true
end

#field_masked?(field_name) ⇒ Boolean

Returns:

  • (Boolean)


109
110
111
112
113
114
115
116
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 109

def field_masked?(field_name)
  override = permission_definition.field_overrides[field_name.to_s]
  return false unless override && override["masked_for"]

  # Masked only if ALL roles are in masked_for
  masked_roles = override["masked_for"].map(&:to_s)
  roles.all? { |r| masked_roles.include?(r) }
end

#field_readable?(field_name) ⇒ Boolean

Returns:

  • (Boolean)


79
80
81
82
83
84
85
86
87
88
89
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 79

def field_readable?(field_name)
  override = permission_definition.field_overrides[field_name.to_s]
  if override && override["readable_by"]
    return (roles & override["readable_by"].map(&:to_s)).any?
  end

  return true if readable_fields.include?(field_name.to_s)

  # Fallback: custom_data grants access to all custom fields
  custom_field_name?(field_name) && readable_fields.include?("custom_data")
end

#field_writable?(field_name, record = nil) ⇒ Boolean

Returns:

  • (Boolean)


91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 91

def field_writable?(field_name, record = nil)
  override = permission_definition.field_overrides[field_name.to_s]
  if override && override["writable_by"]
    base = (roles & override["writable_by"].map(&:to_s)).any?
    return false unless base
    return record ? !workflow_readonly_for_record(record).include?(field_name.to_s) : true
  end

  return true if writable_fields(record).include?(field_name.to_s)

  # Fallback: custom_data grants access to all custom fields
  custom_field_name?(field_name) && writable_fields(record).include?("custom_data")
end

#readable_fieldsObject



66
67
68
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 66

def readable_fields
  @readable_fields ||= compute_readable_fields
end

#workflow_field_readonly?(field_name, record) ⇒ Boolean

Returns:

  • (Boolean)


105
106
107
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 105

def workflow_field_readonly?(field_name, record)
  workflow_readonly_for_record(record).include?(field_name.to_s)
end

#writable_fields(record = nil) ⇒ Object



70
71
72
73
74
75
76
77
# File 'lib/lcp_ruby/authorization/permission_evaluator.rb', line 70

def writable_fields(record = nil)
  # Unsaved records have id=nil and would all collide on the same cache slot
  # while their workflow-driven readonly set may differ — bypass the cache.
  return compute_writable_fields(record) if record && !record.persisted?

  @writable_fields_cache ||= {}
  @writable_fields_cache[record&.id] ||= compute_writable_fields(record)
end