Skip to content
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

feat(printing): add rate limit to print job requests (closes tjcsl#662) #1666

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from

Conversation

aarushtools
Copy link
Contributor

Proposed changes

  • Add a rate limit of 10 POST requests per minute to ion printing
  • Fix some linting errors from previous contributors in the printing/views.py file so checks can pass

Brief description of rationale

Blocks excessive printing by users to prevent abuse or overload of the printers

@coveralls
Copy link

coveralls commented Apr 28, 2024

Coverage Status

coverage: 79.342% (-0.06%) from 79.406%
when pulling ffcb59a on aarushtools:dev
into 94f02ed on tjcsl:dev.

Copy link
Member

@NotFish232 NotFish232 left a comment

Choose a reason for hiding this comment

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

Hi Aarush, thanks for the PR! Nice implementation with SImpleRateThrottle. I have a couple of suggestions.

  1. A rate of 10/minute is probably too lenient, maybe try something more along the lines of a request every 5 minutes?
  2. The current implementation ratelimits even if the request is not successful, i.e. the form is invalid. If a user tries to print and accidentally selects the wrong options (without an actual print job requested), they shouldn't have to wait for the ratelimit to expire.
    Let us know if you need any help with this!

@alanzhu0
Copy link
Member

alanzhu0 commented May 2, 2024

I think 2 requests / 5 minutes is a good ratelimit for successful POST requests.

I don't think turning the printing view into an API view is a good idea. It doesn't give a good error screen when the requests are throttled. Instead, the user should be presented with a good error screen, perhaps using the messages module we already have, and told they cannot print more than twice every 5 minutes. Perhaps you will find another package to accomplish this, but keep in mind only successful POST requests should be tracked. Otherwise, you may need to write your own backend and model for this.

@aarushtools
Copy link
Contributor Author

I tried to use django-ratelimit but decided that the package is just not meant to do what I was looking for and decided to use caching to make a sort of DIY ratelimit. I implemented the django cache system into two functions that I check for directly in the print_job function. The cache key holds an int that is incremented each time a request is made, and it expires in 300 seconds or 5 minutes.

@aarushtools aarushtools changed the title feat(printing): add rate limiting to printing (closes tjcsl#662) feat(printing): add rate limit to print job requests (closes tjcsl#662) May 3, 2024
Copy link
Member

@alanzhu0 alanzhu0 left a comment

Choose a reason for hiding this comment

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

This is good! Can you make the ratelimit a setting variable, instead of directly hardcoded? For example,

# 2 print jobs every 5 minutes
PRINT_RATELIMIT_FREQUENCY = 2
PRINT_RATELIMIT_MINUTES = 5

would control the ratelimit behavior.

@aarushtools
Copy link
Contributor Author

Done 👍
I put those variables in the main settings file for everything

@alanzhu0
Copy link
Member

This is great! Last few comments:

  • Can you write some text on the printing page alerting users to the ratelimit? Use the variables from the settings file.
  • Can you make the ratelimit not apply to administrators (admin_all)? Still send a message along the lines of "you have reached the ratelimit of 2 req / 5 min, but since you are an administrator it will still go through." (for debugging purposes).
  • Same thing for teachers - except don't send the message. So no ratelimit at all for teachers. While you're at it, maybe increase the number of pages teachers can print? 50 is probably a good value. That can go in settings too.

@aarushtools
Copy link
Contributor Author

aarushtools commented May 18, 2024

Everything implemented except for point 2 (partially). I use logger.error() to send a message since messages.error requires request as a property, which I don't have access to in the print_job function. I could add request to the obj object or as a param for the print_job func, but I think that isn't really necessary (but I can still do it if you believe it's the best way)

@alanzhu0
Copy link
Member

That's fine. Good call. Almost ready to merge, I'm assuming you'll fix CI, last thing it looks like "WizardoWaldo" (your alt?) is included in the commit. I don't really care about that but if you do you might want to take that off.

@aarushtools
Copy link
Contributor Author

Yea, sorry about that I'm trying to fix it. Pushing using the PyCharm gui did that for some reason

@aarushtools
Copy link
Contributor Author

I'm not sure why the linting is failing. It seems like it has to do with modules I never edited?

@@ -23,6 +23,7 @@
<b>The following restrictions apply:</b>
<ul>
<li>Print jobs can have a maximum of {{ DJANGO_SETTINGS.PRINTING_PAGES_LIMIT }} pages.</li>
Copy link
Member

Choose a reason for hiding this comment

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

You need to change this to reflect the new teacher/student settings.

@@ -23,6 +23,7 @@
<b>The following restrictions apply:</b>
<ul>
<li>Print jobs can have a maximum of {{ DJANGO_SETTINGS.PRINTING_PAGES_LIMIT }} pages.</li>
<li>You can send {{ DJANGO_SETTINGS.PRINT_RATELIMIT_FREQUENCY}} print jobs every {{ DJANGO_SETTINGS.PRINT_RATELIMIT_MINUTES }} minutes.</li>
Copy link
Member

Choose a reason for hiding this comment

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

Small reword - You may send up to XX print jobs every XX minutes.

if get_user_ratelimit_status(obj.user.username):
# Bypass rate limit for admins but still send error message for debugging purposes
if obj.user.has_admin_permission("printing"):
logger.error(
Copy link
Member

Choose a reason for hiding this comment

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

logger.debug is more appropriate here

@@ -349,17 +380,44 @@ def print_job(obj: PrintJob, do_print: bool = True):
if obj.page_range:
if not range_count:
raise InvalidInputPrintingError("You specified an invalid page range.")
elif range_count > settings.PRINTING_PAGES_LIMIT:
elif range_count > settings.PRINTING_PAGES_LIMIT_STUDENTS and obj.user.is_student:
Copy link
Member

Choose a reason for hiding this comment

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

There are user types other than students and teachers. Apply the teacher limit to teachers and counselors (and, while we're at it, if the user is an admin they can get the teacher limit too). Apply the student limit to everyone else. Same logic for ratelimiting. No ratelimit for teachers and counselors. Ratelimit for everyone else (except admins).

Copy link
Member

Choose a reason for hiding this comment

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

Actually, Justin reminded me is_teacher checks for teacher or counselor. So that's ok. But you will still need to cover other user types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just made the first if statement check for printing admin perms or if they are a teacher, and anyone else gets the student limit because of the order the elifs appear in

Copy link
Member

@alanzhu0 alanzhu0 left a comment

Choose a reason for hiding this comment

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

Very very close. Sorry for all the comments about small details.

Can you do a favor for me? user.has_print_permission is a weird property. All the other admin ones are named like user.is_printing_admin. Could you global replace has_print_permission with is_printing_admin? This isn't strictly related to your PR but it'd be a good change long-term for Ion.

{% else %}
<li>Print jobs can have a maximum of {{ DJANGO_SETTINGS.PRINTING_PAGES_LIMIT_STUDENTS }} pages.</li>
{% endif %}
{% if user.is_student %}
Copy link
Member

Choose a reason for hiding this comment

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

Small nitpick. Following convention this should be not user.is_teacher and not user.has_print_permission

@aarushtools
Copy link
Contributor Author

Very very close. Sorry for all the comments about small details.

No worries, hopefully it's all good now 👍

settings.PRINT_RATELIMIT_MINUTES,
)
# If user needs to be rate limited
elif obj.user.is_student: # Don't rate limit teachers
Copy link
Member

Choose a reason for hiding this comment

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

This goes back to what we were discussing earlier - if printing admin or teacher, bypass, else apply ratelimit logic. Not just students.

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.

None yet

4 participants