Loading LICENSE-3RD-PARTY.md +43 −0 Changes for LICENSE-3RD-PARTY.md: 43 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -30,3 +30,46 @@ - Author: Leo Horie - URL: https://github.com/MithrilJS/mithril.js - License: [MIT License](portal/static/lib/mithril/LICENSE) ## flaskr (Flask Tutorial App) Some files in this project are more or less modified versions of the files from the Flask Tutorial App `flaskr`, which is available at: - https://github.com/pallets/flask/tree/2.0.3/examples/tutorial - https://flask.palletsprojects.com/en/2.0.x/tutorial/ - https://github.com/pallets/flask/tree/2.2.x/examples/tutorial - https://flask.palletsprojects.com/en/2.2.x/tutorial/tests/ It is licensed under the following license (see also https://flask.palletsprojects.com/en/2.0.x/license/): ### BSD-3-Clause License Copyright 2010 Pallets Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. alembic/versions/d3889051d9e7_add_users_and_keys.py 0 → 100644 +53 −0 Changes for alembic/versions/d3889051d9e7_add_users_and_keys.py: 53 added lines, 0 removed lines. Original line number Diff line number Diff line """Add users and keys Revision ID: d3889051d9e7 Revises: 2a60cb3fb390 Create Date: 2024-06-04 16:07:36.652116 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = "d3889051d9e7" down_revision: Union[str, None] = "2a60cb3fb390" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None from portal.model import Uuid, UtcDateTime def upgrade() -> None: op.create_table( "user", sa.Column("name", sa.String(), nullable=False), sa.Column("uuid", Uuid(), nullable=False), sa.PrimaryKeyConstraint("uuid"), ) op.create_index(op.f("ix_user_name"), "user", ["name"], unique=True) op.create_table( "key", sa.Column("secret_hash", sa.String(length=256), nullable=False), sa.Column("created_at", UtcDateTime(), nullable=False), sa.Column("expires_at", UtcDateTime(), nullable=False), sa.Column("user_uuid", Uuid(), nullable=False), sa.Column("uuid", Uuid(), nullable=False), sa.ForeignKeyConstraint( ["user_uuid"], ["user.uuid"], ), sa.PrimaryKeyConstraint("uuid"), ) op.create_index(op.f("ix_key_secret_hash"), "key", ["secret_hash"], unique=True) def downgrade() -> None: op.drop_index(op.f("ix_key_secret_hash"), table_name="key") op.drop_table("key") op.drop_index(op.f("ix_user_name"), table_name="user") op.drop_table("user") portal/api/__init__.py +1 −0 Changes for portal/api/__init__.py: 1 added line, 0 removed lines. Original line number Diff line number Diff line from .blueprint import bp from .auth import * from .rest import * from .version import * portal/api/auth.py 0 → 100644 +83 −0 Changes for portal/api/auth.py: 83 added lines, 0 removed lines. Original line number Diff line number Diff line # Login code adapted from the flaskr tutorial (see LICENSE-3RD-PARTY.md for details) # See https://flask.palletsprojects.com/en/2.0.x/tutorial/views/ import functools from flask import request, g from portal import db from .blueprint import bp from ..model import User, Key from ..problem_details import not_found, unauthorized @bp.before_app_request def set_user_if_valid(): if not request.authorization or request.authorization.type != "basic": g.user = None return g.user = User.authenticate( request.authorization.username, request.authorization.password ) @bp.before_app_request def set_key_if_valid(): if not request.authorization or request.authorization.type != "bearer": g.key = None return g.key = Key.get_by_secret_unless_expired(request.authorization.token) def require_valid(credential_type): """ Requires that valid credentials are provided before allowing access to a route. If no valid credentials are provided, abort and send a 401. This decorator can be used in two ways: - `@require_valid(User)` to require a valid username and password - `@require_valid(Key)` to require a valid key """ def decorator(route): @functools.wraps(route) def protected_route(**kwargs): # Determine which credentials to use credentials = {User: g.user, Key: g.key}.get(credential_type) if credentials is None: return unauthorized() else: # If g.user or g.key is set, we can trust they are valid. return route(**kwargs) return protected_route return decorator @bp.post("/keys") @require_valid(User) def post_key(): key, secret = Key.generate(g.user) return key.to_dict() | {"secret": secret}, 201 @bp.delete("/keys/<string:uuid>") @require_valid(Key) def delete_key(uuid: str): key_to_delete = Key.get_only(uuid) if not key_to_delete: return not_found() if key_to_delete.user_uuid != g.key.user_uuid: # If the key exists, but is from a different user, we pretend it isn't there return not_found() db.session.delete(key_to_delete) db.session.commit() return "", 204 portal/api/rest.py +12 −0 Changes for portal/api/rest.py: 12 added lines, 0 removed lines. Original line number Diff line number Diff line from flask import render_template from .blueprint import bp from ..problem_details import not_found @bp.get("/<path:_>") def fallback_not_found(_): """ If you can't find a matching API endpoint, fallback to returning a 404. We completely ignore the provided path (hence the argument name "_" and not something useful), because if this method is ever reached, we already know the only viable response is a 404. """ return not_found() @bp.get("/") Loading Loading
LICENSE-3RD-PARTY.md +43 −0 Changes for LICENSE-3RD-PARTY.md: 43 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -30,3 +30,46 @@ - Author: Leo Horie - URL: https://github.com/MithrilJS/mithril.js - License: [MIT License](portal/static/lib/mithril/LICENSE) ## flaskr (Flask Tutorial App) Some files in this project are more or less modified versions of the files from the Flask Tutorial App `flaskr`, which is available at: - https://github.com/pallets/flask/tree/2.0.3/examples/tutorial - https://flask.palletsprojects.com/en/2.0.x/tutorial/ - https://github.com/pallets/flask/tree/2.2.x/examples/tutorial - https://flask.palletsprojects.com/en/2.2.x/tutorial/tests/ It is licensed under the following license (see also https://flask.palletsprojects.com/en/2.0.x/license/): ### BSD-3-Clause License Copyright 2010 Pallets Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
alembic/versions/d3889051d9e7_add_users_and_keys.py 0 → 100644 +53 −0 Changes for alembic/versions/d3889051d9e7_add_users_and_keys.py: 53 added lines, 0 removed lines. Original line number Diff line number Diff line """Add users and keys Revision ID: d3889051d9e7 Revises: 2a60cb3fb390 Create Date: 2024-06-04 16:07:36.652116 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = "d3889051d9e7" down_revision: Union[str, None] = "2a60cb3fb390" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None from portal.model import Uuid, UtcDateTime def upgrade() -> None: op.create_table( "user", sa.Column("name", sa.String(), nullable=False), sa.Column("uuid", Uuid(), nullable=False), sa.PrimaryKeyConstraint("uuid"), ) op.create_index(op.f("ix_user_name"), "user", ["name"], unique=True) op.create_table( "key", sa.Column("secret_hash", sa.String(length=256), nullable=False), sa.Column("created_at", UtcDateTime(), nullable=False), sa.Column("expires_at", UtcDateTime(), nullable=False), sa.Column("user_uuid", Uuid(), nullable=False), sa.Column("uuid", Uuid(), nullable=False), sa.ForeignKeyConstraint( ["user_uuid"], ["user.uuid"], ), sa.PrimaryKeyConstraint("uuid"), ) op.create_index(op.f("ix_key_secret_hash"), "key", ["secret_hash"], unique=True) def downgrade() -> None: op.drop_index(op.f("ix_key_secret_hash"), table_name="key") op.drop_table("key") op.drop_index(op.f("ix_user_name"), table_name="user") op.drop_table("user")
portal/api/__init__.py +1 −0 Changes for portal/api/__init__.py: 1 added line, 0 removed lines. Original line number Diff line number Diff line from .blueprint import bp from .auth import * from .rest import * from .version import *
portal/api/auth.py 0 → 100644 +83 −0 Changes for portal/api/auth.py: 83 added lines, 0 removed lines. Original line number Diff line number Diff line # Login code adapted from the flaskr tutorial (see LICENSE-3RD-PARTY.md for details) # See https://flask.palletsprojects.com/en/2.0.x/tutorial/views/ import functools from flask import request, g from portal import db from .blueprint import bp from ..model import User, Key from ..problem_details import not_found, unauthorized @bp.before_app_request def set_user_if_valid(): if not request.authorization or request.authorization.type != "basic": g.user = None return g.user = User.authenticate( request.authorization.username, request.authorization.password ) @bp.before_app_request def set_key_if_valid(): if not request.authorization or request.authorization.type != "bearer": g.key = None return g.key = Key.get_by_secret_unless_expired(request.authorization.token) def require_valid(credential_type): """ Requires that valid credentials are provided before allowing access to a route. If no valid credentials are provided, abort and send a 401. This decorator can be used in two ways: - `@require_valid(User)` to require a valid username and password - `@require_valid(Key)` to require a valid key """ def decorator(route): @functools.wraps(route) def protected_route(**kwargs): # Determine which credentials to use credentials = {User: g.user, Key: g.key}.get(credential_type) if credentials is None: return unauthorized() else: # If g.user or g.key is set, we can trust they are valid. return route(**kwargs) return protected_route return decorator @bp.post("/keys") @require_valid(User) def post_key(): key, secret = Key.generate(g.user) return key.to_dict() | {"secret": secret}, 201 @bp.delete("/keys/<string:uuid>") @require_valid(Key) def delete_key(uuid: str): key_to_delete = Key.get_only(uuid) if not key_to_delete: return not_found() if key_to_delete.user_uuid != g.key.user_uuid: # If the key exists, but is from a different user, we pretend it isn't there return not_found() db.session.delete(key_to_delete) db.session.commit() return "", 204
portal/api/rest.py +12 −0 Changes for portal/api/rest.py: 12 added lines, 0 removed lines. Original line number Diff line number Diff line from flask import render_template from .blueprint import bp from ..problem_details import not_found @bp.get("/<path:_>") def fallback_not_found(_): """ If you can't find a matching API endpoint, fallback to returning a 404. We completely ignore the provided path (hence the argument name "_" and not something useful), because if this method is ever reached, we already know the only viable response is a 404. """ return not_found() @bp.get("/") Loading