406 Commits

Author SHA1 Message Date
Dave Kempe b799a30ac9 deps: adopt 8 dependabot bumps
rustls 0.23.45, uuid 1.26.1, russh 0.63.3, tokio-rustls 0.26.5, aes 0.9.3,
rcgen 0.14.10, tower-http 0.7.1, toml 1.1.6. All are patch level within the
existing Cargo.toml constraints, so this is a lockfile-only change.

Two transitive majors ride along: rcgen takes pem 3.0.6 to 4.0.0, and rustls
takes aws-lc-rs 1.17.3 to 1.18.1 with aws-lc-sys 0.43.0 to 0.45.0. Release
build, 273 tests, clippy and cargo audit all clean.

Closes #215, #216, #217, #219, #224, #225, #226, #227.
v1.10.0
2026-09-19 16:25:17 +10:00
Dave Kempe 24c7a371e4 Merge H.264 passthrough fixes from @pletch (#210)
Supersedes the cherry-picked 2449aa5 (install wrappers once), which stopped
the recursion crash but left passthrough silently dead after the RDPGFX
channel reconnect that xrdp performs at login. b3b2cce guards per callback
on what is actually installed, covering both failure directions.

Also brings 869c738, which releases the sync gate at the pipeline depth
waitForPending() gates on rather than at zero. Releasing only at zero meant
a continuously decoding session never drained fully, so every sync waited
out its 200ms timeout: choppy audio and a framerate throttled by guacd
pacing against an inflated lag figure.

# Conflicts:
#	patches/004-h264-passthrough.patch
2026-09-19 16:11:35 +10:00
Dave Kempe 4e665a9806 Merge OIDC group mapping CLI from @zregvart (#213) 2026-09-19 14:42:45 +10:00
Dave Kempe 492e329670 docs: CLI group mapping, CLI audit log, and two stale OIDC docs
Documents the map-group subcommand in the group-to-role mappings section:
creating a mapping, the one-mapping-per-group rule and the --force re-map,
and that mappings are applied at the user's next OIDC login rather than
immediately, which matters in both directions when revoking access.

Documents the new cli_audit_log table: what is and is not recorded, that
the actor prefers SUDO_USER so sudo names the person rather than root,
that only successful actions are logged, how to read it with sqlite3, and
the 90 day retention.

Also corrects two pre-existing errors found while writing the above:

- deployment-guide.md showed an [oidc.group_role_mappings] config table.
  No such config field exists, so anyone following the guide would add it
  and silently get no mappings at all. Replaced with the CLI commands that
  actually create them. The same block had session_ttl_secs under [oidc];
  the real key is auth_session_ttl_secs and it is top level.
- roles-and-access-control.md documented an enable-user subcommand that
  does not exist. Re-enabling is done from the Admin page or the API, so
  the doc now says that, with a warning about disabling your own account.
2026-09-19 14:42:45 +10:00
Dave Kempe 2021508a99 map-group: friendly re-mapping, CLI audit log, hermetic tests
Follow-up to the map-group subcommand.

oidc_group is UNIQUE, so mapping a group that already had a mapping failed
on the constraint and surfaced a raw "UNIQUE constraint failed" error, with
no way to change a mapping from the CLI at all. The command now looks the
group up first: an unchanged mapping is a no-op, a conflicting one explains
itself and points at the new --force flag, and --force re-maps through
db::update_group_mapping. This matches the API, which already answers 409
rather than 500 on a duplicate.

CLI commands hold the database directly and so bypass the API's
authenticated audit trail, leaving role grants and account deletions made
from a shell with no record anywhere. A new cli_audit_log table and
db::log_cli_event record the admin, user-role and group-mapping mutations,
attributed to SUDO_USER where present so an action taken through sudo names
the person rather than root. Entries are headline only, never API keys or
passwords, and are covered by the existing retention cleanup.

Tests: assert_cmd's Command is aliased so it no longer shadows the clap
Command enum that use super::* brings into scope, and the CLI tests run in
a per-test scratch directory removed on drop rather than writing a database
into tests/cli inside the repo. Adds coverage for role rejection, the
duplicate path, --force re-mapping, and the audit row.
2026-09-18 14:35:20 +10:00
Zoran Regvart fcaae650d2 Create OIDC group mappings via CLI
Adds new CLI subcommand `map-group` to create a mapping of OIDC group to
a role.
2026-09-18 14:35:09 +10:00
pletch 14490cbab5 Revert "fix(h264): convert 4:4:4 chroma in the stream's own colour space"
This reverts 8988165, restoring the unconditional full-range BT.709
conversion. The commit it undoes identified a real inconsistency and
resolved it toward the wrong side.

Its reasoning was that the 4:2:0 path never reaches the shader, so the
combined 4:4:4 picture converted differently from the 4:2:0 one beside
it, and that adopting the decoded frame's colour space would make the two
agree. The two did then agree -- as limited range, on hosts that send
full range. Before it, 4:4:4 was correct and 4:2:0 was crushed; after it,
both were crushed. The symptom it describes, "the combined 4:4:4 picture
renders visibly flatter", was the correct picture beside a crushed one.

What every RDP host actually sends is now read out of the SPS rather than
assumed. Windows writes video_full_range_flag=1 with no colour
description. The xrdp fork writes it with a complete BT.709 description.
Stock xrdp 0.10.6 omits the video_signal_type block altogether -- it
passes x264 no VUI parameters -- while naming its own conversion
XRDP_yuv444_709fr. All three encode full-range BT.709, which is what
MS-RDPEGFX defines.

What the browser reports for those streams is a different question.
Chrome's hardware decoder ignores a bare range flag with no description
beside it and reports limited for a host that plainly declared full, so
a shader that adopts the frame's colour space expands 16-235 to 0-255 on
full-range samples: blacks crushed to zero, whites clipped, chroma
over-saturated by 255/224.

The inconsistency this reinstates is real and is not being dismissed: a
session can render 4:4:4 correctly and 4:2:0 crushed. One path right
beats both wrong, and the repair belongs upstream of the browser --
completing the host's SPS on the wire, so that neither path has to guess
and third-party clients benefit too. That follows separately; it is
server-side and does not belong in this PR.

The coefficients are exact BT.709 rather than the 8-bit approximations
the original cited from FreeRDP's prim_internal.h. Under passthrough
FreeRDP never decodes these streams, so matching its rounding was never
the point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Rxg4cT87G9czw4nUxKhHG
2026-09-10 09:51:39 -04:00
pletch 869c738cf3 fix(h264): release the sync gate at the depth it waits on, not at zero
waitForPending() holds the Guacamole sync response while pendingDecodes
exceeds MAX_PIPELINE_DEPTH, but resolveIfIdle() only fired its resolvers at
pendingDecodes <= 0. So once the backlog crossed the depth it had to drain
completely before a sync could go out, and a session decoding continuously
never reaches zero. Every sync therefore waited out its full timeout,
logging "sync wait timeout" and reporting that much processing lag however
fast the client was really going.

guacd paces frames against the client's processing lag, so the session runs
slow while the client is not in fact behind. The function's own
documentation already said the callback fires "when the backlog is within
the allowed pipeline depth"; only the implementation disagreed.

It shows up first on AVC444, where a picture is two access units and the
backlog therefore runs twice as deep for the same frame rate, making it far
less likely to touch zero between frames. That presents as AVC444 being
expensive rather than as a threshold mismatch, so an A/B that varies the
server between AVC444 and AVC420 while holding the client constant cannot
tell the two apart.
2026-09-08 18:57:51 -04:00
pletch bdea2ff1b8 fix(h264): do not shadow the pixel row in the chroma shader
The colour-space change introduced "float y" for the scaled luma, but y is
already the int pixel row in that scope. GLSL rejected the redefinition and
then every arithmetic line that mixed the two, so the shader failed to
compile and the renderer fell back to 4:2:0 for the whole session:

  ERROR: 0:89: 'y' : redefinition
  ERROR: 0:93: '+' : no operation exists that takes highp int and highp float

Name it luma.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 02:02:20 -04:00
pletch 8988165159 fix(h264): convert 4:4:4 chroma in the stream's own colour space
The combining shader hardcoded BT.709 at full range, citing the coefficients
FreeRDP decodes these streams with. But in passthrough guacd never decodes --
the browser does, and the 4:2:0 path never reaches the shader at all: that
VideoFrame is drawn by the browser, which applies the frame's own colour
space including the limited-range expansion of 16-235 to 0-255.

So the two paths converted differently on the same session. An H.264 stream
carrying no VUI signalling is limited range, and treating it as full leaves
black at RGB 16 and white at 235, with chroma unscaled to match: the combined
4:4:4 picture renders visibly flatter than the 4:2:0 one beside it, depending
only on which codec the server chose.

Take the range and matrix from the decoded frame's colorSpace instead, read
once from the first main view, and default to limited range BT.709 when the
decoder reports nothing -- which is what the absence of signalling means.
VideoFrame.colorSpace was available in scope and never consulted.
2026-09-08 01:49:08 -04:00
pletch b3b2cce36e fix(rdp): reinstall the RDPGFX wrappers after a channel reconnect
The previous commit stopped guac_rdp_rdpgfx_channel_connected() from wrapping
its own wrapper, which fixed the recursion crash, but it guarded on
orig_surface_command being NULL:

    if (rdp_client->settings->enable_h264
            && rdp_client->orig_surface_command == NULL) {

orig_surface_command lives on guac_rdp_client, so it is per-connection, while
the handler it protects is per-channel-instance. FreeRDP's
gdi_graphics_pipeline_uninit() sets SurfaceCommand to NULL and
gdi_graphics_pipeline_init() assigns gdi_SurfaceCommand back
(libfreerdp/gdi/gfx.c:1864, :1911), so a reconnect restores FreeRDP's own
handler -- and the once-only guard, seeing a non-NULL orig_surface_command
from the first install, declines to wrap it again. Passthrough is then dead
for the rest of the session.

xrdp reconnects the channel at login, for the display resize the previous
commit already identified, so this fired on essentially every xrdp session.
It was silent: the surface commands still reached FreeRDP's GDI decoder and
guacd re-encoded them as JPEG/WebP, so the desktop rendered correctly at
~100% of a core while no H.264 reached the browser. A trace on xrdp 0.10.6
showed one keyframe forwarded, the channel unloading 52ms later, and no
"H.264 passthrough enabled" line after the reconnect that followed.

Guard on what is actually installed, per callback. The two are restored on
different schedules -- gdi_graphics_pipeline_init() assigns SurfaceCommand
but never touches CapsConfirm -- so after a reconnect CapsConfirm is still
ours while SurfaceCommand is FreeRDP's again. A single guard keyed on
SurfaceCommand would pass and then point orig_caps_confirm at
guac_rdp_gfx_caps_confirm itself, reaching the same stack exhaustion by the
other callback.

With this, the same host forwards 807 AVC420 deltas and 3 keyframes over 60s
of desktop use, with guacd at 3.3% of a core.
2026-09-02 23:33:58 -04:00
pletch cf917643d5 fix(rdp): install the RDPGFX wrappers once, and only for H.264
guac_rdp_rdpgfx_channel_connected() installed the SurfaceCommand and
CapsConfirm wrappers unconditionally, every time it ran. It runs again when
the RDPGFX channel reconnects, which xrdp provokes with the display resize it
performs at login, and the second pass saved the wrapper the first pass had
installed:

    rdp_client->orig_surface_command = (void*) rdpgfx->SurfaceCommand;
    rdpgfx->SurfaceCommand = guac_rdp_gfx_surface_command;

leaving orig_surface_command pointing at guac_rdp_gfx_surface_command itself.
Any surface command that is not H.264 falls through to orig(context, cmd),
re-enters the wrapper, and recurses until the stack is exhausted and the guacd
child dies -- a white screen in the browser, with no "Client terminated
successfully" in the log and the segfault visible only outside a container.

H.264 commands take skip_decode and never reach that call, so a server sending
nothing but H.264 is unaffected. A stock xrdp is the opposite case: one trace
showed 7440 planar and 12 progressive surface commands against 437 AVC420, so
it dies on the first frame. Distro xrdp packages (0.10.1 on Ubuntu) are built
--enable-rfxcodec with no H.264 encoder at all, so they send planar for
everything and hit this immediately.

The wrappers were also installed for every GFX session regardless of
enable_h264, so this reproduced with RFX alone and H.264 unticked -- no
H.264-capable server required.

Both halves are fixed: install only when the connection asked for H.264, and
only when nothing is installed yet. guac_rdp_client is guac_mem_zalloc'd, so
the NULL check is sound on first entry.
2026-09-01 14:30:06 +10:00
pletch 2449aa500d fix(rdp): install the RDPGFX wrappers once, and only for H.264
guac_rdp_rdpgfx_channel_connected() installed the SurfaceCommand and
CapsConfirm wrappers unconditionally, every time it ran. It runs again when
the RDPGFX channel reconnects, which xrdp provokes with the display resize it
performs at login, and the second pass saved the wrapper the first pass had
installed:

    rdp_client->orig_surface_command = (void*) rdpgfx->SurfaceCommand;
    rdpgfx->SurfaceCommand = guac_rdp_gfx_surface_command;

leaving orig_surface_command pointing at guac_rdp_gfx_surface_command itself.
Any surface command that is not H.264 falls through to orig(context, cmd),
re-enters the wrapper, and recurses until the stack is exhausted and the guacd
child dies -- a white screen in the browser, with no "Client terminated
successfully" in the log and the segfault visible only outside a container.

H.264 commands take skip_decode and never reach that call, so a server sending
nothing but H.264 is unaffected. A stock xrdp is the opposite case: one trace
showed 7440 planar and 12 progressive surface commands against 437 AVC420, so
it dies on the first frame. Distro xrdp packages (0.10.1 on Ubuntu) are built
--enable-rfxcodec with no H.264 encoder at all, so they send planar for
everything and hit this immediately.

The wrappers were also installed for every GFX session regardless of
enable_h264, so this reproduced with RFX alone and H.264 unticked -- no
H.264-capable server required.

Both halves are fixed: install only when the connection asked for H.264, and
only when nothing is installed yet. guac_rdp_client is guac_mem_zalloc'd, so
the NULL check is sound on first entry.
2026-08-31 23:03:57 -04:00
Dave Kempe 945bb7c6a9 v1.10.0 2026-08-31 07:43:21 +10:00
Dave Kempe 833745ed6b Merge H.264 AVC444 passthrough CPU win from @pletch (#208)
Restores GfxAVC444=TRUE, skips the server-side H.264 decode, forwards both
AVC444 views (new 'view' field on the h264 instruction + Yuv444.js WebGL2
4:4:4 reconstruction), and fixes blank H.264 recording playback. Review
findings folded in (f16eed7). guacd CPU ~100%/90% of a core -> ~2%.
2026-08-31 07:34:20 +10:00
Dave Kempe 4e8bf488b4 deps: bump uuid to 1.26.0 (latest) 2026-08-30 19:40:36 +10:00
Dave Kempe ce93fd1feb deps: adopt russh 0.63.1 and uuid 1.25.0
russh 0.63 changed the check_server_key callback to take
&PublicKeyOrCertificate; resolve it to a PublicKey via .public_key() in
both the tunnel and probe handlers, and widen the russh constraint to
0.63. Closes the retargeted dependabot PRs #204 (uuid) and #205 (russh).
2026-08-30 05:48:08 +10:00
Dave Kempe e6ae176317 v1.9.10 v1.9.10 2026-08-28 15:57:07 +10:00
Dave Kempe 3db94840af fix(deps): bump h2 to 0.4.19 for RUSTSEC-2026-0258; chacha20 0.10.2
The CI `cargo audit` step was failing on RUSTSEC-2026-0258 (h2 unbounded
empty DATA frames, a HTTP/2 DoS), pulled in transitively via reqwest/hyper.
Bump h2 0.4.15 -> 0.4.19 (patched) and chacha20 0.10.1 -> 0.10.2 (the prior
version was yanked).
2026-08-28 15:55:30 +10:00
Dave Kempe 2ff0cb12d7 docs: document supported browsers (H.264-accelerated RDP needs Chrome/Firefox) 2026-08-28 15:24:48 +10:00
Dave Kempe a360ee1fdb deps: bump futures-util, russh, uuid, bollard, rusqlite, base64, rcgen
Routine dependabot patch bumps (closes the open PRs on push), plus the
docker/login-action CI action 4.5.2 -> 4.6.0.
2026-08-28 15:24:48 +10:00
Dave Kempe 93c2540afb feat(rdp): configurable keyboard layout (#209)
Adds a per-entry RDP keyboard layout, wiring guacd's `server-layout`
parameter (previously hardcoded to empty) through the address-book entry,
the connect request, and the guacd handshake, with a dropdown in the
connections editor and a migration mapping from Guacamole's `server-layout`
on import. Empty keeps guacd's default (en-us-qwerty).
2026-08-28 15:24:38 +10:00
Dave Kempe a28417a496 fix(credentials): surface My Credentials variables for entries granted at a subfolder
list_credential_variables walked from top-level folders using
check_folder_access + continue, short-circuiting the whole subtree when an
ancestor was not directly accessible. An operator granted access at a
subfolder level saw "No credential variables" in My Credentials even though
the connections tree showed the entry (the tree is descendant-aware). It now
descends past inaccessible ancestors, collecting variables only from folders
the user can directly access. Adds regression tests.
2026-08-28 15:24:26 +10:00
Dave Kempe 7e2cb39690 fix(lint): satisfy clippy 0.1.98
CI's runners updated to clippy 0.1.98, which enforces two lints that
existing code tripped under -D warnings. Rewrite the two late-init
if/else blocks in the SSH tunnel builder as tuple `let` bindings
(needless-late-init), and allow result-large-err on check_folder_access,
whose Err is an axum Response by design.
2026-08-28 15:23:53 +10:00
pletch f16eed73ab fix(h264): size aux snapshots from the render, keep the combine watchdog
Review notes from the upstream issue thread:

- The auxiliary-view snapshot was sized to the auxiliary frame's own
  dimensions. The v1 chroma layout pads that view to a multiple of 16
  rows, so the snapshot could be taller than the picture the renderer
  draws; with no rects the whole thing is blitted, painting a blank
  strip below the image. Size it from the rendered canvas instead.

- The per-frame watchdog was cleared before combineFrame() ran, so the
  4:4:4 path had no timeout despite the comment saying otherwise: a
  copyTo() that never settled would hold the ordered display queue with
  nothing able to release it. Clear it once the frame is done with
  instead -- in the finally for the synchronous paths, and after the
  copy settles for the combine.

- Drop the unreachable view != 0 branch after the combine hand-off.

- '0' no longer coerces to false in override(), so ?h264ChromaFilter=0
  sets threshold zero rather than switching the filter off. Boolean
  overrides are unaffected: they coerce, and 0 is falsy.

- Drop the stale stats() reference from client.html; the decoder has no
  such method.
2026-08-26 19:27:33 -04:00
pletch 691634701e fix(recordings): load the H.264 decoder for recording playback
Recordings capture the raw protocol stream, so a recording of an H.264
session contains h264 instructions. recordings.html loaded neither
H264Decoder.js nor Yuv444.js, so playback of those sessions rendered
nothing.
2026-08-26 19:27:33 -04:00
pletch 5b5661888b feat(rdp): negotiate AVC444, skip the server-side H.264 decode
H.264 passthrough had two independent defects. On Windows hosts it
negotiated no H.264 at all, and where it did engage it delivered about
half the saving it should.

GfxAVC444 = FALSE was set to make the client advertise AVC420-only,
which is not what the combination does. FreeRDP gates the entire RDPGFX
V10+ capability block on

    !GfxH264 || GfxAVC444

(rdpgfx_main.c:254, rdpgfx_send_caps_advertise_pdu), so with GfxH264 set
and GfxAVC444 clear no V10 capability set is sent at all -- the client
advertises only V8 and V8.1. Windows will not offer H.264 at V8.1: it
falls back to CLEARCODEC and CAPROGRESSIVE, which guacd then decodes and
re-encodes as JPEG/WebP, exactly the cost the feature exists to avoid.
The display stays correct, so the regression is invisible without
measuring CPU or counting codecs on the wire. Over a minute of 1080p
video on Windows 11 Pro: 4050 CLEARCODEC + 1447 CAPROGRESSIVE + zero
H.264 frames before, all frames codec 15 after. xrdp is unaffected,
which is why this went unnoticed -- it accepts AVC420 at V8.1 and never
needed the V10 capsets.

AVC444 was disabled because forwarding it was broken, not because
AVC420 was wanted: only bitstream[0] reached the client, rendering as a
green/magenta split. Forwarding one view cannot work -- both bitstreams
decode through the same H264_CONTEXT in FreeRDP's avc444_decompress, so
they are one H.264 sequence with alternating views. Dropping either
breaks reference frames for the other, and the decoder reports no error
because what arrives is well formed. Both views are now forwarded,
tagged with a view field on the h264 instruction; the client decodes
both, draws only view 0, and combines the auxiliary view's chroma into
full 4:4:4 in a WebGL2 pass, falling back to 4:2:0 if it cannot.

The GDI decode is now skipped for captured H.264 commands. Previously
the wrapper always called through to the original handler, so only the
re-encode was saved. RemoteFX, planar and progressive still decode
normally, so a server mixing codecs within a frame stays correct.

guacd CPU with 1080p video playing, sampled per-thread over 30s:

    xrdp (AVC420)        ~100% of a core  ->  2.0%
    Windows 11 (AVC444)   90.6% of a core ->  2.1%

Configuration is unchanged: the per-connection enable-h264 argument is
the only switch, and there are no environment variables.
2026-08-26 19:27:33 -04:00
Dave Kempe 6cfe341920 v1.9.9 v1.9.9 2026-08-11 11:35:09 +10:00
Dave Kempe 140418ca82 feat(import): add --map to rewrite credential fields on Guacamole import
Maps Apache Guacamole passthrough tokens (${GUAC_USERNAME} etc.) to rustguac
credential variables during import, so imported connections arrive wired to
My Credentials. Repeatable FROM=TO; applied to the username, password,
domain, and private_key fields. Adds tests and docs.
2026-08-11 11:32:26 +10:00
Dave Kempe d8cbf6d35c fix(recording): never rotate in-progress recordings; stop when a delete frees no space
Disk-based rotation deleted the oldest .guac unconditionally until statvfs
usage fell below max_disk_percent. Recordings for live sessions are held
open by the recording tee, so unlinking one frees no space and statvfs never
drops: the loop then deleted every recording in the directory in a single
pass while the disk stayed full (a production host lost all recordings this
way).

rotate() now excludes live-session recordings (SessionManager::
active_recording_paths) and stops if a deletion frees no space. Adds
regression tests.
2026-08-11 11:32:26 +10:00
dependabot[bot] 3e6834ff8d deps: bump aes from 0.9.1 to 0.9.2 (#192)
Bumps [aes](https://github.com/RustCrypto/block-ciphers) from 0.9.1 to 0.9.2.
- [Commits](https://github.com/RustCrypto/block-ciphers/compare/aes-v0.9.1...aes-v0.9.2)

---
updated-dependencies:
- dependency-name: aes
  dependency-version: 0.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 20:42:33 +10:00
dependabot[bot] 1521f00938 deps: bump russh from 0.62.4 to 0.62.5 (#193)
Bumps [russh](https://github.com/warp-tech/russh) from 0.62.4 to 0.62.5.
- [Release notes](https://github.com/warp-tech/russh/releases)
- [Commits](https://github.com/warp-tech/russh/compare/v0.62.4...v0.62.5)

---
updated-dependencies:
- dependency-name: russh
  dependency-version: 0.62.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 20:42:29 +10:00
dependabot[bot] 750c9ac5fb deps: bump rustls from 0.23.42 to 0.23.43 (#196)
Bumps [rustls](https://github.com/rustls/rustls) from 0.23.42 to 0.23.43.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.42...v/0.23.43)

---
updated-dependencies:
- dependency-name: rustls
  dependency-version: 0.23.43
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 20:42:25 +10:00
dependabot[bot] 776ac6f450 deps: bump toml from 1.1.3+spec-1.1.0 to 1.1.4+spec-1.1.0 (#197)
Bumps [toml](https://github.com/toml-rs/toml) from 1.1.3+spec-1.1.0 to 1.1.4+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v1.1.3...toml-v1.1.4)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.1.4+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 20:42:20 +10:00
dependabot[bot] 4791056067 build(deps): bump russh from 0.62.4 to 0.62.5 in /fuzz (#194)
Bumps [russh](https://github.com/warp-tech/russh) from 0.62.4 to 0.62.5.
- [Release notes](https://github.com/warp-tech/russh/releases)
- [Commits](https://github.com/warp-tech/russh/compare/v0.62.4...v0.62.5)

---
updated-dependencies:
- dependency-name: russh
  dependency-version: 0.62.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 20:42:15 +10:00
dependabot[bot] 51fc1e1577 ci: bump docker/login-action from 4 to 4.5.2 (#191)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4 to 4.5.2.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v4...v4.5.2)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 20:42:12 +10:00
Dave Kempe e40757b29b v1.9.8 v1.9.8 2026-08-09 08:26:19 +10:00
Dave Kempe f97a3a619c fix(docker): install libspice-client-glib runtime lib so SPICE loads (#195)
The builder stage compiles the guacd SPICE plugin (--with-spice + the
-dev lib), but the runtime stage never installed the SPICE runtime
shared library. libguac-client-spice.so was present yet failed to
dlopen (libspice-client-glib-2.0.so.8 missing), so guacd reported
"Support for protocol spice is not installed". Add the runtime lib
to the runtime stage; apt pulls the rest of the chain (gstreamer,
opus). Bare-metal installs were unaffected. Broken since SPICE was
vendored (c2c5679).
2026-08-09 07:22:07 +10:00
Dave Kempe a897fcc63b v1.9.7 v1.9.7 2026-07-30 20:50:26 +10:00
Dave Kempe a784498886 fix(build): pin guacd to 6719b20d in build-deb.sh, apply patches strictly
build-deb.sh had lost its guacd pin during a patch rebase and was cloning
apache/guacamole-server master (--depth 1, latest) and applying patches
best-effort with git apply --check, silently skipping any that no longer
applied. The sol1 apt CI runs build-deb.sh in a fresh container, so its
.deb built guacd from whatever upstream master was that day, with patches
possibly dropped - a non-reproducible build that diverged from the pinned
release.yml and install.sh paths and shipped a broken guacd.

Restore GUACD_COMMIT=6719b20d (the commit the patch set is rebased onto,
matching release.yml and install.sh), build from a fresh pinned clone into
a throwaway dir (leaving any local ../guacamole-server checkout untouched;
GUACD_SRC_OVERRIDE reuses one for local patch iteration), and apply patches
strictly so a non-applying patch fails the build instead of silently
shipping without it.
2026-07-30 20:41:41 +10:00
Dave Kempe 658b1c1764 v1.9.6 v1.9.6 2026-07-30 11:26:21 +10:00
Dave Kempe ce8aa1de22 docs(screenshots): refresh for multi-Vault UI + add My Credentials
Refresh Connections (credentials banner/pills, Credentials nav, larger
folder chevron), and the RDP/VDI/Web/SSH-tunnel editors, plus the
Sessions/Recordings/Reports/Admin pages (real data sanitised). Add a new
My Credentials screenshot (scroll/filter/collapse, masked fields). Drop
the stale version note. tokens_view and recordings_player_histogram are
left as their prior captures.
2026-07-30 09:49:00 +10:00
Dave Kempe ba207010b7 docs: document multi-Vault backends, per-credential scope, My Credentials UX
- configuration.md: [vault_shared]/[vault_local] backends, the DR rationale,
  user_credentials_default_scope, and the VAULT_SHARED/LOCAL_SECRET_ID env vars.
- credential-variables.md: Credentials nav link, scroll/filter/collapsible
  editor, resumable focused prompt, needed-pill + banner, and the shared/local
  per-credential scope with its outage trade-off.
- migration.md: vault-migrate runbook (dry-run, copy, cutover ordering).
- CLAUDE.md: multi-Vault backend summary under the Vault section.
2026-07-29 20:07:51 +10:00
Dave Kempe 842aeec8f1 feat(connections): persist credentials-banner dismissal
Dismissing the "N credentials to set up" banner now persists in
localStorage instead of a page-lifetime flag, so it stays gone across
refreshes. It records exactly which credentials were dismissed and
re-appears only if a genuinely new unset credential shows up — so a
partial save that shrinks the set won't re-nag, but a new requirement
still surfaces.
2026-07-29 19:40:12 +10:00
Dave Kempe 9c07249407 fix(connections): larger, clearer credential group chevron
The collapsible group caret used the small-triangle glyph (U+25BE) like
the old folder chevron did, so it read faint. Switch to the full-size
U+25BC and bump its size, matching the folder-tree chevron.
2026-07-29 19:36:05 +10:00
Dave Kempe 484b7bcb26 feat(connections): make My Credentials scale - scroll, filter, collapse
The credential editor rendered a flat list inside a modal that scrolled as
a whole, so past a few variables the Save/Close buttons and context
scrolled off and it got unusable. Restructure it: the modal header (title,
description, filter box) and the Save/Close footer are pinned and only the
credential list scrolls. Add a filter box (shown once there are more than a
handful of variables) that live-filters rows by name/domain, and group each
domain into a collapsible section. Focused single-connect mode is unchanged
(few rows, no filter box).
2026-07-29 19:31:04 +10:00
Dave Kempe eaf6525320 feat(connections): proactive credentials banner and per-entry pills
Fetch the user's set credential variables on load and cross-reference
against each entry's credential_variables (already on EntryInfo). Entries
that reference variables the user hasn't set get a "credentials needed"
pill, and a dismissible banner reports how many credentials are still to
set up (aggregated across the search index). Both link to the editor, and
refresh after a save. Turns the old reactive "connect fails, then hunt for
the menu" flow into something visible up front.
2026-07-29 18:21:10 +10:00
Dave Kempe 95e2c6d643 feat(connections): resumable credential prompt + Credentials nav link
When a connect fails for missing credential variables, open the My
Credentials editor focused on just the missing variables ("credentials
needed to connect to X") and, on save, re-run that exact connect instead
of dead-ending the user in the full editor with a toast. openMyCredentials
now takes { onlyVars, heading, onSaved }.

Also promote My Credentials to a visible "Credentials" nav link (kept in
the Settings menu too) so people can find it without hitting a failure
first.
2026-07-29 17:43:41 +10:00
Dave Kempe f5ccc35cdf feat(vault): per-credential shared/local scope for credential variables
Each per-user credential variable can live in the shared backend (fleet-
wide) or the local backend (this instance) - its location is the scope.
Reads merge both backends (local wins); writes partition by a per-key
scope sent from My Credentials, moving a key between backends when its
toggle changes and honouring "leave blank to keep". A "Shared across
sites" toggle appears per credential.

Quick-start preserved: with one Vault the shared and local cells alias the
default, creds_split() is false, the toggle is hidden, and everything is
written to the single store in one call - byte-identical to before. New
config user_credentials_default_scope (default "local") seeds new toggles.
The write partition is a pure function with unit tests covering single-
store, blank-keeps-existing, scope routing, cross-scope moves, and the
default-scope fallback.
2026-07-29 17:39:54 +10:00
Dave Kempe b8c8f55d9a feat(vault): surface unavailable scopes with a greyed tree node
list_all_folders now returns (folders, unavailable_scopes); a scope whose
dedicated backend is down is reported instead of silently dropped, and
GET /api/addressbook includes unavailable_scopes. The Connections tree
renders a non-interactive greyed "<scope> connections unavailable" node
so a partial outage is visible while other scopes stay usable; the 15s
/api/me poll refreshes it.

Single-Vault is unaffected: every scope aliases the one backend, so
unavailable_scopes is always empty while it is up (full outage still
returns 503 and the existing banner). Adds a test asserting a lone
[vault] block is sufficient (quick-start invariant).
2026-07-29 17:04:05 +10:00