Repository: bcgov/nr-forests-access-management
Pull request #1816: feat: #1801 Permissions history page for granting application admin
Author: login: ianliuwk1019
Created: 2025-04-25T17:14:43Z | Merged: 2025-04-30T16:43:09Z

ref: #1801 
- Adjust frontend ManagePermissionTable.vue to include navigating to
- Adjust router_application_admin.py application_admin_repository.py for implementing application admin history.
- Adjust application_admin_service to save audit record for granting or revoking application admin.
- Add methods to permission_audit_service.py to save application admin granting/revoking audit history.
- Add tests


Diff:
diff --git a/frontend/src/components/ManagePermissionsTable/index.vue b/frontend/src/components/ManagePermissionsTable/index.vue
index 61bc530da..d5a0d9845 100644
--- a/frontend/src/components/ManagePermissionsTable/index.vue
+++ b/frontend/src/components/ManagePermissionsTable/index.vue
@@ -76,6 +76,11 @@ import {
     type ConfirmTextType,
 } from "./utils";
 
+type TableRowType =
+    | FamApplicationUserRoleAssignmentGetSchema
+    | FamAccessControlPrivilegeGetResponse
+    | FamAppAdminGetResponse;
+
 const router = useRouter();
 const route = useRoute();
 
@@ -287,12 +292,14 @@ const getQueryErrorValue = () => {
     return undefined;
 };
 
-const navigateToUserDetails = (userId: string) => {
+const navigateToUserDetails = (rowData: TableRowType) => {
     router.push({
         name: UserDetailsRoute.name,
         params: {
-            appId: selectedApp.value?.id,
-            userId,
+            appId: !isAppAdminTable
+                ? selectedApp.value?.id
+                : (rowData as FamAppAdminGetResponse).application_id,
+            userId: rowData.user_id,
         },
     });
 };
@@ -417,12 +424,7 @@ const showConfirmDialog = (header: string, onAccept: () => void) => {
     );
 };
 
