Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@apoorv1316
Copy link
Collaborator

@apoorv1316 apoorv1316 commented Sep 18, 2024

Fixes #1903

Summary by CodeRabbit

Release Notes

  • Improvements
    • Updated timeoff entry permissions. Employees can now only modify entries within the current week; admins and owners retain the ability to modify past-week entries for better control.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

The changes implement PTO entry locking by restricting modification permissions for past-week entries to admin/owner roles only. Authorization logic in TimeoffEntryPolicy is refactored to use helper methods, with corresponding comprehensive test coverage added to validate permissions across different user roles and time periods.

Changes

Cohort / File(s) Summary
Policy Refactoring
app/policies/timeoff_entry_policy.rb
Refactored authorization logic with new conditional checks: update? and destroy? now prevent modifications to past-week entries unless user has admin/owner role. Introduced private helper methods (week_over?, user_has_any_workspace_role?, user_has_admin_or_owner_role?) to replace explicit role checks.
Policy Tests
spec/policies/timeoff_entry_policy_spec.rb
New comprehensive test suite covering TimeoffEntryPolicy permissions across roles (admin, owner, employee, no-role) and time periods (past, current, future weeks). Validates that only admin/owner can modify past-week entries while employees can edit current/future entries.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A timely lock on leaves now past,
No deletions shall forever last,
Admins guard what time has sealed,
Employee futures stay revealed,
The PTO protection's here at last! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Lock timeoff entries' accurately reflects the main change in the pull request, which introduces permission checks to prevent deletion and editing of past PTO entries.
Linked Issues check ✅ Passed The pull request implements the required functionality to prevent editing and deletion of past PTO entries through authorization checks, directly addressing issue #1903.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing PTO locking functionality for issue #1903. The policy refactoring, helper methods, and comprehensive test coverage are all in-scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@app/policies/timeoff_entry_policy.rb`:
- Around line 30-36: The controller loads TimeoffEntry without company scoping
in load_timeoff_entry!, allowing cross-company access; update the
load_timeoff_entry! method in InternalApi::V1::TimeoffEntriesController to query
TimeoffEntry.from_workspace(current_company.id).find(params[:id]) (i.e., apply
the from_workspace scope) so entries are restricted to the current company, or
alternatively add a company check in TimeoffEntryPolicy (e.g., ensure
record.company == user.current_workspace) to enforce workspace ownership.
- Around line 12-14: The policy currently lets any employee update/destroy
entries in the current week; change the authorization in update? (and destroy?
if present) to require ownership when the actor is not admin/owner (i.e.
week_over? ? user_has_admin_or_owner_role? : (user_has_any_workspace_role? &&
record.belongs_to?(user))) — reference update?, week_over?,
user_has_admin_or_owner_role?, user_has_any_workspace_role? and ensure record
ownership is checked (e.g. record.user_id == user.id or a belongs_to? helper).
Also remove :user_id from permitted_attributes for non-admins (keep assignment
only for admin/owner paths) and update controller lookups from
TimeoffEntry.find(params[:id]) to scope for non-admins (use
current_user.timeoff_entries.find(params[:id]) or similar) so records cannot be
loaded or reassigned by other employees.

In `@spec/policies/timeoff_entry_policy_spec.rb`:
- Around line 62-82: The three contexts (current week, past week, future) use
the permit matcher directly, but they should wrap expectations in a permissions
:update?, :destroy? block like the other specs; update each context to call
permissions :update?, :destroy? and move the expect(...).to permit(...) /
expect(subject).not_to permit(...) into that block referencing described_class
(or subject), user and the respective timeoff_entry variables (timeoff_entry,
past_timeoff_entry, future_timeoff_entry) so the tests explicitly assert the
update and destroy permissions.
🧹 Nitpick comments (3)
spec/policies/timeoff_entry_policy_spec.rb (3)

56-83: Employee context is missing index? and create? permission tests.

The policy allows employees to access index? and create? via user_has_any_workspace_role?, but this context only tests update?/destroy? scenarios.

