From 8bedbe746c180ab3f16fdc2ba9c76e5c1ab394c7 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 21 Jan 2022 15:23:41 -0800 Subject: [PATCH] GUACAMOLE-641: Add initial draft implementation of KSM vault support for Guacamole. --- .../bouncycastle-fips-1.0.2.1/LICENSE | 20 + doc/licenses/bouncycastle-fips-1.0.2.1/README | 8 + .../dep-coordinates.txt | 1 + .../jetbrains-annotations-13.0/README | 9 + .../dep-coordinates.txt | 1 + doc/licenses/kotlin-1.5.30/NOTICE.txt | 2 + doc/licenses/kotlin-1.5.30/README | 8 + .../kotlin-1.5.30/dep-coordinates.txt | 5 + .../kotlinx-serialization-1.2.1/NOTICE.txt | 2 + .../kotlinx-serialization-1.2.1/README | 8 + .../dep-coordinates.txt | 2 + doc/licenses/ksm-sdk-16.2.1/LICENSE | 21 + doc/licenses/ksm-sdk-16.2.1/README | 9 + .../ksm-sdk-16.2.1/dep-coordinates.txt | 1 + .../modules/guacamole-vault-ksm/.ratignore | 0 .../modules/guacamole-vault-ksm/pom.xml | 98 ++++ .../vault/ksm/KsmAuthenticationProvider.java | 47 ++ .../ksm/KsmAuthenticationProviderModule.java | 56 ++ .../vault/ksm/conf/KsmConfigProperty.java | 53 ++ .../ksm/conf/KsmConfigurationService.java | 116 +++++ .../guacamole/vault/ksm/secret/KsmClient.java | 491 ++++++++++++++++++ .../vault/ksm/secret/KsmSecretService.java | 62 +++ .../src/main/resources/guac-manifest.json | 16 + extensions/guacamole-vault/pom.xml | 1 + 24 files changed, 1037 insertions(+) create mode 100644 doc/licenses/bouncycastle-fips-1.0.2.1/LICENSE create mode 100644 doc/licenses/bouncycastle-fips-1.0.2.1/README create mode 100644 doc/licenses/bouncycastle-fips-1.0.2.1/dep-coordinates.txt create mode 100644 doc/licenses/jetbrains-annotations-13.0/README create mode 100644 doc/licenses/jetbrains-annotations-13.0/dep-coordinates.txt create mode 100644 doc/licenses/kotlin-1.5.30/NOTICE.txt create mode 100644 doc/licenses/kotlin-1.5.30/README create mode 100644 doc/licenses/kotlin-1.5.30/dep-coordinates.txt create mode 100644 doc/licenses/kotlinx-serialization-1.2.1/NOTICE.txt create mode 100644 doc/licenses/kotlinx-serialization-1.2.1/README create mode 100644 doc/licenses/kotlinx-serialization-1.2.1/dep-coordinates.txt create mode 100644 doc/licenses/ksm-sdk-16.2.1/LICENSE create mode 100644 doc/licenses/ksm-sdk-16.2.1/README create mode 100644 doc/licenses/ksm-sdk-16.2.1/dep-coordinates.txt create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/.ratignore create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/pom.xml create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProvider.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/guac-manifest.json diff --git a/doc/licenses/bouncycastle-fips-1.0.2.1/LICENSE b/doc/licenses/bouncycastle-fips-1.0.2.1/LICENSE new file mode 100644 index 000000000..a02bc176b --- /dev/null +++ b/doc/licenses/bouncycastle-fips-1.0.2.1/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2000 - 2021 The Legion of the Bouncy Castle Inc. +(https://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/licenses/bouncycastle-fips-1.0.2.1/README b/doc/licenses/bouncycastle-fips-1.0.2.1/README new file mode 100644 index 000000000..4d92a89ee --- /dev/null +++ b/doc/licenses/bouncycastle-fips-1.0.2.1/README @@ -0,0 +1,8 @@ +BouncyCastle FIPS Distribution (https://www.bouncycastle.org/fips-java) +----------------------------------------------------------------------- + + Version: 1.0.2.1 + From: 'The Legion of Bouncy Castle' (https://www.bouncycastle.org) + License(s): + MIT (bundled/bouncycastle-fips-1.0.2.1/LICENSE) + diff --git a/doc/licenses/bouncycastle-fips-1.0.2.1/dep-coordinates.txt b/doc/licenses/bouncycastle-fips-1.0.2.1/dep-coordinates.txt new file mode 100644 index 000000000..854f49a58 --- /dev/null +++ b/doc/licenses/bouncycastle-fips-1.0.2.1/dep-coordinates.txt @@ -0,0 +1 @@ +org.bouncycastle:bc-fips:jar:1.0.2.1 diff --git a/doc/licenses/jetbrains-annotations-13.0/README b/doc/licenses/jetbrains-annotations-13.0/README new file mode 100644 index 000000000..e09618cab --- /dev/null +++ b/doc/licenses/jetbrains-annotations-13.0/README @@ -0,0 +1,9 @@ +Annotations for JVM-based languages +(https://github.com/JetBrains/java-annotations) +----------------------------------------------- + + Version: 13.0 + From: 'JetBrains s.r.o.' (http://www.jetbrains.com) + License(s): + Apache v2.0 + diff --git a/doc/licenses/jetbrains-annotations-13.0/dep-coordinates.txt b/doc/licenses/jetbrains-annotations-13.0/dep-coordinates.txt new file mode 100644 index 000000000..bb558cd6c --- /dev/null +++ b/doc/licenses/jetbrains-annotations-13.0/dep-coordinates.txt @@ -0,0 +1 @@ +org.jetbrains:annotations:jar:13.0 diff --git a/doc/licenses/kotlin-1.5.30/NOTICE.txt b/doc/licenses/kotlin-1.5.30/NOTICE.txt new file mode 100644 index 000000000..3efb9a198 --- /dev/null +++ b/doc/licenses/kotlin-1.5.30/NOTICE.txt @@ -0,0 +1,2 @@ +Kotlin Compiler +Copyright 2010-2020 JetBrains s.r.o and respective authors and developers diff --git a/doc/licenses/kotlin-1.5.30/README b/doc/licenses/kotlin-1.5.30/README new file mode 100644 index 000000000..f2de926e9 --- /dev/null +++ b/doc/licenses/kotlin-1.5.30/README @@ -0,0 +1,8 @@ +Kotlin (https://kotlinlang.org/) +-------------------------------- + + Version: 1.5.30 + From: 'JetBrains s.r.o and respective authors and developers' + License(s): + Apache v2.0 + diff --git a/doc/licenses/kotlin-1.5.30/dep-coordinates.txt b/doc/licenses/kotlin-1.5.30/dep-coordinates.txt new file mode 100644 index 000000000..474a373f0 --- /dev/null +++ b/doc/licenses/kotlin-1.5.30/dep-coordinates.txt @@ -0,0 +1,5 @@ +org.jetbrains.kotlin:kotlin-reflect:jar:1.5.30 +org.jetbrains.kotlin:kotlin-stdlib:jar:1.5.30 +org.jetbrains.kotlin:kotlin-stdlib-common:jar:1.5.30 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:jar:1.5.30 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:jar:1.5.30 diff --git a/doc/licenses/kotlinx-serialization-1.2.1/NOTICE.txt b/doc/licenses/kotlinx-serialization-1.2.1/NOTICE.txt new file mode 100644 index 000000000..16f7e9bee --- /dev/null +++ b/doc/licenses/kotlinx-serialization-1.2.1/NOTICE.txt @@ -0,0 +1,2 @@ +kotlinx.serialization library. +Copyright 2017-2019 JetBrains s.r.o and respective authors and developers diff --git a/doc/licenses/kotlinx-serialization-1.2.1/README b/doc/licenses/kotlinx-serialization-1.2.1/README new file mode 100644 index 000000000..5ded4539e --- /dev/null +++ b/doc/licenses/kotlinx-serialization-1.2.1/README @@ -0,0 +1,8 @@ +Kotlin Serialization (https://github.com/Kotlin/kotlinx.serialization) +---------------------------------------------------------------------- + + Version: 1.2.1 + From: 'JetBrains s.r.o and respective authors and developers' + License(s): + Apache v2.0 + diff --git a/doc/licenses/kotlinx-serialization-1.2.1/dep-coordinates.txt b/doc/licenses/kotlinx-serialization-1.2.1/dep-coordinates.txt new file mode 100644 index 000000000..e6fe5522d --- /dev/null +++ b/doc/licenses/kotlinx-serialization-1.2.1/dep-coordinates.txt @@ -0,0 +1,2 @@ +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:jar:1.2.1 +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:jar:1.2.1 diff --git a/doc/licenses/ksm-sdk-16.2.1/LICENSE b/doc/licenses/ksm-sdk-16.2.1/LICENSE new file mode 100644 index 000000000..b63322a17 --- /dev/null +++ b/doc/licenses/ksm-sdk-16.2.1/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Keeper Security + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/licenses/ksm-sdk-16.2.1/README b/doc/licenses/ksm-sdk-16.2.1/README new file mode 100644 index 000000000..4bf0527cf --- /dev/null +++ b/doc/licenses/ksm-sdk-16.2.1/README @@ -0,0 +1,9 @@ +Keeper Secrets Manager Java SDK +(https://github.com/Keeper-Security/secrets-manager) +---------------------------------------------------- + + Version: 16.2.1 + From: 'Keeper Security' (https://www.keepersecurity.com/) + License(s): + MIT (bundled/ksm-sdk-16.2.1/LICENSE) + diff --git a/doc/licenses/ksm-sdk-16.2.1/dep-coordinates.txt b/doc/licenses/ksm-sdk-16.2.1/dep-coordinates.txt new file mode 100644 index 000000000..5b9330316 --- /dev/null +++ b/doc/licenses/ksm-sdk-16.2.1/dep-coordinates.txt @@ -0,0 +1 @@ +com.keepersecurity.secrets-manager:core:jar:16.2.1 diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/.ratignore b/extensions/guacamole-vault/modules/guacamole-vault-ksm/.ratignore new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/pom.xml b/extensions/guacamole-vault/modules/guacamole-vault-ksm/pom.xml new file mode 100644 index 000000000..ac3b75f9a --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/pom.xml @@ -0,0 +1,98 @@ + + + + + 4.0.0 + org.apache.guacamole + guacamole-vault-ksm + jar + 1.4.0 + guacamole-vault-ksm + http://guacamole.apache.org/ + + + org.apache.guacamole + guacamole-vault + 1.4.0 + ../../ + + + + 1.5.30 + + + + + + + org.apache.guacamole + guacamole-ext + provided + + + + + org.apache.guacamole + guacamole-vault-base + 1.4.0 + + + + com.keepersecurity.secrets-manager + core + 16.2.1 + + + + + org.jetbrains.kotlin + kotlin-stdlib + + + org.jetbrains.kotlin + kotlin-stdlib-common + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + + + diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProvider.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProvider.java new file mode 100644 index 000000000..a24dfc875 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProvider.java @@ -0,0 +1,47 @@ +/* + * 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.ksm; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.vault.VaultAuthenticationProvider; + +/** + * VaultAuthenticationProvider implementation which reads secrets from Keeper + * Secrets Manager + */ +public class KsmAuthenticationProvider extends VaultAuthenticationProvider { + + /** + * Creates a new KsmKeyVaultAuthenticationProvider which reads secrets + * from a configured Keeper Secrets Manager. + * + * @throws GuacamoleException + * If configuration details cannot be read from guacamole.properties. + */ + public KsmAuthenticationProvider() throws GuacamoleException { + super(new KsmAuthenticationProviderModule()); + } + + @Override + public String getIdentifier() { + return "keeper-secrets-manager"; + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java new file mode 100644 index 000000000..df3adf8c6 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java @@ -0,0 +1,56 @@ +/* + * 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.ksm; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.vault.VaultAuthenticationProviderModule; +import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; +import org.apache.guacamole.vault.ksm.secret.KsmSecretService; +import org.apache.guacamole.vault.conf.VaultConfigurationService; +import org.apache.guacamole.vault.ksm.secret.KsmClient; +import org.apache.guacamole.vault.secret.VaultSecretService; + +/** + * Guice module which configures injections specific to Keeper Secrets + * Manager support. + */ +public class KsmAuthenticationProviderModule + extends VaultAuthenticationProviderModule { + + /** + * Creates a new KsmAuthenticationProviderModule which + * configures dependency injection for the Keeper Secrets Manager + * authentication provider and related services. + * + * @throws GuacamoleException + * If configuration details in guacamole.properties cannot be parsed. + */ + public KsmAuthenticationProviderModule() throws GuacamoleException {} + + @Override + protected void configureVault() { + + // Bind services specific to Keeper Secrets Manager + bind(KsmClient.class); + bind(VaultConfigurationService.class).to(KsmConfigurationService.class); + bind(VaultSecretService.class).to(KsmSecretService.class); + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java new file mode 100644 index 000000000..aaddb0de0 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java @@ -0,0 +1,53 @@ +/* + * 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.ksm.conf; + +import com.keepersecurity.secretsManager.core.InMemoryStorage; +import com.keepersecurity.secretsManager.core.KeyValueStorage; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.properties.GuacamoleProperty; + +/** + * A GuacamoleProperty whose value is Keeper Secrets Manager {@link KeyValueStorage} + * object. The value of this property must be base64-encoded JSON, as output by + * the Keeper Commander CLI tool via the "sm client add" command. + */ +public abstract class KsmConfigProperty implements GuacamoleProperty { + + @Override + public KeyValueStorage parseValue(String value) throws GuacamoleException { + + // If no property provided, return null. + if (value == null) + return null; + + // Parse base64 value as KSM config storage + try { + return new InMemoryStorage(value); + } + catch (IllegalArgumentException e) { + throw new GuacamoleServerException("Invalid base64 configuration " + + "for Keeper Secrets Manager.", e); + } + + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java new file mode 100644 index 000000000..398b9bb3a --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java @@ -0,0 +1,116 @@ +/* + * 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.ksm.conf; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.properties.BooleanGuacamoleProperty; +import org.apache.guacamole.vault.conf.VaultConfigurationService; +import com.keepersecurity.secretsManager.core.SecretsManagerOptions; + +/** + * Service for retrieving configuration information regarding the Keeper + * Secrets Manager authentication extension. + */ +@Singleton +public class KsmConfigurationService extends VaultConfigurationService { + + /** + * The Guacamole server environment. + */ + @Inject + private Environment environment; + + /** + * The name of the file which contains the YAML mapping of connection + * parameter token to secrets within Keeper Secrets Manager. + */ + private static final String TOKEN_MAPPING_FILENAME = "ksm-token-mapping.yml"; + + /** + * The base64-encoded configuration information generated by the Keeper + * Commander CLI tool. + */ + private static final KsmConfigProperty KSM_CONFIG = new KsmConfigProperty() { + + @Override + public String getName() { + return "ksm-config"; + } + }; + + /** + * Whether unverified server certificates should be accepted. + */ + private static final BooleanGuacamoleProperty ALLOW_UNVERIFIED_CERT = new BooleanGuacamoleProperty() { + + @Override + public String getName() { + return "ksm-allow-unverified-cert"; + } + }; + + /** + * Creates a new KsmConfigurationService which reads the configuration + * from "ksm-token-mapping.yml". The token mapping is a YAML file which + * lists each connection parameter token and the title of the secret from + * which the value for that token should be read. + */ + public KsmConfigurationService() { + super(TOKEN_MAPPING_FILENAME); + } + + /** + * Return whether unverified server certificates should be accepted when + * communicating with Keeper Secrets Manager. + * + * @return + * true if unverified server certificates should be accepted, false + * otherwise. + * + * @throws GuacamoleException + * If the value specified within guacamole.properties cannot be + * parsed. + */ + public boolean getAllowUnverifiedCertificate() throws GuacamoleException { + return environment.getProperty(ALLOW_UNVERIFIED_CERT, false); + } + + /** + * Returns the options required to authenticate with Keeper Secrets Manager + * when retrieving secrets. These options are read from the contents of + * base64-encoded JSON configuration data generated by the Keeper Commander + * CLI tool. + * + * @return + * The options that should be used when connecting to Keeper Secrets + * Manager when retrieving secrets. + * + * @throws GuacamoleException + * If required properties are not specified within + * guacamole.properties or cannot be parsed. + */ + public SecretsManagerOptions getSecretsManagerOptions() throws GuacamoleException { + return new SecretsManagerOptions(environment.getRequiredProperty(KSM_CONFIG), null, + getAllowUnverifiedCertificate()); + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java new file mode 100644 index 000000000..a72a2fdaa --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -0,0 +1,491 @@ +/* + * 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.ksm.secret; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.keepersecurity.secretsManager.core.Hosts; +import com.keepersecurity.secretsManager.core.KeeperFile; +import com.keepersecurity.secretsManager.core.KeeperRecord; +import com.keepersecurity.secretsManager.core.KeeperRecordData; +import com.keepersecurity.secretsManager.core.KeeperRecordField; +import com.keepersecurity.secretsManager.core.KeeperSecrets; +import com.keepersecurity.secretsManager.core.Login; +import com.keepersecurity.secretsManager.core.Notation; +import com.keepersecurity.secretsManager.core.SecretsManager; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Client which retrieves records from Keeper Secrets Manager, allowing + * content-based record retrieval. Note that because KSM is zero-knowledge, + * searching or indexing based on content can only be accomplished by + * retrieving and indexing everything. Except for record UIDs (which contain no + * information), it's not possible for the server to perform a search of + * content on the client's behalf. The client has to perform its own search. + */ +@Singleton +public class KsmClient { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(KsmClient.class); + + /** + * Service for retrieving configuration information. + */ + @Inject + private KsmConfigurationService confService; + + /** + * The publicly-accessible URL for Keeper's documentation covering Keeper + * notation. + */ + private static final String KEEPER_NOTATION_DOC_URL = + "https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation"; + + /** + * The regular expression that Keeper notation must match to be related to + * file retrieval. As the Keeper SDK provides mutually-exclusive for + * retrieving secret values and files via notation, the notation must first + * be tested to determine whether it refers to a file. + */ + private static final Pattern KEEPER_FILE_NOTATION = Pattern.compile("^(keeper://)?[^/]*/file/.+"); + + /** + * The maximum amount of time that an entry will be stored in the cache + * before being refreshed, in milliseconds. + */ + private static final long CACHE_INTERVAL = 5000; + + /** + * Read/write lock which guards access to all cached data, including the + * timestamp recording the last time the cache was refreshed. Readers of + * the cache must first acquire (and eventually release) the read lock, and + * writers of the cache must first acquire (and eventually release) the + * write lock. + */ + private final ReadWriteLock cacheLock = new ReentrantReadWriteLock(); + + /** + * The timestamp that the cache was last refreshed, in milliseconds, as + * returned by System.currentTimeMillis(). This value is automatically + * updated if {@link #validateCache()} refreshes the cache. This value must + * not be accessed without {@link #cacheLock} acquired appropriately. + */ + private volatile long cacheTimestamp = 0; + + /** + * The full cached set of secrets last retrieved from Keeper Secrets + * Manager. This value is automatically updated if {@link #validateCache()} + * refreshes the cache. This value must not be accessed without + * {@link #cacheLock} acquired appropriately. + */ + private KeeperSecrets cachedSecrets = null; + + /** + * All records retrieved from Keeper Secrets Manager, where each key is the + * UID of the corresponding record. The contents of this Map are + * automatically updated if {@link #validateCache()} refreshes the cache. + * This Map must not be accessed without {@link #cacheLock} acquired + * appropriately. + */ + private final Map cachedRecordsByUid = new HashMap<>(); + + /** + * All records retrieved from Keeper Secrets Manager, where each key is the + * hostname or IP address of the corresponding record. The hostname or IP + * address of a record is determined by {@link Hosts} fields, thus a record + * may be associated with multiple hosts. If a record is associated with + * multiple hosts, there will be multiple references to that record within + * this Map. The contents of this Map are automatically updated if + * {@link #validateCache()} refreshes the cache. This Map must not be + * accessed without {@link #cacheLock} acquired appropriately. Before using + * a value from this Map, {@link #cachedAmbiguousHosts} must first be + * checked to verify that there is indeed only one record associated with + * that host. + */ + private final Map cachedRecordsByHost = new HashMap<>(); + + /** + * The set of all hostnames or IP addresses that are associated with + * multiple records, and thus cannot uniquely identify a record. The + * contents of this Set are automatically updated if + * {@link #validateCache()} refreshes the cache. This Set must not be + * accessed without {@link #cacheLock} acquired appropriately.This Set + * must be checked before using a value retrieved from + * {@link #cachedRecordsByHost}. + */ + private final Set cachedAmbiguousHosts = new HashSet<>(); + + /** + * All records retrieved from Keeper Secrets Manager, where each key is the + * username of the corresponding record. The username of a record is + * determined by {@link Login} fields, thus a record may be associated with + * multiple users. If a record is associated with multiple users, there + * will be multiple references to that record within this Map. The contents + * of this Map are automatically updated if {@link #validateCache()} + * refreshes the cache. This Map must not be accessed without + * {@link #cacheLock} acquired appropriately. Before using a value from + * this Map, {@link #cachedAmbiguousUsernames} must first be checked to + * verify that there is indeed only one record associated with that user. + */ + private final Map cachedRecordsByUsername = new HashMap<>(); + + /** + * The set of all usernames that are associated with multiple records, and + * thus cannot uniquely identify a record. The contents of this Set are + * automatically updated if {@link #validateCache()} refreshes the cache. + * This Set must not be accessed without {@link #cacheLock} acquired + * appropriately.This Set must be checked before using a value retrieved + * from {@link #cachedRecordsByUsername}. + */ + private final Set cachedAmbiguousUsernames = new HashSet<>(); + + /** + * Validates that all cached data is current with respect to + * {@link #CACHE_INTERVAL}, refreshing data from the server as needed. + * + * @throws GuacamoleException + * If an error occurs preventing the cached data from being refreshed. + */ + private void validateCache() throws GuacamoleException { + + long currentTime = System.currentTimeMillis(); + + // Perform a read-only check that the cache has actually expired before + // continuing + cacheLock.readLock().lock(); + try { + if (currentTime - cacheTimestamp < CACHE_INTERVAL) + return; + } + finally { + cacheLock.readLock().unlock(); + } + + cacheLock.writeLock().lock(); + try { + + // Cache may have been updated since the read-only check. Re-verify + // that the cache has expired before continuing with a full refresh + if (currentTime - cacheTimestamp < CACHE_INTERVAL) + return; + + // Attempt to pull all records first, allowing that operation to + // succeed/fail BEFORE we clear out the last cached success + KeeperSecrets secrets = SecretsManager.getSecrets(confService.getSecretsManagerOptions()); + List records = secrets.getRecords(); + + // Store all secrets within cache + cachedSecrets = secrets; + + // Clear unambiguous cache of all records by UID + cachedRecordsByUid.clear(); + + // Clear cache of host-based records + cachedAmbiguousHosts.clear(); + cachedRecordsByHost.clear(); + + // Clear cache of login-based records + cachedAmbiguousUsernames.clear(); + cachedRecordsByUsername.clear(); + + // Store all records, sorting each into host-based and login-based + // buckets (note that a single record may be associated with + // multiple hosts and logins) + records.forEach(record -> { + + // Store based on UID ... + cachedRecordsByUid.put(record.getRecordUid(), record); + + // ... and standard fields ... + KeeperRecordData data = record.getData(); + addRecordForHosts(record, (Hosts) data.getField(Hosts.class)); + addRecordForLogin(record, (Login) data.getField(Login.class)); + + // ... and custom fields + List custom = data.getCustom(); + if (custom != null) { + custom.forEach(field -> { + if (field instanceof Hosts) + addRecordForHosts(record, (Hosts) field); + else if (field instanceof Login) + addRecordForLogin(record, (Login) field); + }); + } + + }); + + // Cache has been refreshed + this.cacheTimestamp = System.currentTimeMillis(); + + } + finally { + cacheLock.writeLock().unlock(); + } + + } + + /** + * Associates the given record with each of the hosts in the given Hosts + * field. The given Hosts field may be null. Both {@link #cachedRecordsByHost} + * and {@link #cachedAmbiguousHosts} are updated appropriately. The write + * lock of {@link #cacheLock} must already be acquired before invoking this + * function. + * + * @param record + * The record to associate with the hosts in the given field. + * + * @param hosts + * The Hosts field containing the hosts that the given record should be + * associated with. This may be null. + */ + private void addRecordForHosts(KeeperRecord record, Hosts hosts) { + + if (hosts == null) + return; + + hosts.getValue().stream().map(host -> host.getHostName()) + .forEachOrdered(hostname -> { + + KeeperRecord existing = cachedRecordsByHost.putIfAbsent(hostname, record); + if (existing != null && record != existing) + cachedAmbiguousHosts.add(hostname); + + }); + + } + + /** + * Associates the given record with each of the usernames in the given + * Login field. The given Hosts field may be null. Both + * {@link #cachedRecordsByUsername} and {@link #cachedAmbiguousUsernames} + * are updated appropriately. The write lock of {@link #cacheLock} must + * already be acquired before invoking this function. + * + * @param record + * The record to associate with the hosts in the given field. + * + * @param login + * The Login field containing the usernames that the given record + * should be associated with. This may be null. + */ + private void addRecordForLogin(KeeperRecord record, Login login) { + + if (login == null) + return; + + login.getValue().stream() + .forEachOrdered(username -> { + + KeeperRecord existing = cachedRecordsByUsername.putIfAbsent(username, record); + if (existing != null && record != existing) + cachedAmbiguousUsernames.add(username); + + }); + + } + + /** + * Returns all records accessible via Keeper Secrets Manager. The records + * returned are arbitrarily ordered. + * + * @return + * An unmodifiable Collection of all records accessible via Keeper + * Secrets Manager, in no particular order. + * + * @throws GuacamoleException + * If an error occurs that prevents records from being retrieved. + */ + public Collection getRecords() throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + return Collections.unmodifiableCollection(cachedRecordsByUid.values()); + } + finally { + cacheLock.readLock().unlock(); + } + } + + /** + * Returns the record having the given UID. If no such record exists, null + * is returned. + * + * @param uid + * The UID of the record to return. + * + * @return + * The record having the given UID, or null if there is no such record. + * + * @throws GuacamoleException + * If an error occurs that prevents the record from being retrieved. + */ + public KeeperRecord getRecord(String uid) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + return cachedRecordsByUid.get(uid); + } + finally { + cacheLock.readLock().unlock(); + } + } + + /** + * Returns the record associated with the given hostname or IP address. If + * no such record exists, or there are multiple such records, null is + * returned. + * + * @param hostname + * The hostname of the record to return. + * + * @return + * The record associated with the given hostname, or null if there is + * no such record or multiple such records. + * + * @throws GuacamoleException + * If an error occurs that prevents the record from being retrieved. + */ + public KeeperRecord getRecordByHost(String hostname) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + + if (cachedAmbiguousHosts.contains(hostname)) + return null; + + return cachedRecordsByHost.get(hostname); + + } + finally { + cacheLock.readLock().unlock(); + } + } + + /** + * Returns the record associated with the given username. If no such record + * exists, or there are multiple such records, null is returned. + * + * @param username + * The username of the record to return. + * + * @return + * The record associated with the given username, or null if there is + * no such record or multiple such records. + * + * @throws GuacamoleException + * If an error occurs that prevents the record from being retrieved. + */ + public KeeperRecord getRecordByLogin(String username) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + + if (cachedAmbiguousUsernames.contains(username)) + return null; + + return cachedRecordsByUsername.get(username); + + } + finally { + cacheLock.readLock().unlock(); + } + } + + /** + * Returns the value of the secret stored within Keeper Secrets Manager and + * represented by the given Keeper notation. Keeper notation locates the + * value of a specific field, custom field, or file associated with a + * specific record. See: https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation + * + * @param notation + * The Keeper notation of the secret to retrieve. + * + * @return + * A Future which completes with the value of the secret represented by + * the given Keeper notation, or null if there is no such secret. + * + * @throws GuacamoleException + * If the requested secret cannot be retrieved or the Keeper notation + * is invalid. + */ + public Future getSecret(String notation) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + + Matcher fileNotationMatcher = KEEPER_FILE_NOTATION.matcher(notation); + if (fileNotationMatcher.matches()) { + + // Retrieve any relevant file asynchronously + KeeperFile file = Notation.getFile(cachedSecrets, notation); + return CompletableFuture.supplyAsync(() -> { + return new String(SecretsManager.downloadFile(file), StandardCharsets.UTF_8); + }); + + } + + // Retrieve string values synchronously + return CompletableFuture.completedFuture(Notation.getValue(cachedSecrets, notation)); + + } + + // Unfortunately, the notation parser within the Keeper SDK throws + // plain Errors for retrieval failures ... + catch (Error e) { + logger.warn("Record \"{}\" does not exist.", notation); + logger.debug("Retrieval of record by Keeper notation failed.", e); + return CompletableFuture.completedFuture(null); + } + + // ... and plain Exceptions for parse failures (no subclasses) + catch (Exception e) { + logger.warn("\"{}\" is not valid Keeper notation. Please check " + + "the documentation at {} for valid formatting.", + notation, KEEPER_NOTATION_DOC_URL); + logger.debug("Provided Keeper notation could not be parsed.", e); + return CompletableFuture.completedFuture(null); + } + finally { + cacheLock.readLock().unlock(); + } + + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java new file mode 100644 index 000000000..c8ae0d7e3 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -0,0 +1,62 @@ +/* + * 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.ksm.secret; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.concurrent.Future; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.vault.secret.VaultSecretService; + +/** + * Service which retrieves secrets from Keeper Secrets Manager. + */ +@Singleton +public class KsmSecretService implements VaultSecretService { + + /** + * Client for retrieving records and secrets from Keeper Secrets Manager. + */ + @Inject + private KsmClient ksm; + + @Override + public String canonicalize(String nameComponent) { + try { + + // As Keeper notation is essentially a URL, encode all components + // using standard URL escaping + return URLEncoder.encode(nameComponent, "UTF-8"); + + } + catch (UnsupportedEncodingException e) { + throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); + } + } + + @Override + public Future getValue(String name) throws GuacamoleException { + return ksm.getSecret(name); + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/guac-manifest.json b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/guac-manifest.json new file mode 100644 index 000000000..a584b88b8 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/guac-manifest.json @@ -0,0 +1,16 @@ +{ + + "guacamoleVersion" : "1.4.0", + + "name" : "Keeper Secrets Manager", + "namespace" : "keeper-secrets-manager", + + "authProviders" : [ + "org.apache.guacamole.vault.ksm.KsmAuthenticationProvider" + ], + + "translations" : [ + "translations/en.json" + ] + +} diff --git a/extensions/guacamole-vault/pom.xml b/extensions/guacamole-vault/pom.xml index 281572b3a..80eb3da57 100644 --- a/extensions/guacamole-vault/pom.xml +++ b/extensions/guacamole-vault/pom.xml @@ -47,6 +47,7 @@ modules/guacamole-vault-azure + modules/guacamole-vault-ksm