mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-18 13:12:16 +01:00
Compare commits
1 Commits
fix/dropdo
...
9007ad686a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9007ad686a |
@@ -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', $timeEntry);
|
$this->checkPermission($organization, 'time-entries:update:own');
|
||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
|
$this->checkPermission($organization, 'time-entries:update:all');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($timeEntry->end !== null && $request->has('end') && $request->input('end') === null) {
|
if ($timeEntry->end !== null && $request->has('end') && $request->input('end') === null) {
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"brick/money": "^0.10.0",
|
"brick/money": "^0.10.0",
|
||||||
"datomatic/laravel-enum-helper": "^2.0.0",
|
"datomatic/laravel-enum-helper": "^2.0.0",
|
||||||
"dedoc/scramble": "^0.12.2",
|
"dedoc/scramble": "^0.13.20",
|
||||||
"filament/filament": "^3.2",
|
"filament/filament": "^3.2",
|
||||||
"flowframe/laravel-trend": "^0.4.0",
|
"flowframe/laravel-trend": "^0.5.0",
|
||||||
"gotenberg/gotenberg-php": "^2.8",
|
"gotenberg/gotenberg-php": "^2.8",
|
||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"inertiajs/inertia-laravel": "^2.0.3",
|
"inertiajs/inertia-laravel": "^2.0.3",
|
||||||
|
|||||||
1685
composer.lock
generated
1685
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -35,8 +35,7 @@ watch(open, (isOpen) => {
|
|||||||
sortedItems.value = [...props.items].sort((a, b) => {
|
sortedItems.value = [...props.items].sort((a, b) => {
|
||||||
const aSelected = model.value.includes(props.getKeyFromItem(a)) ? 0 : 1;
|
const aSelected = model.value.includes(props.getKeyFromItem(a)) ? 0 : 1;
|
||||||
const bSelected = model.value.includes(props.getKeyFromItem(b)) ? 0 : 1;
|
const bSelected = model.value.includes(props.getKeyFromItem(b)) ? 0 : 1;
|
||||||
if (aSelected !== bSelected) return aSelected - bSelected;
|
return aSelected - bSelected;
|
||||||
return props.getNameForItem(a).localeCompare(props.getNameForItem(b));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,30 +20,25 @@ const selectedTimeEntries = defineModel<TimeEntry[]>('selected', {
|
|||||||
default: [],
|
default: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = defineProps<{
|
||||||
defineProps<{
|
timeEntries: TimeEntry[];
|
||||||
timeEntries: TimeEntry[];
|
projects: Project[];
|
||||||
projects: Project[];
|
tasks: Task[];
|
||||||
tasks: Task[];
|
tags: Tag[];
|
||||||
tags: Tag[];
|
clients: Client[];
|
||||||
clients: Client[];
|
createTag: (name: string) => Promise<Tag | undefined>;
|
||||||
createTag: (name: string) => Promise<Tag | undefined>;
|
updateTimeEntry: (entry: TimeEntry) => void;
|
||||||
updateTimeEntry: (entry: TimeEntry) => void;
|
updateTimeEntries: (ids: string[], changes: Partial<TimeEntry>) => void;
|
||||||
updateTimeEntries: (ids: string[], changes: Partial<TimeEntry>) => void;
|
deleteTimeEntries: (entries: TimeEntry[]) => void;
|
||||||
deleteTimeEntries: (entries: TimeEntry[]) => void;
|
createTimeEntry: (entry: Omit<CreateTimeEntryBody, 'member_id'>) => void;
|
||||||
createTimeEntry: (entry: Omit<CreateTimeEntryBody, 'member_id'>) => void;
|
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
|
||||||
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
|
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
|
||||||
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
|
currency: string;
|
||||||
currency: string;
|
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,47 +2490,6 @@ 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