Skip to content

entries: one entry, several protocols - #299

Open
jseifeddine wants to merge 4 commits into
sol1:mainfrom
jseifeddine:feat/multi-protocol-entries
Open

jseifeddine wants to merge 4 commits into
sol1:mainfrom
jseifeddine:feat/multi-protocol-entries

Conversation

@jseifeddine

@jseifeddine jseifeddine commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Closes #296.

One connection entry can now offer several protocols for the same machine (RDP, SSH, VNC, SPICE, Proxmox, Web, VDI), instead of one entry per protocol. This follows the model from the discussion on #296: every protocol lives in protocols, and the host level holds the defaults they inherit. Rebased on v1.13.4.

Entry- and protocol-level access rules were split out into draft #320, to be settled with #294.

Data model

{
  "hostname": "10.0.0.10",
  "display_name": "server01.example.com",
  "username": "Administrator",
  "protocols": [
    { "type": "rdp", "port": 3389 },
    { "type": "ssh", "port": 22, "username": "admin" },
    { "type": "ssh", "name": "ssh-2222", "port": 2222 },
    { "type": "web", "url": "https://10.0.0.10/", "hostname": "" }
  ]
}
  • Host level = defaults. A protocol inherits every host field it doesn't set. It overrides by setting the field, and clears an inherited value with "" or []. port is never inherited, so each protocol gets its own or its type's standard port.
  • The first protocol is the primary. Connect and links without a protocol use it.
  • name is optional, defaulting to the type, and must be unique within the entry. It tells two protocols of the same type apart. Links select a protocol by it, and stored secrets are matched on it. To rename a protocol through the API, send its old name as renamed_from and its secrets move with it.
  • Any entry field can be set per protocol. That covers the open questions in the issue: hostname, the recording settings and fullscreen can each be set on the host and overridden per protocol.

Credentials

Resolved per protocol: the protocol's own, then the host's, then the folder .defaults for that protocol's type, then $credential variables. Connecting resolves host and protocol to one flat single-type entry, so the existing connect path (folder defaults, variables, recording, the session request) is reused unchanged.

