With this change, we will be able to use both id and ubid in matched_policies. The motivation for this change is to be able to use ubids directly in our web endpoints. If we use ubids, we can avoid passing the actual ids to frontend, and eventually use same serializers for both web and api endpoints.
147 lines
4.7 KiB
Ruby
147 lines
4.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Authorization
|
|
class Unauthorized < CloverError
|
|
def initialize
|
|
super(403, "Forbidden", "Sorry, you don't have permission to continue with this request.")
|
|
end
|
|
end
|
|
|
|
def self.has_permission?(subject_id, actions, object_id)
|
|
!matched_policies(subject_id, actions, object_id).empty?
|
|
end
|
|
|
|
def self.authorize(subject_id, actions, object_id)
|
|
unless has_permission?(subject_id, actions, object_id)
|
|
fail Unauthorized
|
|
end
|
|
end
|
|
|
|
def self.all_permissions(subject_id, object_id)
|
|
matched_policies(subject_id, nil, object_id).flat_map { _1[:actions] }
|
|
end
|
|
|
|
def self.authorized_resources(subject_id, actions)
|
|
matched_policies(subject_id, actions).map { _1[:tagged_id] }
|
|
end
|
|
|
|
def self.expand_actions(actions)
|
|
extended_actions = Set["*"]
|
|
Array(actions).each do |action|
|
|
extended_actions << action
|
|
parts = action.split(":")
|
|
parts[0..-2].each_with_index.each { |_, i| extended_actions << "#{parts[0..i].join(":")}:*" }
|
|
end
|
|
extended_actions.to_a
|
|
end
|
|
|
|
def self.matched_policies(subject_id, actions = nil, object_id = nil)
|
|
object_filter = if object_id
|
|
begin
|
|
Sequel.lit("AND object_applied_tags.tagged_id = ?", UBID.parse(object_id).to_uuid)
|
|
rescue UBIDParseError
|
|
Sequel.lit("AND object_applied_tags.tagged_id = ?", object_id)
|
|
end
|
|
else
|
|
Sequel.lit("")
|
|
end
|
|
|
|
actions_filter = if actions
|
|
Sequel.lit("AND actions ?| array[:actions]", {actions: Sequel.pg_array(expand_actions(actions))})
|
|
else
|
|
Sequel.lit("")
|
|
end
|
|
|
|
DB[<<~SQL, {subject_id: subject_id, actions_filter: actions_filter, object_filter: object_filter}].all
|
|
SELECT object_applied_tags.tagged_id, object_applied_tags.tagged_table, subjects, actions, objects
|
|
FROM accounts AS subject
|
|
JOIN applied_tag AS subject_applied_tags ON subject.id = subject_applied_tags.tagged_id
|
|
JOIN access_tag AS subject_access_tags ON subject_applied_tags.access_tag_id = subject_access_tags.id
|
|
JOIN access_policy AS acl ON subject_access_tags.project_id = acl.project_id
|
|
JOIN jsonb_to_recordset(acl.body->'acls') as items(subjects JSONB, actions JSONB, objects JSONB) ON TRUE
|
|
JOIN access_tag AS object_access_tags ON subject_access_tags.project_id = object_access_tags.project_id
|
|
JOIN applied_tag AS object_applied_tags ON object_access_tags.id = object_applied_tags.access_tag_id AND objects ? object_access_tags."name"
|
|
WHERE subject.id = :subject_id
|
|
AND subjects ? subject_access_tags."name"
|
|
:actions_filter
|
|
:object_filter
|
|
SQL
|
|
end
|
|
|
|
def self.generate_default_acls(subject, object)
|
|
{
|
|
acls: [
|
|
{subjects: [subject], actions: ["*"], objects: [object]}
|
|
]
|
|
}
|
|
end
|
|
|
|
module Dataset
|
|
def authorized(subject_id, actions)
|
|
# We can't use "id" column directly, because it's ambiguous in big joined queries.
|
|
# We need to determine table of id explicitly.
|
|
# @opts is the hash of options for this dataset, and introduced at Sequel::Dataset.
|
|
from = @opts[:from].first
|
|
where { {Sequel[from][:id] => Authorization.authorized_resources(subject_id, actions)} }
|
|
end
|
|
end
|
|
|
|
module HyperTagMethods
|
|
def self.included(base)
|
|
base.class_eval do
|
|
many_to_many :projects, join_table: AccessTag.table_name, left_key: :hyper_tag_id, right_key: :project_id
|
|
end
|
|
end
|
|
|
|
def hyper_tag_name(project = nil)
|
|
raise NoMethodError
|
|
end
|
|
|
|
def hyper_tag(project)
|
|
AccessTag.where(project_id: project.id, hyper_tag_id: id).first
|
|
end
|
|
|
|
def associate_with_project(project)
|
|
return if project.nil?
|
|
|
|
DB.transaction do
|
|
self_tag = AccessTag.create_with_id(
|
|
project_id: project.id,
|
|
name: hyper_tag_name(project),
|
|
hyper_tag_id: id,
|
|
hyper_tag_table: self.class.table_name
|
|
)
|
|
project_tag = project.hyper_tag(project)
|
|
tag(self_tag)
|
|
tag(project_tag) if self_tag.id != project_tag.id
|
|
self_tag
|
|
end
|
|
end
|
|
|
|
def dissociate_with_project(project)
|
|
return if project.nil?
|
|
|
|
DB.transaction do
|
|
project_tag = project.hyper_tag(project)
|
|
untag(project_tag)
|
|
hyper_tag(project).destroy
|
|
end
|
|
end
|
|
end
|
|
|
|
module TaggableMethods
|
|
def self.included(base)
|
|
base.class_eval do
|
|
many_to_many :applied_access_tags, class: AccessTag, join_table: AppliedTag.table_name, left_key: :tagged_id, right_key: :access_tag_id
|
|
end
|
|
end
|
|
|
|
def tag(access_tag)
|
|
AppliedTag.create(access_tag_id: access_tag.id, tagged_id: id, tagged_table: self.class.table_name)
|
|
end
|
|
|
|
def untag(access_tag)
|
|
AppliedTag.where(access_tag_id: access_tag.id, tagged_id: id).destroy
|
|
end
|
|
end
|
|
end
|