A project can have multiple access policies, and each access policy can have multiple entries. Each entry in an access policy can convert into an access control entry. If there are multiple subjects, actions, or objects in the access policy entry, a tag can be created for each, and that tag can be used in the access control entry. This breaks the process down into three separate methods: * parse_access_policy_rows: parses the access_policy table rows into arrays of uuids, one for each access control entry. The subject, action, and object entries in each array can themselves be arrays of uuids. Also populates :admin and :member keys for projects. These keys will be used to populate the Admin and Member subject tags for the projects. * convert_access_policy_rows: calls parse_access_policy_rows, and further converts the entries, returning an unsaved AccessControlEntry for each. The subject_id, action_id, and object_id values, if they need to be tags, are arrays, with the unsaved tag as the first element, and an array of checked member uuids as the second element. This removes the :admin and :member keys and uses them to populate the unsaved Admin and Member subject tags. Admin and Member subject tags will be created even if they would be empty, though empty Admin subject tags will be added to the failures_array. * save_converted_access_policy_rows: calls convert_access_policy_rows, and saves each access control entry. If the access control entry references tags, saves the tags first. For all three methods, they return a hash with :projects and :failures keys. The :projects value is a hash with :aces values described above. The :failures key is an array of arrays of failure information, which can be used for debugging. If the AccessPolicy body has only a single entry, and all actions are allowed, treat it as admin. This is not perfect, as projects can have multiple AccessPolicies, and one could have one entry that allows all actions to a subset of objects, with the intention being no access to other objects. However, the vast majority of projects do not have admin managed policies, so there needs to be a way to convert non-managed admin-style policies to members of the generated Admin subject tags. Treat users with Project:policy action as admins. This action is not used, but was used by default at some point in the past. If the user could modify the policy, they are de facto an admin of the system, since they could modify the policy to grant any action they needed. If there is no project admin, but only a single account associated with the project, make that account the project admin. Do not use an exact match for member policies, because the InferenceEndpoint:view was recently added for new policies, but existing policies were not updated for it. For ApiKey subjects, include them in the admin tag, but do not allow the to be in other tags. Convert them from being in other tags to having separate ACEs just for the APIKey. For subjects with admin access, do not create separate ACEs just for them, since they don't need it if they have admin access. This mostly affects APIKeys, since those have individual APIKey entries, though it could affect Accounts as well. Ignore soft-deleted projects and projects without accounts when doing the conversion. No specs for this, it will only be used once to convert access policies to access control entries.
208 lines
7.8 KiB
Ruby
208 lines
7.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module AccessPolicyConverter
|
|
# :nocov:
|
|
def self.convert_to_tag(klass, project_id, name, values, failures)
|
|
# No need to build single-element tags
|
|
return values unless values.is_a?(Array)
|
|
|
|
tag = klass.new_with_id(project_id:, name:)
|
|
old_values = values
|
|
values, issues = tag.check_members_to_add(values)
|
|
unless issues.empty?
|
|
failures << [:member_check_failed, project_id, name, klass.name, issues, old_values - values]
|
|
end
|
|
[tag, values]
|
|
end
|
|
|
|
def self.save_converted_access_policy_rows(rows)
|
|
projects, failures = convert_access_policy_rows(rows).values_at(:projects, :failures)
|
|
keys = [:subject_id, :action_id, :object_id]
|
|
projects.each do |project_id, hash|
|
|
hash[:aces] = hash[:aces].map do |ace|
|
|
keys.each do |key|
|
|
value = ace.send(key)
|
|
if value.is_a?(Array)
|
|
tag, values = value
|
|
tag.save_changes
|
|
tag.add_members(values)
|
|
ace[key] = tag.id
|
|
end
|
|
end
|
|
ace.save_changes
|
|
end
|
|
end
|
|
{projects:, failures:}
|
|
end
|
|
|
|
def self.convert_access_policy_rows(rows)
|
|
projects, failures = parse_access_policy_rows(rows).values_at(:projects, :failures)
|
|
keys = [:subject_id, :action_id, :object_id]
|
|
projects.each do |project_id, hash|
|
|
aces = hash[:aces] = hash[:aces].map do |ap_id, i, subject_id, action_id, object_id|
|
|
tag_name = "Access-Policy-#{ap_id}-#{i}"
|
|
subject_id = convert_to_tag(SubjectTag, project_id, tag_name, subject_id, failures)
|
|
action_id = convert_to_tag(ActionTag, project_id, tag_name, action_id, failures)
|
|
object_id = convert_to_tag(ObjectTag, project_id, tag_name, object_id, failures)
|
|
ace = AccessControlEntry.new_with_id(project_id:, subject_id:, action_id:, object_id:)
|
|
subject_id, action_id, object_id = ace.values.values_at(:subject_id, :action_id, :object_id)
|
|
keys.each do |column|
|
|
ace[column] = nil if ace[column].is_a?(Array)
|
|
end
|
|
unless ace.valid?
|
|
ace.errors.delete(:subject_id) if ace.subject_id.nil?
|
|
unless ace.errors.empty?
|
|
failures << [:ace_validation_failed, project_id, ap_id, i, subject_id, action_id, object_id, ace.errors]
|
|
end
|
|
end
|
|
ace.subject_id, ace.action_id, ace.object_id = subject_id, action_id, object_id
|
|
ace
|
|
end
|
|
|
|
admin_tag = convert_to_tag(SubjectTag, project_id, "Admin", hash[:admin], failures)
|
|
aces << AccessControlEntry.new_with_id(project_id:, subject_id: admin_tag)
|
|
|
|
member_tag = convert_to_tag(SubjectTag, project_id, "Member", hash[:member], failures)
|
|
aces << AccessControlEntry.new_with_id(project_id:, subject_id: member_tag, action_id: "ffffffff-ff00-834a-87ff-ff828ea2dd80") # Member global action tag
|
|
hash.delete(:admin)
|
|
hash.delete(:member)
|
|
end
|
|
{projects:, failures:}
|
|
end
|
|
|
|
def self.parse_access_policy_rows(rows)
|
|
projects = {}
|
|
failures = []
|
|
action_map = {
|
|
"Postgres:Firewall:edit" => "ffffffff-ff00-835a-87ff-f05a007343a0",
|
|
"Postgres:Firewall:view" => "ffffffff-ff00-835a-87ff-f05a00d85dc0",
|
|
"Project:*" => "ffffffff-ff00-834a-87ff-ff82d2028210",
|
|
"*" => Sequel::NULL,
|
|
"Vm:*" => "ffffffff-ff00-834a-87ff-ff8374028210",
|
|
"PrivateSubnet:*" => "ffffffff-ff00-834a-87ff-ff82d9028210",
|
|
"Firewall:*" => "ffffffff-ff00-834a-87ff-ff81fc028210",
|
|
"LoadBalancer:*" => "ffffffff-ff00-834a-87ff-ff802b028210",
|
|
"Postgres:*" => "ffffffff-ff00-834a-87ff-ff82d0028210"
|
|
}.merge(ActionType::NAME_MAP)
|
|
name_map = {}
|
|
account_project_set = {}
|
|
DB[:access_tag].select_map([:project_id, :name, :hyper_tag_id, :hyper_tag_table]).each do |project_id, name, hyper_tag_id, table|
|
|
(name_map[project_id] ||= {})[name] = hyper_tag_id
|
|
account_project_set[project_id] ||= true if table == "accounts"
|
|
end
|
|
inactive_project_set = DB[:project].exclude(:visible).select_hash(:id, Sequel[true].as(:v))
|
|
admin_array = ["*"]
|
|
member_array = ["Vm:*", "PrivateSubnet:*", "Firewall:*", "Postgres:*", "Project:view", "Project:github"]
|
|
|
|
project_id = nil
|
|
ap_id = nil
|
|
convert = lambda do |i, acl, type, map|
|
|
values = acl[type]
|
|
unless values.is_a?(Array)
|
|
failures << [:not_array, project_id, ap_id, i, type]
|
|
next []
|
|
end
|
|
values = values.map do
|
|
unless (value_id = map[_1])
|
|
failures << [:unrecognized_name, project_id, ap_id, i, type, _1]
|
|
end
|
|
value_id
|
|
end
|
|
values.compact!
|
|
values
|
|
end
|
|
|
|
rows.each do |row|
|
|
ap_id, project_id, name, body, managed = row.values_at(:id, :project_id, :name, :body, :managed)
|
|
raise unless project_id
|
|
next if inactive_project_set[project_id] # Ignore soft-deleted projects
|
|
next unless account_project_set[project_id] # Ignore projects without accounts
|
|
|
|
acls = body["acls"]
|
|
|
|
if managed
|
|
case name
|
|
when "admin"
|
|
is_admin = true
|
|
unless acls.length == 1 && acls[0]["actions"] == admin_array
|
|
failures << [:bad_admin_policy, project_id, ap_id, name]
|
|
next
|
|
end
|
|
when "member"
|
|
is_member = true
|
|
unless acls.length == 1 && member_array.all? { acls[0]["actions"].include?(_1) }
|
|
failures << [:bad_member_policy, project_id, ap_id, name, acls]
|
|
next
|
|
end
|
|
else
|
|
failures << [:bad_managed_name, project_id, ap_id, name]
|
|
end
|
|
end
|
|
|
|
unless acls.is_a?(Array)
|
|
failures << [:acls_not_array, project_id, ap_id]
|
|
next
|
|
end
|
|
|
|
acls.each_with_index do |acl, i|
|
|
subject_id = convert.call(i, acl, "subjects", name_map[project_id])
|
|
if subject_id.empty?
|
|
unless acl["subjects"].empty?
|
|
failures << [:no_valid_subjects, project_id, ap_id, i]
|
|
end
|
|
next
|
|
end
|
|
|
|
ace_hash = projects[project_id] ||= {admin: [], member: [], aces: []}
|
|
# Project:policy is not a valid action, but it was used previously. If a user was
|
|
# granted the ability to change the project policy, de facto they are equivalent to an
|
|
# admin of the project, since they could change the policy to grant themselves any
|
|
# access they needed.
|
|
if is_admin || acl["actions"] == admin_array || acl["actions"].include?("Project:policy")
|
|
ace_hash[:admin].concat(subject_id)
|
|
subject_id = []
|
|
elsif is_member
|
|
api_keys, subject_id = subject_id.partition { UBID.from_uuidish(_1).to_s.start_with?("et") }
|
|
ace_hash[:member].concat(subject_id)
|
|
subject_id = api_keys.map { [_1] }
|
|
else
|
|
api_keys, subject_id = subject_id.partition { UBID.from_uuidish(_1).to_s.start_with?("et") }
|
|
subject_id = [subject_id] + api_keys.map { [_1] }
|
|
end
|
|
|
|
subject_id.each do |subject_id|
|
|
action_id = convert.call(i, acl, "actions", action_map)
|
|
object_id = convert.call(i, acl, "objects", name_map[project_id])
|
|
ace_hash[:aces] << [ap_id, i, subject_id, action_id, object_id].map! do
|
|
v = (_1.is_a?(Array) && _1.length == 1) ? _1[0] : _1
|
|
v = nil if v == Sequel::NULL
|
|
v
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
projects.each do |project_id, hash|
|
|
admin = hash[:admin]
|
|
if admin.empty?
|
|
project_accounts = Project[project_id].accounts
|
|
|
|
if project_accounts.length == 1
|
|
# If a project has only a single account, that account is the admin
|
|
admin.concat(project_accounts.map(&:id))
|
|
else
|
|
failures << [:no_admin_members, project_id]
|
|
end
|
|
else
|
|
hash[:aces].delete_if do
|
|
# No point in separate AccessControlEntry if subject has full admin access
|
|
admin.include?(_1[2])
|
|
end
|
|
end
|
|
end
|
|
|
|
{projects:, failures:}
|
|
end
|
|
# :nocov:
|
|
end
|