mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-18 13:12:16 +01:00
Compare commits
3 Commits
77a5e979c6
...
v0.12.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b73aa543fd | ||
|
|
2d6f9e514f | ||
|
|
f8e668790b |
@@ -629,9 +629,9 @@ class TimeEntryController extends Controller
|
|||||||
/** @var Member|null $member */
|
/** @var Member|null $member */
|
||||||
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||||
if ($timeEntry->member->user_id === Auth::id() && ($member === null || $member->user_id === Auth::id())) {
|
if ($timeEntry->member->user_id === Auth::id() && ($member === null || $member->user_id === Auth::id())) {
|
||||||
$this->checkPermission($organization, 'time-entries:update:own');
|
$this->checkPermission($organization, 'time-entries:update:own', $timeEntry);
|
||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:update:all');
|
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($timeEntry->end !== null && $request->has('end') && $request->input('end') === null) {
|
if ($timeEntry->end !== null && $request->has('end') && $request->input('end') === null) {
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ const selectedTimeEntries = defineModel<TimeEntry[]>('selected', {
|
|||||||
default: [],
|
default: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
timeEntries: TimeEntry[];
|
timeEntries: TimeEntry[];
|
||||||
projects: Project[];
|
projects: Project[];
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
@@ -37,8 +38,12 @@ const props = defineProps<{
|
|||||||
organizationBillableRate: number | null;
|
organizationBillableRate: number | null;
|
||||||
enableEstimatedTime: boolean;
|
enableEstimatedTime: boolean;
|
||||||
canCreateProject: boolean;
|
canCreateProject: boolean;
|
||||||
groupSimilarTimeEntries: boolean;
|
groupSimilarTimeEntries?: boolean;
|
||||||
}>();
|
}>(),
|
||||||
|
{
|
||||||
|
groupSimilarTimeEntries: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const groupedTimeEntries = computed(() => {
|
const groupedTimeEntries = computed(() => {
|
||||||
const groupedEntriesByDay: Record<string, TimeEntry[]> = {};
|
const groupedEntriesByDay: Record<string, TimeEntry[]> = {};
|
||||||
|
|||||||
@@ -2490,6 +2490,47 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$response->assertForbidden();
|
$response->assertForbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_update_endpoint_fails_if_time_entry_belongs_to_different_organization_than_url_even_with_update_all_permission(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// Attacker: has `time-entries:update:all` in their own organization (orgA).
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'time-entries:update:all',
|
||||||
|
'projects:view:all',
|
||||||
|
]);
|
||||||
|
$attackerProject = Project::factory()->forOrganization($data->organization)->create();
|
||||||
|
|
||||||
|
// Victim: entirely separate organization (orgB). Attacker has NO membership in orgB.
|
||||||
|
$victimOrgData = $this->createUserWithPermission([], true);
|
||||||
|
$victimTimeEntry = TimeEntry::factory()
|
||||||
|
->forOrganization($victimOrgData->organization)
|
||||||
|
->forMember($victimOrgData->ownerMember)
|
||||||
|
->create([
|
||||||
|
'description' => 'victim-original',
|
||||||
|
'project_id' => null,
|
||||||
|
'task_id' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act: PUT to /organizations/{orgA}/time-entries/{victim_uuid} — URL org is attacker's
|
||||||
|
// own org, but the route-bound time entry belongs to orgB.
|
||||||
|
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $victimTimeEntry->getKey()]), [
|
||||||
|
'description' => 'attacker-overwrite',
|
||||||
|
'project_id' => $attackerProject->getKey(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert: must be rejected. Before the fix this returned 200 and rewrote the
|
||||||
|
// victim row with attacker's project_id while keeping organization_id = orgB.
|
||||||
|
$response->assertForbidden();
|
||||||
|
$this->assertDatabaseHas(TimeEntry::class, [
|
||||||
|
'id' => $victimTimeEntry->getKey(),
|
||||||
|
'organization_id' => $victimOrgData->organization->getKey(),
|
||||||
|
'description' => 'victim-original',
|
||||||
|
'project_id' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_update_endpoint_fails_if_user_has_no_permission_to_update_time_entries_for_other_users_in_organization(): void
|
public function test_update_endpoint_fails_if_user_has_no_permission_to_update_time_entries_for_other_users_in_organization(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
Reference in New Issue
Block a user