diff --git a/kairon/api/app/routers/auth.py b/kairon/api/app/routers/auth.py index bd0dbd571..8c4d4f417 100644 --- a/kairon/api/app/routers/auth.py +++ b/kairon/api/app/routers/auth.py @@ -4,13 +4,14 @@ from starlette.requests import Request from kairon.idp.processor import IDPProcessor +from kairon.shared.account.activity_log import UserActivityLogger from kairon.shared.data.utils import DataUtility from kairon.shared.organization.processor import OrgProcessor from kairon.shared.utils import Utility, MailUtility from kairon.shared.auth import Authentication from kairon.api.models import Response, IntegrationRequest, RecaptchaVerifiedOAuth2PasswordRequestForm from kairon.shared.authorization.processor import IntegrationProcessor -from kairon.shared.constants import ADMIN_ACCESS, TESTER_ACCESS +from kairon.shared.constants import ADMIN_ACCESS, TESTER_ACCESS, UserActivityType from kairon.shared.data.constant import ACCESS_ROLES, TOKEN_TYPE from kairon.shared.models import User @@ -164,6 +165,8 @@ async def sso_callback( MailUtility.format_and_send_mail, mail_type='password_generated', email=user_details['email'], first_name=user_details['first_name'], password=user_details['password'].get_secret_value() ) + UserActivityLogger.add_log(a_type=UserActivityType.social_login.value, email=user_details['email'], + data={"username": user_details['email'], "sso_type": sso_type}) return { "data": {"access_token": access_token, "token_type": "bearer"}, "message": """It is your responsibility to keep the token secret. @@ -171,6 +174,18 @@ async def sso_callback( } +@router.post("/logout", response_model=Response) +async def logout( + current_user: User = Depends(Authentication.get_current_user) +): + """ + Invalidate user session and revoke authentication token upon successful logout. + """ + UserActivityLogger.add_log(a_type=UserActivityType.logout.value, account=current_user.account, + email=current_user.email, data={"username": current_user.email}) + return Response(message="User Logged out!") + + @router.get('/login/idp/{realm_name}') async def idp_login( realm_name: str = Path(description="Domain name for your company", examples=["KAIRON"])): diff --git a/kairon/shared/constants.py b/kairon/shared/constants.py index 811519545..3068384df 100644 --- a/kairon/shared/constants.py +++ b/kairon/shared/constants.py @@ -58,6 +58,8 @@ class UserActivityType(str, Enum): delete_asset = "delete_asset" link_usage = "link_usage" login = 'login' + social_login = 'social_login' + logout = 'logout' login_refresh_token = "login_refresh_token" invalid_login = 'invalid_login' download = "download" diff --git a/tests/integration_test/services_test.py b/tests/integration_test/services_test.py index db57daf70..8a55c9879 100644 --- a/tests/integration_test/services_test.py +++ b/tests/integration_test/services_test.py @@ -1345,6 +1345,38 @@ def test_add_scheduled_broadcast_with_no_language_code(mock_event_server): assert not actual["data"] +def test_logout(): + response = client.post( + "/api/auth/login", + data={"username": "integration@demo.ai", "password": "Welcome@10"}, + ) + actual = response.json() + assert actual["success"] + assert actual["error_code"] == 0 + + access_token = actual["data"]["access_token"] + token_type = actual["data"]["token_type"] + response = client.post( + url=f"/api/auth/logout", + headers={"Authorization": token_type + " " + access_token}, + ) + actual = response.json() + print(actual) + assert actual["success"] + assert actual["message"] == "User Logged out!" + assert not actual["data"] + assert actual["error_code"] == 0 + + values = list(AuditLogData.objects(user="integration@demo.ai", action='activity', entity='logout').order_by( + "-timestamp")) + audit_log_data = values[0].to_mongo().to_dict() + print(audit_log_data) + assert audit_log_data["action"] == 'activity' + assert audit_log_data['entity'] == 'logout' + assert audit_log_data['user'] == 'integration@demo.ai' + assert audit_log_data['data']['username'] == 'integration@demo.ai' + + @responses.activate def test_default_values(): response = client.get( @@ -21250,8 +21282,18 @@ def test_sso_get_login_token_invalid_type(): def test_sso_get_login_token(monkeypatch): + token = "fgyduhsaifusijfisofwh87eyfhw98yqwhfc8wufchwufehwncj" + async def __mock_verify_and_process(*args, **kwargs): - return True, {}, "fgyduhsaifusijfisofwh87eyfhw98yqwhfc8wufchwufehwncj" + return ( + False, + { + "email": "new_user@digite.com", + "first_name": "new", + "password": SecretStr("123456789"), + }, + token, + ) monkeypatch.setattr(Authentication, "verify_and_process", __mock_verify_and_process) response = client.get( @@ -21266,6 +21308,15 @@ async def __mock_verify_and_process(*args, **kwargs): ) assert actual["success"] assert actual["error_code"] == 0 + values = list(AuditLogData.objects(user="new_user@digite.com", action='activity', entity='social_login').order_by( + "-timestamp")) + audit_log_data = values[0].to_mongo().to_dict() + assert audit_log_data['user'] == 'new_user@digite.com' + assert audit_log_data["attributes"] == [{'key': 'email', 'value': 'new_user@digite.com'}] + assert audit_log_data["action"] == 'activity' + assert audit_log_data['entity'] == 'social_login' + assert audit_log_data['data']['username'] == 'new_user@digite.com' + assert audit_log_data['data']['sso_type'] == 'google' response = client.get( url=f"/api/auth/login/sso/callback/linkedin?code=123456789", @@ -21280,6 +21331,15 @@ async def __mock_verify_and_process(*args, **kwargs): ) assert actual["success"] assert actual["error_code"] == 0 + values = list(AuditLogData.objects(user="new_user@digite.com", action='activity', entity='social_login').order_by( + "-timestamp")) + audit_log_data = values[0].to_mongo().to_dict() + assert audit_log_data['user'] == 'new_user@digite.com' + assert audit_log_data["attributes"] == [{'key': 'email', 'value': 'new_user@digite.com'}] + assert audit_log_data["action"] == 'activity' + assert audit_log_data['entity'] == 'social_login' + assert audit_log_data['data']['username'] == 'new_user@digite.com' + assert audit_log_data['data']['sso_type'] == 'linkedin' response = client.get( url=f"/api/auth/login/sso/callback/facebook?code=123456789", @@ -21294,6 +21354,15 @@ async def __mock_verify_and_process(*args, **kwargs): ) assert actual["success"] assert actual["error_code"] == 0 + values = list(AuditLogData.objects(user="new_user@digite.com", action='activity', entity='social_login').order_by( + "-timestamp")) + audit_log_data = values[0].to_mongo().to_dict() + assert audit_log_data['user'] == 'new_user@digite.com' + assert audit_log_data["attributes"] == [{'key': 'email', 'value': 'new_user@digite.com'}] + assert audit_log_data["action"] == 'activity' + assert audit_log_data['entity'] == 'social_login' + assert audit_log_data['data']['username'] == 'new_user@digite.com' + assert audit_log_data['data']['sso_type'] == 'facebook' def test_trigger_mail_on_new_signup_with_sso(monkeypatch):