diff --git a/CHANGELOG.md b/CHANGELOG.md index e3281490e9ad75f80198b9e547edf0f24ffebb86..fdea5a7c927d7ab8bc2d1f7146db077a655fbb1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.8.3] + +- Introduce backend code for resetting 2FA, and add cli command for that. +- Upgrade Kratos api library `ory-kratos-client` to 1.0.0. +- Patch our usage of Kratos api pagination of identities list. + ## [0.8.2] - End the Kratos session in prelogout. This makes sure that we end the "SSO diff --git a/backend/areas/users/user_service.py b/backend/areas/users/user_service.py index d899e3ea5de3150f79e6387106340160462aa790..b5e7681e86340108734bb43c3f0a08b1568ba109 100644 --- a/backend/areas/users/user_service.py +++ b/backend/areas/users/user_service.py @@ -22,10 +22,13 @@ kratos_identity_api = identity_api.IdentityApi(kratos_client) class UserService: @staticmethod def get_users(): - page = 1 + page = 0 userList = [] - while page > 0: - res = KratosApi.get("/admin/identities?per_page=1000&page={}".format(page)).json() + while page >= 0: + if page == 0: + res = KratosApi.get("/admin/identities?per_page=1000").json() + else: + res = KratosApi.get("/admin/identities?per_page=1000&page={}".format(page)).json() for r in res: # removed the app role assignment function, passing simple user data # userList.append(UserService.__insertAppRoleToUser(r["id"], r)) diff --git a/backend/helpers/kratos_user.py b/backend/helpers/kratos_user.py index d5f50d1120713a85c2444cb4892c7c4e8fc8398b..13a10e3a2f703cf86ac21c156154c98709236c47 100644 --- a/backend/helpers/kratos_user.py +++ b/backend/helpers/kratos_user.py @@ -133,9 +133,12 @@ class KratosUser(): kratos_id = None # Get out user ID by iterating over all available IDs - page = 1 - while page > 0: - data = api.list_identities(per_page=1000, page=page) + page = 0 + while page >= 0: + if page == 0: + data = api.list_identities(per_page=1000) + else: + data = api.list_identities(per_page=1000, page=page) for kratos_obj in data: # Unique identifier we use if kratos_obj.traits['email'] == email: @@ -158,9 +161,12 @@ class KratosUser(): kratos_id = None return_list = [] # Get out user ID by iterating over all available ID - page = 1 - while page > 0: - data = api.list_identities(per_page=1000, page=page) + page = 0 + while page >= 0: + if page == 0: + data = api.list_identities(per_page=1000) + else: + data = api.list_identities(per_page=1000, page=page) for kratos_obj in data: kratos_id = str(kratos_obj.id) return_list.append(KratosUser(api, kratos_id)) diff --git a/backend/requirements.txt b/backend/requirements.txt index fc2836ad096a8f50d217569cc0a9b8eccdc5db34..246dcc78d94c34198ec9ed1448b638a08be62280 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -23,7 +23,7 @@ MarkupSafe==2.1.1 mypy-extensions==0.4.3 NamedAtomicLock==1.1.3 oauthlib==3.2.0 -ory-kratos-client==0.11.0 +ory-kratos-client==1.0.0 ory-hydra-client==1.11.8 pathspec==0.9.0 platformdirs==2.5.1 diff --git a/backend/web/login/login.py b/backend/web/login/login.py index 71117746cc5f5e7f27243c78dca1a3cf18d11bf0..627f8ec1acbd09d453862de507a3bb7cfd99cef0 100644 --- a/backend/web/login/login.py +++ b/backend/web/login/login.py @@ -560,19 +560,28 @@ def get_kratos_cookie(): return cookie - -@web.route("/prelogout", methods=["GET"]) -def prelogout(): - """Handles the Hydra OpenID Connect Logout flow +@web.route("/logout", methods=["GET"]) +def logout(): + """Handles the Hydra OpenID Connect Logout flow and Kratos logout flow. Steps: 1. Hydra's /oauth2/sessions/logout endpoint is called by an application 2. Hydra calls this endpoint with a `logout_challenge` get parameter 3. We retrieve the logout request using the challenge - 4. We accept the Hydra logout request - 5. We redirect to Hydra to clean-up cookies. - 6. Hyrda calls back to us with a post logout handle (/logout) - + 4. We accept the Hydra logout request. This returns a URL -- let's call it + "next redirect" -- that we should redirect the browser to to finish the + Hydra logout. + 5. We create a Kratos logout flow, setting its `return_to` parameter to the + "next redirect". This returns a Kratos logout URL. + 6. We return a small HTML page to the browser, based on the `clear.html` + template, which clears dashboard local storage and redirects to the Kratos + logout URL. + 7. The browser follows that redirect, Kratos does its thing and redirects + to the "next redirect". + 8. The browser follows the "next redirect", Hydra does its thing and + redirects to the "post-logout URL". We set that to the dashboard root URL + by default, but OIDC clients can override it to something else. For + example, Nextcloud sets it to the root Nextcloud URL. Args: logout_challenge (string): Reference to a Hydra logout challenge object @@ -595,7 +604,7 @@ def prelogout(): current_app.logger.error( "Conflict. Logout request with challenge '%s' has been used already.", challenge) - abort(503) + abort(404, "Logout request has been accepted already.") current_app.logger.info("Logout request hydra, subject %s", logout_request.subject) @@ -604,20 +613,28 @@ def prelogout(): # browser flow and we can't do both. try: hydra_return = hydra_admin_api.accept_logout_request(challenge) + next_redirect = hydra_return.redirect_to except Exception as ex: - current_app.logger.info("Error logging out hydra: %s", str(ex)) + current_app.logger.info("Error accepting hydra logout request: %s", str(ex)) + next_redirect = DASHBOARD_URL - # Now start ending the kratos session. + # Now end the kratos session. kratos_cookie = get_kratos_cookie() if not kratos_cookie: # No kratos cookie, already logged out from kratos. current_app.logger.info("Expected kratos cookie but not found. Redirecting to hydra post-logout"); - return redirect(hydra_post_logout) + # We skip the Kratos logout, but we still need to follow + # `next_redirect` -- probably the Hydra logout URL -- and clear + # dashboard storage. + return render_template("clear.html", + url=next_redirect) try: # Create a Logout URL for Browsers - kratos_api_response = \ - admin_frontend_api.create_browser_logout_flow( - cookie=kratos_cookie) + current_app.logger.info(f"Creating logout flow, with return_to={next_redirect}") + kratos_api_response = admin_frontend_api.create_browser_logout_flow( + return_to=next_redirect, + cookie=kratos_cookie) + current_app.logger.info("Kratos api response to creating logout flow:") current_app.logger.info(kratos_api_response) return render_template("clear.html", url=kratos_api_response.logout_url) @@ -625,41 +642,7 @@ def prelogout(): current_app.logger.error("Exception when calling" " create_browser_logout_flow: %s\n", ex) - - current_app.logger.info("Hydra logout not completed. Redirecting to kratos logout, maybe user removed cookies manually") - return redirect("logout") - - -@web.route("/logout", methods=["GET"]) -def logout(): - """Handles the Kratos Logout flow - - Steps: - 1. We got here from hyrda - 2. We retrieve the Kratos cookie from the browser - 3. We generate a Kratos logout URL - 4. We redirect to the Kratos logout URIL - """ - - kratos_cookie = get_kratos_cookie() - if not kratos_cookie: - # No kratos cookie, already logged out - current_app.logger.info("Expected kratos cookie but not found. Redirecting to login"); - return render_template("clear.html", - url="login") - - try: - # Create a Logout URL for Browsers - kratos_api_response = \ - admin_frontend_api.create_browser_logout_flow( - cookie=kratos_cookie) - current_app.logger.info(kratos_api_response) - except ory_kratos_client.ApiException as ex: - current_app.logger.error("Exception when calling" - " create_self_service_logout_flow_url_for_browsers: %s\n", - ex) - return render_template("clear.html", - url=kratos_api_response.logout_url) + return redirect(DASHBOARD_URL) if DEMO_INSTANCE: diff --git a/backend/web/templates/clear.html b/backend/web/templates/clear.html index c1a37ddbc0c3462c3173dc5c02a4e80a392b1655..e7f06a7769ac9ebd73dda9f6685b29ee7e3d4bde 100644 --- a/backend/web/templates/clear.html +++ b/backend/web/templates/clear.html @@ -6,9 +6,9 @@ // Wipe the local storage localStorage.removeItem("persist:root"); // Redirect - window.location = '{{ url }}'; + window.location = '{{ url | safe }}'; </script> -Redirecting ... +Clearing session data and redirecting... {% endblock %} diff --git a/deployment/helmchart/CHANGELOG.md b/deployment/helmchart/CHANGELOG.md index 5a900410217645bb8c03090b1f389b473b6e077c..c75b0d57c2de8b282a748beba041d7b60833d76c 100644 --- a/deployment/helmchart/CHANGELOG.md +++ b/deployment/helmchart/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [1.8.3] + +* Update dashboard to version 0.8.3. + ## [1.8.2] * Update dashboard to version 0.8.2. diff --git a/deployment/helmchart/Chart.yaml b/deployment/helmchart/Chart.yaml index 3e603f9912eedf2569267bb57af9061f0f1e82f3..c2554d29cfd24bdb4f00f7c60b466f757247473d 100644 --- a/deployment/helmchart/Chart.yaml +++ b/deployment/helmchart/Chart.yaml @@ -1,7 +1,7 @@ annotations: category: Dashboard apiVersion: v2 -appVersion: 0.8.2 +appVersion: 0.8.3 dependencies: - name: common # https://artifacthub.io/packages/helm/bitnami/common @@ -23,4 +23,4 @@ name: stackspin-dashboard sources: - https://open.greenhost.net/stackspin/dashboard/ - https://open.greenhost.net/stackspin/dashboard-backend/ -version: 1.8.2 +version: 1.8.3 diff --git a/deployment/helmchart/values.yaml b/deployment/helmchart/values.yaml index 324e62aa794d2c1036ff2b595d7b979b5cc59de3..df3f412f310f9c7c2627a3c5934cdb322f315f79 100644 --- a/deployment/helmchart/values.yaml +++ b/deployment/helmchart/values.yaml @@ -68,7 +68,7 @@ dashboard: image: registry: open.greenhost.net:4567 repository: stackspin/dashboard/dashboard - tag: 0.8.2 + tag: 0.8.3 digest: "" ## Optionally specify an array of imagePullSecrets. ## Secrets must be manually created in the namespace. @@ -236,7 +236,7 @@ backend: image: registry: open.greenhost.net:4567 repository: stackspin/dashboard/dashboard-backend - tag: 0.8.2 + tag: 0.8.3 digest: "" ## Optionally specify an array of imagePullSecrets. ## Secrets must be manually created in the namespace. @@ -723,7 +723,7 @@ tests: image: registry: open.greenhost.net:4567 repository: stackspin/dashboard/cypress-test - tag: 0.8.2 + tag: 0.8.3 pullPolicy: IfNotPresent credentials: user: ""