Add missing permission tests
   context "when user is an employee" do
     before do
       create(:employment, company:, user:)
       user.add_role :employee, company
     end

+    permissions :index?, :create? do
+      it "is permitted to access timeoff_entries" do
+        expect(subject).to permit(user, timeoff_entry)
+      end
+    end
+
     context "when leave date is in the current week" do

69-69: Potentially flaky date calculation for "past week" test.

1.week.ago may not reliably land in a "past week" if the test runs early in the week. The policy checks Time.current > record.leave_date.end_of_week(:sunday), so consider using a date that's guaranteed to be in a completed week.

Suggested fix
-    let(:past_timeoff_entry) { create(:timeoff_entry, user:, leave_type:, leave_date: 1.week.ago) }
+    let(:past_timeoff_entry) { create(:timeoff_entry, user:, leave_type:, leave_date: Date.current.prev_week) }

This ensures the date is always in the previous calendar week regardless of when the test runs.


93-106: Test description mentions "admin or owner" but only tests admin.

The test case claims to verify that both admin and owner can update/destroy past-week entries, but only the admin role is tested.

Add owner test case
   context "when leave date is past the week" do
     let(:past_timeoff_entry) { create(:timeoff_entry, user:, leave_type:, leave_date: 1.week.ago) }

-    before do
-      create(:employment, company:, user:)
-      user.add_role :admin, company
-    end
-
-    permissions :update?, :destroy? do
-      it "is permitted for admin or owner to update or destroy" do
-        expect(subject).to permit(user, past_timeoff_entry)
+    context "when user is an admin" do
+      before do
+        create(:employment, company:, user:)
+        user.add_role :admin, company
+      end
+
+      permissions :update?, :destroy? do
+        it "is permitted to update or destroy" do
+          expect(subject).to permit(user, past_timeoff_entry)
+        end
+      end
+    end
+
+    context "when user is an owner" do
+      before do
+        create(:employment, company:, user:)
+        user.add_role :owner, company
+      end
+
+      permissions :update?, :destroy? do
+        it "is permitted to update or destroy" do
+          expect(subject).to permit(user, past_timeoff_entry)
+        end
       end
     end
   end
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bc587a0 and 0bcb495.

📒 Files selected for processing (2)
  • app/policies/timeoff_entry_policy.rb
  • spec/policies/timeoff_entry_policy_spec.rb
🧰 Additional context used
🧬 Code graph analysis (2)
app/policies/timeoff_entry_policy.rb (1)
app/models/user.rb (2)
  • current_workspace (162-164)
  • current_workspace (166-168)
