Follows #319. The option moves out of the Session panel header into the
Controls panel's new Keyboard section, beside View, and is remembered in
the browser like the View choice. The lock follows the setting whenever
fullscreen is entered, and switching it while fullscreen applies at once.
Browsers without the Keyboard Lock API show it disabled with a note.
When entering full screen mode and if the check box to lock the keyboard
has been checked, all keyboard events will be sent to the client. For
example when connecting to a Windows machine Win+R would open the Run
dialog on the remote machine, not on the local.
(cherry picked from commit 3cfcc5cdd3)
The RDP keyboard layout could only be set per connection, and VDI desktops
never got one, so a Swiss or German user on a desktop set up for their
layout typed with a US keymap: Y and Z swapped and no umlauts.
Settings > My keyboard layout on the Connections page is remembered in that
browser and sent with each connect as keyboard_layout. The server uses it
for an RDP entry left at the server default and for VDI desktops; a layout
set on the entry still wins, and a name guacd has no keymap for is dropped.
The entry editor also lists the nine guacd layouts it was missing.
A desktop wider than the window, such as a two-monitor VNC console viewed
from a laptop, was always scaled to fit and came out too small to read. The
Controls panel now has a View choice: Fit to window (the default) or Actual
size, with scroll bars, which draws it at 100% inside a scrolling container.
The choice is remembered per browser. The scroll bars are styled so they
stay visible where the platform would hide them until you scroll.
A VNC connection can choose how the server encodes its screen for
rustguac: Auto (default, unchanged), LAN (Ultra/ZRLE/Hextile first, low
compression, top JPEG quality), WAN (Tight/ZRLE, compression and JPEG
quality 6) or Slow link (Tight, compression 9, JPEG quality 3, 16-bit
colour unless the connection sets a colour depth). They set guacd's
encodings, compress-level and quality-level; Auto sends none of them.
An unknown preset is refused rather than ignored. Web sessions are
unchanged.
Docs: the VNC section of the overview now covers screen size, the
presets, and which VNC authentication methods work (VNC password,
VeNCrypt, macOS, UltraVNC MS-Logon II) and which don't (RSA-AES,
UltraVNC encryption plugins, MS-Logon III).
guacd asks a VNC server to resize its screen to the browser window by
default. A physical console (UltraVNC, RealVNC, x11vnc) can't, and
libvncclient then stops requesting updates while it waits for a resize
that never comes ("Skipping Update - resize in progress"), so the
session all but freezes. Seen with UltraVNC on a two-monitor console.
VNC entries and ad-hoc VNC now send disable-display-resize unless the
new vnc_resize option is on ("Resize the remote desktop to fit the
window" in the editor), and guacd scales the screen instead. Web
sessions, on our own Xvnc, still resize. The VNC argument mapping moves
into vnc_arg_value() so it can be tested.
In the editor's ad-hoc mode a poweruser was offered file transfer even
when [drive] has it off, and VDI CPU, memory and idle limits above the
site defaults, all of which apply_adhoc_policy drops. The auth status
now says whether the drive is enabled (not only configured), and the
editor hides file transfer from ad-hoc users below admin when it is off
and says the VDI limits are capped. Admins see everything as before.
Ad-hoc sessions had their own form on the Sessions page with a subset
of the options a connection entry has. "Connect to a host" now opens
the Connections entry editor in an ad-hoc mode instead, so an ad-hoc
connection has every option an entry has, now and as entries gain more.
The button sits beside the global search box on every page that has
one. It is shown to poweruser and above under the same rules as the
search box, and opens the editor in place on Connections or goes to
/connections.html#adhoc elsewhere. Ad-hoc mode has no name or folder,
Connect instead of Save, and for admins a "Save as entry" action.
POST /api/addressbook/adhoc/connect takes the entry as the editor
builds it. It resolves only the caller's own credential variables,
never lends folder defaults, and goes through apply_adhoc_policy and
the session rate limit like every other ad-hoc route.
POST /api/sessions is unchanged.
session_request_from_entry() replaces the two copies of the entry to
session request conversion, used by the Connections connect endpoint
and /api/connect, so an option added to entries works in all three.
can_connect_adhoc() is the one place that decides who may connect
ad-hoc.
The Sessions ad-hoc form is removed. loadFolders() now runs its
callback, so #open= links work again.
Refs sol1/rustguac#295
(cherry picked from commit dd40bf08ec)
A few examples used real-world names instead of reserved ones:
- the web session example pointed at www.saucedemo.com, a real
third-party site, with that site's demo login; it now uses
app.example.com and neutral credentials;
- the jump host examples used *.corp.local, and .local is the mDNS
domain, so copied settings resolve oddly; they now use
*.example.internal, as does the Squid proxy example;
- the move-entry response showed the folder "mrgelato"; now
"customer-a";
- the CLI audit log sample showed the user "dave"; now "alice", with
the columns kept aligned;
- the commented-out Knocknoc redirect in 403.http suggested
knocknoc.mycompany.com, a registered domain; now knocknoc.example.com.
Fixes#304
(cherry picked from commit b43de8678a)
A .guac recording only plays in rustguac's own player. The Recordings
page now has an mp4 button per recording: it starts a conversion,
shows progress, and downloads the video when ready. The file is a
standard MP4 that plays in VLC and browsers.
- POST /api/recordings/:name/mp4 starts it (202 running, 200 ready,
409 while the session is live, 503 without guacenc),
GET .../mp4/status reports it, GET .../mp4 downloads it. Same access
and name checks as the recording download (poweruser or admin).
- guacenc writes <input>.m4v beside its input, so each job links the
recording into a work directory inside the cache and moves the
finished file to <cache>/<stem>.mp4; the recording directory is never
written. guacenc exits 0 even on failure, so success is the output
existing and not being empty. Runs with a timeout, killed on timeout.
- At most mp4_max_jobs at once (default 1). The video is the
recording's own size (its largest layer 0 size), scaled down to fit
1920x1080 and rounded to even.
- Cached MP4s go after mp4_cache_hours (24), when their recording goes,
and all of them before rotation removes any recording for disk space.
Leftover work directories are cleared at startup and hourly.
- The listing carries each recording's mp4 state only when conversion
is available, so the button is hidden on a build without guacenc.
Config: mp4_enabled, guacenc_path, mp4_cache_path, mp4_cache_hours,
mp4_max_jobs, mp4_timeout_secs, mp4_bitrate, mp4_max_width/height under
[recording]. Docs in configuration.md (MP4 downloads, including
converting by hand) and api.md, which also had the recordings role as
operator; it is poweruser.
Tests: the converter (cache layout, failure leaves nothing, live and
missing refused, purge, size parsing and fitting) with a stand-in
guacenc, the shared access check, and a browser spec for the button.
Users are found by email, and with auto_create_users = false the email
becomes the gate. An email alone is not proof of identity, since some
providers let people set it, so a user is now bound to the OIDC subject
of their first login and a login with the same email but a different
subject is refused (and logged). A login the provider explicitly marks
email_verified: false is refused too.
New CLI-only command reset-oidc-subject releases the binding, for moving
to a different identity provider or a recreated provider account; it is
written to the CLI audit log.
Emails match ignoring case, so an admin typing JSmith@Example.com no
longer locks out a provider that sends jsmith@example.com, nor creates a
second account with auto-create on; a user cannot be added twice in
different case. add-user and POST /api/users check the email and name,
and create_user reports "already exists" as a type instead of matching
"UNIQUE" in the error text.
Tests for the login rules, and CLI integration tests for add-user and
reset-oidc-subject including their audit rows. Docs: how a login is
matched, moving providers, POST /api/users, the CLI commands, and the
login page message (a redirect, not a 403).
With `[oidc] auto_create_users = false`, OIDC login no longer creates
an account for a user who isn't in the database yet. Only users an
admin has added beforehand can log in. The option defaults to true, so
existing deployments behave as before.
An admin can add users ahead of their first login with any of:
- CLI: `rustguac add-user --email <email> --role <role> [--name <name>]`
- API: `POST /api/users` (admin only), returns 201, or 409 if the user exists
- Admin page: an "Add User" form under the users table
A user added this way keeps the role the admin gave them; default_role
is not applied. Group-to-role mappings still apply on every login.
A rejected SSO login now redirects to the login page with an error code
instead of returning JSON. This covers a user with no account
(sso_error=no_account) and a disabled account (sso_error=disabled).
SSO errors are now shown under the SSO button: they used to go into the
API key form, which is hidden when SSO is on, so "SSO login failed" was
never visible. The "provider unavailable" notice uses the same element,
and all SSO messages are defined in one place in index.html.
(cherry picked from commit 9bfdc52155)
In a fullscreen-on-connect session the client runs in a frame on the
Connections tab, and every other page refuses to load in a frame. Home in
the Ctrl+Alt+Shift panel, reconnect and the disconnected overlay only
moved the frame, so the browser showed "cannot open this page" in place
of the session. Every navigation now goes through rgGo(), which moves the
whole tab when embedded, and Close session hands back to the launcher.
tests/client-navigation.mjs fails if anything navigates around it.
The launcher tore the session down whenever the tab left fullscreen, so
holding Escape, F11 or a gesture from the OS ended it. Leaving fullscreen
now keeps the session at the tab's size and shows a Fullscreen button
that goes back; only Exit, or the session ending, closes it. Tested with
the browser's fullscreen events faked, since headless Chromium refuses
the real request.
A browser only lets the tab that holds the Connect gesture go fullscreen;
opening a new tab drops that gesture, so a fullscreen-on-connect entry
always fell back to a manual click. Connect now fullscreens the CURRENT
tab on the click — the one moment a valid gesture exists — and loads the
session in a full-viewport same-origin iframe (rgLaunchFullscreen in
search.js, shared by the connections page and the global search): overlay,
spinner and an Exit button, torn down on Exit, on fullscreen exit or on a
session-end message from the embedded client. The client in ?embedded=1
mode skips all in-client fullscreen logic (the parent tab owns it) and
posts ready/end to the parent. X-Frame-Options becomes SAMEORIGIN (was
DENY) with CSP frame-ancestors 'self', so a same-origin page can host the
client in a frame while cross-origin framing stays blocked.
The final viewport size cannot be predicted (a fullscreen viewport on a
notched Mac is shorter than the screen), so it is measured: the launcher
enters fullscreen, waits until the viewport has held one size for 150ms,
then sends the connect with that width and height; a refused request
connects at the tab's size at once and a 2.5s cap never leaves the user on
a spinner. The embedded client shows its connect cover from load and lifts
it — then runs the post-connect fit sends — only on the launcher's
'viewport' message, so the user sees only the final picture. The ⧉ (open
in a new window) button opens the client explicitly windowed (fs=0) on a
small splash page, static/launch.html, which measures its own viewport
once stable and posts it to the opener (rgOpenSessionWindow); the session
is created at that size, so nothing resizes on load. No screen_* is sent
for a popup. When the browser refuses the load-time request, the next
click or key enters fullscreen, taken in the capture phase and never sent
to the remote, and everything is released once fullscreen lands.
Fixes#257
tests: same-origin framing only, and the zero-click fullscreen launcher
The CSP test pins frame-ancestors to exactly 'self'. The browser spec drives
Connect on a fullscreen-on-connect entry: the launcher overlay opens in the
same tab, the connect request carries the measured viewport and no screen
size, the embedded client loads in the frame, and Exit tears it down.
(cherry picked from commit ea456b696f)
The cached index was keyed to the address-book version alone, which
starts again at 0 when the server restarts, so a tab could keep an index
from before the restart whose version happened to match. Key it to the
epoch as well (#270), tested. The connections page's live refresh now
calls rgSearchRecheck() when the book changes, so the index is checked
at once rather than at the next 30 s poll.
The '/' search box leaves the connections folder header for a bigger,
centred box in the brand row, inline with the logo. Results open in a dim,
blurred overlay over an untouched folder view: a centred panel with the
summary, the Type and Folder facets and the results table (fixed columns,
one line per cell, folder shown by display name over its key, Connect, new
window and open-folder gathered into one actions cell). Focusing the box or
pressing / opens it and lists every connection the user can see; typing
narrows it; Escape from anywhere, a mousedown outside the box and panel, or
opening a folder closes it (an open facet menu takes the first Escape).
static/search.js is the single implementation, used on every page
including connections, which dropped ~16k of duplicated markup, CSS and JS;
the styles live once in rustguac.css so the results look identical
everywhere. The index is cached in sessionStorage and only re-fetched when
the address book's version changes, so moving between pages is instant; the
connections page keeps the folder-specific hooks it needs (rgSelectFolder
selects a folder in place, rgSearchIndex feeds the credentials banner) and
opening a folder from another page navigates to connections with
class, and the module stands down cleanly on a page with no brand row.
Connections also gets a more generous content gutter, the folder action
buttons move down to the filter row beside Type, the folder title is inset
to align with the table, and the Connect button keeps its size and spins
in place instead of growing to 'Connecting...'.
Part of #254
tests: the global search overlay on every page
search: keep page modals above the brand-row search box
The global search lifts the brand row to z-index 300 so the box is not
dimmed by its own results overlay (200), but the page modals sat at 100, so
the search input showed through the ad-hoc session dialog and could still
be focused. Modal overlays now sit at 1000, above the search chrome and
below the themed dialogs (4000) and the embedded session (5000). A spec
pins it on the ad-hoc and folder modals.
search: never on the login page, nor for anyone the server does not recognise
The global search was included on index.html, so the login page carried
the search box, and because the index was cached in sessionStorage it
kept answering with the whole address book after logout, or to the next
login in the same tab.
- index.html no longer loads search.js at all.
- search.js only renders for an operator or above: the role the server
bakes on <html data-role-level> decides on first paint, and an API-key
user (served the anonymous variant) is confirmed through /api/me first.
Anonymous or viewer: nothing injected, cache swept.
- The cached index is keyed to the identity /api/me returns; a cache left
by a different login is dropped rather than shown.
- A 401/403 from the index or version endpoints removes the box, the
overlay and the cache; the logout link clears the cache before the
login page shows.
e2e: login page has no box and never loads search.js; anonymous and
viewer callers get nothing and a stale cache is wiped; a different
login's cache is dropped; the same login keeps its cache across pages;
logout and expiry forget it.
(cherry picked from commit b8c5c0f9ee)
static/ui.js provides promise-based rgConfirm/rgAlert that show a themed
modal matching the app, injected self-contained so it works on any page
including the standalone client. Every native alert()/confirm() is
replaced: delete user and revoke token (admin), revoke token (tokens),
delete recording (recordings), popup-blocked (client), and terminate
session, delete entry, delete folder and the SSH host-key probe
(connections). Destructive actions get a red confirm button, the host-key
trust prompt an accent one.
From an independent review of the module: rgConfirm/rgAlert resolve any
already-open dialog before replacing it, so a second call can't leave the
first promise hanging; Enter acts on the focused button (Cancel cancels),
and Tab is trapped between the two buttons.
Part of #254
tests: themed confirm/alert dialogs replace the native ones
(cherry picked from commit f8779d083b)
folders: optional display name in the folder config (#254)
A folder can carry a display_name beside its description. It is stored
in the Vault .config (omitted when empty, so existing configs are
unchanged), returned in folder listings and GET .../config, and accepted
by POST /api/addressbook/folders and PUT .../folders/:scope/:folder.
The NetBox tenant sync sets it to the tenant name, so the connections
page can show 'Dani & Fiori's Gourmet Pizzeria' over the folder key
instead of three spellings of the tenant in one line.
A folder can carry a display_name beside its description. It is stored in
the Vault .config (omitted when empty, so existing configs are unchanged),
returned in folder listings and GET .../config, accepted by POST
/api/addressbook/folders and PUT .../folders/:scope/:folder, and carried
in the search index as folder_display_name so results can show it the way
the folder view does. The NetBox tenant sync sets it to the tenant name.
On the connections page a folder with a display name shows it as the title
with the folder key as a sub-header, in the header and the sidebar, so
'Dani & Fiori's Gourmet Pizzeria' reads over the key instead of three
spellings of the tenant in one line. The folder modal gets a Display name
field, asked for before the description.
Part of #254
tests: folder display name is optional in storage and shown over the key
A legacy .config without display_name loads with an empty one and is
stored unchanged; a set name round-trips. The browser spec checks the header,
the sidebar and the folder modal field.
(cherry picked from commit 46ac163761)
reports: show entry names as host over a tenant subline
Session history and the most-used table showed "host (Tenant)" names
wrapped across three lines; they now read as the host over a quieter
tenant line, as on Connections and Recordings.
Part of #254
sessions, recordings: show the session type with the shared badge (#254)
The sessions page printed the type as plain text and the recordings page
as a coloured word; connections and reports already used .type-badge
from rustguac.css. All four now use the same badge. The Proxmox badge
colour moves into the shared stylesheet with the others.
Shared facet filters (static/filters.js) on the sessions, recordings,
reports and connections pages: a dropdown per facet (Type, Folder, User,
Owner, Status as each page has them), multi-select with counts that narrow
as other facets apply, All/None, a find box for long lists, a Clear filters
link, and selections kept per page in sessionStorage. They sit beside the
existing text search and combine with it; the buttons are compacted to the
height and padding of the other controls and the dropdown reserves room for
its scrollbar so it no longer sits over the counts.
The list pages share one toolbar: a borderless row with the search box, the
facets and the page action on the right (New ad-hoc session, Export CSV),
placeholders standardised to 'Search <noun> (fields…)'. Sessions gains a
search (user, host, owner, ID, type) and a User facet; Reports moves from
its own .filter-bar to the shared one. The redundant page titles go — the
nav already names and marks the active page — and section headings centre
their heading and action button on one line.
Connections: the folder header is sticky at the top of the entries pane, so
it stays under the nav and the active-sessions strip while a long table
scrolls. All four pages show the session type with the shared .type-badge
(the Proxmox colour moves into rustguac.css), reports and recordings show
"host (Tenant)" names as the host over a quieter tenant line, and the key
glyph before Credentials in the nav is sized to the text.
Part of #254
tests: shared facet filters and the list toolbar on every list page
(cherry picked from commit be866264bd)
The ad-hoc session form sat in a collapsible framed box above the session
list. It now opens as a modal from a button on the right of the Sessions
heading, dropping the page's framed form box inside the dialog, with the
primary button first and Cancel at the same height as Connect, matching the
other modals. Section headings centre their heading and action button on
one line.
Part of #254
tests: the ad-hoc session form opens as a modal
(cherry picked from commit 7048ca6860)
Three fixes on top of the live refresh:
- Folder names. GET /api/addressbook/version returned every changed
folder to any operator, so one granted only teamA saw paths in folders
they cannot open as soon as an admin or a webhook wrote there. For
callers below admin an entries row is now kept only for a folder they
can open directly (the check_folder_access rule), and tree rows lose
their path: a folder that came, went or changed access may be one they
cannot see. The page treats a path-less tree row like `all`. Folder
config is read only when there are rows to answer, so an empty poll
still costs no Vault read, and a burst of more than 32 rows is answered
with one path-less row instead of a check per folder.
- Restarts. The version counter starts again at 0, so a tab that saw v5
and polled after the new process reached v5 or more was answered from a
different log and could miss changes. The answer now carries a random
per-process epoch; a tab that sees it change refreshes what it shows.
The docs no longer claim a restart always answers null.
- Moves. A PUT ...?move=true that moved an entry logged only the target
folder, so a tab showing the source kept the moved entry. It now
reports both, and so does a move that copied the entry but could not
remove the original (502), for this path and POST .../move: the tracker
records a failed request when its handler reports what it changed.
connections: live refresh from a per-folder address-book change counter
The Connections page keeps itself current without reloading. rustguac
keeps an in-memory change log of the address book: every successful
write through its API (entries, folders, folder config, moves, copies,
bulk) is logged against the folders it touched under a version counter.
The page polls GET /api/addressbook/version?since=N every 3 seconds
while visible and gets back only what changed after the version it last
saw: one row per folder, saying whether its entries or the folder itself
moved. The poll is answered from memory; it reads nothing from Vault.
A tab acts only on what it shows. Entries changed in the open folder:
that folder is re-read. Entries changed elsewhere: the cached copy is
forgotten and nothing is read until the folder is opened. A folder
created, deleted or reconfigured: the folder list is re-read (no
entries), plus the open folder if it was in that subtree. The search
index is never rebuilt on a poll: it is marked stale and rebuilt the
next time the box is focused or typed in; while results are showing,
only the changed folder's rows are re-read and patched in. So one
change costs a tab at most one folder's entries plus one folder-list
listing, never the whole book, however many tabs are open. There is no
timed refresh. A null answer (first poll, restart, or a tab further
behind than the 512-entry log reaches) refreshes what is shown once,
under the same bound.
Folders are attributed from the request path; the four routes whose
folders only the body knows (create folder, move, copy, bulk) report
them from the handler through a response extension, and a handler that
reports nothing is logged as `all` so a page refreshes rather than
misses it. A user's own credential variables (/api/me/credentials) are
not address-book changes and never reach the log: the saving tab
reloads its own status, and no other tab needs to know.
Hidden tabs do not poll and catch up with an immediate check when
shown; a tab with a dialog open does not poll either, so the change
waits for the dialog instead of being missed.
Tests: the attribution table, the log's since/dedup/eviction/restart
answers, the middleware on a router (a credentials save logs nothing, a
failed write logs nothing, reported folders are logged precisely), and
a plain-node test pinning what the page re-reads for each change.
Closes#253 (part 3/3).
(cherry picked from commit da82239076)
Sizing. The entry connect request now carries the screen's size as well
as the window's (screen_width/screen_height, sent by every connect site
in connections.html and the client's relaunch). initial_session_size()
picks the size the session will be shown at: an RDP/VNC entry that opens
fullscreen on connect is created at the screen's size, everything else at
the window's, and every RDP size is rounded down to the 16px grid that
guacd's display channel (patches/007) rounds the first post-connect size
send to. Creation and that send therefore agree, guacd coalesces them,
and there is no resize, no H.264 decoder rebuild and no flash on
connect. The client's post-connect size send stays as it is.
Reuse. A repeat Connect on the same entry within 15 s no longer opens a
duplicate: a Pending session (guacd connected, owner never attached) this
user created for the entry is handed back instead. The match is made on
the fully built CreateSessionRequest and includes its credentials:
CreateSessionRequest::credential_fingerprint() is a SHA-256 over the
target and every credential field (username, password, private key,
domain, auth package, KDC, jump hosts and their credentials, container
and Proxmox credentials), stored on the Session and compared, never
logged or served. A retry with a different password, key or domain, such
as a mistyped prompt, gets a new session rather than the old Pending one
with the old credentials. Reuse is also scoped to the same user, the
same entry key, Pending status with a live guacd stream, and sessions
younger than SessionManager::REUSABLE_PENDING_SECS.
#238 (native/HiDPI resolution, display_scale) has not merged: both sizes
here are CSS pixels, matching today's post-connect send. When it lands
the mod-16 rounding must be applied to the device-pixel size, after the
devicePixelRatio multiply; the rounding site carries that note.
Tests: fullscreen_sizing_applies_to_bitmap_sessions_only and
round_down_mod16_floors_at_sixteen (api);
credential_fingerprint_tracks_target_and_credentials,
fresh_pending_session_reused_for_same_entry_and_credentials,
retry_with_different_credentials_gets_a_new_session and
reuse_is_scoped_to_user_entry_status_and_age (session).
Part 1/2 of #257.
(cherry picked from commit 4496f05e8a)
# Conflicts:
# src/api.rs
Folder access is a union, so a subfolder that inherits from its parent
but also grants groups or users of its own admits people the parent
doesn't. The defaults walk only stopped at inherit_from_parent: false,
so a contractor added to corp/partner through allowed_users could
connect with corp's default key. A parent's defaults now flow only into
a subfolder that inherits and grants nobody of its own
(FolderConfig::shares_parent_audience), tested.
Also:
- an entry set to prompt is no longer listed as using folder credentials
- PUT .../defaults on a folder that doesn't exist is a 404, rather than
writing a .defaults that makes an admin-only phantom folder
- the folder dialog takes a domain for RDP defaults
- docs: the new boundary, and that a $variable in a default resolves per
user
A folder can lend a username, password, private key and domain per session
type to entries that have no password or private key of their own, so an
entry created without secrets (a NetBox webhook, a bulk import) connects the
moment it exists. The defaults live at `<folder>/.defaults` beside
`.config`, are reserved like it (never listed, not a valid entry name) and
are removed with the folder. `GET/PUT/DELETE .../defaults` manage them
(secrets write-only, audited as update_folder_defaults /
delete_folder_defaults) and the folder dialog gains a Default credentials
section. Listings report such entries as `has_credentials: true` with
`inherited_credentials: true`. An entry's own credentials always win and an
entry set to prompt still prompts.
A subfolder without its own `.defaults` takes its nearest ancestor's, per
type, but the walk follows the access rule: it only continues past a folder
whose `.config` has `inherit_from_parent: true`. That flag is how a folder
is isolated for access, so a key set on a parent must not reach a subfolder
shared with someone else through `allowed_users`. The isolated folder's own
`.defaults` still apply to its entries. A folder with no `.config`, or one
written before the flag existed, counts as not inheriting, exactly as
`resolve_folder_access` treats it. The walk is `resolve_effective_defaults`
over a small `DefaultsSource` trait so it is tested without a Vault.
Both connect paths resolve defaults through one helper,
`lend_folder_defaults`: the Connections endpoint
(`POST .../entries/:entry/connect`) and `/api/connect` quick links, so an
entry that works from the page works from a link. `rustguac vault-migrate`
copies each folder's `.defaults` alongside its `.config`, so a DR split keeps
them (the dry run lists them as `[defaults]`).
Docs cover the storage layout, the inheritance rule and its boundary, the
migrate behaviour, and the warning that a web entry using a default
password has that password typed into a page the user can read.
Part 2/3 of #253.
(cherry picked from commit cc01469b07)
The served page now depends on the session cookie, so say so with
Vary: Cookie and no cache hands one role's copy to another. The page
routes don't need the WebSocket ticket store: without it optional_auth
skips the ?ticket= path, so a page URL can't use up a single-use ticket.
The .entry-name styles now live in rustguac.css, so drop the copy in
connections.html.
pages: bake the caller's role into the served page, version the logo
The header flashed on every navigation: gated links (Sessions, Reports,
Tokens, Admin) were hidden in the markup and shown after /api/me
answered, the logo was revalidated on each page load, and the nav
differed between pages, so switching tabs reflowed the header.
Serve each page in five variants, one per role level, that differ only
in a data-role-level attribute on <html>; optional auth now runs on the
page routes so a logged-in request gets its variant with the navigation
already right on first paint. The ETag is per variant. The logo joins
scripts and stylesheets in content-addressed, immutable caching.
Part of #254
pages: one navigation for every page, gated by role in CSS
Every page carries the same eight links in the same order, including
the Credentials entry (which opens the dialog on the connections page
and deep-links to it from elsewhere), with data-min-role on the gated
ones instead of inline display:none. rustguac.css shows them from the
data-role-level the server writes on <html>; the pages set that
attribute themselves from /api/me for API-key users, whose page GET
carries no credential. Admin-only controls follow the same gate, so
nothing pops in after the first paint. Recordings shows "host (Tenant)"
entry names as the host over a quieter tenant line.
Part of #254
tests: one navigation on every page, gated by data-role-level
(cherry picked from commit 5eed92c6ec)
entryNameHtml() splits the name into .entry-name and .entry-sub spans,
but nothing styled them, so the two ran together on one line
("web01.acme.internalAcme Holdings"). Show the tenant on its own quieter
line, and test it.
The sticky sidebar was sized from the viewport rather than from where
it starts under the header, so at the top of the page its bottom hung
off screen and the page had to be scrolled before the list could be
scrolled to its end. The page is now an app shell: the header stays,
and the sidebar and the entries area are two independent scroll panes
that always fit the viewport. Selecting a folder scrolls the entries
pane, not the page.
The sidebar is a bordered panel: one pinned header band on a surface
tone with the Folders label, a compact new-folder button and the filter
box, then the list with hairline dividers, 46px rows and one 12px edge
for every band. "host (Tenant)" names read as the host over a quieter
tenant line in the entries and search tables, and a role-gate
reconciliation covers API-key users.
Part of #254
connections: scroll only the entries pane on selection, clip the body
jumpToEntriesHeader() used scrollIntoView(), which scrolls every
scrollable ancestor, and a body with overflow hidden can still be
scrolled by script: selecting a folder pushed the brand row and the nav
off the top of the page. Scroll the entries pane to its top instead, and
make the body overflow: clip, which no script can scroll. The body's
border box now spans the full height so the page padding stays inside
the viewport.
Part of #254
tests: app shell with two independent scroll panes and a clipped body (#254)
(cherry picked from commit d7ad9e93e9)
The sidebar lists one folder per customer and gets long. Add a live
filter box (name, path, description; loaded subfolders included, with
ancestors of a match kept visible) mounted in a new #sidebar-tools div,
persisted in sessionStorage so live refreshes keep it. Make the tree
keyboard-navigable (arrows, Home/End, Enter/Space, Left/Right to
collapse/expand) with role=tree/treeitem, aria-selected/expanded and a
roving tabindex, and add an 'f' shortcut that mirrors the '/' one.
Implemented by post-processing the DOM renderFolders() builds and by
wrapping that function; its body is unchanged.
(cherry picked from commit 267ca7092d)
With a long folder list, clicking a folder appeared to do nothing: the
entries table was off screen, the only loading state was a text swap,
and nothing drew the eye to the changed title. loadEntries() also
called renderFolders() just to update one count badge, rebuilding the
whole sidebar on every click.
- Jump to the entries header when the shown folder changes (smooth,
'auto' under prefers-reduced-motion); never on initial load, live
refresh, forceRefresh after a mutation or search clear.
- .changed on #entries-header replays a 350ms title slide-in and an
accent underline flash on the action bar; cleared on animationend.
- Skeleton table (4 shimmering rows, same geometry as the real table)
while entries are fetched from the server; the cached path renders
immediately with no skeleton.
- updateFolderCount() swaps the number in the existing .folder-count
badge in place; renderFolders() only as a fallback when the badge
does not exist yet.
- renderFolders() is wrapped (not edited) to preserve the sidebar
scroll position across rebuilds and to reveal the selected row within
the sidebar (nearest, no page scroll) when the selection changes or
after loadFolders() rebuilds the tree.
(cherry picked from commit 6fd18394a7)
With 20+ customer folders the sidebar shared the page scroll with the
entries table, so picking a folder far down the list changed content off
screen, and every leaf row was pushed ~90px in by a hidden chevron
placeholder while names were ellipsised.
- .sidebar is position: sticky with its own thin scrollbar and
max-height tied to the viewport; .main keeps scrolling with the page.
- The FOLDERS header is wrapped in .sidebar-pin together with a new
empty #sidebar-tools slot, both pinned at the top of the sidebar.
- buildFolderNode() renders a .tree-chevron only for folders with
children; leaves get no placeholder (removed the .tree-chevron.leaf
rule), so rows start right after the 3px accent bar.
- Name, entry-count pill (.folder-count, now just the number with a
data-count attribute and "N entries" title) and a smaller scope glyph
share one line; description sits beneath at --fz-xs; rows are ~40px.
- Nested .tree-children indent 14px behind the existing dashed guide.
All new CSS is in one block at the end of the page's <style>.
(cherry picked from commit 198226c4b7)
The 1710px spec from the fork passed against main as well as against the
change it was meant to pin: its fixture names are short enough to fit
either way under Linux's monospace font, and the overrun on the reporter's
Mac came from wider glyphs and longer real names. With realistic data
(dotted FQDNs, service accounts, long emails and folder paths, which have
no natural break point) main overran by 370px, and the tighter padding on
its own still overran Connections and Recordings.
Table cells on Connections, Recordings and Reports now wrap long text
inside the cell (overflow-wrap: anywhere), with the Connect and action
buttons kept on one line. The spec gains a long-data test per page: all
three fail on main, two fail with the padding change alone, all pass now.
The e2e README lists only the specs that exist.
The 1800px breakpoint set a tighter page gutter on the connections page
only, so switching to Sessions or any other tab snapped the content
sideways. Move it to rustguac.css so every page moves together.
Part of #254
(cherry picked from commit b9db3ca2d2)
The 18px tables with 20px cell padding ran past the right edge on a
1710px MacBook display, the recording link cut off. Same treatment as
the connections table: 16px cells with tighter padding, 14px headers,
actions kept on one line. Nothing is hidden.
Part of #254
(cherry picked from commit 1b93b0c8c1)
The 18px table with 20px cell padding and a separate cell each for
edit, clone and delete overran the main column by ~300px on a 1710px
MacBook display, cutting "delete" off the right edge.
Put the three admin actions in one cell, and add a max-width: 1800px
breakpoint on the connections page: 20px page padding, 240-280px
sidebar, 16px cells with tighter padding, 14px headers, a 110px
Connect button and a smaller folder title. The icon-only "open in new
window" button keeps its compact size. Nothing is hidden; at 1710px the
table fits with about 250px to spare.
Fixes#252
(cherry picked from commit e7358ab9c9)
The comments on the #251 fit said Windows RDP without GFX rounds the
width down. It is our own guacd: patches/007-rdp-disp-mod16 rounds both
dimensions to a multiple of 16, with or without GFX, to stop H.264's
padding showing as a green band along the edge. Say so where the margin
is handled, and that the rounding must not be relaxed to make the margin
go away, since that brings the band back.
With fullscreen-on-connect, the first mousedown after CONNECTED is the
browser's fullscreen gesture, but Guacamole.Mouse forwarded it to guacd
as well, and the transition that followed swallowed its release. guacd
was left with the button held and the terminal selected text on every
pointer move; the same with a key press as the gesture.
Stop the gesture event before the display's handlers see it, and on
entering fullscreen release everything explicitly: keyboard.reset() and
a mouse state with no buttons at the last pointer position, so a release
lost for any other reason during the transition cannot stick either.
Fixes#256
(cherry picked from commit ee03b3e65c)
Stretching the display to fill a mod-16 shortfall costs sharpness when the
picture is already drawn one framebuffer pixel per physical pixel, and
nearly every RDP session hits the rounding. So the fill now applies only
when the picture is already being resampled (scale x devicePixelRatio != 1),
where it costs nothing; a pixel-exact session is never stretched, and the
framebuffer is centred in the available area so the small margin is split
evenly in the dark background instead of showing as a bare strip down one
edge. offsetLeft includes the centring margins and ignores the fill
transform, so the pointer maths that divides out fillX/fillY is unchanged.
Fixes#251
(cherry picked from commit d562946d0b)
Windows RDP without GFX rounds the requested width down to a multiple
of 16, so a 1710-wide window gets a 1696-wide desktop. The uniform fit
in scaleDisplay() then keeps the scale at 1 (the height matches) and
the missing 14px shows as a strip of bare page down the right edge.
After the uniform fit, stretch the display element by the remaining
fraction on each axis when it is at most 2.5%, and divide the same
factors out of pointer coordinates before sendMouseState. A larger gap
is a real aspect mismatch and keeps the letterbox. Multi-monitor
pop-outs reset the fill.
Fixes#251
(cherry picked from commit ebfab63ea7)
VeNCrypt, RealVNC and macOS Screen Sharing need a username as well as a
password. Pass it to guacd's VNC `username` arg and expose it in the
connection editor, the connect-time credentials prompt and ad-hoc
sessions.
(cherry picked from commit fac8d0aec5)
Folder access was by OIDC group only, so giving one person one VM meant
a group per person. Folders now also take allowed_users, a list of
login emails, next to allowed_groups. A user can open a folder if a
group matches or their email is listed (ignoring case). They need not
have logged in yet: access is decided from their identity when they
use rustguac. The list lives in the folder's config in Vault, so it
applies on every instance sharing that Vault.
Every folder access decision goes through resolve_folder_access, which
now takes the user (email and groups) rather than just groups, so the
compiler enforced the change at every call site: the tree, listings,
search, credential variables and connect.
The tree now also shows a folder the user cannot open when it leads to
one they can, at the top level and in subfolder listings, as the code
already intended one level down. Without that, a grant on users/jsmith
was unreachable from the tree when users itself was admin-only (this
also applied to group grants). Such a folder is listed without entries,
and opening it says it has nothing shared with you directly.
The allowed_users list is only returned to admins (folder config);
listings that operators read leave it out, so a user does not learn who
else has access. Saving a folder without the field keeps the stored
list, so older clients cannot wipe it. Entries are trimmed, repeats
dropped, and anything with spaces or control characters refused, up to
500 per folder. The folder dialog gains an "Allowed users" box, and the
bulk move dialog's access warning compares users as well as groups.
Docs: folder access rewritten, including the correction that a folder
with no allowed groups is admin-only (the docs said it was open to
every authenticated user), and the API reference.
Requested by @3zAlb.
Re-filing more than a handful of entries meant opening each one. Admins
now get a checkbox column on the entries table (select all, shift-click
for ranges) and an action bar with Move to, Copy to and Delete. One
dialog handles all three: a filterable list of every folder, a preview
naming any entries that already exist in the target, a choice to leave
those or give them a new name, warnings when the move crosses between
shared and instance or the folders have different access groups, and a
result per entry afterwards.
POST /api/addressbook/bulk does the work, up to 1000 entries a request,
8 at a time. Each entry goes through the same stored-entry copy as the
single move: credentials included, Vault check-and-set so nothing is
overwritten, and a move removes the original only after the copy is
written. Clashes are skipped or renamed (name-2 .. name-99, within the
64-character limit); there is no overwrite option. Repeated items are
acted on once. Each entry is audited on its own, headline only. The
route alone accepts a 256KB body, since 1000 entries is about 100KB;
it is admin-only and authentication happens before the body is read.
GET /api/addressbook/folder-paths lists every folder for the picker; the
existing "Move to folder" list only knew about folders already expanded
in the tree.
Delete now checks the entry exists first, in both the bulk and single
endpoints. Vault reports success for deleting a path that is not there,
so a missing entry was answered, and audited, as deleted.
Moving an entry to another folder was done in the browser as "create
a copy from the edit form, then delete the original". The page never
receives an entry's secrets, so the copy had no password, private key,
jump host credentials, Proxmox token or container password, and a
$variable reference stored in the password field was lost the same way.
The original was then deleted permanently. Clone had the same flaw.
(#232)
Two admin-only endpoints now do it from the stored entry:
- POST .../entries/{entry}/move copies the full entry to the target and
only then removes the original. If that removal fails it says the
entry was copied, rather than reporting a plain failure.
- POST .../entries/{entry}/copy copies it under a new name. Clone uses
it and then applies the form's edits as an ordinary update, which
keeps stored secrets it is not given.
The target is written with Vault check-and-set (cas 0), so an existing
entry is never overwritten, even by a concurrent writer. Scope, folder
and names go through the existing Vault validation (only shared and
instance scopes, no traversal, .config reserved, so a copy cannot
replace a folder's access settings), and those failures are 400s. Both
actions are audited, headline only. A note on the shared helper records
that any future per-folder editing (#231) must require rights on both
the source and the target folder.
A busy main thread does not delay pointer movement, it discards it: moves
arriving while a handler runs are merged into one, and what is dispatched
afterwards carries only the last position. A drag feels that immediately --
the path between two points is gone, so a dragged scrollbar thumb is dropped
rather than merely lagged -- while a hover or a video degrades gracefully
under the same load.
getCoalescedEvents() returns what was merged. Replaying it in order restores
the path, though not the timing: nothing running on a blocked thread can do
that, and the positions still arrive together once the thread is free. What
changes is that the remote end sees where the pointer went rather than only
where it ended up.
Deliberately additive. The mousemove listener is untouched and still delivers
every position it did before, so touch, the compatibility mouse events it
produces and the counter that swallows them all behave as they did; the
dispatched position arrives twice and the second is dropped by the
unchanged-coordinates check already in move(). Migrating this file to pointer
events outright is the tidier shape and a far larger change -- every listener
would have to move together, or synthetic events from touch would arrive with
nothing left counting them down.
Only while a button is held, because the path matters for a drag and the
endpoint is all that matters for a hover, and replaying every merged move of
an idle pointer would multiply a session's upstream mouse traffic for movement
nobody can perceive. Capped and evenly sampled, because a pointer reporting at
1000Hz behind a thread blocked for 100ms offers a hundred positions for one
drag, and answering a rendering problem with a traffic one is no answer. An
ordinary pointer never reaches the cap.
The pop-out monitor windows get the same replay. They bypass Guacamole.Mouse
entirely -- it does not track the X axis correctly in a popup -- and map the
pointer themselves, so a drag on a second head would otherwise lose its path
exactly as the first one did. The sampler moves to a static so there is one
definition of the rule rather than two that can drift, and that path needs one
thing the main display does not: it sends unconditionally, where move() drops
an unchanged position, so it keeps what it last sent and drops a repeat --
keyed on the buttons as well as the position, since a press and a release
happen at one place and are not repeats of each other. Feature-detected on the
event rather than on PointerEvent, because a pop-out window is a realm of its
own and the constructor this page can see is not the one that made the event
it is handed.
That pointer handling lives in Guacamole.MonitorPointer rather than inline in
client.html, because these windows cannot be opened without a live
multi-monitor session: while it sat in the page the only thing a test could do
was match the source text and hope, and the two things most worth checking
there are a coordinate mapping and a suppression rule -- exactly the kind that
look right in a diff and are off by a term. It takes the canvas, a function
returning this monitor's rectangle in the combined framebuffer (a call rather
than a captured value, since the layout changes under a running window) and a
function to send a state.
Writing those tests found a real defect in the wheel, which sends a momentary
press and release: it built one state object, sent it, mutated it and sent it
again, which works only while every consumer copies synchronously. It sends
two states now.
Tested rather than tried, because this path only runs once the browser has
already thrown something away: the case it exists for is the one that cannot
be reliably produced, and a broken version feels like the behaviour it
replaced. Twenty tests cover the mapping, the button mask, the repeat
suppression, the wheel's reset of it and the replay, plus a harness driving
real pointer input through the debugger with the main thread deliberately
blocked -- because the unit tests feed events shaped the way the code expects,
and that assumption is the one worth checking against a browser.
(cherry picked from commit d8ce743fe0801bd8c776281ee5541abf8040a339)
A client with no hardware H.264 decoder lost H.264 altogether, and lost it in
the worst available way. Chrome reports a configuration carrying
hardwareAcceleration 'prefer-hardware' as unsupported outright rather than
falling back, so configure() failed asynchronously, the decoder closed, every
frame was then held waiting for a keyframe that cured nothing -- and since
guacd suppresses ordinary image operations for a layer carrying H.264, what
the user saw was a permanently black screen rather than a degraded picture.
Measured on Chrome 153 against an Intel UHD 770 with no VA-API decode
available: isConfigSupported() answers supported for avc1.4d4020 with no hint
and with 'prefer-software', and not supported with 'prefer-hardware'. The
same box then decoded the stream perfectly once the hint was dropped.
So ask for hardware once and, if that is refused, build again without asking.
Dropping the hint unconditionally is the smaller change and the wrong one: it
hands the choice to the browser on every client, including the ones this
whole path exists for, where a software decode costs far more than it saves.
It says so when it happens. A session quietly decoding H.264 in software is
worth knowing about -- it is the difference between the cost this feature was
built to avoid and the cost it was built around -- so the fallback reports
decoder_software_fallback rather than only appearing as a slow session.
Tested, because the hint reads like a preference and restoring it would look
harmless. The test drives the shape Chrome actually presents -- configure()
accepted, the refusal arriving later through the error callback -- and checks
that hardware is asked for first, asked for only once, that no frame is
stranded by the refusal, and that the fallback is audible.
Reachable on a VM, a machine whose driver is blocklisted, or a browser with
acceleration switched off, none of which are exotic.
(cherry picked from commit 59fc416e80daab20ac07bdac5dbebb6ff7eddf75)
Upstream: ported by hand, as the fork's decoder has diverged. The
fallback reports through console.warn only, since the fork's diagnostic()
channel is not carried here. tests/h264-hardware-fallback.mjs passes
unchanged (node tests/h264-hardware-fallback.mjs).