WIP: feat/automated-account-deletion #174

Draft
b12f wants to merge 34 commits from feat/automated-account-deletion into main
5 changed files with 160 additions and 38 deletions
Showing only changes of commit 4e09d57d7c - Show all commits
flake.lock
tests
keycloak.nix
support
client.nix
keycloak-realm-export
puppeteer-socket/src

8
flake.lock generated
View file

@ -335,11 +335,11 @@
]
},
"locked": {
"lastModified": 1724595780,
"narHash": "sha256-c6XxFH+qo3SbstKAFLcvGn3GHVJxbuXE2VtBnrjBk10=",
"lastModified": 1737810569,
"narHash": "sha256-b3ymxmPuMPnAG6Z8FNErmKzjmUcQkXiTs6WkAE1qBkk=",
"ref": "main",
"rev": "f2a3da5f2637a859897c136e650b88046a89f9fd",
"revCount": 4,
"rev": "af99e9e38fcbdd691c12aa6044cf831c8eea28b4",
"revCount": 6,
"type": "git",
"url": "https://git.pub.solar/pub-solar/keycloak-event-listener"
},

View file

@ -44,8 +44,11 @@ in
import re
import sys
def puppeteer_run(cmd):
client.succeed(f'puppeteer-run \'{cmd}\' ')
def puppeteer_succeed(cmd):
return client.succeed(f'puppeteer-run \'{cmd}\' ')
def puppeteer_execute(cmd):
return client.execute(f'puppeteer-run \'{cmd}\' ')
start_all()
@ -69,32 +72,39 @@ in
client.wait_for_unit("system.slice")
client.wait_for_file("/tmp/puppeteer.sock")
puppeteer_run('page.goto("https://auth.test.pub.solar")')
puppeteer_run('page.waitForNetworkIdle()')
####### Registration #######
puppeteer_succeed('page.goto("https://auth.test.pub.solar")')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("initial")
puppeteer_run('page.locator("::-p-text(Sign in)").click()')
puppeteer_run('page.waitForNetworkIdle()')
puppeteer_succeed('page.locator("::-p-text(Sign in)").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("sign-in")
puppeteer_run('page.locator("::-p-text(Register)").click()')
puppeteer_run('page.waitForNetworkIdle()')
puppeteer_succeed('page.locator("::-p-text(Register)").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("register")
puppeteer_run('page.locator("[name=username]").fill("test-user")')
puppeteer_run('page.locator("[name=email]").fill("test-user@test.pub.solar")')
puppeteer_run('page.locator("[name=password]").fill("Password1234")')
puppeteer_run('page.locator("[name=password-confirm]").fill("Password1234")')
puppeteer_succeed('page.locator("[name=username]").fill("test-user")')
puppeteer_succeed('page.locator("[name=email]").fill("test-user@test.pub.solar")')
puppeteer_succeed('page.locator("[name=password]").fill("Password1234")')
puppeteer_succeed('page.locator("[name=password-confirm]").fill("Password1234")')
client.screenshot("register-filled-in")
puppeteer_run('page.locator("input[type=submit][value=Register]").click()')
puppeteer_run('page.waitForNetworkIdle()')
client.screenshot("after-register")
puppeteer_succeed('page.locator("input[type=submit][value=Register]").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("before-email-confirm")
# Sometimes offlineimap errors out
# ERROR: [Errno 2] No such file or directory: '/home/test-user/.local/share/offlineimap'
client.succeed("${su "mkdir -p ~/.local/share/offlineimap"}")
client.succeed("${su "offlineimap"}")
client.succeed("${su "[ $(messages -s ~/Maildir/test-user@test.pub.solar/INBOX) -eq 1 ]"}")
puppeteer_run('page.locator("a::-p-text(Click here)").click()')
puppeteer_run('page.waitForNetworkIdle()')
puppeteer_succeed('page.locator("a::-p-text(Click here)").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.succeed("${su "offlineimap"}")
client.succeed("${su "[ $(messages -s ~/Maildir/test-user@test.pub.solar/INBOX) -eq 2 ]"}")
mail_text = client.execute("${su "echo p | mail -Nf ~/Maildir/test-user@test.pub.solar/INBOX"}")[1]
boundary_match = re.search('boundary="(.*)"', mail_text, flags=re.M)
if not boundary_match:
@ -105,11 +115,94 @@ in
print(url_match)
if not url_match:
sys.exit(1)
puppeteer_run(f'page.goto("{url_match.group(1)}")')
puppeteer_run('page.waitForNetworkIdle()')
client.screenshot("email-confirmed")
puppeteer_succeed(f'page.goto("{url_match.group(1)}")')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("registration-complete")
sys.exit(0)
time.sleep(1)
####### Logout #######
puppeteer_succeed('page.locator("[data-testid=options-toggle]").click()')
puppeteer_succeed('page.locator("::-p-text(Sign out)").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("logged-out")
####### Login plain #######
puppeteer_succeed('page.locator("[name=username]").fill("test-user")')
puppeteer_succeed('page.locator("::-p-text(Sign In)").click()')
puppeteer_succeed('page.locator("::-p-text(Restart login)").click()')
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")')
puppeteer_succeed('page.locator("::-p-text(Sign In)").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("logged-in")
####### Add TOTP #######
puppeteer_succeed('page.locator("::-p-text(Account security)").click()')
puppeteer_succeed('page.locator("::-p-text(Signing in)").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("signing-in-settings")
puppeteer_succeed('page.locator(`[data-testid="otp/create"]`).click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("TOTP-setup-qr")
puppeteer_succeed('page.locator("::-p-text(Unable to scan?)").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("TOTP-setup-manual")
totp_secret_key = puppeteer_execute('(async () => { const el = await page.waitForSelector("#kc-totp-secret-key"); return el.evaluate(e => e.textContent); })()')[1]
totp = client.execute(f'oathtool --totp -b "{totp_secret_key}"')[1].replace("\n", "")
puppeteer_succeed(f'page.locator("[name=totp]").fill("{totp}")')
puppeteer_succeed('page.locator("[name=userLabel]").fill("My TOTP")')
client.screenshot("TOTP-form-filled")
puppeteer_succeed('page.locator("input[type=submit][value=Submit]").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("TOTP-added")
####### Login w/ TOTP #######
puppeteer_succeed('page.locator("[data-testid=options-toggle]").click()')
puppeteer_succeed('page.locator("::-p-text(Sign out)").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("logged-out")
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")')
puppeteer_succeed('page.locator("::-p-text(Sign In)").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("TOTP-login-form")
print("Sleeping 30s to make sure we roll over into the next TOTP token")
time.sleep(30)
totp = client.execute(f'oathtool --totp -b "{totp_secret_key}"')[1].replace("\n", "")
puppeteer_succeed(f'page.locator("[name=otp]").fill("{totp}")')
puppeteer_succeed('page.locator("::-p-text(Sign In)").click()')
puppeteer_succeed('page.waitForNetworkIdle()')
client.screenshot("TOTP-signed-in")
####### Delete TOTP #######
puppeteer_succeed('page.locator(`[data-testid="otp/credential-list"] button::-p-text(Delete)`).click()')
puppeteer_succeed('page.waitForNetworkIdle()')
puppeteer_succeed('page.locator("main").scroll({ scrollTop: 200 })')
client.screenshot("TOTP-deleted")
'';
}

View file

@ -17,16 +17,13 @@ in
];
security.polkit.enable = true;
services.xserver.enable = true;
services.xserver.displayManager.gdm.enable = true;
services.xserver.desktopManager.gnome.enable = true;
services.xserver.displayManager.autoLogin.enable = true;
services.xserver.displayManager.autoLogin.user = "test-user";
environment.systemPackages = [
puppeteer-run
pkgs.alacritty
pkgs.mailutils
pkgs.oath-toolkit
pkgs.firefox
];
services.getty.autologinUser = "test-user";
@ -41,6 +38,7 @@ in
wayland.windowManager.sway = {
enable = true;
systemd.enable = true;
extraSessionCommands = ''
export WLR_RENDERER=pixman
'';

View file

@ -483,6 +483,31 @@
"webAuthnPolicyPasswordlessAcceptableAaguids": [],
"webAuthnPolicyPasswordlessExtraOrigins": [],
"users": [
{
"id" : "49fcf95e-6fb3-4430-a29a-506a8b20e77c",
"createdTimestamp" : 1673444664000,
"username" : "existing-user",
"enabled" : true,
"totp" : false,
"emailVerified" : true,
"firstName" : "Existing",
"lastName" : "Tester",
"email" : "existing-user@test.pub.solar",
"credentials" : [ {
"id" : "b9fbc30d-4269-49cc-a0ea-9170dc44a30c",
"type" : "password",
"createdDate" : 1673444664000,
"secretData" : "{\"value\":\"yxEZKVTeZlKufE5q4v0Hvxlggg2EaRta5zBtIMxialgwOHrQ3h4Hmre//uk9SlrEv2eqo4aH4bFgPDoktOTyHQ==\",\"salt\":\"d3mk1F43bvQrbV1D+jC1NQ==\",\"additionalParameters\":{}}",
"credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
} ],
"disableableCredentialTypes" : [ ],
"requiredActions" : [ ],
"realmRoles" : [
"default-roles-test.pub.solar"
],
"notBefore" : 0,
"groups" : [ ]
},
{
"id": "a0a10fbb-2d1d-4bf1-918d-86659f7dcef1",
"username": "service-account-admin-cli",

View file

@ -16,12 +16,18 @@ const EXECUTABLE = process.env.EXECUTABLE || 'firefox';
});
const page = await firefoxBrowser.newPage();
page.on('request', request => {
console.log(request.url());
});
// page.on('request', request => {
// console.log(`[puppeteer req] ${request.url()}`);
// });
page.on('response', response => {
console.log(response.url());
// page.on('response', response => {
// console.log(`[puppeteer res] ${response.url()}`);
// });
await page.setViewport({
width: 1200,
height: 600,
deviceScaleFactor: 1,
});
const server = http.createServer({});
@ -41,9 +47,9 @@ const EXECUTABLE = process.env.EXECUTABLE || 'firefox';
const responseText = (() => {
try {
return JSON.stringify({ data: val });
return val.toString();
} catch (err) {
return JSON.stringify({ data: val.toString() });
return val;
}
})();