spec/policies/timeoff_entry_policy_spec.rb (1)
app/models/timeoff_entry.rb (1)
  • company (56-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Running tests
🔇 Additional comments (2)
app/policies/timeoff_entry_policy.rb (2)

26-28: Temporal locking logic looks correct.

The week_over? method properly checks if the current time is past the end of the week (Sunday) containing the leave date. This achieves the PR objective of locking past PTO entries from employee modification.


16-18: Good: destroy? correctly delegates to update?.

This ensures consistent permission behavior between update and delete operations, which aligns with the PR requirement to prevent both editing and deletion of past entries.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 12 to 14
def update?
authorize_current_user
week_over? ? user_has_admin_or_owner_role? : user_has_any_workspace_role?
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the timeoff_entry_policy.rb file
find . -type f -name "*timeoff_entry_policy*" -o -name "timeoff*policy*" | head -20

Repository: saeloun/miru-web

Length of output: 143


🏁 Script executed:

# Read the full policy file
cat -n ./app/policies/timeoff_entry_policy.rb

Repository: saeloun/miru-web

Length of output: 1391


🏁 Script executed:

# Read the spec file to understand intended behavior
cat -n ./spec/policies/timeoff_entry_policy_spec.rb

Repository: saeloun/miru-web

Length of output: 4126


🏁 Script executed:

# Search for timeoff entries controller and any ownership validation
find . -type f -name "*timeoff*controller*" | head -10

Repository: saeloun/miru-web

Length of output: 123


🏁 Script executed:

# Read the timeoff entries controller
cat -n ./app/controllers/internal_api/v1/timeoff_entries_controller.rb

Repository: saeloun/miru-web

Length of output: 2757


🏁 Script executed:

# Check if there are any model-level validations on ownership
rg -n "record\.user|validates.*user|owner" ./app/models/timeoff_entry.rb -A 2 -B 2

Repository: saeloun/miru-web

Length of output: 42


🏁 Script executed:

# Find the timeoff_entry model
find . -type f -name "*timeoff_entry*" | grep -E "models|app"

Repository: saeloun/miru-web

Length of output: 164


🏁 Script executed:

# Read the timeoff_entry model
cat -n ./app/models/timeoff_entry.rb

Repository: saeloun/miru-web

Length of output: 4941


Add ownership check to prevent employees from modifying other users' timeoff entries.

The policy allows any employee to update/destroy timeoff entries within the current week without verifying ownership. The controller loads entries without scoping to the current user (TimeoffEntry.find(params[:id]) instead of current_user.timeoff_entries.find(params[:id])), and the model has no ownership validation. This allows Employee A to modify or delete Employee B's entries, which violates the principle that users should only modify their own PTO.

The tests only verify self-modifications and don't expose this gap. Additionally, permitted_attributes includes :user_id, allowing reassignment of entries to other users.

Employees should only be able to modify their own entries during the current week. Admins/owners should be able to modify any entry regardless of week.

🤖 Prompt for AI Agents
In `@app/policies/timeoff_entry_policy.rb` around lines 12 - 14, The policy
currently lets any employee update/destroy entries in the current week; change
the authorization in update? (and destroy? if present) to require ownership when
the actor is not admin/owner (i.e. week_over? ? user_has_admin_or_owner_role? :
(user_has_any_workspace_role? && record.belongs_to?(user))) — reference update?,
week_over?, user_has_admin_or_owner_role?, user_has_any_workspace_role? and
ensure record ownership is checked (e.g. record.user_id == user.id or a
belongs_to? helper). Also remove :user_id from permitted_attributes for
non-admins (keep assignment only for admin/owner paths) and update controller
lookups from TimeoffEntry.find(params[:id]) to scope for non-admins (use
current_user.timeoff_entries.find(params[:id]) or similar) so records cannot be
loaded or reassigned by other employees.

Comment on lines +30 to +36
def user_has_any_workspace_role?
user.has_any_role?(
{ name: :admin, resource: user.current_workspace },
{ name: :owner, resource: user.current_workspace },
{ name: :employee, resource: user.current_workspace }
)
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check timeoff entries controller and model for scoping
echo "=== Searching for timeoff_entries controller ==="
find . -type f -name "*timeoff*" -o -name "*controller*" | head -20

echo -e "\n=== Looking for timeoff_entries controller specifically ==="
fd "timeoff.*controller" --type f

echo -e "\n=== Searching for timeoff_entry references in controllers ==="
rg "timeoff_entr" --type ruby -g "*controller*" -A5 -B2

Repository: saeloun/miru-web

Length of output: 7377


🏁 Script executed:

#!/bin/bash
# Check TimeoffEntry model for company association
cat -n app/models/timeoff_entry.rb

echo -e "\n=== Searching for company association ==="
rg "belongs_to.*company|has_many.*timeoff" --type ruby -A2 -B2

Repository: saeloun/miru-web

Length of output: 10709


Controller loads timeoff entries without company scoping, allowing authorization bypass.

The load_timeoff_entry! method in app/controllers/internal_api/v1/timeoff_entries_controller.rb uses TimeoffEntry.find(params[:id]) without filtering by the current company. This allows users to load and modify timeoff entries from any company if they know the entry ID. Although the policy validates that the user has a role in their workspace, it does not verify that the loaded entry belongs to that workspace.

The controller properly scopes all related resources (current_company.users.find(...), current_company.leave_types.find(...), etc.), but timeoff entries are loaded unscoped. The model provides a from_workspace scope for this purpose, so load_timeoff_entry! should be changed to:

`@timeoff_entry` ||= TimeoffEntry.from_workspace(current_company.id).find(params[:id])

Alternatively, the policy could verify record.company == user.current_workspace explicitly.

🤖 Prompt for AI Agents
In `@app/policies/timeoff_entry_policy.rb` around lines 30 - 36, The controller
loads TimeoffEntry without company scoping in load_timeoff_entry!, allowing
cross-company access; update the load_timeoff_entry! method in
InternalApi::V1::TimeoffEntriesController to query
TimeoffEntry.from_workspace(current_company.id).find(params[:id]) (i.e., apply
the from_workspace scope) so entries are restricted to the current company, or
alternatively add a company check in TimeoffEntryPolicy (e.g., ensure
record.company == user.current_workspace) to enforce workspace ownership.

Comment on lines +62 to +82
context "when leave date is in the current week" do
it "is permitted to update or destroy timeoff_entries" do
expect(described_class).to permit(user, timeoff_entry)
end
end

context "when leave date is in the past week" do
let(:past_timeoff_entry) { create(:timeoff_entry, user:, leave_type:, leave_date: 1.week.ago) }

it "is not permitted to update or destroy timeoff_entries" do
expect(subject).not_to permit(user, past_timeoff_entry)
end
end

context "when leave date is in the future" do
let(:future_timeoff_entry) { create(:timeoff_entry, user:, leave_type:, leave_date: 1.week.from_now) }

it "is permitted to update or destroy timeoff_entries" do
expect(subject).to permit(user, future_timeoff_entry)
end
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing permissions block in employee context tests.

These tests don't wrap the expectations in a permissions :update?, :destroy? block like the other contexts. Without this, the permit matcher behavior is undefined or will test all permissions indiscriminately.

Proposed fix
   context "when leave date is in the current week" do
-    it "is permitted to update or destroy timeoff_entries" do
-      expect(described_class).to permit(user, timeoff_entry)
+    permissions :update?, :destroy? do
+      it "is permitted to update or destroy timeoff_entries" do
+        expect(subject).to permit(user, timeoff_entry)
+      end
     end
   end

   context "when leave date is in the past week" do
     let(:past_timeoff_entry) { create(:timeoff_entry, user:, leave_type:, leave_date: 1.week.ago) }

-    it "is not permitted to update or destroy timeoff_entries" do
-      expect(subject).not_to permit(user, past_timeoff_entry)
+    permissions :update?, :destroy? do
+      it "is not permitted to update or destroy timeoff_entries" do
+        expect(subject).not_to permit(user, past_timeoff_entry)
+      end
     end
   end

   context "when leave date is in the future" do
     let(:future_timeoff_entry) { create(:timeoff_entry, user:, leave_type:, leave_date: 1.week.from_now) }

-    it "is permitted to update or destroy timeoff_entries" do
-      expect(subject).to permit(user, future_timeoff_entry)
+    permissions :update?, :destroy? do
+      it "is permitted to update or destroy timeoff_entries" do
+        expect(subject).to permit(user, future_timeoff_entry)
+      end
     end
   end
🤖 Prompt for AI Agents
In `@spec/policies/timeoff_entry_policy_spec.rb` around lines 62 - 82, The three
contexts (current week, past week, future) use the permit matcher directly, but
they should wrap expectations in a permissions :update?, :destroy? block like
the other specs; update each context to call permissions :update?, :destroy? and
move the expect(...).to permit(...) / expect(subject).not_to permit(...) into
that block referencing described_class (or subject), user and the respective
timeoff_entry variables (timeoff_entry, past_timeoff_entry,
future_timeoff_entry) so the tests explicitly assert the update and destroy
permissions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PTO should be lockable

3 participants