Class: LcpRuby::Metadata::PermissionDefinition

Inherits:
Object
  • Object
show all
Defined in:
lib/lcp_ruby/metadata/permission_definition.rb

Constant Summary collapse

DENY_ALL_ROLE =

Sentinel role name used by ‘.deny_all` factory. Never a real role —`roles` is always absent, so every CRUD/action/scope query falls through to `role_config_for`’s empty ‘{}` fallback and returns false. Visible in error logs and PolicyFactory traces, so the fail-closed state is self-documenting (vs. relying on `default_role: nil` accidentally fail-closing via the constructor’s nil-coercion to “viewer”).

"__deny_all__"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attrs = {}) ⇒ PermissionDefinition

Returns a new instance of PermissionDefinition.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 17

def initialize(attrs = {})
  @model = attrs[:model].to_s
  @roles = attrs[:roles] || {}
  @default_role = attrs[:default_role] || "viewer"
  @field_overrides = attrs[:field_overrides] || {}
  @record_rules = attrs[:record_rules] || []
  @inherits_from = normalize_inherits(attrs[:inherits_from])
  @raw_hash = attrs[:raw_hash]
  @source_path = attrs[:source_path]
  @source_type = attrs[:source_type]
  # Per-entry provenance (`:per_model` / `:default`) populated only on
  # merged definitions by Metadata::PermissionMerger. nil otherwise.
  @source_map = attrs[:source_map]
end

Instance Attribute Details

#default_roleObject (readonly)

Returns the value of attribute default_role.



13
14
15
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 13

def default_role
  @default_role
end

#field_overridesObject (readonly)

Returns the value of attribute field_overrides.



13
14
15
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 13

def field_overrides
  @field_overrides
end

#inherits_fromObject (readonly)

Returns the value of attribute inherits_from.



13
14
15
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 13

def inherits_from
  @inherits_from
end

#modelObject (readonly)

Returns the value of attribute model.



13
14
15
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 13

def model
  @model
end

#raw_hashObject (readonly)

Returns the value of attribute raw_hash.



13
14
15
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 13

def raw_hash
  @raw_hash
end

#record_rulesObject (readonly)

Returns the value of attribute record_rules.



13
14
15
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 13

def record_rules
  @record_rules
end

#rolesObject (readonly)

Returns the value of attribute roles.



13
14
15
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 13

def roles
  @roles
end

#source_mapObject (readonly)

Returns the value of attribute source_map.



13
14
15
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 13

def source_map
  @source_map
end

#source_pathObject

Returns the value of attribute source_path.



13
14
15
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 13

def source_path
  @source_path
end

#source_typeObject

Returns the value of attribute source_type.



13
14
15
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 13

def source_type
  @source_type
end

Class Method Details

.deny_all(model_name) ⇒ Object

Builds a fail-closed PermissionDefinition: empty roles hash plus the ‘DENY_ALL_ROLE` sentinel default_role (see constant docstring above for why a sentinel rather than `nil`).

See docs/design/authorization_hardening.md § “PermissionDefinition.deny_all”.



59
60
61
62
63
64
65
66
67
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 59

def self.deny_all(model_name)
  new(
    model: model_name,
    roles: {},
    default_role: DENY_ALL_ROLE,
    field_overrides: {},
    record_rules: []
  )
end

.from_hash(hash) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 41

def self.from_hash(hash)
  new(
    model: hash["model"],
    roles: hash["roles"] || {},
    default_role: hash["default_role"],
    field_overrides: hash["field_overrides"] || {},
    record_rules: hash["record_rules"] || [],
    inherits_from: hash["inherits_from"],
    raw_hash: hash
  )
end

.rule_name(rule) ⇒ Object

Read a record_rule’s ‘name:` regardless of whether the hash uses string or symbol keys. Returns nil for non-Hash entries (defensive for malformed YAML that the schema validator already rejects).



72
73
74
75
76
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 72

def self.rule_name(rule)
  return nil unless rule.is_a?(Hash)

  rule["name"] || rule[:name]
end

Instance Method Details

#default?Boolean

Returns:

  • (Boolean)


139
140
141
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 139

def default?
  model == "_default"
end

#effective_scope_for(role_name) ⇒ Object

Returns the effective scope hash for a single role, applying inheritance fallback when the role does not define its own scope.

Override semantics:

role.key?("scope") == false  → inherit (synthesize "inherits" spec)
role["scope"] == "all"       → opt out, no inheritance, sees all
role["scope"] == Hash        → role-level override, no inheritance
role["scope"] == nil         → REJECTED at validation time


107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 107

def effective_scope_for(role_name)
  role = role_config_for(role_name)
  return role["scope"] if role.key?("scope")
  return nil unless inherits_from

  # Synthesize an internal scope spec that ScopeBuilder turns into a
  # parent-sub-query. This Hash never appears in user-supplied YAML;
  # the schema validator rejects type=inherits in user input.
  {
    "type" => "inherits",
    "parent" => inherits_from[:parents],
    "via" => inherits_from[:via],
    "mode" => inherits_from[:mode]
  }
end

#extendsObject

‘extends:` is read from raw_hash on demand. raw_hash is the authoritative source of truth (key presence distinguishes “author wrote nothing” from “author wrote `_default`”); see Metadata::PermissionMerger#merge_default_role for the same rule applied to default_role.



37
38
39
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 37

def extends
  @raw_hash&.[]("extends")
end

#merged_effective_scope_for(role_names) ⇒ Object

Computes the effective scope for a set of roles, applying both inheritance fallback and OR-merge semantics for multi-role users. Returns one of:

nil     — no role defines a scope and inherits_from is absent
"all"   — at least one role grants unrestricted access
Hash    — a single scope spec (path/field_match/.../inherits)
{type:"union", scopes:[...]} — multiple distinct scopes to OR


130
131
132
133
134
135
136
137
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 130

def merged_effective_scope_for(role_names)
  scopes = role_names.map { |r| effective_scope_for(r) }.compact
  return "all" if scopes.any? { |s| s == "all" }
  return nil if scopes.empty?
  return scopes.first if scopes.size == 1

  { "type" => "union", "scopes" => scopes }
end

#merged_role_config_for(role_names) ⇒ Object

Merge configs from multiple roles using union/most-permissive semantics. Returns a single virtual config hash.

NOTE: the returned hash no longer carries a “scope” key. Scope resolution now flows exclusively through ‘merged_effective_scope_for`, which knows how to incorporate `inherits_from` fallback. Callers that previously read `effective_config` must migrate to `merged_effective_scope_for(roles)`.



90
91
92
93
94
95
96
97
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 90

def merged_role_config_for(role_names)
  configs = role_names.map { |r| role_config_for(r) }
  configs = configs.reject(&:empty?)
  return roles[default_role.to_s] || {} if configs.empty?
  return configs.first if configs.size == 1

  merge_configs(configs)
end

#role_config_for(role_name) ⇒ Object



78
79
80
# File 'lib/lcp_ruby/metadata/permission_definition.rb', line 78

def role_config_for(role_name)
  roles[role_name.to_s] || roles[default_role.to_s] || {}
end