-const handleDelete = (
-    privilegeObject:
-        | FamApplicationUserRoleAssignmentGetSchema
-        | FamAccessControlPrivilegeGetResponse
-        | FamAppAdminGetResponse
-) => {
+const handleDelete = (privilegeObject: TableRowType) => {
     const userName = formatUserNameAndId(
         privilegeObject.user.user_name,
         privilegeObject.user.first_name,
@@ -466,10 +468,7 @@ const handleDelete = (
 
 // New tag logic
 const highlightNewUserAccessRow = (
-    rowData:
-        | FamApplicationUserRoleAssignmentGetSchema
-        | FamAccessControlPrivilegeGetResponse
-        | FamAppAdminGetResponse
+    rowData: TableRowType
 ): object | undefined => {
     switch (props.tableType) {
         case ManagePermissionsTableEnum.AppAdmin:
@@ -770,10 +769,9 @@ const downloadManagePermissionsCSVData = () => {
                 <template #body="{ data }">
                     <div class="nowrap-cell action-button-group">
                         <button
-                            v-if="!isAppAdminTable"
                             title="User permission history"
                             class="btn btn-icon"
-                            @click="navigateToUserDetails(data.user_id)"
+                            @click="navigateToUserDetails(data)"
                         >
                             <RecentlyViewedIcon />
                         </button>
diff --git a/frontend/src/components/UserPermissionHistoryTable/PermissionDetailsCol.vue b/frontend/src/components/UserPermissionHistoryTable/PermissionDetailsCol.vue
index 1cbfb544e..baf65c77d 100644
--- a/frontend/src/components/UserPermissionHistoryTable/PermissionDetailsCol.vue
+++ b/frontend/src/components/UserPermissionHistoryTable/PermissionDetailsCol.vue
@@ -1,11 +1,11 @@
 <script setup lang="ts">
+import Chip from "@/components/UI/Chip.vue";
 import OrganizationsList from "@/components/UserPermissionHistoryTable/OrganizationList.vue";
 import {
     PrivilegeChangeTypeEnum,
     PrivilegeDetailsPermissionTypeEnum,
 } from "fam-app-acsctl-api/model";
 import type { PrivilegeDetailsSchema } from "fam-app-acsctl-api/model/privilege-details-schema";
-import Chip from "@/components/UI/Chip.vue";
 
 const props = defineProps<{
     permissionDetails: PrivilegeDetailsSchema;
@@ -70,6 +70,19 @@ const props = defineProps<{
             />
         </div>
     </div>
+
+    <!-- Details display for: permission Type change history as an 'ApplicationAdmin' -->
+    <div
+        v-if="
+            props.permissionDetails.permission_type ===
+            PrivilegeDetailsPermissionTypeEnum.ApplicationAdmin
+        "
+    >
+        <div class="permission-type-container">
+            <p>Role:</p>
+            <Chip :label="props.permissionDetails.permission_type" />
+        </div>
+    </div>
 </template>
 
 <style lang="scss">
diff --git a/server/admin_management/api/app/models/model.py b/server/admin_management/api/app/models/model.py
index 1c9930ff3..9b6c09ad0 100644
--- a/server/admin_management/api/app/models/model.py
+++ b/server/admin_management/api/app/models/model.py
@@ -363,7 +363,7 @@ class FamApplicationAdmin(Base):
         onupdate=func.now(),
         comment="The date and time the record was created or last updated.",
     )
-    application = relationship(
+    application: Mapped[FamApplication] = relationship(
         "FamApplication", back_populates="fam_application_admin", lazy="joined"
     )
     user = relationship(
diff --git a/server/admin_management/api/app/repositories/application_admin_repository.py b/server/admin_management/api/app/repositories/application_admin_repository.py
index ada7a91d9..6d82716c6 100644
--- a/server/admin_management/api/app/repositories/application_admin_repository.py
+++ b/server/admin_management/api/app/repositories/application_admin_repository.py
@@ -40,13 +40,13 @@ def get_application_admin_by_id(
         )
 
     def create_application_admin(
-        self, application_id: int, user_id: int, requester: str
+        self, application_id: int, user_id: int, requester_cognito_user_id: str
     ) -> FamApplicationAdmin:
         new_fam_application_admin = FamApplicationAdmin(
             **{
                 "user_id": user_id,
                 "application_id": application_id,
-                "create_user": requester,
+                "create_user": requester_cognito_user_id,
             }
         )
         self.db.add(new_fam_application_admin)
@@ -57,7 +57,7 @@ def create_application_admin(
         )
         return new_fam_application_admin
 
-    def delete_application_admin(self, application_admin_id: int):
+    def delete_application_admin(self, application_admin_id: int) -> FamApplicationAdmin:
         record = (
             self.db.query(FamApplicationAdmin)
             .filter(FamApplicationAdmin.application_admin_id == application_admin_id)
@@ -65,6 +65,7 @@ def delete_application_admin(self, application_admin_id: int):
         )
         self.db.delete(record)
         self.db.flush()
+        return record
 
     def get_user_app_admin_grants(self, user_id: int) -> List[FamApplication]:
         """
diff --git a/server/admin_management/api/app/routers/router_application_admin.py b/server/admin_management/api/app/routers/router_application_admin.py
index 75e626991..3b9776984 100644
--- a/server/admin_management/api/app/routers/router_application_admin.py
+++ b/server/admin_management/api/app/routers/router_application_admin.py
@@ -103,7 +103,7 @@ def create_application_admin(
         )
 
         response = application_admin_service.create_application_admin(
-            application_admin_request, target_user, requester.cognito_user_id
+            application_admin_request, target_user, requester
         )
 
         # get target user from database, so for existing user, we can get the cognito user id
@@ -166,7 +166,7 @@ def delete_application_admin(
         audit_event_log.application = application_admin.application
         audit_event_log.target_user = application_admin.user
 
-        return application_admin_service.delete_application_admin(application_admin_id)
+        return application_admin_service.delete_application_admin(requester, application_admin_id)
 
     except Exception as e:
         audit_event_log.event_outcome = AuditEventOutcome.FAIL
diff --git a/server/admin_management/api/app/services/application_admin_service.py b/server/admin_management/api/app/services/application_admin_service.py
index b2bff5d26..b8d68e896 100644
--- a/server/admin_management/api/app/services/application_admin_service.py
+++ b/server/admin_management/api/app/services/application_admin_service.py
@@ -6,6 +6,7 @@
     ApplicationAdminRepository
 from api.app.schemas import schemas
 from api.app.services.application_service import ApplicationService
+from api.app.services.permission_audit_service import PermissionAuditService
 from api.app.services.user_service import UserService
 from api.app.utils import utils
 from sqlalchemy.orm import Session
@@ -18,6 +19,7 @@ def __init__(self, db: Session):
         self.application_admin_repo = ApplicationAdminRepository(db)
         self.application_service = ApplicationService(db)
         self.user_service = UserService(db)
+        self.permission_audit_service = PermissionAuditService(db)
 
     def get_application_admins(self) -> List[schemas.FamAppAdminGetResponse]:
         application_admins = []
@@ -36,7 +38,7 @@ def get_application_admin_by_id(
 
     def create_application_admin(
         self, request: schemas.FamAppAdminCreateRequest,
-        target_user: schemas.TargetUser, requester: str
+        target_user: schemas.TargetUser, requester: schemas.Requester
     ) -> schemas.FamAppAdminGetResponse:
         # Request has information: user_name, user_type_code, application_id
         LOGGER.debug(
@@ -45,22 +47,22 @@ def create_application_admin(
 
         # Verify if user already exists or add a new user
         fam_user = self.user_service.find_or_create(
-            request.user_type_code, request.user_name, request.user_guid, requester
+            request.user_type_code, request.user_name, request.user_guid, requester.cognito_user_id
         )
         fam_user = self.user_service.update_user_properties_from_verified_target_user(
-            fam_user.user_id, target_user, requester
+            fam_user.user_id, target_user, requester.cognito_user_id
         )
 
         # Verify if user is admin already
-        fam_application_admin_user = (
+        fam_application_admin = (
             self.application_admin_repo.get_application_admin_by_app_and_user_id(
                 request.application_id, fam_user.user_id
             )
         )
-        if fam_application_admin_user:
+        if fam_application_admin:
             LOGGER.debug(
                 "FamApplicationAdmin already exists with id: "
-                + f"{fam_application_admin_user.application_admin_id}."
+                + f"{fam_application_admin.application_admin_id}."
             )
             error_msg = "User is admin already."
             utils.raise_http_exception(
@@ -68,22 +70,32 @@ def create_application_admin(
                 error_msg=error_msg)
         else:
             # Create application admin if user is not admin yet
-            fam_application_admin_user = (
+            fam_application_admin = (
                 self.application_admin_repo.create_application_admin(
-                    request.application_id, fam_user.user_id, requester
+                    request.application_id, fam_user.user_id, requester.cognito_user_id
                 )
             )
+            # save audit record
+            self.permission_audit_service.store_application_admin_permission_granted_audit_history(
+                requester, fam_user, fam_application_admin
+            )
 
-        fam_application_admin_user_dict = fam_application_admin_user.__dict__
+        fam_application_admin_dict = fam_application_admin.__dict__
         app_admin_user_assignment = schemas.FamAppAdminGetResponse(
-            **fam_application_admin_user_dict
+            **fam_application_admin_dict
         )
         LOGGER.debug(
             f"Application admin user assignment executed successfully: {app_admin_user_assignment}"
         )
         return app_admin_user_assignment
 
-    def delete_application_admin(self, application_admin_id: int):
-        return self.application_admin_repo.delete_application_admin(
+    def delete_application_admin(self, requester: schemas.Requester, application_admin_id: int):
+        deleted_record = self.application_admin_repo.delete_application_admin(
             application_admin_id
         )
+
+        # save audit record
+        self.permission_audit_service.store_application_admin_permissions_revoked_audit_history(
+            requester, deleted_record
+        )
+
diff --git a/server/admin_management/api/app/services/permission_audit_service.py b/server/admin_management/api/app/services/permission_audit_service.py
index 16ff49e89..457b337b2 100644
--- a/server/admin_management/api/app/services/permission_audit_service.py
+++ b/server/admin_management/api/app/services/permission_audit_service.py
@@ -9,7 +9,8 @@
                                PrivilegeDetailsScopeTypeEnum)
 from api.app.integration.forest_client_integration import \
     ForestClientIntegrationService
-from api.app.models.model import FamAccessControlPrivilege, FamUser
+from api.app.models.model import (FamAccessControlPrivilege,
+                                  FamApplicationAdmin, FamUser)
 from api.app.repositories.permission_audit_repository import \
     PermissionAuditRepository
 from api.app.schemas.forest_client_integration import \
@@ -36,7 +37,7 @@ def store_delegated_admin_permissions_granted_audit_history(
         self,
         requester: Requester,
         change_target_user: FamUser,
-        new_delegated_admin_permission_granted_list: List[FamAccessControlPrivilegeCreateResponse] # 'dadmin' => 'delegated_admin'
+        new_delegated_admin_permission_granted_list: List[FamAccessControlPrivilegeCreateResponse]
     ):
         success_granted_list = list(filter(
             lambda res: res.status_code == HTTPStatus.OK, new_delegated_admin_permission_granted_list
@@ -60,6 +61,31 @@ def store_delegated_admin_permissions_granted_audit_history(
         LOGGER.debug(f"Adding audit record for ({change_type}): {audit_record}")
         self.repo.save(audit_record)
 
+    def store_application_admin_permission_granted_audit_history(
+        self,
+        requester: Requester,
+        change_target_user: FamUser,
+        fam_application_admin: FamApplicationAdmin
+    ):
+        change_type = PrivilegeChangeTypeEnum.GRANT
+        privilege_details = PrivilegeDetailsSchema(
+            permission_type=PrivilegeDetailsPermissionTypeEnum.APPLICATION_ADMIN,
+        )
+        audit_record = PermissionAuditHistoryCreateSchema(
+            application_id=fam_application_admin.application.application_id,
+            create_user=requester.user_name,
+            change_performer_user_id=requester.user_id,
+            change_target_user_id=change_target_user.user_id,
+            change_performer_user_details=PermissionAuditService.to_change_performer_user_details(requester),
+            privilege_change_type_code=change_type,
+            privilege_details=privilege_details,
+            create_date=datetime.datetime.now(datetime.UTC),
+            change_date=datetime.datetime.now(datetime.UTC)
+        )
+
+        LOGGER.debug(f"Adding audit record for ({change_type}): {audit_record}")
+        self.repo.save(audit_record)
+
     def store_delegated_admin_permissions_revoked_audit_history(
         self, requester: Requester, delete_record: FamAccessControlPrivilege
     ):
@@ -82,6 +108,30 @@ def store_delegated_admin_permissions_revoked_audit_history(
         LOGGER.debug(f"Adding audit record for ({change_type}): {audit_record}")
         self.repo.save(audit_record)
 
+    def store_application_admin_permissions_revoked_audit_history(
+        self, requester: Requester, delete_record: FamApplicationAdmin
+    ):
+        revoked_permission_target_user = delete_record.user
+        revoked_permission_role = delete_record.application
+        change_type = PrivilegeChangeTypeEnum.REVOKE
+        privilege_details = PrivilegeDetailsSchema(
+            permission_type=PrivilegeDetailsPermissionTypeEnum.APPLICATION_ADMIN,
+        )
+        audit_record = PermissionAuditHistoryCreateSchema(
+            application_id=revoked_permission_role.application_id,
+            create_user=requester.user_name,
+            change_performer_user_id=requester.user_id,
+            change_target_user_id=revoked_permission_target_user.user_id,
+            change_performer_user_details=PermissionAuditService.to_change_performer_user_details(requester),
+            privilege_change_type_code=change_type,
+            privilege_details=privilege_details,
+            create_date=datetime.datetime.now(datetime.UTC),
+            change_date=datetime.datetime.now(datetime.UTC)
+        )
+
+        LOGGER.debug(f"Adding audit record for ({change_type}): {audit_record}")
+        self.repo.save(audit_record)
+
     @staticmethod
     def to_change_performer_user_details(
         requester: Requester
diff --git a/server/admin_management/tests/services/test_application_admin_service.py b/server/admin_management/tests/services/test_application_admin_service.py
index bea978a4d..47daac34f 100644
--- a/server/admin_management/tests/services/test_application_admin_service.py
+++ b/server/admin_management/tests/services/test_application_admin_service.py
@@ -1,4 +1,5 @@
 import logging
+from unittest.mock import MagicMock
 
 import pytest
 from api.app.schemas import schemas
@@ -11,7 +12,11 @@
 LOGGER = logging.getLogger(__name__)
 
 
-def test_create_application_admin(application_admin_service: ApplicationAdminService):
+def test_create_application_admin(application_admin_service: ApplicationAdminService, new_idir_requester):
+
+    application_admin_service.permission_audit_service.store_application_admin_permission_granted_audit_history = MagicMock()
+
+    requester = new_idir_requester
     # test invalid user type
     with pytest.raises(ValidationError) as e:
         application_admin_service.create_application_admin(
@@ -22,7 +27,7 @@ def test_create_application_admin(application_admin_service: ApplicationAdminSer
                     "application_id": TEST_NEW_APPLICATION_ADMIN.get("application_id"),
                 }
             ),
-            TEST_CREATOR,
+            requester,
         )
     assert str(e.value).find("Input should be 'I' or 'B'") != -1
 
@@ -31,7 +36,7 @@ def test_create_application_admin(application_admin_service: ApplicationAdminSer
     new_application_admin = application_admin_service.create_application_admin(
         schemas.FamAppAdminCreateRequest(**TEST_NEW_APPLICATION_ADMIN),
         mocked_admin_target_user,
-        TEST_CREATOR,
+        requester,
     )
     assert new_application_admin.application_id == TEST_NEW_APPLICATION_ADMIN.get(
         "application_id"
@@ -47,11 +52,40 @@ def test_create_application_admin(application_admin_service: ApplicationAdminSer
         == application_admin.application_admin_id
     )
 
+    # Verify audit history service is called
+    application_admin_service.permission_audit_service.store_application_admin_permission_granted_audit_history.assert_called()
+
     # test create duplication application admin
     with pytest.raises(HTTPException) as e:
         application_admin_service.create_application_admin(
             schemas.FamAppAdminCreateRequest(**TEST_NEW_APPLICATION_ADMIN),
             mocked_admin_target_user,
-            TEST_CREATOR,
+            requester,
         )
     assert str(e._excinfo).find("User is admin already") != -1
+
+
+def test_delete_application_admin(application_admin_service: ApplicationAdminService, new_idir_requester):
+    application_admin_service.permission_audit_service.store_application_admin_permissions_revoked_audit_history = MagicMock()
+    application_admin_service.application_admin_repo.delete_application_admin = MagicMock()
+
+    requester = new_idir_requester
+    test_application_admin_id = 1
+
+    # Mock the deleted record
+    mocked_deleted_record = MagicMock()
+    mocked_deleted_record.application_admin_id = test_application_admin_id
+    application_admin_service.application_admin_repo.delete_application_admin.return_value = mocked_deleted_record
+
+    # Test delete application admin
+    application_admin_service.delete_application_admin(requester, test_application_admin_id)
+
+    # Verify the repository's delete method is called with the correct ID
+    application_admin_service.application_admin_repo.delete_application_admin.assert_called_once_with(
+        test_application_admin_id
+    )
+
+    # Verify the audit history service is called with the correct parameters
+    application_admin_service.permission_audit_service.store_application_admin_permissions_revoked_audit_history.assert_called_once_with(
+        requester, mocked_deleted_record
+    )
\ No newline at end of file
diff --git a/server/admin_management/tests/services/test_permission_audit_service.py b/server/admin_management/tests/services/test_permission_audit_service.py
index 728555479..c8849bc19 100644
--- a/server/admin_management/tests/services/test_permission_audit_service.py
+++ b/server/admin_management/tests/services/test_permission_audit_service.py
@@ -1,6 +1,5 @@
 import copy
 import logging
-from datetime import datetime
 from http import HTTPStatus
 from typing import List
 
@@ -12,20 +11,24 @@
 from api.app.integration.forest_client_integration import \
     ForestClientIntegrationService
 from api.app.models.model import (FamAccessControlPrivilege, FamApplication,
-                                  FamForestClient, FamPrivilegeChangeAudit,
-                                  FamRole, FamUser)
+                                  FamApplicationAdmin, FamPrivilegeChangeAudit,
+                                  FamUser)
 from api.app.schemas.schemas import (FamAccessControlPrivilegeCreateResponse,
-                                     FamAccessControlPrivilegeGetResponse,
-                                     FamApplicationBase, FamForestClientBase,
-                                     FamRoleBase, FamRoleWithClientDto,
-                                     FamUserInfoDto, FamUserTypeDto, Requester)
+                                     Requester)
 from api.app.services.permission_audit_service import PermissionAuditService
 from fastapi import HTTPException
 from mock import patch
 from sqlalchemy.orm import Session
 from tests.constants import (MOCK_FIND_CLIENT_00001011_RETURN,
                              TEST_APPLICATION_ID_FOM_DEV, TEST_USER_GUID_BCEID,
-                             TEST_USER_NAME_BCEID)
+                             TEST_USER_GUID_IDIR, TEST_USER_NAME_BCEID,
+                             TEST_USER_NAME_IDIR)
+from tests.test_data.access_control_privilege_data import (
+    sameple_delegated_admin_role_with_client_revoked_record,
+    sameple_delegated_admin_role_with_no_client_revoked_record,
+    sameple_delegated_admin_role_with_notfound_client_revoked_record,
+    sample_delegated_admin_permission_granted_no_scope_details,
+    sample_delegated_admin_permission_granted_with_scope_details)
 
 LOGGER = logging.getLogger(__name__)
 
@@ -280,6 +283,156 @@ def test_store_delegated_admin_audit_history_revoke_role_client_search_error(
 	assert audit_record is None
 
 
+def test_store_application_admin_permission_granted_audit_history_success(
+	db_pg_session: Session,
+	permission_audit_service: PermissionAuditService,
+	setup_new_user,
+	new_idir_requester: Requester,
+	mocker
+):
+	"""
+	Test service saving application admin permission granted history successfully.
+	"""
+	performer: Requester = new_idir_requester
+	change_target_user: FamUser = setup_new_user(
+		UserType.BCEID,
+		TEST_USER_NAME_IDIR,
+		TEST_USER_GUID_IDIR
+	)
+	mock_application_admin_user = FamApplicationAdmin(
+		application=FamApplication(
+			application_id=TEST_APPLICATION_ID_FOM_DEV,
+			application_name="FOM_DEV"
+		),
+		user=change_target_user
+	)
+	change_performer_user_details_fn_spy = mocker.spy(PermissionAuditService, 'to_change_performer_user_details')
+
+	# Test the service: granting application admin permission.
+	permission_audit_service.store_application_admin_permission_granted_audit_history(
+		performer, change_target_user, mock_application_admin_user
+	)
+
+	# Find the audit record and verify
+	audit_record = db_pg_session.query(FamPrivilegeChangeAudit).filter(
+		FamPrivilegeChangeAudit.application_id == mock_application_admin_user.application.application_id,
+		FamPrivilegeChangeAudit.change_performer_user_id == performer.user_id,
+		FamPrivilegeChangeAudit.change_target_user_id == change_target_user.user_id
+	).one()
+	assert audit_record is not None
+	assert change_performer_user_details_fn_spy.call_count == 1
+	assert audit_record.privilege_change_type_code == PrivilegeChangeTypeEnum.GRANT
+	assert audit_record.change_target_user_id == change_target_user.user_id
+	assert audit_record.privilege_details["permission_type"] == PrivilegeDetailsPermissionTypeEnum.APPLICATION_ADMIN
+	verify_change_performer_user(audit_record, performer)
+
+
+def test_store_application_admin_permission_granted_audit_history_nosaving_when_no_application_admin(
+	db_pg_session: Session,
+	permission_audit_service: PermissionAuditService,
+	setup_new_user,
+	new_idir_requester: Requester
+):
+	"""
+	Test service saving application admin permission granted history with no application admin.
+	Should raise an exception and no history being saved.
+	"""
+	performer: Requester = new_idir_requester
+	change_target_user: FamUser = setup_new_user(
+		UserType.BCEID,
+		TEST_USER_NAME_IDIR,
+		TEST_USER_GUID_IDIR
+	)
+	mock_application_admin_user = None
+
+	with pytest.raises(Exception):
+		permission_audit_service.store_application_admin_permission_granted_audit_history(
+			performer, change_target_user, mock_application_admin_user
+		)
+
+	# Verify no audit record is saved
+	audit_record = db_pg_session.query(FamPrivilegeChangeAudit).filter(
+		FamPrivilegeChangeAudit.change_performer_user_id == performer.user_id,
+		FamPrivilegeChangeAudit.change_target_user_id == change_target_user.user_id
+	).one_or_none()
+	assert audit_record is None
+
+
+def test_store_application_admin_permissions_revoked_audit_history_success(
+	db_pg_session: Session,
+	permission_audit_service: PermissionAuditService,
+	setup_new_user,
+	new_idir_requester: Requester,
+	mocker
+):
+	"""
+	Test service saving application admin permission revoked history successfully.
+	"""
+	performer: Requester = new_idir_requester
+	change_target_user: FamUser = setup_new_user(
+		UserType.BCEID,
+		TEST_USER_NAME_IDIR,
+		TEST_USER_GUID_IDIR
+	)
+	mock_application_admin_user = FamApplicationAdmin(
+		application=FamApplication(
+			application_id=TEST_APPLICATION_ID_FOM_DEV,
+			application_name="FOM_DEV"
+		),
+		user=change_target_user
+	)
+	change_performer_user_details_fn_spy = mocker.spy(PermissionAuditService, 'to_change_performer_user_details')
+
+	# Test the service: revoking application admin permission.
+	permission_audit_service.store_application_admin_permissions_revoked_audit_history(
+		performer, mock_application_admin_user
+	)
+
+	# Find the audit record and verify
+	audit_record = db_pg_session.query(FamPrivilegeChangeAudit).filter(
+		FamPrivilegeChangeAudit.application_id == mock_application_admin_user.application.application_id,
+		FamPrivilegeChangeAudit.change_performer_user_id == performer.user_id,
+		FamPrivilegeChangeAudit.change_target_user_id == change_target_user.user_id
+	).one()
+	assert audit_record is not None
+	assert change_performer_user_details_fn_spy.call_count == 1
+	assert audit_record.privilege_change_type_code == PrivilegeChangeTypeEnum.REVOKE
+	assert audit_record.change_target_user_id == change_target_user.user_id
+	assert audit_record.privilege_details["permission_type"] == PrivilegeDetailsPermissionTypeEnum.APPLICATION_ADMIN
+	verify_change_performer_user(audit_record, performer)
+
+
+def test_store_application_admin_permissions_revoked_audit_history_nosaving_when_no_application_admin(
+	db_pg_session: Session,
+	permission_audit_service: PermissionAuditService,
+	setup_new_user,
+	new_idir_requester: Requester
+):
+	"""
+	Test service saving application admin permission revoked history with no application admin.
+	Should raise an exception and no history being saved.
+	"""
+	performer: Requester = new_idir_requester
+	change_target_user: FamUser = setup_new_user(
+		UserType.BCEID,
+		TEST_USER_NAME_IDIR,
+		TEST_USER_GUID_IDIR
+	)
+	mock_application_admin_user = None
+
+	with pytest.raises(Exception):
+		permission_audit_service.store_application_admin_permissions_revoked_audit_history(
+			performer, mock_application_admin_user
+		)
+
+	# Verify no audit record is saved
+	audit_record = db_pg_session.query(FamPrivilegeChangeAudit).filter(
+		FamPrivilegeChangeAudit.change_performer_user_id == performer.user_id,
+		FamPrivilegeChangeAudit.change_target_user_id == change_target_user.user_id
+	).one_or_none()
+	assert audit_record is None
+
+
 def verify_change_performer_user(audit_record: FamPrivilegeChangeAudit, performer: FamUser):
 	audit_record.change_performer_user_id == performer.user_id
 	change_performer_user_details_dict = audit_record.change_performer_user_details
@@ -335,68 +488,3 @@ def verify_delegated_admin_revoked_privilege_details(
 		scope = audit_scopes[0]
 		scope.get("scope_type") == PrivilegeDetailsScopeTypeEnum.CLIENT  # Current FAM supports 'CLIENT' type only, more in future.
 		scope.get("client_id") == org_id
-
-
-# sample end user permission granted response - role with no scope
-sample_delegated_admin_permission_granted_no_scope_details = FamAccessControlPrivilegeCreateResponse(
-	**{'status_code': HTTPStatus.OK,
-		'detail': FamAccessControlPrivilegeGetResponse(
-		access_control_privilege_id=999, user_id=9, role_id=4,
-		user=FamUserInfoDto(user_name='dadminuser', first_name='first', last_name='last', email='a@b.com',
-			user_type_relation=FamUserTypeDto(user_type_code=UserType.BCEID, description='BCEID')),
-		role=FamRoleWithClientDto(role_name='FOM_REVIEWER',
-		application=FamApplicationBase(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'),
-		role_id=999, display_name='Reviewer', role_purpose='Provides the privilege to review all FOMs in the system', forest_client_relation=None, parent_role=None),
-		create_date=datetime(2024, 11, 1, 19, 44, 47)),
-		'error_message': None
-	}
- )
-
-# sample delegated admin permission granted response - role with forest_client scope
-sample_delegated_admin_permission_granted_with_scope_details = FamAccessControlPrivilegeCreateResponse(
-	**{'status_code': HTTPStatus.OK,
-		'detail': FamAccessControlPrivilegeGetResponse(
-		access_control_privilege_id=888, user_id=9, role_id=127,
-		user=FamUserInfoDto(user_name='dadminuser', first_name='first', last_name='last', email='a@b.com',
-			user_type_relation=FamUserTypeDto(user_type_code=UserType.BCEID, description='BCEID')),
-		role=FamRoleWithClientDto(role_name='FOM_SUBMITTER_00001012',
-		application=FamApplicationBase(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'),
-		role_id=127, display_name='Submitter', role_purpose='Provides the privilege to submit a FOM (on behalf of a specific forest client)',
-		forest_client_relation=FamForestClientBase(client_name=None, forest_client_number="00001012", status=None),
-		parent_role=FamRoleBase(role_name="FOM_SUBMITTER", role_type_code="A",
-			application=FamApplicationBase(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'))),
-			create_date=datetime(2024, 11, 1, 19, 44, 47)),
-		'error_message': None
-	}
- )
-
-sameple_delegated_admin_role_with_no_client_revoked_record = FamAccessControlPrivilege(**{
-	"user_id": 111, "role_id": 999,
-	"user": FamUser(**{"user_id": 111}),
-	"role": FamRole(** {"display_name": "Reviewer",
-		"application": FamApplication(** {"application_id": TEST_APPLICATION_ID_FOM_DEV})
-	})
-})
-
-sameple_delegated_admin_role_with_client_revoked_record = FamAccessControlPrivilege(**{
-	"user_id": 111, "role_id": 999,
-	"user": FamUser(**{"user_id": 111}),
-	"role": FamRole(**{"display_name": "Submitter", "role_name": "FOM_SUBMITTER_00001011",
-		"application": FamApplication(** {"application_id": TEST_APPLICATION_ID_FOM_DEV, }),
-		"client_number_id": 3, "forest_client_relation": FamForestClient(**{
-			"forest_client_number": "00001011"
-		})
-   })
-})
-
-sameple_delegated_admin_role_with_notfound_client_revoked_record = FamAccessControlPrivilege(**{
-	"user_id": 111, "role_id": 999,
-	"user": FamUser(**{"user_id": 111}),
-	"role": FamRole(**{"display_name": "Submitter", "role_name": "FOM_SUBMITTER_09090909",
-		"application": FamApplication(** {"application_id": TEST_APPLICATION_ID_FOM_DEV, }),
-		"client_number_id": 3, "forest_client_relation": FamForestClient(**{
-			"forest_client_number": "09090909"
-		})
-   })
-})
-
diff --git a/server/admin_management/tests/test_data/access_control_privilege_data.py b/server/admin_management/tests/test_data/access_control_privilege_data.py
index 80ec2c92d..e9f813cd3 100644
--- a/server/admin_management/tests/test_data/access_control_privilege_data.py
+++ b/server/admin_management/tests/test_data/access_control_privilege_data.py
@@ -1,8 +1,17 @@
 
-from typing import List
+from datetime import datetime
+from http import HTTPStatus
 
+from api.app.constants import UserType
+from api.app.models.model import (FamAccessControlPrivilege, FamApplication,
+                                  FamForestClient, FamRole, FamUser)
 from api.app.schemas.pagination import PagedResultsSchema
-from api.app.schemas.schemas import FamAccessControlPrivilegeGetResponse
+from api.app.schemas.schemas import (FamAccessControlPrivilegeCreateResponse,
+                                     FamAccessControlPrivilegeGetResponse,
+                                     FamApplicationBase, FamForestClientBase,
+                                     FamRoleBase, FamRoleWithClientDto,
+                                     FamUserInfoDto, FamUserTypeDto)
+from tests.constants import TEST_APPLICATION_ID_FOM_DEV
 
 APP_DELEGATED_ADMIN_MOCK_RESULT_4_JSON_RECORDS = [
 {
@@ -152,4 +161,67 @@
         "page_size": 50
     },
     "results": APP_DELEGATED_ADMIN_MOCK_RESULT_4_JSON_RECORDS
+})
+
+# sample end user permission granted response - role with no scope
+sample_delegated_admin_permission_granted_no_scope_details = FamAccessControlPrivilegeCreateResponse(
+	**{'status_code': HTTPStatus.OK,
+		'detail': FamAccessControlPrivilegeGetResponse(
+		access_control_privilege_id=999, user_id=9, role_id=4,
+		user=FamUserInfoDto(user_name='dadminuser', first_name='first', last_name='last', email='a@b.com',
+			user_type_relation=FamUserTypeDto(user_type_code=UserType.BCEID, description='BCEID')),
+		role=FamRoleWithClientDto(role_name='FOM_REVIEWER',
+		application=FamApplicationBase(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'),
+		role_id=999, display_name='Reviewer', role_purpose='Provides the privilege to review all FOMs in the system', forest_client_relation=None, parent_role=None),
+		create_date=datetime(2024, 11, 1, 19, 44, 47)),
+		'error_message': None
+	}
+ )
+
+# sample delegated admin permission granted response - role with forest_client scope
+sample_delegated_admin_permission_granted_with_scope_details = FamAccessControlPrivilegeCreateResponse(
+	**{'status_code': HTTPStatus.OK,
+		'detail': FamAccessControlPrivilegeGetResponse(
+		access_control_privilege_id=888, user_id=9, role_id=127,
+		user=FamUserInfoDto(user_name='dadminuser', first_name='first', last_name='last', email='a@b.com',
+			user_type_relation=FamUserTypeDto(user_type_code=UserType.BCEID, description='BCEID')),
+		role=FamRoleWithClientDto(role_name='FOM_SUBMITTER_00001012',
+		application=FamApplicationBase(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'),
+		role_id=127, display_name='Submitter', role_purpose='Provides the privilege to submit a FOM (on behalf of a specific forest client)',
+		forest_client_relation=FamForestClientBase(client_name=None, forest_client_number="00001012", status=None),
+		parent_role=FamRoleBase(role_name="FOM_SUBMITTER", role_type_code="A",
+			application=FamApplicationBase(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'))),
+			create_date=datetime(2024, 11, 1, 19, 44, 47)),
+		'error_message': None
+	}
+ )
+
+sameple_delegated_admin_role_with_no_client_revoked_record = FamAccessControlPrivilege(**{
+	"user_id": 111, "role_id": 999,
+	"user": FamUser(**{"user_id": 111}),
+	"role": FamRole(** {"display_name": "Reviewer",
+		"application": FamApplication(** {"application_id": TEST_APPLICATION_ID_FOM_DEV})
+	})
+})
+
+sameple_delegated_admin_role_with_client_revoked_record = FamAccessControlPrivilege(**{
+	"user_id": 111, "role_id": 999,
+	"user": FamUser(**{"user_id": 111}),
+	"role": FamRole(**{"display_name": "Submitter", "role_name": "FOM_SUBMITTER_00001011",
+		"application": FamApplication(** {"application_id": TEST_APPLICATION_ID_FOM_DEV, }),
+		"client_number_id": 3, "forest_client_relation": FamForestClient(**{
+			"forest_client_number": "00001011"
+		})
+   })
+})
+
+sameple_delegated_admin_role_with_notfound_client_revoked_record = FamAccessControlPrivilege(**{
+	"user_id": 111, "role_id": 999,
+	"user": FamUser(**{"user_id": 111}),
+	"role": FamRole(**{"display_name": "Submitter", "role_name": "FOM_SUBMITTER_09090909",
+		"application": FamApplication(** {"application_id": TEST_APPLICATION_ID_FOM_DEV, }),
+		"client_number_id": 3, "forest_client_relation": FamForestClient(**{
+			"forest_client_number": "09090909"
+		})
+   })
 })
\ No newline at end of file
diff --git a/server/backend/testspg/crud/services/test_permission_audit_service.py b/server/backend/testspg/crud/services/test_permission_audit_service.py
index 3ed672078..6fa6d1e72 100644
--- a/server/backend/testspg/crud/services/test_permission_audit_service.py
+++ b/server/backend/testspg/crud/services/test_permission_audit_service.py
@@ -1,6 +1,5 @@
 import copy
 import logging
-from datetime import datetime
 from http import HTTPStatus
 from typing import List
 
@@ -13,28 +12,24 @@
     PermissionAuditService
 from api.app.integration.forest_client_integration import \
     ForestClientIntegrationService
-from api.app.models.model import (FamApplication, FamForestClient,
-                                  FamPrivilegeChangeAudit, FamRole, FamUser,
+from api.app.models.model import (FamPrivilegeChangeAudit, FamUser,
                                   FamUserRoleXref)
-from api.app.schemas.fam_application import FamApplicationSchema
-from api.app.schemas.fam_application_user_role_assignment_get import \
-    FamApplicationUserRoleAssignmentGetSchema
-from api.app.schemas.fam_forest_client import FamForestClientSchema
-from api.app.schemas.fam_role_min import FamRoleMinSchema
-from api.app.schemas.fam_role_with_client import FamRoleWithClientSchema
-from api.app.schemas.fam_user_info import FamUserInfoSchema
 from api.app.schemas.fam_user_role_assignment_create_response import \
     FamUserRoleAssignmentCreateRes
-from api.app.schemas.fam_user_type import FamUserTypeSchema
 from api.app.schemas.requester import RequesterSchema
 from fastapi import HTTPException
 from mock import patch
 from sqlalchemy.orm import Session
-from testspg.constants import (FOM_DEV_APPLICATION_ID,
-                               MOCK_FIND_CLIENT_00001011_RETURN,
+from testspg.constants import (MOCK_FIND_CLIENT_00001011_RETURN,
                                TEST_USER_GUID_IDIR, TEST_USER_NAME_IDIR,
                                USER_GUID_BCEID_LOAD_2_TEST,
                                USER_NAME_BCEID_LOAD_2_TEST)
+from testspg.test_data.app_user_roles_mock_data import (
+    sameple_user_role_with_client_revoked_record,
+    sameple_user_role_with_no_client_revoked_record,
+    sameple_user_role_with_notfound_client_revoked_record,
+    sample_end_user_permission_granted_no_scope_details,
+    sample_end_user_permission_granted_with_scope_details)
 
 LOGGER = logging.getLogger(__name__)
 
@@ -349,65 +344,3 @@ def verify_end_user_revoked_privilege_details(
 		scope.get("scope_type") == PrivilegeDetailsScopeTypeEnum.CLIENT  # Current FAM supports 'CLIENT' type only, more in future.
 		scope.get("client_id") == org_id
 
-
-# sample end user permission granted response - role with no scope
-sample_end_user_permission_granted_no_scope_details = FamUserRoleAssignmentCreateRes(
-	**{'status_code': HTTPStatus.OK,
-		'detail': FamApplicationUserRoleAssignmentGetSchema(
-		user_role_xref_id=999, user_id=9, role_id=4,
-		user=FamUserInfoSchema(user_name='enduser', first_name='first', last_name='last', email='a@b.com',
-			user_type_relation=FamUserTypeSchema(user_type_code=UserType.BCEID, description='BCEID')),
-		role=FamRoleWithClientSchema(role_name='FOM_REVIEWER', role_type_code='C',
-		application=FamApplicationSchema(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'),
-		role_id=999, display_name='Reviewer', role_purpose='Provides the privilege to review all FOMs in the system', forest_client_relation=None, parent_role=None),
-		create_date=datetime(2024, 11, 1, 19, 44, 47)),
-		'error_message': None
-	}
- )
-
-# sample end user permission granted response - role with forest_client scope
-sample_end_user_permission_granted_with_scope_details = FamUserRoleAssignmentCreateRes(
-	**{'status_code': HTTPStatus.OK,
-		'detail': FamApplicationUserRoleAssignmentGetSchema(
-		user_role_xref_id=888, user_id=9, role_id=127,
-		user=FamUserInfoSchema(user_name='enduser', first_name='first', last_name='last', email='a@b.com',
-			user_type_relation=FamUserTypeSchema(user_type_code=UserType.BCEID, description='BCEID')),
-		role=FamRoleWithClientSchema(role_name='FOM_SUBMITTER_00001012', role_type_code='C',
-		application=FamApplicationSchema(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'),
-		role_id=127, display_name='Submitter', role_purpose='Provides the privilege to submit a FOM (on behalf of a specific forest client)',
-		forest_client_relation=FamForestClientSchema(client_name=None, forest_client_number="00001012", status=None),
-		parent_role=FamRoleMinSchema(role_name="FOM_SUBMITTER", role_type_code="A",
-			application=FamApplicationSchema(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'))),
-		create_date=datetime(2024, 11, 1, 19, 44, 47)),
-		'error_message': None
-	}
- )
-
-sameple_user_role_with_no_client_revoked_record = FamUserRoleXref(**{
-	"user_id": 111, "role_id": 999,
-	"user": FamUser(**{"user_id": 111}),
-	"role": FamRole(** {"display_name": "Reviewer", "application": FamApplication(** {"application_id": 2})})
-})
-
-sameple_user_role_with_client_revoked_record = FamUserRoleXref(**{
-	"user_id": 111, "role_id": 999,
-	"user": FamUser(**{"user_id": 111}),
-	"role": FamRole(**{"display_name": "Submitter", "role_name": "FOM_SUBMITTER_00001011",
-		"application": FamApplication(** {"application_id": FOM_DEV_APPLICATION_ID, }),
-		"client_number_id": 3, "forest_client_relation": FamForestClient(**{
-			"forest_client_number": "00001011"
-		})
-   })
-})
-
-sameple_user_role_with_notfound_client_revoked_record = FamUserRoleXref(**{
-	"user_id": 111, "role_id": 999,
-	"user": FamUser(**{"user_id": 111}),
-	"role": FamRole(**{"display_name": "Submitter", "role_name": "FOM_SUBMITTER_09090909",
-		"application": FamApplication(** {"application_id": FOM_DEV_APPLICATION_ID, }),
-		"client_number_id": 3, "forest_client_relation": FamForestClient(**{
-			"forest_client_number": "09090909"
-		})
-   })
-})
-
diff --git a/server/backend/testspg/test_data/app_user_roles_mock_data.py b/server/backend/testspg/test_data/app_user_roles_mock_data.py
index 0a279fdf4..4932e9d68 100644
--- a/server/backend/testspg/test_data/app_user_roles_mock_data.py
+++ b/server/backend/testspg/test_data/app_user_roles_mock_data.py
@@ -1,7 +1,22 @@
 
+from datetime import datetime
+from http import HTTPStatus
+
+from api.app.constants import UserType
+from api.app.models.model import (FamApplication, FamForestClient, FamRole,
+                                  FamUser, FamUserRoleXref)
+from api.app.schemas.fam_application import FamApplicationSchema
 from api.app.schemas.fam_application_user_role_assignment_get import \
     FamApplicationUserRoleAssignmentGetSchema
+from api.app.schemas.fam_forest_client import FamForestClientSchema
+from api.app.schemas.fam_role_min import FamRoleMinSchema
+from api.app.schemas.fam_role_with_client import FamRoleWithClientSchema
+from api.app.schemas.fam_user_info import FamUserInfoSchema
+from api.app.schemas.fam_user_role_assignment_create_response import \
+    FamUserRoleAssignmentCreateRes
+from api.app.schemas.fam_user_type import FamUserTypeSchema
 from api.app.schemas.pagination import PagedResultsSchema
+from testspg.constants import FOM_DEV_APPLICATION_ID
 
 APP_USER_ROLE_PAGED_RESULT_2_RECORDS = PagedResultsSchema[FamApplicationUserRoleAssignmentGetSchema](
   **{
@@ -86,4 +101,65 @@
       }
     ]
   }
-)
\ No newline at end of file
+)
+
+# sample end user permission granted response - role with no scope
+sample_end_user_permission_granted_no_scope_details = FamUserRoleAssignmentCreateRes(
+	**{'status_code': HTTPStatus.OK,
+		'detail': FamApplicationUserRoleAssignmentGetSchema(
+		user_role_xref_id=999, user_id=9, role_id=4,
+		user=FamUserInfoSchema(user_name='enduser', first_name='first', last_name='last', email='a@b.com',
+			user_type_relation=FamUserTypeSchema(user_type_code=UserType.BCEID, description='BCEID')),
+		role=FamRoleWithClientSchema(role_name='FOM_REVIEWER', role_type_code='C',
+		application=FamApplicationSchema(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'),
+		role_id=999, display_name='Reviewer', role_purpose='Provides the privilege to review all FOMs in the system', forest_client_relation=None, parent_role=None),
+		create_date=datetime(2024, 11, 1, 19, 44, 47)),
+		'error_message': None
+	}
+ )
+
+# sample end user permission granted response - role with forest_client scope
+sample_end_user_permission_granted_with_scope_details = FamUserRoleAssignmentCreateRes(
+	**{'status_code': HTTPStatus.OK,
+		'detail': FamApplicationUserRoleAssignmentGetSchema(
+		user_role_xref_id=888, user_id=9, role_id=127,
+		user=FamUserInfoSchema(user_name='enduser', first_name='first', last_name='last', email='a@b.com',
+			user_type_relation=FamUserTypeSchema(user_type_code=UserType.BCEID, description='BCEID')),
+		role=FamRoleWithClientSchema(role_name='FOM_SUBMITTER_00001012', role_type_code='C',
+		application=FamApplicationSchema(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'),
+		role_id=127, display_name='Submitter', role_purpose='Provides the privilege to submit a FOM (on behalf of a specific forest client)',
+		forest_client_relation=FamForestClientSchema(client_name=None, forest_client_number="00001012", status=None),
+		parent_role=FamRoleMinSchema(role_name="FOM_SUBMITTER", role_type_code="A",
+			application=FamApplicationSchema(application_id=2, application_name='FOM_DEV', application_description='Forest Operations Map (DEV)'))),
+		create_date=datetime(2024, 11, 1, 19, 44, 47)),
+		'error_message': None
+	}
+ )
+
+sameple_user_role_with_no_client_revoked_record = FamUserRoleXref(**{
+	"user_id": 111, "role_id": 999,
+	"user": FamUser(**{"user_id": 111}),
+	"role": FamRole(** {"display_name": "Reviewer", "application": FamApplication(** {"application_id": 2})})
+})
+
+sameple_user_role_with_client_revoked_record = FamUserRoleXref(**{
+	"user_id": 111, "role_id": 999,
+	"user": FamUser(**{"user_id": 111}),
+	"role": FamRole(**{"display_name": "Submitter", "role_name": "FOM_SUBMITTER_00001011",
+		"application": FamApplication(** {"application_id": FOM_DEV_APPLICATION_ID, }),
+		"client_number_id": 3, "forest_client_relation": FamForestClient(**{
+			"forest_client_number": "00001011"
+		})
+   })
+})
+
+sameple_user_role_with_notfound_client_revoked_record = FamUserRoleXref(**{
+	"user_id": 111, "role_id": 999,
+	"user": FamUser(**{"user_id": 111}),
+	"role": FamRole(**{"display_name": "Submitter", "role_name": "FOM_SUBMITTER_09090909",
+		"application": FamApplication(** {"application_id": FOM_DEV_APPLICATION_ID, }),
+		"client_number_id": 3, "forest_client_relation": FamForestClient(**{
+			"forest_client_number": "09090909"
+		})
+   })
+})
\ No newline at end of file


Reviews:
user: login: basilv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T19:01:32Z
user: login: basilv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T19:02:41Z
user: login: basilv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T19:05:46Z
user: login: basilv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T19:06:21Z
user: login: ianliuwk1019
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T19:23:21Z
user: login: ianliuwk1019
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T19:29:54Z
user: login: basilv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T19:35:08Z
user: login: ianliuwk1019
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T19:45:56Z
user: login: basilv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T23:00:57Z
user: login: ianliuwk1019
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T03:18:37Z
user: login: basilv
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:20:36Z

Review comments:
diff_hunk: @@ -103,7 +103,7 @@ def create_application_admin(
         )
 
         response = application_admin_service.create_application_admin(
-            application_admin_request, target_user, requester.cognito_user_id
path: server/admin_management/api/app/routers/router_application_admin.py
user: login: basilv
body: This looks wrong - create_application_admin above was changed to accept cognito_user_id as an argument?
created_at: 2025-04-29T19:01:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,7 +103,7 @@ def create_application_admin(
         )
 
         response = application_admin_service.create_application_admin(
-            application_admin_request, target_user, requester.cognito_user_id
path: server/admin_management/api/app/routers/router_application_admin.py
user: login: basilv
body: Or this is fine because the service was changed as well.
created_at: 2025-04-29T19:02:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,31 @@ def store_delegated_admin_permissions_granted_audit_history(
         LOGGER.debug(f"Adding audit record for ({change_type}): {audit_record}")
         self.repo.save(audit_record)
 
+    def store_application_admin_permission_granted_audit_history(
+        self,
+        requester: Requester,
+        change_target_user: FamUser,
+        fam_application_admin_user: FamApplicationAdmin
+    ):
+        change_type = PrivilegeChangeTypeEnum.GRANT
+        privilege_details = PrivilegeDetailsSchema(
+            permission_type=PrivilegeDetailsPermissionTypeEnum.APPLICATION_ADMIN,
+        )
+        audit_record = PermissionAuditHistoryCreateSchema(
+            application_id=fam_application_admin_user.application.application_id,
path: server/admin_management/api/app/services/permission_audit_service.py
user: login: basilv
body: This line to obtain the application id looks weird.
created_at: 2025-04-29T19:05:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,31 @@ def store_delegated_admin_permissions_granted_audit_history(
         LOGGER.debug(f"Adding audit record for ({change_type}): {audit_record}")
         self.repo.save(audit_record)
 
+    def store_application_admin_permission_granted_audit_history(
+        self,
+        requester: Requester,
+        change_target_user: FamUser,
+        fam_application_admin_user: FamApplicationAdmin
+    ):
+        change_type = PrivilegeChangeTypeEnum.GRANT
+        privilege_details = PrivilegeDetailsSchema(
+            permission_type=PrivilegeDetailsPermissionTypeEnum.APPLICATION_ADMIN,
+        )
+        audit_record = PermissionAuditHistoryCreateSchema(
+            application_id=fam_application_admin_user.application.application_id,
path: server/admin_management/api/app/services/permission_audit_service.py
user: login: basilv
body: Why does the admin user's application matter?
created_at: 2025-04-29T19:06:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,7 +103,7 @@ def create_application_admin(
         )
 
         response = application_admin_service.create_application_admin(
-            application_admin_request, target_user, requester.cognito_user_id
path: server/admin_management/api/app/routers/router_application_admin.py
user: login: ianliuwk1019
body: Yes, this line was only a refactored code to accept the 'requester' object change from the service method. 
created_at: 2025-04-29T19:23:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,31 @@ def store_delegated_admin_permissions_granted_audit_history(
         LOGGER.debug(f"Adding audit record for ({change_type}): {audit_record}")
         self.repo.save(audit_record)
 
+    def store_application_admin_permission_granted_audit_history(
+        self,
+        requester: Requester,
+        change_target_user: FamUser,
+        fam_application_admin_user: FamApplicationAdmin
+    ):
+        change_type = PrivilegeChangeTypeEnum.GRANT
+        privilege_details = PrivilegeDetailsSchema(
+            permission_type=PrivilegeDetailsPermissionTypeEnum.APPLICATION_ADMIN,
+        )
+        audit_record = PermissionAuditHistoryCreateSchema(
+            application_id=fam_application_admin_user.application.application_id,
path: server/admin_management/api/app/services/permission_audit_service.py
user: login: ianliuwk1019
body: When application admin is granted we need to store the audit record for this.
To save the audit record, one attribute to be saved is the application_id, and this can be obtained from the currently granted `fam_application_admin_user` (this is mapped to the `fam_application_admin` table entity) from it's `application` relationship to grab the id. 
created_at: 2025-04-29T19:29:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,31 @@ def store_delegated_admin_permissions_granted_audit_history(
         LOGGER.debug(f"Adding audit record for ({change_type}): {audit_record}")
         self.repo.save(audit_record)
 
+    def store_application_admin_permission_granted_audit_history(
+        self,
+        requester: Requester,
+        change_target_user: FamUser,
+        fam_application_admin_user: FamApplicationAdmin
+    ):
+        change_type = PrivilegeChangeTypeEnum.GRANT
+        privilege_details = PrivilegeDetailsSchema(
+            permission_type=PrivilegeDetailsPermissionTypeEnum.APPLICATION_ADMIN,
+        )
+        audit_record = PermissionAuditHistoryCreateSchema(
+            application_id=fam_application_admin_user.application.application_id,
path: server/admin_management/api/app/services/permission_audit_service.py
user: login: basilv
body: Ah, I think I was confused between the admin user performing the action and the admin access being granted, which is really a permission being granted and not a user, so I was confused by it being called fam_application_admin_user.
created_at: 2025-04-29T19:35:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,31 @@ def store_delegated_admin_permissions_granted_audit_history(
         LOGGER.debug(f"Adding audit record for ({change_type}): {audit_record}")
         self.repo.save(audit_record)
 
+    def store_application_admin_permission_granted_audit_history(
+        self,
+        requester: Requester,
+        change_target_user: FamUser,
+        fam_application_admin_user: FamApplicationAdmin
+    ):
+        change_type = PrivilegeChangeTypeEnum.GRANT
+        privilege_details = PrivilegeDetailsSchema(
+            permission_type=PrivilegeDetailsPermissionTypeEnum.APPLICATION_ADMIN,
+        )
+        audit_record = PermissionAuditHistoryCreateSchema(
+            application_id=fam_application_admin_user.application.application_id,
path: server/admin_management/api/app/services/permission_audit_service.py
user: login: ianliuwk1019
body: I see, the variable name confuses your thought.
I like to refactor the variable name (not only on this file context, some other files too related to this name), what name would you give or prefer so that you don't get confused?
The table entity class is called `FamApplicationAdmin` and the variable is associated with that type, perhaps removing `_user` at the end (`fam_application_admin`), will it be less confusing to you (or maybe, `fam_application_admin_grant`?  
created_at: 2025-04-29T19:45:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,31 @@ def store_delegated_admin_permissions_granted_audit_history(
         LOGGER.debug(f"Adding audit record for ({change_type}): {audit_record}")
         self.repo.save(audit_record)
 
+    def store_application_admin_permission_granted_audit_history(
+        self,
+        requester: Requester,
+        change_target_user: FamUser,
+        fam_application_admin_user: FamApplicationAdmin
+    ):
+        change_type = PrivilegeChangeTypeEnum.GRANT
+        privilege_details = PrivilegeDetailsSchema(
+            permission_type=PrivilegeDetailsPermissionTypeEnum.APPLICATION_ADMIN,
+        )
+        audit_record = PermissionAuditHistoryCreateSchema(
+            application_id=fam_application_admin_user.application.application_id,
path: server/admin_management/api/app/services/permission_audit_service.py
user: login: basilv
body: Yes, removing _user suffix would likely be clearer.
created_at: 2025-04-29T23:00:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,6 +61,31 @@ def store_delegated_admin_permissions_granted_audit_history(
         LOGGER.debug(f"Adding audit record for ({change_type}): {audit_record}")
         self.repo.save(audit_record)
 
+    def store_application_admin_permission_granted_audit_history(
+        self,
+        requester: Requester,
+        change_target_user: FamUser,
+        fam_application_admin_user: FamApplicationAdmin
+    ):
+        change_type = PrivilegeChangeTypeEnum.GRANT
+        privilege_details = PrivilegeDetailsSchema(
+            permission_type=PrivilegeDetailsPermissionTypeEnum.APPLICATION_ADMIN,
+        )
+        audit_record = PermissionAuditHistoryCreateSchema(
+            application_id=fam_application_admin_user.application.application_id,
path: server/admin_management/api/app/services/permission_audit_service.py
user: login: ianliuwk1019
body: Variable is renamed to `fam_application_admin` without _user suffix. Thanks Basil!
created_at: 2025-04-30T03:18:37Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-30T03:16:27Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=nr-forests-access-management_admin&pullRequest=1816) **Quality Gate passed for 'nr-forests-access-management_admin'**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=nr-forests-access-management_admin&pullRequest=1816&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=nr-forests-access-management_admin&pullRequest=1816&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=nr-forests-access-management_admin&pullRequest=1816&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=nr-forests-access-management_admin&pullRequest=1816&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=nr-forests-access-management_admin&pullRequest=1816&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=nr-forests-access-management_admin&pullRequest=1816)


author_association: NONE