diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/.ratignore b/extensions/guacamole-vault/modules/guacamole-vault-openbao/.ratignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/README.md b/extensions/guacamole-vault/modules/guacamole-vault-openbao/README.md new file mode 100644 index 0000000000..6c2cb03046 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/README.md @@ -0,0 +1,258 @@ +# OpenBao Vault Extension for Apache Guacamole + +This extension integrates Apache Guacamole with [OpenBao](https://openbao.org/) vault to automatically retrieve connection credentials from OpenBao at connection time. + +## Overview + +The OpenBao vault extension allows Guacamole to retrieve credentials from an OpenBao server using token-based authentication. Connection parameters configured with special tokens like `${OPENBAO_SECRET}` are automatically replaced with values retrieved from OpenBao when a user connects. + +## Features + +- **Automatic Credential Retrieval**: Fetches credentials from OpenBao without requiring users to re-enter passwords +- **Token-Based Resolution**: Uses `${OPENBAO_SECRET}` and `${GUAC_USERNAME}` tokens in connection parameters +- **KV v2 Support**: Works with OpenBao KV v2 secrets engine +- **Simple Configuration**: Minimal configuration required in `guacamole.properties` +- **Secure**: Uses OpenBao's token-based authentication for secure API access + +## How It Works + +1. User logs into Guacamole with their username +2. User initiates a connection configured with `${OPENBAO_SECRET}` token +3. Extension queries OpenBao API to retrieve the secret for that username +4. Password is extracted and injected into the connection parameters +5. Connection proceeds with the retrieved credentials + +## Configuration + +### OpenBao Server Setup + +Before using this extension, you need: + +1. An OpenBao server running and accessible from the Guacamole server +2. A KV v2 secrets engine mounted (eg path: `guacamole-credentails`) +3. An OpenBao authentication token with read access to the secrets +4. Secrets stored with a `password` field in the data + +Example secret structure: +```json +{ + "data": { + "data": { + "username": "user1", + "password": "SecretPassword123" + } + } +} +``` + +### Guacamole Configuration + +Add the following properties to `guacamole.properties`: + +```properties +# OpenBao server URL (required) +openbao-server-url: http://openbao.example.com:8200 + +# OpenBao authentication token (required unless AppRole is configured) +openbao-token: s.YourTokenHere + +# KV mount path (optional, default: guacamole-credentails) +openbao-mount-path: guacamole-credentails +``` + +#### AppRole Authentication (optional) + +As an alternative to a static `openbao-token`, the extension can +authenticate via the AppRole auth method, which typically issues +shorter-lived tokens: + +```properties +# Required for AppRole +openbao-role-id: +openbao-secret-id: + +# Optional; defaults to "approle" +openbao-approle-path: approle +``` + +When both `openbao-role-id` and `openbao-secret-id` are set, the +extension performs an AppRole login at +`/v1/auth//login` and uses the returned +`client_token` for all subsequent requests. The token is cached and +refreshed automatically on a 403 response. If AppRole is configured, +`openbao-token` is ignored. + +**Note**: The extension uses hardcoded defaults for: +- KV version: `2` (KV v2 secrets engine) +- Connection timeout: `5000ms` (5 seconds) +- Request timeout: `10000ms` (10 seconds) + +### Connection Configuration + +When creating connections in Guacamole, use these token patterns: + +- **`${OPENBAO_SECRET}`**: Replaced with the `password` field of the + secret stored at `/data/`. +- **`${GUAC_USERNAME}`**: Replaced with the logged-in Guacamole username. + +Example RDP connection: +- Username: `${GUAC_USERNAME}` +- Password: `${OPENBAO_SECRET}` +- Hostname: `192.168.1.100` + +#### Arbitrary Secret Paths (via token mapping) + +For secrets that are not stored at the per-user path, an additional +name format can be used in the vault token mapping YAML +(`openbao-token-mapping.yml`): + +``` +openbao:[:] +``` + +- `` is the path under the configured mount, without the + `/data/` prefix (KV v2 is handled automatically). +- `` selects which field to return from the secret's data; + defaults to `password` if omitted. + +Example `openbao-token-mapping.yml`: + +```yaml +DB_PASSWORD: "openbao:db/prod-ro:password" +SSH_KEY: "openbao:ssh-keys/${GUAC_USERNAME}:private_key" +JUMPBOX_PW: "openbao:shared/jumpbox" +``` + +This mechanism is additive. Existing configurations that use only +`${OPENBAO_SECRET}` continue to work unchanged. + +## Secret Path Mapping + +The extension maps Guacamole usernames directly to OpenBao secret paths: + +``` +Guacamole username: "john" +OpenBao secret path: /v1/guacamole-credentails/data/john +``` + +For each user, create a corresponding secret in OpenBao at the path matching their Guacamole username. + +## Building + +Build the extension from the guacamole-client source tree: + +```bash +cd extensions/guacamole-vault +mvn clean package +``` + +The built extension will be located at: +``` +modules/guacamole-vault-openbao/target/guacamole-vault-openbao-.jar +``` + +## Installation + +1. Copy the built JAR to the Guacamole extensions directory: + ```bash + cp guacamole-vault-openbao-*.jar /etc/guacamole/extensions/ + ``` + +2. Ensure `guacamole-vault-base-*.jar` is also present in the extensions directory (it's a dependency) + +3. Configure `guacamole.properties` as described above + +4. Restart Guacamole (e.g., restart Tomcat) + +## Security Considerations + +1. **Protect the OpenBao Token**: Use a dedicated token with minimal permissions (read-only access to required secret paths) + +2. **Use TLS in Production**: Always use HTTPS URLs for OpenBao in production: + ```properties + openbao-server-url: https://openbao.example.com:8200 + ``` + +3. **Network Security**: Restrict OpenBao access to the Guacamole server using firewall rules + +4. **Audit Logging**: Enable OpenBao audit logging to track credential access + +5. **Token Rotation**: Regularly rotate OpenBao tokens and update the configuration + +## Troubleshooting + +### Extension Not Loading + +Check the Guacamole logs (typically in Tomcat's `catalina.out`) for errors. Common issues: + +- Missing `guacamole-vault-base` dependency +- Incorrect permissions on JAR files +- Configuration errors in `guacamole.properties` + +### Secret Not Found + +Error: `Secret not found in OpenBao for username: john` + +Solutions: +1. Verify the secret exists in OpenBao at the expected path +2. Check that the Guacamole username matches the secret name in OpenBao +3. Verify the token has read access to the secret + +### Permission Denied + +Error: `Permission denied accessing OpenBao. Check token permissions.` + +Solutions: +1. Verify the token has appropriate policies attached +2. Check that the token hasn't expired +3. Ensure the token has read access to the KV mount path + +### Connection Timeout + +Error: `Failed to communicate with OpenBao` + +Solutions: +1. Verify OpenBao is accessible from the Guacamole server +2. Check firewall rules between Guacamole and OpenBao +3. Verify the OpenBao URL is correct in the configuration + +## Example Deployment + +1. **Setup OpenBao**: + ```bash + # Start OpenBao + bao server -dev + + # Enable KV v2 engine + bao secrets enable -path=guacamole-credentails kv-v2 + + # Create a secret + bao kv put guacamole-credentails/john password=SecretPass123 + ``` + +2. **Configure Guacamole**: + ```properties + openbao-server-url: http://openbao.example.com:8200 + openbao-token: s.yourtokenhere + openbao-mount-path: guacamole-credentails + ``` + +3. **Create Connection**: + - Name: Windows Server + - Protocol: RDP + - Hostname: 192.168.1.100 + - Username: `${GUAC_USERNAME}` + - Password: `${OPENBAO_SECRET}` + +4. **Connect**: Log in as user "john" and connect to the Windows Server connection. The password will be automatically retrieved from OpenBao. + +## License + +This extension is licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +## Support + +For issues or questions: +- Apache Guacamole: https://guacamole.apache.org/ +- OpenBao: https://openbao.org/ +- Issue Tracker: https://issues.apache.org/jira/browse/GUACAMOLE/ diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/pom.xml b/extensions/guacamole-vault/modules/guacamole-vault-openbao/pom.xml new file mode 100644 index 0000000000..41fc201711 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/pom.xml @@ -0,0 +1,70 @@ + + + + + 4.0.0 + org.apache.guacamole + guacamole-vault-openbao + jar + guacamole-vault-openbao + http://guacamole.apache.org/ + + + org.apache.guacamole + guacamole-vault + ${revision} + ../../ + + + + + + + org.apache.guacamole + guacamole-ext + + + + + org.apache.guacamole + guacamole-vault-base + ${revision} + + + + + org.apache.httpcomponents.client5 + httpclient5 + 5.2.1 + + + + + com.google.code.gson + gson + 2.10.1 + + + + + diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProvider.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProvider.java new file mode 100644 index 0000000000..075335812d --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProvider.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.vault.VaultAuthenticationProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * OpenBao authentication provider that retrieves RDP passwords from OpenBao. + * This provider integrates with the Guacamole vault framework to automatically + * fetch passwords from OpenBao based on the logged-in username. + */ +public class OpenBaoAuthenticationProvider extends VaultAuthenticationProvider { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(OpenBaoAuthenticationProvider.class); + + /** + * Creates a new OpenBaoAuthenticationProvider. + * + * @throws GuacamoleException + * If an error occurs during initialization. + */ + public OpenBaoAuthenticationProvider() throws GuacamoleException { + super(new OpenBaoAuthenticationProviderModule()); + logger.info("OpenBaoAuthenticationProvider initialized"); + } + + @Override + public String getIdentifier() { + return "openbao"; + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProviderModule.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProviderModule.java new file mode 100644 index 0000000000..2092d13417 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/OpenBaoAuthenticationProviderModule.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao; + +import com.google.inject.Scopes; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.vault.VaultAuthenticationProviderModule; +import org.apache.guacamole.vault.conf.VaultConfigurationService; +import org.apache.guacamole.vault.openbao.conf.OpenBaoConfigurationService; +import org.apache.guacamole.vault.openbao.secret.OpenBaoClient; +import org.apache.guacamole.vault.openbao.secret.OpenBaoSecretService; +import org.apache.guacamole.vault.openbao.user.OpenBaoAttributeService; +import org.apache.guacamole.vault.openbao.user.OpenBaoDirectoryService; +import org.apache.guacamole.vault.secret.VaultSecretService; +import org.apache.guacamole.vault.conf.VaultAttributeService; +import org.apache.guacamole.vault.user.VaultDirectoryService; + +/** + * Guice module for configuring OpenBao vault integration. + * Binds the OpenBao-specific implementations to the vault base interfaces. + */ +public class OpenBaoAuthenticationProviderModule extends VaultAuthenticationProviderModule { + + /** + * Creates a new OpenBaoAuthenticationProviderModule. + * + * @throws GuacamoleException + * If an error occurs while reading guacamole.properties. + */ + public OpenBaoAuthenticationProviderModule() throws GuacamoleException { + super(); + } + + @Override + protected void configureVault() { + + // Bind configuration service + bind(VaultConfigurationService.class) + .to(OpenBaoConfigurationService.class) + .in(Scopes.SINGLETON); + + // Bind secret service + bind(VaultSecretService.class) + .to(OpenBaoSecretService.class) + .in(Scopes.SINGLETON); + + // Bind attribute service + bind(VaultAttributeService.class) + .to(OpenBaoAttributeService.class) + .in(Scopes.SINGLETON); + + // Bind directory service + bind(VaultDirectoryService.class) + .to(OpenBaoDirectoryService.class) + .in(Scopes.SINGLETON); + + // Bind OpenBao client + bind(OpenBaoClient.class) + .in(Scopes.SINGLETON); + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfig.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfig.java new file mode 100644 index 0000000000..b651f478c7 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfig.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.conf; + +import org.apache.guacamole.properties.StringGuacamoleProperty; +import org.apache.guacamole.properties.URIGuacamoleProperty; + +/** + * Configuration properties for OpenBao vault integration. + */ +public class OpenBaoConfig { + + /** + * OpenBao server URL (e.g., "http://localhost:8200"). + * This property is REQUIRED and must be configured in guacamole.properties. + */ + public static final URIGuacamoleProperty OPENBAO_SERVER_URL = + new URIGuacamoleProperty() { + @Override + public String getName() { + return "openbao-server-url"; + } + }; + + /** + * OpenBao authentication token. Required unless AppRole + * authentication is configured via {@link #OPENBAO_ROLE_ID} and + * {@link #OPENBAO_SECRET_ID}. + */ + public static final StringGuacamoleProperty OPENBAO_TOKEN = + new StringGuacamoleProperty() { + @Override + public String getName() { + return "openbao-token"; + } + }; + + /** + * OpenBao AppRole role ID. Optional. When both this property and + * {@link #OPENBAO_SECRET_ID} are set, AppRole authentication is used + * instead of a static token. + */ + public static final StringGuacamoleProperty OPENBAO_ROLE_ID = + new StringGuacamoleProperty() { + @Override + public String getName() { + return "openbao-role-id"; + } + }; + + /** + * OpenBao AppRole secret ID. Optional. See {@link #OPENBAO_ROLE_ID}. + */ + public static final StringGuacamoleProperty OPENBAO_SECRET_ID = + new StringGuacamoleProperty() { + @Override + public String getName() { + return "openbao-secret-id"; + } + }; + + /** + * OpenBao AppRole auth mount path (default: "approle"). Only relevant + * when {@link #OPENBAO_ROLE_ID} / {@link #OPENBAO_SECRET_ID} are set. + */ + public static final StringGuacamoleProperty OPENBAO_APPROLE_PATH = + new StringGuacamoleProperty() { + @Override + public String getName() { + return "openbao-approle-path"; + } + }; + + /** + * OpenBao KV secrets engine mount path (default: "rdp-creds"). + * This is the mount point where RDP credentials are stored. + */ + public static final StringGuacamoleProperty OPENBAO_MOUNT_PATH = + new StringGuacamoleProperty() { + @Override + public String getName() { + return "openbao-mount-path"; + } + }; +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfigurationService.java new file mode 100644 index 0000000000..3167293b06 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/conf/OpenBaoConfigurationService.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.conf; + +import com.google.inject.Inject; +import java.net.URI; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.vault.conf.VaultConfigurationService; + +/** + * Service for retrieving OpenBao configuration from guacamole.properties. + */ +public class OpenBaoConfigurationService extends VaultConfigurationService { + + /** + * The Guacamole server environment. + */ + @Inject + private Environment environment; + + /** + * Creates a new OpenBaoConfigurationService. + */ + public OpenBaoConfigurationService() { + super("openbao-token-mapping.yml", "guacamole.properties.openbao"); + } + + /** + * Returns the OpenBao server URL as a parsed URI. + * + * @return The OpenBao server URI (e.g., "http://localhost:8200"). + * @throws GuacamoleException + * If the property is not defined in guacamole.properties or is + * not a valid URI. + */ + public URI getServerUrl() throws GuacamoleException { + return environment.getRequiredProperty(OpenBaoConfig.OPENBAO_SERVER_URL); + } + + /** + * Returns the OpenBao authentication token, or null if AppRole + * authentication is configured instead (see {@link #getRoleId()} and + * {@link #getSecretId()}). + * + * @return The OpenBao authentication token, or null. + * @throws GuacamoleException + * If an error occurs reading the property. + */ + public String getToken() throws GuacamoleException { + return environment.getProperty(OpenBaoConfig.OPENBAO_TOKEN); + } + + /** + * Returns the OpenBao AppRole role ID, or null if not configured. + * + * @return The configured AppRole role ID, or null. + * @throws GuacamoleException + * If an error occurs reading the property. + */ + public String getRoleId() throws GuacamoleException { + return environment.getProperty(OpenBaoConfig.OPENBAO_ROLE_ID); + } + + /** + * Returns the OpenBao AppRole secret ID, or null if not configured. + * + * @return The configured AppRole secret ID, or null. + * @throws GuacamoleException + * If an error occurs reading the property. + */ + public String getSecretId() throws GuacamoleException { + return environment.getProperty(OpenBaoConfig.OPENBAO_SECRET_ID); + } + + /** + * Returns the OpenBao AppRole auth backend mount path (default: "approle"). + * + * @return The AppRole auth mount path. + * @throws GuacamoleException + * If an error occurs reading the property. + */ + public String getAppRolePath() throws GuacamoleException { + return environment.getProperty( + OpenBaoConfig.OPENBAO_APPROLE_PATH, + "approle" + ); + } + + /** + * Returns true if AppRole authentication has been configured (both + * role ID and secret ID are present). + * + * @return true if AppRole should be used, false to use a static token. + * @throws GuacamoleException + * If an error occurs reading the properties. + */ + public boolean isAppRoleConfigured() throws GuacamoleException { + String roleId = getRoleId(); + String secretId = getSecretId(); + return roleId != null && !roleId.isEmpty() + && secretId != null && !secretId.isEmpty(); + } + + /** + * Returns the OpenBao KV secrets engine mount path. + * + * @return The mount path (default: "rdp-creds"). + * @throws GuacamoleException + * If an error occurs reading the property. + */ + public String getMountPath() throws GuacamoleException { + return environment.getProperty( + OpenBaoConfig.OPENBAO_MOUNT_PATH, + "rdp-creds" + ); + } + + /** + * Returns the OpenBao KV version. + * Hardcoded to "2" for KV v2 secrets engine. + * + * @return The KV version "2". + */ + public String getKvVersion() { + return "2"; + } + + /** + * Returns the connection timeout in milliseconds. + * Hardcoded to 5000ms (5 seconds). + * + * @return The connection timeout of 5000ms. + */ + public int getConnectionTimeout() { + return 5000; + } + + /** + * Returns the request timeout in milliseconds. + * Hardcoded to 10000ms (10 seconds). + * + * @return The request timeout of 10000ms. + */ + public int getRequestTimeout() { + return 10000; + } + + @Override + public boolean getSplitWindowsUsernames() throws GuacamoleException { + // Not needed for OpenBao - return false + return false; + } + + @Override + public boolean getMatchUserRecordsByDomain() throws GuacamoleException { + // Not needed for OpenBao - return false + return false; + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoClient.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoClient.java new file mode 100644 index 0000000000..ad3eec6f2a --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoClient.java @@ -0,0 +1,427 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.secret; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import com.google.inject.Inject; +import java.io.IOException; +import java.net.URI; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.vault.openbao.conf.OpenBaoConfigurationService; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.util.Timeout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Client for communicating with OpenBao REST API. + */ +public class OpenBaoClient { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(OpenBaoClient.class); + + /** + * Gson instance for JSON parsing. Gson is thread-safe, so a single + * static instance is reused across all calls. + */ + private static final Gson GSON = new Gson(); + + /** + * Service for retrieving OpenBao configuration. + */ + @Inject + private OpenBaoConfigurationService configService; + + /** + * Shared HTTP client. Lazily created on first use and reused for the + * lifetime of this instance; Apache HttpClient 5 is thread-safe and + * designed to be reused across requests. + */ + private volatile CloseableHttpClient httpClient; + + /** + * Cached AppRole token. Populated on first successful AppRole login + * and re-used until invalidated (e.g. on a 403 response). + */ + private volatile String cachedAppRoleToken; + + /** + * Returns the shared {@link CloseableHttpClient}, creating it on first + * access. Thread-safe double-checked initialization. + * + * @return + * The shared HTTP client instance. + */ + private CloseableHttpClient getHttpClient() { + CloseableHttpClient client = httpClient; + if (client == null) { + synchronized (this) { + client = httpClient; + if (client == null) { + client = HttpClients.createDefault(); + httpClient = client; + } + } + } + return client; + } + + /** + * Builds a {@link RequestConfig} from the configured connection and + * request timeouts. + * + * @return + * A request configuration reflecting current timeouts. + */ + private RequestConfig buildRequestConfig() { + return RequestConfig.custom() + .setConnectionRequestTimeout( + Timeout.ofMilliseconds(configService.getConnectionTimeout())) + .setResponseTimeout( + Timeout.ofMilliseconds(configService.getRequestTimeout())) + .build(); + } + + /** + * Validates that the configured server URL, mount path, and auth + * credentials are present and non-empty, throwing a + * {@link GuacamoleServerException} with a clear message otherwise. + * + * @return + * The validated server URL as a string (without trailing slash). + * + * @throws GuacamoleException + * If a required property is missing, empty, or invalid. + */ + private String validatedServerUrl() throws GuacamoleException { + + URI serverUri = configService.getServerUrl(); + if (serverUri == null || serverUri.toString().trim().isEmpty()) { + throw new GuacamoleServerException( + "OpenBao server URL (\"openbao-server-url\") is not configured."); + } + + String mountPath = configService.getMountPath(); + if (mountPath == null || mountPath.trim().isEmpty()) { + throw new GuacamoleServerException( + "OpenBao mount path (\"openbao-mount-path\") must not be empty."); + } + + // Strip trailing slash to keep path concatenation predictable + String url = serverUri.toString(); + if (url.endsWith("/")) + url = url.substring(0, url.length() - 1); + + return url; + } + + /** + * Resolves a usable OpenBao auth token, either from the configured + * static {@code openbao-token} or, if AppRole is configured, by + * performing an AppRole login. AppRole tokens are cached for the + * lifetime of this client and refreshed only when explicitly + * invalidated via {@link #invalidateCachedToken()}. + * + * @return + * A non-empty OpenBao auth token. + * + * @throws GuacamoleException + * If no valid authentication credentials are configured or if + * AppRole login fails. + */ + private String resolveAuthToken() throws GuacamoleException { + + if (configService.isAppRoleConfigured()) { + String token = cachedAppRoleToken; + if (token == null || token.isEmpty()) { + synchronized (this) { + token = cachedAppRoleToken; + if (token == null || token.isEmpty()) { + token = loginWithAppRole(); + cachedAppRoleToken = token; + } + } + } + return token; + } + + String token = configService.getToken(); + if (token == null || token.trim().isEmpty()) { + throw new GuacamoleServerException( + "OpenBao authentication is not configured. Set " + + "\"openbao-token\", or both \"openbao-role-id\" and " + + "\"openbao-secret-id\"."); + } + + return token; + } + + /** + * Invalidates any cached AppRole token, forcing a fresh login on the + * next request. Called when a 403 indicates the token has expired or + * been revoked. + */ + private synchronized void invalidateCachedToken() { + cachedAppRoleToken = null; + } + + /** + * Performs an AppRole login against OpenBao and returns the issued + * client token. + * + * @return + * The client token returned by OpenBao. + * + * @throws GuacamoleException + * If the login fails or the response cannot be parsed. + */ + private String loginWithAppRole() throws GuacamoleException { + + String serverUrl = validatedServerUrl(); + String roleId = configService.getRoleId(); + String secretId = configService.getSecretId(); + String approlePath = configService.getAppRolePath(); + + String loginUrl = serverUrl + "/v1/auth/" + approlePath + "/login"; + + JsonObject payload = new JsonObject(); + payload.addProperty("role_id", roleId); + payload.addProperty("secret_id", secretId); + + HttpPost httpPost = new HttpPost(loginUrl); + httpPost.setHeader("Accept", "application/json"); + httpPost.setEntity(new StringEntity(payload.toString(), ContentType.APPLICATION_JSON)); + httpPost.setConfig(buildRequestConfig()); + + logger.info("Authenticating to OpenBao using AppRole at {}", loginUrl); + + try (ClassicHttpResponse response = getHttpClient().executeOpen(null, httpPost, null)) { + int statusCode = response.getCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + + if (statusCode != 200) { + throw new GuacamoleServerException( + "OpenBao AppRole login failed (HTTP " + statusCode + "): " + + responseBody); + } + + JsonObject json = GSON.fromJson(responseBody, JsonObject.class); + if (json == null || !json.has("auth")) + throw new GuacamoleServerException( + "OpenBao AppRole login response missing \"auth\" object."); + + JsonObject auth = json.getAsJsonObject("auth"); + if (!auth.has("client_token")) + throw new GuacamoleServerException( + "OpenBao AppRole login response missing \"auth.client_token\"."); + + return auth.get("client_token").getAsString(); + } + catch (IOException | ParseException | JsonSyntaxException e) { + throw new GuacamoleServerException( + "Failed to communicate with OpenBao during AppRole login", e); + } + } + + /** + * Retrieves the secret at the given path, relative to the configured + * mount. For a KV v2 engine this resolves to + * {@code /v1//data/}; for KV v1 it resolves to + * {@code /v1//}. + * + * @param secretPath + * The path (relative to the mount) of the secret to retrieve. + * + * @return + * The parsed JSON response from OpenBao. + * + * @throws GuacamoleException + * If the secret cannot be retrieved from OpenBao. + */ + public JsonObject getSecret(String secretPath) throws GuacamoleException { + + if (secretPath == null || secretPath.trim().isEmpty()) { + throw new GuacamoleServerException( + "OpenBao secret path must not be empty."); + } + + String serverUrl = validatedServerUrl(); + String mountPath = configService.getMountPath(); + String kvVersion = configService.getKvVersion(); + + // Strip any leading slash so URL concatenation stays predictable + String normalizedPath = secretPath.startsWith("/") + ? secretPath.substring(1) : secretPath; + + String apiPath; + if ("2".equals(kvVersion)) + apiPath = String.format("/v1/%s/data/%s", mountPath, normalizedPath); + else + apiPath = String.format("/v1/%s/%s", mountPath, normalizedPath); + + String fullUrl = serverUrl + apiPath; + logger.debug("Fetching secret from OpenBao: {}", fullUrl); + + JsonObject result = executeGet(fullUrl, resolveAuthToken()); + if (result != null) + return result; + + // A null result from executeGet means we got a 403 with an + // AppRole token; retry once with a freshly-issued token. + if (configService.isAppRoleConfigured()) { + invalidateCachedToken(); + logger.info("OpenBao AppRole token may have expired; retrying with a fresh token."); + JsonObject retry = executeGet(fullUrl, resolveAuthToken()); + if (retry != null) + return retry; + } + + throw new GuacamoleServerException( + "Permission denied accessing OpenBao. Check token permissions."); + } + + /** + * Issues a GET against {@code fullUrl} authenticated with the given + * token. Returns the parsed JSON response on success, or {@code null} + * if the response was a 403 (caller may choose to refresh credentials + * and retry). + * + * @param fullUrl + * The fully-qualified URL to GET. + * + * @param token + * The OpenBao auth token to present via {@code X-Vault-Token}. + * + * @return + * The parsed JSON response, or {@code null} on 403. + * + * @throws GuacamoleException + * On non-200, non-403 responses or communication failures. + */ + private JsonObject executeGet(String fullUrl, String token) + throws GuacamoleException { + + HttpGet httpGet = new HttpGet(fullUrl); + httpGet.setHeader("X-Vault-Token", token); + httpGet.setHeader("Accept", "application/json"); + httpGet.setConfig(buildRequestConfig()); + + try (ClassicHttpResponse response = getHttpClient().executeOpen(null, httpGet, null)) { + int statusCode = response.getCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + + if (statusCode == 200) { + logger.debug("OpenBao response 200 for {}", fullUrl); + return GSON.fromJson(responseBody, JsonObject.class); + } + + if (statusCode == 404) { + throw new GuacamoleServerException( + "Secret not found in OpenBao at: " + fullUrl); + } + + if (statusCode == 403) { + // Signal to caller so it can retry after refreshing auth. + return null; + } + + throw new GuacamoleServerException( + "OpenBao error (HTTP " + statusCode + "): " + responseBody); + } + catch (IOException | ParseException | JsonSyntaxException e) { + logger.error("Failed to communicate with OpenBao at {}: {}", + fullUrl, e.getMessage()); + throw new GuacamoleServerException( + "Failed to communicate with OpenBao", e); + } + } + + /** + * Extracts the {@code password} field from an OpenBao secret response. + * + * @param response + * The JSON response previously returned by {@link #getSecret(String)}. + * + * @return + * The password string, or null if not present. + */ + public String extractPassword(JsonObject response) { + return extractField(response, "password"); + } + + /** + * Extracts an arbitrary string field from an OpenBao secret response. + * Supports both KV v2 ({@code data.data.}) and KV v1 + * ({@code data.}) layouts. + * + * @param response + * The JSON response previously returned by {@link #getSecret(String)}. + * + * @param fieldName + * The name of the field to extract from the secret's data object. + * + * @return + * The field value, or null if not present or not a string. + */ + public String extractField(JsonObject response, String fieldName) { + + if (response == null || fieldName == null) + return null; + + if (!response.has("data")) + return null; + + JsonObject data = response.getAsJsonObject("data"); + + // KV v2 nests the user-supplied data under an inner "data" object. + JsonObject values; + if (data.has("data") && data.get("data").isJsonObject()) + values = data.getAsJsonObject("data"); + else + values = data; + + if (!values.has(fieldName)) { + logger.debug("Field \"{}\" not found in OpenBao secret", fieldName); + return null; + } + + if (!values.get(fieldName).isJsonPrimitive()) { + logger.debug("Field \"{}\" in OpenBao secret is not a primitive", fieldName); + return null; + } + + return values.get(fieldName).getAsString(); + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoSecretService.java new file mode 100644 index 0000000000..6024c7cb09 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/secret/OpenBaoSecretService.java @@ -0,0 +1,232 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.secret; + +import com.google.gson.JsonObject; +import com.google.inject.Inject; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.Connectable; +import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.protocol.GuacamoleConfiguration; +import org.apache.guacamole.token.TokenFilter; +import org.apache.guacamole.vault.secret.VaultSecretService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * OpenBao implementation of VaultSecretService. Resolves the legacy + * {@code ${OPENBAO_SECRET}} and {@code ${GUAC_USERNAME}} tokens as well + * as an arbitrary-path syntax of the form + * {@code openbao:[:]}, which, when supplied as a secret + * name via the YAML token mapping, retrieves the given field from the + * named secret under the configured mount. + */ +public class OpenBaoSecretService implements VaultSecretService { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(OpenBaoSecretService.class); + + /** + * Prefix identifying the arbitrary-path secret name syntax: + * {@code openbao:[:]}. + */ + private static final String OPENBAO_PREFIX = "openbao:"; + + /** + * The secret name used for the legacy per-user password lookup. + */ + private static final String OPENBAO_SECRET_NAME = "OPENBAO_SECRET"; + + /** + * The secret name used to resolve to the logged-in Guacamole username. + */ + private static final String GUAC_USERNAME_NAME = "GUAC_USERNAME"; + + /** + * The default field fetched from an OpenBao secret when no explicit + * {@code :field} suffix is supplied. + */ + private static final String DEFAULT_FIELD = "password"; + + /** + * Client for communicating with OpenBao. + */ + @Inject + private OpenBaoClient openBaoClient; + + /** + * Constructor that logs when the service is created. + */ + public OpenBaoSecretService() { + logger.info("OpenBaoSecretService initialized"); + } + + @Override + public String canonicalize(String token) { + + if (token == null) + return null; + + // Existing names (including their ${...} form) are passed through. + if (OPENBAO_SECRET_NAME.equals(token) || ("${" + OPENBAO_SECRET_NAME + "}").equals(token)) + return OPENBAO_SECRET_NAME; + + if (GUAC_USERNAME_NAME.equals(token) || ("${" + GUAC_USERNAME_NAME + "}").equals(token)) + return GUAC_USERNAME_NAME; + + // Arbitrary-path form: let it flow through unchanged so getValue() + // can parse it. + if (token.startsWith(OPENBAO_PREFIX)) + return token; + + return null; + } + + @Override + public Future getValue(String token) throws GuacamoleException { + // Without user context we cannot resolve per-user lookups. + logger.warn("getValue(String) called without user context - cannot determine username"); + return CompletableFuture.completedFuture(null); + } + + @Override + public Future getValue(UserContext userContext, Connectable connectable, String token) + throws GuacamoleException { + + if (token == null) + return CompletableFuture.completedFuture(null); + + String username = userContext.self().getIdentifier(); + + // Legacy: ${GUAC_USERNAME} → username + if (GUAC_USERNAME_NAME.equals(token)) + return CompletableFuture.completedFuture(username); + + // Legacy: ${OPENBAO_SECRET} → password from /data/ + if (OPENBAO_SECRET_NAME.equals(token)) + return CompletableFuture.completedFuture( + fetchField(username, DEFAULT_FIELD, username)); + + // Additive: openbao:[:] + if (token.startsWith(OPENBAO_PREFIX)) { + String spec = token.substring(OPENBAO_PREFIX.length()); + int sep = spec.lastIndexOf(':'); + + String path; + String field; + if (sep > 0 && sep < spec.length() - 1) { + path = spec.substring(0, sep); + field = spec.substring(sep + 1); + } + else { + path = spec; + field = DEFAULT_FIELD; + } + + if (path.isEmpty()) { + logger.warn("Empty path supplied for token: {}", token); + return CompletableFuture.completedFuture(null); + } + + return CompletableFuture.completedFuture(fetchField(path, field, username)); + } + + logger.debug("Token \"{}\" not recognized by OpenBao secret service", token); + return CompletableFuture.completedFuture(null); + } + + /** + * Retrieves {@code field} from the OpenBao secret at {@code path}, + * logging the supplied {@code contextLabel} for diagnostics. Returns + * null rather than throwing when retrieval fails, to preserve the + * existing behavior of allowing Guacamole to proceed (possibly with + * an empty credential). + * + * @param path + * Path of the secret to fetch, relative to the configured mount. + * + * @param field + * Name of the field to extract. + * + * @param contextLabel + * Identifier used only for log messages. + * + * @return + * The field value, or null on failure or if the field is absent. + */ + private String fetchField(String path, String field, String contextLabel) { + try { + JsonObject response = openBaoClient.getSecret(path); + String value = openBaoClient.extractField(response, field); + + if (value == null) + logger.warn("Field \"{}\" not found in OpenBao secret at \"{}\" (context: {})", + field, path, contextLabel); + + return value; + } + catch (GuacamoleException e) { + logger.error("Failed to retrieve secret \"{}\" from OpenBao (context: {}): {}", + path, contextLabel, e.getMessage()); + logger.debug("Underlying exception:", e); + return null; + } + } + + @Override + public Map> getTokens(UserContext userContext, + Connectable connectable, + GuacamoleConfiguration config, + TokenFilter tokenFilter) throws GuacamoleException { + + Map> tokens = new HashMap<>(); + String username = userContext.self().getIdentifier(); + + // GUAC_USERNAME is always available. + tokens.put(GUAC_USERNAME_NAME, CompletableFuture.completedFuture(username)); + + // Best-effort pre-population of OPENBAO_SECRET from the per-user + // secret at /data/. Missing/unreachable secrets + // are logged but do not abort token resolution. + try { + JsonObject response = openBaoClient.getSecret(username); + String password = openBaoClient.extractPassword(response); + if (password != null) + tokens.put(OPENBAO_SECRET_NAME, + CompletableFuture.completedFuture(password)); + else + logger.warn("Password not found in OpenBao for user: {}", username); + } + catch (GuacamoleException e) { + logger.warn("Failed to pre-populate OPENBAO_SECRET for user {}: {}", + username, e.getMessage()); + logger.debug("Underlying exception:", e); + } + + logger.debug("Returning {} OpenBao tokens: {}", tokens.size(), tokens.keySet()); + return tokens; + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoAttributeService.java new file mode 100644 index 0000000000..06ee6679a6 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoAttributeService.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.user; + +import org.apache.guacamole.form.Form; +import org.apache.guacamole.vault.conf.VaultAttributeService; + +import java.util.Collection; +import java.util.Collections; + +/** + * OpenBao implementation of VaultAttributeService. + * Defines attributes that trigger OpenBao secret lookups. + */ +public class OpenBaoAttributeService implements VaultAttributeService { + + @Override + public Collection
getConnectionAttributes() { + // No additional connection attributes needed for OpenBao + // The password field in RDP connections will automatically use OPENBAO:password token + return Collections.emptyList(); + } + + @Override + public Collection getConnectionGroupAttributes() { + // No additional connection group attributes + return Collections.emptyList(); + } + + @Override + public Collection getUserAttributes() { + // No additional user attributes + return Collections.emptyList(); + } + + @Override + public Collection getUserPreferenceAttributes() { + // No additional user preference attributes + return Collections.emptyList(); + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoDirectoryService.java b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoDirectoryService.java new file mode 100644 index 0000000000..31c24a9a84 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/java/org/apache/guacamole/vault/openbao/user/OpenBaoDirectoryService.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.vault.openbao.user; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.ActiveConnection; +import org.apache.guacamole.net.auth.Connection; +import org.apache.guacamole.net.auth.ConnectionGroup; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.SharingProfile; +import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.net.auth.UserGroup; +import org.apache.guacamole.vault.user.VaultDirectoryService; + +/** + * OpenBao implementation of VaultDirectoryService. + * Since OpenBao only provides secrets (not user/group/connection management), + * all directory methods simply pass through the underlying directories unchanged. + */ +public class OpenBaoDirectoryService extends VaultDirectoryService { + + @Override + public Directory getUserDirectory(Directory underlyingUserDirectory) + throws GuacamoleException { + // OpenBao doesn't manage users, just return the underlying directory + return underlyingUserDirectory; + } + + @Override + public Directory getUserGroupDirectory(Directory underlyingUserGroupDirectory) + throws GuacamoleException { + // OpenBao doesn't manage user groups, just return the underlying directory + return underlyingUserGroupDirectory; + } + + @Override + public Directory getConnectionDirectory(Directory underlyingConnectionDirectory) + throws GuacamoleException { + // OpenBao doesn't manage connections, just return the underlying directory + return underlyingConnectionDirectory; + } + + @Override + public Directory getConnectionGroupDirectory( + Directory underlyingConnectionGroupDirectory) throws GuacamoleException { + // OpenBao doesn't manage connection groups, just return the underlying directory + return underlyingConnectionGroupDirectory; + } + + @Override + public Directory getActiveConnectionDirectory( + Directory underlyingActiveConnectionDirectory) throws GuacamoleException { + // OpenBao doesn't manage active connections, just return the underlying directory + return underlyingActiveConnectionDirectory; + } + + @Override + public Directory getSharingProfileDirectory( + Directory underlyingSharingProfileDirectory) throws GuacamoleException { + // OpenBao doesn't manage sharing profiles, just return the underlying directory + return underlyingSharingProfileDirectory; + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/resource-templates/guac-manifest.json b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/resource-templates/guac-manifest.json new file mode 100644 index 0000000000..32b272cc52 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-openbao/src/main/resource-templates/guac-manifest.json @@ -0,0 +1,12 @@ +{ + + "guacamoleVersion" : "${project.version}", + + "name" : "OpenBao Vault", + "namespace" : "openbao", + + "authProviders" : [ + "org.apache.guacamole.vault.openbao.OpenBaoAuthenticationProvider" + ] + +} diff --git a/extensions/guacamole-vault/pom.xml b/extensions/guacamole-vault/pom.xml index 14313ff7a4..1a2d005a56 100644 --- a/extensions/guacamole-vault/pom.xml +++ b/extensions/guacamole-vault/pom.xml @@ -45,6 +45,7 @@ modules/guacamole-vault-ksm + modules/guacamole-vault-openbao diff --git a/guacamole-docker/build.d/010-map-guacamole-extensions.sh b/guacamole-docker/build.d/010-map-guacamole-extensions.sh index 3804120e8a..da9708381b 100644 --- a/guacamole-docker/build.d/010-map-guacamole-extensions.sh +++ b/guacamole-docker/build.d/010-map-guacamole-extensions.sh @@ -115,5 +115,6 @@ map_extensions <<'EOF' guacamole-display-statistics................DISPLAY_STATISTICS_ guacamole-history-recording-storage.........RECORDING_ guacamole-vault/ksm.........................KSM_ + guacamole-vault/openbao.....................OPENBAO_ EOF