From 0591264213e90ff9cb4e8c0fcdc0df74cce676b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20L=C3=BCtzner?= Date: Tue, 20 Oct 2026 13:22:16 +0200 Subject: [PATCH] v0.3.7 highlight: single sign-on only, with the login page as its picture (#248) The one feature in v0.3.7 (#169) is marked as its highlight and moved to the top of Unreleased. Its picture is the login page with the provider's button and no password form, taken by a new sso-only test in whats-new-screenshots.spec.ts. The two inline-image entries said the same thing twice; they are one entry now, linked to #213. Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 41 ++++++++++----------- public/whats-new/sso-only.png | Bin 0 -> 8470 bytes tests/e2e/whats-new-screenshots.spec.ts | 45 ++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 23 deletions(-) create mode 100644 public/whats-new/sso-only.png diff --git a/CHANGELOG.md b/CHANGELOG.md index c47339da..c2f13943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ The published image tags: `latest` follows the most recent release below, ## Unreleased + +**Single sign-on can be the only way in.** A new setting under Admin → SSO, **Single sign-on only** +(or `OIDC_DISABLE_PASSWORD_LOGIN=true` in the environment), hides the password form and makes the +server refuse a password sign-in, so everybody comes in through the provider. It is in force only +while a provider is set up, so a provider that was removed cannot leave an install with no way in, +and the box can be ticked only by an administrator who has signed in through the provider +themselves. If the provider is down, `php bin/console app:sso:allow-password-login` brings the form +back. App passwords for mail clients and the phone apps, and sessions that are already open, are not +affected. Off by default. Schema change: one boolean column on `oidc_config`, false for every +existing install. [#169](https://github.com/karatektus/pl_mail/issues/169). + **A reply threads for the person who gets it.** A reply sent from plMail, in the web composer or over JMAP, reached the recipient without `In-Reply-To` or `References`, so Outlook, Gmail and Apple Mail showed it as a new conversation instead of part of the one it answered. The draft always held @@ -124,19 +135,14 @@ a sandbox that sends no session cookie, so each image was answered with a redire page. They now load through a signed link (`/mail/inline/{id}`) that only answers for an image of the message being read, and nothing that is not a picture. The link stops working after a day or two, so one that leaks through a forward or a log does not open the image for good; the message -signs a fresh one each time it is shown. Images labelled `application/octet-stream` or -`image/octet-stream` load the same way when their bytes are a picture. Messages already in the +signs a fresh one each time it is shown. Many mailers label an inline image +`application/octet-stream`, or `image/octet-stream`, which is not a registered type at all and is +what Vaultwarden's notification mails put on their logo, and such a part was served as a download. +For those labels only, the file's own bytes now decide: a PNG, JPEG, GIF or WebP is shown, while +SVG, HTML and anything unrecognised are still downloads. A remote image with that label is +identified the same way by the image proxy and served under its real type. Messages already in the mailbox are fixed too, no backfill needed. No schema change. - -**Single sign-on can be the only way in.** A new setting under Admin → SSO, **Single sign-on only** -(or `OIDC_DISABLE_PASSWORD_LOGIN=true` in the environment), hides the password form and makes the -server refuse a password sign-in, so everybody comes in through the provider. It is in force only -while a provider is set up, so a provider that was removed cannot leave an install with no way in, -and the box can be ticked only by an administrator who has signed in through the provider -themselves. If the provider is down, `php bin/console app:sso:allow-password-login` brings the form -back. App passwords for mail clients and the phone apps, and sessions that are already open, are not -affected. Off by default. Schema change: one boolean column on `oidc_config`, false for every -existing install. [#169](https://github.com/karatektus/pl_mail/issues/169). +[#213](https://github.com/karatektus/pl_mail/issues/213). **A deleted label no longer comes back after the next sync.** On a plain IMAP account, and for a folder on Outlook, deleting a label left its folder on the mail server, and the next sync made the @@ -152,17 +158,6 @@ reached Outlook at all, and now does; labels nested under a Gmail label are dele it. No schema change. [#40](https://github.com/karatektus/pl_mail/issues/40). -**Inline images labelled `application/octet-stream` show up.** Many mailers do not say what an -inline image is — they label it `application/octet-stream`, or `image/octet-stream`, which is not -a registered type at all and is what Vaultwarden's notification mails put on their logo — and the -attachment route served every such part as a download, so the image never appeared in the message. For those labels only, the file's own -bytes now decide, held to the same allow-list as everything else: a PNG, JPEG, GIF or WebP is -served inline, while SVG, HTML and anything unrecognised are still downloads. The image proxy -already did this for unlabelled remote images and now shares the same check, so a remote image -labelled `image/octet-stream` is now also identified from its bytes and served under its real -type, rather than passed through under the label it came with. No schema change. -Part of [#213](https://github.com/karatektus/pl_mail/issues/213). - **A mail can go to Bcc recipients only.** Sending with nobody in To was refused with "Add at least one recipient before sending", even with addresses in Cc or Bcc: the window counted all three, but the server only counted To. It now counts all three as well. A mail with no recipient at all is diff --git a/public/whats-new/sso-only.png b/public/whats-new/sso-only.png new file mode 100644 index 0000000000000000000000000000000000000000..fca4ddad53ef85d2c7aaee052c514e9a349ca8b4 GIT binary patch literal 8470 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/whats-new-screenshots.spec.ts b/tests/e2e/whats-new-screenshots.spec.ts index ad805ce9..f798fb9b 100644 --- a/tests/e2e/whats-new-screenshots.spec.ts +++ b/tests/e2e/whats-new-screenshots.spec.ts @@ -188,4 +188,49 @@ test.describe("What's new screenshots, as an administrator", () => { await capture(page, "sso", panel, 560); }); + + /** + * Single sign-on only (#169): the login page with the provider's button and + * no password form, which is the feature as everybody else meets it. + * + * The provider is saved through Admin → SSO, since the client secret is + * stored encrypted and SQL cannot write it. The box itself is set in the + * database: the form only lets an administrator tick it who has signed in + * through that provider, and this stack has no provider to sign in at. + * Undone in `finally`, so the stack is left with password sign-in and no + * provider, as the other pictures and the regression suite expect. + */ + test("sso-only", async ({ page }) => { + await login(page, TEST_DEMO_ADMIN.email, TEST_DEMO_ADMIN.password); + await page.goto("/admin?section=sso"); + + const panel = page.locator("turbo-frame#admin-sso"); + await panel.getByLabel("Issuer").fill("https://auth.example.com/application/o/plmail/"); + await panel.getByLabel("Client ID").fill("plmail"); + await panel.getByLabel("Client secret").fill("not-a-real-secret"); + await panel.getByLabel("Name on the button").fill("Authentik"); + await panel.getByRole("button", { name: "Save", exact: true }).click(); + await expect(panel.getByLabel("Issuer")).toHaveValue("https://auth.example.com/application/o/plmail/"); + + try { + consoleCommand(`dbal:run-sql "UPDATE oidc_config SET password_login_disabled = true"`); + await page.context().clearCookies(); + await page.goto("/login"); + + await expect(page.getByLabel("Password")).toHaveCount(0); + await expect(page.getByText("Authentik")).toBeVisible(); + + // With a margin of the page around it: cropped to the box, the + // logo touches the top edge and the card's bottom border is lost. + const box = await page.locator("div.max-w-sm").first().boundingBox(); + expect(box, "sso-only: no login card").not.toBeNull(); + await page.screenshot({ + path: `${OUT}/sso-only.png`, + clip: { x: box!.x - 32, y: box!.y - 32, width: box!.width + 64, height: box!.height + 64 }, + }); + } finally { + consoleCommand("app:sso:allow-password-login"); + consoleCommand(`dbal:run-sql "DELETE FROM oidc_config"`); + } + }); });