Skip to content

Commit 0f77b41

Browse files
fix(appointments): Rate limit config creation and booking
Abusing the appointment config endpoint can lead to additional server load. Sending bulks of booking requests can lead to mass notifications and emails and server load, too. Signed-off-by: Christoph Wurst <[email protected]>
1 parent ceb2673 commit 0f77b41

File tree

6 files changed

+53
-6
lines changed

6 files changed

+53
-6
lines changed

Diff for: lib/Controller/AppointmentConfigController.php

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use OCA\Calendar\Service\Appointments\AppointmentConfigService;
3636
use OCP\AppFramework\Controller;
3737
use OCP\AppFramework\Http;
38+
use OCP\AppFramework\Http\Attribute\UserRateLimit;
3839
use OCP\IRequest;
3940
use Psr\Log\LoggerInterface;
4041
use function array_keys;
@@ -147,7 +148,9 @@ private function validateAvailability(array $availability): void {
147148
* @param int|null $end
148149
* @param int|null $futureLimit
149150
* @return JsonResponse
151+
* @UserRateThrottle(limit=10, period=1200)
150152
*/
153+
#[UserRateLimit(limit: 10, period: 1200)]
151154
public function create(
152155
string $name,
153156
string $description,

Diff for: lib/Controller/BookingController.php

+7
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
use OCA\Calendar\Service\Appointments\BookingService;
3838
use OCP\AppFramework\Controller;
3939
use OCP\AppFramework\Http;
40+
use OCP\AppFramework\Http\Attribute\AnonRateLimit;
41+
use OCP\AppFramework\Http\Attribute\UserRateLimit;
4042
use OCP\AppFramework\Http\TemplateResponse;
4143
use OCP\AppFramework\Services\IInitialState;
4244
use OCP\AppFramework\Utility\ITimeFactory;
@@ -162,7 +164,12 @@ public function getBookableSlots(int $appointmentConfigId,
162164
* @param string $description
163165
* @param string $timeZone
164166
* @return JsonResponse
167+
*
168+
* @AnonRateThrottle(limit=10, period=1200)
169+
* @UserRateThrottle(limit=10, period=300)
165170
*/
171+
#[AnonRateLimit(limit: 10, period: 1200)]
172+
#[UserRateLimit(limit: 10, period: 300)]
166173
public function bookSlot(int $appointmentConfigId,
167174
int $start,
168175
int $end,

Diff for: src/components/AppointmentConfigModal.vue

+12-1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@
134134
</div>
135135
</fieldset>
136136
</div>
137+
<NcNoteCard v-if="rateLimitingReached"
138+
type="warning">
139+
{{ t('calendar', 'It seems a rate limit has been reached. Please try again later.') }}
140+
</NcNoteCard>
137141
<NcButton class="appointment-config-modal__submit-button"
138142
type="primary"
139143
:disabled="!editing.name || editing.length === 0"
@@ -147,7 +151,7 @@
147151

148152
<script>
149153
import { CalendarAvailability } from '@nextcloud/calendar-availability-vue'
150-
import { NcModal as Modal, NcButton, NcCheckboxRadioSwitch } from '@nextcloud/vue'
154+
import { NcModal as Modal, NcButton, NcCheckboxRadioSwitch, NcNoteCard } from '@nextcloud/vue'
151155
import TextInput from './AppointmentConfigModal/TextInput.vue'
152156
import TextArea from './AppointmentConfigModal/TextArea.vue'
153157
import AppointmentConfig from '../models/appointmentConfig.js'
@@ -177,6 +181,7 @@ export default {
177181
Confirmation,
178182
NcButton,
179183
NcCheckboxRadioSwitch,
184+
NcNoteCard,
180185
},
181186
props: {
182187
config: {
@@ -194,6 +199,7 @@ export default {
194199
enablePreparationDuration: false,
195200
enableFollowupDuration: false,
196201
enableFutureLimit: false,
202+
rateLimitingReached: false,
197203
showConfirmation: false,
198204
}
199205
},
@@ -282,6 +288,8 @@ export default {
282288
this.editing.calendarFreeBusyUris = this.editing.calendarFreeBusyUris.filter(uri => uri !== this.calendarUrlToUri(calendar.url))
283289
},
284290
async save() {
291+
this.rateLimitingReached = false
292+
285293
if (!this.enablePreparationDuration) {
286294
this.editing.preparationDuration = this.defaultConfig.preparationDuration
287295
}
@@ -307,6 +315,9 @@ export default {
307315
}
308316
this.showConfirmation = true
309317
} catch (error) {
318+
if (error?.response?.status === 429) {
319+
this.rateLimitingReached = true
320+
}
310321
logger.error('Failed to save config', { error, config, isNew: this.isNew })
311322
}
312323
},

Diff for: src/components/Appointments/AppointmentDetails.vue

+13-3
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,14 @@
5353
:disabled="isLoading" />
5454
</div>
5555
</div>
56-
<div v-if="showError"
57-
class="booking-error">
56+
<NcNoteCard v-if="showRateLimitingWarning"
57+
type="warning">
58+
{{ $t('calendar', 'It seems a rate limit has been reached. Please try again later.') }}
59+
</NcNoteCard>
60+
<NcNoteCard v-if="showError"
61+
type="error">
5862
{{ $t('calendar', 'Could not book the appointment. Please try again later or contact the organizer.') }}
59-
</div>
63+
</NcNoteCard>
6064
<div class="buttons">
6165
<NcLoadingIcon v-if="isLoading" :size="32" class="loading-icon" />
6266
<NcButton type="primary" :disabled="isLoading" @click="save">
@@ -73,6 +77,7 @@ import {
7377
NcButton,
7478
NcLoadingIcon,
7579
NcModal as Modal,
80+
NcNoteCard,
7681
} from '@nextcloud/vue'
7782
import autosize from '../../directives/autosize.js'
7883
@@ -85,6 +90,7 @@ export default {
8590
NcButton,
8691
NcLoadingIcon,
8792
Modal,
93+
NcNoteCard,
8894
},
8995
directives: {
9096
autosize,
@@ -110,6 +116,10 @@ export default {
110116
required: true,
111117
type: String,
112118
},
119+
showRateLimitingWarning: {
120+
required: true,
121+
type: Boolean,
122+
},
113123
showError: {
114124
required: true,
115125
type: Boolean,

Diff for: src/views/Appointments/Booking.vue

+8-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
:visitor-info="visitorInfo"
7777
:time-zone-id="timeZone"
7878
:show-error="bookingError"
79+
:show-rate-limiting-warning="bookingRateLimit"
7980
:is-loading="bookingLoading"
8081
@save="onSave"
8182
@close="selectedSlot = undefined" />
@@ -172,6 +173,7 @@ export default {
172173
bookingConfirmed: false,
173174
bookingError: false,
174175
bookingLoading: false,
176+
bookingRateLimit: false,
175177
}
176178
},
177179
watch: {
@@ -229,6 +231,7 @@ export default {
229231
})
230232

231233
this.bookingError = false
234+
this.bookingRateLimit = false
232235
try {
233236
await bookSlot(this.config, slot, displayName, email, description, timeZone)
234237

@@ -238,7 +241,11 @@ export default {
238241
this.bookingConfirmed = true
239242
} catch (e) {
240243
console.error('could not book appointment', e)
241-
this.bookingError = true
244+
if (e?.response?.status === 429) {
245+
this.bookingRateLimit = true
246+
} else {
247+
this.bookingError = true
248+
}
242249
} finally {
243250
this.bookingLoading = false
244251
}

Diff for: tests/psalm-baseline.xml

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<files psalm-version="5.15.0@5c774aca4746caf3d239d9c8cadb9f882ca29352">
2+
<files psalm-version="5.19.0@06b71be009a6bd6d81b9811855d6629b9fe90e1b">
33
<file src="lib/AppInfo/Application.php">
44
<MissingDependency>
55
<code>CalendarWidgetV2</code>
@@ -13,6 +13,15 @@
1313
<code>$expectedDayKeys !== $actualDayKeys</code>
1414
<code><![CDATA[$slotKeys !== ['end', 'start']]]></code>
1515
</RedundantCondition>
16+
<UndefinedAttributeClass>
17+
<code>UserRateLimit</code>
18+
</UndefinedAttributeClass>
19+
</file>
20+
<file src="lib/Controller/BookingController.php">
21+
<UndefinedAttributeClass>
22+
<code>AnonRateLimit</code>
23+
<code>UserRateLimit</code>
24+
</UndefinedAttributeClass>
1625
</file>
1726
<file src="lib/Dashboard/CalendarWidgetV2.php">
1827
<UndefinedClass>

0 commit comments

Comments
 (0)