Skip to content

Commit

Permalink
new: [API] New endpoint to let an administrator delete a user and a n…
Browse files Browse the repository at this point in the history
…ew endpoint in order to return information about the currently authenticated user (GET /user/me)
  • Loading branch information
cedricbonhomme committed Jul 31, 2024
1 parent 54db9ad commit 2cbcbc5
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 11 deletions.
62 changes: 62 additions & 0 deletions website/web/api/v1/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@

import logging
from sqlalchemy import exc
from flask_login import current_user # type: ignore[import-untyped]
from flask_restx import fields # type: ignore[import-untyped]
from flask_restx import abort
from flask_restx import Namespace
from flask_restx import reqparse
from flask_restx import Resource
from sqlalchemy import delete

from website.models import Bundle
from website.models import Comment
from website.models import User
from website.notifications import notifications
from website.web.bootstrap import db
Expand Down Expand Up @@ -72,6 +76,64 @@
)


@user_ns.route("/user/me")
class UserSelf(Resource): # type: ignore[misc]
@user_ns.doc(description="Get information about the currently authenticated user.") # type: ignore[misc]
@user_ns.doc(
responses={
200: "Success.",
}
) # type: ignore[misc]
@user_ns.marshal_with(user, skip_none=True) # type: ignore[misc]
def get(self) -> Tuple[dict[Any, Any], int]:
"Get information about the currently authenticated user."
me = User.query.filter(
User.id == current_user.id,
User.is_active == True,
User.is_confirmed == True,
).first()
if not me:
return {"message": "User not found."}, 404
return me, 200


@user_ns.route("/user/<string:user_id>")
class UserItem(Resource): # type: ignore[misc]
@user_ns.doc(description="Delete a user.") # type: ignore[misc]
@user_ns.doc(
responses={
204: "Success.",
403: "Administrator permission required or not the current user.",
404: "User not found.",
}
) # type: ignore[misc]
@auth_func
def delete(self, user_id: str) -> Tuple[dict[Any, Any], int]:
"""Endpoint for deleting a comment."""
user = User.query.filter(User.id == user_id).first()
if not user:
return {"message": "User not found."}, 404

if not current_user.is_admin and current_user.id != user.id:
return abort(403, "You cannot delete this user.")

if current_user.is_admin:
# Delete contributed comments
statement = delete(Comment).where(Comment.author_id == user_id)
db.session.execute(statement)
# Delete contributed bundles
statement = delete(Bundle).where(Bundle.author_id == user_id)
db.session.execute(statement)
# Delete the user
db.session.delete(user)
db.session.commit()
else:
user.is_active = False
db.session.commit()

return {}, 204


@user_ns.route("/user/")
class UsersList(Resource): # type: ignore[misc]
@user_ns.doc("list_users") # type: ignore[misc]
Expand Down
4 changes: 2 additions & 2 deletions website/web/templates/user/edit_user.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ <h1>Profile</h1>
</div>
</div>
<hr /><br />
<a href="{{ url_for('user_bp.delete_account', user_id=user.id) }}" class="btn btn-warning" onclick="return confirm('You are going to delete your account.');">Delete your account</a>
<a href="{{ url_for('user_bp.delete_account') }}" class="btn btn-warning" onclick="return confirm('You are going to delete your account.');">Delete your account</a>
{% if not config["ENFORCE_2FA"] %}
<a href="{{ url_for('user_bp.toggle_2FA', user_id=user.id) }}" class="btn btn-warning">{% if user.is_two_factor_authentication_enabled %}Disable{% else %}Enable{% endif %} 2FA</a>
<a href="{{ url_for('user_bp.toggle_2FA') }}" class="btn btn-warning">{% if user.is_two_factor_authentication_enabled %}Disable{% else %}Enable{% endif %} 2FA</a>
{% endif %}
<br /><br />
<p>Deleting your account will not impact any of the contributions you have previously made.</p>
Expand Down
10 changes: 7 additions & 3 deletions website/web/views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,23 @@ def dashboard() -> str:
nb_bundles=Bundle.query.count(),
local_instance_uuid=get_config("generic", "local_instance_uuid"),
local_instance_name=get_config("generic", "local_instance_name"),
storage_info = vulnerabilitylookup.get_info(),
storage_info=vulnerabilitylookup.get_info(),
)