Existing installations

  • Flat entries keep working unchanged. An entry with a top-level type and no protocols is read as one protocol and connects exactly as before; tests compare the resolved entry field by field with the stored one. Nothing in Vault is rewritten on upgrade.
  • Compact storage. An entry is stored in the flat form whenever that loses nothing. It moves to the new form only when it needs to: more than one protocol, a protocol name, or a host-level default. So older instances sharing a Vault keep reading single-protocol entries. An older instance cannot read an entry in the new form: it leaves it out of its lists and refuses an update to it (an edit, or the webhook's PUT) rather than overwrite it. Creating a new entry with the same name there would replace it. docs/migration.md says to upgrade every instance before using multi-protocol entries.
  • The write API still accepts the flat body, including the current NetBox webhook's. Tests send exactly the documented webhook body through PUT ...?move=true and check that it upserts, moves, keeps stored secrets and connects as before.
  • Existing links (/api/connect?scope&folder&entry) open the primary. Old &protocol=<type> links still work, because a protocol is matched by name and then by type.

API

  • POST .../entries/:entry/connect and /api/connect take an optional protocol.
  • Entry listings gain protocols. The existing top-level fields (session_type, port, ...) still describe the primary, so existing clients keep working.
  • Updates keep stored secrets per protocol, matched by name, or by renamed_from for a rename.
  • POST /api/addressbook/adhoc/connect refuses a protocols list: an ad-hoc connection is one protocol.
  • Sessions and recordings record the protocol they used, shown on Sessions, Recordings and the Active Sessions card.

UI

  • Editor: a Host section (hostname, username, password), then a tab per protocol. Inherited values show as placeholders, with a "don't inherit" option for hostname, username and password. Also: add and remove protocols, and make primary. In ad-hoc mode (Connect to a host) the Host section and tabs are hidden, since an ad-hoc connection is one protocol.
  • List: a badge for every protocol, the primary first and outlined, wrapping onto as many lines as needed (no +N chip, per the review on Proposal: allow one connection entry to offer multiple protocols (SSH / RDP / VNC / Web) #296). The Type filter matches any protocol.
  • Connect: a split button. ▾ is part of Connect and lists every protocol, the primary marked default, each with its port (Proxmox shows the VM, VDI the image). It is keyboard accessible (Enter, Space, arrows, Home, End, Esc) and stays inside the viewport. Single-protocol entries keep a plain Connect.

NetBox

docs/netbox.md keeps the single-protocol webhook documented and supported. It adds a multi-protocol body template, rendered and checked under Jinja2's sandbox:

  • remote_protocol / remote_port keep their meaning as the primary protocol and its port;
  • optional remote_protocols (multiple selection) and remote_ports (JSON, only for non-standard ports) add the rest;
  • per-protocol Custom Links (&protocol=) can share a Custom Link group, so they show as one Console dropdown.

A sync source that drives protocols some other way (for example from device tags) only needs to send the protocols array.

Known limits

  • A single-protocol body replaces the list. A flat update, such as the original NetBox webhook, replaces an entry's protocols with that one protocol, keeping its secret. A sync that should keep several protocols has to send them all.
  • Names are fixed once saved in the editor. An existing protocol's name and type can't be changed there, because secrets are matched by name. The API can rename with renamed_from.
  • Phone width. The Connections page has no phone layout yet, the same as on main (ui: a small-screen layout, so the pages are usable on a phone #314 adds one).

With #298

#298 is merged, and this is rebased on top of it: the selected protocol goes through its shared session_request_from_entry() conversion, the ad-hoc endpoint refuses a protocols list, and the ad-hoc editor hides the Host section and protocol tabs.

Testing

  • cargo fmt --check and cargo clippy --all-targets -- -D warnings: clean.
  • cargo test: 453 passed in the main binary, plus 33 in the other suites. The two vault::tests::test_build_client_mtls_* tests fail in my container with UnsupportedCertVersion, the same as on unchanged main.
  • Playwright, full suite: 92/92, including main's ad-hoc specs. multi-protocol-entries.spec.mjs covers:
    • old and new forms;
    • the editor;
    • all eight badges showing, with an 8-protocol fixture at 1710px, 1180px and 390px;
    • the split button's geometry, the full menu with the default badge, the keyboard flow, and the menu near the bottom edge.
  • Unit tests in src/entry_protocols.rs and src/api.rs cover:
    • flat-entry equivalence for every type;
    • per-protocol secret merge, including renamed_from;
    • the old NetBox body;
    • protocol selection by name and by type;
    • the ad-hoc refusal.

@davekempe

Copy link
Copy Markdown
Member

Thanks Jad, this is solid work and it answers all our questions from #296: credentials falling back protocol → host → folder defaults, $variables and prompting per protocol, protocol on quick-connect links, and the protocol shown in sessions, recordings and search.

We're going to park it for now though, for two reasons:

Two things we found, for when we pick it up:

  • An entry created with an access rule is stored in the flat form, so an older instance sharing the Vault ignores the rule and shows the entry to the whole folder. It needs converting to the list form before compact() in prepare_entry_body (and the ?move=true create path).
  • Renaming a protocol through the API drops its stored secrets (the merge matches by name). The editor doesn't allow it, so it's API-only, but worth a note or a renamed_from.

If you'd like to keep it moving, splitting the access rules into their own PR would make the rest easier to take on its own, and it removes the first issue.

A machine reached over more than one protocol needed one entry per
protocol, with the hostname, display name and credentials
copied between them. An entry can now hold a list of protocols.

The entry's own fields are the host's defaults. Each item in
`protocols` inherits them and sets only what differs; an empty value
clears an inherited one, and `port` is never inherited. The first
protocol is the primary. An optional `name` tells two protocols of the
same type apart.

- Existing flat entries are read as one protocol and connect exactly as
  before. An entry is stored in the flat form whenever that loses
  nothing, so older instances keep reading it, and the write API still
  accepts the flat body, including the NetBox webhook's.
- Connecting resolves host and protocol to one flat entry, so folder
  defaults, credential variables, recording and the rest apply per
  protocol unchanged. Credentials come from the protocol, then the
  host, then the folder's defaults for that type.
- The connect endpoint and `/api/connect` take an optional `protocol`
  (a name, or a type), defaulting to the primary. Sessions and
  recordings record the protocol they used.
- Updates keep stored secrets per protocol, matched by name.
- Connections: the editor has a Host section and a tab per protocol.
  The list shows a badge per protocol, the primary first, folding the
  rest into a +N chip. Connect is a split button whose menu lists every
  protocol, the primary marked as default.
- Docs: entries in api.md, a multi-protocol NetBox webhook in
  netbox.md, and an upgrade note in migration.md.

Refs sol1#296
The Type cell of an entry with several protocols kept to two lines and
hid the rest behind a +N chip, so seeing them all took a hover or the
Connect menu. It now shows a badge for every protocol, the primary
first, and wraps onto as many lines as it needs.

Refs sol1#296
An update keeps each protocol's stored secrets by matching names, so a
protocol renamed through the API lost its password or key. The editor
does not allow renaming, but an API client or script can.

A protocol in an update body can now name its old name in
`renamed_from`, and its stored secrets move with it to the new name.
It is used for that update only and never stored or returned; a
malformed one is refused like a malformed name.

Refs sol1#296
@jseifeddine
jseifeddine force-pushed the feat/multi-protocol-entries branch from d7a6c85 to f6b6f8c Compare October 21, 2026 23:58
An ad-hoc connection is one protocol to one host. A body with a
`protocols` list failed with "unknown session type", since it has no
top-level type; it now says what is wrong.

Refs sol1#296
@jseifeddine

Copy link
Copy Markdown
Contributor Author

Thanks for the review, Dave. I've taken your suggestion and updated the PR (description updated to match).

Access rules split out. The entry- and protocol-level allowed_groups / allowed_users are now in draft #320, stacked on this PR and marked as waiting on #294. This PR no longer adds an access layer: folder access is unchanged, and every user who can open a folder sees all of an entry's protocols.

Your two findings:

Other changes since your review:

Upgrade order across instances sharing a Vault: with the rules removed, an older instance can't expose anything it shouldn't. It can't read an entry in the new form, so it leaves it out of its lists and refuses an update to it (an edit, or the webhook's PUT) rather than overwrite it. The risk left is that the entry is missing on that instance until it's upgraded, plus one edge case: creating a new entry with the same name there would replace it. docs/migration.md covers this and says to upgrade every instance first. I considered a config switch to refuse new-form writes until it's turned on. I left it out because it can't stop older instances, and every single-instance deployment would have to opt in. Happy to add it if you'd prefer the gate.

Tests: clippy is clean, cargo test passes apart from the two mTLS tests that also fail on main in my container, and Playwright passes 92/92.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: allow one connection entry to offer multiple protocols (SSH / RDP / VNC / Web)

2 participants