Skip to content
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

Allow admins to manage all resources #1511

Merged
merged 4 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
30 changes: 24 additions & 6 deletions app/models/api/token_ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,26 @@ class TokenAbility
include CanCan::Ability

def initialize(token, user)
# Ensure admins can fully manage all resources before any restrictions apply.
if user&.admin?
can :manage, :all
return
end

can :read, Scenario, private: false
scopes = token[:scopes]

# scenarios:read
# --------------
return unless scopes.include?('scenarios:read')
can :read, Scenario, id: ScenarioUser.where(user_id: user.id, role_id: User::ROLES.key(:scenario_viewer)..).pluck(:scenario_id)

can :read, Scenario, id: ScenarioUser.where(
user_id: user.id,
role_id: User::ROLES.key(:scenario_viewer)..
).pluck(:scenario_id)

# scenarios:write
# ---------------

return unless scopes.include?('scenarios:write')

can :create, Scenario
Expand All @@ -25,18 +35,26 @@ def initialize(token, user)
cannot :update, Scenario, private: false, id: ScenarioUser.pluck(:scenario_id)

# Self-owned scenario.
can :update, Scenario, id: ScenarioUser.where(user_id: user.id, role_id: User::ROLES.key(:scenario_collaborator)..).pluck(:scenario_id)
can :update, Scenario, id: ScenarioUser.where(
user_id: user.id,
role_id: User::ROLES.key(:scenario_collaborator)..
).pluck(:scenario_id)

# Actions that involve reading one scenario and writing to another.
can :clone, Scenario, private: false
can :clone, Scenario, id: ScenarioUser.where(user_id: user.id, role_id: User::ROLES.key(:scenario_collaborator)..).pluck(:scenario_id)
can :clone, Scenario, id: ScenarioUser.where(
user_id: user.id,
role_id: User::ROLES.key(:scenario_collaborator)..
).pluck(:scenario_id)

# scenarios:delete
# ----------------

return unless scopes.include?('scenarios:delete')

can :destroy, Scenario, id: ScenarioUser.where(user_id: user.id, role_id: User::ROLES.key(:scenario_owner)).pluck(:scenario_id)
can :destroy, Scenario, id: ScenarioUser.where(
user_id: user.id,
role_id: User::ROLES.key(:scenario_owner)
).pluck(:scenario_id)
end
end
end
7 changes: 6 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class User < ApplicationRecord

attr_accessor :identity_user

delegate :roles, :admin?, to: :identity_user, allow_nil: true
delegate :roles, to: :identity_user, allow_nil: true

has_many :scenario_users, dependent: :destroy
has_many :scenarios, through: :scenario_users
Expand Down Expand Up @@ -42,6 +42,11 @@ def couple_scenario_users
end
end

# Override admin? to fall back to the attribute when identity_user is nil.
def admin?
identity_user&.admin? || admin
end

# Performs sign-in steps for an Identity::User.
#
# If a matching user exists in the database, it will be updated with the latest data from the
Expand Down
76 changes: 75 additions & 1 deletion spec/models/api/token_ability_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@

# ------------------------------------------------------------------------------------------------

shared_examples_for "a token without the 'scenarios:read' scope" do
shared_examples_for 'a token without the "scenarios:read" scope' do
it 'may view an unowned public scenario' do
expect(ability).to be_able_to(:read, public_scenario)
end
Expand All @@ -74,6 +74,14 @@
it 'may not view an owned private scenario' do
expect(ability).not_to be_able_to(:read, other_private_scenario)
end

context 'when admin' do
let(:user) { create(:user, admin: true, roles:) }

it 'may not view an owned private scenario' do
expect(ability).not_to be_able_to(:read, other_private_scenario)
end
end
end

shared_examples_for 'a token with the "scenarios:read" scope' do
Expand All @@ -92,6 +100,14 @@
it 'may view an owned private scenario' do
expect(ability).to be_able_to(:read, owned_private_scenario)
end

context 'when admin' do
let(:user) { create(:user, admin: true, roles:) }

it 'may view an owned private scenario' do
expect(ability).to be_able_to(:read, other_private_scenario)
end
end
end

shared_examples_for 'a token with the "scenarios:write" scope' do
Expand Down Expand Up @@ -138,6 +154,22 @@
it 'may not clone an other-owned private scenario' do
expect(ability).not_to be_able_to(:clone, other_private_scenario)
end

context 'when admin' do
let(:user) { create(:user, admin: true, roles:) }

it 'may change an other-owned public scenario' do
expect(ability).to be_able_to(:update, other_public_scenario)
end

it 'may change an other-owned private scenario' do
expect(ability).to be_able_to(:update, other_private_scenario)
end

it 'may clone an other-owned private scenario' do
expect(ability).to be_able_to(:clone, other_private_scenario)
end
end
end

shared_examples_for 'a token without the "scenarios:write" scope' do
Expand Down Expand Up @@ -176,6 +208,22 @@
it 'may not clone an owned private scenario' do
expect(ability).not_to be_able_to(:clone, owned_private_scenario)
end

context 'when admin' do
let(:user) { create(:user, admin: true, roles:) }

it 'may not change an other-owned public scenario' do
expect(ability).not_to be_able_to(:update, other_public_scenario)
end

it 'may not change an other-owned private scenario' do
expect(ability).not_to be_able_to(:update, other_private_scenario)
end

it 'may not clone an other-owned private scenario' do
expect(ability).not_to be_able_to(:clone, other_private_scenario)
end
end
end

shared_examples_for 'a token with the "scenarios:delete" scope' do
Expand All @@ -198,6 +246,18 @@
it 'may not delete an other-owned private scenario' do
expect(ability).not_to be_able_to(:destroy, other_private_scenario)
end

context 'when admin' do
let(:user) { create(:user, admin: true, roles:) }

it 'may delete an other-owned public scenario' do
expect(ability).to be_able_to(:destroy, other_public_scenario)
end

it 'may delete an other-owned private scenario' do
expect(ability).to be_able_to(:destroy, other_private_scenario)
end
end
end

shared_examples_for 'a token without the "scenarios:delete" scope' do
Expand All @@ -220,11 +280,25 @@
it 'may not delete an other-owned private scenario' do
expect(ability).not_to be_able_to(:destroy, other_private_scenario)
end

context 'when admin' do
let(:user) { create(:user, admin: true, roles:) }

it 'may not delete an other-owned public scenario' do
expect(ability).not_to be_able_to(:destroy, other_public_scenario)
end

it 'may not delete an other-owned private scenario' do
expect(ability).not_to be_able_to(:destroy, other_private_scenario)
end
end
end

# ------------------------------------------------------------------------------------------------

context 'when the token scope is "public"' do
# Read
include_examples 'a token without the "scenarios:read" scope'

# Update
include_examples 'a token without the "scenarios:write" scope'
Expand Down