From df63217120fa37ac329ec63e30e6b0f678093eff Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 12 Jun 2026 10:52:01 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9B=20Fixes=20OIDC=20is=20admin=20?= =?UTF-8?q?lookup=20for=20gitlab=20(#2202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/auth-oidc.js | 8 ++++---- src/utils/auth/OidcAuth.js | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/services/auth-oidc.js b/services/auth-oidc.js index 54783109fb..9dbc29b05e 100644 --- a/services/auth-oidc.js +++ b/services/auth-oidc.js @@ -79,13 +79,13 @@ async function getIssuerContext(issuer) { return ctx; } -/* Pure helper: true when the token's claims map to the configured admin group/role. - Handles standard OIDC top-level `groups`/`roles` claims plus Keycloak's nested - realm_access / resource_access role shapes (mirrors what KeycloakAuth.js does - client-side). */ +/* Helper is true when the token's claims map to the configured admin group/role. + Handles standard OIDC top-level `groups`/`roles` claims, GitLab's `groups_direct` + plus Keycloak's nested realm_access / resource_access role shapes */ function deriveIsAdmin(claims, settings) { if (!claims) return false; const groups = Array.isArray(claims.groups) ? [...claims.groups] : []; + if (Array.isArray(claims.groups_direct)) groups.push(...claims.groups_direct); const roles = Array.isArray(claims.roles) ? [...claims.roles] : []; if (settings.kind === 'keycloak') { diff --git a/src/utils/auth/OidcAuth.js b/src/utils/auth/OidcAuth.js index 87e92fefbf..7a96554a6c 100644 --- a/src/utils/auth/OidcAuth.js +++ b/src/utils/auth/OidcAuth.js @@ -148,7 +148,11 @@ class OidcAuth { /* Mirror the OIDC user into the localStorage keys other parts of Dashy read */ persistUserInfo(user) { - const { roles = [], groups = [] } = user.profile; + const { roles = [] } = user.profile; + // GitLab puts groups in `groups_direct`, so merge both claim names + const groups = [...new Set( + [user.profile.groups, user.profile.groups_direct].filter(Array.isArray).flat(), + )]; const info = { groups, roles }; const isAdmin = (Array.isArray(groups) && groups.includes(this.adminGroup)) || (Array.isArray(roles) && roles.includes(this.adminRole)) From a2e9a52f087b65693dbaf44a085b1db7137fd1ed Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 12 Jun 2026 11:04:17 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A5=85=20Better=20error=20catch/displ?= =?UTF-8?q?ay=20for=20config=20save=20fail=20(#2199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mixins/ConfigSaving.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mixins/ConfigSaving.js b/src/mixins/ConfigSaving.js index 661097b633..36e9116a3c 100644 --- a/src/mixins/ConfigSaving.js +++ b/src/mixins/ConfigSaving.js @@ -59,9 +59,9 @@ export default { }) .catch((error) => { this.saveSuccess = false; - this.responseText = error; - this.showToast(error, false); - ErrorHandler(`Failed to save config. ${error}`); + this.responseText = error.response?.data?.message || error.message || String(error); + this.showToast(this.$t('config-editor.error-msg-cannot-save'), false); + ErrorHandler(`Failed to save config. ${this.responseText}`); this.progress.end(); return false; }); From 265685dfc47c798d12bf495ba8120e4a32550a6d Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 12 Jun 2026 11:05:28 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=A7=AA=20Tests=20for=20OIDC=20admin?= =?UTF-8?q?=20lookup=20(#2202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/server/auth-oidc.test.js | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/server/auth-oidc.test.js diff --git a/tests/server/auth-oidc.test.js b/tests/server/auth-oidc.test.js new file mode 100644 index 0000000000..c9127fc110 --- /dev/null +++ b/tests/server/auth-oidc.test.js @@ -0,0 +1,35 @@ +// @vitest-environment node +import { describe, it, expect } from 'vitest'; +import { deriveIsAdmin } from '../../services/auth-oidc'; + +const oidc = { kind: 'oidc', clientId: 'dashy', adminGroup: 'admins', adminRole: null }; +const keycloak = { kind: 'keycloak', clientId: 'dashy', adminGroup: null, adminRole: 'admin' }; + +describe('deriveIsAdmin', () => { + it('matches adminGroup against the standard groups claim', () => { + expect(deriveIsAdmin({ groups: ['admins', 'users'] }, oidc)).toBe(true); + expect(deriveIsAdmin({ groups: ['users'] }, oidc)).toBe(false); + }); + + it('matches adminGroup against GitLab groups_direct claim', () => { + expect(deriveIsAdmin({ groups_direct: ['admins'] }, oidc)).toBe(true); + expect(deriveIsAdmin({ groups_direct: ['users'] }, oidc)).toBe(false); + }); + + it('matches adminRole against top-level roles claim', () => { + const settings = { ...oidc, adminGroup: null, adminRole: 'admin' }; + expect(deriveIsAdmin({ roles: ['admin'] }, settings)).toBe(true); + }); + + it('matches Keycloak realm and client roles', () => { + expect(deriveIsAdmin({ realm_access: { roles: ['admin'] } }, keycloak)).toBe(true); + expect(deriveIsAdmin({ resource_access: { dashy: { roles: ['admin'] } } }, keycloak)).toBe(true); + expect(deriveIsAdmin({ realm_access: { roles: ['viewer'] } }, keycloak)).toBe(false); + }); + + it('is false on missing claims or no admin config', () => { + expect(deriveIsAdmin(null, oidc)).toBe(false); + expect(deriveIsAdmin({}, oidc)).toBe(false); + expect(deriveIsAdmin({ groups: ['admins'] }, { ...oidc, adminGroup: null })).toBe(false); + }); +}); From ae839623cfe322868d1d624b4dd017e9c1061b4e Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 12 Jun 2026 11:06:28 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9D=20Docs=20for=20attestation=20a?= =?UTF-8?q?nd=20checksum=20verification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/management.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/management.md b/docs/management.md index 02222ee4b1..af06b4b937 100644 --- a/docs/management.md +++ b/docs/management.md @@ -206,6 +206,24 @@ For more information, see the [Watchtower Docs](https://containrrr.dev/watchtowe Stop your current instance of Dashy, then navigate into the source directory. Pull down the latest code, with `git pull origin master`, then update dependencies with `yarn`, rebuild with `yarn build`, and start the server again with `yarn start`. +### Verifying a Release Download + +Each [GitHub release](https://github.com/lissy93/dashy/releases) bundles a SHA256 checksum and a SLSA build-provenance attestation alongside the source tarball (`dashy-.tar.gz`). You don't need either to run Dashy, but they let you confirm a download arrived intact and was genuinely built from our source, rather than tampered with in transit or on a mirror. + +Check the tarball is intact using the `.sha256` file published next to it: + +```bash +sha256sum -c dashy-.tar.gz.sha256 +``` + +An `OK` means the file is untampered. To go further and prove it was built by our CI from our repo, verify the attestation with the [GitHub CLI](https://cli.github.com/): + +```bash +gh attestation verify dashy-.tar.gz --repo lissy93/dashy +``` + +The release notes for each version also list the checksum and a link to view the attestation directly. + **[⬆️ Back to Top](#management)** --- @@ -797,6 +815,22 @@ Only use trusted images, from verified/ official sources. If an app is open sour Unless otherwise configured, containers can communicate among each other, so running one bad image may lead to other areas of your setup being compromised. Docker images typically contain both original code, as well as up-stream packages, and even if that image has come from a trusted source, the up-stream packages it includes may not have. +Every Dashy image published to [GHCR](https://github.com/lissy93/dashy/pkgs/container/dashy) ships with a build-provenance attestation and an SBOM (software bill of materials), both signed keylessly via [Sigstore](https://www.sigstore.dev/). Provenance cryptographically ties the image back to the exact GitHub Actions run and commit that built it, so you can confirm it really came from our pipeline and was not swapped out along the way. The SBOM lists every package baked into the image, which is handy when a new CVE lands and you want to know in seconds whether you're affected. + +To verify the image you're about to run, use the [GitHub CLI](https://cli.github.com/): + +```bash +gh attestation verify oci://ghcr.io/lissy93/dashy:latest --repo lissy93/dashy +``` + +A green check means it was genuinely built by us, from our repo. Worth doing on a fresh Proxmox or homelab box, especially before exposing Dashy beyond your LAN. + +To pull the SBOM and inspect what's inside, use [cosign](https://github.com/sigstore/cosign): + +```bash +cosign download sbom ghcr.io/lissy93/dashy:latest +``` + ### Specify the Tag Using fixed tags (as opposed to `:latest` ) will ensure immutability, meaning the base image will not change between builds. Note that for Dashy, the app is being actively developed, new features, bug fixes and general improvements are merged each week, and if you use a fixed version you will not enjoy these benefits. So it's up to you weather you would prefer a stable and reproducible environment, or the latest features and enhancements.