Skip to content

Commit

Permalink
Fixes and test coverage for returning proper groups for limited admins.
Browse files Browse the repository at this point in the history
  • Loading branch information
GUI committed Jan 25, 2024
1 parent 404c2cc commit beeeae3
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 28 deletions.
4 changes: 3 additions & 1 deletion src/api-umbrella/web-app/actions/v1/admins.lua
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ function _M.index(self)
"created_at",
"updated_at",
},
preload = { "groups" },
preload = {
"authorized_groups",
},
csv_filename = "admins",
})
end
Expand Down
40 changes: 23 additions & 17 deletions src/api-umbrella/web-app/models/admin.lua
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ Admin = model_ext.new_class("admins", {
foreign_key = "admin_id",
association_foreign_key = "admin_group_id",
order = "name",
}),
model_ext.has_and_belongs_to_many("authorized_groups", "AdminGroup", {
join_table = "admin_groups_admins",
foreign_key = "admin_id",
association_foreign_key = "admin_group_id",
order = "name",
transform_sql = function(sql)
local scope_sql = admin_group_policy.authorized_query_scope(ngx.ctx.current_admin)
if scope_sql then
Expand Down Expand Up @@ -178,26 +184,26 @@ Admin = model_ext.new_class("admins", {
return decrypted
end,

group_ids = function(self)
local group_ids = {}
local groups = self:get_groups()
authorized_group_ids = function(self)
local authorized_group_ids = {}
local groups = self:get_authorized_groups()
for _, group in ipairs(groups) do
table.insert(group_ids, group.id)
table.insert(authorized_group_ids, group.id)
end

return group_ids
return authorized_group_ids
end,

group_names = function(self)
local group_names = {}
for _, group in ipairs(self:get_groups()) do
table.insert(group_names, group.name)
authorized_group_names = function(self)
local authorized_group_names = {}
for _, group in ipairs(self:get_authorized_groups()) do
table.insert(authorized_group_names, group.name)
end
if self.superuser then
table.insert(group_names, t("Superuser"))
table.insert(authorized_group_names, t("Superuser"))
end

return group_names
return authorized_group_names
end,

group_permission_ids = function(self)
Expand Down Expand Up @@ -240,9 +246,9 @@ Admin = model_ext.new_class("admins", {
return false
end,

groups_as_json = function(self)
authorized_groups_as_json = function(self)
local admin_groups = {}
for _, admin_group in ipairs(self:get_groups()) do
for _, admin_group in ipairs(self:get_authorized_groups()) do
table.insert(admin_groups, admin_group:embedded_json())
end

Expand Down Expand Up @@ -276,9 +282,9 @@ Admin = model_ext.new_class("admins", {
updater = {
username = json_null_default(self.updated_by_username),
},
groups = json_null_default(self:groups_as_json()),
group_ids = json_null_default(self:group_ids()),
group_names = json_null_default(self:group_names()),
groups = json_null_default(self:authorized_groups_as_json()),
group_ids = json_null_default(self:authorized_group_ids()),
group_names = json_null_default(self:authorized_group_names()),
deleted_at = json_null,
version = 1,
}
Expand Down Expand Up @@ -313,7 +319,7 @@ Admin = model_ext.new_class("admins", {
as_csv = function(self)
return {
json_null_default(self.username),
json_null_default(table.concat(self:group_names(), "\n")),
json_null_default(table.concat(self:authorized_group_names(), "\n")),
json_null_default(time.postgres_to_iso8601(self.current_sign_in_at)),
json_null_default(time.postgres_to_iso8601(self.created_at)),
}
Expand Down
8 changes: 4 additions & 4 deletions src/api-umbrella/web-app/policies/admin_policy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ function _M.is_authorized_show(current_admin, data, permission_id)
return true
end

if data["superuser"] and data["superuser"] ~= false then
return false
end

if not permission_id then
permission_id = "admin_view"
end

if data["superuser"] and data["superuser"] ~= false and permission_id ~= "admin_view" then
return false
end

local any_groups_allowed = false
local all_groups_allowed = false
if not is_empty(data["group_ids"]) then
Expand Down
174 changes: 168 additions & 6 deletions test/apis/v1/admins/test_admin_permissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,22 @@ def test_multi_group_multi_scope_permitted_as_multi_scope_admin
assert_admin_permitted(factory, admin)
end

def test_multi_group_multi_scope_forbidden_as_single_scope_admin
def test_multi_group_multi_scope_permitted_read_forbidden_modify_as_single_scope_admin
factory = :google_and_yahoo_multi_group_admin

admin = FactoryBot.create(:limited_admin, :groups => [
FactoryBot.create(:admin_group, :api_scopes => [
ApiScope.find_or_create_by_instance!(FactoryBot.build(:google_api_scope)),
]),
])
assert_admin_forbidden(factory, admin)
assert_admin_permitted_read_forbidden_modify(factory, admin)

admin = FactoryBot.create(:limited_admin, :groups => [
FactoryBot.create(:admin_group, :api_scopes => [
ApiScope.find_or_create_by_instance!(FactoryBot.build(:yahoo_api_scope)),
]),
])
assert_admin_forbidden(factory, admin)
assert_admin_permitted_read_forbidden_modify(factory, admin)
end

def test_single_group_multi_scope_permitted_as_superuser
Expand All @@ -72,22 +72,22 @@ def test_single_group_multi_scope_permitted_as_multi_scope_admin
assert_admin_permitted(factory, admin)
end

def test_single_group_multi_scope_forbidden_as_single_scope_admin
def test_single_group_multi_scope_permitted_read_forbidden_modify_as_single_scope_admin
factory = :google_and_yahoo_single_group_admin

admin = FactoryBot.create(:limited_admin, :groups => [
FactoryBot.create(:admin_group, :api_scopes => [
ApiScope.find_or_create_by_instance!(FactoryBot.build(:google_api_scope)),
]),
])
assert_admin_forbidden(factory, admin)
assert_admin_permitted_read_forbidden_modify(factory, admin)

admin = FactoryBot.create(:limited_admin, :groups => [
FactoryBot.create(:admin_group, :api_scopes => [
ApiScope.find_or_create_by_instance!(FactoryBot.build(:yahoo_api_scope)),
]),
])
assert_admin_forbidden(factory, admin)
assert_admin_permitted_read_forbidden_modify(factory, admin)
end

def test_superuser_as_superuser
Expand Down Expand Up @@ -297,6 +297,160 @@ def test_notes_only_visible_to_admin_managers_and_superusers
refute_includes("Private notes", response.body)
end

def test_only_returns_permitted_groups_even_if_other_groups_exist
superuser_admin = FactoryBot.create(:admin)
superuser_google_admin = FactoryBot.create(:google_admin, :superuser => true)
google_and_yahoo_multi_group_admin = FactoryBot.create(:google_and_yahoo_multi_group_admin)
google_and_yahoo_single_group_admin = FactoryBot.create(:google_and_yahoo_single_group_admin)
google_admin = FactoryBot.create(:google_admin)
yahoo_admin = FactoryBot.create(:yahoo_admin)
admins = [
superuser_admin,
superuser_google_admin,
google_and_yahoo_multi_group_admin,
google_and_yahoo_single_group_admin,
google_admin,
yahoo_admin,
]

admins.each do |calling_admin|
# Make the request to fetch all of the admins as each different admin to
# see how the responses differ.
response = Typhoeus.get("https://127.0.0.1:9081/api-umbrella/v1/admins.json", http_options.deep_merge(admin_token(calling_admin)))
assert_response_code(200, response)
data = MultiJson.load(response.body)

# Verify the expected admins in the response depending on what admin is
# making the request.
admin_ids = data.fetch("data").map { |r| r["id"] }
case calling_admin
# Superusers should return all admins and all groups they belong to.
when superuser_admin
assert_equal([
superuser_admin.id,
superuser_google_admin.id,
google_and_yahoo_multi_group_admin.id,
google_and_yahoo_single_group_admin.id,
google_admin.id,
yahoo_admin.id,
].sort, admin_ids.sort)

# Admins authorized to both scopes should return all admins in either
# scope.
when google_and_yahoo_multi_group_admin, google_and_yahoo_single_group_admin
assert_equal([
superuser_google_admin.id,
google_and_yahoo_multi_group_admin.id,
google_and_yahoo_single_group_admin.id,
google_admin.id,
yahoo_admin.id,
].sort, admin_ids.sort)

# Admins only authorized to a single scope should return any admins with
# intersecting permissions, even if they won't have edit privileges on
# some admins that belong to groups they are not fully authorized to (like
# google_and_yahoo_multi_group_admin and
# google_and_yahoo_single_group_admin).
when google_admin
assert_equal([
superuser_google_admin.id,
google_and_yahoo_multi_group_admin.id,
google_and_yahoo_single_group_admin.id,
google_admin.id,
].sort, admin_ids.sort)
when yahoo_admin
assert_equal([
google_and_yahoo_multi_group_admin.id,
google_and_yahoo_single_group_admin.id,
yahoo_admin.id,
].sort, admin_ids.sort)
end

# Verify the data on each admin that's part of the API response.
data.fetch("data").each do |admin_data|
case admin_data.fetch("id")
when superuser_admin.id
groups = superuser_admin.groups
assert_equal(0, groups.length)
assert_equal([], admin_data.fetch("groups"))
assert_equal([], admin_data.fetch("group_ids"))
assert_equal(["Superuser"], admin_data.fetch("group_names"))

when superuser_google_admin.id
groups = superuser_google_admin.groups
assert_equal(1, groups.length)
assert_equal([{ "id" => groups.first.id, "name" => groups.first.name }], admin_data.fetch("groups"))
assert_equal([groups.first.id], admin_data.fetch("group_ids"))
assert_equal([groups.first.name, "Superuser"], admin_data.fetch("group_names"))

when google_and_yahoo_multi_group_admin.id
groups = google_and_yahoo_multi_group_admin.groups
assert_equal(2, groups.length)

# There are 2 groups, but for our more limited admins, it should only
# return the 1 that the calling admin is authorized to.
case calling_admin
when google_admin
assert_equal([{ "id" => groups.first.id, "name" => groups.first.name }], admin_data.fetch("groups"))
assert_match(/Google Admin Group/, admin_data.fetch("groups").first.fetch("name"))
assert_equal([groups.first.id], admin_data.fetch("group_ids"))
assert_equal([groups.first.name], admin_data.fetch("group_names"))
assert_match(/Google Admin Group/, admin_data.fetch("group_names").first)
when yahoo_admin
assert_equal([{ "id" => groups.last.id, "name" => groups.last.name }], admin_data.fetch("groups"))
assert_match(/Yahoo Admin Group/, admin_data.fetch("groups").first.fetch("name"))
assert_equal([groups.last.id], admin_data.fetch("group_ids"))
assert_equal([groups.last.name], admin_data.fetch("group_names"))
assert_match(/Yahoo Admin Group/, admin_data.fetch("group_names").first)
else
assert_equal(groups.map { |group| { "id" => group.id, "name" => group.name } }.sort_by { |g| g.fetch("id") }, admin_data.fetch("groups").sort_by { |g| g.fetch("id") })
assert_equal(groups.map { |group| group.id }.sort, admin_data.fetch("group_ids").sort)
assert_equal(groups.map { |group| group.name }.sort, admin_data.fetch("group_names").sort)
end

when google_and_yahoo_single_group_admin.id
groups = google_and_yahoo_single_group_admin.groups
assert_equal(1, groups.length)

# For single-scope admins, the admin can view this other admin (since
# its groups intersect), but it won't be able to update it or view its
# groups it belongs to (since this is not a group that the
# single-scope admin is fully authorized to).
case calling_admin
when google_admin, yahoo_admin
assert_equal([], admin_data.fetch("groups"))
assert_equal([], admin_data.fetch("group_ids"))
assert_equal([], admin_data.fetch("group_names"))
else
assert_equal([{ "id" => groups.first.id, "name" => groups.first.name }], admin_data.fetch("groups"))
assert_equal([groups.first.id], admin_data.fetch("group_ids"))
assert_equal([groups.first.name], admin_data.fetch("group_names"))
end
when google_admin.id
groups = google_admin.groups
assert_equal(1, groups.length)
assert_equal([{ "id" => groups.first.id, "name" => groups.first.name }], admin_data.fetch("groups"))
assert_equal([groups.first.id], admin_data.fetch("group_ids"))
assert_equal([groups.first.name], admin_data.fetch("group_names"))
when yahoo_admin.id
groups = yahoo_admin.groups
assert_equal(1, groups.length)
assert_equal([{ "id" => groups.first.id, "name" => groups.first.name }], admin_data.fetch("groups"))
assert_equal([groups.first.id], admin_data.fetch("group_ids"))
assert_equal([groups.first.name], admin_data.fetch("group_names"))
else
raise "Unknown admin id"
end

# Verify that the "show" API returns the same nested group data.
response = Typhoeus.get("https://127.0.0.1:9081/api-umbrella/v1/admins/#{admin_data.fetch("id")}.json", http_options.deep_merge(admin_token(calling_admin)))
assert_response_code(200, response)
show_data = MultiJson.load(response.body)
assert_equal(admin_data, show_data.fetch("admin"))
end
end
end

private

def assert_admin_permitted(factory, admin)
Expand All @@ -322,6 +476,14 @@ def assert_admin_forbidden(factory, admin)
assert_admin_forbidden_destroy(factory, admin)
end

def assert_admin_permitted_read_forbidden_modify(factory, admin)
assert_admin_permitted_index(factory, admin)
assert_admin_permitted_show(factory, admin)
assert_admin_forbidden_create(factory, admin)
assert_admin_forbidden_update(factory, admin)
assert_admin_forbidden_destroy(factory, admin)
end

def assert_admin_permitted_index(factory, admin)
record = FactoryBot.create(factory)
response = Typhoeus.get("https://127.0.0.1:9081/api-umbrella/v1/admins.json", http_options.deep_merge(admin_token(admin)))
Expand Down
3 changes: 3 additions & 0 deletions test/factories/admin_groups.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,17 @@
end

factory :google_admin_group do
sequence(:name) { |n| "Google Admin Group #{n}" }
api_scopes { [ApiScope.find_or_create_by_instance!(FactoryBot.build(:google_api_scope))] }
end

factory :yahoo_admin_group do
sequence(:name) { |n| "Yahoo Admin Group #{n}" }
api_scopes { [ApiScope.find_or_create_by_instance!(FactoryBot.build(:yahoo_api_scope))] }
end

factory :google_and_yahoo_multi_scope_admin_group do
sequence(:name) { |n| "Google & Yahoo Admin Group #{n}" }
api_scopes do
[
ApiScope.find_or_create_by_instance!(FactoryBot.build(:google_api_scope)),
Expand Down
6 changes: 6 additions & 0 deletions test/factories/admins.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
end
end

factory :yahoo_admin do
groups do
[FactoryBot.create(:yahoo_admin_group)]
end
end

factory :google_and_yahoo_multi_group_admin do
groups do
[
Expand Down

0 comments on commit beeeae3

Please sign in to comment.