Skip to content

APT-1935: Use SpiceDB for lease-related auth checks #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 175 additions & 4 deletions lib/declarative_authorization/authorization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,23 +186,194 @@ def permit!(privilege, options = {})
attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
rules = matching_auth_rules(roles, privileges, options[:context])

# Test each rule in turn to see whether any one of them is satisfied.
# _____ _ ____ ____
# / ___| (_) | _ \| _ \
# \ `--. _ __ _ ___ ___| | | | |_) |
# `--. \ '_ \| |/ __/ _ \ | | | _ <
# /\__/ / |_) | | (_| __/ |_| | |_) |
# \____/| .__/|_|\___\___|____/|____/
# | |
# |_|
use_spicedb_auth = false

rules.each do |rule|
return true if rule.validate?(attr_validator, options[:skip_attribute_test])
unless rule.role.to_s.start_with?("leases__", "lease_renewals__")
# Existing behavior for non-lease-related rules
return true if rule.validate?(attr_validator, options[:skip_attribute_test])
next
end

use_spicedb_auth = true

auth_service_class = Rails.application.config.try(:spicedb_authorization_service)
@auth_service = auth_service_class.new

vhost_id = Core::Company.guid if defined?(Core::Company)
raise StandardError, "Vhost ID not found" if vhost_id.nil?

puts "\n==== Processing new rule ===="
puts "Rule: #{rule.inspect}"
puts "Role: #{rule.role}"
puts "Join Operator: #{rule.join_operator}" if rule.respond_to?(:join_operator)

permission_to_check = rule.role.to_s.gsub("__", "_") + "_permission"
puts "Permission to check: #{permission_to_check}"

if rule.attributes.empty?
puts "Rule has no attributes, checking spicedb directly"

authorized = @auth_service.check_permission(
resource: { type: "vhost", id: vhost_id },
permission: permission_to_check
)
puts "Authorized? #{authorized}"

return true if authorized
else
puts "Rule has #{rule.attributes.count} attributes, examining them:"

matching_attributes_count = 0

rule.attributes.each_with_index do |attribute, index|
puts "\n -- Attribute ##{index + 1}: #{attribute.inspect}"

if attribute.instance_variable_defined?('@conditions_hash')
conditions = attribute.instance_variable_get('@conditions_hash')
puts " Conditions hash: #{conditions.inspect}"
else
puts " !! No conditions_hash instance variable found, skipping"
next
end

next unless conditions.is_a?(Hash)

puts " Checking #{conditions.count} conditions against current values:"
matching_conditions_count = 0
any_condition_failed = false

if conditions.key?(:granular_permissions)
rule_requires = conditions[:granular_permissions][1]
actual_value = options[:object]&.granular_permissions if options[:object].respond_to?(:granular_permissions)

puts " Granular permissions - Rule requires: #{rule_requires}, Actual: #{actual_value}"
if rule_requires == actual_value
puts " ✓ Granular permissions condition matched!"
matching_conditions_count += 1
else
puts " ✗ Granular permissions condition did not match"
any_condition_failed = true
end
end

if conditions.key?(:is_renewal) && !any_condition_failed
rule_requires = conditions[:is_renewal][1]
# If options[:object] has is_renewal, we'll use that value instead of obtaining it from SpiceDB.
# This allows for Blue Moon leases and others to be checked without having to make a SpiceDB call for the value of is_renewal.
if options[:object].respond_to?(:is_renewal)
actual_value = options[:object].is_renewal
else
# Check if the lease document is a renewal. Ideally we implement a method in the auth service that is better
# suited for this kind of check, but for the POC we'll just use lookup_subjects since it does what we need.
puts " Checking SpiceDB for `renewal` relation on lease document uuid: #{options[:object]&.lease_document_uuid.to_s}"
response = @auth_service.lookup_subjects(
resource_type: "lease_document",
resource_id: options[:object]&.lease_document_uuid.to_s,
permission: "renewal",
subject_type: "lease_document"
)
actual_value = response.any?
end

puts " Is renewal - Rule requires: #{rule_requires}, Actual: #{actual_value}"
if rule_requires == actual_value
puts " ✓ Is_renewal condition matched!"
matching_conditions_count += 1
else
puts " ✗ Is_renewal condition did not match"
any_condition_failed = true
end
end

if conditions.key?(:id) && !any_condition_failed
condition_type = conditions[:id][0]
condition_proc = conditions[:id][1]

if condition_type == :id_in_scope && condition_proc.is_a?(Proc)
puts " Checking Occupancy ID in scope condition"
user_has_access_to_occupancy = attribute.validate?(attr_validator)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When calling attribute.validate method, the condition_proc is executed, which returns a list of Occupancy ids the user has access to. This logic exists in the visible_by_conditions method here

puts " User has access to occupancy? #{user_has_access_to_occupancy}"
if user_has_access_to_occupancy
puts " ✓ This attribute matched conditions"
matching_conditions_count += 1
else
puts " ✗ This attribute did not match conditions"
any_condition_failed = true
end
end
end

if matching_conditions_count == conditions.count
puts " ✓ All conditions matched for this attribute"
matching_attributes_count += 1
else
puts " ✗ Not all conditions matched for this attribute"
end
end

puts " Finished checking attributes, checking if rule is authorized"

if rule.respond_to?(:join_operator) && rule.join_operator == :and
puts " Rule uses AND operator, checking if all attributes matched: #{matching_attributes_count == rule.attributes.count}"
if matching_attributes_count == rule.attributes.count
puts " All attributes matched, checking SpiceDB permission"
authorized = @auth_service.check_permission(
resource: { type: "vhost", id: vhost_id },
permission: permission_to_check
)
puts " Authorized? #{authorized}"
return true if authorized
else
puts " Not all attributes matched, authorization denied for this rule"
end
else
puts " Rule uses OR operator (default), checking if any attribute matched: #{matching_attributes_count > 0}"
if matching_attributes_count > 0
puts " At least one attribute matched, checking SpiceDB permission"
authorized = @auth_service.check_permission(
resource: { type: "vhost", id: vhost_id },
permission: permission_to_check
)
puts " Authorized? #{authorized}"
return true if authorized
else
puts " No attributes matched, authorization denied for this rule"
end
end
end
end

source_prefix = use_spicedb_auth ? "SpiceDB authorization failed. " : ""

if options[:bang]
if rules.empty?
raise NotAuthorized, "No matching rules found for #{privilege} for User with id #{user.try(:id)} " +
"(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
"context #{options[:context].inspect})."
else
raise AttributeAuthorizationError, "#{privilege} not allowed for User with id #{user.try(:id)} on #{(options[:object] || options[:context]).inspect}."
raise AttributeAuthorizationError, "#{source_prefix}#{privilege} not allowed for User with id #{user.try(:id)} " +
"on #{(options[:object] || options[:context]).inspect}."
end
else
false
end
end
end
# _____ _ _ _____
# | ___| | \ | | | _ |
# | |__ | \| | | | | |
# | __| | . ` | | | | |
# | |___ | |\ | \ \_/ /
# \____/ \_| \_/ \___/


# Calls permit! but doesn't raise authorization errors. If no exception is
# raised, permit? returns true and yields to the optional block.
Expand Down