diff --git a/backend/areas/users/user_service.py b/backend/areas/users/user_service.py index eefee8e3fa71a86ceaf0239fb53d599241719fb7..0783ef70a9a02add97df1e6fac2dfe06907add17 100644 --- a/backend/areas/users/user_service.py +++ b/backend/areas/users/user_service.py @@ -1,19 +1,26 @@ import ory_kratos_client +from ory_kratos_client.model.json_patch \ + import JsonPatch +from ory_kratos_client.model.json_patch_document \ + import JsonPatchDocument from ory_kratos_client.model.update_recovery_flow_body \ import UpdateRecoveryFlowBody from ory_kratos_client.api import frontend_api, identity_api -from config import KRATOS_ADMIN_URL +from datetime import datetime +import time + +from flask import current_app + +from config import KRATOS_ADMIN_URL from database import db from areas.apps import App, AppRole, AppsService from areas.roles import Role, RoleService from areas.tags import TagUser from helpers import KratosApi - -from flask import current_app - from helpers.error_handler import KratosError + kratos_admin_api_configuration = \ ory_kratos_client.Configuration(host=KRATOS_ADMIN_URL, discard_unknown_keys=True) kratos_client = ory_kratos_client.ApiClient(kratos_admin_api_configuration) @@ -207,8 +214,8 @@ class UserService: db.session.delete(ar) db.session.commit() - @staticmethod - def post_multiple_users(data): + @classmethod + def post_multiple_users(cls, data): # check if data is array # for every item in array call Kratos created_users = [] @@ -220,7 +227,7 @@ class UserService: if not user_email: return try: - UserService.post_user(user_data) + cls.post_user(user_data) current_app.logger.info(f"Batch create user: {user_email}") created_users.append(user_email) except KratosError as err: @@ -251,6 +258,26 @@ class UserService: return {"success": success_response, "existing": existing_response, "failed": failed_response} + @staticmethod + def recovery_complete(userID): + # Current unix time. + now = int(datetime.today().timestamp()) + current_app.logger.info("Waiting a little while before setting last_recovery...") + time.sleep(0.5) + current_app.logger.info(f"Set last_recovery for {userID} to {now}") + patch = JsonPatch(op="replace", path="/metadata_admin/last_recovery", value=now) + patch_doc = JsonPatchDocument(value=[patch]) + kratos_identity_api.patch_identity(userID, json_patch_document=patch_doc) + + @staticmethod + def login_complete(userID): + # Current unix time. + now = int(datetime.today().timestamp()) + current_app.logger.info(f"Set last_login for {userID} to {now}") + patch = JsonPatch(op="replace", path="/metadata_admin/last_login", value=now) + patch_doc = JsonPatchDocument(value=[patch]) + kratos_identity_api.patch_identity(userID, json_patch_document=patch_doc) + @staticmethod def __insertAppRoleToUser(userId, userRes): apps = App.query.all() diff --git a/backend/areas/users/users.py b/backend/areas/users/users.py index b91f504db7a166fce9c9b0e0f24e1f2014694888..9427a1d13c205790a5743c8608c694ee56ce841f 100644 --- a/backend/areas/users/users.py +++ b/backend/areas/users/users.py @@ -5,9 +5,9 @@ from flask_jwt_extended import get_jwt, jwt_required from areas import api_v1 from helpers import KratosApi -from helpers.auth_guard import admin_required +from helpers.auth_guard import admin_required, kratos_webhook -from .validation import schema, schema_multiple, schema_multi_edit +from .validation import schema, schema_multiple, schema_multi_edit, schema_recovery_complete from .user_service import UserService @@ -44,6 +44,34 @@ def reset_2fa(id): res = UserService.reset_2fa(id) return jsonify(res) +# This is supposed to be called by Kratos as a webhook after a user has +# successfully recovered their account. +@api_v1.route("/users/recovery_complete", methods=["POST"]) +@expects_json(schema_recovery_complete) +@kratos_webhook() +def recovery_complete(): + data = request.get_json() + try: + UserService.recovery_complete(data["user_id"]) + except Exception as e: + current_app.logger.warn(f"Exception in /users/recovery_complete: {e}") + raise + return jsonify(message="Updated last recovery time.") + +# This is supposed to be called by Kratos as a webhook after a user has +# successfully logged in to their account. +@api_v1.route("/users/login_complete", methods=["POST"]) +@expects_json(schema_recovery_complete) +@kratos_webhook() +def login_complete(): + data = request.get_json() + try: + UserService.login_complete(data["user_id"]) + except Exception as e: + current_app.logger.warn(f"Exception in /users/login_complete: {e}") + raise + return jsonify(message="Updated last login time.") + @api_v1.route("/users", methods=["POST"]) @jwt_required() @cross_origin() diff --git a/backend/areas/users/validation.py b/backend/areas/users/validation.py index 67b02cb117c93113abb5c75d0e9eb097a09261dd..6b50029b305efa34e8a3f545422e7a6c4cae3e32 100644 --- a/backend/areas/users/validation.py +++ b/backend/areas/users/validation.py @@ -94,3 +94,15 @@ schema_multi_edit = { } } } + +schema_recovery_complete = { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Kratos ID of the user", + "minLength": 1, + }, + }, + "required": ["user_id"], +} diff --git a/backend/helpers/auth_guard.py b/backend/helpers/auth_guard.py index 36bbeeb1c3d3916ed68119fbd246496091aa9fc1..b9dd97234133eaeec0862ed3d5f671a5c31f858c 100644 --- a/backend/helpers/auth_guard.py +++ b/backend/helpers/auth_guard.py @@ -1,9 +1,11 @@ from functools import wraps +import os from areas.roles.role_service import RoleService +from helpers import Unauthorized +from flask import request from flask_jwt_extended import get_jwt, verify_jwt_in_request -from helpers import Unauthorized def admin_required(): @@ -22,3 +24,17 @@ def admin_required(): return decorator return wrapper + +def kratos_webhook(): + def wrapper(fn): + @wraps(fn) + def decorator(*args, **kwargs): + header = request.headers.get("Authorization") + if header is not None and header == os.environ.get("KRATOS_WEBHOOK_SECRET"): + return fn(*args, **kwargs) + else: + raise Unauthorized("This needs a valid api key.") + + return decorator + + return wrapper diff --git a/deployment/helmchart/templates/configmaps.yaml b/deployment/helmchart/templates/configmaps.yaml index 679c700471025158185d80acd67c818b7bbc8746..571dbd6e5269effeb2fde4ae5b1b7d096dac96bb 100644 --- a/deployment/helmchart/templates/configmaps.yaml +++ b/deployment/helmchart/templates/configmaps.yaml @@ -18,6 +18,7 @@ data: TOKEN_URL: {{ .Values.backend.oidc.tokenUrl }} KRATOS_PUBLIC_URL: {{ .Values.backend.kratos.publicUrl }} KRATOS_ADMIN_URL: {{ .Values.backend.kratos.adminUrl }} + KRATOS_WEBHOOK_SECRET: {{ .Values.backend.kratos.webhookSecret }} HYDRA_PUBLIC_URL: {{ .Values.backend.oidc.baseUrl }} HYDRA_ADMIN_URL: {{ .Values.backend.hydra.adminUrl }} DASHBOARD_URL: {{ .Values.backend.dashboardUrl }}