-
Notifications
You must be signed in to change notification settings - Fork 87
Lock timeoff entries #1909
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
base: develop
Are you sure you want to change the base?
Lock timeoff entries #1909
Conversation
📝 WalkthroughWalkthroughThe 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this 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 missingindex?andcreate?permission tests.The policy allows employees to access
index?andcreate?viauser_has_any_workspace_role?, but this context only testsupdate?/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.agomay not reliably land in a "past week" if the test runs early in the week. The policy checksTime.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.
📒 Files selected for processing (2)
app/policies/timeoff_entry_policy.rbspec/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 toupdate?.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.
| def update? | ||
| authorize_current_user | ||
| week_over? ? user_has_admin_or_owner_role? : user_has_any_workspace_role? | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -20Repository: saeloun/miru-web
Length of output: 143
🏁 Script executed:
# Read the full policy file
cat -n ./app/policies/timeoff_entry_policy.rbRepository: 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.rbRepository: 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 -10Repository: saeloun/miru-web
Length of output: 123
🏁 Script executed:
# Read the timeoff entries controller
cat -n ./app/controllers/internal_api/v1/timeoff_entries_controller.rbRepository: 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 2Repository: 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.rbRepository: 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.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -B2Repository: 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 -B2Repository: 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.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Fixes #1903
Summary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.