-
Notifications
You must be signed in to change notification settings - Fork 170
Web
web
is the service responsible for serving static content for The Blue Alliance. Although web
handles the majority of responsibility in The Blue Alliance's stack, it's responsibilities should be limited to serving web pages and read-only actions. Create or update actions should be delegate to other services.
Components in web
(GameDay, login, etc.) make calls to Firebase and need to have Firebase keys set in order to work properly. Keys are referenced from a tba_keys.js
file. This file is not checked in to source control, but an template of the file is. You can copy the template and add your own keys to the file.
$ cp src/backend/web/static/javascript/tba_js/tba_keys_template.js src/backend/web/static/javascript/tba_js/tba_keys.js
Edit the fields specified in the file and save. If you're using the development container, make sure to sync this file to the container. Finally, rebuild web resources to compile the secrets file with the Javascript.
If you make changes to JavaScript or CSS files for the web
service, you will have to recompile the files in order for the changes to show up in your browser. After syncing changes from your local environment to the development container, run the run_buildweb.sh
script from inside the development container.
$ ./ops/build/run_buildweb.sh
Redirecting to a page via the next
URL parameter is a pattern used in The Blue Alliance codebase. Since this next
parameter can be modified by a user, caution should be taken to ensure the next
parameter is redirecting back to The Blue Alliance as expected before redirecting the user. redirect
offers some helpful commands for safely redirecting.
Method | Description |
---|---|
is_safe_url |
Given a URL, check that the URL redirects back to the current host and is a HTTP URL. |
safe_next_redirect |
Get a redirect Response from the next parameter. If the next parameter is not valid, the Response will be a redirect to the given fallback URL. |
from backend.web.redirect import is_safe_url
def route() -> Response:
next = request.args.get("next")
# Make sure `next` is safe - otherwise drop it
next = next if is_safe_url(next) else None
return make_response(render_template(..., next=next))
from backend.web.redirect import safe_next_redirect
def logout() -> Response:
# Do some logout stuff here. Redirect to the `next` page. Fallback to the index page.
return safe_next_redirect("/")
In some cases, data might want to be passed between pages. For example, a form flow with multiple steps or an action that has a failure state might want to use the state from the previous page when loading the next page. This data can be passed using the Flask session. Session data is stored cryptographically in cookies and is safe to store secret or privileged information for a given user.
from flask import request, session
def route() -> Response:
if request.method == "POST":
error = some_failable_work()
if error:
session["route_error"] = "work_failed"
return redirect(url_for("route"))
return make_response(
render_template(
..., status=session.pop("route_error", None)
)
)
The web
service uses Flask WTF to enable CSRF protection. Since The Blue Alliance does not leverage FlaskForm
, the CSRF token must be passed manually via the form. Form POSTs to the web
service without a CSRF token will throw an exception.
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>
Use the authentication decorators to ensure proper permission for a user for a given route.
Method | Description |
---|---|
require_login |
Ensures that a user is currently logged in and registered. If the user is not logged in, this method will automatically redirect the user to the login page and after logging in will redirect the user back to the original page. If the user is not registered, this method will automatically redirect the user to the registration page and after registering will redirect the user back to the original page. |
require_login_only |
Like require_login but does not require that a user is registered. |
enforce_login |
Ensures that a user is currently logged in - otherwise, this method will throw a 401. |
require_admin |
Ensures that a user is an admin. If the user is not logged in, this method will automatically redirect the user to the login page and after logging in will redirect the user back to the original page. |
require_permission |
Ensures that a user has a given permission. If the user is not logged in, this method will automatically redirect the user to the login page and after logging in will redirect the user back to the original page. If the user does not have the given permission, this method will throw a 401. |
require_any_permission |
Ensures that a user has any of the given permission. If the user is not logged in, this method will automatically redirect the user to the login page and after logging in will redirect the user back to the original page. If the user does not have any of the given permission, this method will throw a 401. |
from backend.web.decorators import require_login
@require_login
def route() -> str:
return render_template(...)
See the common page for details on how to obtain a user object through current_user()
. The User
object from current_user()
is available in the HTML templates via the user
property. If being used on a page that is not gated behind an authentication decorator, this property may be null. If the route is protected via the authentication decorators, the current_user()
call can be wrapped in a none_throws
call to ensure the object is not None
.
from pyre_extensions import none_throws
from backend.common.auth import current_user
from backend.web.decorators import require_login
@require_login
def route() -> str:
user = none_throws(current_user())
return render_template(..., api_read_keys=user.api_read_keys)
{% if user %}
<p>Welcome back, {{ user.display_name }}</p>
{% else %}
<a href="login">Login</a>
{% endif }
Requests that act on user data or on behalf of a user should be careful that the executing user has the proper permissions to perform the requested action. As previously mentioned, POST requests should pass a csrf_token
. Required information should be fetched for only the currently logged in user. In some cases, a POST request might pass a user ID using the user.uid
property in the template to ensure that the user is acting upon their own data when the request is made when relying on the currently logged in user is not enough validation (ex: editing account info).
Ex: Consider deleting an API key. A form might POST the API key to delete. The query to fetch the API key to delete should fetch the API key for the passed API key identifier for the currently logged in user. Failing to query the API key scoped to the given user would allow any user to delete any API key, regardless of if they're the owner of the API key.
The User
model exposes several methods and properties that fetch data scoped for the user, and are considered safe. These methods should be preferred as opposed to writing custom queries or logic.
A login_user
pytest fixture is available for stubbing the current_user
function. current_user
will return a User
Mock that can be used for verifying functionality.
def test_register_account(login_user) -> None:
login_user.is_registered = False
with patch.object(login_user, "register") as mock_register:
...
mock_register.assert_called_with("Zach")