From 6e2b4c3cc63d364933f6da03e80e0487c1d11edd Mon Sep 17 00:00:00 2001 From: b12f <git@benjaminbaedorf.eu> Date: Sat, 1 Feb 2025 03:44:43 +0100 Subject: [PATCH] feat: add auto-delete-accounts --- flake.lock | 8 +- .../keycloak/auto-delete-accounts/.gitignore | 1 + .../auto-delete-accounts.mjs | 75 +++++++++++++++++++ .../auto-delete-accounts/package-lock.json | 26 +++++++ .../auto-delete-accounts/package.json | 14 ++++ .../keycloak/automated-account-deletion.nix | 10 +++ tests/keycloak.nix | 11 +++ 7 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 modules/keycloak/auto-delete-accounts/.gitignore create mode 100644 modules/keycloak/auto-delete-accounts/auto-delete-accounts.mjs create mode 100644 modules/keycloak/auto-delete-accounts/package-lock.json create mode 100644 modules/keycloak/auto-delete-accounts/package.json create mode 100644 modules/keycloak/automated-account-deletion.nix diff --git a/flake.lock b/flake.lock index f2c2514..bb217bf 100644 --- a/flake.lock +++ b/flake.lock @@ -335,11 +335,11 @@ ] }, "locked": { - "lastModified": 1737819581, - "narHash": "sha256-i9rZSxy33BlDpp4JY9SI2zEFo5EhMnS7cAhqHAPRUZA=", + "lastModified": 1738364013, + "narHash": "sha256-GAg9RaSThTW8+nL/rTsb12i4EHWl5uBqtq8Sp2jEoVg=", "ref": "main", - "rev": "bb9c6f3e3608f0d342ab74d921caddfe4a8bf5d6", - "revCount": 7, + "rev": "1b6c4dda9361bcc108a283e2c38867983474da17", + "revCount": 8, "type": "git", "url": "https://git.pub.solar/pub-solar/keycloak-event-listener" }, diff --git a/modules/keycloak/auto-delete-accounts/.gitignore b/modules/keycloak/auto-delete-accounts/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/modules/keycloak/auto-delete-accounts/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/modules/keycloak/auto-delete-accounts/auto-delete-accounts.mjs b/modules/keycloak/auto-delete-accounts/auto-delete-accounts.mjs new file mode 100644 index 0000000..a2655bd --- /dev/null +++ b/modules/keycloak/auto-delete-accounts/auto-delete-accounts.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env zx + +import { add, sub, isEqual, isAfter } from "date-fns"; + +const realm = argv.realm; +const clientId = argv.clientId; +const server = argv.server; + +/* + * You'll have to set KC_CLI_CLIENT_SECRET + */ + +let users = JSON.parse(await $`kcadm.sh get users -r ${realm} --server ${server} --client ${clientId} --no-config"`); + +// Set a last-login value to today for any accounts that do not have one + +const noLastLogin = users.filter(user => !user.attributes?.["last-login"]?.length); + +const now = new Date(); +const todayString = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${now.getDate()}`; +await Promise.all(noLastLogin.map((user) => { + const attributes = { + "last-login": [todayString], + ...user.attributes, + }; + $`kcadm.sh update users/${user.id} -s 'attributes=${JSON.stringify(attributes)}' -r ${realm} --server ${server} --client ${clientId} --no-config"`; +})); + +users = JSON.parse(await $`kcadm.sh get users -r ${realm} --server ${server} --client ${clientId} --no-config"`); + +// Handle non-validated users + +const nonValidated = users.filter(user => !user.emailVerified); + +await Promise.all(nonValidated.map((user) => { + const lastLogin = new Date(user.attributes?.["last-login"]); + + const deletionDate = add(lastLogin, { months: 1 }); + + if (isEqual(now, sub(deletionDate, { weeks: 1 })) { + // send reminder + } + + if (isEqual(now, sub(deletionDate, { days: 1 })) { + // send reminder + } + + if (isAfter(now, deletionDate)) { + // delete + } +}).filter(n => !!n)); + +const validated = users.filter(user => user.emailVerified); + +await Promise.all(validated.map((user) => { + const lastLogin = new Date(user.attributes?.["last-login"]); + + const deletionDate = add(lastLogin, { years: 2 }); + + if (isEqual(now, sub(deletionDate, { months: 1 })) { + // Send reminder to validated accounts that have not logged in for 2 years - 1 month + } + + if (isEqual(now, sub(deletionDate, { weeks: 1 })) { + // Send reminder to validated accounts that have not logged in for 2 years - 1 week + } + + if (isEqual(now, sub(deletionDate, { days: 1 })) { + // Send reminder to validated accounts that have not logged in for 2 years - 1 day + } + + if (isEqual(now, deletionDate)) { + // Delete validated that have not logged in for more than 2 years + } +}).filter(n => !!n)); diff --git a/modules/keycloak/auto-delete-accounts/package-lock.json b/modules/keycloak/auto-delete-accounts/package-lock.json new file mode 100644 index 0000000..fc87a95 --- /dev/null +++ b/modules/keycloak/auto-delete-accounts/package-lock.json @@ -0,0 +1,26 @@ +{ + "name": "auto-delete-accounts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "auto-delete-accounts", + "version": "1.0.0", + "license": "AGPL-3.0-or-later", + "dependencies": { + "date-fns": "^4.1.0" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + } + } +} diff --git a/modules/keycloak/auto-delete-accounts/package.json b/modules/keycloak/auto-delete-accounts/package.json new file mode 100644 index 0000000..f199b56 --- /dev/null +++ b/modules/keycloak/auto-delete-accounts/package.json @@ -0,0 +1,14 @@ +{ + "name": "auto-delete-accounts", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "AGPL-3.0-or-later", + "description": "", + "dependencies": { + "date-fns": "^4.1.0" + } +} diff --git a/modules/keycloak/automated-account-deletion.nix b/modules/keycloak/automated-account-deletion.nix new file mode 100644 index 0000000..68544a5 --- /dev/null +++ b/modules/keycloak/automated-account-deletion.nix @@ -0,0 +1,10 @@ +{ + writeShellScriptBin, + keycloak, + jq +}: +writeShellScriptBin "autodelete-accounts" '' + set -e + + USERS=$(${keycloak}/bin/kcadm.sh get users -r test.pub.solar --server http://localhost:8080 --realm master --user admin --password password --no-config") +'' diff --git a/tests/keycloak.nix b/tests/keycloak.nix index 3148f98..daef05f 100644 --- a/tests/keycloak.nix +++ b/tests/keycloak.nix @@ -181,6 +181,9 @@ in puppeteer_succeed('page.waitForNetworkIdle()') client.screenshot("logged-out") + auth_server.wait_for_file('/tmp/continue', 3600) + auth_server.execute('rm /tmp/continue') + puppeteer_succeed('page.locator("[name=username]").fill("test-user")') puppeteer_succeed('page.locator("::-p-text(Sign In)").click()') puppeteer_succeed('page.locator("[name=password]").fill("Password1234")') @@ -204,6 +207,9 @@ in puppeteer_succeed('page.waitForNetworkIdle()') client.screenshot("TOTP-signed-in") + auth_server.wait_for_file('/tmp/continue', 600) + auth_server.execute('rm /tmp/continue') + ####### Delete TOTP ####### puppeteer_scroll_into_view('[data-testid="otp/credential-list"]') @@ -212,5 +218,10 @@ in # puppeteer_succeed('page.locator(`[data-testid="otp/credential-list"] button::-p-text(Delete)`).click()') # client.screenshot("TOTP-deleted") + + ####### Automated account deletion ####### + + auth_server.succeed("${pkgs.keycloak}/bin/kcadm.sh get users -r test.pub.solar --server http://localhost:8080 --realm master --user admin --password password --no-config") + ''; }