Skip to content

Commit

Permalink
Merge pull request #67 from enjaku4/return-all-roles
Browse files Browse the repository at this point in the history
  • Loading branch information
enjaku4 authored Dec 29, 2024
2 parents fd041ba + 3093687 commit 3f53c4c
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 3 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## v4.1.0

### Features:

- Added `Rabarber::Role.all_names` method to retrieve all roles available in the application, grouped by context
- Added `Rabarber::HasRoles#all_roles` method to retrieve all roles assigned to a user, grouped by context

### Bugs:

- Fixed potential bug in role revocation caused by checking for the presence of a role in the cache instead of the database

## v4.0.2

### Misc:
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ To get the list of roles assigned to the user, use:
user.roles
```

**`#all_roles`**

To get all roles assigned to the user, grouped by context, use:

```rb
user.all_roles
```

---

To manipulate roles directly, you can use `Rabarber::Role` methods:
Expand Down Expand Up @@ -203,12 +211,20 @@ Rabarber::Role.remove(:admin, force: true)

**`.names(context: nil)`**

If you need to list the role names available in your application, use:
If you need to list the roles available in your application, use:

```rb
Rabarber::Role.names
```

**`.all_names`**

If you need list all roles available in your application, grouped by context, use:

```rb
Rabarber::Role.all_names
```

**`.assignees(role_name, context: nil)`**

To get all the users to whom the role is assigned, use:
Expand Down
6 changes: 5 additions & 1 deletion lib/rabarber/models/concerns/has_roles.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def roles(context: nil)
Rabarber::Core::Cache.fetch(roleable_id, context: processed_context) { rabarber_roles.names(context: processed_context) }
end

def all_roles
rabarber_roles.all_names
end

def has_role?(*role_names, context: nil)
processed_context = process_context(context)
processed_roles = process_role_names(role_names)
Expand Down Expand Up @@ -55,7 +59,7 @@ def revoke_roles(*role_names, context: nil)
processed_context = process_context(context)

roles_to_revoke = Rabarber::Role.where(
name: processed_role_names.intersection(roles(context: processed_context)), **processed_context
name: processed_role_names.intersection(rabarber_roles.names(context: processed_context)), **processed_context
)

if roles_to_revoke.any?
Expand Down
20 changes: 20 additions & 0 deletions lib/rabarber/models/role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,23 @@ class Role < ActiveRecord::Base
format: { with: Rabarber::Input::Role::REGEX },
strict: true

belongs_to :context, polymorphic: true, optional: true

before_destroy :delete_assignments

class << self
def names(context: nil)
where(process_context(context)).pluck(:name).map(&:to_sym)
end

def all_names
includes(:context).group_by(&:context).transform_values { |roles| roles.map { _1.name.to_sym } }
rescue ActiveRecord::RecordNotFound => e
raise Rabarber::Error, "Context not found: #{e.model}##{e.id}"
rescue NameError => e
raise Rabarber::Error, "Context not found: #{e.name}"
end

def add(name, context: nil)
name = process_role_name(name)
processed_context = process_context(context)
Expand Down Expand Up @@ -77,6 +87,16 @@ def process_context(context)
end
end

def context
return context_type.constantize if context_type.present? && context_id.blank?

record = super

raise ActiveRecord::RecordNotFound.new(nil, context_type, nil, context_id) if context_id.present? && !record

record
end

private

def delete_assignments
Expand Down
2 changes: 1 addition & 1 deletion lib/rabarber/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Rabarber
VERSION = "4.0.2"
VERSION = "4.1.0"
end
8 changes: 8 additions & 0 deletions spec/rabarber/audit/events/roles_assigned_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@
).and_call_original
subject
end

context "when context is invalid" do
let(:context) { 42 }

it "raises an error" do
expect { subject }.to raise_error(Rabarber::Error, "Unexpected context: 42")
end
end
end
8 changes: 8 additions & 0 deletions spec/rabarber/audit/events/roles_revoked_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@
).and_call_original
subject
end

context "when context is invalid" do
let(:context) { 42 }

it "raises an error" do
expect { subject }.to raise_error(Rabarber::Error, "Unexpected context: 42")
end
end
end
38 changes: 38 additions & 0 deletions spec/rabarber/models/concerns/has_roles_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,44 @@
end
end

describe "#all_roles" do
subject { user.all_roles }

let(:user) { User.create! }

context "when the user has no roles" do
it { is_expected.to eq({}) }
end

context "when the user has some roles" do
let(:project) { Project.create! }

before do
user.assign_roles(:admin, :manager)
user.assign_roles(:viewer, context: User)
user.assign_roles(:manager, context: project)
end

it { is_expected.to eq(nil => [:admin, :manager], User => [:viewer], project => [:manager]) }

context "when the instance context can't be found" do
before { project.destroy! }

it "raises an error" do
expect { subject }.to raise_error(Rabarber::Error, "Context not found: Project##{project.id}")
end
end

context "when the class context doesn't exist" do
before { Rabarber::Role.take.update!(context_type: "Foo") }

it "raises an error" do
expect { subject }.to raise_error(Rabarber::Error, "Context not found: Foo")
end
end
end
end

describe "#has_role?" do
subject { user.has_role?(*roles, context:) }

Expand Down
82 changes: 82 additions & 0 deletions spec/rabarber/models/role_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,49 @@
end
end

describe ".all_names" do
subject { described_class.all_names }

context "when there are no roles" do
it { is_expected.to eq({}) }
end

context "when there are some roles" do
let(:project1) { Project.create! }
let(:project2) { Project.create! }
let(:user) { User.create! }

before do
described_class.add(:admin)
described_class.add(:accountant)
described_class.add(:admin, context: Project)
described_class.add(:manager, context: Project)
described_class.add(:manager, context: project1)
described_class.add(:viewer, context: project2)
described_class.add(:manager, context: project2)
described_class.add(:editor, context: user)
end

it { is_expected.to eq(nil => [:admin, :accountant], Project => [:admin, :manager], project1 => [:manager], project2 => [:viewer, :manager], user => [:editor]) }

context "when the instance context can't be found" do
before { project1.destroy! }

it "raises an error" do
expect { subject }.to raise_error(Rabarber::Error, "Context not found: Project##{project1.id}")
end
end

context "when the class context doesn't exist" do
before { described_class.take.update!(context_type: "Foo") }

it "raises an error" do
expect { subject }.to raise_error(Rabarber::Error, "Context not found: Foo")
end
end
end
end

shared_examples_for "role name is processed" do |roles|
it "uses Input::Role to process the given roles" do
roles.each do |role|
Expand Down Expand Up @@ -409,4 +452,43 @@
it_behaves_like "role name is processed", ["admin"]
end
end

describe "#context" do
subject { role.context }

context "when the role has global context" do
let(:role) { described_class.create!(name: "admin") }

it { is_expected.to be_nil }
end

context "when the role has an instance context" do
let(:project) { Project.create! }
let(:role) { described_class.create!(name: "admin", context_type: project.model_name, context_id: project.id) }

it { is_expected.to eq(project) }
end

context "when the role has a class context" do
let(:role) { described_class.create!(name: "admin", context_type: "Project") }

it { is_expected.to eq(Project) }
end

context "when the instance context can't be found" do
let(:role) { described_class.create!(name: "admin", context_type: "Project", context_id: 42) }

it "raises an error" do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
end
end

context "when the class context doesn't exist" do
let(:role) { described_class.create!(name: "admin", context_type: "Foo") }

it "raises an error" do
expect { subject }.to raise_error(NameError)
end
end
end
end

0 comments on commit 3f53c4c

Please sign in to comment.