diff --git a/epauth/views.py b/epauth/views.py index d0243f7d..e21d6cdd 100644 --- a/epauth/views.py +++ b/epauth/views.py @@ -1,3 +1,5 @@ +import logging + import msal from django.conf import settings as s from django.contrib.auth import get_user_model @@ -8,6 +10,7 @@ from django.shortcuts import redirect from epdb.logic import UserManager, GroupManager from epdb.models import Group +auth_log = logging.getLogger("auth") def get_msal_app_with_cache(request): """ @@ -28,8 +31,21 @@ def get_msal_app_with_cache(request): return msal_app, cache +def get_remote_address(request): + remote_address = "" + + if request is not None: + remote_address = request.META.get("HTTP_X_FORWARDED_FOR") + + if not remote_address: + remote_address = request.META.get("REMOTE_ADDR", "") + + return remote_address def entra_login(request): + + auth_log.info(f"Login request from {get_remote_address(request)}") + msal_app = msal.ConfidentialClientApplication( client_id=s.MS_ENTRA_CLIENT_ID, client_credential=s.MS_ENTRA_CLIENT_SECRET, @@ -54,6 +70,10 @@ def entra_callback(request): # Acquire token using the flow and callback request result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET) + if "error" in result: + auth_log.error(f"Login attempt by {get_remote_address(request)} failed due to {result['error']}") + return redirect("/") + # Save the token cache to session if cache.has_state_changed: request.session["msal_token_cache"] = cache.serialize() @@ -61,7 +81,8 @@ def entra_callback(request): claims = result["id_token_claims"] user_name = claims.get("name") - user_email = claims.get("emailaddress", claims.get("email")) + # preferred_username is a fallback for 2nd CWID + user_email = claims.get("emailaddress", claims.get("email", claims.get("preferred_username"))) user_oid = claims.get("oid") if not all([user_name, user_email, user_oid]): @@ -78,8 +99,10 @@ def entra_callback(request): u.save() else: + auth_log.info(f"Registering {user_name} with OID {user_oid}") u = UserManager.create_user(user_name, user_email, None, uuid=user_oid, is_active=True) + auth_log.info(f"User {user_name} {"(admin)" if u.is_superuser else ""} with OID {user_oid} successfully logged in as {u.username} from {get_remote_address(request)}") login(request, u) # EDIT START @@ -102,10 +125,20 @@ def entra_callback(request): else: g = Group.objects.get(uuid=id) - for group_uuid in claims.get("groups", []): - if Group.objects.filter(uuid=group_uuid).exists(): - g = Group.objects.get(uuid=group_uuid) - g.user_member.add(u) + sync_groups = list(s.ENTRA_GROUPS.keys()) + list(s.ENTRA_SECRET_GROUPS.keys()) + user_groups = claims.get("groups", []) + + for uuid in sync_groups: + if uuid in user_groups: + g = Group.objects.get(uuid=uuid) + if not g.user_member.contains(u): + g.user_member.add(u) + auth_log.info(f"Login Group Sync: Adding {u.username} to Group {g.name} ({ uuid })") + else: + g = Group.objects.get(uuid=uuid) + if g.user_member.contains(u): + g.user_member.remove(u) + auth_log.info(f"Login Group Sync: Removing {u.username} from Group {g.name} ({ uuid })") # EDIT END diff --git a/epdb/logic.py b/epdb/logic.py index 5a9297e6..77175ab7 100644 --- a/epdb/logic.py +++ b/epdb/logic.py @@ -35,6 +35,7 @@ from utilities.chem import FormatConverter from utilities.misc import PackageExporter, PackageImporter logger = logging.getLogger(__name__) +auth_log = logging.getLogger("auth") Package = s.GET_PACKAGE_MODEL() @@ -316,13 +317,19 @@ class GroupManager(object): if isinstance(member, Group): if add_or_remove == "add": group.group_member.add(member) + auth_log.info(f"{caller.username} ({caller.url}) adds {member.name} ({member.url}) to {group.name} ({group.url})") else: group.group_member.remove(member) + auth_log.info( + f"{caller.username} ({caller.url}) removes {member.name} ({member.url}) to {group.name} ({group.url})") else: if add_or_remove == "add": group.user_member.add(member) + auth_log.info(f"{caller.username} ({caller.url}) adds {member.username} ({member.url}) to {group.name} ({group.url})") else: group.user_member.remove(member) + auth_log.info( + f"{caller.username} ({caller.url}) adds {member.username} ({member.url}) to {group.name} ({group.url})") group.save() @@ -589,11 +596,25 @@ class PackageManager(object): if qs.count() != 0: logger.info(f"Deleting Perm {qs.first()}") qs.delete() + auth_log.info(f"{caller.username} ({caller.url}) revokes {grantee.name} ({grantee.url}) all Permissions on {package.name} ({package.url})") else: logger.debug(f"No Permission object for {perm_cls} with filter {data} found!") else: + old_perm = None + old_perms_qs = perm_cls.objects.filter(**data) + + if old_perms_qs.exists(): + old_perm = old_perms_qs.first().permission + _ = perm_cls.objects.update_or_create(defaults={"permission": new_perm}, **data) + grantee_name = grantee.username if isinstance(grantee, User) else grantee.name + + if old_perm is None: + auth_log.info(f"{caller.username} ({caller.url}) grants {grantee_name} ({grantee.url}) '{new_perm}' Permissions on {package.name} ({package.url})") + else: + auth_log.info(f"{caller.username} ({caller.url}) set {grantee_name} ({grantee.url}) Permissions from '{old_perm}' to '{new_perm}' on {package.name} ({package.url})") + @staticmethod def grant_read(caller: User, package: Package, grantee: Union[User, Group]): PackageManager.update_permissions(caller, package, grantee, Permission.READ[0]) diff --git a/epdb/views.py b/epdb/views.py index e448ddac..647248fb 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -61,6 +61,7 @@ from .models import ( ) logger = logging.getLogger(__name__) +auth_log = logging.getLogger("auth") Package = s.GET_PACKAGE_MODEL() @@ -147,6 +148,19 @@ def handler500(request): return render(request, "errors/error.html", context, status=500) +def delete_with_log(request, obj): + caller = request.user + obj_type = obj.__class__.__name__ + + try: + obj.delete() + except Exception as e: + logger.info(f"Tried to delete {obj_type}: {obj.name} ({obj.url}) but deletion failed! Exception {e}") + auth_log.error( + f"{caller.username} ({caller.url}) tried to delete {obj_type}: {obj.name} ({obj.url}) but deletion failed!") + raise e + + def login(request): context = get_base_context(request) @@ -1282,7 +1296,8 @@ def package(request, package_uuid): "You cannot delete the default package. If you want to delete this package you have to set another default package first.", ) - logger.debug(current_package.delete()) + delete_with_log(request, current_package) + return redirect(s.SERVER_URL + "/package") elif hidden == "publish-package": for g in Group.objects.filter(public=True): @@ -2273,7 +2288,7 @@ def package_pathway(request, package_uuid, pathway_uuid): elif request.method == "POST": if hidden := request.POST.get("hidden", None): if hidden == "delete": - current_pathway.delete() + delete_with_log(request, current_pathway) return redirect(current_package.url + "/pathway") else: return HttpResponseBadRequest() @@ -2491,7 +2506,7 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid): if hidden := request.POST.get("hidden", None): if hidden == "delete": # pre_delete signal will take care of edge deletion - current_node.delete() + delete_with_log(request, current_node) return redirect(current_pathway.url) else: @@ -2645,7 +2660,7 @@ def package_pathway_edge(request, package_uuid, pathway_uuid, edge_uuid): if hidden := request.POST.get("hidden", None): if hidden == "delete": - current_edge.delete() + delete_with_log(request, current_edge) return redirect(current_pathway.url) if "selected-scenarios" in request.POST: @@ -3007,7 +3022,7 @@ def group(request, group_uuid): if hidden := request.POST.get("hidden", None): if hidden == "delete": - current_group.delete() + delete_with_log(request, current_group) return redirect(s.SERVER_URL + "/group") else: return HttpResponseBadRequest() diff --git a/templates/actions/objects/package.html b/templates/actions/objects/package.html index a49f2c26..2e3e560e 100644 --- a/templates/actions/objects/package.html +++ b/templates/actions/objects/package.html @@ -15,6 +15,7 @@ Edit Permissions + {% if meta.current_package.get_classification_level_display != "Secret" %}