You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@guacamole.apache.org by "ciroiriarte (via GitHub)" <gi...@apache.org> on 2026/07/19 03:50:40 UTC
Re: [PR] GUACAMOLE-2137 : OpenBao/Hashicorp Vault extension [guacamole-client]
ciroiriarte commented on PR #1216:
URL: https://github.com/apache/guacamole-client/pull/1216#issuecomment-5014111455
I've been going through this branch with a view to helping it land, and built the
`TokenFilter` test coverage that a core token-grammar change is going to need either
way. Posting what it found, plus a few other things worth a look.
## The token pattern change alters existing behavior
I wrote characterization tests for the current parameter token grammar — they pass on
`main` and pin down existing behavior rather than asserting anything new.
```
main (0b65b78) Tests run: 11, Failures: 0, Errors: 0 BUILD SUCCESS
this PR (d3b3b5e) Tests run: 11, Failures: 1, Errors: 2 BUILD FAILURE
TokenFilterGrammarTest.testUnrecognizedModifierYieldsRawValue:74
expected:<[Value-of-C]> but was:<[${TOKEN_C:BOGUS}]>
TokenFilterGrammarTest.testUnrecognizedModifierResolvesUnderStrictMode:89
» GuacamoleTokenUndefinedException
TokenFilterGrammarTest.testNonTokenTextIsNotTreatedAsToken:113
» GuacamoleTokenUndefinedException
```
Those three methods cover six distinct inputs (JUnit reports the first failing
assertion per method):
| Input | `main` | this branch |
| --- | --- | --- |
| `${TOKEN:BOGUS}` | `Value-of-C` | `${TOKEN:BOGUS}` (literal) |
| `${TOKEN:lower}` | `Value-of-C` | `${TOKEN:lower}` (literal) |
| `${TOKEN:BOGUS}` (strict) | `Value-of-C` | throws `GuacamoleTokenUndefinedException` |
| `${NOT A TOKEN}` (strict) | literal | throws `GuacamoleTokenUndefinedException` |
| `${a/b/c}` (strict) | literal | throws `GuacamoleTokenUndefinedException` |
| `${some.value}` (strict) | literal | throws `GuacamoleTokenUndefinedException` |
Two distinct causes:
**Unrecognized modifiers.** On `main` the modifier group is `(\:(.*))?` and the `switch`
in `filter()` falls through to `default:`, appending the token value unchanged. The new
pattern only recognizes `LOWER|UPPER|OPTIONAL`, and any other `:suffix` is absorbed into
the token *name* — so `${TOKEN:BOGUS}` becomes a lookup for a token literally named
`TOKEN:BOGUS`, which is undefined.
**Names outside `[A-Za-z0-9_]`.** On `main`, `${a/b/c}` doesn't match the pattern at all,
so it is never a token and is passed through literally. The new pattern matches it, making
it an *undefined token*. In non-strict mode the output is identical, so this is invisible
in most paths — but strict mode changes from "pass through" to "throw".
## Why strict mode matters here specifically
`filterStrict()` has exactly one caller in the tree, and it's in the vault extension:
`VaultUserContext.getTokens()`, resolving secret names from `vault-token-mapping.yml`.
That caller treats the exception as "skip this secret":
```java
catch (GuacamoleTokenUndefinedException e) {
logger.debug("Secret for token \"{}\" will not be retrieved. ...");
continue;
}
```
So for a mapping entry whose secret name contains `/` or `.` — normal for vault paths —
behavior changes from "use it as a literal secret name" to "silently skip it", with only a
debug-level log. No error surfaces; the credential just never arrives. That looks like a
regression against the already-shipped KSM extension, and it's the reason I'd suggest the
`TokenFilter` change be split into its own PR with these tests attached, rather than
riding along with the extension.
I've opened those tests separately as #1231 (GUACAMOLE-2302) against `main`, since they
apply to the existing grammar regardless of what happens here. If they land, they'd give
any change to the token pattern — this one or a later one — something concrete to be
evaluated against.
Two things I checked that turned out fine: no catastrophic backtracking on the new
pattern (worst case I could construct ran in single-digit ms), and escaping via
`$${TOKEN}` is unaffected.
## `TokenFilter.getToken()` is public API
```java
- public String getToken(String name) {
+ public String getToken(String name, String modifier) {
```
`guacamole-ext` is the published extension API, so dropping the one-arg form breaks
third-party extensions that call it. Keeping it as an overload delegating to the new
method would resolve this.
## Sub-token substitution bypasses `canonicalize()`
Separate from the above. `guacamole-vault-base` routes token values through
`VaultSecretService.canonicalize()` — `VaultUserContext.createFilter()` overrides
`setToken()` specifically so a value can't alter the secret name it lands in.
This extension implements `canonicalize()` (`URLEncoder.encode(...)`), but I can't find a
call to it: the single-brace sub-token path in `HvSecretService.applyToken()` does a raw
replace.
```java
value = filter.filter(value);
if (!value.contains("${") && token.contains(placeholder) && !token.contains("$$" + placeholder)) {
token = token.replace(placeholder, value);
}
```
`filter.filter(value)` expands nested `${...}` tokens but doesn't canonicalize, and in
`getValue()` the filter is a plain `new TokenFilter()` rather than the canonicalizing
subclass. So for `vault://kv1/users/{GUAC_USERNAME}/password`, an identifier containing
`/` or `..` changes which path is read. Under LDAP/SSO those identifiers aren't always
constrained, and Vault policy only contains this when the Guacamole token is already
tightly scoped.
Running each substituted value through `canonicalize()` in `applyToken()` would cover it,
and probably also the escaping edge case in the `// FIXME` above that function.
## Cache isolation differs between the two entry points
`getTokens()` isolates per invocation:
```java
final String key = UUID.randomUUID().toString();
```
`getValue()` doesn't:
```java
final String key = guacUsername + "-" + username + "-" +
finalName.substring(0, finalName.lastIndexOf('/'));
```
Fine for KV. For SSH OTP and LDAP dynamic/service accounts it means a secret can be shared
between concurrent resolutions within the cache window, and the `lastIndexOf('/')`
truncation puts everything under a path into one entry regardless of engine. Might be
worth making the cache engine-aware.
Minor: the description says a 10 second cache, `DEFAULT_CACHE_LIFETIME` is `5000`.
## Offer
Happy to take on the conflict resolution against main, or run an interop matrix across
HashiCorp OSS and OpenBao for the engines you support — I have a test environment set up
for both. Say what would help rather than duplicate what you already have in flight.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: dev-unsubscribe@guacamole.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org