#
# Users
#


@admin_bp.route("/users", methods=["GET"])
@login_required # type: ignore[misc]
@admin_permission.require(http_exception=403) # type: ignore[misc]
def list_users() -> str:
total_nb_users = User.query.count()
nb_users_without_2fa = User.query.filter(User.is_two_factor_authentication_enabled==False).count()
nb_users_without_2fa = User.query.filter(
User.is_two_factor_authentication_enabled == False
).count()
# Load normal users and configure the associated pagination element
users = (
User.query.filter()
Expand Down Expand Up @@ -255,6 +258,7 @@ def delete_user(user_id: int) -> WerkzeugResponse:
# Comments
#


@admin_bp.route("/comments", methods=["GET"])
@login_required # type: ignore[misc]
@admin_permission.require(http_exception=403) # type: ignore[misc]
Expand Down Expand Up @@ -320,11 +324,11 @@ def delete_comment(comment_uuid: str) -> WerkzeugResponse:
return redirect(url_for("admin_bp.list_comments"))



#
# Bundles
#


@admin_bp.route("/bundles", methods=["GET"])
@login_required # type: ignore[misc]
@admin_permission.require(http_exception=403) # type: ignore[misc]
Expand Down
16 changes: 12 additions & 4 deletions website/web/views/session_mgmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ def login() -> str | WerkzeugResponse:
In case of successful authentication the user is redirected to the
Two-Factor Authentication page."""
if current_user.is_authenticated:
if not application.config["ENFORCE_2FA"] or current_user.is_two_factor_authentication_enabled:
if (
not application.config["ENFORCE_2FA"]
or current_user.is_two_factor_authentication_enabled
):
flash("You are already logged in.", "info")
return redirect(url_for("user_bp.form"))
else:
Expand All @@ -105,7 +108,10 @@ def login() -> str | WerkzeugResponse:
User.is_active == True,
User.is_confirmed == True,
).first()
if application.config["ENFORCE_2FA"] and not user.is_two_factor_authentication_enabled:
if (
application.config["ENFORCE_2FA"]
and not user.is_two_factor_authentication_enabled
):
# 2FA enforced and is not enabled for this user: redirect to 2FA setup
flash(
"You have not enabled 2-Factor Authentication. Please enable first to login.",
Expand All @@ -115,7 +121,10 @@ def login() -> str | WerkzeugResponse:
elif application.config["ENFORCE_2FA"]:
# 2FA enforced and is enabled for this user: redirect to 2FA verification
return redirect(url_for("user_bp.verify_two_factor_auth"))
elif not application.config["ENFORCE_2FA"] and user.is_two_factor_authentication_enabled:
elif (
not application.config["ENFORCE_2FA"]
and user.is_two_factor_authentication_enabled
):
# 2FA is not enforced but enabled for this user: redirect to 2FA verification
return redirect(url_for("user_bp.verify_two_factor_auth"))
else:
Expand All @@ -124,7 +133,6 @@ def login() -> str | WerkzeugResponse:
login_user_bundle(user)
return redirect(url_for("user_bp.form"))


return render_template("user/login.html", form=form)


Expand Down
9 changes: 7 additions & 2 deletions website/web/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ def toggle_2FA() -> WerkzeugResponse:
user = User.query.filter(User.id == current_user.id).first()
if user is None:
abort(404)
user.is_two_factor_authentication_enabled = not user.is_two_factor_authentication_enabled
user.is_two_factor_authentication_enabled = (
not user.is_two_factor_authentication_enabled
)
db.session.commit()
if user.is_two_factor_authentication_enabled:
session["username"] = user.login
Expand Down Expand Up @@ -218,7 +220,10 @@ def confirm_account(token: str = "") -> str | WerkzeugResponse:
flash("Password must be the same.", "danger")
return render_template("user/account_recovery_set_password.html", form=form)

if application.config["ENFORCE_2FA"] and not user.is_two_factor_authentication_enabled:
if (
application.config["ENFORCE_2FA"]
and not user.is_two_factor_authentication_enabled
):
session["username"] = user.login
return redirect(url_for("user_bp.setup_two_factor_auth"))

Expand Down

0 comments on commit 2cbcbc5

Please sign in to comment.