From 436fdd97dcbf0f6be9fc9808bfb0df24e31dad67 Mon Sep 17 00:00:00 2001 From: Hardik Shah Date: Sat, 2 Mar 2019 20:45:58 -0800 Subject: [PATCH] Open Distro for Elasticsearch Security initial release --- .gitignore | 36 + CODE_OF_CONDUCT.md | 4 + CONTRIBUTING.md | 61 + LICENSE | 202 +++ NOTICE.txt | 11 + README.md | 77 ++ THIRD-PARTY.txt | 71 ++ dev/scan_veracode.sh | 71 ++ plugin-descriptor.properties | 25 + plugin-security.policy | 79 ++ pom.xml | 436 +++++++ securityconfig/action_groups.yml | 137 ++ securityconfig/config.yml | 221 ++++ securityconfig/elasticsearch.yml.example | 199 +++ securityconfig/internal_users.yml | 45 + securityconfig/roles.yml | 183 +++ securityconfig/roles_mapping.yml | 34 + settings.xml | 10 + src/main/assemblies/plugin.xml | 43 + .../assemblies/securityadmin-standalone.xml | 43 + src/main/assemblies/veracode.xml | 24 + .../security/OpenDistroSecurityPlugin.java | 1111 +++++++++++++++++ .../configupdate/ConfigUpdateAction.java | 55 + .../ConfigUpdateNodeResponse.java | 87 ++ .../configupdate/ConfigUpdateRequest.java | 80 ++ .../ConfigUpdateRequestBuilder.java | 51 + .../configupdate/ConfigUpdateResponse.java | 60 + .../TransportConfigUpdateAction.java | 128 ++ .../action/whoami/TransportWhoAmIAction.java | 77 ++ .../security/action/whoami/WhoAmIAction.java | 55 + .../security/action/whoami/WhoAmIRequest.java | 54 + .../action/whoami/WhoAmIRequestBuilder.java | 46 + .../action/whoami/WhoAmIResponse.java | 107 ++ .../security/auditlog/AuditLog.java | 87 ++ .../auditlog/AuditLogSslExceptionHandler.java | 90 ++ .../security/auditlog/NullAuditLog.java | 142 +++ .../security/auth/AuthDomain.java | 76 ++ .../security/auth/AuthenticationBackend.java | 83 ++ .../security/auth/AuthorizationBackend.java | 75 ++ .../security/auth/BackendRegistry.java | 787 ++++++++++++ .../security/auth/Destroyable.java | 35 + .../security/auth/HTTPAuthenticator.java | 90 ++ .../security/auth/UserInjector.java | 156 +++ .../InternalAuthenticationBackend.java | 176 +++ .../internal/NoOpAuthenticationBackend.java | 62 + .../internal/NoOpAuthorizationBackend.java | 57 + .../security/compliance/ComplianceConfig.java | 326 +++++ .../ComplianceIndexingOperationListener.java | 46 + .../configuration/ActionGroupHolder.java | 97 ++ .../security/configuration/AdminDNs.java | 159 +++ .../configuration/ClusterInfoHolder.java | 120 ++ .../security/configuration/CompatConfig.java | 102 ++ .../configuration/ConfigCallback.java | 43 + .../ConfigurationChangeListener.java | 45 + .../configuration/ConfigurationLoader.java | 208 +++ .../ConfigurationRepository.java | 97 ++ .../configuration/DlsFlsRequestValve.java | 58 + .../configuration/EmptyFilterLeafReader.java | 186 +++ .../IndexBaseConfigurationRepository.java | 433 +++++++ .../configuration/InvalidConfigException.java | 60 + .../LegacyConfigurationLoader.java | 208 +++ ...penDistroSecurityIndexSearcherWrapper.java | 108 ++ .../filter/OpenDistroSecurityFilter.java | 342 +++++ .../filter/OpenDistroSecurityRestFilter.java | 158 +++ .../security/http/HTTPBasicAuthenticator.java | 83 ++ .../http/HTTPClientCertAuthenticator.java | 127 ++ .../security/http/HTTPProxyAuthenticator.java | 100 ++ ...OpenDistroSecurityHttpServerTransport.java | 51 + ...stroSecurityNonSslHttpServerTransport.java | 70 ++ .../security/http/RemoteIpDetector.java | 343 +++++ .../security/http/XFFResolver.java | 108 ++ .../security/privileges/DlsFlsEvaluator.java | 173 +++ ...penDistroSecurityIndexAccessEvaluator.java | 122 ++ .../privileges/PrivilegesEvaluator.java | 707 +++++++++++ .../PrivilegesEvaluatorResponse.java | 93 ++ .../privileges/PrivilegesInterceptor.java | 68 + .../privileges/SnapshotRestoreEvaluator.java | 111 ++ .../privileges/TermsAggregationEvaluator.java | 108 ++ .../resolver/IndexResolverReplacer.java | 904 ++++++++++++++ .../security/rest/KibanaInfoAction.java | 119 ++ .../rest/OpenDistroSecurityHealthAction.java | 109 ++ .../rest/OpenDistroSecurityInfoAction.java | 143 +++ .../security/rest/TenantInfoAction.java | 167 +++ .../security/securityconf/ConfigModel.java | 830 ++++++++++++ .../security/support/Base64Helper.java | 145 +++ .../security/support/ConfigConstants.java | 242 ++++ .../security/support/ConfigHelper.java | 92 ++ .../security/support/HTTPHelper.java | 106 ++ .../security/support/HeaderHelper.java | 87 ++ .../security/support/MapUtils.java | 69 + .../security/support/ModuleInfo.java | 188 +++ .../security/support/ModuleType.java | 140 +++ .../OpenDistroSecurityDeprecationHandler.java | 50 + .../support/OpenDistroSecurityUtils.java | 91 ++ .../security/support/PemKeyReader.java | 388 ++++++ .../security/support/ReflectionHelper.java | 378 ++++++ .../support/SnapshotRestoreHelper.java | 109 ++ .../security/support/SourceFieldsContext.java | 114 ++ .../security/support/WildcardMatcher.java | 588 +++++++++ .../security/tools/Hasher.java | 90 ++ .../tools/OpenDistroSecurityAdmin.java | 1042 ++++++++++++++++ .../DefaultInterClusterRequestEvaluator.java | 142 +++ .../InterClusterRequestEvaluator.java | 65 + .../transport/OIDClusterRequestEvaluator.java | 66 + .../OpenDistroSecurityInterceptor.java | 223 ++++ .../OpenDistroSecurityRequestHandler.java | 328 +++++ .../security/user/AuthCredentials.java | 233 ++++ .../security/user/CustomAttributesAware.java | 38 + .../security/user/User.java | 250 ++++ src/main/resources/KEYS | 31 + .../security/AggregationTests.java | 126 ++ ...waysFalseInterClusterRequestEvaluator.java | 59 + .../security/HealthTests.java | 82 ++ .../security/HttpIntegrationTests.java | 684 ++++++++++ .../security/IndexIntegrationTests.java | 529 ++++++++ .../InitializationIntegrationTests.java | 206 +++ .../security/IntegrationTests.java | 836 +++++++++++++ .../security/SlowIntegrationTests.java | 149 +++ .../security/SnapshotRestoreTests.java | 310 +++++ .../security/SystemIntegratorsTests.java | 261 ++++ .../security/TracingTests.java | 530 ++++++++ .../TransportClientIntegrationTests.java | 780 ++++++++++++ .../security/UtilTests.java | 103 ++ .../ccstest/CrossClusterSearchTests.java | 265 ++++ .../security/ccstest/RemoteReindexTests.java | 133 ++ .../PrivilegesInterceptorImpl.java | 62 + .../test/AbstractSecurityUnitTest.java | 256 ++++ .../security/test/DynamicSecurityConfig.java | 131 ++ .../security/test/NodeSettingsSupplier.java | 38 + .../security/test/SingleClusterTest.java | 101 ++ .../helper/cluster/ClusterConfiguration.java | 114 ++ .../test/helper/cluster/ClusterHelper.java | 329 +++++ .../test/helper/cluster/ClusterInfo.java | 46 + .../security/test/helper/file/FileHelper.java | 148 +++ .../test/helper/network/SocketUtils.java | 338 +++++ .../security/test/helper/rest/RestHelper.java | 299 +++++ .../rules/OpenDistroSecurityTestWatcher.java | 59 + .../test/plugin/UserInjectorPlugin.java | 115 ++ .../elasticsearch/node/PluginAwareNode.java | 67 + src/test/resources/action_groups.yml | 60 + src/test/resources/action_groups_packaged.yml | 91 ++ src/test/resources/composite_config.yml | 75 ++ src/test/resources/config.yml | 75 ++ src/test/resources/config_anon.yml | 18 + src/test/resources/config_clientcert.yml | 18 + src/test/resources/config_dnfof.yml | 76 ++ src/test/resources/config_invalidlic.yml | 75 ++ src/test/resources/config_ldap.yml | 27 + src/test/resources/config_lic.yml | 75 ++ src/test/resources/config_lic_rk.yml | 75 ++ src/test/resources/config_multirolespan.yml | 76 ++ src/test/resources/config_proxy.yml | 28 + src/test/resources/config_proxy_custom.yml | 29 + .../resources/config_rest_impersonation.yml | 20 + .../resources/config_transport_username.yml | 76 ++ src/test/resources/config_xff.yml | 74 ++ src/test/resources/data1.json | 26 + src/test/resources/data2.json | 6 + src/test/resources/data3.json | 7 + src/test/resources/internal_empty.yml | 2 + src/test/resources/internal_users.yml | 183 +++ .../internal_users_spock_add_roles.yml | 61 + .../internal_users_transport_username.yml | 183 +++ src/test/resources/kirk-keystore.jks | Bin 0 -> 4525 bytes src/test/resources/ldap.ldif | 58 + src/test/resources/log4j2-test.properties | 32 + src/test/resources/mapping1.json | 76 ++ src/test/resources/mapping2.json | 79 ++ src/test/resources/mapping3.json | 16 + src/test/resources/mapping4.json | 19 + src/test/resources/node-0-keystore.jks | Bin 0 -> 4593 bytes src/test/resources/node-1-keystore.jks | Bin 0 -> 4595 bytes src/test/resources/node-2-keystore.jks | Bin 0 -> 4593 bytes src/test/resources/node-untspec5-keystore.p12 | Bin 0 -> 3861 bytes src/test/resources/node-untspec6-keystore.p12 | Bin 0 -> 3861 bytes src/test/resources/roles.yml | 516 ++++++++ src/test/resources/roles_bs.yml | 26 + src/test/resources/roles_bulk.yml | 12 + src/test/resources/roles_ccs.yml | 13 + src/test/resources/roles_composite.yml | 41 + src/test/resources/roles_deny.yml | 1 + src/test/resources/roles_itt1635.yml | 23 + src/test/resources/roles_mapping.yml | 166 +++ .../roles_mapping_transport_username.yml | 166 +++ src/test/resources/spock-keystore.jks | Bin 0 -> 4528 bytes src/test/resources/truststore.jks | Bin 0 -> 1096 bytes src/test/resources/truststore_fail.jks | Bin 0 -> 2215 bytes tools/hash.bat | 4 + tools/hash.sh | 12 + tools/install_demo_configuration.sh | 426 +++++++ tools/securityadmin.bat | 3 + tools/securityadmin.sh | 12 + 192 files changed, 28759 insertions(+) create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 NOTICE.txt create mode 100644 README.md create mode 100644 THIRD-PARTY.txt create mode 100755 dev/scan_veracode.sh create mode 100644 plugin-descriptor.properties create mode 100644 plugin-security.policy create mode 100644 pom.xml create mode 100644 securityconfig/action_groups.yml create mode 100644 securityconfig/config.yml create mode 100644 securityconfig/elasticsearch.yml.example create mode 100644 securityconfig/internal_users.yml create mode 100644 securityconfig/roles.yml create mode 100644 securityconfig/roles_mapping.yml create mode 100644 settings.xml create mode 100644 src/main/assemblies/plugin.xml create mode 100644 src/main/assemblies/securityadmin-standalone.xml create mode 100644 src/main/assemblies/veracode.xml create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/OpenDistroSecurityPlugin.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateAction.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateNodeResponse.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateRequest.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateRequestBuilder.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateResponse.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/TransportConfigUpdateAction.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/TransportWhoAmIAction.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIAction.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIRequest.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIRequestBuilder.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIResponse.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/AuditLog.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/AuditLogSslExceptionHandler.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/NullAuditLog.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthDomain.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthenticationBackend.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthorizationBackend.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auth/BackendRegistry.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auth/Destroyable.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auth/HTTPAuthenticator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auth/UserInjector.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/InternalAuthenticationBackend.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthenticationBackend.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthorizationBackend.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceConfig.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceIndexingOperationListener.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ActionGroupHolder.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/AdminDNs.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ClusterInfoHolder.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/CompatConfig.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigCallback.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationChangeListener.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationLoader.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationRepository.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/DlsFlsRequestValve.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/EmptyFilterLeafReader.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/IndexBaseConfigurationRepository.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/InvalidConfigException.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/LegacyConfigurationLoader.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/OpenDistroSecurityIndexSearcherWrapper.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityFilter.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityRestFilter.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPBasicAuthenticator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPClientCertAuthenticator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPProxyAuthenticator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityHttpServerTransport.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityNonSslHttpServerTransport.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/http/RemoteIpDetector.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/http/XFFResolver.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/DlsFlsEvaluator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/OpenDistroSecurityIndexAccessEvaluator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluatorResponse.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesInterceptor.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/SnapshotRestoreEvaluator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/TermsAggregationEvaluator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/resolver/IndexResolverReplacer.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/rest/KibanaInfoAction.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/rest/OpenDistroSecurityHealthAction.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/rest/OpenDistroSecurityInfoAction.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/rest/TenantInfoAction.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/ConfigModel.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/Base64Helper.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/ConfigConstants.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/ConfigHelper.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/HTTPHelper.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/HeaderHelper.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/MapUtils.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleInfo.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleType.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/OpenDistroSecurityDeprecationHandler.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/OpenDistroSecurityUtils.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/PemKeyReader.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/ReflectionHelper.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/SnapshotRestoreHelper.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/SourceFieldsContext.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/support/WildcardMatcher.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/tools/Hasher.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/tools/OpenDistroSecurityAdmin.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/transport/DefaultInterClusterRequestEvaluator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/transport/InterClusterRequestEvaluator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OIDClusterRequestEvaluator.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OpenDistroSecurityInterceptor.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OpenDistroSecurityRequestHandler.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentials.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/user/CustomAttributesAware.java create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/security/user/User.java create mode 100644 src/main/resources/KEYS create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/AggregationTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/AlwaysFalseInterClusterRequestEvaluator.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/HealthTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/HttpIntegrationTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/IndexIntegrationTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/InitializationIntegrationTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/IntegrationTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/SlowIntegrationTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/SnapshotRestoreTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/SystemIntegratorsTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/TracingTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/TransportClientIntegrationTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/UtilTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/ccstest/CrossClusterSearchTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/ccstest/RemoteReindexTests.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/configuration/PrivilegesInterceptorImpl.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/AbstractSecurityUnitTest.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/DynamicSecurityConfig.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/NodeSettingsSupplier.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/SingleClusterTest.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterConfiguration.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterHelper.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterInfo.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/file/FileHelper.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/network/SocketUtils.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/rest/RestHelper.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/rules/OpenDistroSecurityTestWatcher.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/security/test/plugin/UserInjectorPlugin.java create mode 100644 src/test/java/org/elasticsearch/node/PluginAwareNode.java create mode 100644 src/test/resources/action_groups.yml create mode 100644 src/test/resources/action_groups_packaged.yml create mode 100644 src/test/resources/composite_config.yml create mode 100644 src/test/resources/config.yml create mode 100644 src/test/resources/config_anon.yml create mode 100644 src/test/resources/config_clientcert.yml create mode 100644 src/test/resources/config_dnfof.yml create mode 100644 src/test/resources/config_invalidlic.yml create mode 100644 src/test/resources/config_ldap.yml create mode 100644 src/test/resources/config_lic.yml create mode 100644 src/test/resources/config_lic_rk.yml create mode 100644 src/test/resources/config_multirolespan.yml create mode 100644 src/test/resources/config_proxy.yml create mode 100644 src/test/resources/config_proxy_custom.yml create mode 100644 src/test/resources/config_rest_impersonation.yml create mode 100644 src/test/resources/config_transport_username.yml create mode 100644 src/test/resources/config_xff.yml create mode 100644 src/test/resources/data1.json create mode 100644 src/test/resources/data2.json create mode 100644 src/test/resources/data3.json create mode 100644 src/test/resources/internal_empty.yml create mode 100644 src/test/resources/internal_users.yml create mode 100644 src/test/resources/internal_users_spock_add_roles.yml create mode 100644 src/test/resources/internal_users_transport_username.yml create mode 100644 src/test/resources/kirk-keystore.jks create mode 100755 src/test/resources/ldap.ldif create mode 100644 src/test/resources/log4j2-test.properties create mode 100644 src/test/resources/mapping1.json create mode 100644 src/test/resources/mapping2.json create mode 100644 src/test/resources/mapping3.json create mode 100644 src/test/resources/mapping4.json create mode 100644 src/test/resources/node-0-keystore.jks create mode 100644 src/test/resources/node-1-keystore.jks create mode 100644 src/test/resources/node-2-keystore.jks create mode 100644 src/test/resources/node-untspec5-keystore.p12 create mode 100644 src/test/resources/node-untspec6-keystore.p12 create mode 100644 src/test/resources/roles.yml create mode 100644 src/test/resources/roles_bs.yml create mode 100644 src/test/resources/roles_bulk.yml create mode 100644 src/test/resources/roles_ccs.yml create mode 100644 src/test/resources/roles_composite.yml create mode 100644 src/test/resources/roles_deny.yml create mode 100644 src/test/resources/roles_itt1635.yml create mode 100644 src/test/resources/roles_mapping.yml create mode 100644 src/test/resources/roles_mapping_transport_username.yml create mode 100644 src/test/resources/spock-keystore.jks create mode 100644 src/test/resources/truststore.jks create mode 100644 src/test/resources/truststore_fail.jks create mode 100644 tools/hash.bat create mode 100755 tools/hash.sh create mode 100755 tools/install_demo_configuration.sh create mode 100644 tools/securityadmin.bat create mode 100755 tools/securityadmin.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..06f5a72868 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +netty*/ +smoketests/ +target/ +test-output/ +kibana*/ +logstash*/ +deploy_all.sh +/build.gradle +*.log +.externalToolBuilders +maven-eclipse.xml + +## eclipse ignores (use 'mvn eclipse:eclipse' to build eclipse projects) +## The only configuration files which are not ignored are certain files in +## .settings (as listed below) since these files ensure common coding +## style across Eclipse and IDEA. +## Other files (.project, .classpath) should be generated through Maven which +## will correctly set the classpath based on the declared dependencies. +.project +.classpath +eclipse-build +*/.project +*/.classpath +*/eclipse-build +/.settings/ +!/.settings/org.eclipse.core.resources.prefs +!/.settings/org.eclipse.jdt.core.prefs +!/.settings/org.eclipse.jdt.ui.prefs +!/.settings/org.eclipse.jdt.groovy.core.prefs +bin +elasticsearch-*/ +.DS_Store +data/ +puppet/.vagrant +test.sh +.vagrant/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..5b627cfa60 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,4 @@ +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..b6c75e9fb1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional +documentation, we greatly value feedback and contributions from our community. + +Please read through this document before submitting any issues or pull requests to ensure we have all the necessary +information to effectively respond to your bug report or contribution. + + +## Reporting Bugs/Feature Requests + +We welcome you to use the GitHub issue tracker to report bugs or suggest features. + +When filing an issue, please check [existing open](https://github.com/mauve-hedgehog/opendistro-elasticsearch-security/issues), or [recently closed](https://github.com/mauve-hedgehog/opendistro-elasticsearch-security/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +* A reproducible test case or series of steps +* The version of our code being used +* Any modifications you've made relevant to the bug +* Anything unusual about your environment or deployment + + +## Contributing via Pull Requests +Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: + +1. You are working against the latest source on the *master* branch. +2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. +3. You open an issue to discuss any significant work - we would hate for your time to be wasted. + +To send us a pull request, please: + +1. Fork the repository. +2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. +3. Ensure local tests pass. +4. Commit to your fork using clear commit messages. +5. Send us a pull request, answering any default questions in the pull request interface. +6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. + +GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and +[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + + +## Finding contributions to work on +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/mauve-hedgehog/OpenES-HealthService/labels/help%20wanted) issues is a great place to start. + + +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. + + +## Security issue notifications +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. + + +## Licensing + +See the [LICENSE](https://github.com/mauve-hedgehog/OpenES-HealthService/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. + +We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..7a4a3ea242 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. \ No newline at end of file diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000000..6064149bef --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,11 @@ +Copyright 2015-2017 floragunn GmbH + +Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed by The Apache Software +Foundation (http://www.apache.org/). + +This product includes software developed by The Legion of the Bouncy Castle Inc. +(http://www.bouncycastle.org) + +See THIRD-PARTY.txt for additional third party licenses used by this product. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000..5305631ed7 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Open Distro for Elasticsearch Security + +Open Distro for Elasticsearch Security is an Elasticsearch plugin that offers encryption, authentication, and authorization. It supports authentication via Active Directory, LDAP, Kerberos, JSON web tokens, SAML, OpenID and more. It includes fine grained role-based access control to indices, documents and fields. It also provides multi-tenancy support in Kibana. + +## Basic features + +* Full data in transit encryption +* Node-to-node encryption +* Certificate revocation lists +* Role-based cluster level access control +* Role-based index level access control +* User-, role- and permission management +* Internal user database +* HTTP basic authentication +* PKI authentication +* Proxy authentication +* User Impersonation + + +## Advance features + +opendistro-elasticsearch-security-advanced-modules adds: + +* Active Directory / LDAP +* Kerberos / SPNEGO +* JSON web token (JWT) +* OpenID +* SAML +* Document-level security +* Field-level security +* Audit logging +* Compliance logging for GDPR, HIPAA, PCI, SOX and ISO compliance +* True Kibana multi-tenancy +* REST management API + + +## Documentation + +Please refer to the [Official documentation] for detailed information on installing and configuring opendistro-elasticsearch-security plugin. + +## Quick Start + +* Install Elasticsearch + +* Install the opendistro-elasticsearch-security plugin for your Elasticsearch version 6.5.4, e.g.: + +``` +/bin/elasticsearch-plugin install \ + -b com.amazon.opendistroforelasticsearch:elasticsearch-security:0.7.0.0 +``` + +* ``cd`` into ``/plugins/opendistro_security/tools`` + +* Execute ``./install_demo_configuration.sh``, ``chmod`` the script first if necessary. This will generate all required TLS certificates and add the Security Plugin Configurationto your ``elasticsearch.yml`` file. + +* Start Elasticsearch + +* Test the installation by visiting ``https://localhost:9200``. When prompted, use admin/admin as username and password. This user has full access to the cluster. + +* Display information about the currently logged in user by visiting ``https://localhost:9200/_opendistro/_security/authinfo``. + + +## Config hot reloading + +The Security Plugin Configuration is stored in a dedicated index in Elasticsearch itself. Changes to the configuration are pushed to this index via the command line tool. This will trigger a reload of the configuration on all nodes automatically. This has several advantages over configuration via elasticsearch.yml: + +* Configuration is stored in a central place +* No configuration files on the nodes necessary +* Configuration changes do not require a restart +* Configuration changes take effect immediately + +## Support + + +## Legal +Open Distro For Elasticsearch Security +Copyright 2019- Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/THIRD-PARTY.txt b/THIRD-PARTY.txt new file mode 100644 index 0000000000..cfdd02170f --- /dev/null +++ b/THIRD-PARTY.txt @@ -0,0 +1,71 @@ + +Lists of 69 third-party dependencies. + (The Apache Software License, Version 2.0) HPPC Collections (com.carrotsearch:hppc:0.7.1 - http://labs.carrotsearch.com/hppc.html/hppc) + (The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.8.10 - https://github.com/FasterXML/jackson-core) + (The Apache Software License, Version 2.0) Jackson dataformat: CBOR (com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.8.10 - http://github.com/FasterXML/jackson-dataformats-binary) + (The Apache Software License, Version 2.0) Jackson dataformat: Smile (com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.8.10 - http://github.com/FasterXML/jackson-dataformats-binary) + (The Apache Software License, Version 2.0) Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.8.10 - https://github.com/FasterXML/jackson) + (The Apache Software License, Version 2.0) OpenDistro Security SSL (com.amazon.opendistroforelasticsearch:opendistro-elasticsearch-security-ssl:0.0.7.0 - https://github.com/mauve-hedgehog/opendistro-elasticsearch-security-ssl) + (Apache License 2.0) compiler (com.github.spullara.mustache.java:compiler:0.9.3 - http://github.com/spullara/mustache.java) + (The Apache Software License, Version 2.0) FindBugs-jsr305 (com.google.code.findbugs:jsr305:1.3.9 - http://findbugs.sourceforge.net/) + (Apache 2.0) error-prone annotations (com.google.errorprone:error_prone_annotations:2.0.18 - http://nexus.sonatype.org/oss-repository-hosting.html/error_prone_parent/error_prone_annotations) + (The Apache Software License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:23.0 - https://github.com/google/guava/guava) + (The Apache Software License, Version 2.0) J2ObjC Annotations (com.google.j2objc:j2objc-annotations:1.1 - https://github.com/google/j2objc/) + (The Apache Software License, Version 2.0) T-Digest (com.tdunning:t-digest:3.0 - https://github.com/tdunning/t-digest) + (Lesser General Public License (LGPL)) JTS Topology Suite (com.vividsolutions:jts:1.13 - http://sourceforge.net/projects/jts-topo-suite) + (Apache License, Version 2.0) Apache Commons CLI (commons-cli:commons-cli:1.3.1 - http://commons.apache.org/proper/commons-cli/) + (Apache License, Version 2.0) Apache Commons Codec (commons-codec:commons-codec:1.10 - http://commons.apache.org/proper/commons-codec/) + (Apache License, Version 2.0) Apache Commons IO (commons-io:commons-io:2.6 - http://commons.apache.org/proper/commons-io/) + (The Apache Software License, Version 2.0) Commons Logging (commons-logging:commons-logging:1.1.3 - http://commons.apache.org/proper/commons-logging/) + (Apache License, Version 2.0) Netty/Buffer (io.netty:netty-buffer:4.1.16.Final - http://netty.io/netty-buffer/) + (Apache License, Version 2.0) Netty/Codec (io.netty:netty-codec:4.1.16.Final - http://netty.io/netty-codec/) + (Apache License, Version 2.0) Netty/Codec/HTTP (io.netty:netty-codec-http:4.1.16.Final - http://netty.io/netty-codec-http/) + (Apache License, Version 2.0) Netty/Common (io.netty:netty-common:4.1.16.Final - http://netty.io/netty-common/) + (Apache License, Version 2.0) Netty/Handler (io.netty:netty-handler:4.1.16.Final - http://netty.io/netty-handler/) + (Apache License, Version 2.0) Netty/Resolver (io.netty:netty-resolver:4.1.16.Final - http://netty.io/netty-resolver/) + (Apache License, Version 2.0) Netty/TomcatNative [OpenSSL - Dynamic] (io.netty:netty-tcnative:2.0.7.Final - https://github.com/netty/netty-tcnative/netty-tcnative/) + (Apache License, Version 2.0) Netty/Transport (io.netty:netty-transport:4.1.16.Final - http://netty.io/netty-transport/) + (Apache 2) Joda-Time (joda-time:joda-time:2.9.9 - http://www.joda.org/joda-time/) + (Eclipse Public License 1.0) JUnit (junit:junit:4.12 - http://junit.org) + (The MIT License) JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.2 - http://pholser.github.io/jopt-simple) + (Apache License, Version 2.0) Apache HttpAsyncClient (org.apache.httpcomponents:httpasyncclient:4.1.2 - http://hc.apache.org/httpcomponents-asyncclient) + (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.2 - http://hc.apache.org/httpcomponents-client) + (Apache License, Version 2.0) Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.5 - http://hc.apache.org/httpcomponents-core-ga) + (Apache License, Version 2.0) Apache HttpCore NIO (org.apache.httpcomponents:httpcore-nio:4.4.5 - http://hc.apache.org/httpcomponents-core-ga) + (Apache License, Version 2.0) Apache Log4j API (org.apache.logging.log4j:log4j-api:2.9.1 - https://logging.apache.org/log4j/2.x/log4j-api/) + (Apache License, Version 2.0) Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.9.1 - https://logging.apache.org/log4j/2.x/log4j-core/) + (Apache 2) Lucene Common Analyzers (org.apache.lucene:lucene-analyzers-common:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-analyzers-common) + (Apache 2) Lucene Memory (org.apache.lucene:lucene-backward-codecs:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-backward-codecs) + (Apache 2) Lucene Core (org.apache.lucene:lucene-core:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-core) + (Apache 2) Lucene Grouping (org.apache.lucene:lucene-grouping:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-grouping) + (Apache 2) Lucene Highlighter (org.apache.lucene:lucene-highlighter:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-highlighter) + (Apache 2) Lucene Join (org.apache.lucene:lucene-join:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-join) + (Apache 2) Lucene Memory (org.apache.lucene:lucene-memory:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-memory) + (Apache 2) Lucene Miscellaneous (org.apache.lucene:lucene-misc:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-misc) + (Apache 2) Lucene Queries (org.apache.lucene:lucene-queries:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-queries) + (Apache 2) Lucene QueryParsers (org.apache.lucene:lucene-queryparser:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-queryparser) + (Apache 2) Lucene Sandbox (org.apache.lucene:lucene-sandbox:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-sandbox) + (Apache 2) Lucene Spatial (org.apache.lucene:lucene-spatial:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-spatial) + (Apache 2) Lucene Spatial Extras (org.apache.lucene:lucene-spatial-extras:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-spatial-extras) + (Apache 2) Lucene Spatial 3D (org.apache.lucene:lucene-spatial3d:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-spatial3d) + (Apache 2) Lucene Suggest (org.apache.lucene:lucene-suggest:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-suggest) + (Apache Software License, Version 1.1) (Bouncy Castle Licence) Bouncy Castle OpenPGP API (org.bouncycastle:bcpg-jdk15on:1.59 - http://www.bouncycastle.org/java.html) + (Bouncy Castle Licence) Bouncy Castle Provider (org.bouncycastle:bcprov-jdk15on:1.59 - http://www.bouncycastle.org/java.html) + (MIT license) Animal Sniffer Annotations (org.codehaus.mojo:animal-sniffer-annotations:1.14 - http://mojo.codehaus.org/animal-sniffer/animal-sniffer-annotations) + (The Apache Software License, Version 2.0) server (org.elasticsearch:elasticsearch:6.2.0 - https://github.com/elastic/elasticsearch) + (The Apache Software License, Version 2.0) cli (org.elasticsearch:elasticsearch-cli:6.2.0 - https://github.com/elastic/elasticsearch) + (The Apache Software License, Version 2.0) elasticsearch-core (org.elasticsearch:elasticsearch-core:6.2.0 - https://github.com/elastic/elasticsearch) + (The Apache Software License, Version 2.0) Elastic JNA Distribution (org.elasticsearch:jna:4.5.1 - https://github.com/java-native-access/jna) + (The Apache Software License, Version 2.0) Elasticsearch SecureSM (org.elasticsearch:securesm:1.2 - http://nexus.sonatype.org/oss-repository-hosting.html/securesm) + (The Apache Software License, Version 2.0) rest (org.elasticsearch.client:elasticsearch-rest-client:6.2.0 - https://github.com/elastic/elasticsearch) + (The Apache Software License, Version 2.0) aggs-matrix-stats (org.elasticsearch.plugin:aggs-matrix-stats-client:6.2.0 - https://github.com/elastic/elasticsearch) + (The Apache Software License, Version 2.0) lang-mustache (org.elasticsearch.plugin:lang-mustache-client:6.2.0 - https://github.com/elastic/elasticsearch) + (The Apache Software License, Version 2.0) parent-join (org.elasticsearch.plugin:parent-join-client:6.2.0 - https://github.com/elastic/elasticsearch) + (The Apache Software License, Version 2.0) percolator (org.elasticsearch.plugin:percolator-client:6.2.0 - https://github.com/elastic/elasticsearch) + (The Apache Software License, Version 2.0) reindex (org.elasticsearch.plugin:reindex-client:6.2.0 - https://github.com/elastic/elasticsearch) + (The Apache Software License, Version 2.0) transport-netty4 (org.elasticsearch.plugin:transport-netty4-client:6.2.0 - https://github.com/elastic/elasticsearch) + (New BSD License) Hamcrest All (org.hamcrest:hamcrest-all:1.3 - https://github.com/hamcrest/JavaHamcrest/hamcrest-all) + (New BSD License) Hamcrest Core (org.hamcrest:hamcrest-core:1.3 - https://github.com/hamcrest/JavaHamcrest/hamcrest-core) + (Public Domain, per Creative Commons CC0) HdrHistogram (org.hdrhistogram:HdrHistogram:2.1.9 - http://hdrhistogram.github.io/HdrHistogram/) + (The Apache Software License, Version 2.0) Spatial4J (org.locationtech.spatial4j:spatial4j:0.6 - http://www.locationtech.org/projects/locationtech.spatial4j) + (Apache License, Version 2.0) SnakeYAML (org.yaml:snakeyaml:1.17 - http://www.snakeyaml.org) diff --git a/dev/scan_veracode.sh b/dev/scan_veracode.sh new file mode 100755 index 0000000000..34b8ecf9e7 --- /dev/null +++ b/dev/scan_veracode.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +export APPID=421799 +#export SANDBOXID=537580 +set -e +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd $DIR/.. + + +if [ -z "$VERA_USER" ];then + echo "No VERA_USER set" + exit -1 +fi + +if [ -z "$VERA_PASSWORD" ];then + echo "No VERA_PASSWORD set" + exit -1 +fi + +echo "App Id: $APPID" +echo "Sandbox Id: $SANDBOXID" + +echo "Build Security ..." +mvn clean package -Pveracode -DskipTests > /dev/null 2>&1 +PLUGIN_FILE=($DIR/../target/veracode/opendistro_security*.zip) + +FILESIZE=$(wc -c <"$PLUGIN_FILE") +echo "" +echo "Upload $PLUGIN_FILE with a size of $((FILESIZE / 1048576)) mb" + + +#curl -Ss --fail --compressed -u "$VERA_USER:$VERA_PASSWORD" https://analysiscenter.veracode.com/api/5.0/getapplist.do -F "include_user_info=true" | xmllint --format - +#curl -Ss --fail --compressed -u "$VERA_USER:$VERA_PASSWORD" https://analysiscenter.veracode.com/api/5.0/getsandboxlist.do -F "app_id=$APPID" | xmllint --format - +curl -Ss --fail --compressed -u "$VERA_USER:$VERA_PASSWORD" "https://analysiscenter.veracode.com/api/5.0/uploadfile.do" \ + -F "app_id=$APPID" \ + -F "file=@$PLUGIN_FILE" \ + -F "sandbox_id=$SANDBOXID" \ + | xmllint --format - 2>&1 | tee vera.log + +echo "" +echo "Start pre scan" + +#curl -Ss --fail --compressed -u "$VERA_USER:$VERA_PASSWORD" https://analysiscenter.veracode.com/api/5.0/beginprescan.do -F "app_id=$APPID" -F "sandbox_id=$SANDBOXID" -F "auto_scan=false" | xmllint --format - +#curl -Ss --fail --compressed -u "$VERA_USER:$VERA_PASSWORD" https://analysiscenter.veracode.com/api/5.0/getprescanresults.do -F "app_id=$APPID" -F "sandbox_id=$SANDBOXID" -F "build_id=2008250" | xmllint --format - + +#curl -Ss --fail --compressed -u "$VERA_USER:$VERA_PASSWORD" "https://analysiscenter.veracode.com/api/5.0/beginscan.do" -F "app_id=$APPID" -F "sandbox_id=$SANDBOXID" -F "modules=932413446,932413464,932413518,932413454,932413453" + +curl -Ss --fail --compressed -u "$VERA_USER:$VERA_PASSWORD" https://analysiscenter.veracode.com/api/5.0/beginprescan.do -F "app_id=$APPID" -F "sandbox_id=$SANDBOXID" -F "auto_scan=true" -F "scan_all_nonfatal_top_level_modules=true" | xmllint --format - 2>&1 | tee -a vera.log + +echo "" +echo "" +echo "----- Veralog ------" +cat vera.log +echo "--------------------" +echo "" +echo "" +echo "Check for errors ..." +set +e +grep -i error vera.log && (echo "Error executing veracode"; exit -1) +grep -i denied vera.log && (echo "Access denied for veracode"; exit -1) +echo "No errors" +set -e + +#curl -Ss --fail --compressed -u "$VERA_USER:$VERA_PASSWORD" "https://analysiscenter.veracode.com/api/5.0/beginscan.do" -F "app_id=$APPID" -F "sandbox_id=$SANDBOXID" -F "scan_all_top_level_modules=true" | xmllint --format - + +#echo "Polling results" + +#curl -Ss --fail --compressed -u "$VERA_USER:$VERA_PASSWORD" https://analysiscenter.veracode.com/api/5.0/beginprescan.do -F "app_id=$APPID" -F "sandbox_id=$SANDBOXID" -F "auto_scan=true" -F "scan_all_nonfatal_top_level_modules=true" | xmllint --format - + +#curl -Ss --fail --compressed -u "$VERA_USER:$VERA_PASSWORD" https://analysiscenter.veracode.com/api/5.0/getbuildlist.do -F "app_id=$APPID" -F "sandbox_id=$SANDBOXID" | xmllint --format - +#curl --fail --compressed -k -v -u [api user] https://analysiscenter.veracode.com/api/5.0/detailedreport.do?build_id=49645c. + diff --git a/plugin-descriptor.properties b/plugin-descriptor.properties new file mode 100644 index 0000000000..cc01a35e7b --- /dev/null +++ b/plugin-descriptor.properties @@ -0,0 +1,25 @@ +# +# 'description': simple summary of the plugin +description=Provide access control related features for Elasticsearch 6 +# +# 'version': plugin's version +version=0.7.0.0 +# +# 'name': the plugin name +name=opendistro_security +# +# 'classname': the name of the class to load, fully-qualified. +classname=com.amazon.opendistroforelasticsearch.security.OpenDistroSecurityPlugin +# +# 'java.version' version of java the code is built against +# use the system property java.specification.version +# version string must be a sequence of nonnegative decimal integers +# separated by "."'s and may have leading zeros +java.version=1.8 +# +# 'elasticsearch.version' version of elasticsearch compiled against +# You will have to release a new version of the plugin for each new +# elasticsearch release. This version is checked when the plugin +# is loaded so Elasticsearch will refuse to start in the presence of +# plugins with the incorrect elasticsearch.version. +elasticsearch.version=6.5.4 diff --git a/plugin-security.policy b/plugin-security.policy new file mode 100644 index 0000000000..d011d1aa66 --- /dev/null +++ b/plugin-security.policy @@ -0,0 +1,79 @@ +/* + * Copyright 2015-2017 floragunn GmbH + * + * Licensed 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. + * + */ + + /* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + + +grant { + permission java.lang.RuntimePermission "shutdownHooks"; + permission java.lang.RuntimePermission "getClassLoader"; + permission java.lang.RuntimePermission "setContextClassLoader"; + permission javax.security.auth.AuthPermission "modifyPrivateCredentials"; + permission javax.security.auth.AuthPermission "doAs"; + permission javax.security.auth.kerberos.ServicePermission "*","accept"; + permission java.util.PropertyPermission "*","read,write"; + + //Enable when we switch to UnboundID LDAP SDK + //permission java.util.PropertyPermission "*", "read,write"; + //permission java.lang.RuntimePermission "setFactory"; + //permission javax.net.ssl.SSLPermission "setHostnameVerifier"; + + //below permissions are needed for netty native open ssl support + permission java.lang.RuntimePermission "accessClassInPackage.sun.misc"; + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; + permission java.security.SecurityPermission "getProperty.ssl.KeyManagerFactory.algorithm"; + permission java.lang.RuntimePermission "accessDeclaredMembers"; + permission java.lang.RuntimePermission "accessClassInPackage.sun.security.x509"; + permission java.lang.RuntimePermission "accessClassInPackage.sun.nio.ch"; + permission java.io.FilePermission "/proc/sys/net/core/somaxconn","read"; + + permission java.security.SecurityPermission "setProperty.ocsp.enable"; + + permission java.net.NetPermission "getNetworkInformation"; + permission java.net.NetPermission "getProxySelector"; + permission java.net.SocketPermission "*", "connect,accept,resolve"; + + permission java.security.SecurityPermission "putProviderProperty.BC"; + permission java.security.SecurityPermission "insertProvider.BC"; + + permission java.lang.RuntimePermission "accessUserInformation"; + + permission java.security.SecurityPermission "org.apache.xml.security.register"; + permission java.util.PropertyPermission "org.apache.xml.security.ignoreLineBreaks", "write"; + + permission java.lang.RuntimePermission "createClassLoader"; + + //Java 9+ + permission java.lang.RuntimePermission "accessClassInPackage.com.sun.jndi.ldap"; +}; + +grant codeBase "${codebase.netty-common}" { + permission java.lang.RuntimePermission "loadLibrary.*"; +}; \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000..6e69e28793 --- /dev/null +++ b/pom.xml @@ -0,0 +1,436 @@ + + + + + + 4.0.0 + + + com.amazon.opendistroforelasticsearch + opendistro_security_parent + 0.7.0.0 + + + opendistro_security + jar + 0.7.0.0 + Open Distro Security for Elasticsearch + Provide access control related features for Elasticsearch 6 + https://github.com/mauve-hedgehog/opendistro-elasticsearch-security + 2015 + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 0.7.0.0 + 0.7.0.0 + 6.5.4 + + + 2.0.15.Final + 1.60 + 2.11.1 + 25.1-jre + 1.3.1 + + + ${basedir}/src/main/assemblies/plugin.xml + ${basedir}/src/main/assemblies/securityadmin-standalone.xml + ${basedir}/src/main/assemblies/veracode.xml + + + 1.10.19 + + + + https://github.com/mauve-hedgehog/opendistro-elasticsearch-security + scm:git:git@github.com:mauve-hedgehog/opendistro-elasticsearch-security.git + scm:git:git@github.com:mauve-hedgehog/opendistro-elasticsearch-security.git + 0.7.0.0 + + + + GitHub + https://github.com/mauve-hedgehog/opendistro-elasticsearch-security/issues + + + + + + com.amazon.opendistroforelasticsearch + opendistro_security_ssl + ${opendistro_security_ssl.version} + + + + + org.elasticsearch.plugin + transport-netty4-client + ${elasticsearch.version} + + + jna + org.elasticsearch + + + jts + com.vividsolutions + + + log4j-api + org.apache.logging.log4j + + + spatial4j + org.locationtech.spatial4j + + + + + + + com.google.guava + guava + ${guava.version} + + + + + commons-cli + commons-cli + ${commons.cli.version} + + + + + org.bouncycastle + bcpg-jdk15on + ${bc.version} + + + + + org.elasticsearch + elasticsearch + ${elasticsearch.version} + provided + + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + provided + + + + + + commons-io + commons-io + test + + + + org.hamcrest + hamcrest-all + test + + + + junit + junit + test + + + + io.netty + netty-tcnative + ${netty-native.version} + ${os.detected.classifier} + test + + + + org.elasticsearch.plugin + reindex-client + ${elasticsearch.version} + test + + + + org.elasticsearch.plugin + percolator-client + ${elasticsearch.version} + test + + + + org.elasticsearch.plugin + lang-mustache-client + ${elasticsearch.version} + test + + + + org.elasticsearch.plugin + parent-join-client + ${elasticsearch.version} + test + + + + org.elasticsearch.plugin + aggs-matrix-stats-client + ${elasticsearch.version} + test + + + + org.mockito + mockito-all + ${mockito.version} + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + 3 + true + + + fork_${surefire.forkNumber} + + + + **/*.java + + + + + + + com.gkatzioura.maven.cloud + s3-storage-wagon + 1.8 + + + + + + advanced + + + + org.apache.maven.plugins + maven-assembly-plugin + + + plugin + package + + false + posix + ${project.build.directory}/releases/ + + ${elasticsearch.assembly.descriptor} + + + + single + + + + securityadmin + package + + true + posix + ${project.build.directory}/releases/ + + ${securitystandalone.descriptor} + + + + single + + + + + + com.floragunn + checksum-maven-plugin + 1.7.1 + + + + files + + package + + + + + + ${project.build.directory}/releases/ + + *.zip + + + *securityadmin* + + + + + SHA-512 + + true + true + true + false + + + + + + + com.amazon.opendistroforelasticsearch + opendistro_security_advanced_modules + ${opendistro_security_advanced_modules.version} + + + jna + org.elasticsearch + + + jts + com.vividsolutions + + + log4j-api + org.apache.logging.log4j + + + spatial4j + org.locationtech.spatial4j + + + + + org.elasticsearch.plugin + lang-mustache-client + ${elasticsearch.version} + + + + org.elasticsearch.plugin + parent-join-client + ${elasticsearch.version} + + + + org.elasticsearch.plugin + aggs-matrix-stats-client + ${elasticsearch.version} + + + + + veracode + + + + org.apache.maven.plugins + maven-assembly-plugin + + + veracode + package + + true + posix + ${project.build.directory}/veracode/ + + ${veracode.descriptor} + + + + single + + + + + + + + + com.amazon.opendistroforelasticsearch + opendistro_security_advanced_modules + ${opendistro_security_advanced_modules.version} + + + + io.netty + netty-tcnative + ${netty-native.version} + linux-x86_64 + + + org.conscrypt + conscrypt-openjdk-uber + 1.0.0.RC9 + + + + + diff --git a/securityconfig/action_groups.yml b/securityconfig/action_groups.yml new file mode 100644 index 0000000000..457bb0b793 --- /dev/null +++ b/securityconfig/action_groups.yml @@ -0,0 +1,137 @@ +UNLIMITED: + readonly: true + permissions: + - "*" + +###### INDEX LEVEL ###### + +INDICES_ALL: + readonly: true + permissions: + - "indices:*" + +# for backward compatibility +ALL: + readonly: true + permissions: + - INDICES_ALL + +MANAGE: + readonly: true + permissions: + - "indices:monitor/*" + - "indices:admin/*" + +CREATE_INDEX: + readonly: true + permissions: + - "indices:admin/create" + - "indices:admin/mapping/put" + +MANAGE_ALIASES: + readonly: true + permissions: + - "indices:admin/aliases*" + +# for backward compatibility +MONITOR: + readonly: true + permissions: + - INDICES_MONITOR + +INDICES_MONITOR: + readonly: true + permissions: + - "indices:monitor/*" + +DATA_ACCESS: + readonly: true + permissions: + - "indices:data/*" + - CRUD + +WRITE: + readonly: true + permissions: + - "indices:data/write*" + - "indices:admin/mapping/put" + +READ: + readonly: true + permissions: + - "indices:data/read*" + - "indices:admin/mappings/fields/get*" + +DELETE: + readonly: true + permissions: + - "indices:data/write/delete*" + +CRUD: + readonly: true + permissions: + - READ + - WRITE + +SEARCH: + readonly: true + permissions: + - "indices:data/read/search*" + - "indices:data/read/msearch*" + - SUGGEST + +SUGGEST: + readonly: true + permissions: + - "indices:data/read/suggest*" + +INDEX: + readonly: true + permissions: + - "indices:data/write/index*" + - "indices:data/write/update*" + - "indices:admin/mapping/put" + - "indices:data/write/bulk*" + +GET: + readonly: true + permissions: + - "indices:data/read/get*" + - "indices:data/read/mget*" + +###### CLUSTER LEVEL ###### + +CLUSTER_ALL: + readonly: true + permissions: + - "cluster:*" + +CLUSTER_MONITOR: + readonly: true + permissions: + - "cluster:monitor/*" + +CLUSTER_COMPOSITE_OPS_RO: + readonly: true + permissions: + - "indices:data/read/mget" + - "indices:data/read/msearch" + - "indices:data/read/mtv" + - "indices:data/read/coordinate-msearch*" + - "indices:admin/aliases/exists*" + - "indices:admin/aliases/get*" + - "indices:data/read/scroll" + +CLUSTER_COMPOSITE_OPS: + readonly: true + permissions: + - "indices:data/write/bulk" + - "indices:admin/aliases*" + - "indices:data/write/reindex" + - CLUSTER_COMPOSITE_OPS_RO + +MANAGE_SNAPSHOTS: + readonly: true + permissions: + - "cluster:admin/snapshot/*" + - "cluster:admin/repository/*" diff --git a/securityconfig/config.yml b/securityconfig/config.yml new file mode 100644 index 0000000000..ba8ebd2fd8 --- /dev/null +++ b/securityconfig/config.yml @@ -0,0 +1,221 @@ +# This is the main Open Distro Security configuration file where authentication +# and authorization is defined. +# +# You need to configure at least one authentication domain in the authc of this file. +# An authentication domain is responsible for extracting the user credentials from +# the request and for validating them against an authentication backend like Active Directory for example. +# +# If more than one authentication domain is configured the first one which succeeds wins. +# If all authentication domains fail then the request is unauthenticated. +# In this case an exception is thrown and/or the HTTP status is set to 401. +# +# After authentication authorization (authz) will be applied. There can be zero or more authorizers which collect +# the roles from a given backend for the authenticated user. +# +# Both, authc and auth can be enabled/disabled separately for REST and TRANSPORT layer. Default is true for both. +# http_enabled: true +# transport_enabled: true +# +# 5.x Migration: "enabled: true/false" will also be respected currently but only to provide backward compatibility. +# +# For HTTP it is possible to allow anonymous authentication. If that is the case then the HTTP authenticators try to +# find user credentials in the HTTP request. If credentials are found then the user gets regularly authenticated. +# If none can be found the user will be authenticated as an "anonymous" user. This user has always the username "anonymous" +# and one role named "anonymous_backendrole". +# If you enable anonymous authentication all HTTP authenticators will not challenge. +# +# +# Note: If you define more than one HTTP authenticators make sure to put non-challenging authenticators like "proxy" or "clientcert" +# first and the challenging one last. +# Because it's not possible to challenge a client with two different authentication methods (for example +# Kerberos and Basic) only one can have the challenge flag set to true. You can cope with this situation +# by using pre-authentication, e.g. sending a HTTP Basic authentication header in the request. +# +# Default value of the challenge flag is true. +# +# +# HTTP +# basic (challenging) +# proxy (not challenging, needs xff) +# kerberos (challenging) +# clientcert (not challenging, needs https) +# jwt (not challenging) +# host (not challenging) #DEPRECATED, will be removed in a future version. +# host based authentication is configurable in roles_mapping + +# Authc +# internal +# noop +# ldap + +# Authz +# ldap +# noop + +opendistro_security: + dynamic: + # Set filtered_alias_mode to 'disallow' to forbid more than 2 filtered aliases per index + # Set filtered_alias_mode to 'warn' to allow more than 2 filtered aliases per index but warns about it (default) + # Set filtered_alias_mode to 'nowarn' to allow more than 2 filtered aliases per index silently + #filtered_alias_mode: warn + #kibana: + # Kibana multitenancy - + # see + # To make this work you need to install + #multitenancy_enabled: true + #server_username: kibanaserver + #index: '.kibana' + #do_not_fail_on_forbidden: false + http: + anonymous_auth_enabled: false + xff: + enabled: false + internalProxies: '192\.168\.0\.10|192\.168\.0\.11' # regex pattern + #internalProxies: '.*' # trust all internal proxies, regex pattern + remoteIpHeader: 'x-forwarded-for' + proxiesHeader: 'x-forwarded-by' + #trustedProxies: '.*' # trust all external proxies, regex pattern + ###### see https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html for regex help + ###### more information about XFF https://en.wikipedia.org/wiki/X-Forwarded-For + ###### and here https://tools.ietf.org/html/rfc7239 + ###### and https://tomcat.apache.org/tomcat-8.0-doc/config/valve.html#Remote_IP_Valve + authc: + kerberos_auth_domain: + http_enabled: false + transport_enabled: false + order: 6 + http_authenticator: + type: kerberos + challenge: true + config: + # If true a lot of kerberos/security related debugging output will be logged to standard out + krb_debug: false + # If true then the realm will be stripped from the user name + strip_realm_from_principal: true + authentication_backend: + type: noop + basic_internal_auth_domain: + http_enabled: true + transport_enabled: true + order: 4 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + proxy_auth_domain: + http_enabled: false + transport_enabled: false + order: 3 + http_authenticator: + type: proxy + challenge: false + config: + user_header: "x-proxy-user" + roles_header: "x-proxy-roles" + authentication_backend: + type: noop + jwt_auth_domain: + http_enabled: false + transport_enabled: false + order: 0 + http_authenticator: + type: jwt + challenge: false + config: + signing_key: "base64 encoded HMAC key or public RSA/ECDSA pem key" + jwt_header: "Authorization" + jwt_url_parameter: null + roles_key: null + subject_key: null + authentication_backend: + type: noop + clientcert_auth_domain: + http_enabled: false + transport_enabled: false + order: 2 + http_authenticator: + type: clientcert + config: + username_attribute: cn #optional, if omitted DN becomes username + challenge: false + authentication_backend: + type: noop + ldap: + http_enabled: false + transport_enabled: false + order: 5 + http_authenticator: + type: basic + challenge: false + authentication_backend: + # LDAP authentication backend (authenticate users against a LDAP or Active Directory) + type: ldap + config: + # enable ldaps + enable_ssl: false + # enable start tls, enable_ssl should be false + enable_start_tls: false + # send client certificate + enable_ssl_client_auth: false + # verify ldap hostname + verify_hostnames: true + hosts: + - localhost:8389 + bind_dn: null + password: null + userbase: 'ou=people,dc=example,dc=com' + # Filter to search for users (currently in the whole subtree beneath userbase) + # {0} is substituted with the username + usersearch: '(sAMAccountName={0})' + # Use this attribute from the user as username (if not set then DN is used) + username_attribute: null + authz: + roles_from_myldap: + http_enabled: false + transport_enabled: false + authorization_backend: + # LDAP authorization backend (gather roles from a LDAP or Active Directory, you have to configure the above LDAP authentication backend settings too) + type: ldap + config: + # enable ldaps + enable_ssl: false + # enable start tls, enable_ssl should be false + enable_start_tls: false + # send client certificate + enable_ssl_client_auth: false + # verify ldap hostname + verify_hostnames: true + hosts: + - localhost:8389 + bind_dn: null + password: null + rolebase: 'ou=groups,dc=example,dc=com' + # Filter to search for roles (currently in the whole subtree beneath rolebase) + # {0} is substituted with the DN of the user + # {1} is substituted with the username + # {2} is substituted with an attribute value from user's directory entry, of the authenticated user. Use userroleattribute to specify the name of the attribute + rolesearch: '(member={0})' + # Specify the name of the attribute which value should be substituted with {2} above + userroleattribute: null + # Roles as an attribute of the user entry + userrolename: disabled + #userrolename: memberOf + # The attribute in a role entry containing the name of that role, Default is "name". + # Can also be "dn" to use the full DN as rolename. + rolename: cn + # Resolve nested roles transitive (roles which are members of other roles and so on ...) + resolve_nested_roles: true + userbase: 'ou=people,dc=example,dc=com' + # Filter to search for users (currently in the whole subtree beneath userbase) + # {0} is substituted with the username + usersearch: '(uid={0})' + # Skip users matching a user name, a wildcard or a regex pattern + #skip_users: + # - 'cn=Michael Jackson,ou*people,o=TEST' + # - '/\S*/' + roles_from_another_ldap: + enabled: false + authorization_backend: + type: ldap + #config goes here ... \ No newline at end of file diff --git a/securityconfig/elasticsearch.yml.example b/securityconfig/elasticsearch.yml.example new file mode 100644 index 0000000000..5a3d213407 --- /dev/null +++ b/securityconfig/elasticsearch.yml.example @@ -0,0 +1,199 @@ +############## Open Distro Security configuration ############### + +########################################################### +# Add the following settings to your standard elasticsearch.yml +# alongside with the Open Distro Security TLS settings. +# Settings must always be the same on all nodes in the cluster. + +############## Common configuration settings ############## + +# Enable or disable the Open Distro Security enterprise modules +# By default enterprise modules are enabled. If you use any of the modules in production you need +# to obtain a license. If you want to use the free Community Edition, you can switch +# all enterprise features off by setting the following key to false +opendistro_security.enterprise_modules_enabled: true + +# Specify a list of DNs which denote the other nodes in the cluster. +# This settings support wildcards and regular expressions +# This setting only has effect if 'opendistro_security.cert.intercluster_request_evaluator_class' is not set. +opendistro_security.nodes_dn: + - "CN=*.example.com, OU=SSL, O=Test, L=Test, C=DE" + - "CN=node.other.com, OU=SSL, O=Test, L=Test, C=DE" + +# Defines the DNs (distinguished names) of certificates +# to which admin privileges should be assigned (mandatory) +opendistro_security.authcz.admin_dn: + - "CN=kirk,OU=client,O=client,l=tEst, C=De" + +# Define how backend roles should be mapped to Open Distro Security roles +# MAPPING_ONLY - mappings must be configured explicitely in roles_mapping.yml (default) +# BACKENDROLES_ONLY - backend roles are mapped to Open Distro Security rules directly. Settings in roles_mapping.yml have no effect. +# BOTH - backend roles are mapped to Open Distro Security roles mapped directly and via roles_mapping.yml in addition +opendistro_security.roles_mapping_resolution: MAPPING_ONLY + +############## REST Management API configuration settings ############## +# Enable or disable role based access to the REST management API +# Default is that no role is allowed to access the REST management API. +#opendistro_security.restapi.roles_enabled: ["all_access","xyz_role"] + +# Disable particular endpoints and their HTTP methods for roles. +# By default all endpoints/methods are allowed. +#opendistro_security.restapi.endpoints_disabled..: +# Example: +#opendistro_security.restapi.endpoints_disabled.all_access.ACTIONGROUPS: ["PUT","POST","DELETE"] +#opendistro_security.restapi.endpoints_disabled.xyz_role.LICENSE: ["DELETE"] + +# The following endpoints exist: +# ACTIONGROUPS +# CACHE +# CONFIG +# ROLES +# ROLESMAPPING +# INTERNALUSERS +# SYSTEMINFO +# PERMISSIONSINFO + +############## Auditlog configuration settings ############## +# General settings + +# Enable/disable rest request logging (default: true) +#opendistro_security.audit.enable_rest: true +# Enable/disable transport request logging (default: false) +#opendistro_security.audit.enable_transport: false +# Enable/disable bulk request logging (default: false) +# If enabled all subrequests in bulk requests will be logged too +#opendistro_security.audit.resolve_bulk_requests: false +# Disable some categories +#opendistro_security.audit.config.disabled_categories: ["AUTHENTICATED","GRANTED_PRIVILEGES"] +# Disable some requests (wildcard or regex of actions or rest request paths) +#opendistro_security.audit.ignore_requests: ["indices:data/read/*","*_bulk"] +# Tune threadpool size, default is 10 and 0 means disabled +#opendistro_security.audit.threadpool.size: 0 +# Tune threadpool max size queue length, default is 100000 +#opendistro_security.audit.threadpool.max_queue_len: 100000 + +# If enable_request_details is true then the audit log event will also contain +# details like the search query. Default is false. +#opendistro_security.audit.enable_request_details: true +# Ignore users, e.g. do not log audit requests from that users (default: no ignored users) +#opendistro_security.audit.ignore_users: ['kibanaserver','some*user','/also.*regex possible/']" + +# Destination of the auditlog events +opendistro_security.audit.type: internal_elasticsearch +#opendistro_security.audit.type: external_elasticsearch +#opendistro_security.audit.type: debug +#opendistro_security.audit.type: webhook + +# external_elasticsearch settings +#opendistro_security.audit.config.http_endpoints: ['localhost:9200','localhost:9201','localhost:9202']" +# Auditlog index can be a static one or one with a date pattern (default is 'auditlog6') +#opendistro_security.audit.config.index: auditlog6 # make sure you secure this index properly +#opendistro_security.audit.config.index: "'auditlog6-'YYYY.MM.dd" #rotates index daily - make sure you secure this index properly +#opendistro_security.audit.config.type: auditlog +#opendistro_security.audit.config.username: auditloguser +#opendistro_security.audit.config.password: auditlogpassword +#opendistro_security.audit.config.enable_ssl: false +#opendistro_security.audit.config.verify_hostnames: false +#opendistro_security.audit.config.enable_ssl_client_auth: false +#opendistro_security.audit.config.cert_alias: mycert +#opendistro_security.audit.config.pemkey_filepath: key.pem +#opendistro_security.audit.config.pemkey_content: <...pem base 64 content> +#opendistro_security.audit.config.pemkey_password: secret +#opendistro_security.audit.config.pemcert_filepath: cert.pem +#opendistro_security.audit.config.pemcert_content: <...pem base 64 content> +#opendistro_security.audit.config.pemtrustedcas_filepath: ca.pem +#opendistro_security.audit.config.pemtrustedcas_content: <...pem base 64 content> + +# webhook settings +#opendistro_security.audit.config.webhook.url: "http://mywebhook/endpoint" +# One of URL_PARAMETER_GET,URL_PARAMETER_POST,TEXT,JSON,SLACK +#opendistro_security.audit.config.webhook.format: JSON +#opendistro_security.audit.config.webhook.ssl.verify: false +#opendistro_security.audit.config.webhook.ssl.pemtrustedcas_filepath: ca.pem +#opendistro_security.audit.config.webhook.ssl.pemtrustedcas_content: <...pem base 64 content> + +# log4j settings +#opendistro_security.audit.config.log4j.logger_name: auditlogger +#opendistro_security.audit.config.log4j.level: INFO + +############## Kerberos configuration settings ############## +# If Kerberos authentication should be used you have to configure: + +# The Path to the krb5.conf file +# Can be absolute or relative to the Elasticsearch config directory +#opendistro_security.kerberos.krb5_filepath: '/etc/krb5.conf' + +# The Path to the keytab where the acceptor_principal credentials are stored. +# Must be relative to the Elasticsearch config directory +#opendistro_security.kerberos.acceptor_keytab_filepath: 'eskeytab.tab' + +# Acceptor (Server) Principal name, must be present in acceptor_keytab_path file +#opendistro_security.kerberos.acceptor_principal: 'HTTP/localhost' + +############## Advanced configuration settings ############## +# Enable transport layer impersonation +# Allow DNs (distinguished names) to impersonate as other users +#opendistro_security.authcz.impersonation_dn: +# "CN=spock,OU=client,O=client,L=Test,C=DE": +# - worf +# "cn=webuser,ou=IT,ou=IT,dc=company,dc=com": +# - user2 +# - user1 + +# Enable rest layer impersonation +# Allow users to impersonate as other users +#opendistro_security.authcz.rest_impersonation_user: +# "picard": +# - worf +# "john": +# - steve +# - martin + +# If this is set to true Open Distro Security will automatically initialize the configuration index +# with the files in the config directory if the index does not exist. +# WARNING: This will use well-known default passwords. +# Use only in a private network/environment. +#opendistro_security.allow_default_init_opendistrosecurityindex: false + +# If this is set to true then allow to startup with demo certificates. +# These are certificates issued by floragunn GmbH for demo purposes. +# WARNING: This certificates are well known and therefore unsafe +# Use only in a private network/environment. +#opendistro_security.allow_unsafe_democertificates: false + +############## Expert settings ############## +# WARNING: Expert settings, do only use if you know what you are doing +# If you set wrong values here this this could be a security risk +# or make Open Distro Security stop working + +# Name of the index where .opendistro_security stores its configuration. + +#opendistro_security.config_index_name: .opendistro_security + +# This defines the OID of server node certificates +#opendistro_security.cert.oid: '1.2.3.4.5.5' + +# This specifies the implementation of com.amazon.opendistroforelasticsearch.security.transport.InterClusterRequestEvaluator +# that is used to determine inter-cluster request. +# Instances of com.amazon.opendistroforelasticsearch.security.transport.InterClusterRequestEvaluator must implement a single argument +# constructor that takes an org.elasticsearch.common.settings.Settings +#opendistro_security.cert.intercluster_request_evaluator_class: com.amazon.opendistroforelasticsearch.security.transport.DefaultInterClusterRequestEvaluator + +# Allow snapshot restore for normal users +# By default only requests signed by an admin TLS certificate can do this +# To enable snapshot restore for normal users set 'opendistro_security.enable_snapshot_restore_privilege: true' +# The user who wants to restore a snapshot must have the 'cluster:admin/snapshot/restore' privilege and must also have +# "indices:admin/create" and "indices:data/write/index" for the indices to be restores. +# A snapshot can only be restored when it does not contain global state and does not restore the '.opendistro_security' index +# If 'opendistro_security.check_snapshot_restore_write_privileges: false' is set then the additional indices checks are omitted. + +# This makes it less secure. +#opendistro_security.enable_snapshot_restore_privilege: true +#opendistro_security.check_snapshot_restore_write_privileges: false + +# Authentication cache timeout in minutes (A value of 0 disables caching, default is 60) +#opendistro_security.cache.ttl_minutes: 60 + +# Disable Open Distro Security +# WARNING: This can expose your configuration (including passwords) to the public. +#opendistro_security.disabled: false diff --git a/securityconfig/internal_users.yml b/securityconfig/internal_users.yml new file mode 100644 index 0000000000..1712d3792d --- /dev/null +++ b/securityconfig/internal_users.yml @@ -0,0 +1,45 @@ +# This is the internal user database +# The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh + +#password is: admin +admin: + readonly: true + hash: $2a$12$VcCDgh2NDk07JGN0rjGbM.Ad41qVR/YFJcgHp0UGns5JDymv..TOG + roles: + - admin + attributes: + #no dots allowed in attribute names + attribute1: value1 + attribute2: value2 + attribute3: value3 + +#password is: logstash +logstash: + hash: $2a$12$u1ShR4l4uBS3Uv59Pa2y5.1uQuZBrZtmNfqB3iM/.jL0XoV9sghS2 + roles: + - logstash + +#password is: kibanaserver +kibanaserver: + readonly: true + hash: $2a$12$4AcgAt3xwOWadA5s5blL6ev39OXDNhmOesEoo33eZtrq2N0YrU3H. + +#password is: kibanaro +kibanaro: + hash: $2a$12$JJSXNfTowz7Uu5ttXfeYpeYE0arACvcwlPBStB1F.MI7f0U9Z4DGC + roles: + - kibanauser + - readall + +#password is: readall +readall: + hash: $2a$12$ae4ycwzwvLtZxwZ82RmiEunBbIPiAmGZduBAjKN0TXdwQFtCwARz2 + #password is: readall + roles: + - readall + +#password is: snapshotrestore +snapshotrestore: + hash: $2y$12$DpwmetHKwgYnorbgdvORCenv4NAK8cPUg8AI6pxLCuWf/ALc0.v7W + roles: + - snapshotrestore \ No newline at end of file diff --git a/securityconfig/roles.yml b/securityconfig/roles.yml new file mode 100644 index 0000000000..b42ad22de7 --- /dev/null +++ b/securityconfig/roles.yml @@ -0,0 +1,183 @@ +#: +# cluster: +# - '' +# indices: +# '': +# '': +# - '' +# _dls_: '' +# _fls_: +# - '' +# - '' + +# When a user make a request to Elasticsearch then the following roles will be evaluated to see if the user has +# permissions for the request. A request is always associated with an action and is executed against and index (or alias) +# and a type. If a request is executed against all indices (or all types) then the asterix ('*') is needed. +# Every role a user has will be examined if it allows the action against an index (or type). At least one role must match +# for the request to be successful. If no role match then the request will be denied. Currently a match must happen within +# one single role - that means that permissions can not span multiple roles. + +# For , and simple wildcards and regular expressions are possible. +# A asterix (*) will match any character sequence (or an empty sequence) +# A question mark (?) will match any single character (but NOT empty character) +# Example: '*my*index' will match 'my_first_index' as well as 'myindex' but not 'myindex1' +# Example: '?kibana' will match '.kibana' but not 'kibana' + +# To use a full blown regex you have to pre- and apend a '/' to use regex instead of simple wildcards +# '//' +# Example: '/\S*/' will match any non whitespace characters + +# Important: +# Index, alias or type names can not contain dots (.) in the or expression. +# Reason is that we currently parse the config file into a elasticsearch settings object which cannot cope with dots in keys. +# Workaround: Just configure something like '?kibana' instead of '.kibana' or 'my?index' instead of 'my.index' +# This limitation will likely removed with next version of Open Distro Security + +# Allows everything, but no changes to .opendistro_security configuration index +all_access: + readonly: true + cluster: + - UNLIMITED + indices: + '*': + '*': + - UNLIMITED + tenants: + admin_tenant: RW + +# Read all, but no write permissions +readall: + readonly: true + cluster: + - CLUSTER_COMPOSITE_OPS_RO + indices: + '*': + '*': + - READ + +# Read all and monitor, but no write permissions +readall_and_monitor: + cluster: + - CLUSTER_MONITOR + - CLUSTER_COMPOSITE_OPS_RO + indices: + '*': + '*': + - READ + +# For users which use kibana, access to indices must be granted separately +kibana_user: + readonly: true + cluster: + - INDICES_MONITOR + - CLUSTER_COMPOSITE_OPS + indices: + '?kibana': + '*': + - MANAGE + - INDEX + - READ + - DELETE + '?kibana-6': + '*': + - MANAGE + - INDEX + - READ + - DELETE + '?kibana_*': + '*': + - MANAGE + - INDEX + - READ + - DELETE + '?tasks': + '*': + - INDICES_ALL + '?management-beats': + '*': + - INDICES_ALL + '*': + '*': + - indices:data/read/field_caps* + - indices:data/read/xpack/rollup* + - indices:admin/mappings/get* + - indices:admin/get + +# For the kibana server +kibana_server: + readonly: true + cluster: + - CLUSTER_MONITOR + - CLUSTER_COMPOSITE_OPS + - cluster:admin/xpack/monitoring* + - indices:admin/template* + - indices:data/read/scroll* + indices: + '?kibana': + '*': + - INDICES_ALL + '?kibana-6': + '*': + - INDICES_ALL + '?kibana_*': + '*': + - INDICES_ALL + '?reporting*': + '*': + - INDICES_ALL + '?monitoring*': + '*': + - INDICES_ALL + '?tasks': + '*': + - INDICES_ALL + '?management-beats*': + '*': + - INDICES_ALL + '*': + '*': + - "indices:admin/aliases*" + +# For logstash and beats +logstash: + cluster: + - CLUSTER_MONITOR + - CLUSTER_COMPOSITE_OPS + - indices:admin/template/get + - indices:admin/template/put + indices: + 'logstash-*': + '*': + - CRUD + - CREATE_INDEX + '*beat*': + '*': + - CRUD + - CREATE_INDEX + +# Allows adding and modifying repositories and creating and restoring snapshots +manage_snapshots: + cluster: + - MANAGE_SNAPSHOTS + indices: + '*': + '*': + - "indices:data/write/index" + - "indices:admin/create" + +# Allows user to access security rest apis programatically +security_rest_api_access: + readonly: true + +# Restrict users so they can only view visualization and dashboard on kibana +kibana_read_only: + readonly: true + +# Allows each user to access own named index +own_index: + cluster: + - CLUSTER_COMPOSITE_OPS + indices: + '${user_name}': + '*': + - INDICES_ALL diff --git a/securityconfig/roles_mapping.yml b/securityconfig/roles_mapping.yml new file mode 100644 index 0000000000..e460d9c3b7 --- /dev/null +++ b/securityconfig/roles_mapping.yml @@ -0,0 +1,34 @@ +# In this file users, backendroles and hosts can be mapped to Open Distro Security roles. +# Permissions for Open Distro Security roles are configured in roles.yml + +all_access: + readonly: true + backendroles: + - admin + +logstash: + backendroles: + - logstash + +kibana_server: + readonly: true + users: + - kibanaserver + +kibana_user: + backendroles: + - kibanauser + +readall: + readonly: true + backendroles: + - readall + +manage_snapshots: + readonly: true + backendroles: + - snapshotrestore + +own_index: + users: + - '*' \ No newline at end of file diff --git a/settings.xml b/settings.xml new file mode 100644 index 0000000000..b13d45626f --- /dev/null +++ b/settings.xml @@ -0,0 +1,10 @@ + + + + + ossrh-fg + ${env.SONATYPE_USER} + ${env.SONATYPE_PASSWORD} + + + diff --git a/src/main/assemblies/plugin.xml b/src/main/assemblies/plugin.xml new file mode 100644 index 0000000000..48f0e69ce1 --- /dev/null +++ b/src/main/assemblies/plugin.xml @@ -0,0 +1,43 @@ + + + plugin + + zip + + false + + + true + ${file.separator} + true + true + + + + + ${basedir}/src/main/assemblies/ + ${file.separator} + false + + LICENSE + + + + ${project.basedir} + ${file.separator} + + tools/** + securityconfig/** + + 0755 + + + ${file.separator} + + plugin-descriptor.properties + plugin-security.policy + + + + + diff --git a/src/main/assemblies/securityadmin-standalone.xml b/src/main/assemblies/securityadmin-standalone.xml new file mode 100644 index 0000000000..256c9edd82 --- /dev/null +++ b/src/main/assemblies/securityadmin-standalone.xml @@ -0,0 +1,43 @@ + + + securityadmin-standalone + + zip + tar.gz + + false + + + ${project.basedir} + ${file.separator} + + tools/** + + 0755 + + + ${project.basedir} + ${file.separator}deps + + securityconfig/** + + 0755 + + + + + ${file.separator}deps + true + false + provided + + + ${file.separator}deps + true + false + + com.amazon.opendistroforelasticsearch:dlic* + + + + \ No newline at end of file diff --git a/src/main/assemblies/veracode.xml b/src/main/assemblies/veracode.xml new file mode 100644 index 0000000000..5f30a832d2 --- /dev/null +++ b/src/main/assemblies/veracode.xml @@ -0,0 +1,24 @@ + + + veracode + + zip + + false + + + true + ${file.separator}elasticsearch${file.separator} + true + true + compile + + org.elasticsearch:jna + org.apache.lucene:lucene-backward-codecs + org.apache.logging.log4j:log4j-core + *:*:*:test*:* + + + + + diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/OpenDistroSecurityPlugin.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/OpenDistroSecurityPlugin.java new file mode 100644 index 0000000000..d78bc0c9c9 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/OpenDistroSecurityPlugin.java @@ -0,0 +1,1111 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.security.AccessController; +import java.security.MessageDigest; +import java.security.PrivilegedAction; +import java.security.Security; +import java.util.ArrayList; +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.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.lucene.search.QueryCachingPolicy; +import org.apache.lucene.search.Weight; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.SpecialPermission; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.search.SearchScrollAction; +import org.elasticsearch.action.support.ActionFilter; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.component.Lifecycle.State; +import org.elasticsearch.common.component.LifecycleComponent; +import org.elasticsearch.common.component.LifecycleListener; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Property; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.NodeEnvironment; +import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.http.HttpServerTransport.Dispatcher; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexModule; +import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.cache.query.QueryCache; +import org.elasticsearch.index.shard.IndexSearcherWrapper; +import org.elasticsearch.index.shard.SearchOperationListener; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.plugins.ClusterPlugin; +import org.elasticsearch.plugins.MapperPlugin; +import org.elasticsearch.repositories.RepositoriesService; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestHandler; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.search.internal.ScrollContext; +import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.RemoteClusterService; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.transport.Transport.Connection; +import org.elasticsearch.transport.TransportChannel; +import org.elasticsearch.transport.TransportInterceptor; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportRequestHandler; +import org.elasticsearch.transport.TransportRequestOptions; +import org.elasticsearch.transport.TransportResponse; +import org.elasticsearch.transport.TransportResponseHandler; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.watcher.ResourceWatcherService; + +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateAction; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.TransportConfigUpdateAction; +import com.amazon.opendistroforelasticsearch.security.action.whoami.TransportWhoAmIAction; +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIAction; +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLogSslExceptionHandler; +import com.amazon.opendistroforelasticsearch.security.auditlog.NullAuditLog; +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog.Origin; +import com.amazon.opendistroforelasticsearch.security.auth.BackendRegistry; +import com.amazon.opendistroforelasticsearch.security.auth.internal.InternalAuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.compliance.ComplianceConfig; +import com.amazon.opendistroforelasticsearch.security.compliance.ComplianceIndexingOperationListener; +import com.amazon.opendistroforelasticsearch.security.configuration.ActionGroupHolder; +import com.amazon.opendistroforelasticsearch.security.configuration.AdminDNs; +import com.amazon.opendistroforelasticsearch.security.configuration.ClusterInfoHolder; +import com.amazon.opendistroforelasticsearch.security.configuration.CompatConfig; +import com.amazon.opendistroforelasticsearch.security.configuration.ConfigurationChangeListener; +import com.amazon.opendistroforelasticsearch.security.configuration.DlsFlsRequestValve; +import com.amazon.opendistroforelasticsearch.security.configuration.IndexBaseConfigurationRepository; +import com.amazon.opendistroforelasticsearch.security.configuration.OpenDistroSecurityIndexSearcherWrapper; +import com.amazon.opendistroforelasticsearch.security.filter.OpenDistroSecurityFilter; +import com.amazon.opendistroforelasticsearch.security.filter.OpenDistroSecurityRestFilter; +import com.amazon.opendistroforelasticsearch.security.http.OpenDistroSecurityHttpServerTransport; +import com.amazon.opendistroforelasticsearch.security.http.OpenDistroSecurityNonSslHttpServerTransport; +import com.amazon.opendistroforelasticsearch.security.http.XFFResolver; +import com.amazon.opendistroforelasticsearch.security.privileges.PrivilegesEvaluator; +import com.amazon.opendistroforelasticsearch.security.privileges.PrivilegesInterceptor; +import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer; +import com.amazon.opendistroforelasticsearch.security.rest.KibanaInfoAction; +import com.amazon.opendistroforelasticsearch.security.rest.OpenDistroSecurityHealthAction; +import com.amazon.opendistroforelasticsearch.security.rest.OpenDistroSecurityInfoAction; +import com.amazon.opendistroforelasticsearch.security.rest.TenantInfoAction; +import com.amazon.opendistroforelasticsearch.security.ssl.OpenDistroSecuritySSLPlugin; +import com.amazon.opendistroforelasticsearch.security.ssl.SslExceptionHandler; +import com.amazon.opendistroforelasticsearch.security.ssl.http.netty.ValidatingDispatcher; +import com.amazon.opendistroforelasticsearch.security.ssl.transport.OpenDistroSecuritySSLNettyTransport; +import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.HeaderHelper; +import com.amazon.opendistroforelasticsearch.security.support.ModuleInfo; +import com.amazon.opendistroforelasticsearch.security.support.OpenDistroSecurityUtils; +import com.amazon.opendistroforelasticsearch.security.support.ReflectionHelper; +import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; +import com.amazon.opendistroforelasticsearch.security.transport.DefaultInterClusterRequestEvaluator; +import com.amazon.opendistroforelasticsearch.security.transport.InterClusterRequestEvaluator; +import com.amazon.opendistroforelasticsearch.security.transport.OpenDistroSecurityInterceptor; +import com.amazon.opendistroforelasticsearch.security.user.User; +import com.google.common.collect.Lists; + +public final class OpenDistroSecurityPlugin extends OpenDistroSecuritySSLPlugin implements ClusterPlugin, MapperPlugin { + + private static final String KEYWORD = ".keyword"; + private final boolean tribeNodeClient; + private final boolean dlsFlsAvailable; + private final Constructor dlsFlsConstructor; + private volatile OpenDistroSecurityRestFilter securityRestHandler; + private volatile OpenDistroSecurityInterceptor odsi; + private volatile PrivilegesEvaluator evaluator; + private volatile ThreadPool threadPool; + private volatile IndexBaseConfigurationRepository cr; + private volatile AdminDNs adminDns; + private volatile ClusterService cs; + private volatile AuditLog auditLog; + private volatile BackendRegistry backendRegistry; + private volatile SslExceptionHandler sslExceptionHandler; + private volatile Client localClient; + private final boolean disabled; + private final boolean enterpriseModulesEnabled; + private final boolean sslOnly; + private final List demoCertHashes = new ArrayList(3); + private volatile OpenDistroSecurityFilter odsf; + private volatile ComplianceConfig complianceConfig; + private volatile IndexResolverReplacer irr; + + @Override + public void close() throws IOException { + //TODO implement close + super.close(); + } + + private final SslExceptionHandler evaluateSslExceptionHandler() { + if (client || tribeNodeClient || disabled || sslOnly) { + return new SslExceptionHandler(){}; + } + + return Objects.requireNonNull(sslExceptionHandler); + } + + private static boolean isDisabled(final Settings settings) { + return settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_DISABLED, false); + } + + private static boolean isSslOnlyMode(final Settings settings) { + return settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_SSL_ONLY, false); + } + + public OpenDistroSecurityPlugin(final Settings settings, final Path configPath) { + super(settings, configPath, isDisabled(settings)); + + disabled = isDisabled(settings); + + if(disabled) { + this.tribeNodeClient = false; + this.dlsFlsAvailable = false; + this.dlsFlsConstructor = null; + this.enterpriseModulesEnabled = false; + this.sslOnly = false; + complianceConfig = null; + log.warn("Open Distro Security plugin installed but disabled. This can expose your configuration (including passwords) to the public."); + return; + } + + sslOnly = isSslOnlyMode(settings); + + if(sslOnly) { + this.tribeNodeClient = false; + this.dlsFlsAvailable = false; + this.dlsFlsConstructor = null; + this.enterpriseModulesEnabled = false; + complianceConfig = null; + log.warn("Open Distro Security plugin run in ssl only mode. No authentication or authorization is performed"); + return; + } + + + demoCertHashes.add("54a92508de7a39d06242a0ffbf59414d7eb478633c719e6af03938daf6de8a1a"); + demoCertHashes.add("742e4659c79d7cad89ea86aab70aea490f23bbfc7e72abd5f0a5d3fb4c84d212"); + demoCertHashes.add("db1264612891406639ecd25c894f256b7c5a6b7e1d9054cbe37b77acd2ddd913"); + demoCertHashes.add("2a5398e20fcb851ec30aa141f37233ee91a802683415be2945c3c312c65c97cf"); + demoCertHashes.add("33129547ce617f784c04e965104b2c671cce9e794d1c64c7efe58c77026246ae"); + demoCertHashes.add("c4af0297cc75546e1905bdfe3934a950161eee11173d979ce929f086fdf9794d"); + demoCertHashes.add("7a355f42c90e7543a267fbe3976c02f619036f5a34ce712995a22b342d83c3ce"); + demoCertHashes.add("a9b5eca1399ec8518081c0d4a21a34eec4589087ce64c04fb01a488f9ad8edc9"); + + //new certs 04/2018 + demoCertHashes.add("d14aefe70a592d7a29e14f3ff89c3d0070c99e87d21776aa07d333ee877e758f"); + demoCertHashes.add("54a70016e0837a2b0c5658d1032d7ca32e432c62c55f01a2bf5adcb69a0a7ba9"); + demoCertHashes.add("bdc141ab2272c779d0f242b79063152c49e1b06a2af05e0fd90d505f2b44d5f5"); + demoCertHashes.add("3e839e2b059036a99ee4f742814995f2fb0ced7e9d68a47851f43a3c630b5324"); + demoCertHashes.add("9b13661c073d864c28ad7b13eda67dcb6cbc2f04d116adc7c817c20b4c7ed361"); + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + if(Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + return null; + } + }); + + enterpriseModulesEnabled = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_ENTERPRISE_MODULES_ENABLED, true); + ReflectionHelper.init(enterpriseModulesEnabled); + + ReflectionHelper.registerMngtRestApiHandler(settings); + + log.info("Clustername: {}", settings.get("cluster.name","elasticsearch")); + + if(!transportSSLEnabled) { + throw new IllegalStateException(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_ENABLED+" must be set to 'true'"); + } + + if(log.isDebugEnabled() && this.settings.getByPrefix("tribe").size() > 0) { + log.debug("Tribe configuration detected: {}", this.settings); + } + + boolean tribeNode = this.settings.get("tribe.name", null) == null && this.settings.getByPrefix("tribe").size() > 0; + tribeNodeClient = this.settings.get("tribe.name", null) != null; + + log.debug("This node [{}] is a transportClient: {}/tribeNode: {}/tribeNodeClient: {}", settings.get("node.name"), client, tribeNode, tribeNodeClient); + + if(!client) { + dlsFlsConstructor = ReflectionHelper.instantiateDlsFlsConstructor(); + dlsFlsAvailable = dlsFlsConstructor != null; + } else { + dlsFlsAvailable = false; + dlsFlsConstructor = null; + } + + if(!client && !tribeNodeClient) { + final List filesWithWrongPermissions = AccessController.doPrivileged(new PrivilegedAction>() { + @Override + public List run() { + final Path confPath = new Environment(settings, configPath).configFile().toAbsolutePath(); + if(Files.isDirectory(confPath, LinkOption.NOFOLLOW_LINKS)) { + try (Stream s = Files.walk(confPath)) { + return s + .distinct() + .filter(p->checkFilePermissions(p)) + .collect(Collectors.toList()); + } catch (Exception e) { + log.error(e); + return null; + } + } + + return Collections.emptyList(); + } + }); + + if(filesWithWrongPermissions != null && filesWithWrongPermissions.size() > 0) { + for(final Path p: filesWithWrongPermissions) { + if(Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)) { + log.warn("Directory "+p+" has insecure file permissions (should be 0700)"); + } else { + log.warn("File "+p+" has insecure file permissions (should be 0600)"); + } + } + } + } + + if(!client && !tribeNodeClient && !settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES, false)) { + //check for demo certificates + final List files = AccessController.doPrivileged(new PrivilegedAction>() { + @Override + public List run() { + final Path confPath = new Environment(settings, configPath).configFile().toAbsolutePath(); + if(Files.isDirectory(confPath, LinkOption.NOFOLLOW_LINKS)) { + try (Stream s = Files.walk(confPath)) { + return s + .distinct() + .map(p->sha256(p)) + .collect(Collectors.toList()); + } catch (Exception e) { + log.error(e); + return null; + } + } + + return Collections.emptyList(); + } + }); + + if(files != null) { + demoCertHashes.retainAll(files); + if(!demoCertHashes.isEmpty()) { + log.error("Demo certificates found but "+ConfigConstants.OPENDISTRO_SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES+" is set to false."); + throw new RuntimeException("Demo certificates found "+demoCertHashes); + } + } else { + throw new RuntimeException("Unable to look for demo certificates"); + } + + } + } + + private String sha256(Path p) { + + if(!Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) { + return ""; + } + + if(!Files.isReadable(p)) { + log.debug("Unreadable file "+p+" found"); + return ""; + } + + try { + MessageDigest digester = MessageDigest.getInstance("SHA256"); + final String hash = org.bouncycastle.util.encoders.Hex.toHexString(digester.digest(Files.readAllBytes(p))); + log.debug(hash +" :: "+p); + return hash; + } catch (Exception e) { + throw new ElasticsearchSecurityException("Unable to digest file "+p, e); + } + } + + private boolean checkFilePermissions(final Path p) { + + if (p == null) { + return false; + } + + + Set perms; + + try { + perms = Files.getPosixFilePermissions(p, LinkOption.NOFOLLOW_LINKS); + } catch (Exception e) { + if(log.isDebugEnabled()) { + log.debug("Cannot determine posix file permissions for {} due to {}", p, e); + } + //ignore, can happen on windows + return false; + } + + if(Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)) { + if (perms.contains(PosixFilePermission.OTHERS_EXECUTE)) { + // no x for others must be set + return true; + } + } else { + if (perms.contains(PosixFilePermission.OWNER_EXECUTE) + || perms.contains(PosixFilePermission.GROUP_EXECUTE) + || perms.contains(PosixFilePermission.OTHERS_EXECUTE)) { + // no x must be set + return true; + } + } + + + if (perms.contains(PosixFilePermission.OTHERS_READ) || perms.contains(PosixFilePermission.OTHERS_WRITE)) { + // no permissions for "others" allowed + return true; + } + + //if (perms.contains(PosixFilePermission.GROUP_READ) || perms.contains(PosixFilePermission.GROUP_WRITE)) { + // // no permissions for "group" allowed + // return true; + //} + + return false; + } + + + @Override + public List getRestHandlers(Settings settings, RestController restController, ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, Supplier nodesInCluster) { + + final List handlers = new ArrayList(1); + + if (!client && !tribeNodeClient && !disabled) { + + handlers.addAll(super.getRestHandlers(settings, restController, clusterSettings, indexScopedSettings, settingsFilter, indexNameExpressionResolver, nodesInCluster)); + + if(!sslOnly) { + handlers.add(new OpenDistroSecurityInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool))); + handlers.add(new KibanaInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool))); + handlers.add(new OpenDistroSecurityHealthAction(settings, restController, Objects.requireNonNull(backendRegistry))); + handlers.add(new TenantInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool), + Objects.requireNonNull(cs), Objects.requireNonNull(adminDns))); + + Collection apiHandler = ReflectionHelper + .instantiateMngtRestApiHandler(settings, configPath, restController, localClient, adminDns, cr, cs, Objects.requireNonNull(principalExtractor), evaluator, threadPool, Objects.requireNonNull(auditLog)); + handlers.addAll(apiHandler); + log.debug("Added {} management rest handler(s)", apiHandler.size()); + } + } + + return handlers; + } + + @Override + public UnaryOperator getRestHandlerWrapper(final ThreadContext threadContext) { + + if(client || disabled || sslOnly) { + return (rh) -> rh; + } + + return (rh) -> securityRestHandler.wrap(rh); + } + + @Override + public List> getActions() { + List> actions = new ArrayList<>(1); + if(!tribeNodeClient && !disabled && !sslOnly) { + actions.add(new ActionHandler<>(ConfigUpdateAction.INSTANCE, TransportConfigUpdateAction.class)); + actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class)); + } + return actions; + } + + private IndexSearcherWrapper loadFlsDlsIndexSearcherWrapper(final IndexService indexService, + final ComplianceIndexingOperationListener ciol, final ComplianceConfig complianceConfig) { + try { + IndexSearcherWrapper flsdlsWrapper = (IndexSearcherWrapper) dlsFlsConstructor + .newInstance(indexService, settings, Objects.requireNonNull(adminDns), + Objects.requireNonNull(cs), + Objects.requireNonNull(auditLog), + Objects.requireNonNull(ciol), + Objects.requireNonNull(complianceConfig)); + if(log.isDebugEnabled()) { + log.debug("FLS/DLS enabled for index {}", indexService.index().getName()); + } + return flsdlsWrapper; + } catch(Exception ex) { + throw new RuntimeException("Failed to enable FLS/DLS", ex); + } + } + + @Override + public void onIndexModule(IndexModule indexModule) { + //called for every index! + + if (!disabled && !client && !sslOnly) { + log.debug("Handle complianceConfig="+complianceConfig+"/dlsFlsAvailable: "+dlsFlsAvailable+"/auditLog="+auditLog.getClass()+" for onIndexModule() of index "+indexModule.getIndex().getName()); + if (dlsFlsAvailable) { + + final ComplianceIndexingOperationListener ciol; + + assert complianceConfig!=null:"compliance config must not be null here"; + + if(complianceConfig.writeHistoryEnabledForIndex(indexModule.getIndex().getName())) { + ciol = ReflectionHelper.instantiateComplianceListener(complianceConfig, Objects.requireNonNull(auditLog)); + indexModule.addIndexOperationListener(ciol); + } else { + ciol = new ComplianceIndexingOperationListener(); + } + + indexModule.setSearcherWrapper(indexService -> loadFlsDlsIndexSearcherWrapper(indexService, ciol, complianceConfig)); + indexModule.forceQueryCacheProvider((indexSettings,nodeCache)->new QueryCache() { + + @Override + public Index index() { + return indexSettings.getIndex(); + } + + @Override + public void close() throws ElasticsearchException { + clear("close"); + } + + @Override + public void clear(String reason) { + nodeCache.clearIndex(index().getName()); + } + + @Override + public Weight doCache(Weight weight, QueryCachingPolicy policy) { + final Map> allowedFlsFields = (Map>) HeaderHelper.deserializeSafeFromHeader(threadPool.getThreadContext(), + ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER); + + if(OpenDistroSecurityUtils.evalMap(allowedFlsFields, index().getName()) != null) { + return weight; + } else { + + final Map> maskedFieldsMap = (Map>) HeaderHelper.deserializeSafeFromHeader(threadPool.getThreadContext(), + ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER); + + if(OpenDistroSecurityUtils.evalMap(maskedFieldsMap, index().getName()) != null) { + return weight; + } else { + return nodeCache.doCache(weight, policy); + } + } + + } + }); + } else { + + assert complianceConfig==null:"compliance config must be null here"; + + indexModule.setSearcherWrapper(indexService -> new OpenDistroSecurityIndexSearcherWrapper(indexService, settings, Objects + .requireNonNull(adminDns))); + } + + indexModule.addSearchOperationListener(new SearchOperationListener() { + + @Override + public void onNewScrollContext(SearchContext context) { + + final ScrollContext scrollContext = context.scrollContext(); + + if(scrollContext != null) { + + final boolean interClusterRequest = HeaderHelper.isInterClusterRequest(threadPool.getThreadContext()); + if(Origin.LOCAL.toString().equals(threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)) + && (interClusterRequest || HeaderHelper.isDirectRequest(threadPool.getThreadContext())) + + ){ + scrollContext.putInContext("_opendistro_security_scroll_auth_local", Boolean.TRUE); + + } else { + scrollContext.putInContext("_opendistro_security_scroll_auth", threadPool.getThreadContext() + .getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER)); + } + } + } + + @Override + public void validateSearchContext(SearchContext context, TransportRequest transportRequest) { + + final ScrollContext scrollContext = context.scrollContext(); + if(scrollContext != null) { + final Object _isLocal = scrollContext.getFromContext("_opendistro_security_scroll_auth_local"); + final Object _user = scrollContext.getFromContext("_opendistro_security_scroll_auth"); + if(_user != null && (_user instanceof User)) { + final User scrollUser = (User) _user; + final User currentUser = threadPool.getThreadContext() + .getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + if(!scrollUser.equals(currentUser)) { + auditLog.logMissingPrivileges(SearchScrollAction.NAME, transportRequest, context.getTask()); + log.error("Wrong user {} in scroll context, expected {}", scrollUser, currentUser); + throw new ElasticsearchSecurityException("Wrong user in scroll context", RestStatus.FORBIDDEN); + } + } else if(_isLocal != Boolean.TRUE) { + auditLog.logMissingPrivileges(SearchScrollAction.NAME, transportRequest, context.getTask()); + throw new ElasticsearchSecurityException("No user in scroll context", RestStatus.FORBIDDEN); + } + } + } + }); + } + } + + @Override + public List getActionFilters() { + List filters = new ArrayList<>(1); + if (!tribeNodeClient && !client && !disabled && !sslOnly) { + filters.add(Objects.requireNonNull(odsf)); + } + return filters; + } + + @Override + public List getTransportInterceptors(NamedWriteableRegistry namedWriteableRegistry, ThreadContext threadContext) { + List interceptors = new ArrayList(1); + + if (!client && !tribeNodeClient && !disabled && !sslOnly) { + interceptors.add(new TransportInterceptor() { + + @Override + public TransportRequestHandler interceptHandler(String action, String executor, + boolean forceExecution, TransportRequestHandler actualHandler) { + + return new TransportRequestHandler() { + + @Override + public void messageReceived(T request, TransportChannel channel, Task task) throws Exception { + odsi.getHandler(action, actualHandler).messageReceived(request, channel, task); + } + + @Override + public void messageReceived(T request, TransportChannel channel) throws Exception { + odsi.getHandler(action, actualHandler).messageReceived(request, channel); + } + }; + + } + + @Override + public AsyncSender interceptSender(AsyncSender sender) { + + return new AsyncSender() { + + @Override + public void sendRequest(Connection connection, String action, + TransportRequest request, TransportRequestOptions options, TransportResponseHandler handler) { + odsi.sendRequestDecorate(sender, connection, action, request, options, handler); + } + }; + } + }); + } + + return interceptors; + } + + @Override + public Map> getTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, + NamedWriteableRegistry namedWriteableRegistry, NetworkService networkService) { + Map> transports = new HashMap>(); + + if(sslOnly) { + return super.getTransports(settings, threadPool, bigArrays, pageCacheRecycler, circuitBreakerService, namedWriteableRegistry, networkService); + } + + if (transportSSLEnabled) { + transports.put("com.amazon.opendistroforelasticsearch.security.ssl.http.netty.OpenDistroSecuritySSLNettyTransport", + () -> new OpenDistroSecuritySSLNettyTransport(settings, threadPool, networkService, bigArrays, namedWriteableRegistry, + circuitBreakerService, odsks, evaluateSslExceptionHandler())); + } + return transports; + } + + @Override + public Map> getHttpTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + CircuitBreakerService circuitBreakerService, NamedWriteableRegistry namedWriteableRegistry, + NamedXContentRegistry xContentRegistry, NetworkService networkService, Dispatcher dispatcher) { + + if(sslOnly) { + return super.getHttpTransports(settings, threadPool, bigArrays, circuitBreakerService, namedWriteableRegistry, xContentRegistry, networkService, dispatcher); + } + + Map> httpTransports = new HashMap>(1); + + if(!disabled) { + if (!client && httpSSLEnabled && !tribeNodeClient) { + + final ValidatingDispatcher validatingDispatcher = new ValidatingDispatcher(threadPool.getThreadContext(), dispatcher, + settings, configPath, evaluateSslExceptionHandler()); + //TODO close odshst + final OpenDistroSecurityHttpServerTransport odshst = new OpenDistroSecurityHttpServerTransport(settings, networkService, bigArrays, + threadPool, odsks, evaluateSslExceptionHandler(), xContentRegistry, validatingDispatcher); + + httpTransports.put("com.amazon.opendistroforelasticsearch.security.http.OpenDistroSecurityHttpServerTransport", + () -> odshst); + } else if (!client && !tribeNodeClient) { + httpTransports.put("com.amazon.opendistroforelasticsearch.security.http.OpenDistroSecurityHttpServerTransport", + () -> new OpenDistroSecurityNonSslHttpServerTransport(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher)); + } + } + return httpTransports; + } + + + + @Override + public Collection createComponents(Client localClient, ClusterService clusterService, ThreadPool threadPool, + ResourceWatcherService resourceWatcherService, ScriptService scriptService, NamedXContentRegistry xContentRegistry, + Environment environment, NodeEnvironment nodeEnvironment, NamedWriteableRegistry namedWriteableRegistry) { + + if(sslOnly) { + return super.createComponents(localClient, clusterService, threadPool, resourceWatcherService, scriptService, xContentRegistry, environment, nodeEnvironment, namedWriteableRegistry); + } + + this.threadPool = threadPool; + this.cs = clusterService; + this.localClient = localClient; + + final List components = new ArrayList(); + + if (client || tribeNodeClient || disabled) { + return components; + } + final ClusterInfoHolder cih = new ClusterInfoHolder(); + this.cs.addListener(cih); + + DlsFlsRequestValve dlsFlsValve = ReflectionHelper.instantiateDlsFlsValve(); + + final IndexNameExpressionResolver resolver = new IndexNameExpressionResolver(settings); + irr = new IndexResolverReplacer(resolver, clusterService, cih); + auditLog = ReflectionHelper.instantiateAuditLog(settings, configPath, localClient, threadPool, resolver, clusterService); + complianceConfig = (dlsFlsAvailable && (auditLog.getClass() != NullAuditLog.class))?new ComplianceConfig(environment, Objects.requireNonNull(irr), auditLog):null; + log.debug("Compliance config is "+complianceConfig+" because of dlsFlsAvailable: "+dlsFlsAvailable+" and auditLog="+auditLog.getClass()); + auditLog.setComplianceConfig(complianceConfig); + + sslExceptionHandler = new AuditLogSslExceptionHandler(auditLog); + + final String DEFAULT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = DefaultInterClusterRequestEvaluator.class.getName(); + InterClusterRequestEvaluator interClusterRequestEvaluator = new DefaultInterClusterRequestEvaluator(settings); + + + final String className = settings.get(ConfigConstants.OPENDISTRO_SECURITY_INTERCLUSTER_REQUEST_EVALUATOR_CLASS, + DEFAULT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS); + log.debug("Using {} as intercluster request evaluator class", className); + if (!DEFAULT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS.equals(className)) { + interClusterRequestEvaluator = ReflectionHelper.instantiateInterClusterRequestEvaluator(className, settings); + } + + final PrivilegesInterceptor privilegesInterceptor = ReflectionHelper.instantiatePrivilegesInterceptorImpl(resolver, clusterService, localClient, threadPool); + + adminDns = new AdminDNs(settings); + //final PrincipalExtractor pe = new DefaultPrincipalExtractor(); + cr = (IndexBaseConfigurationRepository) IndexBaseConfigurationRepository.create(settings, this.configPath, threadPool, localClient, clusterService, auditLog, complianceConfig); + final InternalAuthenticationBackend iab = new InternalAuthenticationBackend(cr); + final XFFResolver xffResolver = new XFFResolver(threadPool); + cr.subscribeOnChange(ConfigConstants.CONFIGNAME_CONFIG, xffResolver); + backendRegistry = new BackendRegistry(settings, configPath, adminDns, xffResolver, iab, auditLog, threadPool); + cr.subscribeOnChange(ConfigConstants.CONFIGNAME_CONFIG, backendRegistry); + final ActionGroupHolder ah = new ActionGroupHolder(cr); + evaluator = new PrivilegesEvaluator(clusterService, threadPool, cr, ah, resolver, auditLog, settings, privilegesInterceptor, cih); + + final CompatConfig compatConfig = new CompatConfig(environment); + cr.subscribeOnChange(ConfigConstants.CONFIGNAME_CONFIG, compatConfig); + + odsf = new OpenDistroSecurityFilter(evaluator, adminDns, dlsFlsValve, auditLog, threadPool, cs, complianceConfig, compatConfig); + + + final String principalExtractorClass = settings.get(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_PRINCIPAL_EXTRACTOR_CLASS, null); + + if(principalExtractorClass == null) { + principalExtractor = new com.amazon.opendistroforelasticsearch.security.ssl.transport.DefaultPrincipalExtractor(); + } else { + principalExtractor = ReflectionHelper.instantiatePrincipalExtractor(principalExtractorClass); + } + + odsi = new OpenDistroSecurityInterceptor(settings, threadPool, backendRegistry, auditLog, principalExtractor, + interClusterRequestEvaluator, cs, Objects.requireNonNull(sslExceptionHandler), Objects.requireNonNull(cih)); + components.add(principalExtractor); + + cr.subscribeOnChange(ConfigConstants.CONFIGNAME_CONFIG, new ConfigurationChangeListener() { + + @Override + public void onChange(Settings unused) { + //auditLog.logExternalConfig(settings, environment); + } + }); + + components.add(adminDns); + //components.add(auditLog); + components.add(cr); + components.add(iab); + components.add(xffResolver); + components.add(backendRegistry); + components.add(ah); + components.add(evaluator); + components.add(odsi); + + securityRestHandler = new OpenDistroSecurityRestFilter(backendRegistry, auditLog, threadPool, principalExtractor, settings, configPath, compatConfig); + + return components; + + } + + @Override + public Settings additionalSettings() { + + if(disabled) { + return Settings.EMPTY; + } + + final Settings.Builder builder = Settings.builder(); + + builder.put(super.additionalSettings()); + + if(!sslOnly){ + builder.put(NetworkModule.TRANSPORT_TYPE_KEY, "com.amazon.opendistroforelasticsearch.security.ssl.http.netty.OpenDistroSecuritySSLNettyTransport"); + builder.put(NetworkModule.HTTP_TYPE_KEY, "com.amazon.opendistroforelasticsearch.security.http.OpenDistroSecurityHttpServerTransport"); + } + return builder.build(); + } + @Override + public List> getSettings() { + List> settings = new ArrayList>(); + settings.addAll(super.getSettings()); + + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_SSL_ONLY, false, Property.NodeScope, Property.Filtered)); + + if(!sslOnly) { + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_ADMIN_DN, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here + + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, Property.NodeScope, Property.Filtered)); + settings.add(Setting.groupSetting(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_IMPERSONATION_DN+".", Property.NodeScope)); //not filtered here + + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_CERT_OID, Property.NodeScope, Property.Filtered)); + + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_CERT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS, Property.NodeScope, Property.Filtered)); + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_NODES_DN, Collections.emptyList(), Function.identity(), Property.NodeScope));//not filtered here + + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, + Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, + Property.NodeScope, Property.Filtered)); + + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_DISABLED, false, Property.NodeScope, Property.Filtered)); + + settings.add(Setting.intSetting(ConfigConstants.OPENDISTRO_SECURITY_CACHE_TTL_MINUTES, 60, 0, Property.NodeScope, Property.Filtered)); + + //Security + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_ENTERPRISE_MODULES_ENABLED, true, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false, Property.NodeScope, Property.Filtered)); + + settings.add(Setting.groupSetting(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+".", Property.NodeScope)); //not filtered here + + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_ROLES_MAPPING_RESOLUTION, Property.NodeScope, Property.Filtered)); + //settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_DISABLE_TYPE_SECURITY, false, Property.NodeScope, Property.Filtered)); + + //TODO remove opendistro_security.tribe.clustername? + //settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_TRIBE_CLUSTERNAME, Property.NodeScope, Property.Filtered)); + + // Security - Audit + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_TYPE_DEFAULT, Property.NodeScope, Property.Filtered)); + settings.add(Setting.groupSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_ROUTES + ".", Property.NodeScope)); + settings.add(Setting.groupSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_ENDPOINTS + ".", Property.NodeScope)); + settings.add(Setting.intSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_THREADPOOL_SIZE, 10, Property.NodeScope, Property.Filtered)); + settings.add(Setting.intSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_THREADPOOL_MAX_QUEUE_LEN, 100*1000, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_LOG_REQUEST_BODY, true, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_RESOLVE_INDICES, true, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ENABLE_REST, true, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ENABLE_TRANSPORT, true, Property.NodeScope, Property.Filtered)); + final List disabledCategories = new ArrayList(2); + disabledCategories.add("AUTHENTICATED"); + disabledCategories.add("GRANTED_PRIVILEGES"); + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES, disabledCategories, Function.identity(), Property.NodeScope)); //not filtered here + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES, disabledCategories, Function.identity(), Property.NodeScope)); //not filtered here + final List ignoredUsers = new ArrayList(2); + ignoredUsers.add("kibanaserver"); + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS, ignoredUsers, Function.identity(), Property.NodeScope)); //not filtered here + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_RESOLVE_BULK_REQUESTS, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXCLUDE_SENSITIVE_HEADERS, true, Property.NodeScope, Property.Filtered)); + + + // Security - Audit - Sink + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ES_INDEX, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ES_TYPE, Property.NodeScope, Property.Filtered)); + + // External ES + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_HTTP_ENDPOINTS, Lists.newArrayList("localhost:9200"), Function.identity(), Property.NodeScope)); //not filtered here + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_USERNAME, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PASSWORD, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_ENABLE_SSL, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_VERIFY_HOSTNAMES, true, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_ENABLE_SSL_CLIENT_AUTH, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMCERT_CONTENT, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMCERT_FILEPATH, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMKEY_CONTENT, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMKEY_FILEPATH, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMKEY_PASSWORD, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMTRUSTEDCAS_CONTENT, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMTRUSTEDCAS_FILEPATH, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_JKS_CERT_ALIAS, Property.NodeScope, Property.Filtered)); + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_ENABLED_SSL_CIPHERS, Collections.emptyList(), Function.identity(), Property.NodeScope));//not filtered here + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_ENABLED_SSL_PROTOCOLS, Collections.emptyList(), Function.identity(), Property.NodeScope));//not filtered here + + // Webhooks + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_WEBHOOK_URL, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_WEBHOOK_FORMAT, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_WEBHOOK_SSL_VERIFY, true, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_FILEPATH, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_CONTENT, Property.NodeScope, Property.Filtered)); + + // Log4j + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_LOG4J_LOGGER_NAME, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_LOG4J_LEVEL, Property.NodeScope, Property.Filtered)); + + + // Kerberos + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_KERBEROS_KRB5_FILEPATH, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_KERBEROS_ACCEPTOR_KEYTAB_FILEPATH, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_KERBEROS_ACCEPTOR_PRINCIPAL, Property.NodeScope, Property.Filtered)); + + + // Open Distro Security - REST API + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_RESTAPI_ROLES_ENABLED, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here + settings.add(Setting.groupSetting(ConfigConstants.OPENDISTRO_SECURITY_RESTAPI_ENDPOINTS_DISABLED + ".", Property.NodeScope)); + + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, Property.NodeScope, Property.Filtered)); + + + // Compliance + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_METADATA_ONLY, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_IGNORE_USERS, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_IGNORE_USERS, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_IMMUTABLE_INDICES, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here + settings.add(Setting.simpleString(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED, false, Property.NodeScope, Property.Filtered)); + + //compat + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY, false, Property.NodeScope, Property.Filtered)); + + // system integration + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED, false, Property.NodeScope, Property.Filtered)); + } + + return settings; + } + + @Override + public List getSettingsFilter() { + List settingsFilter = new ArrayList<>(); + + if(disabled) { + return settingsFilter; + } + + settingsFilter.add("opendistro_security.*"); + return settingsFilter; + } + + @Override + public void onNodeStarted() { + final Set securityModules = ReflectionHelper.getModulesLoaded(); + log.info("{} Open Distro Security modules loaded so far: {}", securityModules.size(), securityModules); + if(complianceConfig != null && complianceConfig.isEnabled() && complianceConfig.isLogExternalConfig() && !complianceConfig.isExternalConfigLogged()) { + log.info("logging external config"); + auditLog.logExternalConfig(complianceConfig.getSettings(), complianceConfig.getEnvironment()); + complianceConfig.setExternalConfigLogged(true); + } + } + + //below is a hack because it seems not possible to access RepositoriesService from a non guice class + //the way of how deguice is organized is really a mess - hope this can be fixed in later versions + //TODO check if this could be removed + + @Override + public Collection> getGuiceServiceClasses() { + + if (client || tribeNodeClient || disabled || sslOnly) { + return Collections.emptyList(); + } + + final List> services = new ArrayList<>(1); + services.add(GuiceHolder.class); + return services; + } + + @Override + public Function> getFieldFilter() { + return index -> { + final Map> allowedFlsFields = (Map>) HeaderHelper + .deserializeSafeFromHeader(threadPool.getThreadContext(), ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER); + + final String eval = OpenDistroSecurityUtils.evalMap(allowedFlsFields, index); + + if (eval == null) { + return field -> true; + } else { + + final Set includesExcludes = allowedFlsFields.get(eval); + + final Set includesSet = new HashSet<>(includesExcludes.size()); + final Set excludesSet = new HashSet<>(includesExcludes.size()); + + for (final String incExc : includesExcludes) { + final char firstChar = incExc.charAt(0); + + if (firstChar == '!' || firstChar == '~') { + excludesSet.add(incExc.substring(1)); + } else { + includesSet.add(incExc); + } + } + + if (!excludesSet.isEmpty()) { + return field -> !WildcardMatcher.matchAny(excludesSet, handleKeyword(field)); + } else { + return field -> WildcardMatcher.matchAny(includesSet, handleKeyword(field)); + } + } + }; + } + + private static String handleKeyword(final String field) { + if(field != null && field.endsWith(KEYWORD)) { + return field.substring(0, field.length()-KEYWORD.length()); + } + return field; + } + + public static class GuiceHolder implements LifecycleComponent { + + private static RepositoriesService repositoriesService; + private static RemoteClusterService remoteClusterService; + + @Inject + public GuiceHolder(final RepositoriesService repositoriesService, + final TransportService remoteClusterService) { + GuiceHolder.repositoriesService = repositoriesService; + GuiceHolder.remoteClusterService = remoteClusterService.getRemoteClusterService(); + } + + public static RepositoriesService getRepositoriesService() { + return repositoriesService; + } + + public static RemoteClusterService getRemoteClusterService() { + return remoteClusterService; + } + + @Override + public void close() { + } + + @Override + public State lifecycleState() { + return null; + } + + @Override + public void addLifecycleListener(LifecycleListener listener) { + } + + @Override + public void removeLifecycleListener(LifecycleListener listener) { + } + + @Override + public void start() { + } + + @Override + public void stop() { + } + + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateAction.java new file mode 100644 index 0000000000..e2ca4094c2 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateAction.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.action.configupdate; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +public class ConfigUpdateAction extends Action { + + public static final ConfigUpdateAction INSTANCE = new ConfigUpdateAction(); + public static final String NAME = "cluster:admin/opendistro_security/config/update"; + + protected ConfigUpdateAction() { + super(NAME); + } + + @Override + public ConfigUpdateRequestBuilder newRequestBuilder(final ElasticsearchClient client) { + return new ConfigUpdateRequestBuilder(client, this); + } + + @Override + public ConfigUpdateResponse newResponse() { + return new ConfigUpdateResponse(); + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateNodeResponse.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateNodeResponse.java new file mode 100644 index 0000000000..46a1f71925 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateNodeResponse.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.action.configupdate; + +import java.io.IOException; +import java.util.Arrays; + +import org.elasticsearch.action.support.nodes.BaseNodeResponse; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +public class ConfigUpdateNodeResponse extends BaseNodeResponse { + + private String[] updatedConfigTypes; + private String message; + + ConfigUpdateNodeResponse() { + } + + public ConfigUpdateNodeResponse(final DiscoveryNode node, String[] updatedConfigTypes, String message) { + super(node); + this.updatedConfigTypes = updatedConfigTypes; + this.message = message; + } + + public static ConfigUpdateNodeResponse readNodeResponse(StreamInput in) throws IOException { + ConfigUpdateNodeResponse nodeResponse = new ConfigUpdateNodeResponse(); + nodeResponse.readFrom(in); + return nodeResponse; + } + + public String[] getUpdatedConfigTypes() { + return updatedConfigTypes==null?null:Arrays.copyOf(updatedConfigTypes, updatedConfigTypes.length); + } + + public String getMessage() { + return message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeStringArray(updatedConfigTypes); + out.writeOptionalString(message); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + updatedConfigTypes = in.readStringArray(); + message = in.readOptionalString(); + } + + @Override + public String toString() { + return "ConfigUpdateNodeResponse [updatedConfigTypes=" + Arrays.toString(updatedConfigTypes) + ", message=" + message + "]"; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateRequest.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateRequest.java new file mode 100644 index 0000000000..4d76f0f967 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateRequest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.action.configupdate; + +import java.io.IOException; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.nodes.BaseNodesRequest; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +public class ConfigUpdateRequest extends BaseNodesRequest { + + private String[] configTypes; + + public ConfigUpdateRequest() { + super(); + } + + public ConfigUpdateRequest(final String[] configTypes) { + super(); + this.configTypes = configTypes; + } + + @Override + public void readFrom(final StreamInput in) throws IOException { + super.readFrom(in); + this.configTypes = in.readStringArray(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + out.writeStringArray(configTypes); + } + + public String[] getConfigTypes() { + return configTypes; + } + + public void setConfigTypes(final String[] configTypes) { + this.configTypes = configTypes; + } + + @Override + public ActionRequestValidationException validate() { + if (configTypes == null || configTypes.length == 0) { + return new ActionRequestValidationException(); + } + return null; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateRequestBuilder.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateRequestBuilder.java new file mode 100644 index 0000000000..d9db668e0e --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateRequestBuilder.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.action.configupdate; + +import org.elasticsearch.action.support.nodes.NodesOperationRequestBuilder; +import org.elasticsearch.client.ClusterAdminClient; +import org.elasticsearch.client.ElasticsearchClient; + +public class ConfigUpdateRequestBuilder extends +NodesOperationRequestBuilder { + public ConfigUpdateRequestBuilder(final ClusterAdminClient client) { + this(client, ConfigUpdateAction.INSTANCE); + } + + public ConfigUpdateRequestBuilder(final ElasticsearchClient client, final ConfigUpdateAction action) { + super(client, action, new ConfigUpdateRequest()); + } + + public ConfigUpdateRequestBuilder setShardId(final String[] configTypes) { + request().setConfigTypes(configTypes); + return this; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateResponse.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateResponse.java new file mode 100644 index 0000000000..e6d528c524 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/ConfigUpdateResponse.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.action.configupdate; + +import java.io.IOException; +import java.util.List; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +public class ConfigUpdateResponse extends BaseNodesResponse { + + public ConfigUpdateResponse() { + } + + public ConfigUpdateResponse(final ClusterName clusterName, List nodes, List failures) { + super(clusterName, nodes, failures); + } + + @Override + public List readNodesFrom(final StreamInput in) throws IOException { + return in.readList(ConfigUpdateNodeResponse::readNodeResponse); + } + + @Override + public void writeNodesTo(final StreamOutput out, List nodes) throws IOException { + out.writeStreamableList(nodes); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/TransportConfigUpdateAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/TransportConfigUpdateAction.java new file mode 100644 index 0000000000..d43c4187b9 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/configupdate/TransportConfigUpdateAction.java @@ -0,0 +1,128 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.action.configupdate; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.nodes.BaseNodeRequest; +import org.elasticsearch.action.support.nodes.TransportNodesAction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.inject.Provider; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; + +import com.amazon.opendistroforelasticsearch.security.auth.BackendRegistry; +import com.amazon.opendistroforelasticsearch.security.configuration.ConfigurationRepository; +import com.amazon.opendistroforelasticsearch.security.configuration.IndexBaseConfigurationRepository; + +public class TransportConfigUpdateAction +extends +TransportNodesAction { + + private final Provider backendRegistry; + private final ConfigurationRepository configurationRepository; + + @Inject + public TransportConfigUpdateAction(final Settings settings, + final ThreadPool threadPool, final ClusterService clusterService, final TransportService transportService, + final IndexBaseConfigurationRepository configurationRepository, final ActionFilters actionFilters, final IndexNameExpressionResolver indexNameExpressionResolver, + Provider backendRegistry) { + + super(settings, ConfigUpdateAction.NAME, threadPool, clusterService, transportService, actionFilters, + indexNameExpressionResolver, ConfigUpdateRequest::new, TransportConfigUpdateAction.NodeConfigUpdateRequest::new, + ThreadPool.Names.MANAGEMENT, ConfigUpdateNodeResponse.class); + + this.configurationRepository = configurationRepository; + this.backendRegistry = backendRegistry; + } + + public static class NodeConfigUpdateRequest extends BaseNodeRequest { + + ConfigUpdateRequest request; + + public NodeConfigUpdateRequest() { + } + + public NodeConfigUpdateRequest(final String nodeId, final ConfigUpdateRequest request) { + super(nodeId); + this.request = request; + } + + @Override + public void readFrom(final StreamInput in) throws IOException { + super.readFrom(in); + request = new ConfigUpdateRequest(); + request.readFrom(in); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + request.writeTo(out); + } + } + + protected NodeConfigUpdateRequest newNodeRequest(final String nodeId, final ConfigUpdateRequest request) { + return new NodeConfigUpdateRequest(nodeId, request); + } + + @Override + protected ConfigUpdateNodeResponse newNodeResponse() { + return new ConfigUpdateNodeResponse(clusterService.localNode(), new String[0], null); + } + + + @Override + protected ConfigUpdateResponse newResponse(ConfigUpdateRequest request, List responses, + List failures) { + return new ConfigUpdateResponse(this.clusterService.getClusterName(), responses, failures); + + } + + @Override + protected ConfigUpdateNodeResponse nodeOperation(final NodeConfigUpdateRequest request) { + final Map setn = configurationRepository.reloadConfiguration(Arrays.asList(request.request.getConfigTypes())); + backendRegistry.get().invalidateCache(); + return new ConfigUpdateNodeResponse(clusterService.localNode(), setn.keySet().toArray(new String[0]), null); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/TransportWhoAmIAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/TransportWhoAmIAction.java new file mode 100644 index 0000000000..fbd416ae81 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/TransportWhoAmIAction.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.action.whoami; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; + +import com.amazon.opendistroforelasticsearch.security.configuration.AdminDNs; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.HeaderHelper; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class TransportWhoAmIAction +extends +HandledTransportAction { + + private final AdminDNs adminDNs; + + @Inject + public TransportWhoAmIAction(final Settings settings, + final ThreadPool threadPool, final ClusterService clusterService, final TransportService transportService, + final AdminDNs adminDNs, final ActionFilters actionFilters, final IndexNameExpressionResolver indexNameExpressionResolver) { + + super(settings, WhoAmIAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, WhoAmIRequest::new); + + this.adminDNs = adminDNs; + } + + + @Override + protected void doExecute(WhoAmIRequest request, ActionListener listener) { + final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final String dn = user==null?threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_PRINCIPAL):user.getName(); + final boolean isAdmin = adminDNs.isAdminDN(dn); + final boolean isAuthenticated = isAdmin?true: user != null; + final boolean isNodeCertificateRequest = HeaderHelper.isInterClusterRequest(threadPool.getThreadContext()) || + HeaderHelper.isTrustedClusterRequest(threadPool.getThreadContext()); + + listener.onResponse(new WhoAmIResponse(dn, isAdmin, isAuthenticated, isNodeCertificateRequest)); + + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIAction.java new file mode 100644 index 0000000000..73b8cc9b2d --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIAction.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.action.whoami; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +public class WhoAmIAction extends Action { + + public static final WhoAmIAction INSTANCE = new WhoAmIAction(); + public static final String NAME = "cluster:admin/opendistro_security/whoami"; + + protected WhoAmIAction() { + super(NAME); + } + + @Override + public WhoAmIRequestBuilder newRequestBuilder(final ElasticsearchClient client) { + return new WhoAmIRequestBuilder(client, this); + } + + @Override + public WhoAmIResponse newResponse() { + return new WhoAmIResponse(null, false, false, false); + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIRequest.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIRequest.java new file mode 100644 index 0000000000..af033b34d1 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIRequest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.action.whoami; + +import java.io.IOException; + +import org.elasticsearch.action.support.nodes.BaseNodesRequest; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +public class WhoAmIRequest extends BaseNodesRequest { + + public WhoAmIRequest() { + super(); + } + + @Override + public void readFrom(final StreamInput in) throws IOException { + super.readFrom(in); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIRequestBuilder.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIRequestBuilder.java new file mode 100644 index 0000000000..8a00d9ed2c --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIRequestBuilder.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.action.whoami; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ClusterAdminClient; +import org.elasticsearch.client.ElasticsearchClient; + +public class WhoAmIRequestBuilder extends +ActionRequestBuilder { + public WhoAmIRequestBuilder(final ClusterAdminClient client) { + this(client, WhoAmIAction.INSTANCE); + } + + public WhoAmIRequestBuilder(final ElasticsearchClient client, final WhoAmIAction action) { + super(client, action, new WhoAmIRequest()); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIResponse.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIResponse.java new file mode 100644 index 0000000000..398960a2d5 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/action/whoami/WhoAmIResponse.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.action.whoami; + +import java.io.IOException; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +public class WhoAmIResponse extends ActionResponse implements ToXContent { + + private String dn; + private boolean isAdmin; + private boolean isAuthenticated; + private boolean isNodeCertificateRequest; + + public WhoAmIResponse(String dn, boolean isAdmin, boolean isAuthenticated, boolean isNodeCertificateRequest) { + this.dn = dn; + this.isAdmin = isAdmin; + this.isAuthenticated = isAuthenticated; + this.isNodeCertificateRequest = isNodeCertificateRequest; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(dn); + out.writeBoolean(isAdmin); + out.writeBoolean(isAuthenticated); + out.writeBoolean(isNodeCertificateRequest); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + dn = in.readString(); + isAdmin = in.readBoolean(); + isAuthenticated = in.readBoolean(); + isNodeCertificateRequest = in.readBoolean(); + } + + public String getDn() { + return dn; + } + + public boolean isAdmin() { + return isAdmin; + } + + public boolean isAuthenticated() { + return isAuthenticated; + } + + public boolean isNodeCertificateRequest() { + return isNodeCertificateRequest; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + + builder.startObject("whoami"); + builder.field("dn", dn); + builder.field("is_admin", isAdmin); + builder.field("is_authenticated", isAuthenticated); + builder.field("is_node_certificate_request", isNodeCertificateRequest); + builder.endObject(); + + return builder; + } + + @Override + public String toString() { + return Strings.toString(this, true, true); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/AuditLog.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/AuditLog.java new file mode 100644 index 0000000000..ced020eafb --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/AuditLog.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auditlog; + +import java.io.Closeable; +import java.util.Map; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.index.engine.Engine.Delete; +import org.elasticsearch.index.engine.Engine.DeleteResult; +import org.elasticsearch.index.engine.Engine.Index; +import org.elasticsearch.index.engine.Engine.IndexResult; +import org.elasticsearch.index.get.GetResult; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportRequest; + +import com.amazon.opendistroforelasticsearch.security.compliance.ComplianceConfig; + +public interface AuditLog extends Closeable { + + //login + void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, TransportRequest request, Task task); + void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request); + void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, TransportRequest request, String action, Task task); + void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request); + + //privs + void logMissingPrivileges(String privilege, String effectiveUser, RestRequest request); + void logMissingPrivileges(String privilege, TransportRequest request, Task task); + void logGrantedPrivileges(String privilege, TransportRequest request, Task task); + + //spoof + void logBadHeaders(TransportRequest request, String action, Task task); + void logBadHeaders(RestRequest request); + + void logSecurityIndexAttempt(TransportRequest request, String action, Task task); + + void logSSLException(TransportRequest request, Throwable t, String action, Task task); + void logSSLException(RestRequest request, Throwable t); + + void logDocumentRead(String index, String id, ShardId shardId, Map fieldNameValues, ComplianceConfig complianceConfig); + void logDocumentWritten(ShardId shardId, GetResult originalIndex, Index currentIndex, IndexResult result, ComplianceConfig complianceConfig); + void logDocumentDeleted(ShardId shardId, Delete delete, DeleteResult result); + void logExternalConfig(Settings settings, Environment environment); + + // compliance config + void setComplianceConfig(ComplianceConfig complianceConfig); + + public enum Origin { + REST, TRANSPORT, LOCAL + } + + public enum Operation { + CREATE, UPDATE, DELETE + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/AuditLogSslExceptionHandler.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/AuditLogSslExceptionHandler.java new file mode 100644 index 0000000000..8f0ebfe077 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/AuditLogSslExceptionHandler.java @@ -0,0 +1,90 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auditlog; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportRequest; + +import com.amazon.opendistroforelasticsearch.security.ssl.SslExceptionHandler; + +public class AuditLogSslExceptionHandler implements SslExceptionHandler{ + + private final AuditLog auditLog; + + public AuditLogSslExceptionHandler(final AuditLog auditLog) { + super(); + this.auditLog = auditLog; + } + + @Override + public void logError(Throwable t, RestRequest request, int type) { + switch (type) { + case 0: + auditLog.logSSLException(request, t); + break; + case 1: + auditLog.logBadHeaders(request); + break; + default: + break; + } + } + + @Override + public void logError(Throwable t, boolean isRest) { + if (isRest) { + auditLog.logSSLException(null, t); + } else { + auditLog.logSSLException(null, t, null, null); + } + } + + @Override + public void logError(Throwable t, TransportRequest request, String action, Task task, int type) { + switch (type) { + case 0: + if(t instanceof ElasticsearchException) { + auditLog.logMissingPrivileges(action, request, task); + } else { + auditLog.logSSLException(request, t, action, task); + } + break; + case 1: + auditLog.logBadHeaders(request, action, task); + break; + default: + break; + } + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/NullAuditLog.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/NullAuditLog.java new file mode 100644 index 0000000000..78a6629680 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auditlog/NullAuditLog.java @@ -0,0 +1,142 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auditlog; + +import java.io.IOException; +import java.util.Map; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.index.engine.Engine.Delete; +import org.elasticsearch.index.engine.Engine.DeleteResult; +import org.elasticsearch.index.engine.Engine.Index; +import org.elasticsearch.index.engine.Engine.IndexResult; +import org.elasticsearch.index.get.GetResult; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportRequest; + +import com.amazon.opendistroforelasticsearch.security.compliance.ComplianceConfig; + +public class NullAuditLog implements AuditLog { + + @Override + public void close() throws IOException { + //noop, intentionally left empty + } + + @Override + public void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, TransportRequest request, Task task) { + //noop, intentionally left empty + } + + @Override + public void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request) { + //noop, intentionally left empty + } + + @Override + public void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, TransportRequest request, String action, Task task) { + //noop, intentionally left empty + } + + @Override + public void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request) { + //noop, intentionally left empty + } + + @Override + public void logMissingPrivileges(String privilege, TransportRequest request, Task task) { + //noop, intentionally left empty + } + + @Override + public void logGrantedPrivileges(String privilege, TransportRequest request, Task task) { + //noop, intentionally left empty + } + + @Override + public void logBadHeaders(TransportRequest request, String action, Task task) { + //noop, intentionally left empty + } + + @Override + public void logBadHeaders(RestRequest request) { + //noop, intentionally left empty + } + + @Override + public void logSecurityIndexAttempt(TransportRequest request, String action, Task task) { + //noop, intentionally left empty + } + + @Override + public void logSSLException(TransportRequest request, Throwable t, String action, Task task) { + //noop, intentionally left empty + } + + @Override + public void logSSLException(RestRequest request, Throwable t) { + //noop, intentionally left empty + } + + @Override + public void logMissingPrivileges(String privilege, String effectiveUser, RestRequest request) { + //noop, intentionally left empty + } + + @Override + public void logDocumentRead(String index, String id, ShardId shardId, Map fieldNameValues, ComplianceConfig complianceConfig) { + //noop, intentionally left empty + } + + @Override + public void logDocumentWritten(ShardId shardId, GetResult originalIndex, Index currentIndex, IndexResult result, ComplianceConfig complianceConfig) { + //noop, intentionally left empty + } + + @Override + public void logDocumentDeleted(ShardId shardId, Delete delete, DeleteResult result) { + //noop, intentionally left empty + } + + @Override + public void logExternalConfig(Settings settings, Environment environment) { + //noop, intentionally left empty + } + + @Override + public void setComplianceConfig(ComplianceConfig complianceConfig) { + //noop, intentionally left empty + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthDomain.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthDomain.java new file mode 100644 index 0000000000..7daf8a80bc --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthDomain.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth; + +import java.util.Objects; + +public class AuthDomain implements Comparable { + + private final AuthenticationBackend backend; + private final HTTPAuthenticator httpAuthenticator; + private final int order; + private final boolean challenge; + + public AuthDomain(final AuthenticationBackend backend, final HTTPAuthenticator httpAuthenticator, boolean challenge, final int order) { + super(); + this.backend = Objects.requireNonNull(backend); + this.httpAuthenticator = httpAuthenticator; + this.order = order; + this.challenge = challenge; + } + + public boolean isChallenge() { + return challenge; + } + + public AuthenticationBackend getBackend() { + return backend; + } + + public HTTPAuthenticator getHttpAuthenticator() { + return httpAuthenticator; + } + + public int getOrder() { + return order; + } + + @Override + public String toString() { + return "AuthDomain [backend=" + backend + ", httpAuthenticator=" + httpAuthenticator + ", order=" + order + ", challenge=" + + challenge + "]"; + } + + @Override + public int compareTo(final AuthDomain o) { + return Integer.compare(this.order, o.order); + } +} \ No newline at end of file diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthenticationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthenticationBackend.java new file mode 100644 index 0000000000..b12077b4e9 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthenticationBackend.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth; + +import org.elasticsearch.ElasticsearchSecurityException; + +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; +import com.amazon.opendistroforelasticsearch.security.user.User; + +/** + * Open Distro Security custom authentication backends need to implement this interface. + *

+ * Authentication backends verify {@link AuthCredentials} and, if successfully verified, return a {@link User}. + *

+ * Implementation classes must provide a public constructor + *

+ * {@code public MyHTTPAuthenticator(org.elasticsearch.common.settings.Settings settings, java.nio.file.Path configPath)} + *

+ * The constructor should not throw any exception in case of an initialization problem. + * Instead catch all exceptions and log a appropriate error message. A logger can be instantiated like: + *

+ * {@code private final Logger log = LogManager.getLogger(this.getClass());} + * + *

+ */ +public interface AuthenticationBackend { + + /** + * The type (name) of the authenticator. Only for logging. + * @return the type + */ + String getType(); + + /** + * Validate credentials and return an authenticated user (or throw an ElasticsearchSecurityException) + *

+ * Results of this method are normally cached so that we not need to query the backend for every authentication attempt. + *

+ * @param The credentials to be validated, never null + * @return the authenticated User, never null + * @throws ElasticsearchSecurityException in case an authentication failure + * (when credentials are incorrect, the user does not exist or the backend is not reachable) + */ + User authenticate(AuthCredentials credentials) throws ElasticsearchSecurityException; + + /** + * + * Lookup for a specific user in the authentication backend + * + * @param user The user for which the authentication backend should be queried + * @return true if the user exists in the authentication backend, false otherwise + */ + boolean exists(User user); + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthorizationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthorizationBackend.java new file mode 100644 index 0000000000..e3ea3cbb01 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/AuthorizationBackend.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth; + +import org.elasticsearch.ElasticsearchSecurityException; + +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; +import com.amazon.opendistroforelasticsearch.security.user.User; + +/** + * Open Distro Security custom authorization backends need to implement this interface. + *

+ * Authorization backends populate a prior authenticated {@link User} with roles who's the user is a member of. + *

+ * Implementation classes must provide a public constructor + *

+ * {@code public MyHTTPAuthenticator(org.elasticsearch.common.settings.Settings settings, java.nio.file.Path configPath)} + *

+ * The constructor should not throw any exception in case of an initialization problem. + * Instead catch all exceptions and log a appropriate error message. A logger can be instantiated like: + *

+ * {@code private final Logger log = LogManager.getLogger(this.getClass());} + * + *

+ */ +public interface AuthorizationBackend { + + /** + * The type (name) of the authorizer. Only for logging. + * @return the type + */ + String getType(); + + /** + * Populate a {@link User} with roles. This method will not be called for cached users. + *

+ * Add them by calling either {@code user.addRole()} or {@code user.addRoles()} + *

+ * @param user The authenticated user to populate with roles, never null + * @param credentials Credentials to authenticate to the authorization backend, maybe null. + * This parameter is for future usage, currently always empty credentials are passed! + * @throws ElasticsearchSecurityException in case when the authorization backend cannot be reached + * or the {@code credentials} are insufficient to authenticate to the authorization backend. + */ + void fillRoles(User user, AuthCredentials credentials) throws ElasticsearchSecurityException; + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/BackendRegistry.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/BackendRegistry.java new file mode 100644 index 0000000000..f38b440810 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/BackendRegistry.java @@ -0,0 +1,787 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportRequest; + +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.auth.internal.InternalAuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.internal.NoOpAuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.internal.NoOpAuthorizationBackend; +import com.amazon.opendistroforelasticsearch.security.configuration.AdminDNs; +import com.amazon.opendistroforelasticsearch.security.configuration.ConfigurationChangeListener; +import com.amazon.opendistroforelasticsearch.security.http.HTTPBasicAuthenticator; +import com.amazon.opendistroforelasticsearch.security.http.HTTPClientCertAuthenticator; +import com.amazon.opendistroforelasticsearch.security.http.HTTPProxyAuthenticator; +import com.amazon.opendistroforelasticsearch.security.http.XFFResolver; +import com.amazon.opendistroforelasticsearch.security.ssl.util.Utils; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.HTTPHelper; +import com.amazon.opendistroforelasticsearch.security.support.ReflectionHelper; +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; +import com.amazon.opendistroforelasticsearch.security.user.User; +import com.google.common.base.Strings; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.RemovalListener; +import com.google.common.cache.RemovalNotification; + +public class BackendRegistry implements ConfigurationChangeListener { + + protected final Logger log = LogManager.getLogger(this.getClass()); + private final Map authImplMap = new HashMap<>(); + private final SortedSet restAuthDomains = new TreeSet<>(); + private final Set restAuthorizers = new HashSet<>(); + private final SortedSet transportAuthDomains = new TreeSet<>(); + private final Set transportAuthorizers = new HashSet<>(); + private final List destroyableComponents = new LinkedList<>(); + private volatile boolean initialized; + private final AdminDNs adminDns; + private final XFFResolver xffResolver; + private volatile boolean anonymousAuthEnabled = false; + private final Settings esSettings; + private final Path configPath; + private final InternalAuthenticationBackend iab; + private final AuditLog auditLog; + private final ThreadPool threadPool; + private final UserInjector userInjector; + private final int ttlInMin; + private Cache userCache; + private Cache userCacheTransport; + private Cache authenticatedUserCacheTransport; + private Cache restImpersonationCache; + private volatile String transportUsernameAttribute = null; + + private void createCaches() { + userCache = CacheBuilder.newBuilder() + .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) + .removalListener(new RemovalListener() { + @Override + public void onRemoval(RemovalNotification notification) { + log.debug("Clear user cache for {} due to {}", notification.getKey().getUsername(), notification.getCause()); + } + }).build(); + + userCacheTransport = CacheBuilder.newBuilder() + .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) + .removalListener(new RemovalListener() { + @Override + public void onRemoval(RemovalNotification notification) { + log.debug("Clear user cache for {} due to {}", notification.getKey(), notification.getCause()); + } + }).build(); + + authenticatedUserCacheTransport = CacheBuilder.newBuilder() + .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) + .removalListener(new RemovalListener() { + @Override + public void onRemoval(RemovalNotification notification) { + log.debug("Clear user cache for {} due to {}", notification.getKey().getUsername(), notification.getCause()); + } + }).build(); + + restImpersonationCache = CacheBuilder.newBuilder() + .expireAfterWrite(ttlInMin, TimeUnit.MINUTES) + .removalListener(new RemovalListener() { + @Override + public void onRemoval(RemovalNotification notification) { + log.debug("Clear user cache for {} due to {}", notification.getKey(), notification.getCause()); + } + }).build(); + } + + public BackendRegistry(final Settings settings, final Path configPath, final AdminDNs adminDns, + final XFFResolver xffResolver, final InternalAuthenticationBackend iab, final AuditLog auditLog, final ThreadPool threadPool) { + this.adminDns = adminDns; + this.esSettings = settings; + this.configPath = configPath; + this.xffResolver = xffResolver; + this.iab = iab; + this.auditLog = auditLog; + this.threadPool = threadPool; + this.userInjector = new UserInjector(settings, threadPool, auditLog, xffResolver); + + authImplMap.put("intern_c", InternalAuthenticationBackend.class.getName()); + authImplMap.put("intern_z", NoOpAuthorizationBackend.class.getName()); + + authImplMap.put("internal_c", InternalAuthenticationBackend.class.getName()); + authImplMap.put("internal_z", NoOpAuthorizationBackend.class.getName()); + + authImplMap.put("noop_c", NoOpAuthenticationBackend.class.getName()); + authImplMap.put("noop_z", NoOpAuthorizationBackend.class.getName()); + + authImplMap.put("ldap_c", "com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); + authImplMap.put("ldap_z", "com.amazon.dlic.auth.ldap.backend.LDAPAuthorizationBackend"); + + authImplMap.put("basic_h", HTTPBasicAuthenticator.class.getName()); + authImplMap.put("proxy_h", HTTPProxyAuthenticator.class.getName()); + authImplMap.put("clientcert_h", HTTPClientCertAuthenticator.class.getName()); + authImplMap.put("kerberos_h", "com.amazon.dlic.auth.http.kerberos.HTTPSpnegoAuthenticator"); + authImplMap.put("jwt_h", "com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); + authImplMap.put("openid_h", "com.amazon.dlic.auth.http.jwt.keybyoidc.HTTPJwtKeyByOpenIdConnectAuthenticator"); + authImplMap.put("saml_h", "com.amazon.dlic.auth.http.saml.HTTPSamlAuthenticator"); + + this.ttlInMin = settings.getAsInt(ConfigConstants.OPENDISTRO_SECURITY_CACHE_TTL_MINUTES, 60); + + createCaches(); + } + + public boolean isInitialized() { + return initialized; + } + + public void invalidateCache() { + userCache.invalidateAll(); + userCacheTransport.invalidateAll(); + authenticatedUserCacheTransport.invalidateAll(); + restImpersonationCache.invalidateAll(); + } + + @Override + public void onChange(final Settings settings) { + + //TODO synchronize via semaphore/atomicref + restAuthDomains.clear(); + transportAuthDomains.clear(); + restAuthorizers.clear(); + transportAuthorizers.clear(); + invalidateCache(); + destroyDestroyables(); + transportUsernameAttribute = settings.get("opendistro_security.dynamic.transport_userrname_attribute", null); + anonymousAuthEnabled = settings.getAsBoolean("opendistro_security.dynamic.http.anonymous_auth_enabled", false) + && !esSettings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION, false); + + final Map authzDyn = settings.getGroups("opendistro_security.dynamic.authz"); + + for (final String ad : authzDyn.keySet()) { + final Settings ads = authzDyn.get(ad); + final boolean enabled = ads.getAsBoolean("enabled", true); + final boolean httpEnabled = enabled && ads.getAsBoolean("http_enabled", true); + final boolean transportEnabled = enabled && ads.getAsBoolean("transport_enabled", true); + + + if (httpEnabled || transportEnabled) { + try { + + final String authzBackendClazz = ads.get("authorization_backend.type", "noop"); + final AuthorizationBackend authorizationBackend; + + if(authzBackendClazz.equals(InternalAuthenticationBackend.class.getName()) //NOSONAR + || authzBackendClazz.equals("internal") + || authzBackendClazz.equals("intern")) { + authorizationBackend = iab; + ReflectionHelper.addLoadedModule(InternalAuthenticationBackend.class); + } else { + authorizationBackend = newInstance( + authzBackendClazz,"z", + Settings.builder().put(esSettings).put(ads.getAsSettings("authorization_backend.config")).build(), configPath); + } + + if (httpEnabled) { + restAuthorizers.add(authorizationBackend); + } + + if (transportEnabled) { + transportAuthorizers.add(authorizationBackend); + } + + if (authorizationBackend instanceof Destroyable) { + this.destroyableComponents.add((Destroyable) authorizationBackend); + } + } catch (final Exception e) { + log.error("Unable to initialize AuthorizationBackend {} due to {}", ad, e.toString(),e); + } + } + } + + final Map dyn = settings.getGroups("opendistro_security.dynamic.authc"); + + for (final String ad : dyn.keySet()) { + final Settings ads = dyn.get(ad); + final boolean enabled = ads.getAsBoolean("enabled", true); + final boolean httpEnabled = enabled && ads.getAsBoolean("http_enabled", true); + final boolean transportEnabled = enabled && ads.getAsBoolean("transport_enabled", true); + + if (httpEnabled || transportEnabled) { + try { + AuthenticationBackend authenticationBackend; + final String authBackendClazz = ads.get("authentication_backend.type", InternalAuthenticationBackend.class.getName()); + if(authBackendClazz.equals(InternalAuthenticationBackend.class.getName()) //NOSONAR + || authBackendClazz.equals("internal") + || authBackendClazz.equals("intern")) { + authenticationBackend = iab; + ReflectionHelper.addLoadedModule(InternalAuthenticationBackend.class); + } else { + authenticationBackend = newInstance( + authBackendClazz,"c", + Settings.builder().put(esSettings).put(ads.getAsSettings("authentication_backend.config")).build(), configPath); + } + + String httpAuthenticatorType = ads.get("http_authenticator.type"); //no default + HTTPAuthenticator httpAuthenticator = httpAuthenticatorType==null?null: (HTTPAuthenticator) newInstance(httpAuthenticatorType,"h", + Settings.builder().put(esSettings).put(ads.getAsSettings("http_authenticator.config")).build(), configPath); + + final AuthDomain _ad = new AuthDomain(authenticationBackend, httpAuthenticator, + ads.getAsBoolean("http_authenticator.challenge", true), ads.getAsInt("order", 0)); + + if (httpEnabled && _ad.getHttpAuthenticator() != null) { + restAuthDomains.add(_ad); + } + + if (transportEnabled) { + transportAuthDomains.add(_ad); + } + + if (httpAuthenticator instanceof Destroyable) { + this.destroyableComponents.add((Destroyable) httpAuthenticator); + } + + if (authenticationBackend instanceof Destroyable) { + this.destroyableComponents.add((Destroyable) authenticationBackend); + } + + } catch (final Exception e) { + log.error("Unable to initialize auth domain {} due to {}", ad, e.toString(), e); + } + + } + } + + //Open Distro Security no default authc + initialized = !restAuthDomains.isEmpty() || anonymousAuthEnabled; + } + + public User authenticate(final TransportRequest request, final String sslPrincipal, final Task task, final String action) { + + if(log.isDebugEnabled() && request.remoteAddress() != null) { + log.debug("Transport authentication request from {}", request.remoteAddress()); + } + + User origPKIUser = new User(sslPrincipal); + + if(adminDns.isAdmin(origPKIUser)) { + auditLog.logSucceededLogin(origPKIUser.getName(), true, null, request, action, task); + return origPKIUser; + } + + final String authorizationHeader = threadPool.getThreadContext().getHeader("Authorization"); + //Use either impersonation OR credentials authentication + //if both is supplied credentials authentication win + final AuthCredentials creds = HTTPHelper.extractCredentials(authorizationHeader, log); + + User impersonatedTransportUser = null; + + if(creds != null) { + if(log.isDebugEnabled()) { + log.debug("User {} submitted also basic credentials: {}", origPKIUser.getName(), creds); + } + } + + //loop over all transport auth domains + for (final AuthDomain authDomain: transportAuthDomains) { + + User authenticatedUser = null; + + if(creds == null) { + //no credentials submitted + //impersonation possible + impersonatedTransportUser = impersonate(request, origPKIUser); + origPKIUser = resolveTransportUsernameAttribute(origPKIUser); + authenticatedUser = checkExistsAndAuthz(userCacheTransport, impersonatedTransportUser==null?origPKIUser:impersonatedTransportUser, authDomain.getBackend(), transportAuthorizers); + } else { + //auth credentials submitted + //impersonation not possible, if requested it will be ignored + authenticatedUser = authcz(authenticatedUserCacheTransport, creds, authDomain.getBackend(), transportAuthorizers); + } + + if(authenticatedUser == null) { + if(log.isDebugEnabled()) { + log.debug("Cannot authenticate user {} (or add roles) with authdomain {}/{}, try next", creds==null?(impersonatedTransportUser==null?origPKIUser.getName():impersonatedTransportUser.getName()):creds.getUsername(), authDomain.getBackend().getType(), authDomain.getOrder()); + } + continue; + } + + if(adminDns.isAdmin(authenticatedUser)) { + log.error("Cannot authenticate user because admin user is not permitted to login"); + auditLog.logFailedLogin(authenticatedUser.getName(), true, null, request, task); + return null; + } + + if(log.isDebugEnabled()) { + log.debug("User '{}' is authenticated", authenticatedUser); + } + + auditLog.logSucceededLogin(authenticatedUser.getName(), false, impersonatedTransportUser==null?null:origPKIUser.getName(), request, action, task); + + return authenticatedUser; + }//end looping auth domains + + + //auditlog + if(creds == null) { + auditLog.logFailedLogin(impersonatedTransportUser==null?origPKIUser.getName():impersonatedTransportUser.getName(), false, impersonatedTransportUser==null?null:origPKIUser.getName(), request, task); + } else { + auditLog.logFailedLogin(creds.getUsername(), false, null, request, task); + } + + log.warn("Transport authentication finally failed for {} from {}", creds == null ? impersonatedTransportUser==null?origPKIUser.getName():impersonatedTransportUser.getName():creds.getUsername(), request.remoteAddress()); + + return null; + } + + + /** + * + * @param request + * @param channel + * @return The authenticated user, null means another roundtrip + * @throws ElasticsearchSecurityException + */ + public boolean authenticate(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) { + + final String sslPrincipal = (String) threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); + + if(adminDns.isAdminDN(sslPrincipal)) { + //PKI authenticated REST call + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User(sslPrincipal)); + auditLog.logSucceededLogin(sslPrincipal, true, null, request); + return true; + } + + if (userInjector.injectUser(request)) { + // ThreadContext injected user + return true; + } + + if (!isInitialized()) { + log.error("Not yet initialized (you may need to run securityadmin)"); + channel.sendResponse(new BytesRestResponse(RestStatus.SERVICE_UNAVAILABLE, "Open Distro Security not initialized (SG11).")); + return false; + } + + final TransportAddress remoteAddress = xffResolver.resolve(request); + + if(log.isDebugEnabled()) { + log.debug("Rest authentication request from {} [original: {}]", remoteAddress, request.getRemoteAddress()); + } + + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, remoteAddress); + + boolean authenticated = false; + + User authenticatedUser = null; + + AuthCredentials authCredenetials = null; + + HTTPAuthenticator firstChallengingHttpAuthenticator = null; + + //loop over all http/rest auth domains + for (final AuthDomain authDomain: restAuthDomains) { + + final HTTPAuthenticator httpAuthenticator = authDomain.getHttpAuthenticator(); + + if(authDomain.isChallenge() && firstChallengingHttpAuthenticator == null) { + firstChallengingHttpAuthenticator = httpAuthenticator; + } + + if(log.isTraceEnabled()) { + log.trace("Try to extract auth creds from {} http authenticator", httpAuthenticator.getType()); + } + final AuthCredentials ac; + try { + ac = httpAuthenticator.extractCredentials(request, threadContext); + } catch (Exception e1) { + if(log.isDebugEnabled()) { + log.debug("'{}' extracting credentials from {} http authenticator", e1.toString(), httpAuthenticator.getType(), e1); + } + continue; + } + authCredenetials = ac; + + if (ac == null) { + //no credentials found in request + if(anonymousAuthEnabled) { + continue; + } + + if(authDomain.isChallenge() && httpAuthenticator.reRequestAuthentication(channel, null)) { + auditLog.logFailedLogin("", false, null, request); + log.trace("No 'Authorization' header, send 401 and 'WWW-Authenticate Basic'"); + return false; + } else { + //no reRequest possible + log.trace("No 'Authorization' header, send 403"); + continue; + } + } else { + org.apache.logging.log4j.ThreadContext.put("user", ac.getUsername()); + if (!ac.isComplete()) { + //credentials found in request but we need another client challenge + if(httpAuthenticator.reRequestAuthentication(channel, ac)) { + //auditLog.logFailedLogin(ac.getUsername()+" ", request); --noauditlog + return false; + } else { + //no reRequest possible + continue; + } + + } + } + + //http completed + authenticatedUser = authcz(userCache, ac, authDomain.getBackend(), restAuthorizers); + + if(authenticatedUser == null) { + if(log.isDebugEnabled()) { + log.debug("Cannot authenticate user {} (or add roles) with authdomain {}/{}, try next", ac.getUsername(), authDomain.getBackend().getType(), authDomain.getOrder()); + } + continue; + } + + if(adminDns.isAdmin(authenticatedUser)) { + log.error("Cannot authenticate user because admin user is not permitted to login via HTTP"); + auditLog.logFailedLogin(authenticatedUser.getName(), true, null, request); + channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, "Cannot authenticate user because admin user is not permitted to login via HTTP")); + return false; + } + + final String tenant = Utils.coalesce(request.header("securitytenant"), request.header("security_tenant")); + + if(log.isDebugEnabled()) { + log.debug("User '{}' is authenticated", authenticatedUser); + log.debug("securitytenant '{}'", tenant); + } + + authenticatedUser.setRequestedTenant(tenant); + authenticated = true; + break; + }//end looping auth domains + + + if(authenticated) { + final User impersonatedUser = impersonate(request, authenticatedUser); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, impersonatedUser==null?authenticatedUser:impersonatedUser); + auditLog.logSucceededLogin((impersonatedUser==null?authenticatedUser:impersonatedUser).getName(), false, authenticatedUser.getName(), request); + } else { + if(log.isDebugEnabled()) { + log.debug("User still not authenticated after checking {} auth domains", restAuthDomains.size()); + } + + if(authCredenetials == null && anonymousAuthEnabled) { + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, User.ANONYMOUS); + auditLog.logSucceededLogin(User.ANONYMOUS.getName(), false, null, request); + if(log.isDebugEnabled()) { + log.debug("Anonymous User is authenticated"); + } + return true; + } + + if(firstChallengingHttpAuthenticator != null) { + + if(log.isDebugEnabled()) { + log.debug("Rerequest with {}", firstChallengingHttpAuthenticator.getClass()); + } + + if(firstChallengingHttpAuthenticator.reRequestAuthentication(channel, null)) { + if(log.isDebugEnabled()) { + log.debug("Rerequest {} failed", firstChallengingHttpAuthenticator.getClass()); + } + + log.warn("Authentication finally failed for {} from {}", authCredenetials == null ? null:authCredenetials.getUsername(), remoteAddress); + auditLog.logFailedLogin(authCredenetials == null ? null:authCredenetials.getUsername(), false, null, request); + return false; + } + } + + log.warn("Authentication finally failed for {} from {}", authCredenetials == null ? null:authCredenetials.getUsername(), remoteAddress); + auditLog.logFailedLogin(authCredenetials == null ? null:authCredenetials.getUsername(), false, null, request); + channel.sendResponse(new BytesRestResponse(RestStatus.UNAUTHORIZED, "Authentication finally failed")); + return false; + } + + return authenticated; + } + + /** + * no auditlog, throw no exception, does also authz for all authorizers + * + * @param cache + * @param ac + * @param authDomain + * @return null if user cannot b authenticated + */ + private User checkExistsAndAuthz(final Cache cache, final User user, final AuthenticationBackend authenticationBackend, final Set authorizers) { + if(user == null) { + return null; + } + + try { + return cache.get(user.getName(), new Callable() { + @Override + public User call() throws Exception { + if(log.isDebugEnabled()) { + log.debug(user.getName()+" not cached, return from "+authenticationBackend.getType()+" backend directly"); + } + if(authenticationBackend.exists(user)) { + for (final AuthorizationBackend ab : authorizers) { + try { + ab.fillRoles(user, new AuthCredentials(user.getName())); + } catch (Exception e) { + log.error("Cannot retrieve roles for {} from {} due to {}", user.getName(), ab.getType(), e.toString(), e); + } + } + + return user; + + } + + if(log.isDebugEnabled()) { + log.debug("User "+user.getName()+" does not exist in "+authenticationBackend.getType()); + } + return null; + } + }); + } catch (Exception e) { + if(log.isDebugEnabled()) { + log.debug("Can not check and authorize "+user.getName()+" due to "+e.toString(), e); + } + return null; + } + } + /** + * no auditlog, throw no exception, does also authz for all authorizers + * + * @param cache + * @param ac + * @param authDomain + * @return null if user cannot b authenticated + */ + private User authcz(final Cache cache, final AuthCredentials ac, final AuthenticationBackend authBackend, final Set authorizers) { + if(ac == null) { + return null; + } + try { + + //noop backend configured and no authorizers + //that mean authc and authz was completely done via HTTP (like JWT or PKI) + if(authBackend.getClass() == NoOpAuthenticationBackend.class && authorizers.isEmpty()) { + //no cache + return authBackend.authenticate(ac); + } + + + + return cache.get(ac, new Callable() { + @Override + public User call() throws Exception { + if(log.isDebugEnabled()) { + log.debug(ac.getUsername()+" not cached, return from "+authBackend.getType()+" backend directly"); + } + final User authenticatedUser = authBackend.authenticate(ac); + for (final AuthorizationBackend ab : authorizers) { + try { + ab.fillRoles(authenticatedUser, new AuthCredentials(authenticatedUser.getName())); + } catch (Exception e) { + log.error("Cannot retrieve roles for {} from {} due to {}", authenticatedUser, ab.getType(), e.toString(), e); + } + } + + return authenticatedUser; + } + }); + } catch (Exception e) { + if(log.isDebugEnabled()) { + log.debug("Can not authenticate "+ac.getUsername()+" due to "+e.toString(), e); + } + return null; + } finally { + ac.clearSecrets(); + } + } + + private User impersonate(final TransportRequest tr, final User origPKIuser) throws ElasticsearchSecurityException { + + final String impersonatedUser = threadPool.getThreadContext().getHeader("opendistro_security_impersonate_as"); + + if(Strings.isNullOrEmpty(impersonatedUser)) { + return null; //nothing to do + } + + if (!isInitialized()) { + throw new ElasticsearchSecurityException("Could not check for impersonation because Open Distro Security is not yet initialized"); + } + + if (origPKIuser == null) { + throw new ElasticsearchSecurityException("no original PKI user found"); + } + + User aU = origPKIuser; + + if (adminDns.isAdminDN(impersonatedUser)) { + throw new ElasticsearchSecurityException("'"+origPKIuser.getName() + "' is not allowed to impersonate as an adminuser '" + impersonatedUser+"'"); + } + + try { + if (impersonatedUser != null && !adminDns.isTransportImpersonationAllowed(new LdapName(origPKIuser.getName()), impersonatedUser)) { + throw new ElasticsearchSecurityException("'"+origPKIuser.getName() + "' is not allowed to impersonate as '" + impersonatedUser+"'"); + } else if (impersonatedUser != null) { + aU = new User(impersonatedUser); + if(log.isDebugEnabled()) { + log.debug("Impersonate from '{}' to '{}'",origPKIuser.getName(), impersonatedUser); + } + } + } catch (final InvalidNameException e1) { + throw new ElasticsearchSecurityException("PKI does not have a valid name ('" + origPKIuser.getName() + "'), should never happen", + e1); + } + + return aU; + } + + private User impersonate(final RestRequest request, final User originalUser) throws ElasticsearchSecurityException { + + final String impersonatedUserHeader = request.header("opendistro_security_impersonate_as"); + + if (Strings.isNullOrEmpty(impersonatedUserHeader) || originalUser == null) { + return null; // nothing to do + } + + if (!isInitialized()) { + throw new ElasticsearchSecurityException("Could not check for impersonation because Open Distro Security is not yet initialized"); + } + + if (adminDns.isAdminDN(impersonatedUserHeader)) { + throw new ElasticsearchSecurityException("It is not allowed to impersonate as an adminuser '" + impersonatedUserHeader + "'", + RestStatus.FORBIDDEN); + } + + if (!adminDns.isRestImpersonationAllowed(originalUser.getName(), impersonatedUserHeader)) { + throw new ElasticsearchSecurityException("'" + originalUser.getName() + "' is not allowed to impersonate as '" + impersonatedUserHeader + + "'", RestStatus.FORBIDDEN); + } else { + //loop over all http/rest auth domains + for (final AuthDomain authDomain: restAuthDomains) { + final AuthenticationBackend authenticationBackend = authDomain.getBackend(); + final User impersonatedUser = checkExistsAndAuthz(restImpersonationCache, new User(impersonatedUserHeader), authenticationBackend, restAuthorizers); + + if(impersonatedUser == null) { + log.debug("Unable to impersonate rest user from '{}' to '{}' because the impersonated user does not exists in {}, try next ...", originalUser.getName(), impersonatedUserHeader, authenticationBackend.getType()); + continue; + } + + if (log.isDebugEnabled()) { + log.debug("Impersonate rest user from '{}' to '{}'", originalUser.getName(), impersonatedUserHeader); + } + return impersonatedUser; + } + + log.debug("Unable to impersonate rest user from '{}' to '{}' because the impersonated user does not exists", originalUser.getName(), impersonatedUserHeader); + throw new ElasticsearchSecurityException("No such user:" + impersonatedUserHeader, RestStatus.FORBIDDEN); + } + + } + + private T newInstance(final String clazzOrShortcut, String type, final Settings settings, final Path configPath) { + + String clazz = clazzOrShortcut; + boolean isEnterprise = false; + + if(authImplMap.containsKey(clazz+"_"+type)) { + clazz = authImplMap.get(clazz+"_"+type); + } else { + isEnterprise = true; + } + + if(ReflectionHelper.isEnterpriseAAAModule(clazz)) { + isEnterprise = true; + } + + return ReflectionHelper.instantiateAAA(clazz, settings, configPath, isEnterprise); + } + + private void destroyDestroyables() { + for (Destroyable destroyable : this.destroyableComponents) { + try { + destroyable.destroy(); + } catch (Exception e) { + log.error("Error while destroying " + destroyable, e); + } + } + + this.destroyableComponents.clear(); + } + + private User resolveTransportUsernameAttribute(User pkiUser) { + //#547 + if(transportUsernameAttribute != null && !transportUsernameAttribute.isEmpty()) { + try { + final LdapName sslPrincipalAsLdapName = new LdapName(pkiUser.getName()); + for(final Rdn rdn: sslPrincipalAsLdapName.getRdns()) { + if(rdn.getType().equals(transportUsernameAttribute)) { + return new User((String) rdn.getValue()); + } + } + } catch (InvalidNameException e) { + //cannot happen + } + } + + return pkiUser; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/Destroyable.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/Destroyable.java new file mode 100644 index 0000000000..780e60b810 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/Destroyable.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth; + +public interface Destroyable { + void destroy(); +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/HTTPAuthenticator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/HTTPAuthenticator.java new file mode 100644 index 0000000000..937a82746d --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/HTTPAuthenticator.java @@ -0,0 +1,90 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestRequest; + +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; + +/** + * Open Distro Security custom HTTP authenticators need to implement this interface. + *

+ * A HTTP authenticator extracts {@link AuthCredentials} from a {@link RestRequest} + *

+ * + * Implementation classes must provide a public constructor + *

+ * {@code public MyHTTPAuthenticator(org.elasticsearch.common.settings.Settings settings, java.nio.file.Path configPath)} + *

+ * The constructor should not throw any exception in case of an initialization problem. + * Instead catch all exceptions and log a appropriate error message. A logger can be instantiated like: + *

+ * {@code private final Logger log = LogManager.getLogger(this.getClass());} + *

+ */ +public interface HTTPAuthenticator { + + /** + * The type (name) of the authenticator. Only for logging. + * @return the type + */ + String getType(); + + /** + * Extract {@link AuthCredentials} from {@link RestRequest} + * + * @param request The rest request + * @param context The current thread context + * @return The authentication credentials (complete or incomplete) or null when no credentials are found in the request + *

+ * When the credentials could be fully extracted from the request {@code .markComplete()} must be called on the {@link AuthCredentials} which are returned. + * If the authentication flow needs another roundtrip with the request originator do not mark it as complete. + * @throws ElasticsearchSecurityException + */ + AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws ElasticsearchSecurityException; + + /** + * If the {@code extractCredentials()} call was not successful or the authentication flow needs another roundtrip this method + * will be called. If the custom HTTP authenticator does not support this method is a no-op and false should be returned. + * + * If the custom HTTP authenticator does support re-request authentication or supports authentication flows with multiple roundtrips + * then the response should be sent (through the channel) and true must be returned. + * + * @param channel The rest channel to sent back the response via {@code channel.sendResponse()} + * @param credentials The credentials from the prior authentication attempt + * @return false if re-request is not supported/necessary, true otherwise. + * If true is returned {@code channel.sendResponse()} must be called so that the request completes. + */ + boolean reRequestAuthentication(final RestChannel channel, AuthCredentials credentials); +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/UserInjector.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/UserInjector.java new file mode 100644 index 0000000000..76ec21f76d --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/UserInjector.java @@ -0,0 +1,156 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.http.XFFResolver; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.OpenDistroSecurityUtils; +import com.amazon.opendistroforelasticsearch.security.user.User; +import com.google.common.base.Strings; + +public class UserInjector { + + protected final Logger log = LogManager.getLogger(UserInjector.class); + + private final ThreadPool threadPool; + private final AuditLog auditLog; + private final XFFResolver xffResolver; + private final Boolean injectUserEnabled; + + UserInjector(Settings settings, ThreadPool threadPool, AuditLog auditLog, XFFResolver xffResolver) { + this.threadPool = threadPool; + this.auditLog = auditLog; + this.xffResolver = xffResolver; + this.injectUserEnabled = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false); + + } + + boolean injectUser(RestRequest request) { + + if (!injectUserEnabled) { + return false; + } + + String injectedUserString = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER); + + if (log.isDebugEnabled()) { + log.debug("Injected user string: {}", injectedUserString); + } + + if (Strings.isNullOrEmpty(injectedUserString)) { + return false; + } + // username|role1,role2|remoteIP:port|attributeKey,attributeValue,attributeKey,attributeValue, ...|requestedTenant + String[] parts = injectedUserString.split("\\|"); + + if (parts.length == 0) { + log.error("User string malformed, could not extract parts. User string was '{}.' User injection failed.", injectedUserString); + return false; + } + + // username + if (Strings.isNullOrEmpty(parts[0])) { + log.error("Username must not be null, user string was '{}.' User injection failed.", injectedUserString); + return false; + } + + final User user = new User(parts[0]); + + // backend roles + if (parts.length > 1 && !Strings.isNullOrEmpty(parts[1])) { + if (parts[1].length() > 0) { + user.addRoles(Arrays.asList(parts[1].split(","))); + } + } + + // custom attributes + if (parts.length > 3 && !Strings.isNullOrEmpty(parts[3])) { + Map attributes = OpenDistroSecurityUtils.mapFromArray((parts[3].split(","))); + if (attributes == null) { + log.error("Could not parse custom attributes {}, user injection failed.", parts[3]); + return false; + } else { + user.addAttributes(attributes); + } + } + + // requested tenant + if (parts.length > 4 && !Strings.isNullOrEmpty(parts[4])) { + user.setRequestedTenant(parts[4]); + } + + // remote IP - we can set it only once, so we do it last. If non is given, + // BackendRegistry/XFFResolver will do the job + if (parts.length > 2 && !Strings.isNullOrEmpty(parts[2])) { + // format is ip:port + String[] ipAndPort = parts[2].split(":"); + if (ipAndPort.length != 2) { + log.error("Remote address must have format ip:port, was: {}. User injection failed.", parts[2]); + return false; + } else { + try { + InetAddress iAdress = InetAddress.getByName(ipAndPort[0]); + int port = Integer.parseInt(ipAndPort[1]); + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, new TransportAddress(iAdress, port)); + } catch (UnknownHostException | NumberFormatException e) { + log.error("Cannot parse remote IP or port: {}, user injection failed.", parts[2], e); + return false; + } + } + } else { + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, xffResolver.resolve(request)); + } + + // mark user injected for proper admin handling + user.setInjected(true); + + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); + auditLog.logSucceededLogin(parts[0], true, null, request); + if (log.isTraceEnabled()) { + log.trace("Injected user object:{} ", user.toString()); + } + return true; + + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/InternalAuthenticationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/InternalAuthenticationBackend.java new file mode 100644 index 0000000000..ffb83a2d34 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/InternalAuthenticationBackend.java @@ -0,0 +1,176 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth.internal; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.settings.Settings; + +import com.amazon.opendistroforelasticsearch.security.auth.AuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.AuthorizationBackend; +import com.amazon.opendistroforelasticsearch.security.configuration.ConfigurationRepository; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class InternalAuthenticationBackend implements AuthenticationBackend, AuthorizationBackend { + + private final ConfigurationRepository configurationRepository; + + public InternalAuthenticationBackend(final ConfigurationRepository configurationRepository) { + super(); + this.configurationRepository = configurationRepository; + } + + @Override + public boolean exists(User user) { + + final Settings cfg = getConfigSettings(); + if (cfg == null) { + return false; + } + + String hashed = cfg.get(user.getName() + ".hash"); + + if (hashed == null) { + + for(String username:cfg.names()) { + String u = cfg.get(username + ".username"); + if(user.getName().equals(u)) { + hashed = cfg.get(username+ ".hash"); + break; + } + } + + if(hashed == null) { + return false; + } + } + + final List roles = cfg.getAsList(user.getName() + ".roles", Collections.emptyList()); + + if(roles != null) { + user.addRoles(roles); + } + + return true; + } + + @Override + public User authenticate(final AuthCredentials credentials) { + + final Settings cfg = getConfigSettings(); + if (cfg == null) { + throw new ElasticsearchSecurityException("Internal authentication backend not configured. May be Open Distro Security is not initialized"); + + } + + String hashed = cfg.get(credentials.getUsername() + ".hash"); + + if (hashed == null) { + + for(String username:cfg.names()) { + String u = cfg.get(username + ".username"); + if(credentials.getUsername().equals(u)) { + hashed = cfg.get(username+ ".hash"); + break; + } + } + + if(hashed == null) { + throw new ElasticsearchSecurityException(credentials.getUsername() + " not found"); + } + } + + final byte[] password = credentials.getPassword(); + + if(password == null || password.length == 0) { + throw new ElasticsearchSecurityException("empty passwords not supported"); + } + + ByteBuffer wrap = ByteBuffer.wrap(password); + CharBuffer buf = StandardCharsets.UTF_8.decode(wrap); + char[] array = new char[buf.limit()]; + buf.get(array); + + Arrays.fill(password, (byte)0); + + try { + if (OpenBSDBCrypt.checkPassword(hashed, array)) { + final List roles = cfg.getAsList(credentials.getUsername() + ".roles", Collections.emptyList()); + final Settings customAttributes = cfg.getAsSettings(credentials.getUsername() + ".attributes"); + + if(customAttributes != null) { + for(String attributeName: customAttributes.names()) { + credentials.addAttribute("attr.internal."+attributeName, customAttributes.get(attributeName)); + } + } + + return new User(credentials.getUsername(), roles, credentials); + } else { + throw new ElasticsearchSecurityException("password does not match"); + } + } finally { + Arrays.fill(wrap.array(), (byte)0); + Arrays.fill(buf.array(), '\0'); + Arrays.fill(array, '\0'); + } + } + + @Override + public String getType() { + return "internal"; + } + + private Settings getConfigSettings() { + return configurationRepository.getConfiguration(ConfigConstants.CONFIGNAME_INTERNAL_USERS, false); + } + + @Override + public void fillRoles(User user, AuthCredentials credentials) throws ElasticsearchSecurityException { + final Settings cfg = getConfigSettings(); + if (cfg == null) { + throw new ElasticsearchSecurityException("Internal authentication backend not configured. May be Open Distro Security is not initialized."); + + } + final List roles = cfg.getAsList(credentials.getUsername() + ".roles", Collections.emptyList()); + if(roles != null && !roles.isEmpty() && user != null) { + user.addRoles(roles); + } + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthenticationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthenticationBackend.java new file mode 100644 index 0000000000..e1ef693c3b --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthenticationBackend.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth.internal; + +import java.nio.file.Path; + +import org.elasticsearch.common.settings.Settings; + +import com.amazon.opendistroforelasticsearch.security.auth.AuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class NoOpAuthenticationBackend implements AuthenticationBackend { + + public NoOpAuthenticationBackend(final Settings settings, final Path configPath) { + super(); + } + + @Override + public String getType() { + return "noop"; + } + + @Override + public User authenticate(final AuthCredentials credentials) { + return new User(credentials.getUsername(), credentials.getBackendRoles(), credentials); + } + + @Override + public boolean exists(User user) { + return true; + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthorizationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthorizationBackend.java new file mode 100644 index 0000000000..0f772cca9c --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthorizationBackend.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth.internal; + +import java.nio.file.Path; + +import org.elasticsearch.common.settings.Settings; + +import com.amazon.opendistroforelasticsearch.security.auth.AuthorizationBackend; +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class NoOpAuthorizationBackend implements AuthorizationBackend { + + public NoOpAuthorizationBackend(final Settings settings, final Path configPath) { + super(); + } + + @Override + public String getType() { + return "noop"; + } + + @Override + public void fillRoles(final User user, final AuthCredentials authCreds) { + // no-op + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceConfig.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceConfig.java new file mode 100644 index 0000000000..a58ec11957 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceConfig.java @@ -0,0 +1,326 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.compliance; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +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.ExecutionException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer; +import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer.Resolved; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + + +public class ComplianceConfig { + + private final Logger log = LogManager.getLogger(getClass()); + private final Settings settings; + private final Map> readEnabledFields = new HashMap<>(100); + private final List watchedWriteIndices; + private DateTimeFormatter auditLogPattern = null; + private String auditLogIndex = null; + private final boolean logDiffsForWrite; + private final boolean logWriteMetadataOnly; + private final boolean logReadMetadataOnly; + private final boolean logExternalConfig; + private final boolean logInternalConfig; + private final LoadingCache> cache; + private final Set immutableIndicesPatterns; + private final byte[] salt16; + private final String opendistrosecurityIndex; + private final IndexResolverReplacer irr; + private final Environment environment; + private final AuditLog auditLog; + private volatile boolean enabled = true; + private volatile boolean externalConfigLogged = false; + + public ComplianceConfig(final Environment environment, final IndexResolverReplacer irr, final AuditLog auditLog) { + super(); + this.settings = environment.settings(); + this.environment = environment; + this.irr = irr; + this.auditLog = auditLog; + final List watchedReadFields = this.settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS, + Collections.emptyList(), false); + + watchedWriteIndices = settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES, Collections.emptyList()); + logDiffsForWrite = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS, false); + logWriteMetadataOnly = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY, false); + logReadMetadataOnly = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_METADATA_ONLY, false); + logExternalConfig = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED, false); + logInternalConfig = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED, false); + immutableIndicesPatterns = new HashSet(settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_IMMUTABLE_INDICES, Collections.emptyList())); + final String saltAsString = settings.get(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT, ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT_DEFAULT); + final byte[] saltAsBytes = saltAsString.getBytes(StandardCharsets.UTF_8); + + if(saltAsString.equals(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT_DEFAULT)) { + log.warn("If you plan to use field masking pls configure "+ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT+" to be a random string of 16 chars length identical on all nodes"); + } + + if(saltAsBytes.length < 16) { + throw new ElasticsearchException(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT+" must at least contain 16 bytes"); + } + + if(saltAsBytes.length > 16) { + log.warn(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT+" is greater than 16 bytes. Only the first 16 bytes are used for salting"); + } + + salt16 = Arrays.copyOf(saltAsBytes, 16); + this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + + //opendistro_security.compliance.pii_fields: + // - indexpattern,fieldpattern,fieldpattern,.... + for(String watchedReadField: watchedReadFields) { + final List split = new ArrayList<>(Arrays.asList(watchedReadField.split(","))); + if(split.isEmpty()) { + continue; + } else if(split.size() == 1) { + readEnabledFields.put(split.get(0), Collections.singleton("*")); + } else { + Set _fields = new HashSet(split.subList(1, split.size())); + readEnabledFields.put(split.get(0), _fields); + } + } + + final String type = settings.get(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_TYPE_DEFAULT, null); + if("internal_elasticsearch".equalsIgnoreCase(type)) { + final String index = settings.get(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ES_INDEX,"'security-auditlog-'YYYY.MM.dd"); + try { + auditLogPattern = DateTimeFormat.forPattern(index); //throws IllegalArgumentException if no pattern + } catch (IllegalArgumentException e) { + //no pattern + auditLogIndex = index; + } catch (Exception e) { + log.error("Unable to check if auditlog index {} is part of compliance setup", index, e); + } + } + + log.info("PII configuration [auditLogPattern={}, auditLogIndex={}]: {}", auditLogPattern, auditLogIndex, readEnabledFields); + + + cache = CacheBuilder.newBuilder() + .maximumSize(1000) + .build(new CacheLoader>() { + @Override + public Set load(String index) throws Exception { + return getFieldsForIndex0(index); + } + }); + } + + public boolean isLogExternalConfig() { + return logExternalConfig; + } + + public boolean isExternalConfigLogged() { + return externalConfigLogged; + } + + public void setExternalConfigLogged(boolean externalConfigLogged) { + this.externalConfigLogged = externalConfigLogged; + } + + public boolean isEnabled() { + return this.enabled; + } + + //cached + @SuppressWarnings("unchecked") + private Set getFieldsForIndex0(String index) { + + if(index == null) { + return Collections.EMPTY_SET; + } + + if(auditLogIndex != null && auditLogIndex.equalsIgnoreCase(index)) { + return Collections.EMPTY_SET; + } + + if(auditLogPattern != null) { + if(index.equalsIgnoreCase(getExpandedIndexName(auditLogPattern, null))) { + return Collections.EMPTY_SET; + } + } + + final Set tmp = new HashSet(100); + for(String indexPattern: readEnabledFields.keySet()) { + if(indexPattern != null && !indexPattern.isEmpty() && WildcardMatcher.match(indexPattern, index)) { + tmp.addAll(readEnabledFields.get(indexPattern)); + } + } + return tmp; + } + + private String getExpandedIndexName(DateTimeFormatter indexPattern, String index) { + if(indexPattern == null) { + return index; + } + return indexPattern.print(DateTime.now(DateTimeZone.UTC)); + } + + //do not check for isEnabled + public boolean writeHistoryEnabledForIndex(String index) { + + if(index == null) { + return false; + } + + if(opendistrosecurityIndex.equals(index)) { + return logInternalConfig; + } + + if(auditLogIndex != null && auditLogIndex.equalsIgnoreCase(index)) { + return false; + } + + if(auditLogPattern != null) { + if(index.equalsIgnoreCase(getExpandedIndexName(auditLogPattern, null))) { + return false; + } + } + + return WildcardMatcher.matchAny(watchedWriteIndices, index); + } + + //no patterns here as parameters + //check for isEnabled + public boolean readHistoryEnabledForIndex(String index) { + + if(!this.enabled) { + return false; + } + + if(opendistrosecurityIndex.equals(index)) { + return logInternalConfig; + } + + try { + return !cache.get(index).isEmpty(); + } catch (ExecutionException e) { + log.error(e); + return true; + } + } + + //no patterns here as parameters + //check for isEnabled + public boolean readHistoryEnabledForField(String index, String field) { + + if(!this.enabled) { + return false; + } + + if(opendistrosecurityIndex.equals(index)) { + return logInternalConfig; + } + + try { + final Set fields = cache.get(index); + if(fields.isEmpty()) { + return false; + } + + return WildcardMatcher.matchAny(fields, field); + } catch (ExecutionException e) { + log.error(e); + return true; + } + } + + public boolean logDiffsForWrite() { + return !logWriteMetadataOnly() && logDiffsForWrite; + } + + public boolean logWriteMetadataOnly() { + return logWriteMetadataOnly; + } + + public boolean logReadMetadataOnly() { + return logReadMetadataOnly; + } + + public Settings getSettings() { + return settings; + } + + public Environment getEnvironment() { + return environment; + } + + + //check for isEnabled + public boolean isIndexImmutable(Object request) { + + if(!this.enabled) { + return false; + } + + if(immutableIndicesPatterns.isEmpty()) { + return false; + } + + final Resolved resolved = irr.resolveRequest(request); + final Set allIndices = resolved.getAllIndices(); + + //assert allIndices.size() == 1:"only one index here, not "+allIndices; + //assert allIndices.contains("_all"):"no _all in "+allIndices; + //assert allIndices.contains("*"):"no * in "+allIndices; + //assert allIndices.contains(""):"no EMPTY in "+allIndices; + + return WildcardMatcher.matchAny(immutableIndicesPatterns, allIndices); + } + + public byte[] getSalt16() { + return salt16.clone(); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceIndexingOperationListener.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceIndexingOperationListener.java new file mode 100644 index 0000000000..a383c5f144 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceIndexingOperationListener.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.compliance; + +import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.shard.IndexingOperationListener; + +/** + * noop impl + * + * + */ +public class ComplianceIndexingOperationListener implements IndexingOperationListener { + + public void setIs(IndexService is) { + //noop + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ActionGroupHolder.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ActionGroupHolder.java new file mode 100644 index 0000000000..eeadd9860a --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ActionGroupHolder.java @@ -0,0 +1,97 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.elasticsearch.common.settings.Settings; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; + +public class ActionGroupHolder { + + final ConfigurationRepository configurationRepository; + + public ActionGroupHolder(final ConfigurationRepository configurationRepository) { + this.configurationRepository = configurationRepository; + } + + public Set getGroupMembers(final String groupname) { + + final Settings actionGroups = getSettings(); + + if (actionGroups == null) { + return Collections.emptySet(); + } + + return resolve(actionGroups, groupname); + } + + private Set resolve(final Settings actionGroups, final String entry) { + + final Set ret = new HashSet(); + + List en = actionGroups.getAsList(entry); + if (en.isEmpty()) { + // try Open Distro Security format including readonly and permissions key + en = actionGroups.getAsList(entry +"." + ConfigConstants.CONFIGKEY_ACTION_GROUPS_PERMISSIONS); + } + for (String string: en) { + if (actionGroups.names().contains(string)) { + ret.addAll(resolve(actionGroups,string)); + } else { + ret.add(string); + } + } + return ret; + } + + public Set resolvedActions(final List actions) { + final Set resolvedActions = new HashSet(); + for (String string: actions) { + final Set groups = getGroupMembers(string); + if (groups.isEmpty()) { + resolvedActions.add(string); + } else { + resolvedActions.addAll(groups); + } + } + + return resolvedActions; + } + + private Settings getSettings() { + return configurationRepository.getConfiguration(ConfigConstants.CONFIGNAME_ACTION_GROUPS, false); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/AdminDNs.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/AdminDNs.java new file mode 100644 index 0000000000..6562e384cb --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/AdminDNs.java @@ -0,0 +1,159 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.settings.Settings; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; +import com.amazon.opendistroforelasticsearch.security.user.User; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; + +public class AdminDNs { + + protected final Logger log = LogManager.getLogger(AdminDNs.class); + private final Set adminDn = new HashSet(); + private final Set adminUsernames = new HashSet(); + private final ListMultimap allowedImpersonations = ArrayListMultimap. create(); + private final ListMultimap allowedRestImpersonations = ArrayListMultimap. create(); + private boolean injectUserEnabled; + private boolean injectAdminUserEnabled; + + public AdminDNs(final Settings settings) { + + this.injectUserEnabled = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false); + this.injectAdminUserEnabled = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED, false); + + final List adminDnsA = settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_ADMIN_DN, Collections.emptyList()); + + for (String dn:adminDnsA) { + try { + log.debug("{} is registered as an admin dn", dn); + adminDn.add(new LdapName(dn)); + } catch (final InvalidNameException e) { + // make sure to log correctly depending on user injection settings + if (injectUserEnabled && injectAdminUserEnabled) { + if (log.isDebugEnabled()) { + log.debug("Admin DN not an LDAP name, but admin user injection enabled. Will add {} to admin usernames", dn); + } + adminUsernames.add(dn); + } else { + log.error("Unable to parse admin dn {}",dn, e); + } + } + } + + log.debug("Loaded {} admin DN's {}",adminDn.size(), adminDn); + + final Settings impersonationDns = settings.getByPrefix(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_IMPERSONATION_DN+"."); + + for (String dnString:impersonationDns.keySet()) { + try { + allowedImpersonations.putAll(new LdapName(dnString), settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_IMPERSONATION_DN+"."+dnString)); + } catch (final InvalidNameException e) { + log.error("Unable to parse allowedImpersonations dn {}",dnString, e); + } + } + + log.debug("Loaded {} impersonation DN's {}",allowedImpersonations.size(), allowedImpersonations); + + final Settings impersonationUsersRest = settings.getByPrefix(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+"."); + + for (String user:impersonationUsersRest.keySet()) { + allowedRestImpersonations.putAll(user, settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+"."+user)); + } + + log.debug("Loaded {} impersonation users for REST {}",allowedRestImpersonations.size(), allowedRestImpersonations); + } + + public boolean isAdmin(User user) { + if (isAdminDN(user.getName())) { + return true; + } + + // ThreadContext injected user, may be admin user, only if both flags are enabled and user is injected + if (injectUserEnabled && injectAdminUserEnabled && user.isInjected() && adminUsernames.contains(user.getName())) { + return true; + } + return false; + } + + public boolean isAdminDN(String dn) { + + if(dn == null) return false; + + try { + return isAdminDN(new LdapName(dn)); + } catch (InvalidNameException e) { + return false; + } + } + + private boolean isAdminDN(LdapName dn) { + if(dn == null) return false; + + boolean isAdmin = adminDn.contains(dn); + + if (log.isTraceEnabled()) { + log.trace("Is principal {} an admin cert? {}", dn.toString(), isAdmin); + } + + return isAdmin; + } + + public boolean isTransportImpersonationAllowed(LdapName dn, String impersonated) { + if(dn == null) return false; + + if(isAdminDN(dn)) { + return true; + } + + return WildcardMatcher.matchAny(this.allowedImpersonations.get(dn), impersonated); + } + + public boolean isRestImpersonationAllowed(final String originalUser, final String impersonated) { + if(originalUser == null) { + return false; + } + return WildcardMatcher.matchAny(this.allowedRestImpersonations.get(originalUser), impersonated); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ClusterInfoHolder.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ClusterInfoHolder.java new file mode 100644 index 0000000000..5b0a145c24 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ClusterInfoHolder.java @@ -0,0 +1,120 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import java.util.Iterator; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.ClusterChangedEvent; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateListener; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.index.Index; + +public class ClusterInfoHolder implements ClusterStateListener { + + protected final Logger log = LogManager.getLogger(this.getClass()); + private volatile Boolean has5xNodes = null; + private volatile Boolean has5xIndices = null; + private volatile DiscoveryNodes nodes = null; + private volatile Boolean isLocalNodeElectedMaster = null;; + + @Override + public void clusterChanged(ClusterChangedEvent event) { + if(has5xNodes == null || event.nodesChanged()) { + has5xNodes = Boolean.valueOf(clusterHas5xNodes(event.state())); + if(log.isTraceEnabled()) { + log.trace("has5xNodes: {}", has5xNodes); + } + } + + final List indicesCreated = event.indicesCreated(); + final List indicesDeleted = event.indicesDeleted(); + if(has5xIndices == null || !indicesCreated.isEmpty() || !indicesDeleted.isEmpty()) { + has5xIndices = Boolean.valueOf(clusterHas5xIndices(event.state())); + if(log.isTraceEnabled()) { + log.trace("has5xIndices: {}", has5xIndices); + } + } + + if(nodes == null || event.nodesChanged()) { + nodes = event.state().nodes(); + if(log.isDebugEnabled()) { + log.debug("Cluster Info Holder now initialized for 'nodes'"); + } + } + + isLocalNodeElectedMaster = event.localNodeMaster()?Boolean.TRUE:Boolean.FALSE; + } + + public Boolean getHas5xNodes() { + return has5xNodes; + } + + public Boolean getHas5xIndices() { + return has5xIndices; + } + + public Boolean isLocalNodeElectedMaster() { + return isLocalNodeElectedMaster; + } + + public Boolean hasNode(DiscoveryNode node) { + if(nodes == null) { + if(log.isDebugEnabled()) { + log.debug("Cluster Info Holder not initialized yet for 'nodes'"); + } + return null; + } + + return nodes.nodeExists(node)?Boolean.TRUE:Boolean.FALSE; + } + + private static boolean clusterHas5xNodes(ClusterState state) { + return state.nodes().getMinNodeVersion().before(Version.V_6_0_0_alpha1); + } + + private static boolean clusterHas5xIndices(ClusterState state) { + final Iterator indices = state.metaData().indices().valuesIt(); + for(;indices.hasNext();) { + final IndexMetaData indexMetaData = indices.next(); + if(indexMetaData.getCreationVersion().before(Version.V_6_0_0_alpha1)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/CompatConfig.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/CompatConfig.java new file mode 100644 index 0000000000..80a38c78f8 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/CompatConfig.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; + + +public class CompatConfig implements ConfigurationChangeListener { + + private final Logger log = LogManager.getLogger(getClass()); + private final Settings staticSettings; + private Settings dynamicSecurityConfig; + + public CompatConfig(final Environment environment) { + super(); + this.staticSettings = environment.settings(); + } + + @Override + public void onChange(final Settings dynamicSecurityConfig) { + this.dynamicSecurityConfig = dynamicSecurityConfig; + log.debug("dynamicSecurityConfig updated?: {}", (dynamicSecurityConfig != null)); + } + + //true is default + public boolean restAuthEnabled() { + final boolean restInitiallyDisabled = staticSettings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY, false); + + if(restInitiallyDisabled) { + if(dynamicSecurityConfig == null) { + if(log.isTraceEnabled()) { + log.trace("dynamicSecurityConfig is null, initially static restDisabled"); + } + return false; + } else { + final boolean restDynamicallyDisabled = dynamicSecurityConfig.getAsBoolean("opendistro_security.dynamic.disable_rest_auth", false); + if(log.isTraceEnabled()) { + log.trace("opendistro_security.dynamic.disable_rest_auth {}", restDynamicallyDisabled); + } + return !restDynamicallyDisabled; + } + } else { + return true; + } + + } + + //true is default + public boolean transportInterClusterAuthEnabled() { + final boolean interClusterAuthInitiallyDisabled = staticSettings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY, false); + + if(interClusterAuthInitiallyDisabled) { + if(dynamicSecurityConfig == null) { + if(log.isTraceEnabled()) { + log.trace("dynamicSecurityConfig is null, initially static interClusterAuthDisabled"); + } + return false; + } else { + final boolean interClusterAuthDynamicallyDisabled = dynamicSecurityConfig.getAsBoolean("opendistro_security.dynamic.disable_intertransport_auth", false); + if(log.isTraceEnabled()) { + log.trace("opendistro_security.dynamic.disable_intertransport_auth {}", interClusterAuthDynamicallyDisabled); + } + return !interClusterAuthDynamicallyDisabled; + } + } else { + return true; + } + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigCallback.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigCallback.java new file mode 100644 index 0000000000..cd947c36a8 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigCallback.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import org.elasticsearch.action.get.MultiGetResponse.Failure; +import org.elasticsearch.common.settings.Settings; + +public interface ConfigCallback { + + void success(String id, Settings settings); + void noData(String id); + void singleFailure(Failure failure); + void failure(Throwable t); + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationChangeListener.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationChangeListener.java new file mode 100644 index 0000000000..7be7fb6dd6 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationChangeListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + + +import org.elasticsearch.common.settings.Settings; + +/** + * Callback function on change particular configuration + */ +public interface ConfigurationChangeListener { + + /** + * @param configuration not null updated configuration on that was subscribe current listener + */ + void onChange(Settings configuration); +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationLoader.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationLoader.java new file mode 100644 index 0000000000..e359121682 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationLoader.java @@ -0,0 +1,208 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.get.MultiGetItemResponse; +import org.elasticsearch.action.get.MultiGetRequest; +import org.elasticsearch.action.get.MultiGetResponse; +import org.elasticsearch.action.get.MultiGetResponse.Failure; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.OpenDistroSecurityDeprecationHandler; + +class ConfigurationLoader { + + protected final Logger log = LogManager.getLogger(this.getClass()); + private final Client client; + //private final ThreadContext threadContext; + private final String opendistrosecurityIndex; + + ConfigurationLoader(final Client client, ThreadPool threadPool, final Settings settings) { + super(); + this.client = client; + //this.threadContext = threadPool.getThreadContext(); + this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + log.debug("Index is: {}", opendistrosecurityIndex); + } + + Map load(final String[] events, long timeout, TimeUnit timeUnit) throws InterruptedException, TimeoutException { + final CountDownLatch latch = new CountDownLatch(events.length); + final Map rs = new HashMap(events.length); + + loadAsync(events, new ConfigCallback() { + + @Override + public void success(String id, Settings settings) { + if(latch.getCount() <= 0) { + log.error("Latch already counted down (for {} of {}) (index={})", id, Arrays.toString(events), opendistrosecurityIndex); + } + + rs.put(id, settings); + latch.countDown(); + if(log.isDebugEnabled()) { + log.debug("Received config for {} (of {}) with current latch value={}", id, Arrays.toString(events), latch.getCount()); + } + } + + @Override + public void singleFailure(Failure failure) { + log.error("Failure {} retrieving configuration for {} (index={})", failure==null?null:failure.getMessage(), Arrays.toString(events), opendistrosecurityIndex); + } + + @Override + public void noData(String id) { + log.warn("No data for {} while retrieving configuration for {} (index={})", id, Arrays.toString(events), opendistrosecurityIndex); + } + + @Override + public void failure(Throwable t) { + log.error("Exception {} while retrieving configuration for {} (index={})",t,t.toString(), Arrays.toString(events), opendistrosecurityIndex); + } + }); + + if(!latch.await(timeout, timeUnit)) { + //timeout + throw new TimeoutException("Timeout after "+timeout+""+timeUnit+" while retrieving configuration for "+Arrays.toString(events)+ "(index="+opendistrosecurityIndex+")"); + } + + return rs; + } + + void loadAsync(final String[] events, final ConfigCallback callback) { + if(events == null || events.length == 0) { + log.warn("No config events requested to load"); + return; + } + + final MultiGetRequest mget = new MultiGetRequest(); + + for (int i = 0; i < events.length; i++) { + final String event = events[i]; + mget.add(opendistrosecurityIndex, "security", event); + } + + mget.refresh(true); + mget.realtime(true); + + //try(StoredContext ctx = threadContext.stashContext()) { + // threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); + { + client.multiGet(mget, new ActionListener() { + @Override + public void onResponse(MultiGetResponse response) { + MultiGetItemResponse[] responses = response.getResponses(); + for (int i = 0; i < responses.length; i++) { + MultiGetItemResponse singleResponse = responses[i]; + if(singleResponse != null && !singleResponse.isFailed()) { + GetResponse singleGetResponse = singleResponse.getResponse(); + if(singleGetResponse.isExists() && !singleGetResponse.isSourceEmpty()) { + //success + final Settings _settings = toSettings(singleGetResponse.getSourceAsBytesRef(), singleGetResponse.getId()); + if(_settings != null) { + callback.success(singleGetResponse.getId(), _settings); + } else { + log.error("Cannot parse settings for "+singleGetResponse.getId()); + } + } else { + //does not exist or empty source + callback.noData(singleGetResponse.getId()); + } + } else { + //failure + callback.singleFailure(singleResponse==null?null:singleResponse.getFailure()); + } + } + } + + @Override + public void onFailure(Exception e) { + callback.failure(e); + } + }); + } + } + + private Settings toSettings(final BytesReference ref, final String id) { + if (ref == null || ref.length() == 0) { + log.error("Empty or null byte reference for {}", id); + return null; + } + + XContentParser parser = null; + + try { + parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, OpenDistroSecurityDeprecationHandler.INSTANCE, ref, XContentType.JSON); + parser.nextToken(); + parser.nextToken(); + + if(!id.equals((parser.currentName()))) { + log.error("Cannot parse config for type {} because {}!={}", id, id, parser.currentName()); + return null; + } + + parser.nextToken(); + + return Settings.builder().loadFromStream("dummy.json", new ByteArrayInputStream(parser.binaryValue()), true).build(); + } catch (final IOException e) { + throw ExceptionsHelper.convertToElastic(e); + } finally { + if(parser != null) { + try { + parser.close(); + } catch (IOException e) { + //ignore + } + } + } + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationRepository.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationRepository.java new file mode 100644 index 0000000000..cbee5fb77c --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationRepository.java @@ -0,0 +1,97 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + + +import java.util.Collection; +import java.util.Map; + +import org.elasticsearch.common.settings.Settings; + +/** + * Abstraction layer over Open Distro Security configuration repository + */ +public interface ConfigurationRepository { + + /** + * Load configuration from persistence layer + * + * @param configurationType not null configuration identifier + * @return configuration found by specified type in persistence layer or {@code null} if persistence layer + * doesn't have configuration by requested type, or persistence layer not ready yet + * @throws NullPointerException if specified configuration type is null or empty + */ + + Settings getConfiguration(String configurationType, boolean triggerComplianceWhenCached); + + /** + * Bulk load configuration from persistence layer + * + * @param configTypes not null collection with not null configuration identifiers by that need load configurations + * @return not null map where key it configuration type for found configuration and value it not null {@link Settings} + * that represent configuration for correspond type. If by requested type configuration absent in persistence layer, + * they will be absent in result map + * @throws NullPointerException if specified collection with type null or contain null or empty types + */ + //Map getConfiguration(Collection configTypes); + + /** + * Bulk reload configuration from persistence layer. If configuration was modify manually bypassing business logic define + * in {@link ConfigurationRepository}, this method should catch up it logic. This method can be very slow, because it skip + * all caching logic and should be use only as a last resort. + * + * @param configTypes not null collection with not null configuration identifiers by that need load configurations + * @return not null map where key it configuration type for found configuration and value it not null {@link Settings} + * that represent configuration for correspond type. If by requested type configuration absent in persistence layer, + * they will be absent in result map + * @throws NullPointerException if specified collection with type null or contain null or empty types + */ + Map reloadConfiguration(Collection configTypes); + + /** + * Save changed configuration in persistence layer. After save, changes will be available for + * read via {@link ConfigurationRepository#getConfiguration(String)} + * + * @param configurationType not null configuration identifier + * @param settings not null configuration that need persist + * @throws NullPointerException if specified configuration is null or configuration type is null or empty + */ + void persistConfiguration(String configurationType, Settings settings); + + /** + * Subscribe on configuration change + * + * @param configurationType not null and not empty configuration type of which changes need notify listener + * @param listener not null callback function that will be execute when specified type will modify + * @throws NullPointerException if specified configuration type is null or empty, or callback function is null + */ + void subscribeOnChange(String configurationType, ConfigurationChangeListener listener); +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/DlsFlsRequestValve.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/DlsFlsRequestValve.java new file mode 100644 index 0000000000..d479f3f796 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/DlsFlsRequestValve.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import java.util.Map; +import java.util.Set; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; + +public interface DlsFlsRequestValve { + + /** + * + * @param request + * @param listener + * @return false to stop + */ + boolean invoke(ActionRequest request, ActionListener listener, Map> allowedFlsFields, final Map> maskedFields, Map> queries); + + public static class NoopDlsFlsRequestValve implements DlsFlsRequestValve { + + @Override + public boolean invoke(ActionRequest request, ActionListener listener, Map> allowedFlsFields, final Map> maskedFields, Map> queries) { + return true; + } + + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/EmptyFilterLeafReader.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/EmptyFilterLeafReader.java new file mode 100644 index 0000000000..545b4caa00 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/EmptyFilterLeafReader.java @@ -0,0 +1,186 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.lucene.index.BinaryDocValues; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.FieldInfos; +import org.apache.lucene.index.FilterDirectoryReader; +import org.apache.lucene.index.FilterLeafReader; +import org.apache.lucene.index.LeafMetaData; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.NumericDocValues; +import org.apache.lucene.index.PointValues; +import org.apache.lucene.index.SortedDocValues; +import org.apache.lucene.index.SortedNumericDocValues; +import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.index.Terms; +import org.apache.lucene.util.Bits; +import org.elasticsearch.index.mapper.MapperService; + +import com.google.common.collect.Sets; + +class EmptyFilterLeafReader extends FilterLeafReader { + + private static final Set metaFields = Sets.union(Sets.newHashSet("_version"), + Sets.newHashSet(MapperService.getAllMetaFields())); + + private final FieldInfo[] fi; + + EmptyFilterLeafReader(final LeafReader delegate) { + super(delegate); + final FieldInfos infos = delegate.getFieldInfos(); + final List lfi = new ArrayList(metaFields.size()); + for(String metaField: metaFields) { + final FieldInfo _fi = infos.fieldInfo(metaField); + if(_fi != null) { + lfi.add(_fi); + } + } + fi = lfi.toArray(new FieldInfo[0]); + } + + private static class EmptySubReaderWrapper extends FilterDirectoryReader.SubReaderWrapper { + + @Override + public LeafReader wrap(final LeafReader reader) { + return new EmptyFilterLeafReader(reader); + } + + } + + static class EmptyDirectoryReader extends FilterDirectoryReader { + + public EmptyDirectoryReader(final DirectoryReader in) throws IOException { + super(in, new EmptySubReaderWrapper()); + } + + @Override + protected DirectoryReader doWrapDirectoryReader(final DirectoryReader in) throws IOException { + return new EmptyDirectoryReader(in); + } + + @Override + public CacheHelper getReaderCacheHelper() { + return in.getReaderCacheHelper(); + } + } + + private boolean isMeta(String field) { + return metaFields.contains(field); + } + + @Override + public FieldInfos getFieldInfos() { + return new FieldInfos(fi); + } + + @Override + public NumericDocValues getNumericDocValues(final String field) throws IOException { + return isMeta(field) ? in.getNumericDocValues(field) : null; + } + + @Override + public BinaryDocValues getBinaryDocValues(final String field) throws IOException { + return isMeta(field) ? in.getBinaryDocValues(field) : null; + } + + @Override + public SortedDocValues getSortedDocValues(final String field) throws IOException { + return isMeta(field) ? in.getSortedDocValues(field) : null; + } + + @Override + public SortedNumericDocValues getSortedNumericDocValues(final String field) throws IOException { + return isMeta(field) ? in.getSortedNumericDocValues(field) : null; + } + + @Override + public SortedSetDocValues getSortedSetDocValues(final String field) throws IOException { + return isMeta(field) ? in.getSortedSetDocValues(field) : null; + } + + @Override + public NumericDocValues getNormValues(final String field) throws IOException { + return isMeta(field) ? in.getNormValues(field) : null; + } + + @Override + public PointValues getPointValues(String field) throws IOException { + return isMeta(field) ? in.getPointValues(field) : null; + } + + @Override + public Terms terms(String field) throws IOException { + return isMeta(field) ? in.terms(field) : null; + } + + @Override + public LeafMetaData getMetaData() { + return in.getMetaData(); + } + + @Override + public Bits getLiveDocs() { + return new Bits.MatchNoBits(0); + } + + @Override + public int numDocs() { + return 0; + } + + @Override + public LeafReader getDelegate() { + return in; + } + + @Override + public int maxDoc() { + return in.maxDoc(); + } + + @Override + public CacheHelper getCoreCacheHelper() { + return in.getCoreCacheHelper(); + } + + @Override + public CacheHelper getReaderCacheHelper() { + return in.getReaderCacheHelper(); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/IndexBaseConfigurationRepository.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/IndexBaseConfigurationRepository.java new file mode 100644 index 0000000000..7b52b880f6 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/IndexBaseConfigurationRepository.java @@ -0,0 +1,433 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import java.io.File; +import java.nio.file.Path; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest; +import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.component.LifecycleListener; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext; +import org.elasticsearch.env.Environment; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.compliance.ComplianceConfig; +import com.amazon.opendistroforelasticsearch.security.ssl.util.ExceptionUtils; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.ConfigHelper; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +public class IndexBaseConfigurationRepository implements ConfigurationRepository { + private static final Logger LOGGER = LogManager.getLogger(IndexBaseConfigurationRepository.class); + private static final Pattern DLS_PATTERN = Pattern.compile(".+\\.indices\\..+\\._dls_=.+", Pattern.DOTALL); + private static final Pattern FLS_PATTERN = Pattern.compile(".+\\.indices\\..+\\._fls_\\.[0-9]+=.+", Pattern.DOTALL); + + private final String opendistrosecurityIndex; + private final Client client; + private final ConcurrentMap typeToConfig; + private final Multimap configTypeToChancheListener; + private final ConfigurationLoader cl; + private final LegacyConfigurationLoader legacycl; + private final Settings settings; + private final ClusterService clusterService; + private final AuditLog auditLog; + private final ComplianceConfig complianceConfig; + private ThreadPool threadPool; + + private IndexBaseConfigurationRepository(Settings settings, final Path configPath, ThreadPool threadPool, + Client client, ClusterService clusterService, AuditLog auditLog, ComplianceConfig complianceConfig) { + this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + this.settings = settings; + this.client = client; + this.threadPool = threadPool; + this.clusterService = clusterService; + this.auditLog = auditLog; + this.complianceConfig = complianceConfig; + this.typeToConfig = Maps.newConcurrentMap(); + this.configTypeToChancheListener = ArrayListMultimap.create(); + cl = new ConfigurationLoader(client, threadPool, settings); + legacycl = new LegacyConfigurationLoader(client, threadPool, settings); + + final AtomicBoolean installDefaultConfig = new AtomicBoolean(); + + clusterService.addLifecycleListener(new LifecycleListener() { + + @Override + public void afterStart() { + + final Thread bgThread = new Thread(new Runnable() { + + @Override + public void run() { + try { + + if(installDefaultConfig.get()) { + + try { + String lookupDir = System.getProperty("security.default_init.dir"); + final String cd = lookupDir != null? (lookupDir+"/") : new Environment(settings, configPath).pluginsFile().toAbsolutePath().toString()+"/opendistro_security/securityconfig/"; + File confFile = new File(cd+"config.yml"); + if(confFile.exists()) { + final ThreadContext threadContext = threadPool.getThreadContext(); + try(StoredContext ctx = threadContext.stashContext()) { + threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); + LOGGER.info("Will create {} index so we can apply default config", opendistrosecurityIndex); + + Map indexSettings = new HashMap<>(); + indexSettings.put("index.number_of_shards", 1); + indexSettings.put("index.auto_expand_replicas", "0-all"); + + boolean ok = client.admin().indices().create(new CreateIndexRequest(opendistrosecurityIndex) + .settings(indexSettings)) + .actionGet().isAcknowledged(); + if(ok) { + ConfigHelper.uploadFile(client, cd+"config.yml", opendistrosecurityIndex, "config"); + ConfigHelper.uploadFile(client, cd+"roles.yml", opendistrosecurityIndex, "roles"); + ConfigHelper.uploadFile(client, cd+"roles_mapping.yml", opendistrosecurityIndex, "rolesmapping"); + ConfigHelper.uploadFile(client, cd+"internal_users.yml", opendistrosecurityIndex, "internalusers"); + ConfigHelper.uploadFile(client, cd+"action_groups.yml", opendistrosecurityIndex, "actiongroups"); + LOGGER.info("Default config applied"); + } + } + } else { + LOGGER.error("{} does not exist", confFile.getAbsolutePath()); + } + } catch (Exception e) { + LOGGER.debug("Cannot apply default config (this is not an error!) due to {}", e.getMessage()); + } + } + + LOGGER.debug("Node started, try to initialize it. Wait for at least yellow cluster state...."); + ClusterHealthResponse response = null; + try { + response = client.admin().cluster().health(new ClusterHealthRequest(opendistrosecurityIndex).waitForYellowStatus()).actionGet(); + } catch (Exception e1) { + LOGGER.debug("Catched a {} but we just try again ...", e1.toString()); + } + + while(response == null || response.isTimedOut() || response.getStatus() == ClusterHealthStatus.RED) { + LOGGER.debug("index '{}' not healthy yet, we try again ... (Reason: {})", opendistrosecurityIndex, response==null?"no response":(response.isTimedOut()?"timeout":"other, maybe red cluster")); + try { + Thread.sleep(500); + } catch (InterruptedException e1) { + //ignore + Thread.currentThread().interrupt(); + } + try { + response = client.admin().cluster().health(new ClusterHealthRequest(opendistrosecurityIndex).waitForYellowStatus()).actionGet(); + } catch (Exception e1) { + LOGGER.debug("Catched again a {} but we just try again ...", e1.toString()); + } + continue; + } + + while(true) { + try { + LOGGER.debug("Try to load config ..."); + reloadConfiguration(Arrays.asList(new String[] { "config", "roles", "rolesmapping", "internalusers", "actiongroups"} )); + break; + } catch (Exception e) { + LOGGER.debug("Unable to load configuration due to {}", String.valueOf(ExceptionUtils.getRootCause(e))); + try { + Thread.sleep(3000); + } catch (InterruptedException e1) { + Thread.currentThread().interrupt(); + LOGGER.debug("Thread was interrupted so we cancel initialization"); + break; + } + } + } + + LOGGER.info("Node '{}' initialized", clusterService.localNode().getName()); + + } catch (Exception e) { + LOGGER.error("Unexpected exception while initializing node "+e, e); + } + } + }); + + LOGGER.info("Check if "+opendistrosecurityIndex+" index exists ..."); + + try { + + IndicesExistsRequest ier = new IndicesExistsRequest(opendistrosecurityIndex) + .masterNodeTimeout(TimeValue.timeValueMinutes(1)); + + final ThreadContext threadContext = threadPool.getThreadContext(); + + try(StoredContext ctx = threadContext.stashContext()) { + threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); + + client.admin().indices().exists(ier, new ActionListener() { + + @Override + public void onResponse(IndicesExistsResponse response) { + if(response != null && response.isExists()) { + bgThread.start(); + } else { + if(settings.get("tribe.name", null) == null && settings.getByPrefix("tribe").size() > 0) { + LOGGER.info("{} index does not exist yet, but we are a tribe node. So we will load the config anyhow until we got it ...", opendistrosecurityIndex); + bgThread.start(); + } else { + + if(settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false)){ + LOGGER.info("{} index does not exist yet, so we create a default config", opendistrosecurityIndex); + installDefaultConfig.set(true); + bgThread.start(); + } else { + LOGGER.info("{} index does not exist yet, so no need to load config on node startup. Use securityadmin to initialize cluster", opendistrosecurityIndex); + } + } + } + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Failure while checking {} index {}",e, opendistrosecurityIndex, e); + bgThread.start(); + } + }); + } + } catch (Throwable e2) { + LOGGER.error("Failure while executing IndicesExistsRequest {}",e2, e2); + bgThread.start(); + } + } + }); + } + + + public static ConfigurationRepository create(Settings settings, final Path configPath, final ThreadPool threadPool, Client client, ClusterService clusterService, AuditLog auditLog, ComplianceConfig complianceConfig) { + final IndexBaseConfigurationRepository repository = new IndexBaseConfigurationRepository(settings, configPath, threadPool, client, clusterService, auditLog, complianceConfig); + return repository; + } + + @Override + public Settings getConfiguration(String configurationType, boolean triggerComplianceWhenCached) { + + Settings result = typeToConfig.get(configurationType); + + if (result != null) { + + if(triggerComplianceWhenCached && complianceConfig.isEnabled()) { + Map fields = new HashMap(); + fields.put(configurationType, Strings.toString(result)); + auditLog.logDocumentRead(this.opendistrosecurityIndex, configurationType, null, fields, complianceConfig); + } + return result; + } + + Map loaded = loadConfigurations(Collections.singleton(configurationType)); + + result = loaded.get(configurationType); + + return putSettingsToCache(configurationType, result); + } + + private Settings putSettingsToCache(String configurationType, Settings result) { + if (result != null) { + typeToConfig.putIfAbsent(configurationType, result); + } + + return typeToConfig.get(configurationType); + } + + + /*@Override + public Map getConfiguration(Collection configTypes) { + List typesToLoad = Lists.newArrayList(); + Map result = Maps.newHashMap(); + + for (String type : configTypes) { + Settings conf = typeToConfig.get(type); + if (conf != null) { + result.put(type, conf); + } else { + typesToLoad.add(type); + } + } + + if (typesToLoad.isEmpty()) { + return result; + } + + Map loaded = loadConfigurations(typesToLoad); + + for (Map.Entry entry : loaded.entrySet()) { + Settings conf = putSettingsToCache(entry.getKey(), entry.getValue()); + + if (conf != null) { + result.put(entry.getKey(), conf); + } + } + + return result; + }*/ + + + @Override + public Map reloadConfiguration(Collection configTypes) { + Map loaded = loadConfigurations(configTypes); + typeToConfig.clear(); + typeToConfig.putAll(loaded); + notifyAboutChanges(loaded); + + return loaded; + } + + @Override + public void persistConfiguration(String configurationType, Settings settings) { + //TODO should be use from com.amazon.opendistroforelasticsearch.security.tools.OpenDistroSecurityAdmin + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public synchronized void subscribeOnChange(String configurationType, ConfigurationChangeListener listener) { + LOGGER.debug("Subscribe on configuration changes by type {} with listener {}", configurationType, listener); + configTypeToChancheListener.put(configurationType, listener); + } + + private synchronized void notifyAboutChanges(Map typeToConfig) { + for (Map.Entry entry : configTypeToChancheListener.entries()) { + String type = entry.getKey(); + ConfigurationChangeListener listener = entry.getValue(); + + Settings settings = typeToConfig.get(type); + + if (settings == null) { + continue; + } + + LOGGER.debug("Notify {} listener about change configuration with type {}", listener, type); + listener.onChange(settings); + } + } + + + private Map loadConfigurations(Collection configTypes) { + + final ThreadContext threadContext = threadPool.getThreadContext(); + final Map retVal = new HashMap(); + //final List exception = new ArrayList(1); + // final CountDownLatch latch = new CountDownLatch(1); + + try(StoredContext ctx = threadContext.stashContext()) { + threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); + + boolean securityIndexExists = clusterService.state().metaData().hasConcreteIndex(this.opendistrosecurityIndex); + + if(securityIndexExists) { + if(clusterService.state().metaData().index(this.opendistrosecurityIndex).mapping("config") != null) { + //legacy layout + LOGGER.debug("security index exists and was created before ES 6 (legacy layout)"); + retVal.putAll(validate(legacycl.loadLegacy(configTypes.toArray(new String[0]), 5, TimeUnit.SECONDS), configTypes.size())); + } else { + LOGGER.debug("security index exists and was created with ES 6 (new layout)"); + retVal.putAll(validate(cl.load(configTypes.toArray(new String[0]), 5, TimeUnit.SECONDS), configTypes.size())); + } + } else { + //wait (and use new layout) + LOGGER.debug("security index not exists (yet)"); + retVal.putAll(validate(cl.load(configTypes.toArray(new String[0]), 30, TimeUnit.SECONDS), configTypes.size())); + } + + } catch (Exception e) { + throw new ElasticsearchException(e); + } + return retVal; + } + + private Map validate(Map conf, int expectedSize) throws InvalidConfigException { + + if(conf == null || conf.size() != expectedSize) { + throw new InvalidConfigException("Retrieved only partial configuration"); + } + + final Settings roles = conf.get("roles"); + final String rolesDelimited; + + if (roles != null && (rolesDelimited = roles.toDelimitedString('#')) != null) { + + //.indices.._dls_= OK + //.indices.._fls_.= OK + + final String[] rolesString = rolesDelimited.split("#"); + + for (String role : rolesString) { + if (role.contains("_fls_.") && !FLS_PATTERN.matcher(role).matches()) { + LOGGER.error("Invalid FLS configuration detected, FLS/DLS will not work correctly: {}", role); + } + + if (role.contains("_dls_=") && !DLS_PATTERN.matcher(role).matches()) { + LOGGER.error("Invalid DLS configuration detected, FLS/DLS will not work correctly: {}", role); + } + } + } + + return conf; + } + + private static String formatDate(long date) { + return new SimpleDateFormat("yyyy-MM-dd").format(new Date(date)); + } + +} \ No newline at end of file diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/InvalidConfigException.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/InvalidConfigException.java new file mode 100644 index 0000000000..5b6d73e553 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/InvalidConfigException.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +public class InvalidConfigException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public InvalidConfigException() { + super(); + } + + public InvalidConfigException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public InvalidConfigException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidConfigException(String message) { + super(message); + } + + public InvalidConfigException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/LegacyConfigurationLoader.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/LegacyConfigurationLoader.java new file mode 100644 index 0000000000..18ae4c74b0 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/LegacyConfigurationLoader.java @@ -0,0 +1,208 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.get.MultiGetItemResponse; +import org.elasticsearch.action.get.MultiGetRequest; +import org.elasticsearch.action.get.MultiGetResponse; +import org.elasticsearch.action.get.MultiGetResponse.Failure; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.OpenDistroSecurityDeprecationHandler; + +class LegacyConfigurationLoader { + + protected final Logger log = LogManager.getLogger(this.getClass()); + private final Client client; + //private final ThreadContext threadContext; + private final String opendistrosecurityIndex; + + LegacyConfigurationLoader(final Client client, ThreadPool threadPool, final Settings settings) { + super(); + this.client = client; + //this.threadContext = threadPool.getThreadContext(); + this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + log.debug("Index is: {}", opendistrosecurityIndex); + } + + Map loadLegacy(final String[] events, long timeout, TimeUnit timeUnit) throws InterruptedException, TimeoutException { + final CountDownLatch latch = new CountDownLatch(events.length); + final Map rs = new HashMap(events.length); + + loadAsyncLegacy(events, new ConfigCallback() { + + @Override + public void success(String type, Settings settings) { + if(latch.getCount() <= 0) { + log.error("Latch already counted down (for {} of {}) (index={})", type, Arrays.toString(events), opendistrosecurityIndex); + } + + rs.put(type, settings); + latch.countDown(); + if(log.isDebugEnabled()) { + log.debug("Received config for {} (of {}) with current latch value={}", type, Arrays.toString(events), latch.getCount()); + } + } + + @Override + public void singleFailure(Failure failure) { + log.error("Failure {} retrieving configuration for {} (index={})", failure==null?null:failure.getMessage(), Arrays.toString(events), opendistrosecurityIndex); + } + + @Override + public void noData(String type) { + log.warn("No data for {} while retrieving configuration for {} (index={})", type, Arrays.toString(events), opendistrosecurityIndex); + } + + @Override + public void failure(Throwable t) { + log.error("Exception {} while retrieving configuration for {} (index={})",t,t.toString(), Arrays.toString(events), opendistrosecurityIndex); + } + }); + + if(!latch.await(timeout, timeUnit)) { + //timeout + throw new TimeoutException("Timeout after "+timeout+" "+timeUnit+" while retrieving configuration for "+Arrays.toString(events)+ "(index="+opendistrosecurityIndex+")"); + } + + return rs; + } + + void loadAsyncLegacy(final String[] events, final ConfigCallback callback) { + if(events == null || events.length == 0) { + log.warn("No config events requested to load"); + return; + } + + final MultiGetRequest mget = new MultiGetRequest(); + + for (int i = 0; i < events.length; i++) { + final String event = events[i]; + mget.add(opendistrosecurityIndex, event, "0"); + } + + mget.refresh(true); + mget.realtime(true); + + //try(StoredContext ctx = threadContext.stashContext()) { + // threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); + { + client.multiGet(mget, new ActionListener() { + @Override + public void onResponse(MultiGetResponse response) { + MultiGetItemResponse[] responses = response.getResponses(); + for (int i = 0; i < responses.length; i++) { + MultiGetItemResponse singleResponse = responses[i]; + if(singleResponse != null && !singleResponse.isFailed()) { + GetResponse singleGetResponse = singleResponse.getResponse(); + if(singleGetResponse.isExists() && !singleGetResponse.isSourceEmpty()) { + //success + final Settings _settings = toSettings(singleGetResponse.getSourceAsBytesRef(), singleGetResponse.getType()); + if(_settings != null) { + callback.success(singleGetResponse.getType(), _settings); + } else { + log.error("Cannot parse settings for "+singleGetResponse.getType()); + } + } else { + //does not exist or empty source + callback.noData(singleGetResponse.getType()); + } + } else { + //failure + callback.singleFailure(singleResponse==null?null:singleResponse.getFailure()); + } + } + } + + @Override + public void onFailure(Exception e) { + callback.failure(e); + } + }); + } + } + + private Settings toSettings(final BytesReference ref, final String type) { + if (ref == null || ref.length() == 0) { + log.error("Empty or null byte reference for {}", type); + return null; + } + + XContentParser parser = null; + + try { + parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, OpenDistroSecurityDeprecationHandler.INSTANCE, ref, XContentType.JSON); + parser.nextToken(); + parser.nextToken(); + + if(!type.equals((parser.currentName()))) { + log.error("Cannot parse config for type {} because {}!={}", type, type, parser.currentName()); + return null; + } + + parser.nextToken(); + + return Settings.builder().loadFromStream("dummy.json", new ByteArrayInputStream(parser.binaryValue()), true).build(); + } catch (final IOException e) { + throw ExceptionsHelper.convertToElastic(e); + } finally { + if(parser != null) { + try { + parser.close(); + } catch (IOException e) { + //ignore + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/OpenDistroSecurityIndexSearcherWrapper.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/OpenDistroSecurityIndexSearcherWrapper.java new file mode 100644 index 0000000000..c734952160 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/OpenDistroSecurityIndexSearcherWrapper.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.search.IndexSearcher; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.engine.EngineException; +import org.elasticsearch.index.shard.IndexSearcherWrapper; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.HeaderHelper; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class OpenDistroSecurityIndexSearcherWrapper extends IndexSearcherWrapper { + + protected final Logger log = LogManager.getLogger(this.getClass()); + protected final ThreadContext threadContext; + protected final Index index; + protected final String opendistrosecurityIndex; + private final AdminDNs adminDns; + + //constructor is called per index, so avoid costly operations here + public OpenDistroSecurityIndexSearcherWrapper(final IndexService indexService, final Settings settings, final AdminDNs adminDNs) { + index = indexService.index(); + threadContext = indexService.getThreadPool().getThreadContext(); + this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + this.adminDns = adminDNs; + } + + @Override + public final DirectoryReader wrap(final DirectoryReader reader) throws IOException { + + if (isSecurityIndexRequest() && !isAdminAuthenticatedOrInternalRequest()) { + return new EmptyFilterLeafReader.EmptyDirectoryReader(reader); + } + + + return dlsFlsWrap(reader, isAdminAuthenticatedOrInternalRequest()); + } + + @Override + public final IndexSearcher wrap(final IndexSearcher searcher) throws EngineException { + return dlsFlsWrap(searcher, isAdminAuthenticatedOrInternalRequest()); + } + + protected IndexSearcher dlsFlsWrap(final IndexSearcher searcher, boolean isAdmin) throws EngineException { + return searcher; + } + + protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdmin) throws IOException { + return reader; + } + + protected final boolean isAdminAuthenticatedOrInternalRequest() { + + final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + + if (user != null && adminDns.isAdmin(user)) { + return true; + } + + if ("true".equals(HeaderHelper.getSafeFromHeader(threadContext, ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER))) { + return true; + } + + return false; + } + + protected final boolean isSecurityIndexRequest() { + return index.getName().equals(opendistrosecurityIndex); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityFilter.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityFilter.java new file mode 100644 index 0000000000..aa1fdf584c --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityFilter.java @@ -0,0 +1,342 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.filter; + +import java.util.UUID; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.DocWriteRequest.OpType; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.admin.indices.close.CloseIndexRequest; +import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; +import org.elasticsearch.action.bulk.BulkItemRequest; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkShardRequest; +import org.elasticsearch.action.delete.DeleteRequest; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.MultiGetRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.search.MultiSearchRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.support.ActionFilter; +import org.elasticsearch.action.support.ActionFilterChain; +import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext; +import org.elasticsearch.index.reindex.DeleteByQueryRequest; +import org.elasticsearch.index.reindex.UpdateByQueryRequest; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIAction; +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog.Origin; +import com.amazon.opendistroforelasticsearch.security.compliance.ComplianceConfig; +import com.amazon.opendistroforelasticsearch.security.configuration.AdminDNs; +import com.amazon.opendistroforelasticsearch.security.configuration.CompatConfig; +import com.amazon.opendistroforelasticsearch.security.configuration.DlsFlsRequestValve; +import com.amazon.opendistroforelasticsearch.security.privileges.PrivilegesEvaluator; +import com.amazon.opendistroforelasticsearch.security.privileges.PrivilegesEvaluatorResponse; +import com.amazon.opendistroforelasticsearch.security.support.Base64Helper; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.HeaderHelper; +import com.amazon.opendistroforelasticsearch.security.support.SourceFieldsContext; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class OpenDistroSecurityFilter implements ActionFilter { + + protected final Logger log = LogManager.getLogger(this.getClass()); + protected final Logger actionTrace = LogManager.getLogger("opendistro_security_action_trace"); + private final PrivilegesEvaluator evalp; + private final AdminDNs adminDns; + private DlsFlsRequestValve dlsFlsValve; + private final AuditLog auditLog; + private final ThreadContext threadContext; + private final ClusterService cs; + private final ComplianceConfig complianceConfig; + private final CompatConfig compatConfig; + + public OpenDistroSecurityFilter(final PrivilegesEvaluator evalp, final AdminDNs adminDns, + DlsFlsRequestValve dlsFlsValve, AuditLog auditLog, ThreadPool threadPool, ClusterService cs, + ComplianceConfig complianceConfig, final CompatConfig compatConfig) { + this.evalp = evalp; + this.adminDns = adminDns; + this.dlsFlsValve = dlsFlsValve; + this.auditLog = auditLog; + this.threadContext = threadPool.getThreadContext(); + this.cs = cs; + this.complianceConfig = complianceConfig; + this.compatConfig = compatConfig; + } + + @Override + public int order() { + return Integer.MIN_VALUE; + } + + @Override + public void apply(Task task, final String action, Request request, + ActionListener listener, ActionFilterChain chain) { + try (StoredContext ctx = threadContext.newStoredContext(true)){ + org.apache.logging.log4j.ThreadContext.clearAll(); + apply0(task, action, request, listener, chain); + } + } + + + private void apply0(Task task, final String action, Request request, + ActionListener listener, ActionFilterChain chain) { + + try { + + if(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN) == null) { + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, Origin.LOCAL.toString()); + } + + if(complianceConfig != null && complianceConfig.isEnabled()) { + attachSourceFieldContext(request); + } + + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final boolean userIsAdmin = isUserAdmin(user, adminDns); + final boolean interClusterRequest = HeaderHelper.isInterClusterRequest(threadContext); + final boolean trustedClusterRequest = HeaderHelper.isTrustedClusterRequest(threadContext); + final boolean confRequest = "true".equals(HeaderHelper.getSafeFromHeader(threadContext, ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER)); + final boolean passThroughRequest = action.startsWith("indices:admin/seq_no") + || action.equals(WhoAmIAction.NAME); + + final boolean internalRequest = + (interClusterRequest || HeaderHelper.isDirectRequest(threadContext)) + && action.startsWith("internal:") + && !action.startsWith("internal:transport/proxy"); + + if (user != null) { + org.apache.logging.log4j.ThreadContext.put("user", user.getName()); + } + + if(actionTrace.isTraceEnabled()) { + + String count = ""; + if(request instanceof BulkRequest) { + count = ""+((BulkRequest) request).requests().size(); + } + + if(request instanceof MultiGetRequest) { + count = ""+((MultiGetRequest) request).getItems().size(); + } + + if(request instanceof MultiSearchRequest) { + count = ""+((MultiSearchRequest) request).requests().size(); + } + + actionTrace.trace("Node "+cs.localNode().getName()+" -> "+action+" ("+count+"): userIsAdmin="+userIsAdmin+"/conRequest="+confRequest+"/internalRequest="+internalRequest + +"origin="+threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)+"/directRequest="+HeaderHelper.isDirectRequest(threadContext)+"/remoteAddress="+request.remoteAddress()); + + + threadContext.putHeader("_opendistro_security_trace"+System.currentTimeMillis()+"#"+UUID.randomUUID().toString(), Thread.currentThread().getName()+" FILTER -> "+"Node "+cs.localNode().getName()+" -> "+action+" userIsAdmin="+userIsAdmin+"/conRequest="+confRequest+"/internalRequest="+internalRequest + +"origin="+threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)+"/directRequest="+HeaderHelper.isDirectRequest(threadContext)+"/remoteAddress="+request.remoteAddress()+" "+threadContext.getHeaders().entrySet().stream().filter(p->!p.getKey().startsWith("_opendistro_security_trace")).collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue()))); + + + } + + + if(userIsAdmin + || confRequest + || internalRequest + || passThroughRequest){ + + if(userIsAdmin && !confRequest && !internalRequest && !passThroughRequest) { + auditLog.logGrantedPrivileges(action, request, task); + } + + chain.proceed(task, action, request, listener); + return; + } + + + if(complianceConfig != null && complianceConfig.isEnabled()) { + + boolean isImmutable = false; + + if(request instanceof BulkShardRequest) { + for(BulkItemRequest bsr: ((BulkShardRequest) request).items()) { + isImmutable = checkImmutableIndices(bsr.request(), listener); + if(isImmutable) { + break; + } + } + } else { + isImmutable = checkImmutableIndices(request, listener); + } + + if(isImmutable) { + return; + } + + } + + if(Origin.LOCAL.toString().equals(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)) + && (interClusterRequest || HeaderHelper.isDirectRequest(threadContext)) + ) { + + chain.proceed(task, action, request, listener); + return; + } + + if(user == null) { + + if(action.startsWith("cluster:monitor/state")) { + chain.proceed(task, action, request, listener); + return; + } + + if((interClusterRequest || trustedClusterRequest || request.remoteAddress() == null) && !compatConfig.transportInterClusterAuthEnabled()) { + chain.proceed(task, action, request, listener); + return; + } + + log.error("No user found for "+ action+" from "+request.remoteAddress()+" "+threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)+" via "+threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_CHANNEL_TYPE)+" "+threadContext.getHeaders()); + listener.onFailure(new ElasticsearchSecurityException("No user found for "+action, RestStatus.INTERNAL_SERVER_ERROR)); + return; + } + + final PrivilegesEvaluator eval = evalp; + + if (!eval.isInitialized()) { + log.error("Open Distro Security not initialized for {}", action); + listener.onFailure(new ElasticsearchSecurityException("Open Distro Security not initialized for " + + action, RestStatus.SERVICE_UNAVAILABLE)); + return; + } + + if (log.isTraceEnabled()) { + log.trace("Evaluate permissions for user: {}", user.getName()); + } + + final PrivilegesEvaluatorResponse pres = eval.evaluate(user, action, request, task); + + if (log.isDebugEnabled()) { + log.debug(pres); + } + + if (pres.isAllowed()) { + auditLog.logGrantedPrivileges(action, request, task); + if(!dlsFlsValve.invoke(request, listener, pres.getAllowedFlsFields(), pres.getMaskedFields(), pres.getQueries())) { + return; + } + chain.proceed(task, action, request, listener); + return; + } else { + auditLog.logMissingPrivileges(action, request, task); + log.debug("no permissions for {}", pres.getMissingPrivileges()); + listener.onFailure(new ElasticsearchSecurityException("no permissions for " + pres.getMissingPrivileges()+" and "+user, RestStatus.FORBIDDEN)); + return; + } + } catch (Throwable e) { + log.error("Unexpected exception "+e, e); + listener.onFailure(new ElasticsearchSecurityException("Unexpected exception " + action, RestStatus.INTERNAL_SERVER_ERROR)); + return; + } + } + + private static boolean isUserAdmin(User user, final AdminDNs adminDns) { + if (user != null && adminDns.isAdmin(user)) { + return true; + } + + return false; + } + + private void attachSourceFieldContext(ActionRequest request) { + + if(request instanceof SearchRequest && SourceFieldsContext.isNeeded((SearchRequest) request)) { + if(threadContext.getHeader("_opendistro_security_source_field_context") == null) { + final String serializedSourceFieldContext = Base64Helper.serializeObject(new SourceFieldsContext((SearchRequest) request)); + threadContext.putHeader("_opendistro_security_source_field_context", serializedSourceFieldContext); + } + } else if (request instanceof GetRequest && SourceFieldsContext.isNeeded((GetRequest) request)) { + if(threadContext.getHeader("_opendistro_security_source_field_context") == null) { + final String serializedSourceFieldContext = Base64Helper.serializeObject(new SourceFieldsContext((GetRequest) request)); + threadContext.putHeader("_opendistro_security_source_field_context", serializedSourceFieldContext); + } + } + } + + @SuppressWarnings("rawtypes") + private boolean checkImmutableIndices(Object request, ActionListener listener) { + + if( request instanceof DeleteRequest + || request instanceof UpdateRequest + || request instanceof UpdateByQueryRequest + || request instanceof DeleteByQueryRequest + || request instanceof DeleteIndexRequest + || request instanceof RestoreSnapshotRequest + || request instanceof CloseIndexRequest + || request instanceof IndicesAliasesRequest //TODO only remove index + ) { + + if(complianceConfig != null && complianceConfig.isIndexImmutable(request)) { + //auditLog.log + + //check index for type = remove index + //IndicesAliasesRequest iar = (IndicesAliasesRequest) request; + //for(AliasActions aa: iar.getAliasActions()) { + // if(aa.actionType() == Type.REMOVE_INDEX) { + + // } + //} + + + + listener.onFailure(new ElasticsearchSecurityException("Index is immutable", RestStatus.FORBIDDEN)); + return true; + } + } + + if(request instanceof IndexRequest) { + if(complianceConfig != null && complianceConfig.isIndexImmutable(request)) { + ((IndexRequest) request).opType(OpType.CREATE); + } + } + + return false; + } + +} \ No newline at end of file diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityRestFilter.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityRestFilter.java new file mode 100644 index 0000000000..78c92616b9 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityRestFilter.java @@ -0,0 +1,158 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.filter; + +import java.nio.file.Path; + +import javax.net.ssl.SSLPeerUnverifiedException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestRequest.Method; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog.Origin; +import com.amazon.opendistroforelasticsearch.security.auth.BackendRegistry; +import com.amazon.opendistroforelasticsearch.security.configuration.CompatConfig; +import com.amazon.opendistroforelasticsearch.security.ssl.transport.PrincipalExtractor; +import com.amazon.opendistroforelasticsearch.security.ssl.util.ExceptionUtils; +import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLRequestHelper; +import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLRequestHelper.SSLInfo; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.HTTPHelper; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class OpenDistroSecurityRestFilter { + + protected final Logger log = LogManager.getLogger(this.getClass()); + private final BackendRegistry registry; + private final AuditLog auditLog; + private final ThreadContext threadContext; + private final PrincipalExtractor principalExtractor; + private final Settings settings; + private final Path configPath; + private final CompatConfig compatConfig; + + public OpenDistroSecurityRestFilter(final BackendRegistry registry, final AuditLog auditLog, + final ThreadPool threadPool, final PrincipalExtractor principalExtractor, + final Settings settings, final Path configPath, final CompatConfig compatConfig) { + super(); + this.registry = registry; + this.auditLog = auditLog; + this.threadContext = threadPool.getThreadContext(); + this.principalExtractor = principalExtractor; + this.settings = settings; + this.configPath = configPath; + this.compatConfig = compatConfig; + } + + public RestHandler wrap(RestHandler original) { + return new RestHandler() { + + @Override + public void handleRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception { + org.apache.logging.log4j.ThreadContext.clearAll(); + if(!checkAndAuthenticateRequest(request, channel, client)) { + original.handleRequest(request, channel, client); + } + } + }; + } + + private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception { + + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, Origin.REST.toString()); + + if(HTTPHelper.containsBadHeader(request)) { + final ElasticsearchException exception = ExceptionUtils.createBadHeaderException(); + log.error(exception); + auditLog.logBadHeaders(request); + channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, exception)); + return true; + } + + if(SSLRequestHelper.containsBadHeader(threadContext, ConfigConstants.OPENDISTRO_SECURITY_CONFIG_PREFIX)) { + final ElasticsearchException exception = ExceptionUtils.createBadHeaderException(); + log.error(exception); + auditLog.logBadHeaders(request); + channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, exception)); + return true; + } + + final SSLInfo sslInfo; + try { + if((sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor)) != null) { + if(sslInfo.getPrincipal() != null) { + threadContext.putTransient("_opendistro_security_ssl_principal", sslInfo.getPrincipal()); + } + + if(sslInfo.getX509Certs() != null) { + threadContext.putTransient("_opendistro_security_ssl_peer_certificates", sslInfo.getX509Certs()); + } + threadContext.putTransient("_opendistro_security_ssl_protocol", sslInfo.getProtocol()); + threadContext.putTransient("_opendistro_security_ssl_cipher", sslInfo.getCipher()); + } + } catch (SSLPeerUnverifiedException e) { + log.error("No ssl info", e); + auditLog.logSSLException(request, e); + channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, e)); + return true; + } + + if(!compatConfig.restAuthEnabled()) { + return false; + } + + if(request.method() != Method.OPTIONS + && !"/_opendistro/_security/health".equals(request.path())) { + if (!registry.authenticate(request, channel, threadContext)) { + // another roundtrip + org.apache.logging.log4j.ThreadContext.remove("user"); + return true; + } else { + // make it possible to filter logs by username + org.apache.logging.log4j.ThreadContext.put("user", ((User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER)).getName()); + } + } + + return false; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPBasicAuthenticator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPBasicAuthenticator.java new file mode 100644 index 0000000000..9b031132a3 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPBasicAuthenticator.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http; + +import java.nio.file.Path; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestStatus; + +import com.amazon.opendistroforelasticsearch.security.auth.HTTPAuthenticator; +import com.amazon.opendistroforelasticsearch.security.support.HTTPHelper; +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; + +//TODO FUTURE allow only if protocol==https +public class HTTPBasicAuthenticator implements HTTPAuthenticator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + public HTTPBasicAuthenticator(final Settings settings, final Path configPath) { + + } + + @Override + public AuthCredentials extractCredentials(final RestRequest request, ThreadContext threadContext) { + + final boolean forceLogin = request.paramAsBoolean("force_login", false); + + if(forceLogin) { + return null; + } + + final String authorizationHeader = request.header("Authorization"); + + return HTTPHelper.extractCredentials(authorizationHeader, log); + } + + @Override + public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { + final BytesRestResponse wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, "Unauthorized"); + wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Basic realm=\"Open Distro Security\""); + channel.sendResponse(wwwAuthenticateResponse); + return true; + } + + @Override + public String getType() { + return "basic"; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPClientCertAuthenticator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPClientCertAuthenticator.java new file mode 100644 index 0000000000..1b2ab117b9 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPClientCertAuthenticator.java @@ -0,0 +1,127 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestRequest; + +import com.amazon.opendistroforelasticsearch.security.auth.HTTPAuthenticator; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; + +public class HTTPClientCertAuthenticator implements HTTPAuthenticator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + protected final Settings settings; + + public HTTPClientCertAuthenticator(final Settings settings, final Path configPath) { + this.settings = settings; + } + + @Override + public AuthCredentials extractCredentials(final RestRequest request, final ThreadContext threadContext) { + + final String principal = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); + + if (!Strings.isNullOrEmpty(principal)) { + + final String usernameAttribute = settings.get("username_attribute"); + final String rolesAttribute = settings.get("roles_attribute"); + + try { + final LdapName rfc2253dn = new LdapName(principal); + String username = principal.trim(); + String[] backendRoles = null; + + if(usernameAttribute != null && usernameAttribute.length() > 0) { + final List usernames = getDnAttribute(rfc2253dn, usernameAttribute); + if(usernames.isEmpty() == false) { + username = usernames.get(0); + } + } + + if(rolesAttribute != null && rolesAttribute.length() > 0) { + final List roles = getDnAttribute(rfc2253dn, rolesAttribute); + if(roles.isEmpty() == false) { + backendRoles = roles.toArray(new String[0]); + } + } + + return new AuthCredentials(username, backendRoles).markComplete(); + } catch (InvalidNameException e) { + log.error("Client cert had no properly formed DN (was: {})", principal); + return null; + } + + } else { + log.trace("No CLIENT CERT, send 401"); + return null; + } + } + + @Override + public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { + return false; + } + + @Override + public String getType() { + return "clientcert"; + } + + private List getDnAttribute(LdapName rfc2253dn, String attribute) { + final List attrValues = new ArrayList<>(rfc2253dn.size()); + final List reverseRdn = new ArrayList<>(rfc2253dn.getRdns()); + Collections.reverse(reverseRdn); + + for (Rdn rdn : reverseRdn) { + if (rdn.getType().equalsIgnoreCase(attribute)) { + attrValues.add(rdn.getValue().toString()); + } + } + + return Collections.unmodifiableList(attrValues); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPProxyAuthenticator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPProxyAuthenticator.java new file mode 100644 index 0000000000..00bf8b24c8 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPProxyAuthenticator.java @@ -0,0 +1,100 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http; + +import java.nio.file.Path; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestRequest; + +import com.amazon.opendistroforelasticsearch.security.auth.HTTPAuthenticator; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; + +public class HTTPProxyAuthenticator implements HTTPAuthenticator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + private volatile Settings settings; + + public HTTPProxyAuthenticator(Settings settings, final Path configPath) { + super(); + this.settings = settings; + } + + @Override + public AuthCredentials extractCredentials(final RestRequest request, ThreadContext context) { + + if(context.getTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE) != Boolean.TRUE) { + throw new ElasticsearchSecurityException("xff not done"); + } + + final String userHeader = settings.get("user_header"); + final String rolesHeader = settings.get("roles_header"); + final String rolesSeparator = settings.get("roles_separator", ","); + + if(log.isDebugEnabled()) { + log.debug("headers {}", request.getHeaders()); + log.debug("userHeader {}, value {}", userHeader, userHeader == null?null:request.header(userHeader)); + log.debug("rolesHeader {}, value {}", rolesHeader, rolesHeader == null?null:request.header(rolesHeader)); + } + + if (!Strings.isNullOrEmpty(userHeader) && !Strings.isNullOrEmpty((String) request.header(userHeader))) { + + String[] backendRoles = null; + + if (!Strings.isNullOrEmpty(rolesHeader) && !Strings.isNullOrEmpty((String) request.header(rolesHeader))) { + backendRoles = ((String) request.header(rolesHeader)).split(rolesSeparator); + } + return new AuthCredentials((String) request.header(userHeader), backendRoles).markComplete(); + } else { + if(log.isTraceEnabled()) { + log.trace("No '{}' header, send 401", userHeader); + } + return null; + } + } + + @Override + public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) { + return false; + } + + @Override + public String getType() { + return "proxy"; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityHttpServerTransport.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityHttpServerTransport.java new file mode 100644 index 0000000000..3089c2b10b --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityHttpServerTransport.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http; + +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.ssl.OpenDistroSecurityKeyStore; +import com.amazon.opendistroforelasticsearch.security.ssl.SslExceptionHandler; +import com.amazon.opendistroforelasticsearch.security.ssl.http.netty.OpenDistroSecuritySSLNettyHttpServerTransport; +import com.amazon.opendistroforelasticsearch.security.ssl.http.netty.ValidatingDispatcher; + +public class OpenDistroSecurityHttpServerTransport extends OpenDistroSecuritySSLNettyHttpServerTransport { + + public OpenDistroSecurityHttpServerTransport(final Settings settings, final NetworkService networkService, + final BigArrays bigArrays, final ThreadPool threadPool, final OpenDistroSecurityKeyStore odsks, + final SslExceptionHandler sslExceptionHandler, final NamedXContentRegistry namedXContentRegistry, final ValidatingDispatcher dispatcher) { + super(settings, networkService, bigArrays, threadPool, odsks, namedXContentRegistry, dispatcher, sslExceptionHandler); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityNonSslHttpServerTransport.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityNonSslHttpServerTransport.java new file mode 100644 index 0000000000..d4e1ae2879 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityNonSslHttpServerTransport.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; + +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.http.netty4.Netty4HttpServerTransport; +import org.elasticsearch.threadpool.ThreadPool; + +public class OpenDistroSecurityNonSslHttpServerTransport extends Netty4HttpServerTransport { + + private final ThreadContext threadContext; + + public OpenDistroSecurityNonSslHttpServerTransport(final Settings settings, final NetworkService networkService, final BigArrays bigArrays, + final ThreadPool threadPool, final NamedXContentRegistry namedXContentRegistry, final Dispatcher dispatcher) { + super(settings, networkService, bigArrays, threadPool, namedXContentRegistry, dispatcher); + this.threadContext = threadPool.getThreadContext(); + } + + @Override + public ChannelHandler configureServerChannelHandler() { + return new NonSslHttpChannelHandler(this); + } + + protected class NonSslHttpChannelHandler extends Netty4HttpServerTransport.HttpChannelHandler { + + protected NonSslHttpChannelHandler(Netty4HttpServerTransport transport) { + super(transport, OpenDistroSecurityNonSslHttpServerTransport.this.detailedErrorsEnabled, OpenDistroSecurityNonSslHttpServerTransport.this.threadContext); + } + + @Override + protected void initChannel(Channel ch) throws Exception { + super.initChannel(ch); + } + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/RemoteIpDetector.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/RemoteIpDetector.java new file mode 100644 index 0000000000..bdc6c956e7 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/RemoteIpDetector.java @@ -0,0 +1,343 @@ +/* + * 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. + */ + +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http; + +import java.net.InetSocketAddress; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Pattern; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.http.netty4.Netty4HttpRequest; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; + +class RemoteIpDetector { + + /** + * {@link Pattern} for a comma delimited string that support whitespace characters + */ + private static final Pattern commaSeparatedValuesPattern = Pattern.compile("\\s*,\\s*"); + + /** + * Logger + */ + protected final Logger log = LogManager.getLogger(this.getClass()); + + /** + * Convert a given comma delimited String into an array of String + * + * @return array of String (non null) + */ + protected static String[] commaDelimitedListToStringArray(String commaDelimitedStrings) { + return (commaDelimitedStrings == null || commaDelimitedStrings.length() == 0) ? new String[0] : commaSeparatedValuesPattern + .split(commaDelimitedStrings); + } + + /** + * Convert an array of strings in a comma delimited string + */ + protected static String listToCommaDelimitedString(List stringList) { + if (stringList == null) { + return ""; + } + StringBuilder result = new StringBuilder(); + for (Iterator it = stringList.iterator(); it.hasNext();) { + Object element = it.next(); + if (element != null) { + result.append(element); + if (it.hasNext()) { + result.append(", "); + } + } + } + return result.toString(); + } + + /** + * @see #setInternalProxies(String) + */ + private Pattern internalProxies = Pattern.compile( + "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" + + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" + + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}"); + + /** + * @see #setProxiesHeader(String) + */ + private String proxiesHeader = "X-Forwarded-By"; + + /** + * @see #setRemoteIpHeader(String) + */ + private String remoteIpHeader = "X-Forwarded-For"; + + /** + * @see RemoteIpValve#setTrustedProxies(String) + */ + private Pattern trustedProxies = null; + + /** + * @see #setInternalProxies(String) + * @return Regular expression that defines the internal proxies + */ + public String getInternalProxies() { + if (internalProxies == null) { + return null; + } + return internalProxies.toString(); + } + + /** + * @see #setProxiesHeader(String) + * @return the proxies header name (e.g. "X-Forwarded-By") + */ + public String getProxiesHeader() { + return proxiesHeader; + } + + /** + * @see #setRemoteIpHeader(String) + * @return the remote IP header name (e.g. "X-Forwarded-For") + */ + public String getRemoteIpHeader() { + return remoteIpHeader; + } + + /** + * @see #setTrustedProxies(String) + * @return Regular expression that defines the trusted proxies + */ + public String getTrustedProxies() { + if (trustedProxies == null) { + return null; + } + return trustedProxies.toString(); + } + + String detect(final Netty4HttpRequest request, ThreadContext threadContext){ + final String originalRemoteAddr = ((InetSocketAddress)request.getRemoteAddress()).getAddress().getHostAddress(); + @SuppressWarnings("unused") + final String originalProxiesHeader = request.header(proxiesHeader); + //final String originalRemoteIpHeader = request.getHeader(remoteIpHeader); + + if(log.isTraceEnabled()) { + log.trace("originalRemoteAddr {}", originalRemoteAddr); + } + + //X-Forwarded-For: client1, proxy1, proxy2 + // ^^^^^^ originalRemoteAddr + + //originalRemoteAddr need to be in the list of internalProxies + if (internalProxies !=null && + internalProxies.matcher(originalRemoteAddr).matches()) { + String remoteIp = null; + // In java 6, proxiesHeaderValue should be declared as a java.util.Deque + final LinkedList proxiesHeaderValue = new LinkedList<>(); + final StringBuilder concatRemoteIpHeaderValue = new StringBuilder(); + + //client1, proxy1, proxy2 + final List remoteIpHeaders = request.request().headers().getAll(remoteIpHeader); //X-Forwarded-For + + if(remoteIpHeaders == null || remoteIpHeaders.isEmpty()) { + return originalRemoteAddr; + } + + for (String rh:remoteIpHeaders) { + if (concatRemoteIpHeaderValue.length() > 0) { + concatRemoteIpHeaderValue.append(", "); + } + + concatRemoteIpHeaderValue.append(rh); + } + + if(log.isTraceEnabled()) { + log.trace("concatRemoteIpHeaderValue {}", concatRemoteIpHeaderValue.toString()); + } + + final String[] remoteIpHeaderValue = commaDelimitedListToStringArray(concatRemoteIpHeaderValue.toString()); + int idx; + // loop on remoteIpHeaderValue to find the first trusted remote ip and to build the proxies chain + for (idx = remoteIpHeaderValue.length - 1; idx >= 0; idx--) { + String currentRemoteIp = remoteIpHeaderValue[idx]; + remoteIp = currentRemoteIp; + if (internalProxies.matcher(currentRemoteIp).matches()) { + // do nothing, internalProxies IPs are not appended to the + } else if (trustedProxies != null && + trustedProxies.matcher(currentRemoteIp).matches()) { + proxiesHeaderValue.addFirst(currentRemoteIp); + } else { + idx--; // decrement idx because break statement doesn't do it + break; + } + } + + // continue to loop on remoteIpHeaderValue to build the new value of the remoteIpHeader + final LinkedList newRemoteIpHeaderValue = new LinkedList<>(); + for (; idx >= 0; idx--) { + String currentRemoteIp = remoteIpHeaderValue[idx]; + newRemoteIpHeaderValue.addFirst(currentRemoteIp); + } + + if (remoteIp != null) { + + if (proxiesHeaderValue.size() == 0) { + request.request().headers().remove(proxiesHeader); + } else { + String commaDelimitedListOfProxies = listToCommaDelimitedString(proxiesHeaderValue); + request.request().headers().set(proxiesHeader,commaDelimitedListOfProxies); + } + if (newRemoteIpHeaderValue.size() == 0) { + request.request().headers().remove(remoteIpHeader); + } else { + String commaDelimitedRemoteIpHeaderValue = listToCommaDelimitedString(newRemoteIpHeaderValue); + request.request().headers().set(remoteIpHeader,commaDelimitedRemoteIpHeaderValue); + } + + if (log.isTraceEnabled()) { + final String originalRemoteHost = ((InetSocketAddress)request.getRemoteAddress()).getAddress().getHostName(); + log.trace("Incoming request " + request.request().uri() + " with originalRemoteAddr '" + originalRemoteAddr + + "', originalRemoteHost='" + originalRemoteHost + "', will be seen as newRemoteAddr='" + remoteIp); + } + + //TODO check put in thread context + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE, Boolean.TRUE); + //request.putInContext(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE, Boolean.TRUE); + return remoteIp; + + } else { + log.warn("Remote ip could not be detected, this should normally not happen"); + } + + } else { + if (log.isTraceEnabled()) { + log.trace("Skip RemoteIpDetector for request " + request.request().uri() + " with originalRemoteAddr '" + + request.getRemoteAddress() + "' cause no internal proxy matches"); + } + } + + return originalRemoteAddr; + } + + /** + *

+ * Regular expression that defines the internal proxies. + *

+ *

+ * Default value : 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254.\d{1,3}.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3} + *

+ */ + public void setInternalProxies(String internalProxies) { + if (internalProxies == null || internalProxies.length() == 0) { + this.internalProxies = null; + } else { + this.internalProxies = Pattern.compile(internalProxies); + } + } + + /** + *

+ * The proxiesHeader directive specifies a header into which mod_remoteip will collect a list of all of the intermediate client IP + * addresses trusted to resolve the actual remote IP. Note that intermediate RemoteIPTrustedProxy addresses are recorded in this header, + * while any intermediate RemoteIPInternalProxy addresses are discarded. + *

+ *

+ * Name of the http header that holds the list of trusted proxies that has been traversed by the http request. + *

+ *

+ * The value of this header can be comma delimited. + *

+ *

+ * Default value : X-Forwarded-By + *

+ */ + public void setProxiesHeader(String proxiesHeader) { + this.proxiesHeader = proxiesHeader; + } + + /** + *

+ * Name of the http header from which the remote ip is extracted. + *

+ *

+ * The value of this header can be comma delimited. + *

+ *

+ * Default value : X-Forwarded-For + *

+ * + * @param remoteIpHeader + */ + public void setRemoteIpHeader(String remoteIpHeader) { + this.remoteIpHeader = remoteIpHeader; + } + + /** + *

+ * Regular expression defining proxies that are trusted when they appear in + * the {@link #remoteIpHeader} header. + *

+ *

+ * Default value : empty list, no external proxy is trusted. + *

+ */ + public void setTrustedProxies(String trustedProxies) { + if (trustedProxies == null || trustedProxies.length() == 0) { + this.trustedProxies = null; + } else { + this.trustedProxies = Pattern.compile(trustedProxies); + } + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/XFFResolver.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/XFFResolver.java new file mode 100644 index 0000000000..7b7ab0a5f8 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/XFFResolver.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http; + +import java.net.InetSocketAddress; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.http.netty4.Netty4HttpRequest; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.configuration.ConfigurationChangeListener; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; + +public class XFFResolver implements ConfigurationChangeListener { + + protected final Logger log = LogManager.getLogger(this.getClass()); + private volatile boolean enabled; + private volatile RemoteIpDetector detector; + private final ThreadContext threadContext; + + public XFFResolver(final ThreadPool threadPool) { + super(); + this.threadContext = threadPool.getThreadContext(); + } + + public TransportAddress resolve(final RestRequest request) throws ElasticsearchSecurityException { + + if(log.isTraceEnabled()) { + log.trace("resolve {}", request.getRemoteAddress()); + } + + if(enabled && request.getRemoteAddress() instanceof InetSocketAddress && request instanceof Netty4HttpRequest) { + + final InetSocketAddress isa = new InetSocketAddress(detector.detect((Netty4HttpRequest) request, threadContext), ((InetSocketAddress)request.getRemoteAddress()).getPort()); + + if(isa.isUnresolved()) { + throw new ElasticsearchSecurityException("Cannot resolve address "+isa.getHostString()); + } + + + if(log.isTraceEnabled()) { + if(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE) == Boolean.TRUE) { + log.trace("xff resolved {} to {}", request.getRemoteAddress(), isa); + } else { + log.trace("no xff done for {}",request.getClass()); + } + } + return new TransportAddress(isa); + } else if(request.getRemoteAddress() instanceof InetSocketAddress){ + + if(log.isTraceEnabled()) { + log.trace("no xff done (enabled or no netty request) {},{},{},{}",enabled, request.getClass()); + + } + return new TransportAddress((InetSocketAddress)request.getRemoteAddress()); + } else { + throw new ElasticsearchSecurityException("Cannot handle this request. Remote address is "+request.getRemoteAddress()+" with request class "+request.getClass()); + } + } + + @Override + public void onChange(final Settings settings) { + enabled = settings.getAsBoolean("opendistro_security.dynamic.http.xff.enabled", true); + if(enabled) { + detector = new RemoteIpDetector(); + detector.setInternalProxies(settings.get("opendistro_security.dynamic.http.xff.internalProxies", detector.getInternalProxies())); + detector.setProxiesHeader(settings.get("opendistro_security.dynamic.http.xff.proxiesHeader", detector.getProxiesHeader())); + detector.setRemoteIpHeader(settings.get("opendistro_security.dynamic.http.xff.remoteIpHeader", detector.getRemoteIpHeader())); + detector.setTrustedProxies(settings.get("opendistro_security.dynamic.http.xff.trustedProxies", detector.getTrustedProxies())); + } else { + detector = null; + } + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/DlsFlsEvaluator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/DlsFlsEvaluator.java new file mode 100644 index 0000000000..00f6ae38cb --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/DlsFlsEvaluator.java @@ -0,0 +1,173 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.privileges; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer.Resolved; +import com.amazon.opendistroforelasticsearch.security.securityconf.ConfigModel.SecurityRoles; +import com.amazon.opendistroforelasticsearch.security.support.Base64Helper; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class DlsFlsEvaluator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + private final ThreadPool threadPool; + + public DlsFlsEvaluator(Settings settings, ThreadPool threadPool) { + this.threadPool = threadPool; + } + + public PrivilegesEvaluatorResponse evaluate(final ClusterService clusterService, final IndexNameExpressionResolver resolver, final Resolved requestedResolved, final User user, + final SecurityRoles securityRoles, final PrivilegesEvaluatorResponse presponse) { + + ThreadContext threadContext = threadPool.getThreadContext(); + + // maskedFields + final Map> maskedFieldsMap = securityRoles.getMaskedFields(user, resolver, clusterService); + + if (maskedFieldsMap != null && !maskedFieldsMap.isEmpty()) { + if (threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER) != null) { + if (!maskedFieldsMap.equals(Base64Helper.deserializeObject(threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER)))) { + throw new ElasticsearchSecurityException(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER + " does not match (Security 901D)"); + } else { + if (log.isDebugEnabled()) { + log.debug(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER + " already set"); + } + } + } else { + threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER, Base64Helper.serializeObject((Serializable) maskedFieldsMap)); + if (log.isDebugEnabled()) { + log.debug("attach masked fields info: {}", maskedFieldsMap); + } + } + + presponse.maskedFields = new HashMap<>(maskedFieldsMap); + + if (!requestedResolved.getAllIndices().isEmpty()) { + for (Iterator>> it = presponse.maskedFields.entrySet().iterator(); it.hasNext();) { + Entry> entry = it.next(); + if (!WildcardMatcher.matchAny(entry.getKey(), requestedResolved.getAllIndices(), false)) { + it.remove(); + } + } + } + } + + + + // attach dls/fls map if not already done + // TODO do this only if enterprise module are loaded + final Tuple>, Map>> dlsFls = securityRoles.getDlsFls(user, resolver, clusterService); + final Map> dlsQueries = dlsFls.v1(); + final Map> flsFields = dlsFls.v2(); + + if (!dlsQueries.isEmpty()) { + + if (threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER) != null) { + if (!dlsQueries.equals(Base64Helper.deserializeObject(threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER)))) { + throw new ElasticsearchSecurityException(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER + " does not match (Security 900D)"); + } + } else { + threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER, Base64Helper.serializeObject((Serializable) dlsQueries)); + if (log.isDebugEnabled()) { + log.debug("attach DLS info: {}", dlsQueries); + } + } + + presponse.queries = new HashMap<>(dlsQueries); + + if (!requestedResolved.getAllIndices().isEmpty()) { + for (Iterator>> it = presponse.queries.entrySet().iterator(); it.hasNext();) { + Entry> entry = it.next(); + if (!WildcardMatcher.matchAny(entry.getKey(), requestedResolved.getAllIndices(), false)) { + it.remove(); + } + } + } + + } + + if (!flsFields.isEmpty()) { + + if (threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER) != null) { + if (!flsFields.equals(Base64Helper.deserializeObject(threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER)))) { + throw new ElasticsearchSecurityException(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER + " does not match (Security 901D)"); + } else { + if (log.isDebugEnabled()) { + log.debug(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER + " already set"); + } + } + } else { + threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER, Base64Helper.serializeObject((Serializable) flsFields)); + if (log.isDebugEnabled()) { + log.debug("attach FLS info: {}", flsFields); + } + } + + presponse.allowedFlsFields = new HashMap<>(flsFields); + + if (!requestedResolved.getAllIndices().isEmpty()) { + for (Iterator>> it = presponse.allowedFlsFields.entrySet().iterator(); it.hasNext();) { + Entry> entry = it.next(); + if (!WildcardMatcher.matchAny(entry.getKey(), requestedResolved.getAllIndices(), false)) { + it.remove(); + } + } + } + } + + if (requestedResolved == Resolved._EMPTY) { + presponse.allowed = true; + return presponse.markComplete(); + } + + return presponse; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/OpenDistroSecurityIndexAccessEvaluator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/OpenDistroSecurityIndexAccessEvaluator.java new file mode 100644 index 0000000000..ab98435bfb --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/OpenDistroSecurityIndexAccessEvaluator.java @@ -0,0 +1,122 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.privileges; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.RealtimeRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.tasks.Task; + +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer.Resolved; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; + +public class OpenDistroSecurityIndexAccessEvaluator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + private final String opendistrosecurityIndex; + private final AuditLog auditLog; + private final String[] securityDeniedActionPatternsAll; + private final String[] securityDeniedActionPatternsSnapshotRestoreAllowed; + + private final boolean restoreSecurityIndexEnabled; + + public OpenDistroSecurityIndexAccessEvaluator(final Settings settings, AuditLog auditLog) { + this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + this.auditLog = auditLog; + + final List securityIndexdeniedActionPatternsListAll = new ArrayList(); + securityIndexdeniedActionPatternsListAll.add("indices:data/write*"); + securityIndexdeniedActionPatternsListAll.add("indices:admin/close"); + securityIndexdeniedActionPatternsListAll.add("indices:admin/delete"); + securityIndexdeniedActionPatternsListAll.add("cluster:admin/snapshot/restore"); + + securityDeniedActionPatternsAll = securityIndexdeniedActionPatternsListAll.toArray(new String[0]); + + final List securityIndexdeniedActionPatternsListSnapshotRestoreAllowed = new ArrayList(); + securityIndexdeniedActionPatternsListAll.add("indices:data/write*"); + securityIndexdeniedActionPatternsListAll.add("indices:admin/delete"); + + securityDeniedActionPatternsSnapshotRestoreAllowed = securityIndexdeniedActionPatternsListSnapshotRestoreAllowed.toArray(new String[0]); + + this.restoreSecurityIndexEnabled = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, false); + } + + public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final Task task, final String action, final Resolved requestedResolved, + final PrivilegesEvaluatorResponse presponse) { + + final String[] securityDeniedActionPatterns = this.restoreSecurityIndexEnabled? securityDeniedActionPatternsSnapshotRestoreAllowed : securityDeniedActionPatternsAll; + + if (requestedResolved.getAllIndices().contains(opendistrosecurityIndex) + && WildcardMatcher.matchAny(securityDeniedActionPatterns, action)) { + auditLog.logSecurityIndexAttempt(request, action, task); + log.warn(action + " for '{}' index is not allowed for a regular user", opendistrosecurityIndex); + presponse.allowed = false; + return presponse.markComplete(); + } + + //TODO: newpeval: check if isAll() is all (contains("_all" or "*")) + if (requestedResolved.isAll() + && WildcardMatcher.matchAny(securityDeniedActionPatterns, action)) { + auditLog.logSecurityIndexAttempt(request, action, task); + log.warn(action + " for '_all' indices is not allowed for a regular user"); + presponse.allowed = false; + return presponse.markComplete(); + } + + //TODO: newpeval: check if isAll() is all (contains("_all" or "*")) + if(requestedResolved.getAllIndices().contains(opendistrosecurityIndex) || requestedResolved.isAll()) { + + if(request instanceof SearchRequest) { + ((SearchRequest)request).requestCache(Boolean.FALSE); + if(log.isDebugEnabled()) { + log.debug("Disable search request cache for this request"); + } + } + + if(request instanceof RealtimeRequest) { + ((RealtimeRequest) request).realtime(Boolean.FALSE); + if(log.isDebugEnabled()) { + log.debug("Disable realtime for this request"); + } + } + } + return presponse; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluator.java new file mode 100644 index 0000000000..81c08725c6 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluator.java @@ -0,0 +1,707 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.privileges; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; +import org.elasticsearch.action.bulk.BulkAction; +import org.elasticsearch.action.bulk.BulkItemRequest; +import org.elasticsearch.action.bulk.BulkShardRequest; +import org.elasticsearch.action.delete.DeleteAction; +import org.elasticsearch.action.get.MultiGetAction; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.action.search.MultiSearchAction; +import org.elasticsearch.action.search.SearchScrollAction; +import org.elasticsearch.action.termvectors.MultiTermVectorsAction; +import org.elasticsearch.action.update.UpdateAction; +import org.elasticsearch.cluster.metadata.AliasMetaData; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.index.reindex.ReindexAction; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.configuration.ActionGroupHolder; +import com.amazon.opendistroforelasticsearch.security.configuration.ClusterInfoHolder; +import com.amazon.opendistroforelasticsearch.security.configuration.ConfigurationRepository; +import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer; +import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer.Resolved; +import com.amazon.opendistroforelasticsearch.security.securityconf.ConfigModel; +import com.amazon.opendistroforelasticsearch.security.securityconf.ConfigModel.SecurityRoles; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class PrivilegesEvaluator { + + + protected final Logger log = LogManager.getLogger(this.getClass()); + protected final Logger actionTrace = LogManager.getLogger("opendistro_security_action_trace"); + private final ClusterService clusterService; + + private final IndexNameExpressionResolver resolver; + + private final AuditLog auditLog; + private ThreadContext threadContext; + //private final static IndicesOptions DEFAULT_INDICES_OPTIONS = IndicesOptions.lenientExpandOpen(); + private final ConfigurationRepository configurationRepository; + + private PrivilegesInterceptor privilegesInterceptor; + + private final boolean checkSnapshotRestoreWritePrivileges; + + private ConfigConstants.RolesMappingResolution rolesMappingResolution; + + private final ClusterInfoHolder clusterInfoHolder; + //private final boolean typeSecurityDisabled = false; + private final ConfigModel configModel; + private final IndexResolverReplacer irr; + private final SnapshotRestoreEvaluator snapshotRestoreEvaluator; + private final OpenDistroSecurityIndexAccessEvaluator securityIndexAccessEvaluator; + private final TermsAggregationEvaluator termsAggregationEvaluator; + + private final DlsFlsEvaluator dlsFlsEvaluator; + + + public PrivilegesEvaluator(final ClusterService clusterService, final ThreadPool threadPool, final ConfigurationRepository configurationRepository, final ActionGroupHolder ah, + final IndexNameExpressionResolver resolver, AuditLog auditLog, final Settings settings, final PrivilegesInterceptor privilegesInterceptor, + final ClusterInfoHolder clusterInfoHolder) { + + super(); + this.configurationRepository = configurationRepository; + this.clusterService = clusterService; + this.resolver = resolver; + this.auditLog = auditLog; + + this.threadContext = threadPool.getThreadContext(); + this.privilegesInterceptor = privilegesInterceptor; + + try { + rolesMappingResolution = ConfigConstants.RolesMappingResolution.valueOf(settings.get(ConfigConstants.OPENDISTRO_SECURITY_ROLES_MAPPING_RESOLUTION, ConfigConstants.RolesMappingResolution.MAPPING_ONLY.toString()).toUpperCase()); + } catch (Exception e) { + log.error("Cannot apply roles mapping resolution",e); + rolesMappingResolution = ConfigConstants.RolesMappingResolution.MAPPING_ONLY; + } + + this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES); + + this.clusterInfoHolder = clusterInfoHolder; + //this.typeSecurityDisabled = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_DISABLE_TYPE_SECURITY, false); + configModel = new ConfigModel(ah, configurationRepository); + irr = new IndexResolverReplacer(resolver, clusterService, clusterInfoHolder); + snapshotRestoreEvaluator = new SnapshotRestoreEvaluator(settings, auditLog); + securityIndexAccessEvaluator = new OpenDistroSecurityIndexAccessEvaluator(settings, auditLog); + dlsFlsEvaluator = new DlsFlsEvaluator(settings, threadPool); + termsAggregationEvaluator = new TermsAggregationEvaluator(); + } + + private Settings getRolesSettings() { + return configurationRepository.getConfiguration(ConfigConstants.CONFIGNAME_ROLES, false); + } + + private Settings getRolesMappingSettings() { + return configurationRepository.getConfiguration(ConfigConstants.CONFIGNAME_ROLES_MAPPING, false); + } + + private Settings getConfigSettings() { + return configurationRepository.getConfiguration(ConfigConstants.CONFIGNAME_CONFIG, false); + } + + //TODO: optimize, recreate only if changed + private SecurityRoles getSecurityRoles(final User user, final TransportAddress caller) { + Set roles = mapSecurityRoles(user, caller); + return configModel.load().filter(roles); + } + + + public boolean isInitialized() { + return getRolesSettings() != null && getRolesMappingSettings() != null && getConfigSettings() != null; + } + + public PrivilegesEvaluatorResponse evaluate(final User user, String action0, final ActionRequest request, Task task) { + + if (!isInitialized()) { + throw new ElasticsearchSecurityException("Open Distro Security is not initialized."); + } + + if(action0.startsWith("internal:indices/admin/upgrade")) { + action0 = "indices:admin/upgrade"; + } + + final TransportAddress caller = Objects.requireNonNull((TransportAddress) this.threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS)); + final SecurityRoles securityRoles = getSecurityRoles(user, caller); + + final PrivilegesEvaluatorResponse presponse = new PrivilegesEvaluatorResponse(); + + + if (log.isDebugEnabled()) { + log.debug("### evaluate permissions for {} on {}", user, clusterService.localNode().getName()); + log.debug("action: "+action0+" ("+request.getClass().getSimpleName()+")"); + } + + final Resolved requestedResolved = irr.resolveRequest(request); + + if (log.isDebugEnabled()) { + log.debug("requestedResolved : {}", requestedResolved ); + } + + + // check snapshot/restore requests + if (dlsFlsEvaluator.evaluate(clusterService, resolver, requestedResolved, user, securityRoles, presponse).isComplete()) { + return presponse; + } + + // check snapshot/restore requests + if (snapshotRestoreEvaluator.evaluate(request, task, action0, clusterInfoHolder, presponse).isComplete()) { + return presponse; + } + + // Security index access + if (securityIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse).isComplete()) { + return presponse; + } + + final boolean dnfofEnabled = + getConfigSettings().getAsBoolean("opendistro_security.dynamic.kibana.do_not_fail_on_forbidden", false) + || getConfigSettings().getAsBoolean("opendistro_security.dynamic.do_not_fail_on_forbidden", false); + + if(log.isTraceEnabled()) { + log.trace("dnfof enabled? {}", dnfofEnabled); + } + + final Settings config = getConfigSettings(); + + if (isClusterPerm(action0)) { + if(!securityRoles.impliesClusterPermissionPermission(action0)) { + presponse.missingPrivileges.add(action0); + presponse.allowed = false; + log.info("No {}-level perm match for {} {} [Action [{}]] [RolesChecked {}]", "cluster" , user, requestedResolved, action0, securityRoles.getRoles().stream().map(r->r.getName()).toArray()); + log.info("No permissions for {}", presponse.missingPrivileges); + return presponse; + } else { + + if(request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { + if(log.isDebugEnabled()) { + log.debug("Normally allowed but we need to apply some extra checks for a restore request."); + } + } else { + + + if(privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + + final Boolean replaceResult = privilegesInterceptor.replaceKibanaIndex(request, action0, user, config, requestedResolved, mapTenants(user, caller)); + + if(log.isDebugEnabled()) { + log.debug("Result from privileges interceptor for cluster perm: {}", replaceResult); + } + + if (replaceResult == Boolean.TRUE) { + auditLog.logMissingPrivileges(action0, request, task); + return presponse; + } + + if (replaceResult == Boolean.FALSE) { + presponse.allowed = true; + return presponse; + } + } + + if (dnfofEnabled + && (action0.startsWith("indices:data/read/")) + && !requestedResolved.getAllIndices().isEmpty() + ) { + + if(requestedResolved.getAllIndices().isEmpty()) { + presponse.missingPrivileges.clear(); + presponse.allowed = true; + return presponse; + } + + + Set reduced = securityRoles.reduce(requestedResolved, user, new String[]{action0}, resolver, clusterService); + + if(reduced.isEmpty()) { + presponse.allowed = false; + return presponse; + } + + if(irr.replace(request, true, reduced.toArray(new String[0]))) { + presponse.missingPrivileges.clear(); + presponse.allowed = true; + return presponse; + } + } + + if(log.isDebugEnabled()) { + log.debug("Allowed because we have cluster permissions for "+action0); + } + presponse.allowed = true; + return presponse; + } + + + } + } + + // term aggregations + if (termsAggregationEvaluator.evaluate(request, clusterService, user, securityRoles, resolver, presponse) .isComplete()) { + return presponse; + } + + final Set allIndexPermsRequired = evaluateAdditionalIndexPermissions(request, action0); + final String[] allIndexPermsRequiredA = allIndexPermsRequired.toArray(new String[0]); + + if(log.isDebugEnabled()) { + log.debug("requested {} from {}", allIndexPermsRequired, caller); + } + + presponse.missingPrivileges.clear(); + presponse.missingPrivileges.addAll(allIndexPermsRequired); + + if (log.isDebugEnabled()) { + log.debug("requested resolved indextypes: {}", requestedResolved); + } + + if (log.isDebugEnabled()) { + log.debug("sgr: {}", securityRoles.getRoles().stream().map(d->d.getName()).toArray()); + } + + + //TODO exclude Security index + + if(privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + + final Boolean replaceResult = privilegesInterceptor.replaceKibanaIndex(request, action0, user, config, requestedResolved, mapTenants(user, caller)); + + if(log.isDebugEnabled()) { + log.debug("Result from privileges interceptor: {}", replaceResult); + } + + if (replaceResult == Boolean.TRUE) { + auditLog.logMissingPrivileges(action0, request, task); + return presponse; + } + + if (replaceResult == Boolean.FALSE) { + presponse.allowed = true; + return presponse; + } + } + + if (dnfofEnabled + && (action0.startsWith("indices:data/read/") + || action0.startsWith("indices:admin/mappings/fields/get"))) { + + if(requestedResolved.getAllIndices().isEmpty()) { + presponse.missingPrivileges.clear(); + presponse.allowed = true; + return presponse; + } + + + Set reduced = securityRoles.reduce(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); + + if(reduced.isEmpty()) { + presponse.allowed = false; + return presponse; + } + + + if(irr.replace(request, true, reduced.toArray(new String[0]))) { + presponse.missingPrivileges.clear(); + presponse.allowed = true; + return presponse; + } + } + + + //not bulk, mget, etc request here + boolean permGiven = false; + + if (config.getAsBoolean("opendistro_security.dynamic.multi_rolespan_enabled", false)) { + permGiven = securityRoles.impliesTypePermGlobal(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); + } else { + permGiven = securityRoles.get(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); + + } + + if (!permGiven) { + log.info("No {}-level perm match for {} {} [Action [{}]] [RolesChecked {}]", "index" , user, requestedResolved, action0, securityRoles.getRoles().stream().map(r->r.getName()).toArray()); + log.info("No permissions for {}", presponse.missingPrivileges); + } else { + + if(checkFilteredAliases(requestedResolved.getAllIndices(), action0)) { + presponse.allowed=false; + return presponse; + } + + if(log.isDebugEnabled()) { + log.debug("Allowed because we have all indices permissions for "+action0); + } + } + + presponse.allowed=permGiven; + return presponse; + + } + public Set mapSecurityRoles(final User user, final TransportAddress caller) { + + final Settings rolesMapping = getRolesMappingSettings(); + final Set securityRoles = new TreeSet(); + + if(user == null) { + return Collections.emptySet(); + } + + if(rolesMappingResolution == ConfigConstants.RolesMappingResolution.BOTH + || rolesMappingResolution == ConfigConstants.RolesMappingResolution.BACKENDROLES_ONLY) { + if(log.isDebugEnabled()) { + log.debug("Pass backendroles from {}", user); + } + securityRoles.addAll(user.getRoles()); + } + + if(rolesMapping != null && ((rolesMappingResolution == ConfigConstants.RolesMappingResolution.BOTH + || rolesMappingResolution == ConfigConstants.RolesMappingResolution.MAPPING_ONLY))) { + for (final String roleMap : rolesMapping.names()) { + final Settings roleMapSettings = rolesMapping.getByPrefix(roleMap); + + if (WildcardMatcher.allPatternsMatched(roleMapSettings.getAsList(".and_backendroles", Collections.emptyList()).toArray(new String[0]), user.getRoles().toArray(new String[0]))) { + securityRoles.add(roleMap); + continue; + } + + if (WildcardMatcher.matchAny(roleMapSettings.getAsList(".backendroles", Collections.emptyList()).toArray(new String[0]), user.getRoles().toArray(new String[0]))) { + securityRoles.add(roleMap); + continue; + } + + if (WildcardMatcher.matchAny(roleMapSettings.getAsList(".users"), user.getName())) { + securityRoles.add(roleMap); + continue; + } + + if(caller != null && log.isTraceEnabled()) { + log.trace("caller (getAddress()) is {}", caller.getAddress()); + log.trace("caller unresolved? {}", caller.address().isUnresolved()); + log.trace("caller inner? {}", caller.address().getAddress()==null?"":caller.address().getAddress().toString()); + log.trace("caller (getHostString()) is {}", caller.address().getHostString()); + log.trace("caller (getHostName(), dns) is {}", caller.address().getHostName()); //reverse lookup + } + + if(caller != null) { + //IPV4 or IPv6 (compressed and without scope identifiers) + final String ipAddress = caller.getAddress(); + if (WildcardMatcher.matchAny(roleMapSettings.getAsList(".hosts"), ipAddress)) { + securityRoles.add(roleMap); + continue; + } + + final String hostResolverMode = getConfigSettings().get("opendistro_security.dynamic.hosts_resolver_mode","ip-only"); + + if(caller.address() != null && (hostResolverMode.equalsIgnoreCase("ip-hostname") || hostResolverMode.equalsIgnoreCase("ip-hostname-lookup"))){ + final String hostName = caller.address().getHostString(); + + if (WildcardMatcher.matchAny(roleMapSettings.getAsList(".hosts"), hostName)) { + securityRoles.add(roleMap); + continue; + } + } + + if(caller.address() != null && hostResolverMode.equalsIgnoreCase("ip-hostname-lookup")){ + + final String resolvedHostName = caller.address().getHostName(); + + if (WildcardMatcher.matchAny(roleMapSettings.getAsList(".hosts"), resolvedHostName)) { + securityRoles.add(roleMap); + continue; + } + } + } + } + } + + return Collections.unmodifiableSet(securityRoles); + + } + + public Map mapTenants(final User user, final TransportAddress caller) { + + if(user == null) { + return Collections.emptyMap(); + } + + final Map result = new HashMap<>(); + result.put(user.getName(), true); + + for(String securityRole: mapSecurityRoles(user, caller)) { + Settings tenants = getRolesSettings().getByPrefix(securityRole+".tenants."); + + if(tenants != null) { + for(String tenant: tenants.names()) { + + if(tenant.equals(user.getName())) { + continue; + } + + if("RW".equalsIgnoreCase(tenants.get(tenant, "RO"))) { + result.put(tenant, true); + } else { + if(!result.containsKey(tenant)) { //RW outperforms RO + result.put(tenant, false); + } + } + } + } + + } + + return Collections.unmodifiableMap(result); + } + + public Set getAllConfiguredTenantNames() { + + final Settings roles = getRolesSettings(); + + if(roles == null || roles.isEmpty()) { + return Collections.emptySet(); + } + + final Set configuredTenants = new HashSet<>(); + for(String securityRole: roles.names()) { + Settings tenants = roles.getByPrefix(securityRole+".tenants."); + + if(tenants != null) { + configuredTenants.addAll(tenants.names()); + } + + } + + return Collections.unmodifiableSet(configuredTenants); + } + + public boolean multitenancyEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class + && getConfigSettings().getAsBoolean("opendistro_security.dynamic.kibana.multitenancy_enabled", true); + } + + public boolean notFailOnForbiddenEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class + && getConfigSettings().getAsBoolean("opendistro_security.dynamic.kibana.do_not_fail_on_forbidden", false); + } + + public String kibanaIndex() { + return getConfigSettings().get("opendistro_security.dynamic.kibana.index",".kibana"); + } + + public String kibanaServerUsername() { + return getConfigSettings().get("opendistro_security.dynamic.kibana.server_username","kibanaserver"); + } + + private Set evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { + //--- check inner bulk requests + final Set additionalPermissionsRequired = new HashSet<>(); + + if(!isClusterPerm(originalAction)) { + additionalPermissionsRequired.add(originalAction); + } + + if (request instanceof BulkShardRequest) { + BulkShardRequest bsr = (BulkShardRequest) request; + for (BulkItemRequest bir : bsr.items()) { + switch (bir.request().opType()) { + case CREATE: + additionalPermissionsRequired.add(IndexAction.NAME); + break; + case INDEX: + additionalPermissionsRequired.add(IndexAction.NAME); + break; + case DELETE: + additionalPermissionsRequired.add(DeleteAction.NAME); + break; + case UPDATE: + additionalPermissionsRequired.add(UpdateAction.NAME); + break; + } + } + } + + if (request instanceof IndicesAliasesRequest) { + IndicesAliasesRequest bsr = (IndicesAliasesRequest) request; + for (AliasActions bir : bsr.getAliasActions()) { + switch (bir.actionType()) { + case REMOVE_INDEX: + additionalPermissionsRequired.add(DeleteIndexAction.NAME); + break; + default: + break; + } + } + } + + if (request instanceof CreateIndexRequest) { + CreateIndexRequest cir = (CreateIndexRequest) request; + if(cir.aliases() != null && !cir.aliases().isEmpty()) { + additionalPermissionsRequired.add(IndicesAliasesAction.NAME); + } + } + + if(request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { + additionalPermissionsRequired.addAll(ConfigConstants.OPENDISTRO_SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES); + } + + if(actionTrace.isTraceEnabled() && additionalPermissionsRequired.size() > 1) { + actionTrace.trace(("Additional permissions required: "+additionalPermissionsRequired)); + } + + if(log.isDebugEnabled() && additionalPermissionsRequired.size() > 1) { + log.debug("Additional permissions required: "+additionalPermissionsRequired); + } + + return Collections.unmodifiableSet(additionalPermissionsRequired); + } + + private static boolean isClusterPerm(String action0) { + return ( action0.startsWith("cluster:") + || action0.startsWith("indices:admin/template/") + + || action0.startsWith(SearchScrollAction.NAME) + || (action0.equals(BulkAction.NAME)) + || (action0.equals(MultiGetAction.NAME)) + || (action0.equals(MultiSearchAction.NAME)) + || (action0.equals(MultiTermVectorsAction.NAME)) + || (action0.equals("indices:data/read/coordinate-msearch")) + || (action0.equals(ReindexAction.NAME)) + + ) ; + } + + private boolean checkFilteredAliases(Set requestedResolvedIndices, String action) { + //check filtered aliases + for(String requestAliasOrIndex: requestedResolvedIndices) { + + final List filteredAliases = new ArrayList(); + + final IndexMetaData indexMetaData = clusterService.state().metaData().getIndices().get(requestAliasOrIndex); + + if(indexMetaData == null) { + log.debug("{} does not exist in cluster metadata", requestAliasOrIndex); + continue; + } + + final ImmutableOpenMap aliases = indexMetaData.getAliases(); + + if(aliases != null && aliases.size() > 0) { + + if(log.isDebugEnabled()) { + log.debug("Aliases for {}: {}", requestAliasOrIndex, aliases); + } + + final Iterator it = aliases.keysIt(); + while(it.hasNext()) { + final String alias = it.next(); + final AliasMetaData aliasMetaData = aliases.get(alias); + + if(aliasMetaData != null && aliasMetaData.filteringRequired()) { + filteredAliases.add(aliasMetaData); + if(log.isDebugEnabled()) { + log.debug(alias+" is a filtered alias "+aliasMetaData.getFilter()); + } + } else { + if(log.isDebugEnabled()) { + log.debug(alias+" is not an alias or does not have a filter"); + } + } + } + } + + if(filteredAliases.size() > 1 && WildcardMatcher.match("indices:data/read/*search*", action)) { + //TODO add queries as dls queries (works only if dls module is installed) + final String faMode = getConfigSettings().get("opendistro_security.dynamic.filtered_alias_mode","warn"); + + if(faMode.equals("warn")) { + log.warn("More than one ({}) filtered alias found for same index ({}). This is currently not recommended. Aliases: {}", filteredAliases.size(), requestAliasOrIndex, toString(filteredAliases)); + } else if (faMode.equals("disallow")) { + log.error("More than one ({}) filtered alias found for same index ({}). This is currently not supported. Aliases: {}", filteredAliases.size(), requestAliasOrIndex, toString(filteredAliases)); + return true; + } else { + if (log.isDebugEnabled()) { + log.debug("More than one ({}) filtered alias found for same index ({}). Aliases: {}", filteredAliases.size(), requestAliasOrIndex, toString(filteredAliases)); + } + } + } + } //end-for + + return false; + } + + private List toString(List aliases) { + if(aliases == null || aliases.size() == 0) { + return Collections.emptyList(); + } + + final List ret = new ArrayList<>(aliases.size()); + + for(final AliasMetaData amd: aliases) { + if(amd != null) { + ret.add(amd.alias()); + } + } + + return Collections.unmodifiableList(ret); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluatorResponse.java new file mode 100644 index 0000000000..0ba11ff069 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.privileges; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class PrivilegesEvaluatorResponse { + boolean allowed = false; + Set missingPrivileges = new HashSet(); + Map> allowedFlsFields; + Map> maskedFields; + Map> queries; + PrivilegesEvaluatorResponseState state = PrivilegesEvaluatorResponseState.PENDING; + + public boolean isAllowed() { + return allowed; + } + public Set getMissingPrivileges() { + return new HashSet(missingPrivileges); + } + + public Map> getAllowedFlsFields() { + return allowedFlsFields; + } + + public Map> getMaskedFields() { + return maskedFields; + } + + public Map> getQueries() { + return queries; + } + + public PrivilegesEvaluatorResponse markComplete() { + this.state = PrivilegesEvaluatorResponseState.COMPLETE; + return this; + } + + public PrivilegesEvaluatorResponse markPending() { + this.state = PrivilegesEvaluatorResponseState.PENDING; + return this; + } + + public boolean isComplete() { + return this.state == PrivilegesEvaluatorResponseState.COMPLETE; + } + + public boolean isPending() { + return this.state == PrivilegesEvaluatorResponseState.PENDING; + } + + @Override + public String toString() { + return "PrivEvalResponse [allowed=" + allowed + ", missingPrivileges=" + missingPrivileges + + ", allowedFlsFields=" + allowedFlsFields + ", maskedFields=" + maskedFields + ", queries=" + queries + "]"; + } + + public static enum PrivilegesEvaluatorResponseState { + PENDING, + COMPLETE; + } + +} \ No newline at end of file diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesInterceptor.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesInterceptor.java new file mode 100644 index 0000000000..8fa0a0ffbb --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/PrivilegesInterceptor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.privileges; + +import java.util.Map; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer.Resolved; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class PrivilegesInterceptor { + + protected final IndexNameExpressionResolver resolver; + protected final ClusterService clusterService; + protected final Client client; + protected final ThreadPool threadPool; + + public PrivilegesInterceptor(final IndexNameExpressionResolver resolver, final ClusterService clusterService, + final Client client, ThreadPool threadPool) { + this.resolver = resolver; + this.clusterService = clusterService; + this.client = client; + this.threadPool = threadPool; + } + + public Boolean replaceKibanaIndex(final ActionRequest request, final String action, final User user, final Settings config, final Resolved requestedResolved, final Map tenants) { + throw new RuntimeException("not implemented"); + } + + protected final ThreadContext getThreadContext() { + return threadPool.getThreadContext(); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/SnapshotRestoreEvaluator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/SnapshotRestoreEvaluator.java new file mode 100644 index 0000000000..0a7409d608 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/SnapshotRestoreEvaluator.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.privileges; + +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.tasks.Task; + +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.configuration.ClusterInfoHolder; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.SnapshotRestoreHelper; + +public class SnapshotRestoreEvaluator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + private final boolean enableSnapshotRestorePrivilege; + private final String opendistrosecurityIndex; + private final AuditLog auditLog; + private final boolean restoreSecurityIndexEnabled; + + public SnapshotRestoreEvaluator(final Settings settings, AuditLog auditLog) { + this.enableSnapshotRestorePrivilege = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE); + this.restoreSecurityIndexEnabled = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, false); + + this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + this.auditLog = auditLog; + } + + public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final Task task, final String action, final ClusterInfoHolder clusterInfoHolder, + final PrivilegesEvaluatorResponse presponse) { + + if (!(request instanceof RestoreSnapshotRequest)) { + return presponse; + } + + // snapshot restore for regular users not enabled + if (!enableSnapshotRestorePrivilege) { + log.warn(action + " is not allowed for a regular user"); + presponse.allowed = false; + return presponse.markComplete(); + } + + // if this feature is enabled, users can also snapshot and restore + // the Security index and the global state + if (restoreSecurityIndexEnabled) { + presponse.allowed = true; + return presponse; + } + + + if (clusterInfoHolder.isLocalNodeElectedMaster() == Boolean.FALSE) { + presponse.allowed = true; + return presponse.markComplete(); + } + + final RestoreSnapshotRequest restoreRequest = (RestoreSnapshotRequest) request; + + // Do not allow restore of global state + if (restoreRequest.includeGlobalState()) { + auditLog.logSecurityIndexAttempt(request, action, task); + log.warn(action + " with 'include_global_state' enabled is not allowed"); + presponse.allowed = false; + return presponse.markComplete(); + } + + final List rs = SnapshotRestoreHelper.resolveOriginalIndices(restoreRequest); + + if (rs != null && (rs.contains(opendistrosecurityIndex) || rs.contains("_all") || rs.contains("*"))) { + auditLog.logSecurityIndexAttempt(request, action, task); + log.warn(action + " for '{}' as source index is not allowed", opendistrosecurityIndex); + presponse.allowed = false; + return presponse.markComplete(); + } + return presponse; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/TermsAggregationEvaluator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/TermsAggregationEvaluator.java new file mode 100644 index 0000000000..90bf21dd2a --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/TermsAggregationEvaluator.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.privileges; + +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.index.query.MatchNoneQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.TermsQueryBuilder; +import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; + +import com.amazon.opendistroforelasticsearch.security.securityconf.ConfigModel.SecurityRoles; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class TermsAggregationEvaluator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + private static final String[] READ_ACTIONS = new String[]{ + "indices:data/read/msearch", + "indices:data/read/mget", + "indices:data/read/get", + "indices:data/read/search", + "indices:data/read/field_caps*" + //"indices:admin/mappings/fields/get*" + }; + + private static final QueryBuilder NONE_QUERY = new MatchNoneQueryBuilder(); + + public TermsAggregationEvaluator() { + } + + public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, ClusterService clusterService, User user, SecurityRoles securityRoles, IndexNameExpressionResolver resolver, PrivilegesEvaluatorResponse presponse) { + try { + if(request instanceof SearchRequest) { + SearchRequest sr = (SearchRequest) request; + + if( sr.source() != null + && sr.source().query() == null + && sr.source().aggregations() != null + && sr.source().aggregations().getAggregatorFactories() != null + && sr.source().aggregations().getAggregatorFactories().size() == 1 + && sr.source().size() == 0) { + AggregationBuilder ab = sr.source().aggregations().getAggregatorFactories().iterator().next(); + if( ab instanceof TermsAggregationBuilder + && "terms".equals(ab.getType()) + && "indices".equals(ab.getName())) { + if("_index".equals(((TermsAggregationBuilder) ab).field()) + && ab.getPipelineAggregations().isEmpty() + && ab.getSubAggregations().isEmpty()) { + + + final Set allPermittedIndices = securityRoles.getAllPermittedIndices(user, READ_ACTIONS, resolver, clusterService); + if(allPermittedIndices == null || allPermittedIndices.isEmpty()) { + sr.source().query(NONE_QUERY); + } else { + sr.source().query(new TermsQueryBuilder("_index", allPermittedIndices)); + } + + presponse.allowed = true; + return presponse.markComplete(); + } + } + } + } + } catch (Exception e) { + log.warn("Unable to evaluate terms aggregation",e); + return presponse; + } + + return presponse; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/resolver/IndexResolverReplacer.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/resolver/IndexResolverReplacer.java new file mode 100644 index 0000000000..cc97bed2e2 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/resolver/IndexResolverReplacer.java @@ -0,0 +1,904 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.resolver; + +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.IndicesRequest.Replaceable; +import org.elasticsearch.action.OriginalIndices; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.action.bulk.BulkItemRequest; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkShardRequest; +import org.elasticsearch.action.delete.DeleteRequest; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.MultiGetRequest; +import org.elasticsearch.action.get.MultiGetRequest.Item; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.main.MainRequest; +import org.elasticsearch.action.search.ClearScrollRequest; +import org.elasticsearch.action.search.MultiSearchRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchScrollRequest; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.action.support.nodes.BaseNodesRequest; +import org.elasticsearch.action.support.replication.ReplicationRequest; +import org.elasticsearch.action.support.single.shard.SingleShardRequest; +import org.elasticsearch.action.termvectors.MultiTermVectorsRequest; +import org.elasticsearch.action.termvectors.TermVectorsRequest; +import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.AliasOrIndex; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.index.reindex.ReindexRequest; +import org.elasticsearch.snapshots.SnapshotInfo; +import org.elasticsearch.snapshots.SnapshotUtils; +import org.elasticsearch.transport.RemoteClusterAware; +import org.elasticsearch.transport.RemoteClusterService; +import org.elasticsearch.transport.TransportRequest; + +import com.amazon.opendistroforelasticsearch.security.OpenDistroSecurityPlugin; +import com.amazon.opendistroforelasticsearch.security.configuration.ClusterInfoHolder; +import com.amazon.opendistroforelasticsearch.security.support.SnapshotRestoreHelper; +import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; +import com.google.common.collect.Sets; + +public final class IndexResolverReplacer { + + //private final static IndicesOptions DEFAULT_INDICES_OPTIONS = IndicesOptions.lenientExpandOpen(); + //private static final String[] NO_INDICES_SET = Sets.newHashSet("\\",";",",","/","|").toArray(new String[0]); + private static final Set NULL_SET = Sets.newHashSet((String)null); + private final Map, Method> typeCache = Collections.synchronizedMap(new HashMap, Method>(100)); + private final Map, Method> typesCache = Collections.synchronizedMap(new HashMap, Method>(100)); + private final Logger log = LogManager.getLogger(this.getClass()); + private final IndexNameExpressionResolver resolver; + private final ClusterService clusterService; + private final ClusterInfoHolder clusterInfoHolder; + + public IndexResolverReplacer(IndexNameExpressionResolver resolver, ClusterService clusterService, ClusterInfoHolder clusterInfoHolder) { + super(); + this.resolver = resolver; + this.clusterService = clusterService; + this.clusterInfoHolder = clusterInfoHolder; + } + + public static final boolean isAll(final String... requestedPatterns) { + + final List patterns = requestedPatterns==null?null:Arrays.asList(requestedPatterns); + + if(IndexNameExpressionResolver.isAllIndices(patterns)) { + return true; + } + + if(patterns.contains("*")) { + return true; + } + + if(patterns.contains("_all")) { + return true; + } + + if(new HashSet(patterns).equals(NULL_SET)) { + return true; + } + + return false; + } + + private Resolved resolveIndexPatterns(final String... requestedPatterns) { + + if(log.isTraceEnabled()) { + log.trace("resolve requestedPatterns: "+Arrays.toString(requestedPatterns)); + } + + if(isAll(requestedPatterns)) { + return Resolved._ALL; + } + + ClusterState state = clusterService.state(); + + final SortedMap lookup = state.metaData().getAliasAndIndexLookup(); + final Set aliases = lookup.entrySet().stream().filter(e->e.getValue().isAlias()).map(e->e.getKey()).collect(Collectors.toSet()); + + final Set matchingAliases = new HashSet<>(requestedPatterns.length*10); + final Set matchingIndices = new HashSet<>(requestedPatterns.length*10); + final Set matchingAllIndices = new HashSet<>(requestedPatterns.length*10); + + //fill matchingAliases + for (int i = 0; i < requestedPatterns.length; i++) { + final String requestedPattern = resolver.resolveDateMathExpression(requestedPatterns[i]); + final List _aliases = WildcardMatcher.getMatchAny(requestedPattern, aliases); + matchingAliases.addAll(_aliases); + } + + + //-alias not possible + + { + //final String requestedPattern = resolver.resolveDateMathExpression(requestedPatterns[i]); + //final List _aliases = WildcardMatcher.getMatchAny(requestedPattern, aliases); + //matchingAliases.addAll(_aliases); + + List _indices; + try { + _indices = new ArrayList<>(Arrays.asList(resolver.concreteIndexNames(state, IndicesOptions.fromOptions(false, true, true, false), requestedPatterns))); + if (log.isDebugEnabled()) { + log.debug("Resolved pattern {} to {}", requestedPatterns, _indices); + } + } catch (IndexNotFoundException e1) { + if (log.isDebugEnabled()) { + log.debug("No such indices for pattern {}, use raw value", (Object[]) requestedPatterns); + } + + _indices = new ArrayList<>(requestedPatterns.length); + + for (int i = 0; i < requestedPatterns.length; i++) { + String requestedPattern = requestedPatterns[i]; + _indices.add(resolver.resolveDateMathExpression(requestedPattern)); + } + + /*if(requestedPatterns.length == 1) { + _indices = Collections.singletonList(resolver.resolveDateMathExpression(requestedPatterns[0])); + } else { + log.warn("Multiple ({}) index patterns {} cannot be resolved, assume _all", requestedPatterns.length, requestedPatterns); + //_indices = Collections.singletonList("*"); + _indices = Arrays.asList(requestedPatterns); //date math not handled + }*/ + + } + + final List _aliases = WildcardMatcher.getMatchAny(requestedPatterns, aliases); + + matchingAllIndices.addAll(_indices); + + if(_aliases.isEmpty()) { + matchingIndices.addAll(_indices); //date math resolved? + } else { + + if(!_indices.isEmpty()) { + + for(String al:_aliases) { + Set doubleIndices = lookup.get(al).getIndices().stream().map(a->a.getIndex().getName()).collect(Collectors.toSet()); + _indices.removeAll(doubleIndices); + } + + matchingIndices.addAll(_indices); + } + } + } + return new Resolved.Builder(matchingAliases, matchingIndices, matchingAllIndices, null).build(); + + } + + @SuppressWarnings("rawtypes") + private Set resolveTypes(final Object request) { + // check if type security is enabled + final Class requestClass = request.getClass(); + final Set requestTypes = new HashSet(); + + if (true) { + if (request instanceof BulkShardRequest) { + BulkShardRequest bsr = (BulkShardRequest) request; + for (BulkItemRequest bir : bsr.items()) { + requestTypes.add(bir.request().type()); + } + } else if (request instanceof DocWriteRequest) { + requestTypes.add(((DocWriteRequest) request).type()); + } else if (request instanceof SearchRequest) { + requestTypes.addAll(Arrays.asList(((SearchRequest) request).types())); + } else if (request instanceof GetRequest) { + requestTypes.add(((GetRequest) request).type()); + } else { + + Method typeMethod = null; + if (typeCache.containsKey(requestClass)) { + typeMethod = typeCache.get(requestClass); + } else { + try { + typeMethod = requestClass.getMethod("type"); + typeCache.put(requestClass, typeMethod); + } catch (NoSuchMethodException e) { + typeCache.put(requestClass, null); + } catch (SecurityException e) { + log.error("Cannot evaluate type() for {} due to {}", requestClass, e, e); + } + + } + + Method typesMethod = null; + if (typesCache.containsKey(requestClass)) { + typesMethod = typesCache.get(requestClass); + } else { + try { + typesMethod = requestClass.getMethod("types"); + typesCache.put(requestClass, typesMethod); + } catch (NoSuchMethodException e) { + typesCache.put(requestClass, null); + } catch (SecurityException e) { + log.error("Cannot evaluate types() for {} due to {}", requestClass, e, e); + } + + } + + if (typeMethod != null) { + try { + String type = (String) typeMethod.invoke(request); + if (type != null) { + requestTypes.add(type); + } + } catch (Exception e) { + log.error("Unable to invoke type() for {} due to", requestClass, e); + } + } + + if (typesMethod != null) { + try { + final String[] types = (String[]) typesMethod.invoke(request); + + if (types != null) { + requestTypes.addAll(Arrays.asList(types)); + } + } catch (Exception e) { + log.error("Unable to invoke types() for {} due to", requestClass, e); + } + } + } + + } + + if (log.isTraceEnabled()) { + log.trace("requestTypes {} for {}", requestTypes, request.getClass()); + } + + return Collections.unmodifiableSet(requestTypes); + } + + /*public boolean exclude(final TransportRequest request, String... exclude) { + return getOrReplaceAllIndices(request, new IndicesProvider() { + + @Override + public String[] provide(final String[] original, final Object request, final boolean supportsReplace) { + if(supportsReplace) { + + final List result = new ArrayList(Arrays.asList(original)); + +// if(isAll(original)) { +// result = new ArrayList(Collections.singletonList("*")); +// } else { +// result = new ArrayList(Arrays.asList(original)); +// } + + + + final Set preliminary = new HashSet<>(resolveIndexPatterns(result.toArray(new String[0])).allIndices); + + if(log.isTraceEnabled()) { + log.trace("resolved original {}, excludes {}",preliminary, Arrays.toString(exclude)); + } + + WildcardMatcher.wildcardRetainInSet(preliminary, exclude); + + if(log.isTraceEnabled()) { + log.trace("modified original {}",preliminary); + } + + result.addAll(preliminary.stream().map(a->"-"+a).collect(Collectors.toList())); + + if(log.isTraceEnabled()) { + log.trace("exclude for {}: replaced {} with {}", request.getClass().getSimpleName(), Arrays.toString(original) ,result); + } + + return result.toArray(new String[0]); + } else { + return NOOP; + } + } + }, false); + }*/ + + //dnfof + public boolean replace(final TransportRequest request, boolean retainMode, String... replacements) { + return getOrReplaceAllIndices(request, new IndicesProvider() { + + @Override + public String[] provide(String[] original, Object request, boolean supportsReplace) { + if(supportsReplace) { + if(retainMode && original != null && original.length > 0) { + //TODO datemath? + List originalAsList = Arrays.asList(original); + if(originalAsList.contains("*") || originalAsList.contains("_all")) { + return replacements; + } + + original = resolver.concreteIndexNames(clusterService.state(), IndicesOptions.lenientExpandOpen(), original); + + final String[] retained = WildcardMatcher.getMatchAny(original, replacements).toArray(new String[0]); + return retained; + } + return replacements; + } else { + return NOOP; + } + } + }, false); + } + + public Resolved resolveRequest(final Object request) { + if(log.isDebugEnabled()) { + log.debug("Resolve aliases, indices and types from {}", request.getClass().getSimpleName()); + } + Resolved.Builder resolvedBuilder = new Resolved.Builder(); + final AtomicBoolean returnEmpty = new AtomicBoolean(); + getOrReplaceAllIndices(request, new IndicesProvider() { + + @Override + public String[] provide(String[] original, Object localRequest, boolean supportsReplace) { + + //CCS + if((localRequest instanceof FieldCapabilitiesRequest || localRequest instanceof SearchRequest) + && (request instanceof FieldCapabilitiesRequest || request instanceof SearchRequest)) { + assert supportsReplace: localRequest.getClass().getName()+" does not support replace"; + final Tuple ccsResult = handleCcs((Replaceable) localRequest); + if(ccsResult.v1() == Boolean.TRUE) { + if(ccsResult.v2() == null || ccsResult.v2().length == 0) { + returnEmpty.set(true); + } + original = ccsResult.v2(); + } + + } + if(returnEmpty.get()) { + + if(log.isTraceEnabled()) { + log.trace("CCS return empty indices for local node"); + } + + } else { + final Resolved iResolved = resolveIndexPatterns(original); + + if(log.isTraceEnabled()) { + log.trace("Resolved patterns {} for {} ({}) to {}", original, localRequest.getClass().getSimpleName(), request.getClass().getSimpleName(), iResolved); + } + + resolvedBuilder.add(iResolved); + resolvedBuilder.addTypes(resolveTypes(localRequest)); + + } + + return IndicesProvider.NOOP; + } + }, false); + + if(log.isTraceEnabled()) { + log.trace("Finally resolved for {}: {}", request.getClass().getSimpleName(), resolvedBuilder.build()); + } + + if(returnEmpty.get()) { + return Resolved._EMPTY; + } + + return resolvedBuilder.build(); + } + + + private Tuple handleCcs(final IndicesRequest.Replaceable request) { + + Boolean modified = Boolean.FALSE; + String[] localIndices = request.indices(); + + final RemoteClusterService remoteClusterService = OpenDistroSecurityPlugin.GuiceHolder.getRemoteClusterService(); + + // handle CCS + // TODO how to handle aliases with CCS?? + if (remoteClusterService.isCrossClusterSearchEnabled() && (request instanceof FieldCapabilitiesRequest || request instanceof SearchRequest)) { + IndicesRequest.Replaceable searchRequest = request; + final Map remoteClusterIndices = OpenDistroSecurityPlugin.GuiceHolder.getRemoteClusterService().groupIndices( + searchRequest.indicesOptions(), searchRequest.indices(), idx -> resolver.hasIndexOrAlias(idx, clusterService.state())); + + assert remoteClusterIndices.size() > 0:"Remote cluster size must not be zero"; + + // check permissions? + if (log.isDebugEnabled()) { + log.debug("CCS case, original indices: " + Arrays.toString(localIndices)); + log.debug("remoteClusterIndices ({}): {}", remoteClusterIndices.size(), remoteClusterIndices); + } + + final OriginalIndices originalLocalIndices = remoteClusterIndices.get(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY); + + if(originalLocalIndices == null) { + localIndices = null; + } else { + localIndices = originalLocalIndices.indices(); + } + + modified = Boolean.TRUE; + + if (log.isDebugEnabled()) { + log.debug("remoteClusterIndices keys" + remoteClusterIndices.keySet() + "//remoteClusterIndices " + + remoteClusterIndices); + log.debug("modified local indices: " + Arrays.toString(localIndices)); + } + } + + return new Tuple(modified, localIndices); + + } + + public final static class Resolved implements Serializable, Writeable { + + /** + * + */ + private static final Set All_SET = Sets.newHashSet("*"); + private static final long serialVersionUID = 1L; + public final static Resolved _ALL = new Resolved(All_SET, All_SET, All_SET, All_SET); + public final static Resolved _EMPTY = new Builder().build(); + + private final Set aliases; + private final Set indices; + private final Set allIndices; + private final Set types; + + private Resolved(final Set aliases, final Set indices, final Set allIndices, final Set types) { + super(); + this.aliases = aliases; + this.indices = indices; + this.allIndices = allIndices; + this.types = types; + + if(!aliases.isEmpty() || !indices.isEmpty() || !allIndices.isEmpty()) { + if(types.isEmpty()) { + throw new ElasticsearchException("Empty types for nonempty inidices or aliases"); + } + } + } + + public boolean isAll() { + return aliases.contains("*") && indices.contains("*") && allIndices.contains("*") && types.contains("*"); + } + + public boolean isEmpty() { + return aliases.isEmpty() && indices.isEmpty() && allIndices.isEmpty() && types.isEmpty(); + } + + public Set getAliases() { + return Collections.unmodifiableSet(aliases); + } + + public Set getIndices() { + return Collections.unmodifiableSet(indices); + } + + public Set getAllIndices() { + return Collections.unmodifiableSet(allIndices); + } + + public Set getTypes() { + return Collections.unmodifiableSet(types); + } + + //TODO equals and hashcode?? + + @Override + public String toString() { + return "Resolved [aliases=" + aliases + ", indices=" + indices + ", allIndices=" + allIndices + ", types=" + types + + ", isAll()=" + isAll() + ", isEmpty()=" + isEmpty() + "]"; + } + + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((aliases == null) ? 0 : aliases.hashCode()); + result = prime * result + ((allIndices == null) ? 0 : allIndices.hashCode()); + result = prime * result + ((indices == null) ? 0 : indices.hashCode()); + result = prime * result + ((types == null) ? 0 : types.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Resolved other = (Resolved) obj; + if (aliases == null) { + if (other.aliases != null) + return false; + } else if (!aliases.equals(other.aliases)) + return false; + if (allIndices == null) { + if (other.allIndices != null) + return false; + } else if (!allIndices.equals(other.allIndices)) + return false; + if (indices == null) { + if (other.indices != null) + return false; + } else if (!indices.equals(other.indices)) + return false; + if (types == null) { + if (other.types != null) + return false; + } else if (!types.equals(other.types)) + return false; + return true; + } + + + + private static class Builder { + + final Set aliases = new HashSet(); + final Set indices = new HashSet(); + final Set allIndices = new HashSet(); + final Set types = new HashSet(); + + public Builder() { + this(null, null, null, null); + } + + public Builder(Collection aliases, Collection indices, Collection allIndices, Collection types) { + + if(aliases != null) { + this.aliases.addAll(aliases); + } + + if(indices != null) { + this.indices.addAll(indices); + } + + if(allIndices != null) { + this.allIndices.addAll(allIndices); + } + + if(types != null) { + this.types.addAll(types); + } + } + + public Builder addTypes(Collection types) { + if(types != null && types.size() > 0) { + if(this.types.contains("*")) { + this.types.remove("*"); + } + this.types.addAll(types); + } + return this; + } + + public Builder add(Resolved r) { + + this.aliases.addAll(r.aliases); + this.indices.addAll(r.indices); + this.allIndices.addAll(r.allIndices); + addTypes(r.types); + return this; + } + + public Resolved build() { + if(types.isEmpty()) { + types.add("*"); + } + + return new Resolved(new HashSet(aliases), new HashSet(indices), new HashSet(allIndices), new HashSet(types)); + } + } + + public Resolved(final StreamInput in) throws IOException { + aliases = new HashSet(in.readList(StreamInput::readString)); + indices = new HashSet(in.readList(StreamInput::readString)); + allIndices = new HashSet(in.readList(StreamInput::readString)); + types = new HashSet(in.readList(StreamInput::readString)); + } + + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeStringList(new ArrayList<>(aliases)); + out.writeStringList(new ArrayList<>(indices)); + out.writeStringList(new ArrayList<>(allIndices)); + out.writeStringList(new ArrayList<>(types)); + + } + } + + private List renamedIndices(final RestoreSnapshotRequest request, final List filteredIndices) { + final List renamedIndices = new ArrayList<>(); + for (final String index : filteredIndices) { + String renamedIndex = index; + if (request.renameReplacement() != null && request.renamePattern() != null) { + renamedIndex = index.replaceAll(request.renamePattern(), request.renameReplacement()); + } + renamedIndices.add(renamedIndex); + } + return renamedIndices; + } + + + //-- + + @FunctionalInterface + public interface IndicesProvider { + public static final String[] NOOP = new String[0]; + String[] provide(String[] original, Object request, boolean supportsReplace); + } + + private boolean checkIndices(Object request, String[] indices, boolean needsToBeSizeOne, boolean allowEmpty) { + + if(indices == IndicesProvider.NOOP) { + return false; + } + + if(!allowEmpty && (indices == null || indices.length == 0)) { + if(log.isTraceEnabled() && request != null) { + log.trace("Null or empty indices for "+request.getClass().getName()); + } + return false; + } + + if(!allowEmpty && needsToBeSizeOne && indices.length != 1) { + if(log.isTraceEnabled() && request != null) { + log.trace("To much indices for "+request.getClass().getName()); + } + return false; + } + + for (int i = 0; i < indices.length; i++) { + final String index = indices[i]; + if(index == null || index.isEmpty()) { + //not allowed + if(log.isTraceEnabled() && request != null) { + log.trace("At least one null or empty index for "+request.getClass().getName()); + } + return false; + } + } + + return true; + } + + /** + * new + * @param request + * @param newIndices + * @return + */ + @SuppressWarnings("rawtypes") + private boolean getOrReplaceAllIndices(final Object request, final IndicesProvider provider, boolean allowEmptyIndices) { + + if(log.isTraceEnabled()) { + log.trace("getOrReplaceAllIndices() for "+request.getClass()); + } + + boolean result = true; + + if (request instanceof BulkRequest) { + + for (DocWriteRequest ar : ((BulkRequest) request).requests()) { + result = getOrReplaceAllIndices(ar, provider, false) && result; + } + + } else if (request instanceof MultiGetRequest) { + + for (ListIterator it = ((MultiGetRequest) request).getItems().listIterator(); it.hasNext();){ + Item item = it.next(); + result = getOrReplaceAllIndices(item, provider, false) && result; + /*if(item.index() == null || item.indices() == null || item.indices().length == 0) { + it.remove(); + }*/ + } + + } else if (request instanceof MultiSearchRequest) { + + for (ListIterator it = ((MultiSearchRequest) request).requests().listIterator(); it.hasNext();) { + SearchRequest ar = it.next(); + result = getOrReplaceAllIndices(ar, provider, false) && result; + /*if(ar.indices() == null || ar.indices().length == 0) { + it.remove(); + }*/ + } + + } else if (request instanceof MultiTermVectorsRequest) { + + for (ActionRequest ar : (Iterable) () -> ((MultiTermVectorsRequest) request).iterator()) { + result = getOrReplaceAllIndices(ar, provider, false) && result; + } + + } else if(request instanceof PutMappingRequest) { + PutMappingRequest pmr = (PutMappingRequest) request; + Index concreteIndex = pmr.getConcreteIndex(); + if(concreteIndex != null && (pmr.indices() == null || pmr.indices().length == 0)) { + String[] newIndices = provider.provide(new String[]{concreteIndex.getName()}, request, true); + if(checkIndices(request, newIndices, true, allowEmptyIndices) == false) { + return false; + } + + ((PutMappingRequest) request).indices(newIndices); + ((PutMappingRequest) request).setConcreteIndex(null); + } else { + String[] newIndices = provider.provide(((PutMappingRequest) request).indices(), request, true); + if(checkIndices(request, newIndices, false, allowEmptyIndices) == false) { + return false; + } + ((PutMappingRequest) request).indices(newIndices); + } + } else if(request instanceof RestoreSnapshotRequest) { + + if(clusterInfoHolder.isLocalNodeElectedMaster() == Boolean.FALSE) { + return true; + } + + final RestoreSnapshotRequest restoreRequest = (RestoreSnapshotRequest) request; + final SnapshotInfo snapshotInfo = SnapshotRestoreHelper.getSnapshotInfo(restoreRequest); + + if (snapshotInfo == null) { + log.warn("snapshot repository '" + restoreRequest.repository() + "', snapshot '" + restoreRequest.snapshot() + "' not found"); + provider.provide(new String[]{"*"}, request, false); + } else { + final List requestedResolvedIndices = SnapshotUtils.filterIndices(snapshotInfo.indices(), restoreRequest.indices(), restoreRequest.indicesOptions()); + final List renamedTargetIndices = renamedIndices(restoreRequest, requestedResolvedIndices); + //final Set indices = new HashSet<>(requestedResolvedIndices); + //indices.addAll(renamedTargetIndices); + if(log.isDebugEnabled()) { + log.debug("snapshot: {} contains this indices: {}", snapshotInfo.snapshotId().getName(), renamedTargetIndices); + } + provider.provide(renamedTargetIndices.toArray(new String[0]), request, false); + } + + } else if (request instanceof IndicesAliasesRequest) { + for(AliasActions ar: ((IndicesAliasesRequest) request).getAliasActions()) { + result = getOrReplaceAllIndices(ar, provider, false) && result; + } + } else if (request instanceof DeleteRequest) { + String[] newIndices = provider.provide(((DeleteRequest) request).indices(), request, true); + if(checkIndices(request, newIndices, true, allowEmptyIndices) == false) { + return false; + } + ((DeleteRequest) request).index(newIndices.length!=1?null:newIndices[0]); + } else if (request instanceof UpdateRequest) { + String[] newIndices = provider.provide(((UpdateRequest) request).indices(), request, true); + if(checkIndices(request, newIndices, true, allowEmptyIndices) == false) { + return false; + } + ((UpdateRequest) request).index(newIndices.length!=1?null:newIndices[0]); + } else if (request instanceof SingleShardRequest) { + final SingleShardRequest gr = (SingleShardRequest) request; + final String[] indices = gr.indices(); + final String index = gr.index(); + + final List indicesL = new ArrayList(); + + if (index != null) { + indicesL.add(index); + } + + if (indices != null && indices.length > 0) { + indicesL.addAll(Arrays.asList(indices)); + } + + String[] newIndices = provider.provide(indicesL.toArray(new String[0]), request, true); + if(checkIndices(request, newIndices, true, allowEmptyIndices) == false) { + return false; + } + ((SingleShardRequest) request).index(newIndices.length!=1?null:newIndices[0]); + } else if (request instanceof IndexRequest) { + String[] newIndices = provider.provide(((IndexRequest) request).indices(), request, true); + if(checkIndices(request, newIndices, true, allowEmptyIndices) == false) { + return false; + } + ((IndexRequest) request).index(newIndices.length!=1?null:newIndices[0]); + } else if (request instanceof Replaceable) { + String[] newIndices = provider.provide(((Replaceable) request).indices(), request, true); + if(checkIndices(request, newIndices, false, allowEmptyIndices) == false) { + return false; + } + ((Replaceable) request).indices(newIndices); + } else if (request instanceof BulkShardRequest) { + provider.provide(((ReplicationRequest) request).indices(), request, false); + //replace not supported? + } else if (request instanceof ReplicationRequest) { + String[] newIndices = provider.provide(((ReplicationRequest) request).indices(), request, true); + if(checkIndices(request, newIndices, true, allowEmptyIndices) == false) { + return false; + } + ((ReplicationRequest) request).index(newIndices.length!=1?null:newIndices[0]); + } else if (request instanceof MultiGetRequest.Item) { + String[] newIndices = provider.provide(((MultiGetRequest.Item) request).indices(), request, true); + if(checkIndices(request, newIndices, true, allowEmptyIndices) == false) { + return false; + } + ((MultiGetRequest.Item) request).index(newIndices.length!=1?null:newIndices[0]); + } else if (request instanceof CreateIndexRequest) { + String[] newIndices = provider.provide(((CreateIndexRequest) request).indices(), request, true); + if(checkIndices(request, newIndices, true, allowEmptyIndices) == false) { + return false; + } + ((CreateIndexRequest) request).index(newIndices.length!=1?null:newIndices[0]); + } else if (request instanceof ReindexRequest) { + result = getOrReplaceAllIndices(((ReindexRequest) request).getDestination(), provider, false) && result; + result = getOrReplaceAllIndices(((ReindexRequest) request).getSearchRequest(), provider, false) && result; + } else if (request instanceof BaseNodesRequest) { + //do nothing + } else if (request instanceof MainRequest) { + //do nothing + } else if (request instanceof ClearScrollRequest) { + //do nothing + } else if (request instanceof SearchScrollRequest) { + //do nothing + } else { + if(log.isDebugEnabled()) { + log.debug(request.getClass() + " not supported (It is likely not a indices related request)"); + } + result = false; + } + + return result; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/KibanaInfoAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/KibanaInfoAction.java new file mode 100644 index 0000000000..2d722cc487 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/KibanaInfoAction.java @@ -0,0 +1,119 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.rest; + +import static org.elasticsearch.rest.RestRequest.Method.GET; +import static org.elasticsearch.rest.RestRequest.Method.POST; + +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.privileges.PrivilegesEvaluator; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class KibanaInfoAction extends BaseRestHandler { + + private final Logger log = LogManager.getLogger(this.getClass()); + private final PrivilegesEvaluator evaluator; + private final ThreadContext threadContext; + + public KibanaInfoAction(final Settings settings, final RestController controller, final PrivilegesEvaluator evaluator, final ThreadPool threadPool) { + super(settings); + this.threadContext = threadPool.getThreadContext(); + this.evaluator = evaluator; + controller.registerHandler(GET, "/_opendistro/_security/kibanainfo", this); + controller.registerHandler(POST, "/_opendistro/_security/kibanainfo", this); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + return new RestChannelConsumer() { + + @Override + public void accept(RestChannel channel) throws Exception { + XContentBuilder builder = channel.newBuilder(); //NOSONAR + BytesRestResponse response = null; + + try { + + final User user = (User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final TransportAddress remoteAddress = (TransportAddress) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + + builder.startObject(); + builder.field("user_name", user==null?null:user.getName()); + builder.field("not_fail_on_forbidden_enabled", evaluator.notFailOnForbiddenEnabled()); + builder.field("kibana_mt_enabled", evaluator.multitenancyEnabled()); + builder.field("kibana_index", evaluator.kibanaIndex()); + builder.field("kibana_server_user", evaluator.kibanaServerUsername()); + //builder.field("kibana_index_readonly", evaluator.kibanaIndexReadonly(user, remoteAddress)); + builder.endObject(); + + response = new BytesRestResponse(RestStatus.OK, builder); + } catch (final Exception e1) { + log.error(e1.toString(),e1); + builder = channel.newBuilder(); //NOSONAR + builder.startObject(); + builder.field("error", e1.toString()); + builder.endObject(); + response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); + } finally { + if(builder != null) { + builder.close(); + } + } + + channel.sendResponse(response); + } + }; + } + + @Override + public String getName() { + return "Kibana Info Action"; + } + + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/OpenDistroSecurityHealthAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/OpenDistroSecurityHealthAction.java new file mode 100644 index 0000000000..1f8a8d1734 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/OpenDistroSecurityHealthAction.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.rest; + +import static org.elasticsearch.rest.RestRequest.Method.GET; +import static org.elasticsearch.rest.RestRequest.Method.POST; + +import java.io.IOException; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestStatus; + +import com.amazon.opendistroforelasticsearch.security.auth.BackendRegistry; + +public class OpenDistroSecurityHealthAction extends BaseRestHandler { + + private final BackendRegistry registry; + + public OpenDistroSecurityHealthAction(final Settings settings, final RestController controller, final BackendRegistry registry) { + super(settings); + this.registry = registry; + controller.registerHandler(GET, "/_opendistro/_security/health", this); + controller.registerHandler(POST, "/_opendistro/_security/health", this); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + return new RestChannelConsumer() { + + final String mode = request.param("mode","strict"); + + @Override + public void accept(RestChannel channel) throws Exception { + XContentBuilder builder = channel.newBuilder(); + RestStatus restStatus = RestStatus.OK; + BytesRestResponse response = null; + try { + + + String status = "UP"; + String message = null; + + builder.startObject(); + + if ("strict".equalsIgnoreCase(mode) && registry.isInitialized() == false) { + status = "DOWN"; + message = "Not initialized"; + restStatus = RestStatus.SERVICE_UNAVAILABLE; + } + + builder.field("message", message); + builder.field("mode", mode); + builder.field("status", status); + builder.endObject(); + response = new BytesRestResponse(restStatus, builder); + + } finally { + builder.close(); + } + + + channel.sendResponse(response); + } + + + }; + } + + @Override + public String getName() { + return "Open Distro Security Health Check"; + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/OpenDistroSecurityInfoAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/OpenDistroSecurityInfoAction.java new file mode 100644 index 0000000000..adbad79ef4 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/OpenDistroSecurityInfoAction.java @@ -0,0 +1,143 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.rest; + +import static org.elasticsearch.rest.RestRequest.Method.GET; +import static org.elasticsearch.rest.RestRequest.Method.POST; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.lucene.util.RamUsageEstimator; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.privileges.PrivilegesEvaluator; +import com.amazon.opendistroforelasticsearch.security.support.Base64Helper; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class OpenDistroSecurityInfoAction extends BaseRestHandler { + + private final Logger log = LogManager.getLogger(this.getClass()); + private final PrivilegesEvaluator evaluator; + private final ThreadContext threadContext; + + public OpenDistroSecurityInfoAction(final Settings settings, final RestController controller, final PrivilegesEvaluator evaluator, final ThreadPool threadPool) { + super(settings); + this.threadContext = threadPool.getThreadContext(); + this.evaluator = evaluator; + controller.registerHandler(GET, "/_opendistro/_security/authinfo", this); + controller.registerHandler(POST, "/_opendistro/_security/authinfo", this); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + return new RestChannelConsumer() { + + @Override + public void accept(RestChannel channel) throws Exception { + XContentBuilder builder = channel.newBuilder(); //NOSONAR + BytesRestResponse response = null; + + try { + + + final boolean verbose = request.paramAsBoolean("verbose", false); + + final X509Certificate[] certs = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PEER_CERTIFICATES); + final User user = (User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final TransportAddress remoteAddress = (TransportAddress) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + + builder.startObject(); + builder.field("user", user==null?null:user.toString()); + builder.field("user_name", user==null?null:user.getName()); + builder.field("user_requested_tenant", user==null?null:user.getRequestedTenant()); + builder.field("remote_address", remoteAddress); + builder.field("backend_roles", user==null?null:user.getRoles()); + builder.field("custom_attribute_names", user==null?null:user.getCustomAttributesMap().keySet()); + builder.field("roles", evaluator.mapSecurityRoles(user, remoteAddress)); + builder.field("tenants", evaluator.mapTenants(user, remoteAddress)); + builder.field("principal", (String)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL)); + builder.field("peer_certificates", certs != null && certs.length > 0 ? certs.length + "" : "0"); + builder.field("sso_logout_url", (String)threadContext.getTransient(ConfigConstants.SSO_LOGOUT_URL)); + + if(user != null && verbose) { + try { + builder.field("size_of_user", RamUsageEstimator.humanReadableUnits(Base64Helper.serializeObject(user).length())); + builder.field("size_of_custom_attributes", RamUsageEstimator.humanReadableUnits(Base64Helper.serializeObject((Serializable) user.getCustomAttributesMap()).getBytes(StandardCharsets.UTF_8).length)); + builder.field("size_of_backendroles", RamUsageEstimator.humanReadableUnits(Base64Helper.serializeObject((Serializable)user.getRoles()).getBytes(StandardCharsets.UTF_8).length)); + } catch (Throwable e) { + //ignore + } + } + + + builder.endObject(); + + response = new BytesRestResponse(RestStatus.OK, builder); + } catch (final Exception e1) { + log.error(e1.toString(),e1); + builder = channel.newBuilder(); //NOSONAR + builder.startObject(); + builder.field("error", e1.toString()); + builder.endObject(); + response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); + } finally { + if(builder != null) { + builder.close(); + } + } + + channel.sendResponse(response); + } + }; + } + + @Override + public String getName() { + return "Open Distro Security Info Action"; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/TenantInfoAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/TenantInfoAction.java new file mode 100644 index 0000000000..324781dd7f --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/rest/TenantInfoAction.java @@ -0,0 +1,167 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.rest; + +import static org.elasticsearch.rest.RestRequest.Method.GET; +import static org.elasticsearch.rest.RestRequest.Method.POST; + +import java.io.IOException; +import java.util.SortedMap; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.cluster.metadata.AliasOrIndex; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.configuration.AdminDNs; +import com.amazon.opendistroforelasticsearch.security.privileges.PrivilegesEvaluator; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class TenantInfoAction extends BaseRestHandler { + + private final Logger log = LogManager.getLogger(this.getClass()); + private final PrivilegesEvaluator evaluator; + private final ThreadContext threadContext; + private final ClusterService clusterService; + private final AdminDNs adminDns; + + public TenantInfoAction(final Settings settings, final RestController controller, + final PrivilegesEvaluator evaluator, final ThreadPool threadPool, final ClusterService clusterService, final AdminDNs adminDns) { + super(settings); + this.threadContext = threadPool.getThreadContext(); + this.evaluator = evaluator; + this.clusterService = clusterService; + this.adminDns = adminDns; + controller.registerHandler(GET, "/_opendistro/_security/tenantinfo", this); + controller.registerHandler(POST, "/_opendistro/_security/tenantinfo", this); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + return new RestChannelConsumer() { + + @Override + public void accept(RestChannel channel) throws Exception { + XContentBuilder builder = channel.newBuilder(); //NOSONAR + BytesRestResponse response = null; + + try { + + final User user = (User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + + //only allowed for admins or the kibanaserveruser + if(user == null || + (!user.getName().equals(evaluator.kibanaServerUsername())) + && !adminDns.isAdmin(user)) { + response = new BytesRestResponse(RestStatus.FORBIDDEN,""); + } else { + + builder.startObject(); + + final SortedMap lookup = clusterService.state().metaData().getAliasAndIndexLookup(); + for(final String indexOrAlias: lookup.keySet()) { + final String tenant = tenantNameForIndex(indexOrAlias); + if(tenant != null) { + builder.field(indexOrAlias, tenant); + } + } + + builder.endObject(); + + response = new BytesRestResponse(RestStatus.OK, builder); + } + } catch (final Exception e1) { + log.error(e1.toString(),e1); + builder = channel.newBuilder(); //NOSONAR + builder.startObject(); + builder.field("error", e1.toString()); + builder.endObject(); + response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); + } finally { + if(builder != null) { + builder.close(); + } + } + + channel.sendResponse(response); + } + }; + } + + private String tenantNameForIndex(String index) { + String[] indexParts; + if(index == null + || (indexParts = index.split("_")).length != 3 + ) { + return null; + } + + + if(!indexParts[0].equals(evaluator.kibanaIndex())) { + return null; + } + + try { + final int expectedHash = Integer.parseInt(indexParts[1]); + final String sanitizedName = indexParts[2]; + + for(String tenant: evaluator.getAllConfiguredTenantNames()) { + if(tenant.hashCode() == expectedHash && sanitizedName.equals(tenant.toLowerCase().replaceAll("[^a-z0-9]+",""))) { + return tenant; + } + } + + return "__private__"; + } catch (NumberFormatException e) { + log.warn("Index "+index+" looks like a Security tenant index but we cannot parse the hashcode so we ignore it."); + return null; + } + } + + @Override + public String getName() { + return "Tenant Info Action"; + } + + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/ConfigModel.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/ConfigModel.java new file mode 100644 index 0000000000..b9ed97268c --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/securityconf/ConfigModel.java @@ -0,0 +1,830 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.securityconf; + +import java.util.Arrays; +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.Map.Entry; +import java.util.Objects; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; + +import com.amazon.opendistroforelasticsearch.security.configuration.ActionGroupHolder; +import com.amazon.opendistroforelasticsearch.security.configuration.ConfigurationRepository; +import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer.Resolved; +import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; +import com.amazon.opendistroforelasticsearch.security.user.User; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; + +public class ConfigModel { + + protected final Logger log = LogManager.getLogger(this.getClass()); + private static final Set IGNORED_TYPES = ImmutableSet.of("_dls_", "_fls_","_masked_fields_"); + private final ActionGroupHolder ah; + private final ConfigurationRepository configurationRepository; + + public ConfigModel(final ActionGroupHolder ah, + final ConfigurationRepository configurationRepository) { + super(); + this.ah = ah; + this.configurationRepository = configurationRepository; + } + + public SecurityRoles load() { + final Settings settings = configurationRepository.getConfiguration("roles", false); + SecurityRoles _securityRoles = new SecurityRoles(); + Set securityRoles = settings.names(); + for(String securityRole: securityRoles) { + + SecurityRole _securityRole = new SecurityRole(securityRole); + + final Settings securityRoleSettings = settings.getByPrefix(securityRole); + if (securityRoleSettings.names().isEmpty()) { + continue; + } + + final Set permittedClusterActions = ah.resolvedActions(securityRoleSettings.getAsList(".cluster", Collections.emptyList())); + _securityRole.addClusterPerms(permittedClusterActions); + + Settings tenants = settings.getByPrefix(securityRole+".tenants."); + + if(tenants != null) { + for(String tenant: tenants.names()) { + + //if(tenant.equals(user.getName())) { + // continue; + //} + + if("RW".equalsIgnoreCase(tenants.get(tenant, "RO"))) { + _securityRole.addTenant(new Tenant(tenant, true)); + } else { + _securityRole.addTenant(new Tenant(tenant, false)); + //if(_securityRole.tenants.stream().filter(t->t.tenant.equals(tenant)).count() > 0) { //RW outperforms RO + // _securityRole.addTenant(new Tenant(tenant, false)); + //} + } + } + } + + + final Map permittedAliasesIndices = securityRoleSettings.getGroups(".indices"); + + for (final String permittedAliasesIndex : permittedAliasesIndices.keySet()) { + + final String resolvedRole = securityRole; + final String indexPattern = permittedAliasesIndex; + + final String dls = settings.get(resolvedRole+".indices."+indexPattern+"._dls_"); + final List fls = settings.getAsList(resolvedRole+".indices."+indexPattern+"._fls_"); + final List maskedFields = settings.getAsList(resolvedRole+".indices."+indexPattern+"._masked_fields_"); + + IndexPattern _indexPattern = new IndexPattern(indexPattern); + _indexPattern.setDlsQuery(dls); + _indexPattern.addFlsFields(fls); + _indexPattern.addMaskedFields(maskedFields); + + for(String type: permittedAliasesIndices.get(indexPattern).names()) { + + if(IGNORED_TYPES.contains(type)) { + continue; + } + + TypePerm typePerm = new TypePerm(type); + final List perms = settings.getAsList(resolvedRole+".indices."+indexPattern+"."+type); + typePerm.addPerms(ah.resolvedActions(perms)); + _indexPattern.addTypePerms(typePerm); + } + + _securityRole.addIndexPattern(_indexPattern); + + } + _securityRoles.addSecurityRole(_securityRole); + } + + return _securityRoles; + } + + //beans + + public static class SecurityRoles { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + final Set roles = new HashSet<>(100); + + private SecurityRoles() { + } + + private SecurityRoles addSecurityRole(SecurityRole securityRole) { + if(securityRole != null) { + this.roles.add(securityRole); + } + return this; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((roles == null) ? 0 : roles.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SecurityRoles other = (SecurityRoles) obj; + if (roles == null) { + if (other.roles != null) + return false; + } else if (!roles.equals(other.roles)) + return false; + return true; + } + + @Override + public String toString() { + return "roles=" + roles; + } + + public Set getRoles() { + return Collections.unmodifiableSet(roles); + } + + public SecurityRoles filter(Set keep) { + final SecurityRoles retVal = new SecurityRoles(); + for(SecurityRole sgr: roles) { + if(keep.contains(sgr.getName())) { + retVal.addSecurityRole(sgr); + } + } + return retVal; + } + + public Map> getMaskedFields(User user, IndexNameExpressionResolver resolver, ClusterService cs) { + final Map> maskedFieldsMap = new HashMap>(); + + for(SecurityRole sgr: roles) { + for(IndexPattern ip: sgr.getIpatterns()) { + final Set maskedFields = ip.getMaskedFields(); + final String indexPattern = ip.getUnresolvedIndexPattern(user); + String[] concreteIndices = new String[0]; + + if((maskedFields != null && maskedFields.size() > 0)) { + concreteIndices = ip.getResolvedIndexPattern(user, resolver, cs); + } + + if(maskedFields != null && maskedFields.size() > 0) { + + if(maskedFieldsMap.containsKey(indexPattern)) { + maskedFieldsMap.get(indexPattern).addAll(Sets.newHashSet(maskedFields)); + } else { + maskedFieldsMap.put(indexPattern, new HashSet()); + maskedFieldsMap.get(indexPattern).addAll(Sets.newHashSet(maskedFields)); + } + + for (int i = 0; i < concreteIndices.length; i++) { + final String ci = concreteIndices[i]; + if(maskedFieldsMap.containsKey(ci)) { + maskedFieldsMap.get(ci).addAll(Sets.newHashSet(maskedFields)); + } else { + maskedFieldsMap.put(ci, new HashSet()); + maskedFieldsMap.get(ci).addAll(Sets.newHashSet(maskedFields)); + } + } + } + } + } + return maskedFieldsMap; + } + + public Tuple>,Map>> getDlsFls(User user, IndexNameExpressionResolver resolver, ClusterService cs) { + + final Map> dlsQueries = new HashMap>(); + final Map> flsFields = new HashMap>(); + + for(SecurityRole sgr: roles) { + for(IndexPattern ip: sgr.getIpatterns()) { + final Set fls = ip.getFls(); + final String dls = ip.getDlsQuery(user); + final String indexPattern = ip.getUnresolvedIndexPattern(user); + String[] concreteIndices = new String[0]; + + if((dls != null && dls.length() > 0) || (fls != null && fls.size() > 0)) { + concreteIndices = ip.getResolvedIndexPattern(user, resolver, cs); + } + + if(dls != null && dls.length() > 0) { + + if(dlsQueries.containsKey(indexPattern)) { + dlsQueries.get(indexPattern).add(dls); + } else { + dlsQueries.put(indexPattern, new HashSet()); + dlsQueries.get(indexPattern).add(dls); + } + + + for (int i = 0; i < concreteIndices.length; i++) { + final String ci = concreteIndices[i]; + if(dlsQueries.containsKey(ci)) { + dlsQueries.get(ci).add(dls); + } else { + dlsQueries.put(ci, new HashSet()); + dlsQueries.get(ci).add(dls); + } + } + + } + + if(fls != null && fls.size() > 0) { + + if(flsFields.containsKey(indexPattern)) { + flsFields.get(indexPattern).addAll(Sets.newHashSet(fls)); + } else { + flsFields.put(indexPattern, new HashSet()); + flsFields.get(indexPattern).addAll(Sets.newHashSet(fls)); + } + + for (int i = 0; i < concreteIndices.length; i++) { + final String ci = concreteIndices[i]; + if(flsFields.containsKey(ci)) { + flsFields.get(ci).addAll(Sets.newHashSet(fls)); + } else { + flsFields.put(ci, new HashSet()); + flsFields.get(ci).addAll(Sets.newHashSet(fls)); + } + } + } + } + } + + return new Tuple>, Map>>(dlsQueries, flsFields); + + } + + //kibana special only + public Set getAllPermittedIndices(User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { + Set retVal = new HashSet<>(); + for(SecurityRole sgr: roles) { + retVal.addAll(sgr.getAllResolvedPermittedIndices(Resolved._ALL, user, actions, resolver, cs)); + } + return Collections.unmodifiableSet(retVal); + } + + //dnfof only + public Set reduce(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { + Set retVal = new HashSet<>(); + for(SecurityRole sgr: roles) { + retVal.addAll(sgr.getAllResolvedPermittedIndices(resolved, user, actions, resolver, cs)); + } + if(log.isDebugEnabled()) { + log.debug("Reduced requested resolved indices {} to permitted indices {}.", resolved, retVal.toString()); + } + return Collections.unmodifiableSet(retVal); + } + + //return true on success + public boolean get(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { + for(SecurityRole sgr: roles) { + if(ConfigModel.impliesTypePerm(sgr.getIpatterns(), resolved, user, actions, resolver, cs)) { + return true; + } + } + return false; + } + + public boolean impliesClusterPermissionPermission(String action) { + return roles.stream() + .filter(r->r.impliesClusterPermission(action)).count() > 0; + } + + //rolespan + public boolean impliesTypePermGlobal(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { + Set ipatterns = new HashSet(); + roles.stream().forEach(p->ipatterns.addAll(p.getIpatterns())); + return ConfigModel.impliesTypePerm(ipatterns, resolved, user, actions, resolver, cs); + } + } + + public static class SecurityRole { + + private final String name; + private final Set tenants = new HashSet<>(); + private final Set ipatterns = new HashSet<>(); + private final Set clusterPerms = new HashSet<>(); + + private SecurityRole(String name) { + super(); + this.name = Objects.requireNonNull(name); + } + + private boolean impliesClusterPermission(String action) { + return WildcardMatcher.matchAny(clusterPerms, action); + } + + //get indices which are permitted for the given types and actions + //dnfof + kibana special only + private Set getAllResolvedPermittedIndices(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { + + final Set retVal = new HashSet<>(); + for(IndexPattern p: ipatterns) { + //what if we cannot resolve one (for create purposes) + boolean patternMatch = false; + final Set tperms = p.getTypePerms(); + for(TypePerm tp: tperms) { + if(WildcardMatcher.matchAny(tp.typePattern, resolved.getTypes().toArray(new String[0]))) { + patternMatch = WildcardMatcher.matchAll(tp.perms.toArray(new String[0]), actions); + } + } + if(patternMatch) { + //resolved but can contain patterns for nonexistent indices + final String[] permitted = p.getResolvedIndexPattern(user, resolver, cs); //maybe they do not exists + final Set res = new HashSet<>(); + if(!resolved.isAll() && !resolved.getAllIndices().contains("*") && !resolved.getAllIndices().contains("_all")) { + final Set wanted = new HashSet<>(resolved.getAllIndices()); + //resolved but can contain patterns for nonexistent indices + WildcardMatcher.wildcardRetainInSet(wanted, permitted); + res.addAll(wanted); + } else { + //we want all indices so just return what's permitted + + //#557 + final String[] allIndices = resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), "*"); + final Set wanted = new HashSet<>(Arrays.asList(allIndices)); + WildcardMatcher.wildcardRetainInSet(wanted, permitted); + res.addAll(wanted); + //res.addAll(Arrays.asList(resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), permitted))); + } + retVal.addAll(res); + } + } + + //all that we want and all thats permitted of them + return Collections.unmodifiableSet(retVal); + } + + + + private SecurityRole addTenant(Tenant tenant) { + if(tenant != null) { + this.tenants.add(tenant); + } + return this; + } + + private SecurityRole addIndexPattern(IndexPattern indexPattern) { + if(indexPattern != null) { + this.ipatterns.add(indexPattern); + } + return this; + } + + private SecurityRole addClusterPerms(Collection clusterPerms) { + if(clusterPerms != null) { + this.clusterPerms.addAll(clusterPerms); + } + return this; + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((clusterPerms == null) ? 0 : clusterPerms.hashCode()); + result = prime * result + ((ipatterns == null) ? 0 : ipatterns.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((tenants == null) ? 0 : tenants.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SecurityRole other = (SecurityRole) obj; + if (clusterPerms == null) { + if (other.clusterPerms != null) + return false; + } else if (!clusterPerms.equals(other.clusterPerms)) + return false; + if (ipatterns == null) { + if (other.ipatterns != null) + return false; + } else if (!ipatterns.equals(other.ipatterns)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (tenants == null) { + if (other.tenants != null) + return false; + } else if (!tenants.equals(other.tenants)) + return false; + return true; + } + + @Override + public String toString() { + return System.lineSeparator()+" "+name+System.lineSeparator()+" tenants=" + tenants + System.lineSeparator()+ " ipatterns=" + ipatterns + System.lineSeparator()+ " clusterPerms=" + clusterPerms; + } + + public Set getTenants(User user) { + //TODO filter out user tenants + return Collections.unmodifiableSet(tenants); + } + + public Set getIpatterns() { + return Collections.unmodifiableSet(ipatterns); + } + + public Set getClusterPerms() { + return Collections.unmodifiableSet(clusterPerms); + } + + public String getName() { + return name; + } + + } + + //Security roles + public static class IndexPattern { + private final String indexPattern; + private String dlsQuery; + private final Set fls = new HashSet<>(); + private final Set maskedFields = new HashSet<>(); + private final Set typePerms = new HashSet<>(); + + public IndexPattern(String indexPattern) { + super(); + this.indexPattern = Objects.requireNonNull(indexPattern); + } + + public IndexPattern addFlsFields(List flsFields) { + if(flsFields != null) { + this.fls.addAll(flsFields); + } + return this; + } + + public IndexPattern addMaskedFields(List maskedFields) { + if(maskedFields != null) { + this.maskedFields.addAll(maskedFields); + } + return this; + } + + public IndexPattern addTypePerms(TypePerm typePerm) { + if(typePerm != null) { + this.typePerms.add(typePerm); + } + return this; + } + + public IndexPattern setDlsQuery(String dlsQuery) { + if(dlsQuery != null) { + this.dlsQuery = dlsQuery; + } + return this; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((dlsQuery == null) ? 0 : dlsQuery.hashCode()); + result = prime * result + ((fls == null) ? 0 : fls.hashCode()); + result = prime * result + ((maskedFields == null) ? 0 : maskedFields.hashCode()); + result = prime * result + ((indexPattern == null) ? 0 : indexPattern.hashCode()); + result = prime * result + ((typePerms == null) ? 0 : typePerms.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + IndexPattern other = (IndexPattern) obj; + if (dlsQuery == null) { + if (other.dlsQuery != null) + return false; + } else if (!dlsQuery.equals(other.dlsQuery)) + return false; + if (fls == null) { + if (other.fls != null) + return false; + } else if (!fls.equals(other.fls)) + return false; + if (maskedFields == null) { + if (other.maskedFields != null) + return false; + } else if (!maskedFields.equals(other.maskedFields)) + return false; + if (indexPattern == null) { + if (other.indexPattern != null) + return false; + } else if (!indexPattern.equals(other.indexPattern)) + return false; + if (typePerms == null) { + if (other.typePerms != null) + return false; + } else if (!typePerms.equals(other.typePerms)) + return false; + return true; + } + + @Override + public String toString() { + return System.lineSeparator()+" indexPattern=" + indexPattern + System.lineSeparator()+" dlsQuery=" + dlsQuery + System.lineSeparator()+ " fls=" + fls + System.lineSeparator()+ " typePerms=" + typePerms; + } + + public String getUnresolvedIndexPattern(User user) { + return replaceProperties(indexPattern, user); + } + + private String[] getResolvedIndexPattern(User user, IndexNameExpressionResolver resolver, ClusterService cs) { + String unresolved = getUnresolvedIndexPattern(user); + String[] resolved = null; + if(WildcardMatcher.containsWildcard(unresolved)) { + final String[] aliasesForPermittedPattern = cs.state().getMetaData().getAliasAndIndexLookup() + .entrySet().stream() + .filter(e->e.getValue().isAlias()) + .filter(e->WildcardMatcher.match(unresolved, e.getKey())) + .map(e->e.getKey()).toArray(String[]::new); + + if(aliasesForPermittedPattern != null && aliasesForPermittedPattern.length > 0) { + resolved = resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), aliasesForPermittedPattern); + } + } + + if(resolved == null && !unresolved.isEmpty()) { + resolved = resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), unresolved); + } + if(resolved == null || resolved.length == 0) { + return new String[]{unresolved}; + } else { + //append unresolved value for pattern matching + String[] retval = Arrays.copyOf(resolved, resolved.length +1); + retval[retval.length-1] = unresolved; + return retval; + } + } + + public String getDlsQuery(User user) { + return replaceProperties(dlsQuery, user); + } + + public Set getFls() { + return Collections.unmodifiableSet(fls); + } + + public Set getMaskedFields() { + return Collections.unmodifiableSet(maskedFields); + } + + public Set getTypePerms() { + return Collections.unmodifiableSet(typePerms); + } + + + + } + + public static class TypePerm { + private final String typePattern; + private final Set perms = new HashSet<>(); + + private TypePerm(String typePattern) { + super(); + this.typePattern = Objects.requireNonNull(typePattern); + if(IGNORED_TYPES.contains(typePattern)) { + throw new RuntimeException("typepattern '"+typePattern+"' not allowed"); + } + } + + private TypePerm addPerms(Collection perms) { + if(perms != null) { + this.perms.addAll(perms); + } + return this; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((perms == null) ? 0 : perms.hashCode()); + result = prime * result + ((typePattern == null) ? 0 : typePattern.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TypePerm other = (TypePerm) obj; + if (perms == null) { + if (other.perms != null) + return false; + } else if (!perms.equals(other.perms)) + return false; + if (typePattern == null) { + if (other.typePattern != null) + return false; + } else if (!typePattern.equals(other.typePattern)) + return false; + return true; + } + + @Override + public String toString() { + return System.lineSeparator()+" typePattern=" + typePattern + System.lineSeparator()+ " perms=" + perms; + } + + public String getTypePattern() { + return typePattern; + } + + public Set getPerms() { + return Collections.unmodifiableSet(perms); + } + + } + + public static class Tenant { + private final String tenant; + private final boolean readWrite; + private Tenant(String tenant, boolean readWrite) { + super(); + this.tenant = tenant; + this.readWrite = readWrite; + } + public String getTenant() { + return tenant; + } + public boolean isReadWrite() { + return readWrite; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (readWrite ? 1231 : 1237); + result = prime * result + ((tenant == null) ? 0 : tenant.hashCode()); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Tenant other = (Tenant) obj; + if (readWrite != other.readWrite) + return false; + if (tenant == null) { + if (other.tenant != null) + return false; + } else if (!tenant.equals(other.tenant)) + return false; + return true; + } + @Override + public String toString() { + return System.lineSeparator()+" tenant=" + tenant + System.lineSeparator() +" readWrite=" + readWrite; + } + } + + + private static String replaceProperties(String orig, User user) { + + if(user == null || orig == null) { + return orig; + } + + orig = orig.replace("${user.name}", user.getName()).replace("${user_name}", user.getName()); + orig = replaceRoles(orig, user); + for(Entry entry: user.getCustomAttributesMap().entrySet()) { + if(entry == null || entry.getKey() == null || entry.getValue() == null) { + continue; + } + orig = orig.replace("${"+entry.getKey()+"}", entry.getValue()); + orig = orig.replace("${"+entry.getKey().replace('.', '_')+"}", entry.getValue()); + } + return orig; + } + + private static String replaceRoles(final String orig, final User user) { + String retVal = orig; + if(orig.contains("${user.roles}") || orig.contains("${user_roles}")) { + final String commaSeparatedRoles = toQuotedCommaSeparatedString(user.getRoles()); + retVal = orig.replace("${user.roles}", commaSeparatedRoles).replace("${user_roles}", commaSeparatedRoles); + } + return retVal; + } + + private static String toQuotedCommaSeparatedString(final Set roles) { + return Joiner.on(',').join(Iterables.transform(roles, s->{ + return new StringBuilder(s.length()+2).append('"').append(s).append('"').toString(); + })); + } + + private static boolean impliesTypePerm(Set ipatterns, Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { + Set matchingIndex = new HashSet<>(resolved.getAllIndices()); + + for(String in: resolved.getAllIndices()) { + //find index patterns who are matching + Set matchingActions = new HashSet<>(Arrays.asList(actions)); + Set matchingTypes = new HashSet<>(resolved.getTypes()); + for(IndexPattern p: ipatterns) { + if(WildcardMatcher.matchAny(p.getResolvedIndexPattern(user, resolver, cs), in)) { + //per resolved index per pattern + for(String t: resolved.getTypes()) { + for(TypePerm tp: p.typePerms) { + if(WildcardMatcher.match(tp.typePattern, t)) { + matchingTypes.remove(t); + for(String a: Arrays.asList(actions)) { + if(WildcardMatcher.matchAny(tp.perms, a)) { + matchingActions.remove(a); + } + } + } + } + } + } + } + + if(matchingActions.isEmpty() && matchingTypes.isEmpty()) { + matchingIndex.remove(in); + } + } + + return matchingIndex.isEmpty(); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/Base64Helper.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/Base64Helper.java new file mode 100644 index 0000000000..ad3315ec7b --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/Base64Helper.java @@ -0,0 +1,145 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidClassException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.Serializable; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.elasticsearch.ElasticsearchException; + +import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer; +import com.amazon.opendistroforelasticsearch.security.user.User; +import com.google.common.io.BaseEncoding; + +public class Base64Helper { + + public static String serializeObject(final Serializable object) { + + if (object == null) { + throw new IllegalArgumentException("object must not be null"); + } + + try { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + final ObjectOutputStream out = new ObjectOutputStream(bos); + out.writeObject(object); + final byte[] bytes = bos.toByteArray(); + return BaseEncoding.base64().encode(bytes); + } catch (final Exception e) { + throw new ElasticsearchException(e.toString()); + } + } + + public static Serializable deserializeObject(final String string) { + + if (string == null) { + throw new IllegalArgumentException("string must not be null"); + } + + SafeObjectInputStream in = null; + + try { + final byte[] userr = BaseEncoding.base64().decode(string); + final ByteArrayInputStream bis = new ByteArrayInputStream(userr); //NOSONAR + in = new SafeObjectInputStream(bis); //NOSONAR + return (Serializable) in.readObject(); + } catch (final Exception e) { + throw new ElasticsearchException(e); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + private final static class SafeObjectInputStream extends ObjectInputStream { + + private static final List SAFE_CLASSES = new ArrayList<>(); + + static { + SAFE_CLASSES.add("com.amazon.dlic.auth.ldap.LdapUser"); + SAFE_CLASSES.add("org.ldaptive.SearchEntry"); + SAFE_CLASSES.add("org.ldaptive.LdapEntry"); + SAFE_CLASSES.add("org.ldaptive.AbstractLdapBean"); + SAFE_CLASSES.add("org.ldaptive.LdapAttribute"); + SAFE_CLASSES.add("org.ldaptive.LdapAttribute$LdapAttributeValues"); + } + + public SafeObjectInputStream(InputStream in) throws IOException { + super(in); + } + + @Override + protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { + + Class clazz = super.resolveClass(desc); + + if ( + clazz.isArray() || + clazz.equals(String.class) || + clazz.equals(SocketAddress.class) || + clazz.equals(InetSocketAddress.class) || + InetAddress.class.isAssignableFrom(clazz) || + Number.class.isAssignableFrom(clazz) || + Collection.class.isAssignableFrom(clazz) || + Map.class.isAssignableFrom(clazz) || + Enum.class.isAssignableFrom(clazz) || + clazz.equals(User.class) || + clazz.equals(IndexResolverReplacer.Resolved.class) || + clazz.equals(SourceFieldsContext.class) || + SAFE_CLASSES.contains(clazz.getName()) + ) { + + return clazz; + } + + throw new InvalidClassException("Unauthorized deserialization attempt", clazz.getName()); + } + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ConfigConstants.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ConfigConstants.java new file mode 100644 index 0000000000..712a1527d2 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ConfigConstants.java @@ -0,0 +1,242 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ConfigConstants { + + + public static final String OPENDISTRO_SECURITY_CONFIG_PREFIX = "_opendistro_security_"; + + public static final String OPENDISTRO_SECURITY_CHANNEL_TYPE = OPENDISTRO_SECURITY_CONFIG_PREFIX+"channel_type"; + + public static final String OPENDISTRO_SECURITY_ORIGIN = OPENDISTRO_SECURITY_CONFIG_PREFIX+"origin"; + public static final String OPENDISTRO_SECURITY_ORIGIN_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX+"origin_header"; + + public static final String OPENDISTRO_SECURITY_DLS_QUERY_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX+"dls_query"; + + public static final String OPENDISTRO_SECURITY_FLS_FIELDS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX+"fls_fields"; + + public static final String OPENDISTRO_SECURITY_MASKED_FIELD_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX+"masked_fields"; + + public static final String OPENDISTRO_SECURITY_CONF_REQUEST_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX+"conf_request"; + + public static final String OPENDISTRO_SECURITY_REMOTE_ADDRESS = OPENDISTRO_SECURITY_CONFIG_PREFIX+"remote_address"; + public static final String OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX+"remote_address_header"; + + public static final String OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX+"initial_action_class_header"; + + /** + * Set by SSL plugin for https requests only + */ + public static final String OPENDISTRO_SECURITY_SSL_PEER_CERTIFICATES = OPENDISTRO_SECURITY_CONFIG_PREFIX+"ssl_peer_certificates"; + + /** + * Set by SSL plugin for https requests only + */ + public static final String OPENDISTRO_SECURITY_SSL_PRINCIPAL = OPENDISTRO_SECURITY_CONFIG_PREFIX+"ssl_principal"; + + /** + * If this is set to TRUE then the request comes from a Server Node (fully trust) + * Its expected that there is a _opendistro_security_user attached as header + */ + public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_INTERCLUSTER_REQUEST = OPENDISTRO_SECURITY_CONFIG_PREFIX+"ssl_transport_intercluster_request"; + + public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTED_CLUSTER_REQUEST = OPENDISTRO_SECURITY_CONFIG_PREFIX+"ssl_transport_trustedcluster_request"; + + + /** + * Set by the SSL plugin, this is the peer node certificate on the transport layer + */ + public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_PRINCIPAL = OPENDISTRO_SECURITY_CONFIG_PREFIX+"ssl_transport_principal"; + + public static final String OPENDISTRO_SECURITY_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX+"user"; + public static final String OPENDISTRO_SECURITY_USER_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX+"user_header"; + + public static final String OPENDISTRO_SECURITY_INJECTED_USER = "injected_user"; + + public static final String OPENDISTRO_SECURITY_XFF_DONE = OPENDISTRO_SECURITY_CONFIG_PREFIX+"xff_done"; + + public static final String SSO_LOGOUT_URL = OPENDISTRO_SECURITY_CONFIG_PREFIX+"sso_logout_url"; + + + public static final String OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX = ".opendistro_security"; + + public static final String OPENDISTRO_SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE = "opendistro_security.enable_snapshot_restore_privilege"; + public static final boolean OPENDISTRO_SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE = false; + + public static final String OPENDISTRO_SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES = "opendistro_security.check_snapshot_restore_write_privileges"; + public static final boolean OPENDISTRO_SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES = true; + public static final Set OPENDISTRO_SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES = Collections.unmodifiableSet( + new HashSet(Arrays.asList( + "indices:admin/create", + "indices:data/write/index" + // "indices:data/write/bulk" + ))); + + public final static String CONFIGNAME_ROLES = "roles"; + public final static String CONFIGNAME_ROLES_MAPPING = "rolesmapping"; + public final static String CONFIGNAME_ACTION_GROUPS = "actiongroups"; + public final static String CONFIGNAME_INTERNAL_USERS = "internalusers"; + public final static String CONFIGNAME_CONFIG = "config"; + public final static String CONFIGKEY_ACTION_GROUPS_PERMISSIONS = "permissions"; + public final static String CONFIGKEY_READONLY = "readonly"; + public final static String CONFIGKEY_HIDDEN = "hidden"; + + public final static List CONFIG_NAMES = Collections.unmodifiableList(Arrays.asList(new String[] {CONFIGNAME_ROLES, CONFIGNAME_ROLES_MAPPING, + CONFIGNAME_ACTION_GROUPS, CONFIGNAME_INTERNAL_USERS, CONFIGNAME_CONFIG})); + public static final String OPENDISTRO_SECURITY_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = "opendistro_security.cert.intercluster_request_evaluator_class"; + public static final String OPENDISTRO_SECURITY_ACTION_NAME = OPENDISTRO_SECURITY_CONFIG_PREFIX+"action_name"; + + + public static final String OPENDISTRO_SECURITY_AUTHCZ_ADMIN_DN = "opendistro_security.authcz.admin_dn"; + public static final String OPENDISTRO_SECURITY_CONFIG_INDEX_NAME = "opendistro_security.config_index_name"; + public static final String OPENDISTRO_SECURITY_AUTHCZ_IMPERSONATION_DN = "opendistro_security.authcz.impersonation_dn"; + public static final String OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS="opendistro_security.authcz.rest_impersonation_user"; + + public static final String OPENDISTRO_SECURITY_AUDIT_TYPE_DEFAULT = "opendistro_security.audit.type"; + public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT = "opendistro_security.audit.config"; + public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_ROUTES = "opendistro_security.audit.routes"; + public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_ENDPOINTS = "opendistro_security.audit.endpoints"; + public static final String OPENDISTRO_SECURITY_AUDIT_THREADPOOL_SIZE = "opendistro_security.audit.threadpool.size"; + public static final String OPENDISTRO_SECURITY_AUDIT_THREADPOOL_MAX_QUEUE_LEN = "opendistro_security.audit.threadpool.max_queue_len"; + public static final String OPENDISTRO_SECURITY_AUDIT_LOG_REQUEST_BODY = "opendistro_security.audit.log_request_body"; + public static final String OPENDISTRO_SECURITY_AUDIT_RESOLVE_INDICES = "opendistro_security.audit.resolve_indices"; + public static final String OPENDISTRO_SECURITY_AUDIT_ENABLE_REST = "opendistro_security.audit.enable_rest"; + public static final String OPENDISTRO_SECURITY_AUDIT_ENABLE_TRANSPORT = "opendistro_security.audit.enable_transport"; + public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES = "opendistro_security.audit.config.disabled_transport_categories"; + public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES = "opendistro_security.audit.config.disabled_rest_categories"; + public static final String OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS = "opendistro_security.audit.ignore_users"; + public static final String OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS = "opendistro_security.audit.ignore_requests"; + public static final String OPENDISTRO_SECURITY_AUDIT_RESOLVE_BULK_REQUESTS = "opendistro_security.audit.resolve_bulk_requests"; + public static final boolean OPENDISTRO_SECURITY_AUDIT_SSL_VERIFY_HOSTNAMES_DEFAULT = true; + public static final boolean OPENDISTRO_SECURITY_AUDIT_SSL_ENABLE_SSL_CLIENT_AUTH_DEFAULT = false; + public static final String OPENDISTRO_SECURITY_AUDIT_EXCLUDE_SENSITIVE_HEADERS = "opendistro_security.audit.exclude_sensitive_headers"; + + public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX = "opendistro_security.audit.config."; + + // Internal / External ES + public static final String OPENDISTRO_SECURITY_AUDIT_ES_INDEX = "index"; + public static final String OPENDISTRO_SECURITY_AUDIT_ES_TYPE = "type"; + + // External ES + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_HTTP_ENDPOINTS = "http_endpoints"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_USERNAME = "username"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PASSWORD = "password"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_ENABLE_SSL = "enable_ssl"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_VERIFY_HOSTNAMES = "verify_hostnames"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_ENABLE_SSL_CLIENT_AUTH = "enable_ssl_client_auth"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMKEY_FILEPATH = "pemkey_filepath"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMKEY_CONTENT = "pemkey_content"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMKEY_PASSWORD = "pemkey_password"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMCERT_FILEPATH = "pemcert_filepath"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMCERT_CONTENT = "pemcert_content"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMTRUSTEDCAS_FILEPATH = "pemtrustedcas_filepath"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_PEMTRUSTEDCAS_CONTENT = "pemtrustedcas_content"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_JKS_CERT_ALIAS = "cert_alias"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_ENABLED_SSL_CIPHERS = "enabled_ssl_ciphers"; + public static final String OPENDISTRO_SECURITY_AUDIT_EXTERNAL_ES_ENABLED_SSL_PROTOCOLS = "enabled_ssl_protocols"; + + // Webhooks + public static final String OPENDISTRO_SECURITY_AUDIT_WEBHOOK_URL = "webhook.url"; + public static final String OPENDISTRO_SECURITY_AUDIT_WEBHOOK_FORMAT = "webhook.format"; + public static final String OPENDISTRO_SECURITY_AUDIT_WEBHOOK_SSL_VERIFY = "webhook.ssl.verify"; + public static final String OPENDISTRO_SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_FILEPATH = "webhook.ssl.pemtrustedcas_filepath"; + public static final String OPENDISTRO_SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_CONTENT = "webhook.ssl.pemtrustedcas_content"; + + // Log4j + public static final String OPENDISTRO_SECURITY_AUDIT_LOG4J_LOGGER_NAME = "log4j.logger_name"; + public static final String OPENDISTRO_SECURITY_AUDIT_LOG4J_LEVEL = "log4j.level"; + + //retry + public static final String OPENDISTRO_SECURITY_AUDIT_RETRY_COUNT = "opendistro_security.audit.config.retry_count"; + public static final String OPENDISTRO_SECURITY_AUDIT_RETRY_DELAY_MS = "opendistro_security.audit.config.retry_delay_ms"; + + + public static final String OPENDISTRO_SECURITY_KERBEROS_KRB5_FILEPATH = "opendistro_security.kerberos.krb5_filepath"; + public static final String OPENDISTRO_SECURITY_KERBEROS_ACCEPTOR_KEYTAB_FILEPATH = "opendistro_security.kerberos.acceptor_keytab_filepath"; + public static final String OPENDISTRO_SECURITY_KERBEROS_ACCEPTOR_PRINCIPAL = "opendistro_security.kerberos.acceptor_principal"; + public static final String OPENDISTRO_SECURITY_CERT_OID = "opendistro_security.cert.oid"; + public static final String OPENDISTRO_SECURITY_CERT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = "opendistro_security.cert.intercluster_request_evaluator_class"; + public static final String OPENDISTRO_SECURITY_ENTERPRISE_MODULES_ENABLED = "opendistro_security.enterprise_modules_enabled"; + public static final String OPENDISTRO_SECURITY_NODES_DN = "opendistro_security.nodes_dn"; + public static final String OPENDISTRO_SECURITY_DISABLED = "opendistro_security.disabled"; + public static final String OPENDISTRO_SECURITY_CACHE_TTL_MINUTES = "opendistro_security.cache.ttl_minutes"; + public static final String OPENDISTRO_SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES = "opendistro_security.allow_unsafe_democertificates"; + public static final String OPENDISTRO_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX = "opendistro_security.allow_default_init_securityindex"; + + public static final String OPENDISTRO_SECURITY_ROLES_MAPPING_RESOLUTION = "opendistro_security.roles_mapping_resolution"; + + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY = "opendistro_security.compliance.history.write.metadata_only"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_METADATA_ONLY = "opendistro_security.compliance.history.read.metadata_only"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS = "opendistro_security.compliance.history.read.watched_fields"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES = "opendistro_security.compliance.history.write.watched_indices"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS = "opendistro_security.compliance.history.write.log_diffs"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_IGNORE_USERS = "opendistro_security.compliance.history.read.ignore_users"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_IGNORE_USERS = "opendistro_security.compliance.history.write.ignore_users"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED = "opendistro_security.compliance.history.external_config_enabled"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION = "opendistro_security.compliance.disable_anonymous_authentication"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_IMMUTABLE_INDICES = "opendistro_security.compliance.immutable_indices"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_SALT = "opendistro_security.compliance.salt"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_SALT_DEFAULT = "e1ukloTsQlOgPquJ";//16 chars + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED = "opendistro_security.compliance.history.internal_config_enabled"; + + public static final String OPENDISTRO_SECURITY_SSL_ONLY = "opendistro_security.ssl_only"; + + public enum RolesMappingResolution { + MAPPING_ONLY, + BACKENDROLES_ONLY, + BOTH + } + + + //public static final String OPENDISTRO_SECURITY_TRIBE_CLUSTERNAME = "opendistro_security.tribe.clustername"; + //public static final String OPENDISTRO_SECURITY_DISABLE_TYPE_SECURITY = "opendistro_security.disable_type_security"; + + // REST API + public static final String OPENDISTRO_SECURITY_RESTAPI_ROLES_ENABLED = "opendistro_security.restapi.roles_enabled"; + public static final String OPENDISTRO_SECURITY_RESTAPI_ENDPOINTS_DISABLED = "opendistro_security.restapi.endpoints_disabled"; + public static final String OPENDISTRO_SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX = "opendistro_security.restapi.password_validation_regex"; + public static final String OPENDISTRO_SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE = "opendistro_security.restapi.password_validation_error_message"; + + + // Illegal Opcodes from here on + public static final String OPENDISTRO_SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY = "opendistro_security.unsupported.disable_rest_auth_initially"; + public static final String OPENDISTRO_SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY = "opendistro_security.unsupported.disable_intertransport_auth_initially"; + public static final String OPENDISTRO_SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED = "opendistro_security.unsupported.restore.securityindex.enabled"; + public static final String OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_USER_ENABLED = "opendistro_security.unsupported.inject_user.enabled"; + public static final String OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED = "opendistro_security.unsupported.inject_user.admin.enabled"; +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ConfigHelper.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ConfigHelper.java new file mode 100644 index 0000000000..cbdf06e001 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ConfigHelper.java @@ -0,0 +1,92 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.io.ByteArrayInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; + +public class ConfigHelper { + + private static final Logger LOGGER = LogManager.getLogger(ConfigHelper.class); + + public static void uploadFile(Client tc, String filepath, String index, String id) throws Exception { + LOGGER.info("Will update '" + id + "' with " + filepath); + try (Reader reader = new FileReader(filepath)) { + + final String res = tc + .index(new IndexRequest(index).type("security").id(id).setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(id, readXContent(reader, XContentType.YAML))).actionGet().getId(); + + if (!id.equals(res)) { + throw new Exception(" FAIL: Configuration for '" + id + + "' failed for unknown reasons. Pls. consult logfile of elasticsearch"); + } + } catch (Exception e) { + throw e; + } + } + + public static BytesReference readXContent(final Reader reader, final XContentType xContentType) throws IOException { + BytesReference retVal; + XContentParser parser = null; + try { + parser = XContentFactory.xContent(xContentType).createParser(NamedXContentRegistry.EMPTY, OpenDistroSecurityDeprecationHandler.INSTANCE, reader); + parser.nextToken(); + final XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.copyCurrentStructure(parser); + retVal = BytesReference.bytes(builder); + } finally { + if (parser != null) { + parser.close(); + } + } + + //validate + Settings.builder().loadFromStream("dummy.json", new ByteArrayInputStream(BytesReference.toBytes(retVal)), true).build(); + return retVal; + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/HTTPHelper.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/HTTPHelper.java new file mode 100644 index 0000000000..aabbb50875 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/HTTPHelper.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.Logger; +import org.elasticsearch.rest.RestRequest; + +import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials; + +public class HTTPHelper { + + public static AuthCredentials extractCredentials(String authorizationHeader, Logger log) { + + if (authorizationHeader != null) { + if (!authorizationHeader.trim().toLowerCase().startsWith("basic ")) { + log.warn("No 'Basic Authorization' header, send 401 and 'WWW-Authenticate Basic'"); + return null; + } else { + + final String decodedBasicHeader = new String(Base64.getDecoder().decode(authorizationHeader.split(" ")[1]), + StandardCharsets.UTF_8); + + //username:password + //special case + //username must not contain a :, but password is allowed to do so + // username:pass:word + //blank password + // username: + + final int firstColonIndex = decodedBasicHeader.indexOf(':'); + + String username = null; + String password = null; + + if (firstColonIndex > 0) { + username = decodedBasicHeader.substring(0, firstColonIndex); + + if(decodedBasicHeader.length() - 1 != firstColonIndex) { + password = decodedBasicHeader.substring(firstColonIndex + 1); + } else { + //blank password + password=""; + } + } + + if (username == null || password == null) { + log.warn("Invalid 'Authorization' header, send 401 and 'WWW-Authenticate Basic'"); + return null; + } else { + return new AuthCredentials(username, password.getBytes(StandardCharsets.UTF_8)).markComplete(); + } + } + } else { + return null; + } + } + + public static boolean containsBadHeader(final RestRequest request) { + + final Map> headers; + + if (request != null && ( headers = request.getHeaders()) != null) { + for (final String key: headers.keySet()) { + if ( key != null + && key.trim().toLowerCase().startsWith(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_PREFIX.toLowerCase())) { + return true; + } + } + } + + return false; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/HeaderHelper.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/HeaderHelper.java new file mode 100644 index 0000000000..146cb633f7 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/HeaderHelper.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.io.Serializable; +import java.util.Map; + +import org.elasticsearch.common.util.concurrent.ThreadContext; + +import com.google.common.base.Strings; + +public class HeaderHelper { + + public static boolean isInterClusterRequest(final ThreadContext context) { + return context.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_INTERCLUSTER_REQUEST) == Boolean.TRUE; + } + + public static boolean isDirectRequest(final ThreadContext context) { + + return "direct".equals(context.getTransient(ConfigConstants.OPENDISTRO_SECURITY_CHANNEL_TYPE)) + || context.getTransient(ConfigConstants.OPENDISTRO_SECURITY_CHANNEL_TYPE) == null; + } + + + public static String getSafeFromHeader(final ThreadContext context, final String headerName) { + + if (context == null || headerName == null || headerName.isEmpty()) { + return null; + } + + String headerValue = null; + + Map headers = context.getHeaders(); + if (!headers.containsKey(headerName) || (headerValue = headers.get(headerName)) == null) { + return null; + } + + if (isInterClusterRequest(context) || isTrustedClusterRequest(context) || isDirectRequest(context)) { + return headerValue; + } + + return null; + } + + public static Serializable deserializeSafeFromHeader(final ThreadContext context, final String headerName) { + + final String objectAsBase64 = getSafeFromHeader(context, headerName); + + if (!Strings.isNullOrEmpty(objectAsBase64)) { + return Base64Helper.deserializeObject(objectAsBase64); + } + + return null; + } + + public static boolean isTrustedClusterRequest(final ThreadContext context) { + return context.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTED_CLUSTER_REQUEST) == Boolean.TRUE; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/MapUtils.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/MapUtils.java new file mode 100644 index 0000000000..ae6ecc2c38 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/MapUtils.java @@ -0,0 +1,69 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class MapUtils { + + public static void deepTraverseMap(final Map map, final Callback cb) { + deepTraverseMap(map, cb, null); + } + + private static void deepTraverseMap(final Map map, final Callback cb, final List stack) { + final List localStack; + if(stack == null) { + localStack = new ArrayList(30); + } else { + localStack = stack; + } + for(Map.Entry entry: map.entrySet()) { + if(entry.getValue() != null && entry.getValue() instanceof Map) { + @SuppressWarnings("unchecked") + final Map inner = (Map) entry.getValue(); + localStack.add(entry.getKey()); + deepTraverseMap(inner, cb, localStack); + if(!localStack.isEmpty()) { + localStack.remove(localStack.size()-1); + } + } else { + cb.call(entry.getKey(), map, Collections.unmodifiableList(localStack)); + } + } + } + + public static interface Callback { + public void call(String key, Map map, List stack); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleInfo.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleInfo.java new file mode 100644 index 0000000000..432a83140d --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleInfo.java @@ -0,0 +1,188 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; + +public class ModuleInfo implements Serializable, Writeable{ + + private static final long serialVersionUID = -1077651823194285138L; + + private ModuleType moduleType; + private String classname; + private String classpath = ""; + private String version = ""; + private String buildTime = ""; + private String gitsha1 = ""; + + public ModuleInfo(ModuleType moduleType, String classname) { + assert(moduleType != null); + this.moduleType = moduleType; + this.classname = classname; + } + + public ModuleInfo(final StreamInput in) throws IOException { + moduleType = in.readEnum(ModuleType.class); + classname = in.readString(); + classpath = in.readString(); + version = in.readString(); + buildTime = in.readString(); + gitsha1 = in.readString(); + assert(moduleType != null); + } + + public void setClasspath(String classpath) { + this.classpath = classpath; + } + + public void setVersion(String version) { + this.version = version; + } + + public void setBuildTime(String buildTime) { + this.buildTime = buildTime; + } + + public String getGitsha1() { + return gitsha1; + } + + public void setGitsha1(String gitsha1) { + this.gitsha1 = gitsha1; + } + + public ModuleType getModuleType() { + return moduleType; + } + + public Map getAsMap() { + Map infoMap = new HashMap<>(); + infoMap.put("type", moduleType.name()); + infoMap.put("description", moduleType.getDescription()); + infoMap.put("is_enterprise", moduleType.isEnterprise().toString()); + infoMap.put("default_implementation", moduleType.getDefaultImplClass()); + infoMap.put("actual_implementation", this.classname); + //infoMap.put("classpath", this.classpath); //this can disclose file locations + infoMap.put("version", this.version); + infoMap.put("buildTime", this.buildTime); + infoMap.put("gitsha1", this.gitsha1); + return infoMap; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeEnum(moduleType); + out.writeString(classname); + out.writeString(classpath); + out.writeString(version); + out.writeString(buildTime); + out.writeString(gitsha1); + } + + + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((buildTime == null) ? 0 : buildTime.hashCode()); + result = prime * result + ((classname == null) ? 0 : classname.hashCode()); + result = prime * result + ((moduleType == null) ? 0 : moduleType.hashCode()); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + result = prime * result + ((gitsha1 == null) ? 0 : gitsha1.hashCode()); + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ModuleInfo)) { + return false; + } + ModuleInfo other = (ModuleInfo) obj; + if (buildTime == null) { + if (other.buildTime != null) { + return false; + } + } else if (!buildTime.equals(other.buildTime)) { + return false; + } + if (classname == null) { + if (other.classname != null) { + return false; + } + } else if (!classname.equals(other.classname)) { + return false; + } + if (!moduleType.equals(other.moduleType)) { + return false; + } + if (version == null) { + if (other.version != null) { + return false; + } + } else if (!version.equals(other.version)) { + return false; + } + if (gitsha1 == null) { + if (other.gitsha1 != null) { + return false; + } + } else if (!gitsha1.equals(other.gitsha1)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Module [type=" + this.moduleType.name() + ", implementing class=" + this.classname + "]"; + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleType.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleType.java new file mode 100644 index 0000000000..a30007098a --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/ModuleType.java @@ -0,0 +1,140 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import com.amazon.opendistroforelasticsearch.security.auth.AuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.AuthorizationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.HTTPAuthenticator; +import com.amazon.opendistroforelasticsearch.security.auth.internal.InternalAuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.internal.NoOpAuthenticationBackend; +import com.amazon.opendistroforelasticsearch.security.auth.internal.NoOpAuthorizationBackend; +import com.amazon.opendistroforelasticsearch.security.http.HTTPBasicAuthenticator; +import com.amazon.opendistroforelasticsearch.security.http.HTTPClientCertAuthenticator; +import com.amazon.opendistroforelasticsearch.security.http.HTTPProxyAuthenticator; +import com.amazon.opendistroforelasticsearch.security.ssl.transport.PrincipalExtractor; +import com.amazon.opendistroforelasticsearch.security.transport.InterClusterRequestEvaluator; + +public enum ModuleType implements Serializable { + + REST_MANAGEMENT_API("REST Management API", "com.amazon.opendistroforelasticsearch.security.dlic.rest.api.OpenDistroSecurityRestApiActions", Boolean.TRUE), + DLSFLS("Document- and Field-Level Security", "com.amazon.opendistroforelasticsearch.security.configuration.OpenDistroSecurityFlsDlsIndexSearcherWrapper", Boolean.TRUE), + AUDITLOG("Audit Logging", "com.amazon.opendistroforelasticsearch.security.auditlog.impl.AuditLogImpl", Boolean.TRUE), + MULTITENANCY("Kibana Multitenancy", "com.amazon.opendistroforelasticsearch.security.configuration.PrivilegesInterceptorImpl", Boolean.TRUE), + LDAP_AUTHENTICATION_BACKEND("LDAP authentication backend", "com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend", Boolean.TRUE), + LDAP_AUTHORIZATION_BACKEND("LDAP authorization backend", "com.amazon.dlic.auth.ldap.backend.LDAPAuthorizationBackend", Boolean.TRUE), + KERBEROS_AUTHENTICATION_BACKEND("Kerberos authentication backend", "com.amazon.dlic.auth.http.kerberos.HTTPSpnegoAuthenticator", Boolean.TRUE), + JWT_AUTHENTICATION_BACKEND("JWT authentication backend", "com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator", Boolean.TRUE), + OPENID_AUTHENTICATION_BACKEND("OpenID authentication backend", "com.amazon.dlic.auth.http.jwt.keybyoidc.HTTPJwtKeyByOpenIdConnectAuthenticator", Boolean.TRUE), + SAML_AUTHENTICATION_BACKEND("SAML authentication backend", "com.amazon.dlic.auth.http.saml.HTTPSamlAuthenticator", Boolean.TRUE), + INTERNAL_USERS_AUTHENTICATION_BACKEND("Internal users authentication backend", InternalAuthenticationBackend.class.getName(), Boolean.FALSE), + NOOP_AUTHENTICATION_BACKEND("Noop authentication backend", NoOpAuthenticationBackend.class.getName(), Boolean.FALSE), + NOOP_AUTHORIZATION_BACKEND("Noop authorization backend", NoOpAuthorizationBackend.class.getName(), Boolean.FALSE), + HTTP_BASIC_AUTHENTICATOR("HTTP Basic Authenticator", HTTPBasicAuthenticator.class.getName(), Boolean.FALSE), + HTTP_PROXY_AUTHENTICATOR("HTTP Proxy Authenticator", HTTPProxyAuthenticator.class.getName(), Boolean.FALSE), + HTTP_CLIENTCERT_AUTHENTICATOR("HTTP Client Certificate Authenticator", HTTPClientCertAuthenticator.class.getName(), Boolean.FALSE), + CUSTOM_HTTP_AUTHENTICATOR("Custom HTTP authenticator", null, Boolean.TRUE), + CUSTOM_AUTHENTICATION_BACKEND("Custom authentication backend", null, Boolean.TRUE), + CUSTOM_AUTHORIZATION_BACKEND("Custom authorization backend", null, Boolean.TRUE), + CUSTOM_INTERCLUSTER_REQUEST_EVALUATOR("Intercluster Request Evaluator", null, Boolean.FALSE), + CUSTOM_PRINCIPAL_EXTRACTOR("TLS Principal Extractor", null, Boolean.FALSE), + COMPLIANCE("Compliance", "com.amazon.opendistroforelasticsearch.security.compliance.ComplianceIndexingOperationListenerImpl", Boolean.TRUE), + UNKNOWN("Unknown type", null, Boolean.TRUE); + + private String description; + private String defaultImplClass; + private Boolean isEnterprise = Boolean.TRUE; + private static Map modulesMap = new HashMap<>(); + + static{ + for(ModuleType module : ModuleType.values()) { + if (module.defaultImplClass != null) { + modulesMap.put(module.getDefaultImplClass(), module); + } + } + } + + private ModuleType(String description, String defaultImplClass, Boolean isEnterprise) { + this.description = description; + this.defaultImplClass = defaultImplClass; + this.isEnterprise = isEnterprise; + } + + public static ModuleType getByDefaultImplClass(Class clazz) { + ModuleType moduleType = modulesMap.get(clazz.getName()); + if(moduleType == null) { + if(HTTPAuthenticator.class.isAssignableFrom(clazz)) { + moduleType = ModuleType.CUSTOM_HTTP_AUTHENTICATOR; + } + + if(AuthenticationBackend.class.isAssignableFrom(clazz)) { + moduleType = ModuleType.CUSTOM_AUTHENTICATION_BACKEND; + } + + if(AuthorizationBackend.class.isAssignableFrom(clazz)) { + moduleType = ModuleType.CUSTOM_AUTHORIZATION_BACKEND; + } + + if(AuthorizationBackend.class.isAssignableFrom(clazz)) { + moduleType = ModuleType.CUSTOM_AUTHORIZATION_BACKEND; + } + + if(InterClusterRequestEvaluator.class.isAssignableFrom(clazz)) { + moduleType = ModuleType.CUSTOM_INTERCLUSTER_REQUEST_EVALUATOR; + } + + if(PrincipalExtractor.class.isAssignableFrom(clazz)) { + moduleType = ModuleType.CUSTOM_PRINCIPAL_EXTRACTOR; + } + } + if(moduleType == null) { + moduleType = ModuleType.UNKNOWN; + } + return moduleType; + } + + public String getDescription() { + return this.description; + } + + public String getDefaultImplClass() { + return defaultImplClass; + } + + public Boolean isEnterprise() { + return isEnterprise; + } + + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/OpenDistroSecurityDeprecationHandler.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/OpenDistroSecurityDeprecationHandler.java new file mode 100644 index 0000000000..e1457abdef --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/OpenDistroSecurityDeprecationHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import org.elasticsearch.common.xcontent.DeprecationHandler; + +public class OpenDistroSecurityDeprecationHandler { + + public final static DeprecationHandler INSTANCE = new DeprecationHandler() { + @Override + public void usedDeprecatedField(String usedName, String replacedWith) { + throw new UnsupportedOperationException("deprecated fields not supported here but got [" + + usedName + "] which is a deprecated name for [" + replacedWith + "]"); + } + @Override + public void usedDeprecatedName(String usedName, String modernName) { + throw new UnsupportedOperationException("deprecated fields not supported here but got [" + + usedName + "] which has been replaced with [" + modernName + "]"); + } + }; + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/OpenDistroSecurityUtils.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/OpenDistroSecurityUtils.java new file mode 100644 index 0000000000..eb0a9a7dc6 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/OpenDistroSecurityUtils.java @@ -0,0 +1,91 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class OpenDistroSecurityUtils { + + protected final static Logger log = LogManager.getLogger(OpenDistroSecurityUtils.class); + + private OpenDistroSecurityUtils() { + } + + public static String evalMap(final Map> map, final String index) { + + if (map == null) { + return null; + } + + if (map.get(index) != null) { + return index; + } else if (map.get("*") != null) { + return "*"; + } + if (map.get("_all") != null) { + return "_all"; + } + + //regex + for(final String key: map.keySet()) { + if(WildcardMatcher.containsWildcard(key) + && WildcardMatcher.match(key, index)) { + return key; + } + } + + return null; + } + + @SafeVarargs + public static Map mapFromArray(T ... keyValues) { + if(keyValues == null) { + return Collections.emptyMap(); + } + if (keyValues.length % 2 != 0) { + log.error("Expected even number of key/value pairs, got {}.", Arrays.toString(keyValues)); + return null; + } + Map map = new HashMap<>(); + + for(int i = 0; i certs = fact.generateCertificates(is); + X509Certificate[] x509Certs = new X509Certificate[certs.size()]; + int i=0; + for(Certificate cert: certs) { + x509Certs[i++] = (X509Certificate) cert; + } + return x509Certs; + } + + } + + public static X509Certificate[] loadCertificatesFromStream(InputStream in) throws Exception { + if(in == null) { + return null; + } + + CertificateFactory fact = CertificateFactory.getInstance("X.509"); + Collection certs = fact.generateCertificates(in); + X509Certificate[] x509Certs = new X509Certificate[certs.size()]; + int i=0; + for(Certificate cert: certs) { + x509Certs[i++] = (X509Certificate) cert; + } + return x509Certs; + + } + + + public static InputStream resolveStream(String propName, Settings settings) { + final String content = settings.get(propName, null); + + if(content == null) { + return null; + } + + return new ByteArrayInputStream(content.getBytes(StandardCharsets.US_ASCII)); + } + + public static String resolve(String propName, Settings settings, Path configPath, boolean mustBeValid) { + final String originalPath = settings.get(propName, null); + return resolve(originalPath, propName, settings, configPath, mustBeValid); + } + + public static String resolve(String originalPath, String propName, Settings settings, Path configPath, boolean mustBeValid) { + log.debug("Path is is {}", originalPath); + String path = originalPath; + final Environment env = new Environment(settings, configPath); + + if(env != null && originalPath != null && originalPath.length() > 0) { + path = env.configFile().resolve(originalPath).toAbsolutePath().toString(); + log.debug("Resolved {} to {} against {}", originalPath, path, env.configFile().toAbsolutePath().toString()); + } + + if(mustBeValid) { + checkPath(path, propName); + } + + if("".equals(path)) { + path = null; + } + + return path; + } + + public static KeyStore toTruststore(final String trustCertificatesAliasPrefix, final X509Certificate[] trustCertificates) throws Exception { + + if(trustCertificates == null) { + return null; + } + + KeyStore ks = KeyStore.getInstance(JKS); + ks.load(null); + + if(trustCertificates != null && trustCertificates.length > 0) { + for (int i = 0; i < trustCertificates.length; i++) { + X509Certificate x509Certificate = trustCertificates[i]; + ks.setCertificateEntry(trustCertificatesAliasPrefix+"_"+i, x509Certificate); + } + } + return ks; + } + + public static KeyStore toKeystore(final String authenticationCertificateAlias, final char[] password, final X509Certificate authenticationCertificate[], final PrivateKey authenticationKey) throws Exception { + + if(authenticationCertificateAlias != null && authenticationCertificate != null && authenticationKey != null) { + KeyStore ks = KeyStore.getInstance(JKS); + ks.load(null, null); + ks.setKeyEntry(authenticationCertificateAlias, authenticationKey, password, authenticationCertificate); + return ks; + } else { + return null; + } + + } + + public static char[] randomChars(int len) { + final SecureRandom r = new SecureRandom(); + final char[] ret = new char[len]; + for(int i=0; i modulesLoaded = new HashSet<>(); + + public static Set getModulesLoaded() { + return Collections.unmodifiableSet(modulesLoaded); + } + + private static boolean enterpriseModulesDisabled() { + return !enterpriseModulesEnabled; + } + + public static void registerMngtRestApiHandler(final Settings settings) { + + if (enterpriseModulesDisabled()) { + return; + } + + if(!settings.getAsBoolean("http.enabled", true)) { + + try { + final Class clazz = Class.forName("com.amazon.opendistroforelasticsearch.security.dlic.rest.api.OpenDistroSecurityRestApiActions"); + addLoadedModule(clazz); + } catch (final Throwable e) { + log.warn("Unable to register Rest Management Api Module due to {}", e.toString()); + if(log.isDebugEnabled()) { + log.debug("Stacktrace: ",e); + } + } + } + } + + @SuppressWarnings("unchecked") + public static Collection instantiateMngtRestApiHandler(final Settings settings, final Path configPath, final RestController restController, + final Client localClient, final AdminDNs adminDns, final IndexBaseConfigurationRepository cr, final ClusterService cs, final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, final ThreadPool threadPool, final AuditLog auditlog) { + + if (enterpriseModulesDisabled()) { + return Collections.emptyList(); + } + + try { + final Class clazz = Class.forName("com.amazon.opendistroforelasticsearch.security.dlic.rest.api.OpenDistroSecurityRestApiActions"); + final Collection ret = (Collection) clazz + .getDeclaredMethod("getHandler", Settings.class, Path.class, RestController.class, Client.class, AdminDNs.class, IndexBaseConfigurationRepository.class, + ClusterService.class, PrincipalExtractor.class, PrivilegesEvaluator.class, ThreadPool.class, AuditLog.class) + .invoke(null, settings, configPath, restController, localClient, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditlog); + addLoadedModule(clazz); + return ret; + } catch (final Throwable e) { + log.warn("Unable to enable Rest Management Api Module due to {}", e.toString()); + if(log.isDebugEnabled()) { + log.debug("Stacktrace: ",e); + } + return Collections.emptyList(); + } + } + + @SuppressWarnings("rawtypes") + public static Constructor instantiateDlsFlsConstructor() { + + if (enterpriseModulesDisabled()) { + return null; + } + + try { + final Class clazz = Class.forName("com.amazon.opendistroforelasticsearch.security.configuration.OpenDistroSecurityFlsDlsIndexSearcherWrapper"); + final Constructor ret = clazz.getConstructor(IndexService.class, + Settings.class, AdminDNs.class, ClusterService.class, AuditLog.class, + ComplianceIndexingOperationListener.class, ComplianceConfig.class); + addLoadedModule(clazz); + return ret; + } catch (final Throwable e) { + log.warn("Unable to enable DLS/FLS Module due to {}", e.toString()); + if(log.isDebugEnabled()) { + log.debug("Stacktrace: ",e); + } + return null; + } + } + + public static DlsFlsRequestValve instantiateDlsFlsValve() { + + if (enterpriseModulesDisabled()) { + return new DlsFlsRequestValve.NoopDlsFlsRequestValve(); + } + + try { + final Class clazz = Class.forName("com.amazon.opendistroforelasticsearch.security.configuration.DlsFlsValveImpl"); + final DlsFlsRequestValve ret = (DlsFlsRequestValve) clazz.newInstance(); + return ret; + } catch (final Throwable e) { + log.warn("Unable to enable DLS/FLS Valve Module due to {}", e.toString()); + if(log.isDebugEnabled()) { + log.debug("Stacktrace: ",e); + } + return new DlsFlsRequestValve.NoopDlsFlsRequestValve(); + } + } + + public static AuditLog instantiateAuditLog(final Settings settings, final Path configPath, final Client localClient, final ThreadPool threadPool, + final IndexNameExpressionResolver resolver, final ClusterService clusterService) { + + if (enterpriseModulesDisabled()) { + return new NullAuditLog(); + } + + try { + final Class clazz = Class.forName("com.amazon.opendistroforelasticsearch.security.auditlog.impl.AuditLogImpl"); + final AuditLog impl = (AuditLog) clazz + .getConstructor(Settings.class, Path.class, Client.class, ThreadPool.class, IndexNameExpressionResolver.class, ClusterService.class) + .newInstance(settings, configPath, localClient, threadPool, resolver, clusterService); + addLoadedModule(clazz); + return impl; + } catch (final Throwable e) { + log.warn("Unable to enable Auditlog Module due to {}", e.toString()); + if(log.isDebugEnabled()) { + log.debug("Stacktrace: ",e); + } + return new NullAuditLog(); + } + } + + public static ComplianceIndexingOperationListener instantiateComplianceListener(ComplianceConfig complianceConfig, AuditLog auditlog) { + + if (enterpriseModulesDisabled()) { + return new ComplianceIndexingOperationListener(); + } + + try { + final Class clazz = Class.forName("com.amazon.opendistroforelasticsearch.security.compliance.ComplianceIndexingOperationListenerImpl"); + final ComplianceIndexingOperationListener impl = (ComplianceIndexingOperationListener) clazz + .getConstructor(ComplianceConfig.class, AuditLog.class) + .newInstance(complianceConfig, auditlog); + addLoadedModule(clazz); + return impl; + } catch (final ClassNotFoundException e) { + //TODO produce a single warn msg, this here is issued for every index + log.debug("Unable to enable Compliance Module due to {}", e.toString()); + if(log.isDebugEnabled()) { + log.debug("Stacktrace: ",e); + } + return new ComplianceIndexingOperationListener(); + } catch (final Throwable e) { + log.error("Unable to enable Compliance Module due to {}", e.toString()); + if(log.isDebugEnabled()) { + log.debug("Stacktrace: ",e); + } + return new ComplianceIndexingOperationListener(); + } + } + + public static PrivilegesInterceptor instantiatePrivilegesInterceptorImpl(final IndexNameExpressionResolver resolver, final ClusterService clusterService, + final Client localClient, final ThreadPool threadPool) { + + final PrivilegesInterceptor noop = new PrivilegesInterceptor(resolver, clusterService, localClient, threadPool); + + if (enterpriseModulesDisabled()) { + return noop; + } + + try { + final Class clazz = Class.forName("com.amazon.opendistroforelasticsearch.security.configuration.PrivilegesInterceptorImpl"); + final PrivilegesInterceptor ret = (PrivilegesInterceptor) clazz.getConstructor(IndexNameExpressionResolver.class, ClusterService.class, Client.class, ThreadPool.class) + .newInstance(resolver, clusterService, localClient, threadPool); + addLoadedModule(clazz); + return ret; + } catch (final Throwable e) { + log.warn("Unable to enable Kibana Module due to {}", e.toString()); + if(log.isDebugEnabled()) { + log.debug("Stacktrace: ",e); + } + return noop; + } + } + + @SuppressWarnings("unchecked") + public static T instantiateAAA(final String clazz, final Settings settings, final Path configPath, final boolean checkEnterprise) { + + if (checkEnterprise && enterpriseModulesDisabled()) { + throw new ElasticsearchException("Can not load '{}' because enterprise modules are disabled", clazz); + } + + try { + final Class clazz0 = Class.forName(clazz); + final T ret = (T) clazz0.getConstructor(Settings.class, Path.class).newInstance(settings, configPath); + + addLoadedModule(clazz0); + + return ret; + + } catch (final Throwable e) { + log.warn("Unable to enable '{}' due to {}", clazz, e.toString()); + if(log.isDebugEnabled()) { + log.debug("Stacktrace: ",e); + } + throw new ElasticsearchException(e); + } + } + + public static InterClusterRequestEvaluator instantiateInterClusterRequestEvaluator(final String clazz, final Settings settings) { + + try { + final Class clazz0 = Class.forName(clazz); + final InterClusterRequestEvaluator ret = (InterClusterRequestEvaluator) clazz0.getConstructor(Settings.class).newInstance(settings); + addLoadedModule(clazz0); + return ret; + } catch (final Throwable e) { + log.warn("Unable to load inter cluster request evaluator '{}' due to {}", clazz, e.toString()); + if(log.isDebugEnabled()) { + log.debug("Stacktrace: ",e); + } + return new DefaultInterClusterRequestEvaluator(settings); + } + } + + public static PrincipalExtractor instantiatePrincipalExtractor(final String clazz) { + + try { + final Class clazz0 = Class.forName(clazz); + final PrincipalExtractor ret = (PrincipalExtractor) clazz0.newInstance(); + addLoadedModule(clazz0); + return ret; + } catch (final Throwable e) { + log.warn("Unable to load pricipal extractor '{}' due to {}", clazz, e.toString()); + if(log.isDebugEnabled()) { + log.debug("Stacktrace: ",e); + } + return new DefaultPrincipalExtractor(); + } + } + + public static boolean isEnterpriseAAAModule(final String clazz) { + boolean enterpriseModuleInstalled = false; + + if (clazz.equalsIgnoreCase("com.amazon.dlic.auth.ldap.backend.LDAPAuthorizationBackend")) { + enterpriseModuleInstalled = true; + } + + if (clazz.equalsIgnoreCase("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend")) { + enterpriseModuleInstalled = true; + } + + if (clazz.equalsIgnoreCase("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator")) { + enterpriseModuleInstalled = true; + } + + if (clazz.equalsIgnoreCase("com.amazon.dlic.auth.http.jwt.keybyoidc.HTTPJwtKeyByOpenIdConnectAuthenticator")) { + enterpriseModuleInstalled = true; + } + + if (clazz.equalsIgnoreCase("com.amazon.dlic.auth.http.kerberos.HTTPSpnegoAuthenticator")) { + enterpriseModuleInstalled = true; + } + + if (clazz.equalsIgnoreCase("com.amazon.dlic.auth.http.saml.HTTPSamlAuthenticator")) { + enterpriseModuleInstalled = true; + } + + return enterpriseModuleInstalled; + } + + public static boolean addLoadedModule(Class clazz) { + ModuleInfo moduleInfo = getModuleInfo(clazz); + if (log.isDebugEnabled()) { + log.debug("Loaded module {}", moduleInfo); + } + return modulesLoaded.add(moduleInfo); + } + + private static boolean enterpriseModulesEnabled; + + // TODO static hack + public static void init(final boolean enterpriseModulesEnabled) { + ReflectionHelper.enterpriseModulesEnabled = enterpriseModulesEnabled; + } + + private static ModuleInfo getModuleInfo(final Class impl) { + + ModuleType moduleType = ModuleType.getByDefaultImplClass(impl); + ModuleInfo moduleInfo = new ModuleInfo(moduleType, impl.getName()); + + try { + + final String classPath = impl.getResource(impl.getSimpleName() + ".class").toString(); + moduleInfo.setClasspath(classPath); + + if (!classPath.startsWith("jar")) { + return moduleInfo; + } + + final String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) + "/META-INF/MANIFEST.MF"; + + try (InputStream stream = new URL(manifestPath).openStream()) { + final Manifest manifest = new Manifest(stream); + final Attributes attr = manifest.getMainAttributes(); + moduleInfo.setVersion(attr.getValue("Implementation-Version")); + moduleInfo.setBuildTime(attr.getValue("Build-Time")); + moduleInfo.setGitsha1(attr.getValue("git-sha1")); + } + } catch (final Throwable e) { + log.error("Unable to retrieve module info for " + impl, e); + } + + return moduleInfo; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/SnapshotRestoreHelper.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/SnapshotRestoreHelper.java new file mode 100644 index 0000000000..a11f6578d3 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/SnapshotRestoreHelper.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.List; +import java.util.Objects; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.SpecialPermission; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.elasticsearch.repositories.RepositoriesService; +import org.elasticsearch.repositories.Repository; +import org.elasticsearch.snapshots.SnapshotId; +import org.elasticsearch.snapshots.SnapshotInfo; +import org.elasticsearch.snapshots.SnapshotUtils; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.OpenDistroSecurityPlugin; + +public class SnapshotRestoreHelper { + + protected static final Logger log = LogManager.getLogger(SnapshotRestoreHelper.class); + + public static List resolveOriginalIndices(RestoreSnapshotRequest restoreRequest) { + final SnapshotInfo snapshotInfo = getSnapshotInfo(restoreRequest); + + if (snapshotInfo == null) { + log.warn("snapshot repository '" + restoreRequest.repository() + "', snapshot '" + restoreRequest.snapshot() + "' not found"); + return null; + } else { + return SnapshotUtils.filterIndices(snapshotInfo.indices(), restoreRequest.indices(), restoreRequest.indicesOptions()); + } + + + } + + public static SnapshotInfo getSnapshotInfo(RestoreSnapshotRequest restoreRequest) { + final RepositoriesService repositoriesService = Objects.requireNonNull(OpenDistroSecurityPlugin.GuiceHolder.getRepositoriesService(), "RepositoriesService not initialized"); + final Repository repository = repositoriesService.repository(restoreRequest.repository()); + final String threadName = Thread.currentThread().getName(); + SnapshotInfo snapshotInfo = null; + + try { + setCurrentThreadName(ThreadPool.Names.GENERIC); + for (final SnapshotId snapshotId : repository.getRepositoryData().getSnapshotIds()) { + if (snapshotId.getName().equals(restoreRequest.snapshot())) { + + if(log.isDebugEnabled()) { + log.debug("snapshot found: {} (UUID: {})", snapshotId.getName(), snapshotId.getUUID()); + } + + snapshotInfo = repository.getSnapshotInfo(snapshotId); + break; + } + } + } finally { + setCurrentThreadName(threadName); + } + return snapshotInfo; + } + + private static void setCurrentThreadName(final String name) { + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + Thread.currentThread().setName(name); + return null; + } + }); + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/SourceFieldsContext.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/SourceFieldsContext.java new file mode 100644 index 0000000000..13e6c0258f --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/SourceFieldsContext.java @@ -0,0 +1,114 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.io.Serializable; +import java.util.Arrays; + +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.search.SearchRequest; + +public class SourceFieldsContext implements Serializable { + + private String[] includes; + private String[] excludes; + //private String[] storedFields; + private boolean fetchSource = true; + + /** + * + */ + private static final long serialVersionUID = 1L; + + public static boolean isNeeded(SearchRequest request) { + return (request.source() != null && request.source().fetchSource() != null && (request.source().fetchSource().includes() != null || request + .source().fetchSource().excludes() != null)) + || (request.source() != null && request.source().storedFields() != null + && request.source().storedFields().fieldNames() != null && !request.source().storedFields().fieldNames().isEmpty()); + } + + public static boolean isNeeded(GetRequest request) { + return (request.fetchSourceContext() != null && (request.fetchSourceContext().includes() != null || request.fetchSourceContext() + .excludes() != null)) || (request.storedFields() != null && request.storedFields().length > 0); + } + + public SourceFieldsContext() { + super(); + } + + public SourceFieldsContext(SearchRequest request) { + if (request.source() != null && request.source().fetchSource() != null) { + includes = request.source().fetchSource().includes(); + excludes = request.source().fetchSource().excludes(); + fetchSource = request.source().fetchSource().fetchSource(); + } + + //if (request.source() != null && request.source().storedFields() != null && request.source().storedFields().fieldNames() != null) { + // storedFields = request.source().storedFields().fieldNames().toArray(new String[0]); + //} + } + + public SourceFieldsContext(GetRequest request) { + if (request.fetchSourceContext() != null) { + includes = request.fetchSourceContext().includes(); + excludes = request.fetchSourceContext().excludes(); + fetchSource = request.fetchSourceContext().fetchSource(); + } + + //storedFields = request.storedFields(); + } + + public String[] getIncludes() { + return includes; + } + + public String[] getExcludes() { + return excludes; + } + + //public String[] getStoredFields() { + // return storedFields; + //} + + public boolean hasIncludesOrExcludes() { + return (includes != null && includes.length > 0) || (excludes != null && excludes.length > 0); + } + + public boolean isFetchSource() { + return fetchSource; + } + + @Override + public String toString() { + return "SourceFieldsContext [includes=" + Arrays.toString(includes) + ", excludes=" + Arrays.toString(excludes) + ", fetchSource=" + + fetchSource + "]"; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/support/WildcardMatcher.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/WildcardMatcher.java new file mode 100644 index 0000000000..5135be7e0f --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/support/WildcardMatcher.java @@ -0,0 +1,588 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.Stack; +import java.util.regex.Pattern; + +public class WildcardMatcher { + + private static final int NOT_FOUND = -1; + + /** + * returns true if at least one candidate match at least one pattern (case sensitive) + * @param pattern + * @param candidate + * @return + */ + public static boolean matchAny(final String[] pattern, final String[] candidate) { + + return matchAny(pattern, candidate, false); + } + + public static boolean matchAny(final Collection pattern, final Collection candidate) { + + return matchAny(pattern, candidate, false); + } + + /** + * returns true if at least one candidate match at least one pattern + * + * @param pattern + * @param candidate + * @param ignoreCase + * @return + */ + public static boolean matchAny(final String[] pattern, final String[] candidate, boolean ignoreCase) { + + for (int i = 0; i < pattern.length; i++) { + final String string = pattern[i]; + if (matchAny(string, candidate, ignoreCase)) { + return true; + } + } + + return false; + } + + /** + * returns true if at least one candidate match at least one pattern + * + * @param pattern + * @param candidate + * @param ignoreCase + * @return + */ + public static boolean matchAny(final Collection pattern, final String[] candidate, boolean ignoreCase) { + + for (String string: pattern) { + if (matchAny(string, candidate, ignoreCase)) { + return true; + } + } + + return false; + } + + public static boolean matchAny(final Collection pattern, final Collection candidate, boolean ignoreCase) { + + for (String string: pattern) { + if (matchAny(string, candidate, ignoreCase)) { + return true; + } + } + + return false; + } + + /** + * return true if all candidates find a matching pattern + * + * @param pattern + * @param candidate + * @return + */ + public static boolean matchAll(final String[] pattern, final String[] candidate) { + + + for (int i = 0; i < candidate.length; i++) { + final String string = candidate[i]; + if (!matchAny(pattern, string)) { + return false; + } + } + + return true; + } + + /** + * + * @param pattern + * @param candidate + * @return + */ + public static boolean allPatternsMatched(final String[] pattern, final String[] candidate) { + + int matchedPatternNum = 0; + + for (int i = 0; i < pattern.length; i++) { + final String string = pattern[i]; + if (matchAny(string, candidate)) { + matchedPatternNum++; + } + } + + return matchedPatternNum == pattern.length && pattern.length > 0; + } + + public static boolean matchAny(final String pattern, final String[] candidate) { + return matchAny(pattern, candidate, false); + } + + public static boolean matchAny(final String pattern, final Collection candidate) { + return matchAny(pattern, candidate, false); + } + + /** + * return true if at least one candidate matches the given pattern + * + * @param pattern + * @param candidate + * @param ignoreCase + * @return + */ + public static boolean matchAny(final String pattern, final String[] candidate, boolean ignoreCase) { + + for (int i = 0; i < candidate.length; i++) { + final String string = candidate[i]; + if (match(pattern, string, ignoreCase)) { + return true; + } + } + + return false; + } + + public static boolean matchAny(final String pattern, final Collection candidates, boolean ignoreCase) { + + for (String candidate: candidates) { + if (match(pattern, candidate, ignoreCase)) { + return true; + } + } + + return false; + } + + public static String[] matches(final String pattern, final String[] candidate, boolean ignoreCase) { + + final List ret = new ArrayList(candidate.length); + for (int i = 0; i < candidate.length; i++) { + final String string = candidate[i]; + if (match(pattern, string, ignoreCase)) { + ret.add(string); + } + } + + return ret.toArray(new String[0]); + } + + public static List getMatchAny(final String pattern, final String[] candidate) { + + final List matches = new ArrayList(candidate.length); + + for (int i = 0; i < candidate.length; i++) { + final String string = candidate[i]; + if (match(pattern, string)) { + matches.add(string); + } + } + + return matches; + } + + public static List getMatchAny(final String[] patterns, final String[] candidate) { + + final List matches = new ArrayList(candidate.length); + + for (int i = 0; i < candidate.length; i++) { + final String string = candidate[i]; + if (matchAny(patterns, string)) { + matches.add(string); + } + } + + return matches; + } + + public static List getMatchAny(final String pattern, final Collection candidate) { + + final List matches = new ArrayList(candidate.size()); + + for (final String string: candidate) { + if (match(pattern, string)) { + matches.add(string); + } + } + + return matches; + } + + public static List getMatchAny(final String[] patterns, final Collection candidate) { + + final List matches = new ArrayList(candidate.size()); + + for (final String string: candidate) { + if (matchAny(patterns, string)) { + matches.add(string); + } + } + + return matches; + } + + public static Optional getFirstMatchingPattern(final Collection pattern, final String candidate) { + + for (String p : pattern) { + if (match(p, candidate)) { + return Optional.of(p); + } + } + + return Optional.empty(); + } + + /** + * returns true if the candidate matches at least one pattern + * + * @param pattern + * @param candidate + * @return + */ + public static boolean matchAny(final String pattern[], final String candidate) { + + for (int i = 0; i < pattern.length; i++) { + final String string = pattern[i]; + if (match(string, candidate)) { + return true; + } + } + + return false; + } + + /** + * returns true if the candidate matches at least one pattern + * + * @param pattern + * @param candidate + * @return + */ + public static boolean matchAny(final Collection pattern, final String candidate) { + + for (String string: pattern) { + if (match(string, candidate)) { + return true; + } + } + + return false; + } + + public static boolean match(final String pattern, final String candidate) { + return match(pattern, candidate, false); + } + + public static boolean match(String pattern, String candidate, boolean ignoreCase) { + + if (pattern == null || candidate == null) { + return false; + } + + if(ignoreCase) { + pattern = pattern.toLowerCase(); + candidate = candidate.toLowerCase(); + } + + if (pattern.startsWith("/") && pattern.endsWith("/")) { + // regex + return Pattern.matches("^"+pattern.substring(1, pattern.length() - 1)+"$", candidate); + } else if (pattern.length() == 1 && pattern.charAt(0) == '*') { + return true; + } else if (pattern.indexOf('?') == NOT_FOUND && pattern.indexOf('*') == NOT_FOUND) { + return pattern.equals(candidate); + } else { + return simpleWildcardMatch(pattern, candidate); + } + } + + public static boolean containsWildcard(final String pattern) { + if (pattern != null + && (pattern.indexOf("*") > NOT_FOUND || pattern.indexOf("?") > NOT_FOUND || (pattern.startsWith("/") && pattern + .endsWith("/")))) { + return true; + } + + return false; + } + + /** + * + * @param set will be modified + * @param stringContainingWc + * @return + */ + public static boolean wildcardRemoveFromSet(Set set, String stringContainingWc) { + if(set == null || set.isEmpty()) { + return false; + } + if(!containsWildcard(stringContainingWc) && set.contains(stringContainingWc)) { + return set.remove(stringContainingWc); + } else { + boolean modified = false; + Set copy = new HashSet<>(set); + + for(String it: copy) { + if(WildcardMatcher.match(stringContainingWc, it)) { + modified = set.remove(it) || modified; + } + } + return modified; + } + } + + /** + * + * @param set will be modified + * @param stringContainingWc + * @return + */ + public static boolean wildcardRetainInSet(Set set, String[] setContainingWc) { + if(set == null || set.isEmpty()) { + return false; + } + boolean modified = false; + Set copy = new HashSet<>(set); + + for(String it: copy) { + if(!WildcardMatcher.matchAny(setContainingWc, it)) { + modified = set.remove(it) || modified; + } + } + return modified; + } + + + //All code below is copied (and slightly modified) from Apache Commons IO + + /* + * 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. + */ + + + /** + * Checks a filename to see if it matches the specified wildcard matcher + * allowing control over case-sensitivity. + *

+ * The wildcard matcher uses the characters '?' and '*' to represent a + * single or multiple (zero or more) wildcard characters. + * N.B. the sequence "*?" does not work properly at present in match strings. + * + * @param candidate the filename to match on + * @param pattern the wildcard string to match against + * @return true if the filename matches the wilcard string + * @since 1.3 + */ + private static boolean simpleWildcardMatch(final String pattern, final String candidate) { + if (candidate == null && pattern == null) { + return true; + } + if (candidate == null || pattern == null) { + return false; + } + + final String[] wcs = splitOnTokens(pattern); + boolean anyChars = false; + int textIdx = 0; + int wcsIdx = 0; + final Stack backtrack = new Stack<>(); + + // loop around a backtrack stack, to handle complex * matching + do { + if (backtrack.size() > 0) { + final int[] array = backtrack.pop(); + wcsIdx = array[0]; + textIdx = array[1]; + anyChars = true; + } + + // loop whilst tokens and text left to process + while (wcsIdx < wcs.length) { + + if (wcs[wcsIdx].equals("?")) { + // ? so move to next text char + textIdx++; + if (textIdx > candidate.length()) { + break; + } + anyChars = false; + + } else if (wcs[wcsIdx].equals("*")) { + // set any chars status + anyChars = true; + if (wcsIdx == wcs.length - 1) { + textIdx = candidate.length(); + } + + } else { + // matching text token + if (anyChars) { + // any chars then try to locate text token + textIdx = checkIndexOf(candidate, textIdx, wcs[wcsIdx]); + if (textIdx == NOT_FOUND) { + // token not found + break; + } + final int repeat = checkIndexOf(candidate, textIdx + 1, wcs[wcsIdx]); + if (repeat >= 0) { + backtrack.push(new int[] {wcsIdx, repeat}); + } + } else { + // matching from current position + if (!checkRegionMatches(candidate, textIdx, wcs[wcsIdx])) { + // couldnt match token + break; + } + } + + // matched text token, move text index to end of matched token + textIdx += wcs[wcsIdx].length(); + anyChars = false; + } + + wcsIdx++; + } + + // full match + if (wcsIdx == wcs.length && textIdx == candidate.length()) { + return true; + } + + } while (backtrack.size() > 0); + + return false; + } + + /** + * Splits a string into a number of tokens. + * The text is split by '?' and '*'. + * Where multiple '*' occur consecutively they are collapsed into a single '*'. + * + * @param text the text to split + * @return the array of tokens, never null + */ + private static String[] splitOnTokens(final String text) { + // used by wildcardMatch + // package level so a unit test may run on this + + if (text.indexOf('?') == NOT_FOUND && text.indexOf('*') == NOT_FOUND) { + return new String[] { text }; + } + + final char[] array = text.toCharArray(); + final ArrayList list = new ArrayList<>(); + final StringBuilder buffer = new StringBuilder(); + char prevChar = 0; + for (final char ch : array) { + if (ch == '?' || ch == '*') { + if (buffer.length() != 0) { + list.add(buffer.toString()); + buffer.setLength(0); + } + if (ch == '?') { + list.add("?"); + } else if (prevChar != '*') {// ch == '*' here; check if previous char was '*' + list.add("*"); + } + } else { + buffer.append(ch); + } + prevChar = ch; + } + if (buffer.length() != 0) { + list.add(buffer.toString()); + } + + return list.toArray( new String[ list.size() ] ); + } + + /** + * Checks if one string contains another starting at a specific index using the + * case-sensitivity rule. + *

+ * This method mimics parts of {@link String#indexOf(String, int)} + * but takes case-sensitivity into account. + * + * @param str the string to check, not null + * @param strStartIndex the index to start at in str + * @param search the start to search for, not null + * @return the first index of the search String, + * -1 if no match or {@code null} string input + * @throws NullPointerException if either string is null + * @since 2.0 + */ + private static int checkIndexOf(final String str, final int strStartIndex, final String search) { + final int endIndex = str.length() - search.length(); + if (endIndex >= strStartIndex) { + for (int i = strStartIndex; i <= endIndex; i++) { + if (checkRegionMatches(str, i, search)) { + return i; + } + } + } + return -1; + } + + /** + * Checks if one string contains another at a specific index using the case-sensitivity rule. + *

+ * This method mimics parts of {@link String#regionMatches(boolean, int, String, int, int)} + * but takes case-sensitivity into account. + * + * @param str the string to check, not null + * @param strStartIndex the index to start at in str + * @param search the start to search for, not null + * @return true if equal using the case rules + * @throws NullPointerException if either string is null + */ + private static boolean checkRegionMatches(final String str, final int strStartIndex, final String search) { + return str.regionMatches(false, strStartIndex, search, 0, search.length()); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/tools/Hasher.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/tools/Hasher.java new file mode 100644 index 0000000000..3cbdc31999 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/tools/Hasher.java @@ -0,0 +1,90 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.tools; + +import java.io.Console; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Objects; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; + +public class Hasher { + + public static void main(final String[] args) { + + final Options options = new Options(); + final HelpFormatter formatter = new HelpFormatter(); + options.addOption(Option.builder("p").argName("password").hasArg().desc("Cleartext password to hash").build()); + options.addOption(Option.builder("env").argName("name environment variable").hasArg().desc("name environment variable to read password from").build()); + + final CommandLineParser parser = new DefaultParser(); + try { + final CommandLine line = parser.parse(options, args); + + if(line.hasOption("p")) { + System.out.println(hash(line.getOptionValue("p").toCharArray())); + } else if(line.hasOption("env")) { + final String pwd = System.getenv(line.getOptionValue("env")); + if(pwd == null || pwd.isEmpty()) { + throw new Exception("No environment variable '"+line.getOptionValue("env")+"' set"); + } + System.out.println(hash(pwd.toCharArray())); + } else { + final Console console = System.console(); + if(console == null) { + throw new Exception("Cannot allocate a console"); + } + final char[] passwd = console.readPassword("[%s]", "Password:"); + System.out.println(hash(passwd)); + } + } catch (final Exception exp) { + System.err.println("Parsing failed. Reason: " + exp.getMessage()); + formatter.printHelp("hasher.sh", options, true); + System.exit(-1); + } + } + + public static String hash(final char[] clearTextPassword) { + final byte[] salt = new byte[16]; + new SecureRandom().nextBytes(salt); + final String hash = OpenBSDBCrypt.generate((Objects.requireNonNull(clearTextPassword)), salt, 12); + Arrays.fill(salt, (byte)0); + Arrays.fill(clearTextPassword, '\0'); + return hash; + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/tools/OpenDistroSecurityAdmin.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/tools/OpenDistroSecurityAdmin.java new file mode 100644 index 0000000000..43457c307d --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/tools/OpenDistroSecurityAdmin.java @@ -0,0 +1,1042 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.tools; + +import java.io.ByteArrayInputStream; +import java.io.Console; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; +import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsRequest; +import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.action.admin.cluster.tasks.PendingClusterTasksRequest; +import org.elasticsearch.action.admin.cluster.tasks.PendingClusterTasksResponse; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; +import org.elasticsearch.action.admin.indices.get.GetIndexRequest; +import org.elasticsearch.action.admin.indices.get.GetIndexRequest.Feature; +import org.elasticsearch.action.admin.indices.get.GetIndexResponse; +import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.PluginInfo; +import org.elasticsearch.transport.Netty4Plugin; + +import com.amazon.opendistroforelasticsearch.security.OpenDistroSecurityPlugin; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateAction; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateNodeResponse; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateRequest; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateResponse; +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIAction; +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIRequest; +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIResponse; +import com.amazon.opendistroforelasticsearch.security.ssl.util.ExceptionUtils; +import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.OpenDistroSecurityDeprecationHandler; +import com.google.common.io.Files; + +public class OpenDistroSecurityAdmin { + + private static final String OPENDISTRO_SECURITY_TS_PASS = "OPENDISTRO_SECURITY_TS_PASS"; + private static final String OPENDISTRO_SECURITY_KS_PASS = "OPENDISTRO_SECURITY_KS_PASS"; + private static final String OPENDISTRO_SECURITY_KEYPASS = "OPENDISTRO_SECURITY_KEYPASS"; + //not used in multithreaded fashion, so it's okay to define it as a constant here + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MMM-dd_HH-mm-ss", Locale.ENGLISH); //NOSONAR + private static final Settings ENABLE_ALL_ALLOCATIONS_SETTINGS = Settings.builder() + .put("cluster.routing.allocation.enable", "all") + .build(); + + public static void main(final String[] args) { + try { + main0(args); + } catch (NoNodeAvailableException e) { + System.out.println("ERR: Cannot connect to Elasticsearch. Please refer to elasticsearch logfile for more information"); + System.out.println("Trace:"); + System.out.println(ExceptionsHelper.stackTrace(e)); + System.out.println(); + System.exit(-1); + } + catch (IndexNotFoundException e) { + System.out.println("ERR: No Open Distro Security configuartion index found. Please execute securityadmin with different command line parameters"); + System.out.println("When you run it for the first time do not specify -us, -era, -dra or -rl"); + System.out.println(); + System.exit(-1); + } + catch (Exception e) { + + if (e instanceof ElasticsearchException + && e.getMessage() != null + && e.getMessage().contains("no permissions")) { + + System.out.println("ERR: You try to connect with a TLS node certificate instead of an admin client certificate"); + System.out.println(); + System.exit(-1); + } + + System.out.println("ERR: An unexpected "+e.getClass().getSimpleName()+" occured: "+e.getMessage()); + System.out.println("Trace:"); + System.out.println(ExceptionsHelper.stackTrace(e)); + System.out.println(); + System.exit(-1); + } + } + + private static void main0(final String[] args) throws Exception { + + System.out.println("Open Distro Security Admin v6"); + System.setProperty("security.nowarn.client","true"); + System.setProperty("jdk.tls.rejectClientInitiatedRenegotiation","true"); + + final HelpFormatter formatter = new HelpFormatter(); + Options options = new Options(); + options.addOption( "nhnv", "disable-host-name-verification", false, "Disable hostname verification" ); + options.addOption( "nrhn", "disable-resolve-hostname", false, "Disable DNS lookup of hostnames" ); + options.addOption(Option.builder("ts").longOpt("truststore").hasArg().argName("file").desc("Path to truststore (JKS/PKCS12 format)").build()); + options.addOption(Option.builder("ks").longOpt("keystore").hasArg().argName("file").desc("Path to keystore (JKS/PKCS12 format").build()); + options.addOption(Option.builder("tst").longOpt("truststore-type").hasArg().argName("type").desc("JKS or PKCS12, if not given we use the file extension to dectect the type").build()); + options.addOption(Option.builder("kst").longOpt("keystore-type").hasArg().argName("type").desc("JKS or PKCS12, if not given we use the file extension to dectect the type").build()); + options.addOption(Option.builder("tspass").longOpt("truststore-password").hasArg().argName("password").desc("Truststore password").build()); + options.addOption(Option.builder("kspass").longOpt("keystore-password").hasArg().argName("password").desc("Keystore password").build()); + options.addOption(Option.builder("cd").longOpt("configdir").hasArg().argName("directory").desc("Directory for config files").build()); + options.addOption(Option.builder("h").longOpt("hostname").hasArg().argName("host").desc("Elasticsearch host (default: localhost)").build()); + options.addOption(Option.builder("p").longOpt("port").hasArg().argName("port").desc("Elasticsearch transport port (default: 9300)").build()); + options.addOption(Option.builder("cn").longOpt("clustername").hasArg().argName("clustername").desc("Clustername (do not use together with -icl)").build()); + options.addOption( "sniff", "enable-sniffing", false, "Enable client.transport.sniff" ); + options.addOption( "icl", "ignore-clustername", false, "Ignore clustername (do not use together with -cn)" ); + options.addOption(Option.builder("r").longOpt("retrieve").desc("retrieve current config").build()); + options.addOption(Option.builder("f").longOpt("file").hasArg().argName("file").desc("file").build()); + options.addOption(Option.builder("t").longOpt("type").hasArg().argName("file-type").desc("file-type").build()); + options.addOption(Option.builder("tsalias").longOpt("truststore-alias").hasArg().argName("alias").desc("Truststore alias").build()); + options.addOption(Option.builder("ksalias").longOpt("keystore-alias").hasArg().argName("alias").desc("Keystore alias").build()); + options.addOption(Option.builder("ec").longOpt("enabled-ciphers").hasArg().argName("cipers").desc("Comma separated list of enabled TLS ciphers").build()); + options.addOption(Option.builder("ep").longOpt("enabled-protocols").hasArg().argName("protocols").desc("Comma separated list of enabled TLS protocols").build()); + //TODO mark as deprecated and replace it with "era" if "era" is mature enough + options.addOption(Option.builder("us").longOpt("update_settings").hasArg().argName("number of replicas").desc("Update the number of Open Distro Security index replicas, reload configuration on all nodes and exit").build()); + options.addOption(Option.builder("i").longOpt("index").hasArg().argName("indexname").desc("The index Open Distro Security uses to store the configuration").build()); + options.addOption(Option.builder("era").longOpt("enable-replica-autoexpand").desc("Enable replica auto expand and exit").build()); + options.addOption(Option.builder("dra").longOpt("disable-replica-autoexpand").desc("Disable replica auto expand and exit").build()); + options.addOption(Option.builder("rl").longOpt("reload").desc("Reload the configuration on all nodes, flush all Open Distro Security caches and exit").build()); + options.addOption(Option.builder("ff").longOpt("fail-fast").desc("fail-fast if something goes wrong").build()); + options.addOption(Option.builder("dg").longOpt("diagnose").desc("Log diagnostic trace into a file").build()); + options.addOption(Option.builder("dci").longOpt("delete-config-index").desc("Delete '.opendistro_security' config index and exit.").build()); + options.addOption(Option.builder("esa").longOpt("enable-shard-allocation").desc("Enable all shard allocation and exit.").build()); + options.addOption(Option.builder("arc").longOpt("accept-red-cluster").desc("Also operate on a red cluster. If not specified the cluster state has to be at least yellow.").build()); + + options.addOption(Option.builder("cacert").hasArg().argName("file").desc("Path to trusted cacert (PEM format)").build()); + options.addOption(Option.builder("cert").hasArg().argName("file").desc("Path to admin certificate in PEM format").build()); + options.addOption(Option.builder("key").hasArg().argName("file").desc("Path to the key of admin certificate").build()); + options.addOption(Option.builder("keypass").hasArg().argName("password").desc("Password of the key of admin certificate (optional)").build()); + + options.addOption(Option.builder("noopenssl").longOpt("no-openssl").desc("Do not use OpenSSL even if available (default: use it if available)").build()); + + options.addOption(Option.builder("si").longOpt("show-info").desc("Show system and license info").build()); + + options.addOption(Option.builder("w").longOpt("whoami").desc("Show information about the used admin certificate").build()); + + options.addOption(Option.builder("prompt").longOpt("prompt-for-password").desc("Prompt for password if not supplied").build()); + + options.addOption(Option.builder("er").longOpt("explicit-replicas").hasArg().argName("number of replicas").desc("Set explicit number of replicas or autoexpand expression for .opendistro_security index").build()); + + + //when adding new options also adjust validate(CommandLine line) + + String hostname = "localhost"; + int port = 9300; + String kspass = System.getenv(OPENDISTRO_SECURITY_KS_PASS); + String tspass = System.getenv(OPENDISTRO_SECURITY_TS_PASS); + String cd = "."; + String ks = null; + String ts = null; + String kst = null; + String tst = null; + boolean nhnv = false; + boolean nrhn = false; + boolean sniff = false; + boolean icl = false; + String clustername = "elasticsearch"; + String file = null; + String type = null; + boolean retrieve = false; + String ksAlias = null; + String tsAlias = null; + String[] enabledProtocols = new String[0]; + String[] enabledCiphers = new String[0]; + Integer updateSettings = null; + String index = ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX; + Boolean replicaAutoExpand = null; + boolean reload = false; + boolean failFast = false; + boolean diagnose = false; + boolean deleteConfigIndex = false; + boolean enableShardAllocation = false; + boolean acceptRedCluster = false; + + String keypass = System.getenv(OPENDISTRO_SECURITY_KEYPASS); + boolean useOpenSSLIfAvailable = true; + //boolean simpleAuth = false; + String cacert = null; + String cert = null; + String key = null; + boolean si; + boolean whoami; + final boolean promptForPassword; + String explicitReplicas = null; + + CommandLineParser parser = new DefaultParser(); + try { + CommandLine line = parser.parse( options, args ); + + validate(line); + + hostname = line.getOptionValue("h", hostname); + port = Integer.parseInt(line.getOptionValue("p", String.valueOf(port))); + + promptForPassword = line.hasOption("prompt"); + + if(kspass == null || kspass.isEmpty()) { + kspass = line.getOptionValue("kspass",promptForPassword?null:"changeit"); + } + + if(tspass == null || tspass.isEmpty()) { + tspass = line.getOptionValue("tspass",promptForPassword?null:kspass); + } + + cd = line.getOptionValue("cd", cd); + + if(!cd.endsWith(File.separator)) { + cd += File.separator; + } + + ks = line.getOptionValue("ks",ks); + ts = line.getOptionValue("ts",ts); + kst = line.getOptionValue("kst", kst); + tst = line.getOptionValue("tst", tst); + nhnv = line.hasOption("nhnv"); + nrhn = line.hasOption("nrhn"); + clustername = line.getOptionValue("cn", clustername); + sniff = line.hasOption("sniff"); + icl = line.hasOption("icl"); + file = line.getOptionValue("f", file); + type = line.getOptionValue("t", type); + retrieve = line.hasOption("r"); + ksAlias = line.getOptionValue("ksalias", ksAlias); + tsAlias = line.getOptionValue("tsalias", tsAlias); + index = line.getOptionValue("i", index); + + String enabledCiphersString = line.getOptionValue("ec", null); + String enabledProtocolsString = line.getOptionValue("ep", null); + + if(enabledCiphersString != null) { + enabledCiphers = enabledCiphersString.split(","); + } + + if(enabledProtocolsString != null) { + enabledProtocols = enabledProtocolsString.split(","); + } + + updateSettings = line.hasOption("us")?Integer.parseInt(line.getOptionValue("us")):null; + + reload = line.hasOption("rl"); + + if(line.hasOption("era")) { + replicaAutoExpand = true; + } + + if(line.hasOption("dra")) { + replicaAutoExpand = false; + } + + failFast = line.hasOption("ff"); + diagnose = line.hasOption("dg"); + deleteConfigIndex = line.hasOption("dci"); + enableShardAllocation = line.hasOption("esa"); + acceptRedCluster = line.hasOption("arc"); + + cacert = line.getOptionValue("cacert"); + cert = line.getOptionValue("cert"); + key = line.getOptionValue("key"); + keypass = line.getOptionValue("keypass", keypass); + + useOpenSSLIfAvailable = !line.hasOption("noopenssl"); + + si = line.hasOption("si"); + + whoami = line.hasOption("w"); + + explicitReplicas = line.getOptionValue("er", explicitReplicas); + + } + catch( ParseException exp ) { + System.out.println("ERR: Parsing failed. Reason: " + exp.getMessage()); + formatter.printHelp("securityadmin.sh", options, true); + return; + } + + if(port < 9300) { + System.out.println("WARNING: Seems you want connect to the Elasticsearch HTTP port."+System.lineSeparator() + + " securityadmin connects on the transport port which is normally 9300."); + } + + System.out.print("Will connect to "+hostname+":"+port); + Socket socket = new Socket(); + + try { + + socket.connect(new InetSocketAddress(hostname, port)); + + } catch (java.net.ConnectException ex) { + System.out.println(); + System.out.println("ERR: Seems there is no Elasticsearch running on "+hostname+":"+port+" - Will exit"); + System.exit(-1); + } finally { + try { + socket.close(); + } catch (Exception e) { + //ignore + } + } + + System.out.println(" ... done"); + + final Settings.Builder settingsBuilder = Settings + .builder() + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, !nhnv) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, !nrhn) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_ENABLED, true) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_ENABLE_OPENSSL_IF_AVAILABLE, useOpenSSLIfAvailable) + .putList(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_ENABLED_CIPHERS, enabledCiphers) + .putList(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS, enabledProtocols) + + .put("cluster.name", clustername) + .put("client.transport.ignore_cluster_name", icl) + .put("client.transport.sniff", sniff); + + if(ksAlias != null) { + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS, ksAlias); + } + + if(tsAlias != null) { + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTSTORE_ALIAS, tsAlias); + } + + if(ks != null) { + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, ks); + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE, kst==null?(ks.endsWith(".jks")?"JKS":"PKCS12"):kst); + + if(kspass == null && promptForPassword) { + kspass = promptForPassword("Keystore", "kspass", OPENDISTRO_SECURITY_KS_PASS); + } + + if(kspass != null) { + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_PASSWORD, kspass); + } + } + + if(ts != null) { + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, ts); + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTSTORE_TYPE, tst==null?(ts.endsWith(".jks")?"JKS":"PKCS12"):tst); + + if(tspass == null && promptForPassword) { + tspass = promptForPassword("Truststore", "tspass", OPENDISTRO_SECURITY_TS_PASS); + } + + if(tspass != null) { + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTSTORE_PASSWORD, tspass); + } + } + + if(cacert != null) { + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, cacert); + } + + if(cert != null) { + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, cert); + } + + if(key != null) { + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, key); + + if(keypass == null && promptForPassword) { + keypass = promptForPassword("Pemkey", "keypass", OPENDISTRO_SECURITY_KEYPASS); + } + + if(keypass != null) { + settingsBuilder.put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_PEMKEY_PASSWORD, keypass); + } + } + + Settings settings = settingsBuilder.build(); + + try (@SuppressWarnings("resource") + TransportClient tc = new TransportClientImpl(settings, asCollection(Netty4Plugin.class, OpenDistroSecurityPlugin.class)) + .addTransportAddress(new TransportAddress(new InetSocketAddress(hostname, port)))) { + + try { + issueWarnings(tc); + } catch (Exception e1) { + System.out.println("Unable to check whether cluster is sane: "+e1.getMessage()); + } + + final WhoAmIResponse whoAmIRes = tc.execute(WhoAmIAction.INSTANCE, new WhoAmIRequest()).actionGet(); + System.out.println("Connected as "+whoAmIRes.getDn()); + + if(!whoAmIRes.isAdmin()) { + + System.out.println("ERR: "+whoAmIRes.getDn()+" is not an admin user"); + + if(!whoAmIRes.isNodeCertificateRequest()) { + System.out.println("Seems you use a client certificate but this one is not registered as admin_dn"); + System.out.println("Make sure elasticsearch.yml on all nodes contains:"); + System.out.println("opendistro_security.authcz.admin_dn:"+System.lineSeparator()+ + " - \""+whoAmIRes.getDn()+"\""); + } else { + System.out.println("Seems you use a node certificate. This is not permitted, you have to use a client certificate and register it as admin_dn in elasticsearch.yml"); + } + System.exit(-1); + } else if(whoAmIRes.isNodeCertificateRequest()) { + System.out.println("ERR: Seems you use a node certificate which is also an admin certificate"); + System.out.println(" That may have worked with older Open Distro Security versions but it indicates"); + System.out.println(" a configuration error and is therefore forbidden now."); + if(failFast) { + System.exit(-1); + } + + } + + if(updateSettings != null) { + Settings indexSettings = Settings.builder().put("index.number_of_replicas", updateSettings).build(); + tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config","roles","rolesmapping","internalusers","actiongroups"})).actionGet(); + final AcknowledgedResponse response = tc.admin().indices().updateSettings((new UpdateSettingsRequest(index).settings(indexSettings))).actionGet(); + System.out.println("Reload config on all nodes"); + System.out.println("Update number of replicas to "+(updateSettings) +" with result: "+response.isAcknowledged()); + System.exit(response.isAcknowledged()?0:-1); + } + + if(reload) { + tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config","roles","rolesmapping","internalusers","actiongroups"})).actionGet(); + System.out.println("Reload config on all nodes"); + System.exit(0); + } + + if(si) { + System.exit(0); + } + + if(whoami) { + System.out.println(whoAmIRes.toString()); + System.exit(0); + } + + + if(replicaAutoExpand != null) { + Settings indexSettings = Settings.builder() + .put("index.auto_expand_replicas", replicaAutoExpand?"0-all":"false") + .build(); + tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config","roles","rolesmapping","internalusers","actiongroups"})).actionGet(); + final AcknowledgedResponse response = tc.admin().indices().updateSettings((new UpdateSettingsRequest(index).settings(indexSettings))).actionGet(); + System.out.println("Reload config on all nodes"); + System.out.println("Auto-expand replicas "+(replicaAutoExpand?"enabled":"disabled")); + System.exit(response.isAcknowledged()?0:-1); + } + + if(enableShardAllocation) { + final boolean successful = tc.admin().cluster() + .updateSettings(new ClusterUpdateSettingsRequest() + .transientSettings(ENABLE_ALL_ALLOCATIONS_SETTINGS) + .persistentSettings(ENABLE_ALL_ALLOCATIONS_SETTINGS)) + .actionGet() + .isAcknowledged(); + + if(successful) { + System.out.println("Persistent and transient shard allocation enabled"); + } else { + System.out.println("ERR: Unable to enable shard allocation"); + } + + System.exit(successful?0:-1); + } + + if(failFast) { + System.out.println("Fail-fast is activated"); + } + + if(diagnose) { + generateDiagnoseTrace(tc); + } + + System.out.println("Contacting elasticsearch cluster '"+clustername+"'"+(acceptRedCluster?"":" and wait for YELLOW clusterstate")+" ..."); + + ClusterHealthResponse chr = null; + + while(chr == null) { + try { + final ClusterHealthRequest chrequest = new ClusterHealthRequest().timeout(TimeValue.timeValueMinutes(5)); + if(!acceptRedCluster) { + chrequest.waitForYellowStatus(); + } + chr = tc.admin().cluster().health(chrequest).actionGet(); + } catch (Exception e) { + + Throwable rootCause = ExceptionUtils.getRootCause(e); + + if(!failFast) { + System.out.println("Cannot retrieve cluster state due to: "+e.getMessage()+". This is not an error, will keep on trying ..."); + System.out.println(" Root cause: "+rootCause+" ("+e.getClass().getName()+"/"+rootCause.getClass().getName()+")"); + System.out.println(" * Try running securityadmin.sh with -icl (but no -cl) and -nhnv (If that works you need to check your clustername as well as hostnames in your TLS certificates)"); + System.out.println(" * Make sure that your keystore or PEM certificate is a client certificate (not a node certificate) and configured properly in elasticsearch.yml"); + System.out.println(" * If this is not working, try running securityadmin.sh with --diagnose and see diagnose trace log file)"); + System.out.println(" * Add --accept-red-cluster to allow securityadmin to operate on a red cluster."); + + } else { + System.out.println("ERR: Cannot retrieve cluster state due to: "+e.getMessage()+"."); + System.out.println(" Root cause: "+rootCause+" ("+e.getClass().getName()+"/"+rootCause.getClass().getName()+")"); + System.out.println(" * Try running securityadmin.sh with -icl (but no -cl) and -nhnv (If that works you need to check your clustername as well as hostnames in your TLS certificates)"); + System.out.println(" * Make also sure that your keystore or PEM certificate is a client certificate (not a node certificate) and configured properly in elasticsearch.yml"); + System.out.println(" * If this is not working, try running securityadmin.sh with --diagnose and see diagnose trace log file)"); + System.out.println(" * Add --accept-red-cluster to allow securityadmin to operate on a red cluster."); + + System.exit(-1); + } + + Thread.sleep(3000); + continue; + } + } + + final boolean timedOut = chr.isTimedOut(); + + if (!acceptRedCluster && timedOut) { + System.out.println("ERR: Timed out while waiting for a green or yellow cluster state."); + System.out.println(" * Try running securityadmin.sh with -icl (but no -cl) and -nhnv (If that works you need to check your clustername as well as hostnames in your TLS certificates)"); + System.out.println(" * Make also sure that your keystore or PEM certificate is a client certificate (not a node certificate) and configured properly in elasticsearch.yml"); + System.out.println(" * If this is not working, try running securityadmin.sh with --diagnose and see diagnose trace log file)"); + System.out.println(" * Add --accept-red-cluster to allow securityadmin to operate on a red cluster."); + System.exit(-1); + } + + System.out.println("Clustername: "+chr.getClusterName()); + System.out.println("Clusterstate: "+chr.getStatus()); + System.out.println("Number of nodes: "+chr.getNumberOfNodes()); + System.out.println("Number of data nodes: "+chr.getNumberOfDataNodes()); + + GetIndexResponse securityIndex = null; + try { + securityIndex = tc.admin().indices().getIndex(new GetIndexRequest().indices(index).addFeatures(Feature.MAPPINGS)).actionGet(); + } catch (IndexNotFoundException e1) { + //ignore + } + final boolean indexExists = securityIndex != null; + + final NodesInfoResponse nodesInfo = tc.admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet(); + + if(deleteConfigIndex) { + + boolean success = true; + + if(indexExists) { + success = tc.admin().indices().delete(new DeleteIndexRequest(index)).actionGet().isAcknowledged(); + System.out.print("Deleted index '"+index+"'"); + } else { + System.out.print("No index '"+index+"' exists, so no need to delete it"); + } + + System.exit(success?0:-1); + } + + if (!indexExists) { + System.out.print(index +" index does not exists, attempt to create it ... "); + + Map indexSettings = new HashMap<>(); + indexSettings.put("index.number_of_shards", 1); + + if(explicitReplicas != null) { + if(explicitReplicas.contains("-")) { + indexSettings.put("index.auto_expand_replicas", explicitReplicas); + } else { + indexSettings.put("index.number_of_replicas", Integer.parseInt(explicitReplicas)); + } + } else { + indexSettings.put("index.auto_expand_replicas", "0-all"); + } + + final boolean indexCreated = tc.admin().indices().create(new CreateIndexRequest(index) + .settings(indexSettings)) + .actionGet().isAcknowledged(); + + if (indexCreated) { + System.out.println("done ("+(explicitReplicas!=null?explicitReplicas:"0-all")+" replicas)"); + } else { + System.out.println("failed!"); + System.out.println("FAIL: Unable to create the "+index+" index. See elasticsearch logs for more details"); + System.exit(-1); + } + + } else { + System.out.println(index+" index already exists, so we do not need to create one."); + + try { + ClusterHealthResponse chrsg = tc.admin().cluster().health(new ClusterHealthRequest(index)).actionGet(); + + if (chrsg.isTimedOut()) { + System.out.println("ERR: Timed out while waiting for "+index+" index state."); + } + + if (chrsg.getStatus() == ClusterHealthStatus.RED) { + System.out.println("ERR: "+index+" index state is RED."); + } + + if (chrsg.getStatus() == ClusterHealthStatus.YELLOW) { + System.out.println("INFO: "+index+" index state is YELLOW, it seems you miss some replicas"); + } + + } catch (Exception e) { + if(!failFast) { + System.out.println("Cannot retrieve "+index+" index state state due to "+e.getMessage()+". This is not an error, will keep on trying ..."); + } else { + System.out.println("ERR: Cannot retrieve "+index+" index state state due to "+e.getMessage()+"."); + System.exit(-1); + } + } + } + + final boolean legacy = indexExists + && securityIndex.getMappings() != null + && securityIndex.getMappings().get(index) != null + && securityIndex.getMappings().get(index).containsKey("config"); + + if(legacy) { + System.out.println("Legacy index '"+index+"' detected."); + } + + if(retrieve) { + String date = DATE_FORMAT.format(new Date()); + + boolean success = retrieveFile(tc, cd+"config_"+date+".yml", index, "config", legacy); + success = retrieveFile(tc, cd+"roles_"+date+".yml", index, "roles", legacy) && success; + success = retrieveFile(tc, cd+"roles_mapping_"+date+".yml", index, "rolesmapping", legacy) && success; + success = retrieveFile(tc, cd+"internal_users_"+date+".yml", index, "internalusers", legacy) && success; + success = retrieveFile(tc, cd+"action_groups_"+date+".yml", index, "actiongroups", legacy) && success; + System.exit(success?0:-1); + } + + boolean isCdAbs = new File(cd).isAbsolute(); + + System.out.println("Populate config from "+(isCdAbs?cd:new File(".", cd).getCanonicalPath())); + + if(file != null) { + if(type == null) { + System.out.println("ERR: type missing"); + System.exit(-1); + } + + if(!Arrays.asList(new String[]{"config", "roles", "rolesmapping", "internalusers","actiongroups" }).contains(type)) { + System.out.println("ERR: Invalid type '"+type+"'"); + System.exit(-1); + } + + boolean success = uploadFile(tc, file, index, type, legacy); + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{type})).actionGet(); + + success = checkConfigUpdateResponse(cur, nodesInfo, 1) && success; + + System.out.println("Done with "+(success?"success":"failures")); + System.exit(success?0:-1); + } + + boolean success = uploadFile(tc, cd+"config.yml", index, "config", legacy); + success = uploadFile(tc, cd+"roles.yml", index, "roles", legacy) && success; + success = uploadFile(tc, cd+"roles_mapping.yml", index, "rolesmapping", legacy) && success; + success = uploadFile(tc, cd+"internal_users.yml", index, "internalusers", legacy) && success; + success = uploadFile(tc, cd+"action_groups.yml", index, "actiongroups", legacy) && success; + + if(failFast && !success) { + System.out.println("ERR: cannot upload configuration, see errors above"); + System.exit(-1); + } + + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config","roles","rolesmapping","internalusers","actiongroups"})).actionGet(); + + success = checkConfigUpdateResponse(cur, nodesInfo, 5) && success; + + System.out.println("Done with "+(success?"success":"failures")); + System.exit(success?0:-1); + } + // TODO audit changes to .opendistro_security index + } + + private static boolean checkConfigUpdateResponse(ConfigUpdateResponse response, NodesInfoResponse nir, int expectedConfigCount) { + + int expectedNodeCount = 0; + + for(NodeInfo ni: nir.getNodes()) { + Settings nodeSettings = ni.getSettings(); + + //do not count tribe clients + if(nodeSettings.get("tribe.name", null) == null) { + expectedNodeCount++; + } + } + + boolean success = response.getNodes().size() == expectedNodeCount; + if(!success) { + System.out.println("FAIL: Expected "+expectedNodeCount+" nodes to return response, but got only "+response.getNodes().size()); + } + + for(String nodeId: response.getNodesMap().keySet()) { + ConfigUpdateNodeResponse node = response.getNodesMap().get(nodeId); + boolean successNode = (node.getUpdatedConfigTypes() != null && node.getUpdatedConfigTypes().length == expectedConfigCount); + + if(!successNode) { + System.out.println("FAIL: Expected "+expectedConfigCount+" config types for node "+nodeId+" but got only "+Arrays.toString(node.getUpdatedConfigTypes()) + " due to: "+node.getMessage()==null?"unknown reason":node.getMessage()); + } + + success = success && successNode; + } + + return success; + } + + private static boolean uploadFile(final Client tc, final String filepath, final String index, final String _id, final boolean legacy) { + + String type = "security"; + String id = _id; + + if(legacy) { + type = _id; + id = "0"; + } + + System.out.println("Will update '"+type+"/" + id + "' with " + filepath+" "+(legacy?"(legacy mode)":"")); + + try (Reader reader = new FileReader(filepath)) { + + final String res = tc + .index(new IndexRequest(index).type(type).id(id).setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(_id, readXContent(reader, XContentType.YAML))).actionGet().getId(); + + if (id.equals(res)) { + System.out.println(" SUCC: Configuration for '" + _id + "' created or updated"); + return true; + } else { + System.out.println(" FAIL: Configuration for '" + _id + + "' failed for unknown reasons. Please consult the Elasticsearch logfile."); + } + } catch (Exception e) { + System.out.println(" FAIL: Configuration for '" + _id + "' failed because of " + e.toString()); + } + + return false; + } + + private static boolean retrieveFile(final Client tc, final String filepath, final String index, final String _id, final boolean legacy) { + + String type = "security"; + String id = _id; + + if(legacy) { + type = _id; + id = "0"; + } + + System.out.println("Will retrieve '"+type+"/" +id+"' into "+filepath+" "+(legacy?"(legacy mode)":"")); + try (Writer writer = new FileWriter(filepath)) { + + final GetResponse response = tc.get(new GetRequest(index).type(type).id(id).refresh(true).realtime(false)).actionGet(); + + if (response.isExists()) { + if(response.isSourceEmpty()) { + System.out.println(" FAIL: Configuration for '"+_id+"' failed because of empty source"); + return false; + } + + String yaml = convertToYaml(_id, response.getSourceAsBytesRef(), true); + writer.write(yaml); + System.out.println(" SUCC: Configuration for '"+_id+"' stored in "+filepath); + return true; + } else { + System.out.println(" FAIL: Get configuration for '"+_id+"' because it does not exist"); + } + } catch (Exception e) { + System.out.println(" FAIL: Get configuration for '"+_id+"' failed because of "+e.toString()); + } + + return false; + } + + private static BytesReference readXContent(final Reader reader, final XContentType xContentType) throws IOException { + BytesReference retVal; + XContentParser parser = null; + try { + parser = XContentFactory.xContent(xContentType).createParser(NamedXContentRegistry.EMPTY, OpenDistroSecurityDeprecationHandler.INSTANCE, reader); + parser.nextToken(); + final XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.copyCurrentStructure(parser); + retVal = BytesReference.bytes(builder); + } finally { + if (parser != null) { + parser.close(); + } + } + + //validate + Settings.builder().loadFromStream("dummy.json", new ByteArrayInputStream(BytesReference.toBytes(retVal)), true).build(); + return retVal; + } + + private static String convertToYaml(String type, BytesReference bytes, boolean prettyPrint) throws IOException { + + try (XContentParser parser = JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, OpenDistroSecurityDeprecationHandler.INSTANCE, bytes.streamInput())) { + + parser.nextToken(); + parser.nextToken(); + + if(!type.equals((parser.currentName()))) { + return null; + } + + parser.nextToken(); + + XContentBuilder builder = XContentFactory.yamlBuilder(); + if (prettyPrint) { + builder.prettyPrint(); + } + builder.rawValue(new ByteArrayInputStream(parser.binaryValue()), XContentType.YAML); + return Strings.toString(builder); + } + } + + protected static class TransportClientImpl extends TransportClient { + + public TransportClientImpl(Settings settings, Collection> plugins) { + super(settings, plugins); + } + + public TransportClientImpl(Settings settings, Settings defaultSettings, Collection> plugins) { + super(settings, defaultSettings, plugins, null); + } + } + + @SafeVarargs + protected static Collection> asCollection(Class... plugins) { + return Arrays.asList(plugins); + } + + protected static void generateDiagnoseTrace(final Client tc) { + + final String date = DATE_FORMAT.format(new Date()); + + final StringBuilder sb = new StringBuilder(); + sb.append("Diagnostic securityadmin trace"+System.lineSeparator()); + sb.append("ES client version: "+Version.CURRENT+System.lineSeparator()); + sb.append("Client properties: "+System.getProperties()+System.lineSeparator()); + sb.append(date+System.lineSeparator()); + sb.append(System.lineSeparator()); + + try { + sb.append("Who am i:"+System.lineSeparator()); + final WhoAmIResponse whoAmIRes = tc.execute(WhoAmIAction.INSTANCE, new WhoAmIRequest()).actionGet(); + sb.append(Strings.toString(whoAmIRes,true, true)); + } catch (Exception e1) { + sb.append(ExceptionsHelper.stackTrace(e1)); + } + + try { + sb.append("ClusterHealthRequest:"+System.lineSeparator()); + ClusterHealthResponse nir = tc.admin().cluster().health(new ClusterHealthRequest()).actionGet(); + sb.append(Strings.toString(nir,true, true)); + } catch (Exception e1) { + sb.append(ExceptionsHelper.stackTrace(e1)); + } + + try { + sb.append(System.lineSeparator()+"NodesInfoResponse:"+System.lineSeparator()); + NodesInfoResponse nir = tc.admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet(); + sb.append(Strings.toString(nir,true, true)); + } catch (Exception e1) { + sb.append(ExceptionsHelper.stackTrace(e1)); + } + + try { + sb.append(System.lineSeparator()+"NodesStatsRequest:"+System.lineSeparator()); + NodesStatsResponse nir = tc.admin().cluster().nodesStats(new NodesStatsRequest()).actionGet(); + sb.append(Strings.toString(nir,true, true)); + } catch (Exception e1) { + sb.append(ExceptionsHelper.stackTrace(e1)); + } + + try { + sb.append(System.lineSeparator()+"PendingClusterTasksRequest:"+System.lineSeparator()); + PendingClusterTasksResponse nir = tc.admin().cluster().pendingClusterTasks(new PendingClusterTasksRequest()).actionGet(); + sb.append(Strings.toString(nir,true, true)); + } catch (Exception e1) { + sb.append(ExceptionsHelper.stackTrace(e1)); + } + + try { + sb.append(System.lineSeparator()+"IndicesStatsRequest:"+System.lineSeparator()); + IndicesStatsResponse nir = tc.admin().indices().stats(new IndicesStatsRequest()).actionGet(); + sb.append(Strings.toString(nir, true, true)); + } catch (Exception e1) { + sb.append(ExceptionsHelper.stackTrace(e1)); + } + + try { + File dfile = new File("securityadmin_diag_trace_"+date+".txt"); + Files.asCharSink(dfile, StandardCharsets.UTF_8).write(sb); + System.out.println("Diagnostic trace written to: "+dfile.getAbsolutePath()); + } catch (Exception e1) { + System.out.println("ERR: cannot write diag trace file due to "+e1); + } + } + + private static void validate(CommandLine line) throws ParseException{ + + if(line.hasOption("ts") && line.hasOption("cacert")) { + System.out.println("WARN: It makes no sense to specify -ts as well as -cacert"); + } + + if(line.hasOption("ks") && line.hasOption("cert")) { + System.out.println("WARN: It makes no sense to specify -ks as well as -cert"); + } + + if(line.hasOption("ks") && line.hasOption("key")) { + System.out.println("WARN: It makes no sense to specify -ks as well as -key"); + } + + if(line.hasOption("cd") && line.hasOption("rl")) { + System.out.println("WARN: It makes no sense to specify -cd as well as -r"); + } + + if(line.hasOption("cd") && line.hasOption("f")) { + System.out.println("WARN: It makes no sense to specify -cd as well as -f"); + } + + if(line.hasOption("cd") && line.hasOption("r")) { + System.out.println("WARN: It makes no sense to specify -cd as well as -r"); + } + + if(line.hasOption("cn") && line.hasOption("icl")) { + throw new ParseException("Only set one of -cn or -icl"); + } + + if(!line.hasOption("ks") && !line.hasOption("cert") /*&& !line.hasOption("simple-auth")*/) { + throw new ParseException("Specify at least -ks or -cert"); + } + + if(!line.hasOption("ts") && !line.hasOption("cacert") /*&& !line.hasOption("simple-auth")*/) { + throw new ParseException("Specify at least -ts or -cacert"); + } + + //TODO add more validation rules + } + + private static String promptForPassword(String passwordName, String commandLineOption, String envVarName) throws Exception { + final Console console = System.console(); + if(console == null) { + throw new Exception("Cannot allocate a console. Set env var "+envVarName+" or "+commandLineOption+" on commandline in that case"); + } + return new String(console.readPassword("[%s]", passwordName+" password:")); + } + + private static void issueWarnings(Client tc) { + NodesInfoResponse nir = tc.admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet(); + Version maxVersion = nir.getNodes().stream().max((n1,n2) -> n1.getVersion().compareTo(n2.getVersion())).get().getVersion(); + Version minVersion = nir.getNodes().stream().min((n1,n2) -> n1.getVersion().compareTo(n2.getVersion())).get().getVersion(); + + if(!maxVersion.equals(minVersion)) { + System.out.println("WARNING: Your cluster consists of different node versions. It is not recommended to run securityadmin against a mixed cluster. This may fail."); + System.out.println(" Minimum node version is "+minVersion.toString()); + System.out.println(" Maximum node version is "+maxVersion.toString()); + } else { + System.out.println("Elasticsearch Version: "+minVersion.toString()); + } + + if(nir.getNodes().size() > 0) { + List pluginInfos = nir.getNodes().get(0).getPlugins().getPluginInfos(); + String securityVersion = pluginInfos.stream().filter(p->p.getClassname().equals("com.amazon.opendistroforelasticsearch.security.OpenDistroSecurityPlugin")).map(p->p.getVersion()).findFirst().orElse(""); + System.out.println("Open Distro Security Version: "+securityVersion); + } + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/DefaultInterClusterRequestEvaluator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/DefaultInterClusterRequestEvaluator.java new file mode 100644 index 0000000000..601a8edaed --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/DefaultInterClusterRequestEvaluator.java @@ -0,0 +1,142 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.transport; + +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.transport.TransportRequest; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; + +public final class DefaultInterClusterRequestEvaluator implements InterClusterRequestEvaluator { + + private final Logger log = LogManager.getLogger(this.getClass()); + private final String certOid; + private final List nodesDn; + + public DefaultInterClusterRequestEvaluator(final Settings settings) { + this.certOid = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CERT_OID, "1.2.3.4.5.5"); + this.nodesDn = settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_NODES_DN, Collections.emptyList()); + } + + @Override + public boolean isInterClusterRequest(TransportRequest request, X509Certificate[] localCerts, X509Certificate[] peerCerts, + final String principal) { + + String[] principals = new String[2]; + + if (principal != null && principal.length() > 0) { + principals[0] = principal; + principals[1] = principal.replace(" ",""); + } + + if (principals[0] != null && WildcardMatcher.matchAny(nodesDn, principals, true)) { + + if (log.isTraceEnabled()) { + log.trace("Treat certificate with principal {} as other node because of it matches one of {}", Arrays.toString(principals), + nodesDn); + } + + return true; + + } else { + if (log.isTraceEnabled()) { + log.trace("Treat certificate with principal {} NOT as other node because we it does not matches one of {}", Arrays.toString(principals), + nodesDn); + } + } + + try { + final Collection> ianList = peerCerts[0].getSubjectAlternativeNames(); + if (ianList != null) { + final StringBuilder sb = new StringBuilder(); + + for (final List ian : ianList) { + + if (ian == null) { + continue; + } + + for (@SuppressWarnings("rawtypes") + final Iterator iterator = ian.iterator(); iterator.hasNext();) { + final int id = (int) iterator.next(); + if (id == 8) { // id 8 = OID, id 1 = name (as string or + // ASN.1 encoded byte[]) + Object value = iterator.next(); + + if (value == null) { + continue; + } + + if (value instanceof String) { + sb.append(id + "::" + value); + } else if (value instanceof byte[]) { + log.error("Unable to handle OID san {} with value {} of type byte[] (ASN.1 DER not supported here)", id, + Arrays.toString((byte[]) value)); + } else { + log.error("Unable to handle OID san {} with value {} of type {}", id, value, value.getClass()); + } + } else { + iterator.next(); + } + } + } + + if (sb.indexOf("8::" + this.certOid) >= 0) { + return true; + } + + } else { + if (log.isTraceEnabled()) { + log.trace("No subject alternative names (san) found"); + } + } + } catch (CertificateParsingException e) { + if (log.isDebugEnabled()) { + log.debug("Exception parsing certificate using {}", e, this.getClass()); + } + throw new ElasticsearchException(e); + } + return false; + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/InterClusterRequestEvaluator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/InterClusterRequestEvaluator.java new file mode 100644 index 0000000000..ddacc66c35 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/InterClusterRequestEvaluator.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.transport; + +import java.security.cert.X509Certificate; + +import org.elasticsearch.transport.TransportRequest; + +/** + * Evaluates a request to determine if it is + * intercluster communication. Implementations + * should include a single arg constructor that + * takes org.elasticsearch.common.settings.Settings + * + */ +public interface InterClusterRequestEvaluator { + + /** + * Determine if request is a message from + * another node in the cluster + * + * @param request The transport request to evaluate + * @param localCerts Local certs to use for evaluating the request which include criteria + * specific to the implementation for confirming intercluster + * communication + * + * @param peerCerts Certs to use for evaluating the request which include criteria + * specific to the implementation for confirming intercluster + * communication + * + * @param principal The principal evaluated by the configured principal extractor + * + * @return True when determined to be intercluster, false otherwise + */ + boolean isInterClusterRequest(final TransportRequest request, final X509Certificate[] localCerts, final X509Certificate[] peerCerts, + final String principal); +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OIDClusterRequestEvaluator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OIDClusterRequestEvaluator.java new file mode 100644 index 0000000000..c56bfa07d3 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OIDClusterRequestEvaluator.java @@ -0,0 +1,66 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.transport; + +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.transport.TransportRequest; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; + +/** + * Implementation to evaluate a certificate extension with a given OID + * and value to the same value found on the peer certificate + * + */ +public final class OIDClusterRequestEvaluator implements InterClusterRequestEvaluator { + private final String certOid; + + public OIDClusterRequestEvaluator(final Settings settings) { + this.certOid = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CERT_OID, "1.2.3.4.5.5"); + } + + @Override + public boolean isInterClusterRequest(TransportRequest request, X509Certificate[] localCerts, X509Certificate[] peerCerts, + final String principal) { + if (localCerts != null && localCerts.length > 0 && peerCerts != null && peerCerts.length > 0) { + final byte[] localValue = localCerts[0].getExtensionValue(certOid); + final byte[] peerValue = peerCerts[0].getExtensionValue(certOid); + if (localValue != null && peerValue != null) { + return Arrays.equals(localValue, peerValue); + } + } + return false; + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OpenDistroSecurityInterceptor.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OpenDistroSecurityInterceptor.java new file mode 100644 index 0000000000..3fcd55853d --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OpenDistroSecurityInterceptor.java @@ -0,0 +1,223 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.transport; + +import java.io.IOException; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.Transport.Connection; +import org.elasticsearch.transport.TransportException; +import org.elasticsearch.transport.TransportInterceptor.AsyncSender; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportRequestHandler; +import org.elasticsearch.transport.TransportRequestOptions; +import org.elasticsearch.transport.TransportResponse; +import org.elasticsearch.transport.TransportResponseHandler; + +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog.Origin; +import com.amazon.opendistroforelasticsearch.security.auth.BackendRegistry; +import com.amazon.opendistroforelasticsearch.security.configuration.ClusterInfoHolder; +import com.amazon.opendistroforelasticsearch.security.ssl.SslExceptionHandler; +import com.amazon.opendistroforelasticsearch.security.ssl.transport.PrincipalExtractor; +import com.amazon.opendistroforelasticsearch.security.support.Base64Helper; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.user.User; +import com.google.common.collect.Maps; + +public class OpenDistroSecurityInterceptor { + + protected final Logger actionTrace = LogManager.getLogger("opendistro_security_action_trace"); + private BackendRegistry backendRegistry; + private AuditLog auditLog; + private final ThreadPool threadPool; + private final PrincipalExtractor principalExtractor; + private final InterClusterRequestEvaluator requestEvalProvider; + private final ClusterService cs; + private final Settings settings; + private final SslExceptionHandler sslExceptionHandler; + + public OpenDistroSecurityInterceptor(final Settings settings, + final ThreadPool threadPool, final BackendRegistry backendRegistry, + final AuditLog auditLog, final PrincipalExtractor principalExtractor, + final InterClusterRequestEvaluator requestEvalProvider, + final ClusterService cs, + final SslExceptionHandler sslExceptionHandler, + final ClusterInfoHolder clusterInfoHolder) { + this.backendRegistry = backendRegistry; + this.auditLog = auditLog; + this.threadPool = threadPool; + this.principalExtractor = principalExtractor; + this.requestEvalProvider = requestEvalProvider; + this.cs = cs; + this.settings = settings; + this.sslExceptionHandler = sslExceptionHandler; + } + + public OpenDistroSecurityRequestHandler getHandler(String action, + TransportRequestHandler actualHandler) { + return new OpenDistroSecurityRequestHandler(action, actualHandler, threadPool, backendRegistry, auditLog, + principalExtractor, requestEvalProvider, cs, sslExceptionHandler); + } + + + public void sendRequestDecorate(AsyncSender sender, Connection connection, String action, + TransportRequest request, TransportRequestOptions options, TransportResponseHandler handler) { + + final Map origHeaders0 = getThreadContext().getHeaders(); + final User user0 = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final String origin0 = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN); + final Object remoteAdress0 = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + + try (ThreadContext.StoredContext stashedContext = getThreadContext().stashContext()) { + final RestoringTransportResponseHandler restoringHandler = new RestoringTransportResponseHandler(handler, stashedContext); + getThreadContext().putHeader("_opendistro_security_remotecn", cs.getClusterName().value()); + + if(this.settings.get("tribe.name", null) == null + && settings.getByPrefix("tribe").size() > 0) { + getThreadContext().putHeader("_opendistro_security_header_tn", "true"); + } + + getThreadContext().putHeader( + Maps.filterKeys(origHeaders0, k->k!=null && ( + k.equals(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER) + || k.equals(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADER) + || k.equals(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER) + || k.equals(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER) + || k.equals(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER) + || k.equals(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER) + || k.equals(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER) + || (k.equals("_opendistro_security_source_field_context") && ! (request instanceof SearchRequest) && !(request instanceof GetRequest)) + || k.startsWith("_opendistro_security_trace") + || k.startsWith(ConfigConstants.OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER) + ))); + + ensureCorrectHeaders(remoteAdress0, user0, origin0); + + if(actionTrace.isTraceEnabled()) { + getThreadContext().putHeader("_opendistro_security_trace"+System.currentTimeMillis()+"#"+UUID.randomUUID().toString(), Thread.currentThread().getName()+" IC -> "+action+" "+getThreadContext().getHeaders().entrySet().stream().filter(p->!p.getKey().startsWith("_opendistro_security_trace")).collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue()))); + } + + + sender.sendRequest(connection, action, request, options, restoringHandler); + } + } + + private void ensureCorrectHeaders(final Object remoteAdr, final User origUser, final String origin) { + // keep original address + + if(origin != null && !origin.isEmpty() /*&& !Origin.LOCAL.toString().equalsIgnoreCase(origin)*/ && getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADER) == null) { + getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADER, origin); + } + + if(origin == null && getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADER) == null) { + getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADER, Origin.LOCAL.toString()); + } + + if (remoteAdr != null && remoteAdr instanceof TransportAddress) { + + String remoteAddressHeader = getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER); + + if(remoteAddressHeader == null) { + getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER, Base64Helper.serializeObject(((TransportAddress) remoteAdr).address())); + } /*else { + if(!((InetSocketAddress)Base64Helper.deserializeObject(remoteAddressHeader)).equals(((TransportAddress) remoteAdr).address())) { + throw new RuntimeException("remote address mismatch "+Base64Helper.deserializeObject(remoteAddressHeader)+"!="+((TransportAddress) remoteAdr).address()); + } + }*/ + } + + if(origUser != null) { + String userHeader = getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER); + + if(userHeader == null) { + getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER, Base64Helper.serializeObject(origUser)); + } /*else { + if(!((User)Base64Helper.deserializeObject(userHeader)).getName().equals(origUser.getName())) { + throw new RuntimeException("user mismatch "+Base64Helper.deserializeObject(userHeader)+"!="+origUser); + } + }*/ + } + } + + private ThreadContext getThreadContext() { + return threadPool.getThreadContext(); + } + + //based on + //org.elasticsearch.transport.TransportService.ContextRestoreResponseHandler + //which is private scoped + private static class RestoringTransportResponseHandler implements TransportResponseHandler { + + private final ThreadContext.StoredContext contextToRestore; + private final TransportResponseHandler innerHandler; + + private RestoringTransportResponseHandler(TransportResponseHandler innerHandler, ThreadContext.StoredContext contextToRestore) { + this.contextToRestore = contextToRestore; + this.innerHandler = innerHandler; + } + + @Override + public T read(StreamInput in) throws IOException { + return innerHandler.read(in); + } + + @Override + public void handleResponse(T response) { + contextToRestore.restore(); + innerHandler.handleResponse(response); + } + + @Override + public void handleException(TransportException e) { + contextToRestore.restore(); + innerHandler.handleException(e); + } + + @Override + public String executor() { + return innerHandler.executor(); + } + } + +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OpenDistroSecurityRequestHandler.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OpenDistroSecurityRequestHandler.java new file mode 100644 index 0000000000..47882a3913 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/transport/OpenDistroSecurityRequestHandler.java @@ -0,0 +1,328 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.transport; + +import java.net.InetSocketAddress; +import java.security.cert.X509Certificate; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.bulk.BulkShardRequest; +import org.elasticsearch.action.support.replication.TransportReplicationAction.ConcreteShardRequest; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportChannel; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportRequestHandler; + +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIAction; +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; +import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog.Origin; +import com.amazon.opendistroforelasticsearch.security.auth.BackendRegistry; +import com.amazon.opendistroforelasticsearch.security.ssl.SslExceptionHandler; +import com.amazon.opendistroforelasticsearch.security.ssl.transport.OpenDistroSecuritySSLRequestHandler; +import com.amazon.opendistroforelasticsearch.security.ssl.transport.PrincipalExtractor; +import com.amazon.opendistroforelasticsearch.security.ssl.util.ExceptionUtils; +import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLRequestHelper; +import com.amazon.opendistroforelasticsearch.security.support.Base64Helper; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.HeaderHelper; +import com.amazon.opendistroforelasticsearch.security.user.User; +import com.google.common.base.Strings; + +public class OpenDistroSecurityRequestHandler extends OpenDistroSecuritySSLRequestHandler { + + protected final Logger actionTrace = LogManager.getLogger("opendistro_security_action_trace"); + private final BackendRegistry backendRegistry; + private final AuditLog auditLog; + private final InterClusterRequestEvaluator requestEvalProvider; + private final ClusterService cs; + + OpenDistroSecurityRequestHandler(String action, + final TransportRequestHandler actualHandler, + final ThreadPool threadPool, + final BackendRegistry backendRegistry, + final AuditLog auditLog, + final PrincipalExtractor principalExtractor, + final InterClusterRequestEvaluator requestEvalProvider, + final ClusterService cs, + final SslExceptionHandler sslExceptionHandler) { + super(action, actualHandler, threadPool, principalExtractor, sslExceptionHandler); + this.backendRegistry = backendRegistry; + this.auditLog = auditLog; + this.requestEvalProvider = requestEvalProvider; + this.cs = cs; + } + + @Override + protected void messageReceivedDecorate(final T request, final TransportRequestHandler handler, + final TransportChannel transportChannel, Task task) throws Exception { + + String resolvedActionClass = request.getClass().getSimpleName(); + + if(request instanceof BulkShardRequest) { + if(((BulkShardRequest) request).items().length == 1) { + resolvedActionClass = ((BulkShardRequest) request).items()[0].request().getClass().getSimpleName(); + } + } + + if(request instanceof ConcreteShardRequest) { + resolvedActionClass = ((ConcreteShardRequest) request).getRequest().getClass().getSimpleName(); + } + + String initialActionClassValue = getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER); + + final ThreadContext.StoredContext sgContext = getThreadContext().newStoredContext(false); + + final String originHeader = getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADER); + + if(!Strings.isNullOrEmpty(originHeader)) { + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, originHeader); + } + + try { + + if(transportChannel.getChannelType() == null) { + throw new RuntimeException("Can not determine channel type (null)"); + } + + if(!transportChannel.getChannelType().equals("direct") && !transportChannel.getChannelType().equals("netty") + && !transportChannel.getChannelType().equals("PerformanceAnalyzerTransportChannelType")) { + throw new RuntimeException("Unknown channel type "+transportChannel.getChannelType()); + } + + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_CHANNEL_TYPE, transportChannel.getChannelType()); + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_ACTION_NAME, task.getAction()); + + //bypass non-netty requests + if(transportChannel.getChannelType().equals("direct")) { + final String userHeader = getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER); + + if(!Strings.isNullOrEmpty(userHeader)) { + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, Objects.requireNonNull((User) Base64Helper.deserializeObject(userHeader))); + } + + final String originalRemoteAddress = getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER); + + if(!Strings.isNullOrEmpty(originalRemoteAddress)) { + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, new TransportAddress((InetSocketAddress) Base64Helper.deserializeObject(originalRemoteAddress))); + } + + if(actionTrace.isTraceEnabled()) { + getThreadContext().putHeader("_opendistro_security_trace"+System.currentTimeMillis()+"#"+UUID.randomUUID().toString(), Thread.currentThread().getName()+" DIR -> "+transportChannel.getChannelType()+" "+getThreadContext().getHeaders()); + } + + putInitialActionClassHeader(initialActionClassValue, resolvedActionClass); + + super.messageReceivedDecorate(request, handler, transportChannel, task); + return; + } + + //if the incoming request is an internal:* or a shard request allow only if request was sent by a server node + //if transport channel is not a netty channel but a direct or local channel (e.g. send via network) then allow it (regardless of beeing a internal: or shard request) + //also allow when issued from a remote cluster for cross cluster search + if ( !HeaderHelper.isInterClusterRequest(getThreadContext()) + && !HeaderHelper.isTrustedClusterRequest(getThreadContext()) + && !task.getAction().equals("internal:transport/handshake") + && (task.getAction().startsWith("internal:") || task.getAction().contains("["))) { + + auditLog.logMissingPrivileges(task.getAction(), request, task); + log.error("Internal or shard requests ("+task.getAction()+") not allowed from a non-server node for transport type "+transportChannel.getChannelType()); + transportChannel.sendResponse(new ElasticsearchSecurityException( + "Internal or shard requests not allowed from a non-server node for transport type "+transportChannel.getChannelType())); + return; + } + + + String principal = null; + + if ((principal = getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_PRINCIPAL)) == null) { + Exception ex = new ElasticsearchSecurityException( + "No SSL client certificates found for transport type "+transportChannel.getChannelType()+". Open Distro Security needs the Open Distro Security SSL plugin to be installed"); + auditLog.logSSLException(request, ex, task.getAction(), task); + log.error("No SSL client certificates found for transport type "+transportChannel.getChannelType()+". Open Distro Security needs the Open Distro Security SSL plugin to be installed"); + transportChannel.sendResponse(ex); + return; + } else { + + if(getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN) == null) { + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, Origin.TRANSPORT.toString()); + } + + //network intercluster request or cross search cluster request + if(HeaderHelper.isInterClusterRequest(getThreadContext()) + || HeaderHelper.isTrustedClusterRequest(getThreadContext())) { + + final String userHeader = getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER); + + if(Strings.isNullOrEmpty(userHeader)) { + //user can be null when a node client wants connect + //getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, User.OPENDISTRO_SECURITY_INTERNAL); + } else { + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, Objects.requireNonNull((User) Base64Helper.deserializeObject(userHeader))); + } + + String originalRemoteAddress = getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER); + + if(!Strings.isNullOrEmpty(originalRemoteAddress)) { + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, new TransportAddress((InetSocketAddress) Base64Helper.deserializeObject(originalRemoteAddress))); + } else { + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, request.remoteAddress()); + } + + } else { + + //this is a netty request from a non-server node (maybe also be internal: or a shard request) + //and therefore issued by a transport client + + if(SSLRequestHelper.containsBadHeader(getThreadContext(), ConfigConstants.OPENDISTRO_SECURITY_CONFIG_PREFIX)) { + final ElasticsearchException exception = ExceptionUtils.createBadHeaderException(); + auditLog.logBadHeaders(request, task.getAction(), task); + log.error(exception); + transportChannel.sendResponse(exception); + return; + } + + //TODO Open Distro Security exception handling, introduce authexception + + User user; + //try { + if((user = backendRegistry.authenticate(request, principal, task, task.getAction())) == null) { + org.apache.logging.log4j.ThreadContext.remove("user"); + + if(task.getAction().equals(WhoAmIAction.NAME)) { + super.messageReceivedDecorate(request, handler, transportChannel, task); + return; + } + + if(task.getAction().equals("cluster:monitor/nodes/liveness") + || task.getAction().equals("internal:transport/handshake")) { + super.messageReceivedDecorate(request, handler, transportChannel, task); + return; + } + + + log.error("Cannot authenticate {} for {}", getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER), task.getAction()); + transportChannel.sendResponse(new ElasticsearchSecurityException("Cannot authenticate "+getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER))); + return; + } else { + // make it possible to filter logs by username + org.apache.logging.log4j.ThreadContext.put("user", user.getName()); + } + //} catch (Exception e) { + // log.error("Error authentication transport user "+e, e); + //auditLog.logFailedLogin(principal, false, null, request); + //transportChannel.sendResponse(ExceptionsHelper.convertToElastic(e)); + //return; + //} + + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); + TransportAddress originalRemoteAddress = request.remoteAddress(); + + if(originalRemoteAddress != null && (originalRemoteAddress instanceof TransportAddress)) { + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, originalRemoteAddress); + } else { + log.error("Request has no proper remote address {}", originalRemoteAddress); + transportChannel.sendResponse(new ElasticsearchException("Request has no proper remote address")); + return; + } + } + + if(actionTrace.isTraceEnabled()) { + getThreadContext().putHeader("_opendistro_security_trace"+System.currentTimeMillis()+"#"+UUID.randomUUID().toString(), Thread.currentThread().getName()+" NETTI -> "+transportChannel.getChannelType()+" "+getThreadContext().getHeaders().entrySet().stream().filter(p->!p.getKey().startsWith("_opendistro_security_trace")).collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue()))); + } + + + putInitialActionClassHeader(initialActionClassValue, resolvedActionClass); + + super.messageReceivedDecorate(request, handler, transportChannel, task); + } + } finally { + + if(actionTrace.isTraceEnabled()) { + getThreadContext().putHeader("_opendistro_security_trace"+System.currentTimeMillis()+"#"+UUID.randomUUID().toString(), Thread.currentThread().getName()+" FIN -> "+transportChannel.getChannelType()+" "+getThreadContext().getHeaders()); + } + + if(sgContext != null) { + sgContext.close(); + } + } + } + + private void putInitialActionClassHeader(String initialActionClassValue, String resolvedActionClass) { + if(initialActionClassValue == null) { + if(getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER) == null) { + getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER, resolvedActionClass); + } + } else { + if(getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER) == null) { + getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER, initialActionClassValue); + } + } + + } + + @Override + protected void addAdditionalContextValues(final String action, final TransportRequest request, final X509Certificate[] localCerts, final X509Certificate[] peerCerts, final String principal) + throws Exception { + + boolean isInterClusterRequest = requestEvalProvider.isInterClusterRequest(request, localCerts, peerCerts, principal); + + if (isInterClusterRequest) { + boolean fromTn = Boolean.parseBoolean(getThreadContext().getHeader("_opendistro_security_header_tn")); + if(fromTn || cs.getClusterName().value().equals(getThreadContext().getHeader("_opendistro_security_remotecn"))) { + + if (log.isTraceEnabled() && !action.startsWith("internal:")) { + log.trace("Is inter cluster request ({}/{}/{})", action, request.getClass(), request.remoteAddress()); + } + + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_INTERCLUSTER_REQUEST, Boolean.TRUE); + } else { + getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTED_CLUSTER_REQUEST, Boolean.TRUE); + } + + } else { + if (log.isTraceEnabled()) { + log.trace("Is not an inter cluster request"); + } + } + + super.addAdditionalContextValues(action, request, localCerts, peerCerts, principal); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentials.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentials.java new file mode 100644 index 0000000000..b3eae45f1c --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/user/AuthCredentials.java @@ -0,0 +1,233 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.user; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.elasticsearch.ElasticsearchSecurityException; + +/** + * AuthCredentials are an abstraction to encapsulate credentials like passwords or generic + * native credentials like GSS tokens. + * + */ +public final class AuthCredentials { + + private static final String DIGEST_ALGORITHM = "SHA-256"; + private final String username; + private byte[] password; + private Object nativeCredentials; + private final Set backendRoles = new HashSet(); + private boolean complete; + private final byte[] internalPasswordHash; + private final Map attributes = new HashMap<>(); + + /** + * Create new credentials with a username and native credentials + * + * @param username The username, must not be null or empty + * @param nativeCredentials Arbitrary credentials (like GSS tokens), must not be null + * @throws IllegalArgumentException if username or nativeCredentials are null or empty + */ + public AuthCredentials(final String username, final Object nativeCredentials) { + this(username, null, nativeCredentials); + + if (nativeCredentials == null) { + throw new IllegalArgumentException("nativeCredentials must not be null or empty"); + } + } + + /** + * Create new credentials with a username and password + * + * @param username The username, must not be null or empty + * @param password The password, must not be null or empty + * @throws IllegalArgumentException if username or password is null or empty + */ + public AuthCredentials(final String username, final byte[] password) { + this(username, password, null); + + if (password == null || password.length == 0) { + throw new IllegalArgumentException("password must not be null or empty"); + } + } + + /** + * Create new credentials with a username, a initial optional set of roles and empty password/native credentials + + * @param username The username, must not be null or empty + * @param backendRoles set of roles this user is a member of + * @throws IllegalArgumentException if username is null or empty + */ + public AuthCredentials(final String username, String... backendRoles) { + this(username, null, null, backendRoles); + } + + private AuthCredentials(final String username, byte[] password, Object nativeCredentials, String... backendRoles) { + super(); + + if (username == null || username.isEmpty()) { + throw new IllegalArgumentException("username must not be null or empty"); + } + + this.username = username; + // make defensive copy + this.password = password == null ? null : Arrays.copyOf(password, password.length); + + if(this.password != null) { + try { + MessageDigest digester = MessageDigest.getInstance(DIGEST_ALGORITHM); + internalPasswordHash = digester.digest(this.password); + } catch (NoSuchAlgorithmException e) { + throw new ElasticsearchSecurityException("Unable to digest password", e); + } + } else { + internalPasswordHash = null; + } + + if(password != null) { + Arrays.fill(password, (byte) '\0'); + password = null; + } + + this.nativeCredentials = nativeCredentials; + nativeCredentials = null; + + if(backendRoles != null && backendRoles.length > 0) { + this.backendRoles.addAll(Arrays.asList(backendRoles)); + } + } + + /** + * Wipe password and native credentials + */ + public void clearSecrets() { + if (password != null) { + Arrays.fill(password, (byte) '\0'); + password = null; + } + + nativeCredentials = null; + } + + public String getUsername() { + return username; + } + + /** + * + * @return Defensive copy of the password + */ + public byte[] getPassword() { + // make defensive copy + return password == null ? null : Arrays.copyOf(password, password.length); + } + + public Object getNativeCredentials() { + return nativeCredentials; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(internalPasswordHash); + result = prime * result + ((username == null) ? 0 : username.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AuthCredentials other = (AuthCredentials) obj; + if (internalPasswordHash == null || other.internalPasswordHash == null || !MessageDigest.isEqual(internalPasswordHash, other.internalPasswordHash)) + return false; + if (username == null) { + if (other.username != null) + return false; + } else if (!username.equals(other.username)) + return false; + return true; + } + + @Override + public String toString() { + return "AuthCredentials [username=" + username + ", password empty=" + (password == null) + ", nativeCredentials empty=" + + (nativeCredentials == null) + ",backendRoles="+backendRoles+"]"; + } + + /** + * + * @return Defensive copy of the roles this user is member of. + */ + public Set getBackendRoles() { + return new HashSet(backendRoles); + } + + public boolean isComplete() { + return complete; + } + + /** + * If the credentials are complete and no further roundtrips with the originator are due + * then this method must be called so that the authentication flow can proceed. + *

+ * If this credentials are already marked a complete then a call to this method does nothing. + * + * @return this + */ + public AuthCredentials markComplete() { + this.complete = true; + return this; + } + + public void addAttribute(String name, String value) { + if(name != null && !name.isEmpty()) { + this.attributes.put(name, value); + } + } + + public Map getAttributes() { + return Collections.unmodifiableMap(this.attributes); + } +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/user/CustomAttributesAware.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/user/CustomAttributesAware.java new file mode 100644 index 0000000000..16d087f8c8 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/user/CustomAttributesAware.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.user; + +import java.util.Map; + +public interface CustomAttributesAware { + + Map getCustomAttributesMap(); +} diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/user/User.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/user/User.java new file mode 100644 index 0000000000..6a47d36bbe --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/user/User.java @@ -0,0 +1,250 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.user; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; + +import com.google.common.collect.Lists; + +/** + * A authenticated user and attributes associated to them (like roles, tenant, custom attributes) + *

+ * Do not subclass from this class! + * + */ +public class User implements Serializable, Writeable, CustomAttributesAware { + + public static final User ANONYMOUS = new User("opendistro_security_anonymous", Lists.newArrayList("opendistro_security_anonymous_backendrole"), null); + + private static final long serialVersionUID = -5500938501822658596L; + private final String name; + private final Set roles = new HashSet(); + private String requestedTenant; + private Map attributes = new HashMap<>(); + private boolean isInjected = false; + + public User(final StreamInput in) throws IOException { + super(); + name = in.readString(); + roles.addAll(in.readList(StreamInput::readString)); + requestedTenant = in.readString(); + attributes = in.readMap(StreamInput::readString, StreamInput::readString); + } + + /** + * Create a new authenticated user + * + * @param name The username (must not be null or empty) + * @param roles Roles of which the user is a member off (maybe null) + * @param customAttributes Custom attributes associated with this (maybe null) + * @throws IllegalArgumentException if name is null or empty + */ + public User(final String name, final Collection roles, final AuthCredentials customAttributes) { + super(); + + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("name must not be null or empty"); + } + + this.name = name; + + if (roles != null) { + this.addRoles(roles); + } + + if(customAttributes != null) { + this.attributes.putAll(customAttributes.getAttributes()); + } + + } + + /** + * Create a new authenticated user without roles and attributes + * + * @param name The username (must not be null or empty) + * @throws IllegalArgumentException if name is null or empty + */ + public User(final String name) { + this(name, null, null); + } + + public final String getName() { + return name; + } + + /** + * + * @return A unmodifiable set of the roles this user is a member of + */ + public final Set getRoles() { + return Collections.unmodifiableSet(roles); + } + + /** + * Associate this user with a role + * + * @param role The role + */ + public final void addRole(final String role) { + this.roles.add(role); + } + + /** + * Associate this user with a set of roles + * + * @param roles The roles + */ + public final void addRoles(final Collection roles) { + if(roles != null) { + this.roles.addAll(roles); + } + } + + /** + * Check if this user is a member of a role + * + * @param role The role + * @return true if this user is a member of the role, false otherwise + */ + public final boolean isUserInRole(final String role) { + return this.roles.contains(role); + } + + /** + * Associate this user with a set of roles + * + * @param roles The roles + */ + public final void addAttributes(final Map attributes) { + if(attributes != null) { + this.attributes.putAll(attributes); + } + } + + public final String getRequestedTenant() { + return requestedTenant; + } + + public final void setRequestedTenant(String requestedTenant) { + this.requestedTenant = requestedTenant; + } + + + public boolean isInjected() { + return isInjected; + } + + public void setInjected(boolean isInjected) { + this.isInjected = isInjected; + } + + public final String toStringWithAttributes() { + return "User [name=" + name + ", roles=" + roles + ", requestedTenant=" + requestedTenant + ", attributes=" + attributes + "]"; + } + + @Override + public final String toString() { + return "User [name=" + name + ", roles=" + roles + ", requestedTenant=" + requestedTenant + "]"; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (name == null ? 0 : name.hashCode()); + return result; + } + + @Override + public final boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final User other = (User) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + + /** + * Copy all roles from another user + * + * @param user The user from which the roles should be copied over + */ + public final void copyRolesFrom(final User user) { + if(user != null) { + this.addRoles(user.getRoles()); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeStringList(new ArrayList(roles)); + out.writeString(requestedTenant); + out.writeMap(attributes, StreamOutput::writeString, StreamOutput::writeString); + } + + /** + * Get the custom attributes associated with this user + * + * @return A modifiable map with all the current custom attributes associated with this user + */ + public synchronized final Map getCustomAttributesMap() { + if(attributes == null) { + attributes = new HashMap<>(); + } + return attributes; + } +} diff --git a/src/main/resources/KEYS b/src/main/resources/KEYS new file mode 100644 index 0000000000..f432ffbe90 --- /dev/null +++ b/src/main/resources/KEYS @@ -0,0 +1,31 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG/MacGPG2 v2.0.27 + +mQENBFVgQXsBCADojqXC0F92Aw/4gC/K9N27H5RtEGAxRJI/VdUgYyQJldOsj8U7 +SSzyRBlHfSPL5tPITnYhN8E/0pbD4cew0Uir8/+OTVfnOKjFprZtqDLMfyd1gg8I +Y/CZuncGZLJ4igK8FRq2WcE22TcKlgAK4ng8BQ6DBhttDzENviJ/auDBOUMZb3Cw +6N2rSMZa/6bWaq9yo1iX+A8GKSH1nl+bdne2yGrOjh5PpAwYUY1kZBwo5HyVvw6q +6uOsugOakd+cMkR/eaxDU1wjF8bR0n2fVE3Vs9uAP5xvTxC+FI4V8wsEiHA/XtIi +bYttuBKqD0d0RxCObonoHoAbHFAha9es58RzABEBAAG0QVNlYXJjaCBHdWFyZCAo +ZmxvcmFndW5uIFVHIC0gU2lnbmluZyBrZXkpIDxpbmZvQHNlYXJjaC1ndWFyZC5j +b20+iQE5BBMBCAAjBQJVYEF7AhsDBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AA +CgkQdQJotlHtsv5+vggAv2LGXPt/Fga5QOB//43CKpM6O3s535e8G+6pMRDklS1A +msf7hK/ftspy3PQM0Y6EketKVZTYzID6Qt4qzFKviXz7sGZd7ZVizsR99Si6L3II +K7JQlLbuuhGuDDRIN5rYDwG0YkzqvKqq1IOzw+Ce5PNRP3ZvYTAgAeI0KWAidbov +ssMsl2iyjz52n35i40/k9Ees/xoPaBS7neB8713diT4vjGkvdCK28bdUM/0i9ysC +gd+tAqcP9WN9QvObI+lf6OenvyfAE93nX07tfRGfJ9uX8EHKyH+hlZtfIXOlddUj +5kSVZX/HghWSi/y3Tye1DGdiHFG70pwYFXdIZKq6tLkBDQRVYEF7AQgAupnETjd1 +QPowocjZH9opy8BlWCeZnwDwQ1FMxXffSpU/nzDlG5FTFN8mXN2tMwhsl2jvofJ0 +3BYAGOixqLgXT+qnjVeshob6hVnz3t0MnQUefAJI92upbNcPf9NEgHoM9D5PV25t +FXRHxDz8qotl8J4O/nQuVtI2gyq4eLVcj1lZHObqAf0oOlZ2zpvrMde7rgrtO8Vd +ktMUYoQhunqh2FiXMvBBGYh38d36VYc7zOjUlpsCxRe6pnN6Mqg8IwmDuQBsarvL +1uuEbhjxipKoUtxo2P+F9q9WuMgKoulv49Q85dQ+1/s0m+dn4OIoLoS6S63wrZ5P +NZmadHxwFNieIQARAQABiQEfBBgBCAAJBQJVYEF7AhsMAAoJEHUCaLZR7bL+nYQI +ALVFRj+sk9hf+oTq/HBGTOoSFMbpLOncLBZq1WQmUQaTNQPkjJ5G9VE58zbJmIJj +XdwTB2HJeVM1YRxvsq8fsS2KuBmBhSsCurQ+wDl8BgOtRp3OuS2v6gRBQRLiqLDS +GGdr0X9m/RwGuXmIzK7FVrvRlg2CLqPql+yW/U1IUeI2LSlauciivbcWpu7H6208 +Us90eOsnsMAY7TXYHgOemko7szfbLH/KAEE80IfRdttSMJy6ZMS/+8aCtVNpdIfN +6TsGd3Ry4WdQh1vOj6tWCm0GAcfNYWqyPaVGQ0GR5rNX4ZISA1WDsHntrbCB4F8W +KLAJNEiQkUkRNiV7RFpzhyU= +=AvUC +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/AggregationTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/AggregationTests.java new file mode 100644 index 0000000000..e44c8f310d --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/AggregationTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import org.apache.http.HttpStatus; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentType; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; + +public class AggregationTests extends SingleClusterTest { + + @Test + public void testBasicAggregations() throws Exception { + final Settings settings = Settings.builder() + .build(); + + setup(settings); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.admin().indices().create(new CreateIndexRequest("copysf")).actionGet(); + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_academy").type("students").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_library").type("public").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("klingonempire").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("public").type("legends").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("spock").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("kirk").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("role01_role02").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("xyz").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("starfleet","starfleet_academy","starfleet_library").alias("sf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("klingonempire","vulcangov").alias("nonsf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("public").alias("unrestricted"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("xyz").alias("alias1"))).actionGet(); + + } + + HttpResponse res; + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executePostRequest("_search?pretty", "{\"size\":0,\"aggs\":{\"indices\":{\"terms\":{\"field\":\"_index\",\"size\":40}}}}",encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + System.out.println(res.getBody()); + assertNotContains(res, "*xception*"); + assertNotContains(res, "*erial*"); + assertNotContains(res, "*mpty*"); + assertNotContains(res, "*pendistro_security*"); + assertContains(res, "*vulcangov*"); + assertContains(res, "*starfleet*"); + assertContains(res, "*klingonempire*"); + assertContains(res, "*xyz*"); + assertContains(res, "*role01_role02*"); + assertContains(res, "*\"failed\" : 0*"); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executePostRequest("*/_search?pretty", "{\"size\":0,\"aggs\":{\"indices\":{\"terms\":{\"field\":\"_index\",\"size\":40}}}}",encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + System.out.println(res.getBody()); + assertNotContains(res, "*xception*"); + assertNotContains(res, "*erial*"); + assertNotContains(res, "*mpty*"); + assertNotContains(res, "*pendistro_security*"); + assertContains(res, "*vulcangov*"); + assertContains(res, "*starfleet*"); + assertContains(res, "*klingonempire*"); + assertContains(res, "*xyz*"); + assertContains(res, "*role01_role02*"); + assertContains(res, "*\"failed\" : 0*"); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executePostRequest("_search?pretty", "{\"size\":0,\"aggs\":{\"indices\":{\"terms\":{\"field\":\"_index\",\"size\":40}}}}",encodeBasicHeader("worf", "worf"))).getStatusCode()); + System.out.println(res.getBody()); + assertNotContains(res, "*xception*"); + assertNotContains(res, "*erial*"); + assertNotContains(res, "*mpty*"); + assertNotContains(res, "*pendistro_security*"); + assertNotContains(res, "*vulcangov*"); + assertNotContains(res, "*kirk*"); + assertNotContains(res, "*starfleet*"); + assertContains(res, "*public*"); + assertContains(res, "*xyz*"); + assertContains(res, "*\"failed\" : 0*"); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executePostRequest("_search?pretty", "{\"size\":0,\"aggs\":{\"myindices\":{\"terms\":{\"field\":\"_index\",\"size\":40}}}}",encodeBasicHeader("worf", "worf"))).getStatusCode()); + + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/AlwaysFalseInterClusterRequestEvaluator.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/AlwaysFalseInterClusterRequestEvaluator.java new file mode 100644 index 0000000000..a07a3b0a09 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/AlwaysFalseInterClusterRequestEvaluator.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import java.security.cert.X509Certificate; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.transport.TransportRequest; + +import com.amazon.opendistroforelasticsearch.security.transport.InterClusterRequestEvaluator; + + +public class AlwaysFalseInterClusterRequestEvaluator implements InterClusterRequestEvaluator { + + public AlwaysFalseInterClusterRequestEvaluator(Settings settings) { + super(); + } + + @Override + public boolean isInterClusterRequest(TransportRequest request, X509Certificate[] localCerts, X509Certificate[] peerCerts, + String principal) { + + if(localCerts == null || peerCerts == null || principal == null + || localCerts.length == 0 || peerCerts.length == 0 || principal.length() == 0) { + return true; + } + + return false; + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/HealthTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/HealthTests.java new file mode 100644 index 0000000000..2d70f4b7c0 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/HealthTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import org.apache.http.HttpStatus; +import org.elasticsearch.common.settings.Settings; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.test.DynamicSecurityConfig; +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; + +public class HealthTests extends SingleClusterTest { + + @Test + public void testHealth() throws Exception { + setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY); + + RestHelper rh = nonSslRestHelper(); + HttpResponse res; + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("_opendistro/_security/health?pretty&mode=lenient")).getStatusCode()); + System.out.println(res.getBody()); + assertContains(res, "*UP*"); + assertNotContains(res, "*DOWN*"); + assertNotContains(res, "*strict*"); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("_opendistro/_security/health?pretty")).getStatusCode()); + System.out.println(res.getBody()); + assertContains(res, "*UP*"); + assertContains(res, "*strict*"); + assertNotContains(res, "*DOWN*"); + } + + @Test + public void testHealthUnitialized() throws Exception { + setup(Settings.EMPTY, null, Settings.EMPTY, false); + + RestHelper rh = nonSslRestHelper(); + HttpResponse res; + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("_opendistro/_security/health?pretty&mode=lenient")).getStatusCode()); + System.out.println(res.getBody()); + assertContains(res, "*UP*"); + assertNotContains(res, "*DOWN*"); + assertNotContains(res, "*strict*"); + + Assert.assertEquals(HttpStatus.SC_SERVICE_UNAVAILABLE, (res = rh.executeGetRequest("_opendistro/_security/health?pretty")).getStatusCode()); + System.out.println(res.getBody()); + assertContains(res, "*DOWN*"); + assertContains(res, "*strict*"); + assertNotContains(res, "*UP*"); + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/HttpIntegrationTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/HttpIntegrationTests.java new file mode 100644 index 0000000000..e8f302a275 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/HttpIntegrationTests.java @@ -0,0 +1,684 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import java.io.File; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.FileUtils; +import org.apache.http.HttpStatus; +import org.apache.http.message.BasicHeader; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentType; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateAction; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateRequest; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateResponse; +import com.amazon.opendistroforelasticsearch.security.configuration.PrivilegesInterceptorImpl; +import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.test.DynamicSecurityConfig; +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.file.FileHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; + +public class HttpIntegrationTests extends SingleClusterTest { + + @Test + public void testHTTPBasic() throws Exception { + final Settings settings = Settings.builder() + .putList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+".worf", "knuddel","nonexists") + .build(); + setup(settings); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.admin().indices().create(new CreateIndexRequest("copysf")).actionGet(); + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_academy").type("students").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_library").type("public").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("klingonempire").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("public").type("legends").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("v2").type("legends").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("v3").type("legends").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("spock").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("kirk").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("role01_role02").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("starfleet","starfleet_academy","starfleet_library").alias("sf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("klingonempire","vulcangov").alias("nonsf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("public").alias("unrestricted"))).actionGet(); + + } + + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("").getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("_search").getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeDeleteRequest("nonexistentindex*", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest(".nonexistentindex*", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePutRequest(".opendistro_security/config/2", "{}",encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, rh.executeGetRequest(".opendistro_security/config/0", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, rh.executeGetRequest("xxxxyyyy/config/0", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("abc", "abc:abc")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("userwithnopassword", "")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("userwithblankpassword", "")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("worf", "wrongpasswd")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", new BasicHeader("Authorization", "Basic "+"wrongheader")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", new BasicHeader("Authorization", "Basic ")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", new BasicHeader("Authorization", "Basic")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", new BasicHeader("Authorization", "")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("picard", "picard")).getStatusCode()); + + for(int i=0; i< 10; i++) { + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("worf", "wrongpasswd")).getStatusCode()); + } + + Assert.assertEquals(HttpStatus.SC_OK, rh.executePutRequest("/theindex","{}",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_CREATED, rh.executePutRequest("/theindex/type/1?refresh=true","{\"a\":0}",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + //Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("/theindex/_analyze?text=this+is+a+test",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + //Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("_analyze?text=this+is+a+test",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeDeleteRequest("/theindex",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeDeleteRequest("/klingonempire",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("starfleet/_search", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("_search", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("starfleet/ships/_search?pretty", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeDeleteRequest(".opendistro_security/", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("/.opendistro_security/_close", null,encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("/.opendistro_security/_upgrade", null,encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePutRequest("/.opendistro_security/_mapping/config","{}",encodeBasicHeader("worf", "worf")).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest(".opendistro_security/", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePutRequest(".opendistro_security/config/2", "{}",encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest(".opendistro_security/config/0",encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeDeleteRequest(".opendistro_security/config/0",encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePutRequest(".opendistro_security/config/0","{}",encodeBasicHeader("worf", "worf")).getStatusCode()); + + HttpResponse resc = rh.executeGetRequest("_cat/indices/public?v",encodeBasicHeader("bug108", "nagilum")); + Assert.assertTrue(resc.getBody().contains("green")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("role01_role02/type01/_search?pretty",encodeBasicHeader("user_role01_role02_role03", "user_role01_role02_role03")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("role01_role02/type01/_search?pretty",encodeBasicHeader("user_role01", "user_role01")).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("spock/type01/_search?pretty",encodeBasicHeader("spock", "spock")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("spock/type01/_search?pretty",encodeBasicHeader("kirk", "kirk")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("kirk/type01/_search?pretty",encodeBasicHeader("kirk", "kirk")).getStatusCode()); + + //all + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePutRequest("_mapping/config","{\"i\" : [\"4\"]}",encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest(".opendistro_security/_mget","{\"ids\" : [\"0\"]}",encodeBasicHeader("worf", "worf")).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("starfleet/ships/_search?pretty", encodeBasicHeader("worf", "worf")).getStatusCode()); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest(".opendistro_security").type("security").id("roles").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("roles", FileHelper.readYamlContent("roles_deny.yml"))).actionGet(); + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"roles"})).actionGet(); + Assert.assertEquals(clusterInfo.numNodes, cur.getNodes().size()); + } + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("starfleet/ships/_search?pretty", encodeBasicHeader("worf", "worf")).getStatusCode()); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest(".opendistro_security").type("security").id("roles").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("roles", FileHelper.readYamlContent("roles.yml"))).actionGet(); + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"roles"})).actionGet(); + Assert.assertEquals(clusterInfo.numNodes, cur.getNodes().size()); + } + + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("starfleet/ships/_search?pretty", encodeBasicHeader("worf", "worf")).getStatusCode()); + HttpResponse res = rh.executeGetRequest("_search?pretty", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("\"total\" : 11")); + Assert.assertTrue(!res.getBody().contains(".opendistro_security")); + + res = rh.executeGetRequest("_nodes/stats?pretty", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("total_in_bytes")); + Assert.assertTrue(res.getBody().contains("max_file_descriptors")); + Assert.assertTrue(res.getBody().contains("buffer_pools")); + Assert.assertFalse(res.getBody().contains("\"nodes\" : { }")); + + res = rh.executePostRequest("*/_upgrade", "", encodeBasicHeader("nagilum", "nagilum")); + System.out.println(res.getBody()); + System.out.println(res.getStatusReason()); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + + String bulkBody = + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"1\" } }"+System.lineSeparator()+ + "{ \"field1\" : \"value1\" }" +System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator(); + + res = rh.executePostRequest("_bulk", bulkBody, encodeBasicHeader("writer", "writer")); + System.out.println(res.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("\"errors\":false")); + Assert.assertTrue(res.getBody().contains("\"status\":201")); + + res = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader("security_tenant", "unittesttenant"), encodeBasicHeader("worf", "worf")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("tenants")); + Assert.assertTrue(res.getBody().contains("unittesttenant")); + Assert.assertTrue(res.getBody().contains("\"kltentrw\":true")); + Assert.assertTrue(res.getBody().contains("\"user_name\":\"worf\"")); + + res = rh.executeGetRequest("_opendistro/_security/authinfo", encodeBasicHeader("worf", "worf")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("tenants")); + Assert.assertTrue(res.getBody().contains("\"user_requested_tenant\":null")); + Assert.assertTrue(res.getBody().contains("\"kltentrw\":true")); + Assert.assertTrue(res.getBody().contains("\"user_name\":\"worf\"")); + Assert.assertTrue(res.getBody().contains("\"custom_attribute_names\":[]")); + Assert.assertFalse(res.getBody().contains("attributes=")); + Assert.assertTrue(PrivilegesInterceptorImpl.count > 0); + + res = rh.executeGetRequest("_opendistro/_security/authinfo?pretty", encodeBasicHeader("custattr", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("tenants")); + Assert.assertTrue(res.getBody().contains("\"user_requested_tenant\" : null")); + Assert.assertTrue(res.getBody().contains("\"user_name\" : \"custattr\"")); + Assert.assertTrue(res.getBody().contains("\"custom_attribute_names\" : [")); + Assert.assertTrue(res.getBody().contains("attr.internal.c3")); + Assert.assertTrue(res.getBody().contains("attr.internal.c1")); + Assert.assertTrue(PrivilegesInterceptorImpl.count > 0); + + res = rh.executeGetRequest("v2/_search", encodeBasicHeader("custattr", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + + res = rh.executeGetRequest("v3/_search", encodeBasicHeader("custattr", "nagilum")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, res.getStatusCode()); + + final String reindex = "{"+ + "\"source\": {"+ + "\"index\": \"starfleet\""+ + "},"+ + "\"dest\": {"+ + "\"index\": \"copysf\""+ + "}"+ + "}"; + + res = rh.executePostRequest("_reindex?pretty", reindex, encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("\"total\" : 1")); + Assert.assertTrue(res.getBody().contains("\"batches\" : 1")); + Assert.assertTrue(res.getBody().contains("\"failures\" : [ ]")); + + //rest impersonation + res = rh.executeGetRequest("/_opendistro/_security/authinfo", new BasicHeader("opendistro_security_impersonate_as","knuddel"), encodeBasicHeader("worf", "worf")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("name=knuddel")); + Assert.assertFalse(res.getBody().contains("worf")); + + res = rh.executeGetRequest("/_opendistro/_security/authinfo", new BasicHeader("opendistro_security_impersonate_as","nonexists"), encodeBasicHeader("worf", "worf")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, res.getStatusCode()); + + res = rh.executeGetRequest("/_opendistro/_security/authinfo", new BasicHeader("opendistro_security_impersonate_as","notallowed"), encodeBasicHeader("worf", "worf")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, res.getStatusCode()); + } + + @Test + public void testHTTPSCompressionEnabled() throws Exception { + final Settings settings = Settings.builder() + .put("opendistro_security.ssl.http.enabled",true) + .put("opendistro_security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-0-keystore.jks")) + .put("opendistro_security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("truststore.jks")) + .put("http.compression",true) + .build(); + setup(Settings.EMPTY, new DynamicSecurityConfig(), settings, true); + final RestHelper rh = restHelper(); //ssl resthelper + + HttpResponse res = rh.executeGetRequest("_opendistro/_security/sslinfo", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + System.out.println(res); + assertContains(res, "*ssl_protocol\":\"TLSv1.2*"); + res = rh.executeGetRequest("_nodes", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + System.out.println(res); + assertNotContains(res, "*\"compression\":\"false\"*"); + assertContains(res, "*\"compression\":\"true\"*"); + } + + @Test + public void testHTTPSCompression() throws Exception { + final Settings settings = Settings.builder() + .put("opendistro_security.ssl.http.enabled",true) + .put("opendistro_security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-0-keystore.jks")) + .put("opendistro_security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("truststore.jks")) + .build(); + setup(Settings.EMPTY, new DynamicSecurityConfig(), settings, true); + final RestHelper rh = restHelper(); //ssl resthelper + + HttpResponse res = rh.executeGetRequest("_opendistro/_security/sslinfo", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + System.out.println(res); + assertContains(res, "*ssl_protocol\":\"TLSv1.2*"); + res = rh.executeGetRequest("_nodes", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + System.out.println(res); + assertContains(res, "*\"compression\":\"false\"*"); + assertNotContains(res, "*\"compression\":\"true\"*"); + } + + @Test + public void testHTTPAnon() throws Exception { + + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_anon.yml"), Settings.EMPTY, true); + + RestHelper rh = nonSslRestHelper(); + + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("").getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("worf", "wrong")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + HttpResponse resc = rh.executeGetRequest("_opendistro/_security/authinfo"); + System.out.println(resc.getBody()); + Assert.assertTrue(resc.getBody().contains("opendistro_security_anonymous")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo?pretty=true"); + System.out.println(resc.getBody()); + Assert.assertTrue(resc.getBody().contains("\"remote_address\" : \"")); //check pretty print + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", encodeBasicHeader("nagilum", "nagilum")); + System.out.println(resc.getBody()); + Assert.assertTrue(resc.getBody().contains("nagilum")); + Assert.assertFalse(resc.getBody().contains("opendistro_security_anonymous")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest(".opendistro_security").type("security").id("config").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("config", FileHelper.readYamlContent("config.yml"))).actionGet(); + tc.index(new IndexRequest(".opendistro_security").type("security").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("internalusers").source("internalusers", FileHelper.readYamlContent("internal_users.yml"))).actionGet(); + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config","roles","rolesmapping","internalusers","actiongroups"})).actionGet(); + Assert.assertEquals(clusterInfo.numNodes, cur.getNodes().size()); + } + + + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("").getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("_opendistro/_security/authinfo").getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("worf", "wrong")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + } + + @Test + public void testHTTPClientCert() throws Exception { + final Settings settings = Settings.builder() + .put("opendistro_security.ssl.http.clientauth_mode","REQUIRE") + .put("opendistro_security.ssl.http.enabled",true) + .put("opendistro_security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-0-keystore.jks")) + .put("opendistro_security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("truststore.jks")) + .putList(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_HTTP_ENABLED_PROTOCOLS, "TLSv1.1","TLSv1.2") + .putList(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_HTTP_ENABLED_CIPHERS, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256") + .putList(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS, "TLSv1.1","TLSv1.2") + .putList(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_ENABLED_CIPHERS, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256") + .build(); + + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_clientcert.yml"), settings, true); + + try (TransportClient tc = getInternalTransportClient()) { + + tc.index(new IndexRequest("vulcangov").type("type").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config","roles","rolesmapping","internalusers","actiongroups"})).actionGet(); + Assert.assertEquals(clusterInfo.numNodes, cur.getNodes().size()); + } + + RestHelper rh = restHelper(); + + rh.enableHTTPClientSSL = true; + rh.trustHTTPServerCertificate = true; + rh.sendHTTPClientCertificate = true; + rh.keystore = "spock-keystore.jks"; + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_search").getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePutRequest(".opendistro_security/security/x", "{}").getStatusCode()); + + rh.keystore = "kirk-keystore.jks"; + Assert.assertEquals(HttpStatus.SC_CREATED, rh.executePutRequest(".opendistro_security/security/y", "{}").getStatusCode()); + HttpResponse res; + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("_opendistro/_security/authinfo")).getStatusCode()); + System.out.println(res.getBody()); + } + + @Test + public void testHTTPPlaintextErrMsg() throws Exception { + + try { + final Settings settings = Settings.builder() + .put("opendistro_security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-0-keystore.jks")) + .put("opendistro_security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("truststore.jks")) + .put("opendistro_security.ssl.http.enabled", true) + .build(); + setup(settings); + RestHelper rh = nonSslRestHelper(); + rh.executeGetRequest("", encodeBasicHeader("worf", "worf")); + Assert.fail(); + } catch (Exception e) { + String log = FileUtils.readFileToString(new File("unittest.log"), StandardCharsets.UTF_8); + Assert.assertTrue(log.contains("speaks http plaintext instead of ssl, will close the channel")); + } + + } + + @Test + public void testHTTPProxyDefault() throws Exception { + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_proxy.yml"), Settings.EMPTY, true); + RestHelper rh = nonSslRestHelper(); + + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("").getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", new BasicHeader("x-forwarded-for", "localhost,192.168.0.1,10.0.0.2"),new BasicHeader("x-proxy-user", "scotty"), encodeBasicHeader("nagilum-wrong", "nagilum-wrong")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", new BasicHeader("x-forwarded-for", "localhost,192.168.0.1,10.0.0.2"),new BasicHeader("x-proxy-user-wrong", "scotty"), encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, rh.executeGetRequest("", new BasicHeader("x-forwarded-for", "a"),new BasicHeader("x-proxy-user", "scotty"), encodeBasicHeader("nagilum-wrong", "nagilum-wrong")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, rh.executeGetRequest("", new BasicHeader("x-forwarded-for", "a,b,c"),new BasicHeader("x-proxy-user", "scotty")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", new BasicHeader("x-forwarded-for", "localhost,192.168.0.1,10.0.0.2"),new BasicHeader("x-proxy-user", "scotty")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", new BasicHeader("x-forwarded-for", "localhost,192.168.0.1,10.0.0.2"),new BasicHeader("X-Proxy-User", "scotty")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", new BasicHeader("x-forwarded-for", "localhost,192.168.0.1,10.0.0.2"),new BasicHeader("x-proxy-user", "scotty"),new BasicHeader("x-proxy-roles", "starfleet,engineer")).getStatusCode()); + + } + + @Test + public void testHTTPProxyRolesSeparator() throws Exception { + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_proxy_custom.yml"), Settings.EMPTY, true); + RestHelper rh = nonSslRestHelper(); + // separator is configured as ";" so separating roles with "," leads to one (wrong) backend role + HttpResponse res = rh.executeGetRequest("/_opendistro/_security/authinfo", new BasicHeader("x-forwarded-for", "localhost,192.168.0.1,10.0.0.2"),new BasicHeader("user", "scotty"),new BasicHeader("roles", "starfleet,engineer")); + Assert.assertTrue("Expected one backend role since separator is incorrect", res.getBody().contains("\"backend_roles\":[\"starfleet,engineer\"]")); + // correct separator, now we should see two backend roles + res = rh.executeGetRequest("/_opendistro/_security/authinfo", new BasicHeader("x-forwarded-for", "localhost,192.168.0.1,10.0.0.2"),new BasicHeader("user", "scotty"),new BasicHeader("roles", "starfleet;engineer")); + Assert.assertTrue("Expected two backend roles string since separator is correct: " + res.getBody(), res.getBody().contains("\"backend_roles\":[\"starfleet\",\"engineer\"]")); + + } + + @Test + public void testHTTPBasic2() throws Exception { + + setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY); + + try (TransportClient tc = getInternalTransportClient(this.clusterInfo, Settings.EMPTY)) { + + tc.admin().indices().create(new CreateIndexRequest("copysf")).actionGet(); + + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("starfleet_academy").type("students").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("starfleet_library").type("public").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("klingonempire").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("public").type("legends").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("spock").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("kirk").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("role01_role02").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("starfleet","starfleet_academy","starfleet_library").alias("sf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("klingonempire","vulcangov").alias("nonsf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("public").alias("unrestricted"))).actionGet(); + } + + RestHelper rh = nonSslRestHelper(); + + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("").getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeDeleteRequest("nonexistentindex*", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest(".nonexistentindex*", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePutRequest(".opendistro_security/config/2", "{}",encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, rh.executeGetRequest(".opendistro_security/config/0", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, rh.executeGetRequest("xxxxyyyy/config/0", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("abc", "abc:abc")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("userwithnopassword", "")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("userwithblankpassword", "")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("worf", "wrongpasswd")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", new BasicHeader("Authorization", "Basic "+"wrongheader")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", new BasicHeader("Authorization", "Basic ")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", new BasicHeader("Authorization", "Basic")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", new BasicHeader("Authorization", "")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("picard", "picard")).getStatusCode()); + + for(int i=0; i< 10; i++) { + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("worf", "wrongpasswd")).getStatusCode()); + } + + Assert.assertEquals(HttpStatus.SC_OK, rh.executePutRequest("/theindex","{}",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_CREATED, rh.executePutRequest("/theindex/type/1?refresh=true","{\"a\":0}",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + //Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("/theindex/_analyze?text=this+is+a+test",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + //Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("_analyze?text=this+is+a+test",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeDeleteRequest("/theindex",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeDeleteRequest("/klingonempire",encodeBasicHeader("theindexadmin", "theindexadmin")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("starfleet/_search", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("_search", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("starfleet/ships/_search?pretty", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeDeleteRequest(".opendistro_security/", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("/.opendistro_security/_close", null,encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("/.opendistro_security/_upgrade", null,encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePutRequest("/.opendistro_security/_mapping/config","{}",encodeBasicHeader("worf", "worf")).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest(".opendistro_security/", encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePutRequest(".opendistro_security/config/2", "{}",encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest(".opendistro_security/config/0",encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeDeleteRequest(".opendistro_security/config/0",encodeBasicHeader("worf", "worf")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePutRequest(".opendistro_security/config/0","{}",encodeBasicHeader("worf", "worf")).getStatusCode()); + + HttpResponse resc = rh.executeGetRequest("_cat/indices/public",encodeBasicHeader("bug108", "nagilum")); + System.out.println(resc.getBody()); + //Assert.assertTrue(resc.getBody().contains("green")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("role01_role02/type01/_search?pretty",encodeBasicHeader("user_role01_role02_role03", "user_role01_role02_role03")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("role01_role02/type01/_search?pretty",encodeBasicHeader("user_role01", "user_role01")).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("spock/type01/_search?pretty",encodeBasicHeader("spock", "spock")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("spock/type01/_search?pretty",encodeBasicHeader("kirk", "kirk")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("kirk/type01/_search?pretty",encodeBasicHeader("kirk", "kirk")).getStatusCode()); + + System.out.println("ok"); + //all + + + } + + @Test + public void testBulk() throws Exception { + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .build(); + setup(Settings.EMPTY, new DynamicSecurityConfig().setSecurityRoles("roles_bulk.yml"), settings); + final RestHelper rh = nonSslRestHelper(); + + String bulkBody = + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"1\" } }"+System.lineSeparator()+ + "{ \"field1\" : \"value1\" }" +System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator(); + + HttpResponse res = rh.executePostRequest("_bulk", bulkBody, encodeBasicHeader("bulk", "nagilum")); + System.out.println(res.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("\"errors\":false")); + Assert.assertTrue(res.getBody().contains("\"status\":201")); + } + + @Test + public void test557() throws Exception { + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .build(); + setup(Settings.EMPTY, new DynamicSecurityConfig(), settings); + + try (TransportClient tc = getInternalTransportClient(this.clusterInfo, Settings.EMPTY)) { + + tc.admin().indices().create(new CreateIndexRequest("copysf")).actionGet(); + + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("starfleet_academy").type("students").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + } + + final RestHelper rh = nonSslRestHelper(); + + HttpResponse res = rh.executePostRequest("/*/_search", "{\"size\":0,\"aggs\":{\"indices\":{\"terms\":{\"field\":\"_index\",\"size\":10}}}}", encodeBasicHeader("nagilum", "nagilum")); + System.out.println(res.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("starfleet_academy")); + res = rh.executePostRequest("/*/_search", "{\"size\":0,\"aggs\":{\"indices\":{\"terms\":{\"field\":\"_index\",\"size\":10}}}}", encodeBasicHeader("557", "nagilum")); + System.out.println(res.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("starfleet_academy")); + } + + @Test + public void testITT1635() throws Exception { + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .build(); + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_dnfof.yml").setSecurityRoles("roles_itt1635.yml"), settings); + + try (TransportClient tc = getInternalTransportClient(this.clusterInfo, Settings.EMPTY)) { + + tc.index(new IndexRequest("esb-prod-1").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("esb-prod-2").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":2}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("esb-prod-3").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":3}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("esb-prod-4").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":4}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("esb-prod-5").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":5}", XContentType.JSON)).actionGet(); + + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("esb-prod-1","esb-prod-2","esb-prod-3","esb-prod-4","esb-prod-5").alias("esb-prod-all"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("esb-prod-1").alias("esb-alias-1"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("esb-prod-2").alias("esb-alias-2"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("esb-prod-3").alias("esb-alias-3"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("esb-prod-4").alias("esb-alias-4"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("esb-prod-5").alias("esb-alias-5"))).actionGet(); + + } + + final RestHelper rh = nonSslRestHelper(); + + System.out.println("###1"); + HttpResponse res = rh.executeGetRequest("/esb-prod-*/_search?pretty", encodeBasicHeader("itt1635", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + System.out.println("###2"); + res = rh.executeGetRequest("/esb-alias-*/_search?pretty", encodeBasicHeader("itt1635", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + System.out.println("###3"); + res = rh.executeGetRequest("/esb-prod-all/_search?pretty", encodeBasicHeader("itt1635", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + } + + @Test + public void testTenantInfo() throws Exception { + final Settings settings = Settings.builder() + .build(); + setup(Settings.EMPTY, new DynamicSecurityConfig(), settings); + + /* + + [admin_1, praxisrw, abcdef_2_2, kltentro, praxisro, kltentrw] + admin_1==.kibana_-1139640511_admin1 + praxisrw==.kibana_-1386441176_praxisrw + abcdef_2_2==.kibana_-634608247_abcdef22 + kltentro==.kibana_-2014056171_kltentro + praxisro==.kibana_-1386441184_praxisro + kltentrw==.kibana_-2014056163_kltentrw + + */ + + try (TransportClient tc = getInternalTransportClient(this.clusterInfo, Settings.EMPTY)) { + + tc.index(new IndexRequest(".kibana-6").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":2}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest(".kibana_-1139640511_admin1").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":3}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest(".kibana_-1386441176_praxisrw").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":3}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest(".kibana_-634608247_abcdef22").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":3}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest(".kibana_-12345_123456").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":3}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest(".kibana2_-12345_123456").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":3}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest(".kibana_9876_xxx_ccc").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":3}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest(".kibana_fff_eee").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":3}", XContentType.JSON)).actionGet(); + + + tc.index(new IndexRequest("esb-prod-5").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":5}", XContentType.JSON)).actionGet(); + + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices(".kibana-6").alias(".kibana"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("esb-prod-5").alias(".kibana_-2014056163_kltentrw"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("esb-prod-5").alias("esb-alias-5"))).actionGet(); + + } + + final RestHelper rh = nonSslRestHelper(); + + HttpResponse res = rh.executeGetRequest("/_opendistro/_security/tenantinfo?pretty", encodeBasicHeader("itt1635", "nagilum")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, res.getStatusCode()); + + res = rh.executeGetRequest("/_opendistro/_security/tenantinfo?pretty", encodeBasicHeader("kibanaserver", "kibanaserver")); + System.out.println(res.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("\".kibana_-1139640511_admin1\" : \"admin_1\"")); + Assert.assertTrue(res.getBody().contains("\".kibana_-1386441176_praxisrw\" : \"praxisrw\"")); + Assert.assertTrue(res.getBody().contains(".kibana_-2014056163_kltentrw\" : \"kltentrw\"")); + Assert.assertTrue(res.getBody().contains("\".kibana_-634608247_abcdef22\" : \"abcdef_2_2\"")); + Assert.assertTrue(res.getBody().contains("\".kibana_-12345_123456\" : \"__private__\"")); + Assert.assertFalse(res.getBody().contains(".kibana-6")); + Assert.assertFalse(res.getBody().contains("esb-")); + Assert.assertFalse(res.getBody().contains("xxx")); + Assert.assertFalse(res.getBody().contains(".kibana2")); + } + + @Test + public void testRestImpersonation() throws Exception { + final Settings settings = Settings.builder() + .putList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+".worf", "someotherusernotininternalusersfile") + .build(); + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_rest_impersonation.yml"), settings); + final RestHelper rh = nonSslRestHelper(); + + //rest impersonation + HttpResponse res = rh.executeGetRequest("/_opendistro/_security/authinfo", new BasicHeader("opendistro_security_impersonate_as","someotherusernotininternalusersfile"), encodeBasicHeader("worf", "worf")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("name=someotherusernotininternalusersfile")); + Assert.assertFalse(res.getBody().contains("worf")); + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/IndexIntegrationTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/IndexIntegrationTests.java new file mode 100644 index 0000000000..886be48a82 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/IndexIntegrationTests.java @@ -0,0 +1,529 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +import org.apache.http.HttpStatus; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.indices.InvalidIndexNameException; +import org.elasticsearch.indices.InvalidTypeNameException; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.test.DynamicSecurityConfig; +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; + +public class IndexIntegrationTests extends SingleClusterTest { + + @Test + public void testComposite() throws Exception { + + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("composite_config.yml").setSecurityRoles("roles_composite.yml"), Settings.EMPTY, true); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("klingonempire").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("public").type("legends").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + } + + String msearchBody = + "{\"index\":\"starfleet\", \"type\":\"ships\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator()+ + "{\"index\":\"klingonempire\", \"type\":\"ships\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator()+ + "{\"index\":\"public\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator(); + + + HttpResponse resc = rh.executePostRequest("_msearch", msearchBody, encodeBasicHeader("worf", "worf")); + Assert.assertEquals(200, resc.getStatusCode()); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("\"_index\":\"klingonempire\"")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("hits")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("no permissions for [indices:data/read/search]")); + + } + + @Test + public void testBulkShards() throws Exception { + + setup(Settings.EMPTY, new DynamicSecurityConfig().setSecurityRoles("roles_bs.yml"), Settings.EMPTY, true); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + //create indices and mapping upfront + tc.index(new IndexRequest("test").type("type1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"field2\":\"init\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("lorem").type("type1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"field2\":\"init\"}", XContentType.JSON)).actionGet(); + } + + String bulkBody = + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"1\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value1\" }" +System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"3\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"4\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"5\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"lorem\", \"_type\" : \"type1\", \"_id\" : \"1\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"lorem\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"lorem\", \"_type\" : \"type1\", \"_id\" : \"3\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"lorem\", \"_type\" : \"type1\", \"_id\" : \"4\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"lorem\", \"_type\" : \"type1\", \"_id\" : \"5\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"delete\" : { \"_index\" : \"lorem\", \"_type\" : \"type1\", \"_id\" : \"5\" } }"+System.lineSeparator(); + + System.out.println("############ _bulk"); + HttpResponse res = rh.executePostRequest("_bulk?refresh=true&pretty=true", bulkBody, encodeBasicHeader("worf", "worf")); + System.out.println(res.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("\"errors\" : true")); + Assert.assertTrue(res.getBody().contains("\"status\" : 201")); + Assert.assertTrue(res.getBody().contains("no permissions for")); + + System.out.println("############ check shards"); + System.out.println(rh.executeGetRequest("_cat/shards?v", encodeBasicHeader("nagilum", "nagilum"))); + + + } + + @Test + public void testCreateIndex() throws Exception { + + setup(); + RestHelper rh = nonSslRestHelper(); + + HttpResponse res; + Assert.assertEquals("Unable to create index 'nag'", HttpStatus.SC_OK, rh.executePutRequest("nag1", null, encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals("Unable to create index 'starfleet_library'", HttpStatus.SC_OK, rh.executePutRequest("starfleet_library", null, encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + clusterHelper.waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), clusterInfo.numNodes); + + Assert.assertEquals("Unable to close index 'starfleet_library'", HttpStatus.SC_OK, rh.executePostRequest("starfleet_library/_close", null, encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + Assert.assertEquals("Unable to open index 'starfleet_library'", HttpStatus.SC_OK, (res = rh.executePostRequest("starfleet_library/_open", null, encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + Assert.assertTrue("open index 'starfleet_library' not acknowledged", res.getBody().contains("acknowledged")); + Assert.assertFalse("open index 'starfleet_library' not acknowledged", res.getBody().contains("false")); + + clusterHelper.waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), clusterInfo.numNodes); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePutRequest("public", null, encodeBasicHeader("spock", "spock")).getStatusCode()); + + + } + + @Test + public void testFilteredAlias() throws Exception { + + setup(); + + try (TransportClient tc = getInternalTransportClient()) { + + tc.index(new IndexRequest("theindex").type("type1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("otherindex").type("type1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().alias("alias1").filter(QueryBuilders.termQuery("_type", "type1")).index("theindex"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().alias("alias2").filter(QueryBuilders.termQuery("_type", "type2")).index("theindex"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().alias("alias3").filter(QueryBuilders.termQuery("_type", "type2")).index("otherindex"))).actionGet(); + } + + + RestHelper rh = nonSslRestHelper(); + + //opendistro_security_user1 -> worf + //opendistro_security_user2 -> picard + + HttpResponse resc = rh.executeGetRequest("alias*/_search", encodeBasicHeader("worf", "worf")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, resc.getStatusCode()); + + resc = rh.executeGetRequest("theindex/_search", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, resc.getStatusCode()); + + resc = rh.executeGetRequest("alias3/_search", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + + resc = rh.executeGetRequest("_cat/indices", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + + } + + @Test + public void testIndexTypeEvaluation() throws Exception { + + setup(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("foo1").type("bar").id("1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("foo2").type("bar").id("2").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":2}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("foo").type("baz").id("3").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":3}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("fooba").type("z").id("4").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":4}", XContentType.JSON)).actionGet(); + + try { + tc.index(new IndexRequest("x#a").type("xxx").id("4a").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":4}", XContentType.JSON)).actionGet(); + Assert.fail("Indexname can contain #"); + } catch (InvalidIndexNameException e) { + //expected + } + + + try { + tc.index(new IndexRequest("xa").type("x#a").id("4a").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":4}", XContentType.JSON)).actionGet(); + Assert.fail("Typename can contain #"); + } catch (InvalidTypeNameException e) { + //expected + } + } + + RestHelper rh = nonSslRestHelper(); + + HttpResponse resc = rh.executeGetRequest("/foo1/bar/_search?pretty", encodeBasicHeader("baz", "worf")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("\"content\" : 1")); + + resc = rh.executeGetRequest("/foo2/bar/_search?pretty", encodeBasicHeader("baz", "worf")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("\"content\" : 2")); + + resc = rh.executeGetRequest("/foo/baz/_search?pretty", encodeBasicHeader("baz", "worf")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("\"content\" : 3")); + + resc = rh.executeGetRequest("/fooba/z/_search?pretty", encodeBasicHeader("baz", "worf")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, resc.getStatusCode()); + + resc = rh.executeGetRequest("/foo1/bar/1?pretty", encodeBasicHeader("baz", "worf")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("\"found\" : true")); + Assert.assertTrue(resc.getBody().contains("\"content\" : 1")); + + resc = rh.executeGetRequest("/foo2/bar/2?pretty", encodeBasicHeader("baz", "worf")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("\"content\" : 2")); + Assert.assertTrue(resc.getBody().contains("\"found\" : true")); + + resc = rh.executeGetRequest("/foo/baz/3?pretty", encodeBasicHeader("baz", "worf")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("\"content\" : 3")); + Assert.assertTrue(resc.getBody().contains("\"found\" : true")); + + resc = rh.executeGetRequest("/fooba/z/4?pretty", encodeBasicHeader("baz", "worf")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, resc.getStatusCode()); + + resc = rh.executeGetRequest("/foo*/_search?pretty", encodeBasicHeader("baz", "worf")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, resc.getStatusCode()); + + resc = rh.executeGetRequest("/foo*,-fooba/bar/_search?pretty", encodeBasicHeader("baz", "worf")); + Assert.assertEquals(200, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("\"content\" : 1")); + Assert.assertTrue(resc.getBody().contains("\"content\" : 2")); + } + + @Test + public void testIndices() throws Exception { + + setup(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("nopermindex").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("logstash-1").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("logstash-2").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("logstash-3").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("logstash-4").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + SimpleDateFormat sdf = new SimpleDateFormat("YYYY.MM.dd"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + + String date = sdf.format(new Date()); + tc.index(new IndexRequest("logstash-"+date).type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + } + + RestHelper rh = nonSslRestHelper(); + + HttpResponse res = null; + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("/logstash-1/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + //nonexistent index with permissions + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, (res = rh.executeGetRequest("/logstash-nonex/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + //existent index without permissions + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("/nopermindex/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + //nonexistent index without permissions + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("/does-not-exist-and-no-perm/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + //existent index with permissions + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("/logstash-1/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + //nonexistent index with failed login + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, (res = rh.executeGetRequest("/logstash-nonex/_search", encodeBasicHeader("nouser", "nosuer"))).getStatusCode()); + + //nonexistent index with no login + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, (res = rh.executeGetRequest("/logstash-nonex/_search")).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("/_all/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("/*/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("/nopermindex,logstash-1,nonexist/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("/logstash-1,nonexist/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("/nonexist/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("/%3Clogstash-%7Bnow%2Fd%7D%3E/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("/%3Cnonex-%7Bnow%2Fd%7D%3E/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("/%3Clogstash-%7Bnow%2Fd%7D%3E,logstash-*/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("/%3Clogstash-%7Bnow%2Fd%7D%3E,logstash-1/_search", encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_CREATED, (res = rh.executePutRequest("/logstash-b/logs/1", "{}",encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executePutRequest("/%3Clogstash-cnew-%7Bnow%2Fd%7D%3E", "{}",encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_CREATED, (res = rh.executePutRequest("/%3Clogstash-new-%7Bnow%2Fd%7D%3E/logs/1", "{}",encodeBasicHeader("logstash", "nagilum"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("/_cat/indices?v" ,encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + + System.out.println(res.getBody()); + Assert.assertTrue(res.getBody().contains("logstash-b")); + Assert.assertTrue(res.getBody().contains("logstash-new-20")); + Assert.assertTrue(res.getBody().contains("logstash-cnew-20")); + Assert.assertFalse(res.getBody().contains("<")); + } + + @Test + public void testAliases() throws Exception { + + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .build(); + + setup(settings); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("nopermindex").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("logstash-1").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("logstash-2").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("logstash-3").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("logstash-4").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("logstash-5").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("logstash-del").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("logstash-del-ok").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + String date = new SimpleDateFormat("YYYY.MM.dd").format(new Date()); + tc.index(new IndexRequest("logstash-"+date).type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("nopermindex").alias("nopermalias"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices(".opendistro_security").alias("mysgi"))).actionGet(); + } + + RestHelper rh = nonSslRestHelper(); + + HttpResponse res = null; + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executePostRequest("/mysgi/sg", "{}",encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("/mysgi/_search?pretty", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + assertContains(res, "*\"hits\" : {*\"total\" : 0,*\"hits\" : [ ]*"); + + System.out.println("#### add alias to allowed index"); + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executePutRequest("/logstash-1/_alias/alog1", "",encodeBasicHeader("aliasmngt", "nagilum"))).getStatusCode()); + + System.out.println("#### add alias to not existing (no perm)"); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executePutRequest("/nonexitent/_alias/alnp", "",encodeBasicHeader("aliasmngt", "nagilum"))).getStatusCode()); + + System.out.println("#### add alias to not existing (with perm)"); + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, (res = rh.executePutRequest("/logstash-nonex/_alias/alnp", "",encodeBasicHeader("aliasmngt", "nagilum"))).getStatusCode()); + + System.out.println("#### add alias to not allowed index"); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executePutRequest("/nopermindex/_alias/alnp", "",encodeBasicHeader("aliasmngt", "nagilum"))).getStatusCode()); + + String aliasRemoveIndex = "{"+ + "\"actions\" : ["+ + "{ \"add\": { \"index\": \"logstash-del-ok\", \"alias\": \"logstash-del\" } },"+ + "{ \"remove_index\": { \"index\": \"logstash-del\" } } "+ + "]"+ + "}"; + + System.out.println("#### remove_index"); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executePostRequest("/_aliases", aliasRemoveIndex,encodeBasicHeader("aliasmngt", "nagilum"))).getStatusCode()); + + + System.out.println("#### get alias for permitted index"); + Assert.assertEquals(HttpStatus.SC_OK, (res = rh.executeGetRequest("/logstash-1/_alias/alog1", encodeBasicHeader("aliasmngt", "nagilum"))).getStatusCode()); + + + System.out.println("#### get alias for all indices"); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("/_alias/alog1", encodeBasicHeader("aliasmngt", "nagilum"))).getStatusCode()); + + + System.out.println("#### get alias no perm"); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executeGetRequest("/_alias/nopermalias", encodeBasicHeader("aliasmngt", "nagilum"))).getStatusCode()); + + String alias = + "{"+ + "\"aliases\": {"+ + "\"alias1\": {}"+ + "}"+ + "}"; + + + System.out.println("#### create alias along with index"); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (res = rh.executePutRequest("/beats-withalias", alias,encodeBasicHeader("aliasmngt", "nagilum"))).getStatusCode()); + } + + @Test + public void testAliasResolution() throws Exception { + + final Settings settings = Settings.builder() + .build(); + setup(settings); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("concreteindex-1").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("concreteindex-1").alias("calias-1"))).actionGet(); + tc.index(new IndexRequest(".kibana-6").type("doc").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices(".kibana-6").alias(".kibana"))).actionGet(); + + } + + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("calias-1/_search?pretty", encodeBasicHeader("aliastest", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("calias-*/_search?pretty", encodeBasicHeader("aliastest", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("*kibana/_search?pretty", encodeBasicHeader("aliastest", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest(".ki*ana/_search?pretty", encodeBasicHeader("aliastest", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest(".kibana/_search?pretty", encodeBasicHeader("aliastest", "nagilum")).getStatusCode()); + } + + @Test + public void testCCSIndexResolve() throws Exception { + + setup(); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest(".abc-6").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + } + + HttpResponse res = rh.executeGetRequest("/*:.abc-6,.abc-6/_search", encodeBasicHeader("ccsresolv", "nagilum")); + Assert.assertTrue(res.getBody(),res.getBody().contains("\"content\":1")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + } + + @Test + @Ignore + public void testCCSIndexResolve2() throws Exception { + + setup(); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest(".abc").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("xyz").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":2}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("noperm").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":3}", XContentType.JSON)).actionGet(); + + } + + HttpResponse res = rh.executeGetRequest("/*:.abc,.abc/_search", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody(),res.getBody().contains("\"content\":1")); + + res = rh.executeGetRequest("/ba*bcuzh/_search", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertTrue(res.getBody(),res.getBody().contains("\"content\":12")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + + res = rh.executeGetRequest("/*:.abc/_search", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertTrue(res.getBody(),res.getBody().contains("\"content\":1")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + + res = rh.executeGetRequest("/*:xyz,xyz/_search", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody(),res.getBody().contains("\"content\":2")); + + //res = rh.executeGetRequest("/*noexist/_search", encodeBasicHeader("nagilum", "nagilum")); + //Assert.assertEquals(HttpStatus.SC_NOT_FOUND, res.getStatusCode()); + + res = rh.executeGetRequest("/*:.abc/_search", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody(),res.getBody().contains("\"content\":1")); + + res = rh.executeGetRequest("/*:xyz/_search", encodeBasicHeader("nagilum", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody(),res.getBody().contains("\"content\":2")); + + res = rh.executeGetRequest("/.abc/_search", encodeBasicHeader("ccsresolv", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + res = rh.executeGetRequest("/xyz/_search", encodeBasicHeader("ccsresolv", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + res = rh.executeGetRequest("/*:.abc,.abc/_search", encodeBasicHeader("ccsresolv", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + res = rh.executeGetRequest("/*:xyz,xyz/_search", encodeBasicHeader("ccsresolv", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + res = rh.executeGetRequest("/*:.abc/_search", encodeBasicHeader("ccsresolv", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + res = rh.executeGetRequest("/*:xyz/_search", encodeBasicHeader("ccsresolv", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + res = rh.executeGetRequest("/*:noperm/_search", encodeBasicHeader("ccsresolv", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + res = rh.executeGetRequest("/*:noperm/_search", encodeBasicHeader("ccsresolv", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + System.out.println(res.getBody()); + res = rh.executeGetRequest("/*:noexists/_search", encodeBasicHeader("ccsresolv", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + System.out.println(res.getBody()); + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/InitializationIntegrationTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/InitializationIntegrationTests.java new file mode 100644 index 0000000000..43710ceeb1 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/InitializationIntegrationTests.java @@ -0,0 +1,206 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import java.io.File; +import java.util.Iterator; + +import org.apache.http.Header; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.HttpGet; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateAction; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateRequest; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateResponse; +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIAction; +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIRequest; +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIResponse; +import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.test.DynamicSecurityConfig; +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.file.FileHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; + +public class InitializationIntegrationTests extends SingleClusterTest { + + @Test + public void testEnsureInitViaRestDoesWork() throws Exception { + + final Settings settings = Settings.builder() + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_HTTP_CLIENTAUTH_MODE, "REQUIRE") + .put("opendistro_security.ssl.http.enabled",true) + .put("opendistro_security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-0-keystore.jks")) + .put("opendistro_security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("truststore.jks")) + .build(); + setup(Settings.EMPTY, null, settings, false); + final RestHelper rh = restHelper(); //ssl resthelper + + rh.enableHTTPClientSSL = true; + rh.trustHTTPServerCertificate = true; + rh.sendHTTPClientCertificate = true; + Assert.assertEquals(HttpStatus.SC_SERVICE_UNAVAILABLE, rh.executePutRequest(".opendistro_security/config/0", "{}", encodeBasicHeader("___", "")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_SERVICE_UNAVAILABLE, rh.executePutRequest(".opendistro_security/sg/config", "{}", encodeBasicHeader("___", "")).getStatusCode()); + + + rh.keystore = "kirk-keystore.jks"; + Assert.assertEquals(HttpStatus.SC_CREATED, rh.executePutRequest(".opendistro_security/sg/config", "{}", encodeBasicHeader("___", "")).getStatusCode()); + + Assert.assertFalse(rh.executeSimpleRequest("_nodes/stats?pretty").contains("\"tx_size_in_bytes\" : 0")); + Assert.assertFalse(rh.executeSimpleRequest("_nodes/stats?pretty").contains("\"rx_count\" : 0")); + Assert.assertFalse(rh.executeSimpleRequest("_nodes/stats?pretty").contains("\"rx_size_in_bytes\" : 0")); + Assert.assertFalse(rh.executeSimpleRequest("_nodes/stats?pretty").contains("\"tx_count\" : 0")); + + } + + @Test + public void testWhoAmI() throws Exception { + setup(Settings.EMPTY, new DynamicSecurityConfig().setSecurityInternalUsers("internal_empty.yml") + .setSecurityRoles("roles_deny.yml"), Settings.EMPTY, true); + + try (TransportClient tc = getUserTransportClient(clusterInfo, "spock-keystore.jks", Settings.EMPTY)) { + WhoAmIResponse wres = tc.execute(WhoAmIAction.INSTANCE, new WhoAmIRequest()).actionGet(); + System.out.println(wres); + Assert.assertEquals(wres.toString(), "CN=spock,OU=client,O=client,L=Test,C=DE", wres.getDn()); + Assert.assertFalse(wres.toString(), wres.isAdmin()); + Assert.assertFalse(wres.toString(), wres.isAuthenticated()); + Assert.assertFalse(wres.toString(), wres.isNodeCertificateRequest()); + + } + + try (TransportClient tc = getUserTransportClient(clusterInfo, "node-0-keystore.jks", Settings.EMPTY)) { + WhoAmIResponse wres = tc.execute(WhoAmIAction.INSTANCE, new WhoAmIRequest()).actionGet(); + System.out.println(wres); + Assert.assertEquals(wres.toString(), "CN=node-0.example.com,OU=SSL,O=Test,L=Test,C=DE", wres.getDn()); + Assert.assertFalse(wres.toString(), wres.isAdmin()); + Assert.assertFalse(wres.toString(), wres.isAuthenticated()); + Assert.assertTrue(wres.toString(), wres.isNodeCertificateRequest()); + + } + } + + @Test + public void testConfigHotReload() throws Exception { + + setup(); + RestHelper rh = nonSslRestHelper(); + Header spock = encodeBasicHeader("spock", "spock"); + + for (Iterator iterator = clusterInfo.httpAdresses.iterator(); iterator.hasNext();) { + TransportAddress TransportAddress = (TransportAddress) iterator.next(); + HttpResponse res = rh.executeRequest(new HttpGet("http://"+TransportAddress.getAddress()+":"+TransportAddress.getPort() + "/" + "_opendistro/_security/authinfo?pretty=true"), spock); + Assert.assertTrue(res.getBody().contains("spock")); + Assert.assertFalse(res.getBody().contains("additionalrole")); + Assert.assertTrue(res.getBody().contains("vulcan")); + } + + try (TransportClient tc = getInternalTransportClient()) { + Assert.assertEquals(clusterInfo.numNodes, tc.admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet().getNodes().size()); + tc.index(new IndexRequest(".opendistro_security").type("security").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("internalusers").source("internalusers", FileHelper.readYamlContent("internal_users_spock_add_roles.yml"))).actionGet(); + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config","roles","rolesmapping","internalusers","actiongroups"})).actionGet(); + Assert.assertEquals(clusterInfo.numNodes, cur.getNodes().size()); + } + + for (Iterator iterator = clusterInfo.httpAdresses.iterator(); iterator.hasNext();) { + TransportAddress TransportAddress = (TransportAddress) iterator.next(); + log.debug("http://"+TransportAddress.getAddress()+":"+TransportAddress.getPort()); + HttpResponse res = rh.executeRequest(new HttpGet("http://"+TransportAddress.getAddress()+":"+TransportAddress.getPort() + "/" + "_opendistro/_security/authinfo?pretty=true"), spock); + Assert.assertTrue(res.getBody().contains("spock")); + Assert.assertTrue(res.getBody().contains("additionalrole1")); + Assert.assertTrue(res.getBody().contains("additionalrole2")); + Assert.assertFalse(res.getBody().contains("starfleet")); + } + + try (TransportClient tc = getInternalTransportClient()) { + Assert.assertEquals(clusterInfo.numNodes, tc.admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet().getNodes().size()); + tc.index(new IndexRequest(".opendistro_security").type("security").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("config").source("config", FileHelper.readYamlContent("config_anon.yml"))).actionGet(); + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config"})).actionGet(); + Assert.assertEquals(clusterInfo.numNodes, cur.getNodes().size()); + } + + for (Iterator iterator = clusterInfo.httpAdresses.iterator(); iterator.hasNext();) { + TransportAddress TransportAddress = (TransportAddress) iterator.next(); + HttpResponse res = rh.executeRequest(new HttpGet("http://"+TransportAddress.getAddress()+":"+TransportAddress.getPort() + "/" + "_opendistro/_security/authinfo?pretty=true")); + log.debug(res.getBody()); + Assert.assertTrue(res.getBody().contains("opendistro_security_role_host1")); + Assert.assertTrue(res.getBody().contains("opendistro_security_anonymous")); + Assert.assertTrue(res.getBody().contains("name=opendistro_security_anonymous")); + Assert.assertTrue(res.getBody().contains("roles=[opendistro_security_anonymous_backendrole]")); + Assert.assertEquals(200, res.getStatusCode()); + } + } + + @Test + public void testDefaultConfig() throws Exception { + + System.setProperty("security.default_init.dir", new File("./securityconfig").getAbsolutePath()); + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true) + .build(); + setup(Settings.EMPTY, null, settings, false); + RestHelper rh = nonSslRestHelper(); + Thread.sleep(10000); + + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("admin", "admin")).getStatusCode()); + } + + @Test + public void testDisabled() throws Exception { + + final Settings settings = Settings.builder().put("opendistro_security.disabled", true).build(); + + setup(Settings.EMPTY, null, settings, false); + RestHelper rh = nonSslRestHelper(); + + HttpResponse resc = rh.executeGetRequest("_search"); + Assert.assertEquals(200, resc.getStatusCode()); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("hits")); + } + + @Test + public void testDiscoveryWithoutInitialization() throws Exception { + setup(Settings.EMPTY, null, Settings.EMPTY, false); + Assert.assertEquals(clusterInfo.numNodes, clusterHelper.nodeClient().admin().cluster().health(new ClusterHealthRequest().waitForGreenStatus()).actionGet().getNumberOfNodes()); + Assert.assertEquals(ClusterHealthStatus.GREEN, clusterHelper.nodeClient().admin().cluster().health(new ClusterHealthRequest().waitForGreenStatus()).actionGet().getStatus()); + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/IntegrationTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/IntegrationTests.java new file mode 100644 index 0000000000..3c7cfd3985 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/IntegrationTests.java @@ -0,0 +1,836 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import io.netty.handler.ssl.OpenSsl; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.TreeSet; + +import org.apache.http.HttpStatus; +import org.apache.http.message.BasicHeader; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.admin.cluster.reroute.ClusterRerouteRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.XContentType; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateAction; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateRequest; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateResponse; +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIAction; +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIRequest; +import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIResponse; +import com.amazon.opendistroforelasticsearch.security.http.HTTPClientCertAuthenticator; +import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.test.DynamicSecurityConfig; +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.file.FileHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; + +public class IntegrationTests extends SingleClusterTest { + + @Test + public void testSearchScroll() throws Exception { + + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { + + @Override + public void uncaughtException(Thread t, Throwable e) { + e.printStackTrace(); + + } + }); + + final Settings settings = Settings.builder() + .putList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+".worf", "knuddel","nonexists") + .build(); + setup(settings); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + for(int i=0; i<3; i++) + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + } + + + System.out.println("########search"); + HttpResponse res; + Assert.assertEquals(HttpStatus.SC_OK, (res=rh.executeGetRequest("vulcangov/_search?scroll=1m&pretty=true", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + + System.out.println(res.getBody()); + int start = res.getBody().indexOf("_scroll_id") + 15; + String scrollid = res.getBody().substring(start, res.getBody().indexOf("\"", start+1)); + System.out.println(scrollid); + System.out.println("########search scroll"); + Assert.assertEquals(HttpStatus.SC_OK, (res=rh.executePostRequest("/_search/scroll?pretty=true", "{\"scroll_id\" : \""+scrollid+"\"}", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + + + System.out.println("########search done"); + + + } + + @Test + public void testNotInsecure() throws Exception { + setup(Settings.EMPTY, new DynamicSecurityConfig().setSecurityRoles("roles_deny.yml"), Settings.EMPTY, true); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + //create indices and mapping upfront + tc.index(new IndexRequest("test").type("type1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"field2\":\"init\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("lorem").type("type1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"field2\":\"init\"}", XContentType.JSON)).actionGet(); + + WhoAmIResponse wres = tc.execute(WhoAmIAction.INSTANCE, new WhoAmIRequest()).actionGet(); + System.out.println(wres); + Assert.assertEquals("CN=kirk,OU=client,O=client,L=Test,C=DE", wres.getDn()); + Assert.assertTrue(wres.isAdmin()); + Assert.assertTrue(wres.toString(), wres.isAuthenticated()); + Assert.assertFalse(wres.toString(), wres.isNodeCertificateRequest()); + } + + HttpResponse res = rh.executePutRequest("test/_mapping/type1?pretty", "{\"properties\": {\"name\":{\"type\":\"text\"}}}", encodeBasicHeader("writer", "writer")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, res.getStatusCode()); + + res = rh.executePostRequest("_cluster/reroute", "{}", encodeBasicHeader("writer", "writer")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, res.getStatusCode()); + + try (TransportClient tc = getUserTransportClient(clusterInfo, "spock-keystore.jks", Settings.EMPTY)) { + //create indices and mapping upfront + try { + tc.admin().indices().putMapping(new PutMappingRequest("test").type("typex").source("fieldx","type=text")).actionGet(); + Assert.fail(); + } catch (ElasticsearchSecurityException e) { + Assert.assertTrue(e.toString(),e.getMessage().contains("no permissions for")); + } + + try { + tc.admin().cluster().reroute(new ClusterRerouteRequest()).actionGet(); + Assert.fail(); + } catch (ElasticsearchSecurityException e) { + Assert.assertTrue(e.toString(),e.getMessage().contains("no permissions for [cluster:admin/reroute]")); + } + + WhoAmIResponse wres = tc.execute(WhoAmIAction.INSTANCE, new WhoAmIRequest()).actionGet(); + Assert.assertEquals("CN=spock,OU=client,O=client,L=Test,C=DE", wres.getDn()); + Assert.assertFalse(wres.isAdmin()); + Assert.assertTrue(wres.toString(), wres.isAuthenticated()); + Assert.assertFalse(wres.toString(), wres.isNodeCertificateRequest()); + } + + } + + @Test + public void testDnParsingCertAuth() throws Exception { + Settings settings = Settings.builder() + .put("username_attribute", "cn") + .put("roles_attribute", "l") + .build(); + HTTPClientCertAuthenticator auth = new HTTPClientCertAuthenticator(settings, null); + Assert.assertEquals("abc", auth.extractCredentials(null, newThreadContext("cn=abc,cn=xxx,l=ert,st=zui,c=qwe")).getUsername()); + Assert.assertEquals("abc", auth.extractCredentials(null, newThreadContext("cn=abc,l=ert,st=zui,c=qwe")).getUsername()); + Assert.assertEquals("abc", auth.extractCredentials(null, newThreadContext("CN=abc,L=ert,st=zui,c=qwe")).getUsername()); + Assert.assertEquals("abc", auth.extractCredentials(null, newThreadContext("l=ert,cn=abc,st=zui,c=qwe")).getUsername()); + Assert.assertNull(auth.extractCredentials(null, newThreadContext("L=ert,CN=abc,c,st=zui,c=qwe"))); + Assert.assertEquals("abc", auth.extractCredentials(null, newThreadContext("l=ert,st=zui,c=qwe,cn=abc")).getUsername()); + Assert.assertEquals("abc", auth.extractCredentials(null, newThreadContext("L=ert,st=zui,c=qwe,CN=abc")).getUsername()); + Assert.assertEquals("L=ert,st=zui,c=qwe", auth.extractCredentials(null, newThreadContext("L=ert,st=zui,c=qwe")).getUsername()); + Assert.assertArrayEquals(new String[] {"ert"}, auth.extractCredentials(null, newThreadContext("cn=abc,l=ert,st=zui,c=qwe")).getBackendRoles().toArray(new String[0])); + Assert.assertArrayEquals(new String[] {"bleh", "ert"}, new TreeSet<>(auth.extractCredentials(null, newThreadContext("cn=abc,l=ert,L=bleh,st=zui,c=qwe")).getBackendRoles()).toArray(new String[0])); + + settings = Settings.builder() + .build(); + auth = new HTTPClientCertAuthenticator(settings, null); + Assert.assertEquals("cn=abc,l=ert,st=zui,c=qwe", auth.extractCredentials(null, newThreadContext("cn=abc,l=ert,st=zui,c=qwe")).getUsername()); + } + + private ThreadContext newThreadContext(String sslPrincipal) { + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL, sslPrincipal); + return threadContext; + } + + @Test + public void testDNSpecials() throws Exception { + + final Settings settings = Settings.builder() + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("node-untspec5-keystore.p12")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS, "1") + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE, "PKCS12") + .putList("opendistro_security.nodes_dn", "EMAILADDRESS=unt@tst.com,CN=node-untspec5.example.com,OU=SSL,O=Te\\, st,L=Test,C=DE") + .putList("opendistro_security.authcz.admin_dn", "EMAILADDRESS=unt@xxx.com,CN=node-untspec6.example.com,OU=SSL,O=Te\\, st,L=Test,C=DE") + .put("opendistro_security.cert.oid","1.2.3.4.5.6") + .build(); + + + Settings tcSettings = Settings.builder() + .put("opendistro_security.ssl.transport.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-untspec6-keystore.p12")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE, "PKCS12") + .build(); + + setup(tcSettings, new DynamicSecurityConfig(), settings, true); + RestHelper rh = nonSslRestHelper(); + + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("").getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("worf", "worf")).getStatusCode()); + + } + + @Test + public void testDNSpecials1() throws Exception { + + final Settings settings = Settings.builder() + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("node-untspec5-keystore.p12")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS, "1") + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE, "PKCS12") + .putList("opendistro_security.nodes_dn", "EMAILADDRESS=unt@tst.com,CN=node-untspec5.example.com,OU=SSL,O=Te\\, st,L=Test,C=DE") + .putList("opendistro_security.authcz.admin_dn", "EMAILADDREss=unt@xxx.com, cn=node-untspec6.example.com, OU=SSL,O=Te\\, st,L=Test, c=DE") + .put("opendistro_security.cert.oid","1.2.3.4.5.6") + .build(); + + + Settings tcSettings = Settings.builder() + .put("opendistro_security.ssl.transport.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-untspec6-keystore.p12")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE, "PKCS12") + .build(); + + setup(tcSettings, new DynamicSecurityConfig(), settings, true); + RestHelper rh = nonSslRestHelper(); + + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("").getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("worf", "worf")).getStatusCode()); + } + + @Test + public void testEnsureOpenSSLAvailability() { + Assume.assumeTrue(allowOpenSSL); + Assert.assertTrue(String.valueOf(OpenSsl.unavailabilityCause()), OpenSsl.isAvailable()); + } + + @Test + public void testMultiget() throws Exception { + + setup(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("mindex1").type("type").id("1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("mindex2").type("type").id("2").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":2}", XContentType.JSON)).actionGet(); + } + + //opendistro_security_multiget -> picard + + + String mgetBody = "{"+ + "\"docs\" : ["+ + "{"+ + "\"_index\" : \"mindex1\","+ + "\"_type\" : \"type\","+ + "\"_id\" : \"1\""+ + " },"+ + " {"+ + "\"_index\" : \"mindex2\","+ + " \"_type\" : \"type\","+ + " \"_id\" : \"2\""+ + "}"+ + "]"+ + "}"; + + RestHelper rh = nonSslRestHelper(); + HttpResponse resc = rh.executePostRequest("_mget?refresh=true", mgetBody, encodeBasicHeader("picard", "picard")); + System.out.println(resc.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertFalse(resc.getBody().contains("type2")); + + } + + @Test + public void testRestImpersonation() throws Exception { + + final Settings settings = Settings.builder() + .putList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+".spock", "knuddel","userwhonotexists").build(); + + setup(settings); + + RestHelper rh = nonSslRestHelper(); + + //knuddel: + // hash: _rest_impersonation_only_ + + HttpResponse resp; + resp = rh.executeGetRequest("/_opendistro/_security/authinfo", new BasicHeader("opendistro_security_impersonate_as", "knuddel"), encodeBasicHeader("worf", "worf")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, resp.getStatusCode()); + + resp = rh.executeGetRequest("/_opendistro/_security/authinfo", new BasicHeader("opendistro_security_impersonate_as", "knuddel"), encodeBasicHeader("spock", "spock")); + Assert.assertEquals(HttpStatus.SC_OK, resp.getStatusCode()); + Assert.assertTrue(resp.getBody().contains("name=knuddel")); + Assert.assertFalse(resp.getBody().contains("spock")); + + resp = rh.executeGetRequest("/_opendistro/_security/authinfo", new BasicHeader("opendistro_security_impersonate_as", "userwhonotexists"), encodeBasicHeader("spock", "spock")); + System.out.println(resp.getBody()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, resp.getStatusCode()); + + resp = rh.executeGetRequest("/_opendistro/_security/authinfo", new BasicHeader("opendistro_security_impersonate_as", "invalid"), encodeBasicHeader("spock", "spock")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, resp.getStatusCode()); + } + + @Test + public void testSingle() throws Exception { + + setup(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("shakespeare").type("type").id("1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config","roles","rolesmapping","internalusers","actiongroups"})).actionGet(); + Assert.assertEquals(clusterInfo.numNodes, cur.getNodes().size()); + } + + RestHelper rh = nonSslRestHelper(); + //opendistro_security_shakespeare -> picard + + HttpResponse resc = rh.executeGetRequest("shakespeare/_search", encodeBasicHeader("picard", "picard")); + System.out.println(resc.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("\"content\":1")); + + resc = rh.executeHeadRequest("shakespeare", encodeBasicHeader("picard", "picard")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + + } + + @Test + public void testSpecialUsernames() throws Exception { + + setup(); + RestHelper rh = nonSslRestHelper(); + + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("bug.99", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("a", "b")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("\"'+-,;_?*@<>!$%&/()=#", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("§ÄÖÜäöüß", "nagilum")).getStatusCode()); + + } + + @Test + public void testXff() throws Exception { + + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_xff.yml"), Settings.EMPTY, true); + RestHelper rh = nonSslRestHelper(); + HttpResponse resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader("x-forwarded-for", "10.0.0.7"), encodeBasicHeader("worf", "worf")); + Assert.assertEquals(200, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("10.0.0.7")); + } + + @Test + public void testRegexExcludes() throws Exception { + + setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY); + + try (TransportClient tc = getInternalTransportClient(this.clusterInfo, Settings.EMPTY)) { + tc.index(new IndexRequest("indexa").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"indexa\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("indexb").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"indexb\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("isallowed").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"isallowed\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("special").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"special\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("alsonotallowed").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"alsonotallowed\":1}", XContentType.JSON)).actionGet(); + } + + RestHelper rh = nonSslRestHelper(); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("index*/_search",encodeBasicHeader("rexclude", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("indexa/_search",encodeBasicHeader("rexclude", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("isallowed/_search",encodeBasicHeader("rexclude", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("special/_search",encodeBasicHeader("rexclude", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executeGetRequest("alsonotallowed/_search",encodeBasicHeader("rexclude", "nagilum")).getStatusCode()); + } + + @Test + public void testMultiRoleSpan() throws Exception { + + setup(); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("mindex_1").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("mindex_2").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":2}", XContentType.JSON)).actionGet(); + } + + HttpResponse res = rh.executeGetRequest("/mindex_1,mindex_2/_search", encodeBasicHeader("mindex12", "nagilum")); + System.out.println(res.getBody()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, res.getStatusCode()); + Assert.assertFalse(res.getBody().contains("\"content\":1")); + Assert.assertFalse(res.getBody().contains("\"content\":2")); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest(".opendistro_security").type("security").id("config").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("config", FileHelper.readYamlContent("config_multirolespan.yml"))).actionGet(); + + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config"})).actionGet(); + Assert.assertEquals(clusterInfo.numNodes, cur.getNodes().size()); + } + + res = rh.executeGetRequest("/mindex_1,mindex_2/_search", encodeBasicHeader("mindex12", "nagilum")); + System.out.println(res.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + Assert.assertTrue(res.getBody().contains("\"content\":1")); + Assert.assertTrue(res.getBody().contains("\"content\":2")); + + } + + @Test + public void testMultiRoleSpan2() throws Exception { + + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_multirolespan.yml"), Settings.EMPTY); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("mindex_1").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("mindex_2").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":2}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("mindex_3").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":2}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("mindex_4").type("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":2}", XContentType.JSON)).actionGet(); + } + + HttpResponse res = rh.executeGetRequest("/mindex_1,mindex_2/_search", encodeBasicHeader("mindex12", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + + res = rh.executeGetRequest("/mindex_1,mindex_3/_search", encodeBasicHeader("mindex12", "nagilum")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, res.getStatusCode()); + + res = rh.executeGetRequest("/mindex_1,mindex_4/_search", encodeBasicHeader("mindex12", "nagilum")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, res.getStatusCode()); + + } + + @Test + public void testSGUnderscore() throws Exception { + + setup(); + final RestHelper rh = nonSslRestHelper(); + + HttpResponse res = rh.executePostRequest("abc_xyz_2018_05_24/logs/1", "{\"content\":1}", encodeBasicHeader("underscore", "nagilum")); + + res = rh.executeGetRequest("abc_xyz_2018_05_24/logs/1", encodeBasicHeader("underscore", "nagilum")); + Assert.assertTrue(res.getBody(),res.getBody().contains("\"content\":1")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + res = rh.executeGetRequest("abc_xyz_2018_05_24/_refresh", encodeBasicHeader("underscore", "nagilum")); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + res = rh.executeGetRequest("aaa_bbb_2018_05_24/_refresh", encodeBasicHeader("underscore", "nagilum")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, res.getStatusCode()); + } + + @Test + public void testDeleteByQueryDnfof() throws Exception { + + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_dnfof.yml"), Settings.EMPTY); + + try (TransportClient tc = getInternalTransportClient()) { + for(int i=0; i<3; i++) { + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + } + } + + RestHelper rh = nonSslRestHelper(); + HttpResponse res; + Assert.assertEquals(HttpStatus.SC_OK, (res=rh.executePostRequest("/vulcango*/_delete_by_query?refresh=true&wait_for_completion=true&pretty=true", "{\"query\" : {\"match_all\" : {}}}", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + Assert.assertTrue(res.getBody().contains("\"deleted\" : 3")); + + } + + @Test + public void testUpdate() throws Exception { + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .build(); + setup(settings); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("indexc").type("typec").id("0") + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source("{\"content\":1}", XContentType.JSON)).actionGet(); + } + + HttpResponse res = rh.executePostRequest("indexc/typec/0/_update?pretty=true&refresh=true", "{\"doc\" : {\"content\":2}}", + encodeBasicHeader("user_c", "user_c")); + System.out.println(res.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); + } + + + @Test + public void testDnfof() throws Exception { + + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .build(); + + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_dnfof.yml"), settings); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.admin().indices().create(new CreateIndexRequest("copysf")).actionGet(); + + tc.index(new IndexRequest("indexa").type("doc").id("0").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":\"indexa\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("indexb").type("doc").id("0").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":\"indexb\"}", XContentType.JSON)).actionGet(); + + + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_academy").type("students").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_library").type("public").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("klingonempire").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("public").type("legends").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("spock").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("kirk").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("role01_role02").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("starfleet","starfleet_academy","starfleet_library").alias("sf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("klingonempire","vulcangov").alias("nonsf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("public").alias("unrestricted"))).actionGet(); + + } + + HttpResponse resc; + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("indexa,indexb/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("indexa")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("indexb")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("exception")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("permission")); + + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("indexa,indexb/_search?pretty", encodeBasicHeader("user_b", "user_b"))).getStatusCode()); + System.out.println(resc.getBody()); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("indexa")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("indexb")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("exception")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("permission")); + + String msearchBody = + "{\"index\":\"indexa\", \"type\":\"doc\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator()+ + "{\"index\":\"indexb\", \"type\":\"doc\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator()+ + "{\"index\":\"index*\", \"type\":\"doc\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator(); + System.out.println("#### msearch"); + resc = rh.executePostRequest("_msearch?pretty", msearchBody, encodeBasicHeader("user_a", "user_a")); + Assert.assertEquals(200, resc.getStatusCode()); + System.out.println(resc.getBody()); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("indexa")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("indexb")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("exception")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("permission")); + Assert.assertEquals(3, resc.getBody().split("\"status\" : 200").length); + Assert.assertEquals(2, resc.getBody().split("\"status\" : 403").length); + + resc = rh.executePostRequest("_msearch?pretty", msearchBody, encodeBasicHeader("user_b", "user_b")); + Assert.assertEquals(200, resc.getStatusCode()); + System.out.println(resc.getBody()); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("indexa")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("indexb")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("exception")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("permission")); + Assert.assertEquals(3, resc.getBody().split("\"status\" : 200").length); + Assert.assertEquals(2, resc.getBody().split("\"status\" : 403").length); + + msearchBody = + "{\"index\":\"indexc\", \"type\":\"doc\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator()+ + "{\"index\":\"indexd\", \"type\":\"doc\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator(); + + resc = rh.executePostRequest("_msearch?pretty", msearchBody, encodeBasicHeader("user_b", "user_b")); + Assert.assertEquals(403, resc.getStatusCode()); + + String mgetBody = "{"+ + "\"docs\" : ["+ + "{"+ + "\"_index\" : \"indexa\","+ + "\"_type\" : \"doc\","+ + "\"_id\" : \"0\""+ + " },"+ + " {"+ + "\"_index\" : \"indexb\","+ + " \"_type\" : \"doc\","+ + " \"_id\" : \"0\""+ + "}"+ + "]"+ + "}"; + + System.out.println("#### mget"); + resc = rh.executePostRequest("_mget?pretty", mgetBody, encodeBasicHeader("user_b", "user_b")); + Assert.assertEquals(200, resc.getStatusCode()); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("\"content\" : \"indexa\"")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("\"content\" : \"indexb\"")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("exception")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("permission")); + + mgetBody = "{"+ + "\"docs\" : ["+ + "{"+ + "\"_index\" : \"indexx\","+ + "\"_type\" : \"doc\","+ + "\"_id\" : \"0\""+ + " },"+ + " {"+ + "\"_index\" : \"indexy\","+ + " \"_type\" : \"doc\","+ + " \"_id\" : \"0\""+ + "}"+ + "]"+ + "}"; + + resc = rh.executePostRequest("_mget?pretty", mgetBody, encodeBasicHeader("user_b", "user_b")); + Assert.assertEquals(403, resc.getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("indexa")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("indexb")); + + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("index*/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("indexa")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("indexb")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("exception")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("permission")); + + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("indexa/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("indexb/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("*/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("_all/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("notexists/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, (resc=rh.executeGetRequest("permitnotexistentindex/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("permitnotexistentindex*/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, (resc=rh.executeGetRequest("indexanbh,indexabb*/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("starfleet/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("starfleet/_search?pretty", encodeBasicHeader("worf", "worf"))).getStatusCode()); + System.out.println(resc.getBody()); + + System.out.println("#### _all/_mapping/field/*"); + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("_all/_mapping/field/*", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + System.out.println(resc.getBody()); + } + + + @Test + public void testNoDnfof() throws Exception { + + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .build(); + + setup(Settings.EMPTY, new DynamicSecurityConfig(), settings); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.admin().indices().create(new CreateIndexRequest("copysf")).actionGet(); + + tc.index(new IndexRequest("indexa").type("doc").id("0").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":\"indexa\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("indexb").type("doc").id("0").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":\"indexb\"}", XContentType.JSON)).actionGet(); + + + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_academy").type("students").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_library").type("public").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("klingonempire").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("public").type("legends").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("spock").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("kirk").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("role01_role02").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("starfleet","starfleet_academy","starfleet_library").alias("sf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("klingonempire","vulcangov").alias("nonsf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("public").alias("unrestricted"))).actionGet(); + + } + + HttpResponse resc; + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("indexa,indexb/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("indexa,indexb/_search?pretty", encodeBasicHeader("user_b", "user_b"))).getStatusCode()); + System.out.println(resc.getBody()); + + String msearchBody = + "{\"index\":\"indexa\", \"type\":\"doc\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator()+ + "{\"index\":\"indexb\", \"type\":\"doc\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator(); + System.out.println("#### msearch a"); + resc = rh.executePostRequest("_msearch?pretty", msearchBody, encodeBasicHeader("user_a", "user_a")); + Assert.assertEquals(200, resc.getStatusCode()); + System.out.println(resc.getBody()); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("indexa")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("indexb")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("exception")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("permission")); + + System.out.println("#### msearch b"); + resc = rh.executePostRequest("_msearch?pretty", msearchBody, encodeBasicHeader("user_b", "user_b")); + Assert.assertEquals(200, resc.getStatusCode()); + System.out.println(resc.getBody()); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("indexa")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("indexb")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("exception")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("permission")); + + msearchBody = + "{\"index\":\"indexc\", \"type\":\"doc\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator()+ + "{\"index\":\"indexd\", \"type\":\"doc\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator(); + + System.out.println("#### msearch b2"); + resc = rh.executePostRequest("_msearch?pretty", msearchBody, encodeBasicHeader("user_b", "user_b")); + System.out.println(resc.getBody()); + Assert.assertEquals(200, resc.getStatusCode()); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("indexc")); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("indexd")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("exception")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("permission")); + int count = resc.getBody().split("\"status\" : 403").length; + Assert.assertEquals(3, count); + + String mgetBody = "{"+ + "\"docs\" : ["+ + "{"+ + "\"_index\" : \"indexa\","+ + "\"_type\" : \"doc\","+ + "\"_id\" : \"0\""+ + " },"+ + " {"+ + "\"_index\" : \"indexb\","+ + " \"_type\" : \"doc\","+ + " \"_id\" : \"0\""+ + "}"+ + "]"+ + "}"; + + resc = rh.executePostRequest("_mget?pretty", mgetBody, encodeBasicHeader("user_b", "user_b")); + Assert.assertEquals(200, resc.getStatusCode()); + Assert.assertFalse(resc.getBody(), resc.getBody().contains("\"content\" : \"indexa\"")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("indexb")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("exception")); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("permission")); + + mgetBody = "{"+ + "\"docs\" : ["+ + "{"+ + "\"_index\" : \"indexx\","+ + "\"_type\" : \"doc\","+ + "\"_id\" : \"0\""+ + " },"+ + " {"+ + "\"_index\" : \"indexy\","+ + " \"_type\" : \"doc\","+ + " \"_id\" : \"0\""+ + "}"+ + "]"+ + "}"; + + resc = rh.executePostRequest("_mget?pretty", mgetBody, encodeBasicHeader("user_b", "user_b")); + Assert.assertEquals(200, resc.getStatusCode()); + Assert.assertTrue(resc.getBody(), resc.getBody().contains("exception")); + count = resc.getBody().split("root_cause").length; + Assert.assertEquals(3, count); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("index*/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + + + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("indexa/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("indexb/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("*/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("_all/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("notexists/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, (resc=rh.executeGetRequest("indexanbh,indexabb*/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("starfleet/_search?pretty", encodeBasicHeader("user_a", "user_a"))).getStatusCode()); + System.out.println(resc.getBody()); + + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, (resc=rh.executeGetRequest("starfleet/_search?pretty", encodeBasicHeader("worf", "worf"))).getStatusCode()); + System.out.println(resc.getBody()); + + System.out.println("#### _all/_mapping/field/*"); + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("_all/_mapping/field/*", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + System.out.println(resc.getBody()); + System.out.println("#### _mapping/field/*"); + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("_mapping/field/*", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + System.out.println(resc.getBody()); + System.out.println("#### */_mapping/field/*"); + Assert.assertEquals(HttpStatus.SC_OK, (resc=rh.executeGetRequest("*/_mapping/field/*", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + System.out.println(resc.getBody()); + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/SlowIntegrationTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/SlowIntegrationTests.java new file mode 100644 index 0000000000..63c839de4e --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/SlowIntegrationTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; +import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.node.Node; +import org.elasticsearch.node.PluginAwareNode; +import org.elasticsearch.transport.Netty4Plugin; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.OpenDistroSecurityPlugin; +import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterConfiguration; +import com.amazon.opendistroforelasticsearch.security.test.helper.file.FileHelper; + +public class SlowIntegrationTests extends SingleClusterTest { + + @Test + public void testCustomInterclusterRequestEvaluator() throws Exception { + + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_INTERCLUSTER_REQUEST_EVALUATOR_CLASS, "com.amazon.opendistroforelasticsearch.security.AlwaysFalseInterClusterRequestEvaluator") + .put("discovery.initial_state_timeout","8s") + .build(); + setup(Settings.EMPTY, null, settings, false,ClusterConfiguration.DEFAULT ,5,1); + Assert.assertEquals(1, clusterHelper.nodeClient().admin().cluster().health(new ClusterHealthRequest().waitForGreenStatus()).actionGet().getNumberOfNodes()); + Assert.assertEquals(ClusterHealthStatus.GREEN, clusterHelper.nodeClient().admin().cluster().health(new ClusterHealthRequest().waitForGreenStatus()).actionGet().getStatus()); + } + + @SuppressWarnings("resource") + @Test + public void testNodeClientAllowedWithServerCertificate() throws Exception { + setup(); + Assert.assertEquals(clusterInfo.numNodes, clusterHelper.nodeClient().admin().cluster().health(new ClusterHealthRequest().waitForGreenStatus()).actionGet().getNumberOfNodes()); + Assert.assertEquals(ClusterHealthStatus.GREEN, clusterHelper.nodeClient().admin().cluster().health(new ClusterHealthRequest().waitForGreenStatus()).actionGet().getStatus()); + + + final Settings tcSettings = Settings.builder() + .put(minimumSecuritySettings(Settings.EMPTY).get(0)) + .put("cluster.name", clusterInfo.clustername) + .put("node.data", false) + .put("node.master", false) + .put("node.ingest", false) + .put("path.home", ".") + .put("discovery.initial_state_timeout","8s") + .putList("discovery.zen.ping.unicast.hosts", clusterInfo.nodeHost+":"+clusterInfo.nodePort) + .build(); + + log.debug("Start node client"); + + try (Node node = new PluginAwareNode(false, tcSettings, Netty4Plugin.class, OpenDistroSecurityPlugin.class).start()) { + Thread.sleep(50); + Assert.assertEquals(clusterInfo.numNodes+1, node.client().admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet().getNodes().size()); + } + } + + @SuppressWarnings("resource") + @Test + public void testNodeClientDisallowedWithNonServerCertificate() throws Exception { + setup(); + Assert.assertEquals(clusterInfo.numNodes, clusterHelper.nodeClient().admin().cluster().health(new ClusterHealthRequest().waitForGreenStatus()).actionGet().getNumberOfNodes()); + Assert.assertEquals(ClusterHealthStatus.GREEN, clusterHelper.nodeClient().admin().cluster().health(new ClusterHealthRequest().waitForGreenStatus()).actionGet().getStatus()); + + + final Settings tcSettings = Settings.builder() + .put(minimumSecuritySettings(Settings.EMPTY).get(0)) + .put("cluster.name", clusterInfo.clustername) + .put("node.data", false) + .put("node.master", false) + .put("node.ingest", false) + .put("path.home", ".") + .put("discovery.initial_state_timeout","8s") + .put("opendistro_security.ssl.transport.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("kirk-keystore.jks")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS,"kirk") + .build(); + + log.debug("Start node client"); + + try (Node node = new PluginAwareNode(false, tcSettings, Netty4Plugin.class, OpenDistroSecurityPlugin.class).start()) { + Thread.sleep(50); + Assert.assertEquals(1, node.client().admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet().getNodes().size()); + } catch (Exception e) { + Assert.fail(e.toString()); + } + + } + + @SuppressWarnings("resource") + @Test + public void testNodeClientDisallowedWithNonServerCertificate2() throws Exception { + setup(); + Assert.assertEquals(clusterInfo.numNodes, clusterHelper.nodeClient().admin().cluster().health(new ClusterHealthRequest().waitForGreenStatus()).actionGet().getNumberOfNodes()); + Assert.assertEquals(ClusterHealthStatus.GREEN, clusterHelper.nodeClient().admin().cluster().health(new ClusterHealthRequest().waitForGreenStatus()).actionGet().getStatus()); + + final Settings tcSettings = Settings.builder() + .put(minimumSecuritySettings(Settings.EMPTY).get(0)) + .put("cluster.name", clusterInfo.clustername) + .put("node.data", false) + .put("node.master", false) + .put("node.ingest", false) + .put("path.home", ".") + .put("discovery.initial_state_timeout","8s") + .put("opendistro_security.ssl.transport.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("spock-keystore.jks")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS,"spock") + .build(); + + log.debug("Start node client"); + + try (Node node = new PluginAwareNode(false, tcSettings, Netty4Plugin.class, OpenDistroSecurityPlugin.class).start()) { + Thread.sleep(50); + Assert.assertEquals(1, node.client().admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet().getNodes().size()); + } + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/SnapshotRestoreTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/SnapshotRestoreTests.java new file mode 100644 index 0000000000..beda5a715d --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/SnapshotRestoreTests.java @@ -0,0 +1,310 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.http.HttpStatus; +import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest; +import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentType; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateAction; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateRequest; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateResponse; +import com.amazon.opendistroforelasticsearch.security.test.DynamicSecurityConfig; +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterConfiguration; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; + +@RunWith(Parameterized.class) +public class SnapshotRestoreTests extends SingleClusterTest { + + @Parameters + public static Collection data() { + return Arrays.asList(new ClusterConfiguration[] { + ClusterConfiguration.DEFAULT + }); + } + + @Parameter + public ClusterConfiguration currentClusterConfig; + + @Test + public void testSnapshotEnableSecurityIndexRestore() throws Exception { + + final Settings settings = Settings.builder() + .putList("path.repo", repositoryPath.getRoot().getAbsolutePath()) + .put("opendistro_security.enable_snapshot_restore_privilege", true) + .put("opendistro_security.check_snapshot_restore_write_privileges", false) + .put("opendistro_security.unsupported.restore.securityindex.enabled", true) + .build(); + + setup(settings, currentClusterConfig); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.admin().cluster().putRepository(new PutRepositoryRequest("vulcangov").type("fs").settings(Settings.builder().put("location", repositoryPath.getRoot().getAbsolutePath() + "/vulcangov"))).actionGet(); + tc.admin().cluster().createSnapshot(new CreateSnapshotRequest("vulcangov", "vulcangov_1").indices("vulcangov").includeGlobalState(true).waitForCompletion(true)).actionGet(); + + tc.admin().cluster().putRepository(new PutRepositoryRequest(".opendistro_security").type("fs").settings(Settings.builder().put("location", repositoryPath.getRoot().getAbsolutePath() + "/.opendistro_security"))).actionGet(); + tc.admin().cluster().createSnapshot(new CreateSnapshotRequest(".opendistro_security", "opendistro_security_1").indices(".opendistro_security").includeGlobalState(false).waitForCompletion(true)).actionGet(); + + tc.admin().cluster().putRepository(new PutRepositoryRequest("all").type("fs").settings(Settings.builder().put("location", repositoryPath.getRoot().getAbsolutePath() + "/all"))).actionGet(); + tc.admin().cluster().createSnapshot(new CreateSnapshotRequest("all", "all_1").indices("*").includeGlobalState(false).waitForCompletion(true)).actionGet(); + } + + RestHelper rh = nonSslRestHelper(); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/vulcangov", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/vulcangov/vulcangov_1", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_$1\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"include_global_state\": true, \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // worf not allowed to restore vulcangov index + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","", encodeBasicHeader("worf", "worf")).getStatusCode()); + // Try to restore vulcangov index as .opendistro_security index, not possible since Security index is open + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"indices\": \"vulcangov\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \".opendistro_security\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + // Try to restore .opendistro_security index. + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/.opendistro_security", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/.opendistro_security/opendistro_security_1", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // 500 because Security index is open + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, rh.executePostRequest("_snapshot/.opendistro_security/opendistro_security_1/_restore?wait_for_completion=true","", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // Try to restore .opendistro_security index as .opendistro_security_copy index + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest("_snapshot/.opendistro_security/opendistro_security_1/_restore?wait_for_completion=true","{ \"indices\": \".opendistro_security\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"opendistro_security_copy\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + // Try to restore all indices. + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/all", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/all/all_1", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // 500 because Security index is open + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, rh.executePostRequest("_snapshot/all/all_1/_restore?wait_for_completion=true","", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // Try to restore vulcangov index as .opendistro_security index -> 500 because Security index is open + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, rh.executePostRequest("_snapshot/all/all_1/_restore?wait_for_completion=true","{ \"indices\": \"vulcangov\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \".opendistro_security\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // Try to restore .opendistro_security index as .opendistro_security_copy index. Delete opendistro_security_copy first, was created in test above + Assert.assertEquals(HttpStatus.SC_OK, rh.executeDeleteRequest("opendistro_security_copy", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest("_snapshot/all/all_1/_restore?wait_for_completion=true","{ \"indices\": \".opendistro_security\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"opendistro_security_copy\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + // Try to restore a unknown snapshot + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, rh.executePostRequest("_snapshot/all/unknown-snapshot/_restore?wait_for_completion=true", "", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + // close and restore Security index + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest(".opendistro_security/_close", "", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest("_snapshot/.opendistro_security/opendistro_security_1/_restore?wait_for_completion=true","", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest(".opendistro_security/_open", "", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + } + + @Test + public void testSnapshot() throws Exception { + + final Settings settings = Settings.builder() + .putList("path.repo", repositoryPath.getRoot().getAbsolutePath()) + .put("opendistro_security.enable_snapshot_restore_privilege", true) + .put("opendistro_security.check_snapshot_restore_write_privileges", false) + .build(); + + setup(settings, currentClusterConfig); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.admin().cluster().putRepository(new PutRepositoryRequest("vulcangov").type("fs").settings(Settings.builder().put("location", repositoryPath.getRoot().getAbsolutePath() + "/vulcangov"))).actionGet(); + tc.admin().cluster().createSnapshot(new CreateSnapshotRequest("vulcangov", "vulcangov_1").indices("vulcangov").includeGlobalState(true).waitForCompletion(true)).actionGet(); + + tc.admin().cluster().putRepository(new PutRepositoryRequest(".opendistro_security").type("fs").settings(Settings.builder().put("location", repositoryPath.getRoot().getAbsolutePath() + "/.opendistro_security"))).actionGet(); + tc.admin().cluster().createSnapshot(new CreateSnapshotRequest(".opendistro_security", "opendistro_security_1").indices(".opendistro_security").includeGlobalState(false).waitForCompletion(true)).actionGet(); + + tc.admin().cluster().putRepository(new PutRepositoryRequest("all").type("fs").settings(Settings.builder().put("location", repositoryPath.getRoot().getAbsolutePath() + "/all"))).actionGet(); + tc.admin().cluster().createSnapshot(new CreateSnapshotRequest("all", "all_1").indices("*").includeGlobalState(false).waitForCompletion(true)).actionGet(); + } + + RestHelper rh = nonSslRestHelper(); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/vulcangov", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/vulcangov/vulcangov_1", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_$1\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"include_global_state\": true, \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","", encodeBasicHeader("worf", "worf")).getStatusCode()); + // Try to restore vulcangov index as .opendistro_security index + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"indices\": \"vulcangov\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \".opendistro_security\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + // Try to restore .opendistro_security index. + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/.opendistro_security", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/.opendistro_security/opendistro_security_1", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/.opendistro_security/opendistro_security_1/_restore?wait_for_completion=true","", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // Try to restore .opendistro_security index as .opendistro_security_copy index + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/.opendistro_security/opendistro_security_1/_restore?wait_for_completion=true","{ \"indices\": \".opendistro_security\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"opendistro_security_copy\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + // Try to restore all indices. + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/all", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/all/all_1", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/all/all_1/_restore?wait_for_completion=true","", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // Try to restore .opendistro_security index as .opendistro_security_copy index + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/all/all_1/_restore?wait_for_completion=true","{ \"indices\": \"vulcangov\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \".opendistro_security\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // Try to restore .opendistro_security index as .opendistro_security_copy index + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/all/all_1/_restore?wait_for_completion=true","{ \"indices\": \".opendistro_security\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"opendistro_security_copy\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + // Try to restore a unknown snapshot + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/all/unknown-snapshot/_restore?wait_for_completion=true", "", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // Assert.assertEquals(HttpStatus.SC_FORBIDDEN, executePostRequest("_snapshot/all/unknown-snapshot/_restore?wait_for_completion=true","{ \"indices\": \"the-unknown-index\" }", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + } + + @Test + public void testSnapshotCheckWritePrivileges() throws Exception { + + final Settings settings = Settings.builder() + .putList("path.repo", repositoryPath.getRoot().getAbsolutePath()) + .put("opendistro_security.enable_snapshot_restore_privilege", true) + .put("opendistro_security.check_snapshot_restore_write_privileges", true) + .build(); + + setup(settings, currentClusterConfig); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.admin().cluster().putRepository(new PutRepositoryRequest("vulcangov").type("fs").settings(Settings.builder().put("location", repositoryPath.getRoot().getAbsolutePath() + "/vulcangov"))).actionGet(); + tc.admin().cluster().createSnapshot(new CreateSnapshotRequest("vulcangov", "vulcangov_1").indices("vulcangov").includeGlobalState(true).waitForCompletion(true)).actionGet(); + + tc.admin().cluster().putRepository(new PutRepositoryRequest(".opendistro_security").type("fs").settings(Settings.builder().put("location", repositoryPath.getRoot().getAbsolutePath() + "/.opendistro_security"))).actionGet(); + tc.admin().cluster().createSnapshot(new CreateSnapshotRequest(".opendistro_security", "opendistro_security_1").indices(".opendistro_security").includeGlobalState(false).waitForCompletion(true)).actionGet(); + + tc.admin().cluster().putRepository(new PutRepositoryRequest("all").type("fs").settings(Settings.builder().put("location", repositoryPath.getRoot().getAbsolutePath() + "/all"))).actionGet(); + tc.admin().cluster().createSnapshot(new CreateSnapshotRequest("all", "all_1").indices("*").includeGlobalState(false).waitForCompletion(true)).actionGet(); + + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config","roles","rolesmapping","internalusers","actiongroups"})).actionGet(); + Assert.assertEquals(currentClusterConfig.getNodes(), cur.getNodes().size()); + System.out.println(cur.getNodesMap()); + } + + RestHelper rh = nonSslRestHelper(); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/vulcangov", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/vulcangov/vulcangov_1", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_$1\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"include_global_state\": true, \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_with_global_state_$1\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","", encodeBasicHeader("worf", "worf")).getStatusCode()); + // Try to restore vulcangov index as .opendistro_security index + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"indices\": \"vulcangov\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \".opendistro_security\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + // Try to restore .opendistro_security index. + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/.opendistro_security", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/.opendistro_security/opendistro_security_1", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/.opendistro_security/opendistro_security_1/_restore?wait_for_completion=true","", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // Try to restore .opendistro_security index as .opendistro_security_copy index + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/.opendistro_security/opendistro_security_1/_restore?wait_for_completion=true","{ \"indices\": \".opendistro_security\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"opendistro_security_copy\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + // Try to restore all indices. + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/all", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_snapshot/all/all_1", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/all/all_1/_restore?wait_for_completion=true","", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // Try to restore .opendistro_security index as .opendistro_security_copy index + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/all/all_1/_restore?wait_for_completion=true","{ \"indices\": \"vulcangov\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \".opendistro_security\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + // Try to restore .opendistro_security index as .opendistro_security_copy index + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/all/all_1/_restore?wait_for_completion=true","{ \"indices\": \".opendistro_security\", \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"opendistro_security_copy\" }", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + // Try to restore a unknown snapshot + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/all/unknown-snapshot/_restore?wait_for_completion=true", "", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + + // Tests snapshot with write permissions (OK) + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"$1_restore_1\" }", encodeBasicHeader("restoreuser", "restoreuser")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"$1_restore_2a\" }", encodeBasicHeader("restoreuser", "restoreuser")).getStatusCode()); + + // Test snapshot with write permissions (FAIL) + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"$1_no_restore_1\" }", encodeBasicHeader("restoreuser", "restoreuser")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"$1_no_restore_2\" }", encodeBasicHeader("restoreuser", "restoreuser")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"$1_no_restore_3\" }", encodeBasicHeader("restoreuser", "restoreuser")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/vulcangov/vulcangov_1/_restore?wait_for_completion=true","{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"$1_no_restore_4\" }", encodeBasicHeader("restoreuser", "restoreuser")).getStatusCode()); + } + + @Test + public void testSnapshotRestore() throws Exception { + + final Settings settings = Settings.builder() + .putList("path.repo", repositoryPath.getRoot().getAbsolutePath()) + .put("opendistro_security.enable_snapshot_restore_privilege", true) + .put("opendistro_security.check_snapshot_restore_write_privileges", true) + .build(); + + setup(Settings.EMPTY, new DynamicSecurityConfig().setSecurityActionGroups("action_groups_packaged.yml"), settings, true, currentClusterConfig); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("testsnap1").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("testsnap2").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("testsnap3").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("testsnap4").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("testsnap5").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("testsnap6").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.admin().cluster().putRepository(new PutRepositoryRequest("bckrepo").type("fs").settings(Settings.builder().put("location", repositoryPath.getRoot().getAbsolutePath() + "/bckrepo"))).actionGet(); + } + + RestHelper rh = nonSslRestHelper(); + String putSnapshot = + "{"+ + "\"indices\": \"testsnap1\","+ + "\"ignore_unavailable\": false,"+ + "\"include_global_state\": false"+ + "}"; + + Assert.assertEquals(HttpStatus.SC_OK, rh.executePutRequest("_snapshot/bckrepo/"+putSnapshot.hashCode()+"?wait_for_completion=true&pretty", putSnapshot, encodeBasicHeader("snapresuser", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_OK, rh.executePostRequest("_snapshot/bckrepo/"+putSnapshot.hashCode()+"/_restore?wait_for_completion=true&pretty","{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_$1\" }", encodeBasicHeader("snapresuser", "nagilum")).getStatusCode()); + + putSnapshot = + "{"+ + "\"indices\": \".opendistro_security\","+ + "\"ignore_unavailable\": false,"+ + "\"include_global_state\": false"+ + "}"; + + Assert.assertEquals(HttpStatus.SC_OK, rh.executePutRequest("_snapshot/bckrepo/"+putSnapshot.hashCode()+"?wait_for_completion=true&pretty", putSnapshot, encodeBasicHeader("snapresuser", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/bckrepo/"+putSnapshot.hashCode()+"/_restore?wait_for_completion=true&pretty","{ \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_$1\" }", encodeBasicHeader("snapresuser", "nagilum")).getStatusCode()); + + putSnapshot = + "{"+ + "\"indices\": \"testsnap2\","+ + "\"ignore_unavailable\": false,"+ + "\"include_global_state\": true"+ + "}"; + + Assert.assertEquals(HttpStatus.SC_OK, rh.executePutRequest("_snapshot/bckrepo/"+putSnapshot.hashCode()+"?wait_for_completion=true&pretty", putSnapshot, encodeBasicHeader("snapresuser", "nagilum")).getStatusCode()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, rh.executePostRequest("_snapshot/bckrepo/"+putSnapshot.hashCode()+"/_restore?wait_for_completion=true&pretty","{ \"include_global_state\": true, \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_index_$1\" }", encodeBasicHeader("snapresuser", "nagilum")).getStatusCode()); + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/SystemIntegratorsTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/SystemIntegratorsTests.java new file mode 100644 index 0000000000..506b849b20 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/SystemIntegratorsTests.java @@ -0,0 +1,261 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import org.apache.http.HttpStatus; +import org.apache.http.message.BasicHeader; +import org.elasticsearch.common.settings.Settings; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterConfiguration; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; +import com.google.common.collect.Lists; + +public class SystemIntegratorsTests extends SingleClusterTest { + + @Test + public void testInjectedUserMalformed() throws Exception { + + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, true) + .put("http.type", "com.amazon.opendistroforelasticsearch.security.http.UserInjectingServerTransport") + .build(); + + setup(settings, ClusterConfiguration.USERINJECTOR); + + final RestHelper rh = nonSslRestHelper(); + // username|role1,role2|remoteIP|attributes + + HttpResponse resc; + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, null)); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, resc.getStatusCode()); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "|||")); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, resc.getStatusCode()); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "||127.0.0:80|")); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, resc.getStatusCode()); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "username||ip|")); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, resc.getStatusCode()); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "username||ip:port|")); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, resc.getStatusCode()); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "username||ip:80|")); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, resc.getStatusCode()); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "username||127.0.x:80|")); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, resc.getStatusCode()); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "username||127.0.0:80|key1,value1,key2")); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, resc.getStatusCode()); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "||127.0.0:80|key1,value1,key2,value2")); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, resc.getStatusCode()); + + } + + @Test + public void testInjectedUser() throws Exception { + + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, true) + .put("http.type", "com.amazon.opendistroforelasticsearch.security.http.UserInjectingServerTransport") + .build(); + + setup(settings, ClusterConfiguration.USERINJECTOR); + + final RestHelper rh = nonSslRestHelper(); + // username|role1,role2|remoteIP|attributes + + HttpResponse resc; + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "admin||127.0.0:80|")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("User [name=admin, roles=[], requestedTenant=null]")); + Assert.assertTrue(resc.getBody().contains("\"remote_address\":\"127.0.0.0:80\"")); + Assert.assertTrue(resc.getBody().contains("\"backend_roles\":[]")); + Assert.assertTrue(resc.getBody().contains("\"custom_attribute_names\":[]")); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "admin|role1|127.0.0:80|key1,value1")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("User [name=admin, roles=[role1], requestedTenant=null]")); + Assert.assertTrue(resc.getBody().contains("\"remote_address\":\"127.0.0.0:80\"")); + Assert.assertTrue(resc.getBody().contains("\"backend_roles\":[\"role1\"]")); + Assert.assertTrue(resc.getBody().contains("\"custom_attribute_names\":[\"key1\"]")); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "admin|role1,role2||key1,value1")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("User [name=admin, roles=[role1, role2], requestedTenant=null]")); + // remote IP is assigned by XFFResolver + Assert.assertFalse(resc.getBody().contains("\"remote_address\":null")); + Assert.assertTrue(resc.getBody().contains("\"backend_roles\":[\"role1\",\"role2\"]")); + Assert.assertTrue(resc.getBody().contains("\"custom_attribute_names\":[\"key1\"]")); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "admin|role1,role2|8.8.8.8:8|key1,value1,key2,value2")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("User [name=admin, roles=[role1, role2], requestedTenant=null]")); + // remote IP is assigned by XFFResolver + Assert.assertFalse(resc.getBody().contains("\"remote_address\":null")); + Assert.assertTrue(resc.getBody().contains("\"backend_roles\":[\"role1\",\"role2\"]")); + Assert.assertTrue(resc.getBody().contains("\"custom_attribute_names\":[\"key1\",\"key2\"]")); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "nagilum|role1,role2|8.8.8.8:8|key1,value1,key2,value2")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("User [name=nagilum, roles=[role1, role2], requestedTenant=null]")); + // remote IP is assigned by XFFResolver + Assert.assertTrue(resc.getBody().contains("\"remote_address\":\"8.8.8.8:8\"")); + Assert.assertTrue(resc.getBody().contains("\"backend_roles\":[\"role1\",\"role2\"]")); + // mapped by username + Assert.assertTrue(resc.getBody().contains("\"roles\":[\"opendistro_security_all_access\"")); + Assert.assertTrue(resc.getBody().contains("\"custom_attribute_names\":[\"key1\",\"key2\"]")); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "myuser|role1,vulcanadmin|8.8.8.8:8|key1,value1,key2,value2")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("User [name=myuser, roles=[role1, vulcanadmin], requestedTenant=null]")); + // remote IP is assigned by XFFResolver + Assert.assertTrue(resc.getBody().contains("\"remote_address\":\"8.8.8.8:8\"")); + Assert.assertTrue(resc.getBody().contains("\"backend_roles\":[\"role1\",\"vulcanadmin\"]")); + // mapped by backend role "twitter" + Assert.assertTrue(resc.getBody().contains("\"roles\":[\"opendistro_security_public\",\"opendistro_security_role_vulcans_admin\"]")); + Assert.assertTrue(resc.getBody().contains("\"custom_attribute_names\":[\"key1\",\"key2\"]")); + + // add requested tenant + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "myuser|role1,vulcanadmin|8.8.8.8:8|key1,value1,key2,value2|")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("User [name=myuser, roles=[role1, vulcanadmin], requestedTenant=null]")); + // remote IP is assigned by XFFResolver + Assert.assertTrue(resc.getBody().contains("\"remote_address\":\"8.8.8.8:8\"")); + Assert.assertTrue(resc.getBody().contains("\"backend_roles\":[\"role1\",\"vulcanadmin\"]")); + // mapped by backend role "twitter" + Assert.assertTrue(resc.getBody().contains("\"roles\":[\"opendistro_security_public\",\"opendistro_security_role_vulcans_admin\"]")); + Assert.assertTrue(resc.getBody().contains("\"custom_attribute_names\":[\"key1\",\"key2\"]")); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "myuser|role1,vulcanadmin|8.8.8.8:8|key1,value1,key2,value2|mytenant")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("User [name=myuser, roles=[role1, vulcanadmin], requestedTenant=mytenant]")); + // remote IP is assigned by XFFResolver + Assert.assertTrue(resc.getBody().contains("\"remote_address\":\"8.8.8.8:8\"")); + Assert.assertTrue(resc.getBody().contains("\"backend_roles\":[\"role1\",\"vulcanadmin\"]")); + // mapped by backend role "twitter" + Assert.assertTrue(resc.getBody().contains("\"roles\":[\"opendistro_security_public\",\"opendistro_security_role_vulcans_admin\"]")); + Assert.assertTrue(resc.getBody().contains("\"custom_attribute_names\":[\"key1\",\"key2\"]")); + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "myuser|role1,vulcanadmin|8.8.8.8:8||mytenant with whitespace")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("User [name=myuser, roles=[role1, vulcanadmin], requestedTenant=mytenant with whitespace]")); + // remote IP is assigned by XFFResolver + Assert.assertTrue(resc.getBody().contains("\"remote_address\":\"8.8.8.8:8\"")); + Assert.assertTrue(resc.getBody().contains("\"backend_roles\":[\"role1\",\"vulcanadmin\"]")); + // mapped by backend role "twitter" + Assert.assertTrue(resc.getBody().contains("\"roles\":[\"opendistro_security_public\",\"opendistro_security_role_vulcans_admin\"]")); + + + } + + @Test + public void testInjectedUserDisabled() throws Exception { + + final Settings settings = Settings.builder() + .put("http.type", "com.amazon.opendistroforelasticsearch.security.http.UserInjectingServerTransport") + .build(); + + setup(settings, ClusterConfiguration.USERINJECTOR); + + final RestHelper rh = nonSslRestHelper(); + // username|role1,role2|remoteIP|attributes + + HttpResponse resc; + + resc = rh.executeGetRequest("_opendistro/_security/authinfo", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "admin|role1|127.0.0:80|key1,value1")); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, resc.getStatusCode()); + } + + @Test + public void testInjectedAdminUser() throws Exception { + + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, true) + .put(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED, true) + .putList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_ADMIN_DN, Lists.newArrayList("CN=kirk,OU=client,O=client,L=Test,C=DE","injectedadmin")) + .put("http.type", "com.amazon.opendistroforelasticsearch.security.http.UserInjectingServerTransport") + .build(); + + setup(settings, ClusterConfiguration.USERINJECTOR); + + final RestHelper rh = nonSslRestHelper(); + HttpResponse resc; + + // injected user is admin, access to Security index must be allowed + resc = rh.executeGetRequest(".opendistro_security/_search?pretty", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "injectedadmin|role1|127.0.0:80|key1,value1")); + Assert.assertEquals(HttpStatus.SC_OK, resc.getStatusCode()); + Assert.assertTrue(resc.getBody().contains("\"_id\" : \"config\"")); + Assert.assertTrue(resc.getBody().contains("\"_id\" : \"roles\"")); + Assert.assertTrue(resc.getBody().contains("\"_id\" : \"internalusers\"")); + Assert.assertTrue(resc.getBody().contains("\"total\" : 5")); + + resc = rh.executeGetRequest(".opendistro_security/_search?pretty", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "wrongadmin|role1|127.0.0:80|key1,value1")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, resc.getStatusCode()); + + } + + @Test + public void testInjectedAdminUserAdminInjectionDisabled() throws Exception { + + final Settings settings = Settings.builder() + .put(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, true) + .putList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_ADMIN_DN, Lists.newArrayList("CN=kirk,OU=client,O=client,L=Test,C=DE","injectedadmin")) + .put("http.type", "com.amazon.opendistroforelasticsearch.security.http.UserInjectingServerTransport") + .build(); + + setup(settings, ClusterConfiguration.USERINJECTOR); + + final RestHelper rh = nonSslRestHelper(); + HttpResponse resc; + + // injected user is admin, access to Security index must be allowed + resc = rh.executeGetRequest(".opendistro_security/_search?pretty", new BasicHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, "injectedadmin|role1|127.0.0:80|key1,value1")); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, resc.getStatusCode()); + Assert.assertFalse(resc.getBody().contains("\"_id\" : \"config\"")); + Assert.assertFalse(resc.getBody().contains("\"_id\" : \"roles\"")); + Assert.assertFalse(resc.getBody().contains("\"_id\" : \"internalusers\"")); + Assert.assertFalse(resc.getBody().contains("\"_id\" : \"tattr\"")); + Assert.assertFalse(resc.getBody().contains("\"total\" : 6")); + + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/TracingTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/TracingTests.java new file mode 100644 index 0000000000..75cb19e11e --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/TracingTests.java @@ -0,0 +1,530 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import java.lang.Thread.UncaughtExceptionHandler; + +import org.apache.http.HttpStatus; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentType; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.test.DynamicSecurityConfig; +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterConfiguration; +import com.amazon.opendistroforelasticsearch.security.test.helper.file.FileHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; + +@Ignore("subject for manual execution") +public class TracingTests extends SingleClusterTest { + + @Test + public void testAdvancedMapping() throws Exception { + setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY, true, ClusterConfiguration.DEFAULT); + + try (TransportClient tc = getInternalTransportClient(this.clusterInfo, Settings.EMPTY)) { + tc.admin().indices().create(new CreateIndexRequest("myindex1") + .mapping("mytype1", FileHelper.loadFile("mapping1.json"), XContentType.JSON)).actionGet(); + tc.admin().indices().create(new CreateIndexRequest("myindex2") + .mapping("mytype2", FileHelper.loadFile("mapping2.json"), XContentType.JSON)).actionGet(); + tc.admin().indices().create(new CreateIndexRequest("myindex3") + .mapping("mytype3", FileHelper.loadFile("mapping3.json"), XContentType.JSON)).actionGet(); + tc.admin().indices().create(new CreateIndexRequest("myindex4") + .mapping("mytype4", FileHelper.loadFile("mapping4.json"), XContentType.JSON)).actionGet(); + } + + RestHelper rh = nonSslRestHelper(); + System.out.println("############ write into mapping 1"); + String data1 = FileHelper.loadFile("data1.json"); + System.out.println(rh.executePutRequest("myindex1/mytype1/1?refresh", data1, encodeBasicHeader("nagilum", "nagilum"))); + System.out.println(rh.executePutRequest("myindex1/mytype1/1?refresh", data1, encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ write into mapping 2"); + System.out.println(rh.executePutRequest("myindex2/mytype2/2?refresh", data1, encodeBasicHeader("nagilum", "nagilum"))); + System.out.println(rh.executePutRequest("myindex2/mytype2/2?refresh", data1, encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ write into mapping 3"); + String parent = FileHelper.loadFile("data2.json"); + String child = FileHelper.loadFile("data3.json"); + System.out.println(rh.executePutRequest("myindex3/mytype3/1?refresh", parent, encodeBasicHeader("nagilum", "nagilum"))); + System.out.println(rh.executePutRequest("myindex3/mytype3/2?routing=1&refresh", child, encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ write into mapping 4"); + System.out.println(rh.executePutRequest("myindex4/mytype4/1?refresh", parent, encodeBasicHeader("nagilum", "nagilum"))); + System.out.println(rh.executePutRequest("myindex4/mytype4/2?routing=1&refresh", child, encodeBasicHeader("nagilum", "nagilum"))); + } + + @Test + public void testHTTPTraceNoSource() throws Exception { + + setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY, true, ClusterConfiguration.DEFAULT); + + try (TransportClient tc = getInternalTransportClient(this.clusterInfo, Settings.EMPTY)) { + tc.admin().indices().create(new CreateIndexRequest("a")).actionGet(); + tc.admin().indices().create(new CreateIndexRequest("c")).actionGet(); + tc.admin().indices().create(new CreateIndexRequest("test")).actionGet(); + tc.admin().indices().create(new CreateIndexRequest("u")).actionGet(); + + tc.admin().indices().putMapping(new PutMappingRequest("a").type("b") + .source("_source","enabled=false","content","store=true,type=text","field1","store=true,type=text", "field2","store=true,type=text", "a","store=true,type=text", "b","store=true,type=text", "my.nested.field","store=true,type=text") + ).actionGet(); + + tc.admin().indices().putMapping(new PutMappingRequest("c").type("d") + .source("_source","enabled=false","content","store=true,type=text","field1","store=true,type=text", "field2","store=true,type=text", "a","store=true,type=text", "b","store=true,type=text", "my.nested.field","store=true,type=text") + ).actionGet(); + + tc.admin().indices().putMapping(new PutMappingRequest("test").type("type1") + .source("_source","enabled=false","content","store=true,type=text","field1","store=true,type=text", "field2","store=true,type=text", "a","store=true,type=text", "b","store=true,type=text", "my.nested.field","store=true,type=text") + ).actionGet(); + + tc.admin().indices().putMapping(new PutMappingRequest("u").type("b") + .source("_source","enabled=false","content","store=true,type=text","field1","store=true,type=text", "field2","store=true,type=text", "a","store=true,type=text", "b","store=true,type=text", "my.nested.field","store=true,type=text") + ).actionGet(); + + for(int i=0; i<50;i++) { + tc.index(new IndexRequest("a").type("b").id(i+"").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":"+i+"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("c").type("d").id(i+"").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":"+i+"}", XContentType.JSON)).actionGet(); + } + } + + //setup complex mapping with parent child and nested fields + + + RestHelper rh = nonSslRestHelper(); + System.out.println("############ check shards"); + System.out.println(rh.executeGetRequest("_cat/shards?v", encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ _bulk"); + String bulkBody = + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"1\" } }"+System.lineSeparator()+ + "{ \"field1\" : \"value1\" }" +System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"delete\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator(); + + System.out.println(rh.executePostRequest("_bulk?refresh=true", bulkBody, encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ _bulk"); + bulkBody = + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"1\" } }"+System.lineSeparator()+ + "{ \"field1\" : \"value1\" }" +System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"delete\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator(); + + System.out.println(rh.executePostRequest("_bulk?refresh=true", bulkBody, encodeBasicHeader("nagilum", "nagilum"))); + + + System.out.println("############ cat indices"); + //cluster:monitor/state + //cluster:monitor/health + //indices:monitor/stats + System.out.println(rh.executeGetRequest("_cat/indices", encodeBasicHeader("nagilum", "nagilum"))); + + + System.out.println("############ _search"); + //indices:data/read/search + System.out.println(rh.executeGetRequest("_search", encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ get 1"); + //indices:data/read/get + System.out.println(rh.executeGetRequest("a/b/1", encodeBasicHeader("nagilum", "nagilum"))); + System.out.println("############ get 5"); + System.out.println(rh.executeGetRequest("a/b/5", encodeBasicHeader("nagilum", "nagilum"))); + System.out.println("############ get 17"); + System.out.println(rh.executeGetRequest("a/b/17", encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ index (+create index)"); + //indices:data/write/index + //indices:data/write/bulk + //indices:admin/create + //indices:data/write/bulk[s] + System.out.println(rh.executePostRequest("u/b/1?refresh=true", "{}",encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ index only"); + //indices:data/write/index + //indices:data/write/bulk + //indices:admin/create + //indices:data/write/bulk[s] + System.out.println(rh.executePostRequest("u/b/2?refresh=true", "{}",encodeBasicHeader("nagilum", "nagilum"))); + + + System.out.println("############ delete"); + //indices:data/write/index + //indices:data/write/bulk + //indices:admin/create + //indices:data/write/bulk[s] + System.out.println(rh.executeDeleteRequest("u/b/2?refresh=true",encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ msearch"); + String msearchBody = + "{\"index\":\"a\", \"type\":\"b\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator()+ + "{\"index\":\"a\", \"type\":\"b\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator()+ + "{\"index\":\"public\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator(); + + + System.out.println(rh.executePostRequest("_msearch", msearchBody, encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ mget"); + String mgetBody = "{"+ + "\"docs\" : ["+ + "{"+ + "\"_index\" : \"a\","+ + "\"_type\" : \"b\","+ + "\"_id\" : \"1\""+ + " },"+ + " {"+ + "\"_index\" : \"a\","+ + " \"_type\" : \"b\","+ + " \"_id\" : \"12\""+ + "},"+ + " {"+ + "\"_index\" : \"a\","+ + " \"_type\" : \"b\","+ + " \"_id\" : \"13\""+ + "},"+" {"+ + "\"_index\" : \"a\","+ + " \"_type\" : \"b\","+ + " \"_id\" : \"14\""+ + "}"+ + "]"+ + "}"; + + System.out.println(rh.executePostRequest("_mget?refresh=true", mgetBody, encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ delete by query"); + String dbqBody = "{"+ + ""+ + " \"query\": { "+ + " \"match\": {"+ + " \"content\": 12"+ + " }"+ + " }"+ + "}"; + + System.out.println(rh.executePostRequest("a/b/_delete_by_query", dbqBody, encodeBasicHeader("nagilum", "nagilum"))); + + Thread.sleep(5000); + } + + @Test + public void testHTTPSingle() throws Exception { + + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { + + @Override + public void uncaughtException(Thread t, Throwable e) { + e.printStackTrace(); + + } + }); + + final Settings settings = Settings.builder() + .putList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+".worf", "knuddel","nonexists") + .build(); + setup(settings); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + tc.admin().indices().create(new CreateIndexRequest("copysf")).actionGet(); + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_academy").type("students").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("starfleet_library").type("public").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("klingonempire").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("public").type("legends").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.index(new IndexRequest("spock").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("kirk").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("role01_role02").type("type01").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("starfleet","starfleet_academy","starfleet_library").alias("sf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("klingonempire","vulcangov").alias("nonsf"))).actionGet(); + tc.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(AliasActions.add().indices("public").alias("unrestricted"))).actionGet(); + + } + + System.out.println("########pause1"); + Thread.sleep(5000); + System.out.println("########end pause1"); + + System.out.println("########search"); + Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("_search", encodeBasicHeader("nagilum", "nagilum")).getStatusCode()); + System.out.println("########search done"); + + System.out.println("########pause2"); + Thread.sleep(5000); + System.out.println("########end pause2"); + + System.out.println("############ _bulk"); + String bulkBody = + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"1\" } }"+System.lineSeparator()+ + "{ \"field1\" : \"value1\" }" +System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"delete\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"myindex\", \"_type\" : \"myindex\", \"_id\" : \"1\" } }"+System.lineSeparator()+ + "{ \"field1\" : \"value1\" }" +System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"myindex\", \"_type\" : \"myindex\", \"_id\" : \"1\" } }"+System.lineSeparator()+ + "{ \"field1\" : \"value1\" }" +System.lineSeparator(); + + System.out.println(rh.executePostRequest("_bulk?refresh=true", bulkBody, encodeBasicHeader("nagilum", "nagilum")).getBody()); + System.out.println("############ _end"); + Thread.sleep(5000); + } + + @Test + public void testSearchScroll() throws Exception { + + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { + + @Override + public void uncaughtException(Thread t, Throwable e) { + e.printStackTrace(); + + } + }); + + final Settings settings = Settings.builder() + .putList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+".worf", "knuddel","nonexists") + .build(); + setup(settings); + final RestHelper rh = nonSslRestHelper(); + + try (TransportClient tc = getInternalTransportClient()) { + for(int i=0; i<3; i++) + tc.index(new IndexRequest("vulcangov").type("kolinahr").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + } + + + System.out.println("########search"); + HttpResponse res; + Assert.assertEquals(HttpStatus.SC_OK, (res=rh.executeGetRequest("vulcangov/_search?scroll=1m&pretty=true", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + + System.out.println(res.getBody()); + int start = res.getBody().indexOf("_scroll_id") + 15; + String scrollid = res.getBody().substring(start, res.getBody().indexOf("\"", start+1)); + System.out.println(scrollid); + System.out.println("########search scroll"); + Assert.assertEquals(HttpStatus.SC_OK, (res=rh.executePostRequest("/_search/scroll?pretty=true", "{\"scroll_id\" : \""+scrollid+"\"}", encodeBasicHeader("nagilum", "nagilum"))).getStatusCode()); + + + System.out.println("########search done"); + + + } + + @Test + public void testHTTPTrace() throws Exception { + + setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY, true, ClusterConfiguration.DEFAULT); + + try (TransportClient tc = getInternalTransportClient(this.clusterInfo, Settings.EMPTY)) { + + for(int i=0; i<50;i++) { + tc.index(new IndexRequest("a").type("b").id(i+"").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":"+i+"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("c").type("d").id(i+"").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":"+i+"}", XContentType.JSON)).actionGet(); + } + } + + + + + RestHelper rh = nonSslRestHelper(); + System.out.println("############ check shards"); + System.out.println(rh.executeGetRequest("_cat/shards?v", encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ _bulk"); + String bulkBody = + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"1\" } }"+System.lineSeparator()+ + "{ \"field1\" : \"value1\" }" +System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"delete\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator(); + + System.out.println(rh.executePostRequest("_bulk?refresh=true", bulkBody, encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ _bulk"); + bulkBody = + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"1\" } }"+System.lineSeparator()+ + "{ \"field1\" : \"value1\" }" +System.lineSeparator()+ + "{ \"index\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator()+ + "{ \"field2\" : \"value2\" }"+System.lineSeparator()+ + "{ \"delete\" : { \"_index\" : \"test\", \"_type\" : \"type1\", \"_id\" : \"2\" } }"+System.lineSeparator(); + + System.out.println(rh.executePostRequest("_bulk?refresh=true", bulkBody, encodeBasicHeader("nagilum", "nagilum"))); + + + System.out.println("############ cat indices"); + //cluster:monitor/state + //cluster:monitor/health + //indices:monitor/stats + System.out.println(rh.executeGetRequest("_cat/indices", encodeBasicHeader("nagilum", "nagilum"))); + + + System.out.println("############ _search"); + //indices:data/read/search + System.out.println(rh.executeGetRequest("_search", encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ get 1"); + //indices:data/read/get + System.out.println(rh.executeGetRequest("a/b/1", encodeBasicHeader("nagilum", "nagilum"))); + System.out.println("############ get 5"); + System.out.println(rh.executeGetRequest("a/b/5", encodeBasicHeader("nagilum", "nagilum"))); + System.out.println("############ get 17"); + System.out.println(rh.executeGetRequest("a/b/17", encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ index (+create index)"); + //indices:data/write/index + //indices:data/write/bulk + //indices:admin/create + //indices:data/write/bulk[s] + System.out.println(rh.executePostRequest("u/b/1?refresh=true", "{}",encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ index only"); + //indices:data/write/index + //indices:data/write/bulk + //indices:admin/create + //indices:data/write/bulk[s] + System.out.println(rh.executePostRequest("u/b/2?refresh=true", "{}",encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ update"); + //indices:data/write/index + //indices:data/write/bulk + //indices:admin/create + //indices:data/write/bulk[s] + System.out.println(rh.executePostRequest("u/b/2/_update?refresh=true", "{\"doc\" : {\"a\":1}}",encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ update2"); + //indices:data/write/index + //indices:data/write/bulk + //indices:admin/create + //indices:data/write/bulk[s] + System.out.println(rh.executePostRequest("u/b/2/_update?refresh=true", "{\"doc\" : {\"a\":44, \"b\":55}}",encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ update3"); + //indices:data/write/index + //indices:data/write/bulk + //indices:admin/create + //indices:data/write/bulk[s] + System.out.println(rh.executePostRequest("u/b/2/_update?refresh=true", "{\"doc\" : {\"b\":66}}",encodeBasicHeader("nagilum", "nagilum"))); + + + System.out.println("############ delete"); + //indices:data/write/index + //indices:data/write/bulk + //indices:admin/create + //indices:data/write/bulk[s] + System.out.println(rh.executeDeleteRequest("u/b/2?refresh=true",encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ reindex"); + String reindex = + "{"+ + " \"source\": {"+ + " \"index\": \"a\""+ + " },"+ + " \"dest\": {"+ + " \"index\": \"new_a\""+ + " }"+ + "}"; + + System.out.println(rh.executePostRequest("_reindex", reindex, encodeBasicHeader("nagilum", "nagilum"))); + + + System.out.println("############ msearch"); + String msearchBody = + "{\"index\":\"a\", \"type\":\"b\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator()+ + "{\"index\":\"a\", \"type\":\"b\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator()+ + "{\"index\":\"public\", \"ignore_unavailable\": true}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator(); + + + System.out.println(rh.executePostRequest("_msearch", msearchBody, encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ mget"); + String mgetBody = "{"+ + "\"docs\" : ["+ + "{"+ + "\"_index\" : \"a\","+ + "\"_type\" : \"b\","+ + "\"_id\" : \"1\""+ + " },"+ + " {"+ + "\"_index\" : \"a\","+ + " \"_type\" : \"b\","+ + " \"_id\" : \"12\""+ + "},"+ + " {"+ + "\"_index\" : \"a\","+ + " \"_type\" : \"b\","+ + " \"_id\" : \"13\""+ + "},"+" {"+ + "\"_index\" : \"a\","+ + " \"_type\" : \"b\","+ + " \"_id\" : \"14\""+ + "}"+ + "]"+ + "}"; + + System.out.println(rh.executePostRequest("_mget?refresh=true", mgetBody, encodeBasicHeader("nagilum", "nagilum"))); + + System.out.println("############ delete by query"); + String dbqBody = "{"+ + ""+ + " \"query\": { "+ + " \"match\": {"+ + " \"content\": 12"+ + " }"+ + " }"+ + "}"; + + System.out.println(rh.executePostRequest("a/b/_delete_by_query", dbqBody, encodeBasicHeader("nagilum", "nagilum"))); + + Thread.sleep(5000); + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/TransportClientIntegrationTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/TransportClientIntegrationTests.java new file mode 100644 index 0000000000..e067ab059b --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/TransportClientIntegrationTests.java @@ -0,0 +1,780 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import org.apache.http.Header; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.DocWriteResponse.Result; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext; +import org.elasticsearch.common.xcontent.XContentType; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateAction; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateRequest; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateResponse; +import com.amazon.opendistroforelasticsearch.security.ssl.util.ExceptionUtils; +import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.test.DynamicSecurityConfig; +import com.amazon.opendistroforelasticsearch.security.test.SingleClusterTest; +import com.amazon.opendistroforelasticsearch.security.test.helper.file.FileHelper; + +public class TransportClientIntegrationTests extends SingleClusterTest { + + @Test + public void testTransportClient() throws Exception { + + final Settings settings = Settings.builder() + .putList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_IMPERSONATION_DN+".CN=spock,OU=client,O=client,L=Test,C=DE", "worf", "nagilum") + .put("discovery.initial_state_timeout","8s") + .build(); + setup(settings); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + } + + + Settings tcSettings = Settings.builder() + .put(settings) + .put("opendistro_security.ssl.transport.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("spock-keystore.jks")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS,"spock") + .build(); + + System.out.println("------- 0 ---------"); + + try (TransportClient tc = getInternalTransportClient(clusterInfo, tcSettings)) { + + Assert.assertEquals(clusterInfo.numNodes, tc.admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet().getNodes().size()); + + System.out.println("------- 1 ---------"); + + CreateIndexResponse cir = tc.admin().indices().create(new CreateIndexRequest("vulcan")).actionGet(); + Assert.assertTrue(cir.isAcknowledged()); + + System.out.println("------- 2 ---------"); + + IndexResponse ir = tc.index(new IndexRequest("vulcan").type("secrets").id("s1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"secret\":true}", XContentType.JSON)).actionGet(); + Assert.assertTrue(ir.getResult() == Result.CREATED); + + System.out.println("------- 3 ---------"); + + GetResponse gr =tc.prepareGet("vulcan", "secrets", "s1").setRealtime(true).get(); + Assert.assertTrue(gr.isExists()); + + System.out.println("------- 4 ---------"); + + gr =tc.prepareGet("vulcan", "secrets", "s1").setRealtime(false).get(); + Assert.assertTrue(gr.isExists()); + + System.out.println("------- 5 ---------"); + + SearchResponse actionGet = tc.search(new SearchRequest("vulcan").types("secrets")).actionGet(); + Assert.assertEquals(1, actionGet.getHits().getHits().length); + System.out.println("------- 6 ---------"); + + gr =tc.prepareGet(".opendistro_security", "security", "config").setRealtime(false).get(); + Assert.assertFalse(gr.isExists()); + + System.out.println("------- 7 ---------"); + + gr =tc.prepareGet(".opendistro_security", "security", "config").setRealtime(true).get(); + Assert.assertFalse(gr.isExists()); + + System.out.println("------- 8 ---------"); + + actionGet = tc.search(new SearchRequest(".opendistro_security")).actionGet(); + Assert.assertEquals(0, actionGet.getHits().getHits().length); + + System.out.println("------- 9 ---------"); + + try { + tc.index(new IndexRequest(".opendistro_security").type("security").id("config").source("config", FileHelper.readYamlContent("config.yml"))).actionGet(); + Assert.fail(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + + System.out.println("------- 10 ---------"); + + //impersonation + try { + + StoredContext ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "worf"); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + } finally { + ctx.close(); + } + Assert.fail(); + } catch (ElasticsearchSecurityException e) { + Assert.assertTrue(e.getMessage(), e.getMessage().startsWith("no permissions for [indices:data/read/get]")); + } + + System.out.println("------- 11 ---------"); + + StoredContext ctx = tc.threadPool().getThreadContext().stashContext(); + try { + Header header = encodeBasicHeader("worf", "worf"); + tc.threadPool().getThreadContext().putHeader(header.getName(), header.getValue()); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + Assert.fail(); + } catch (ElasticsearchSecurityException e) { + Assert.assertTrue(e.getMessage().startsWith("no permissions for [indices:data/read/get]")); + } finally { + ctx.close(); + } + + System.out.println("------- 12 ---------"); + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + Header header = encodeBasicHeader("worf", "worf111"); + tc.threadPool().getThreadContext().putHeader(header.getName(), header.getValue()); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + Assert.fail(); + } catch (ElasticsearchSecurityException e) { + e.printStackTrace(); + //Assert.assertTrue(e.getCause().getMessage().contains("password does not match")); + } finally { + ctx.close(); + } + + System.out.println("------- 13 ---------"); + + //impersonation + try { + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "gkar"); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + Assert.fail(); + } finally { + ctx.close(); + } + + } catch (ElasticsearchSecurityException e) { + Assert.assertEquals("'CN=spock,OU=client,O=client,L=Test,C=DE' is not allowed to impersonate as 'gkar'", e.getMessage()); + } + + System.out.println("------- 12 ---------"); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + gr = tc.prepareGet(".opendistro_security", "security", "config").setRealtime(Boolean.TRUE).get(); + Assert.assertFalse(gr.isExists()); + Assert.assertTrue(gr.isSourceEmpty()); + } finally { + ctx.close(); + } + + System.out.println("------- 13 ---------"); + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + gr = tc.prepareGet(".opendistro_security", "config", "0").setRealtime(Boolean.FALSE).get(); + Assert.assertFalse(gr.isExists()); + Assert.assertTrue(gr.isSourceEmpty()); + } finally { + ctx.close(); + } + System.out.println("------- 13.1 ---------"); + + String scrollId = null; + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + SearchResponse searchRes = tc.prepareSearch("starfleet").setTypes("ships").setScroll(TimeValue.timeValueMinutes(5)).get(); + scrollId = searchRes.getScrollId(); + } finally { + ctx.close(); + } + + System.out.println("------- 13.2 ---------"); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + tc.prepareSearchScroll(scrollId).get(); + } finally { + ctx.close(); + } + + + System.out.println("------- 14 ---------"); + + boolean ok=false; + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + ok = true; + ctx.close(); + ctx = tc.threadPool().getThreadContext().stashContext(); + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + Header header = encodeBasicHeader("worf", "worf"); + tc.threadPool().getThreadContext().putHeader(header.getName(), header.getValue()); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + Assert.fail(); + } catch (ElasticsearchSecurityException e) { + Assert.assertTrue(e.getMessage().startsWith("no permissions for [indices:data/read/get]")); + Assert.assertTrue(ok); + } finally { + ctx.close(); + } + + System.out.println("------- 15 ---------"); + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + gr = tc.prepareGet(".opendistro_security", "security", "config").setRealtime(Boolean.TRUE).get(); + Assert.assertFalse(gr.isExists()); + Assert.assertTrue(gr.isSourceEmpty()); + } finally { + ctx.close(); + } + + System.out.println("------- 15 0---------"); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + Header header = encodeBasicHeader("worf", "worf"); + tc.threadPool().getThreadContext().putHeader(header.getName(), header.getValue()); + gr = tc.prepareGet(".opendistro_security", "security", "config").setRealtime(Boolean.TRUE).get(); + Assert.fail(); + } catch (Exception e) { + Assert.assertTrue(e.getMessage().contains("no permissions for [indices:data/read/get] and User [name=worf")); + } + finally { + ctx.close(); + } + + + System.out.println("------- 15 1---------"); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + Header header = encodeBasicHeader("nagilum", "nagilum"); + tc.threadPool().getThreadContext().putHeader(header.getName(), header.getValue()); + gr = tc.prepareGet(".opendistro_security", "security", "config").setRealtime(Boolean.TRUE).get(); + Assert.assertFalse(gr.isExists()); + Assert.assertTrue(gr.isSourceEmpty()); + } finally { + ctx.close(); + } + + System.out.println("------- 16---------"); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + gr = tc.prepareGet(".opendistro_security", "security", "config").setRealtime(Boolean.FALSE).get(); + Assert.assertFalse(gr.isExists()); + Assert.assertTrue(gr.isSourceEmpty()); + } finally { + ctx.close(); + } + + ctx = tc.threadPool().getThreadContext().stashContext(); + SearchResponse searchRes = null; + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + searchRes = tc.prepareSearch("starfleet").setTypes("ships").setScroll(TimeValue.timeValueMinutes(5)).get(); + } finally { + ctx.close(); + } + + Assert.assertNotNull(searchRes.getScrollId()); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "worf"); + tc.prepareSearchScroll(searchRes.getScrollId()).get(); + Assert.fail(); + } catch (Exception e) { + Throwable root = ExceptionUtils.getRootCause(e); + e.printStackTrace(); + Assert.assertTrue(root.getMessage().contains("Wrong user in scroll context")); + } + finally { + ctx.close(); + } + + + ctx = tc.threadPool().getThreadContext().stashContext(); + searchRes = null; + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + searchRes = tc.prepareSearch("starfleet").setTypes("ships").setScroll(TimeValue.timeValueMinutes(5)).get(); + SearchResponse scrollRes = tc.prepareSearchScroll(searchRes.getScrollId()).get(); + Assert.assertEquals(0, scrollRes.getFailedShards()); + } finally { + ctx.close(); + } + + System.out.println("------- TRC end ---------"); + } + + System.out.println("------- CTC end ---------"); + } + + @Test + public void testTransportClientImpersonation() throws Exception { + + final Settings settings = Settings.builder() + .putList("opendistro_security.authcz.impersonation_dn.CN=spock,OU=client,O=client,L=Test,C=DE", "worf", "nagilum") + .build(); + + + setup(settings); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config","roles","rolesmapping","internalusers","actiongroups"})).actionGet(); + Assert.assertEquals(clusterInfo.numNodes, cur.getNodes().size()); + + } + + Settings tcSettings = Settings.builder() + .put("opendistro_security.ssl.transport.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("spock-keystore.jks")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS,"spock") + .put("path.home", ".") + .put("request.headers.opendistro_security_impersonate_as", "worf") + .build(); + + try (TransportClient tc = getInternalTransportClient(clusterInfo, tcSettings)) { + NodesInfoRequest nir = new NodesInfoRequest(); + Assert.assertEquals(clusterInfo.numNodes, tc.admin().cluster().nodesInfo(nir).actionGet().getNodes().size()); + } + } + + @Test + public void testTransportClientImpersonationWildcard() throws Exception { + + final Settings settings = Settings.builder() + .putList("opendistro_security.authcz.impersonation_dn.CN=spock,OU=client,O=client,L=Test,C=DE", "*") + .build(); + + + setup(settings); + + Settings tcSettings = Settings.builder() + .put("opendistro_security.ssl.transport.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("spock-keystore.jks")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS,"spock") + .put("path.home", ".") + .put("request.headers.opendistro_security_impersonate_as", "worf") + .build(); + + try (TransportClient tc = getInternalTransportClient(clusterInfo, tcSettings)) { + NodesInfoRequest nir = new NodesInfoRequest(); + Assert.assertEquals(clusterInfo.numNodes, tc.admin().cluster().nodesInfo(nir).actionGet().getNodes().size()); + } + } + + //--- + + @Test + public void testTransportClientUsernameAttribute() throws Exception { + + final Settings settings = Settings.builder() + .putList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_IMPERSONATION_DN+".CN=spock,OU=client,O=client,L=Test,C=DE", "worf", "nagilum") + .put("discovery.initial_state_timeout","8s") + .build(); + + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_transport_username.yml") + .setSecurityRolesMapping("roles_mapping_transport_username.yml") + .setSecurityInternalUsers("internal_users_transport_username.yml") + , settings); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + } + + + Settings tcSettings = Settings.builder() + .put(settings) + .put("opendistro_security.ssl.transport.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("spock-keystore.jks")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS,"spock") + .build(); + + System.out.println("------- 0 ---------"); + + try (TransportClient tc = getInternalTransportClient(clusterInfo, tcSettings)) { + + Assert.assertEquals(clusterInfo.numNodes, tc.admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet().getNodes().size()); + + System.out.println("------- 1 ---------"); + + CreateIndexResponse cir = tc.admin().indices().create(new CreateIndexRequest("vulcan")).actionGet(); + Assert.assertTrue(cir.isAcknowledged()); + + System.out.println("------- 2 ---------"); + + IndexResponse ir = tc.index(new IndexRequest("vulcan").type("secrets").id("s1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"secret\":true}", XContentType.JSON)).actionGet(); + Assert.assertTrue(ir.getResult() == Result.CREATED); + + System.out.println("------- 3 ---------"); + + GetResponse gr =tc.prepareGet("vulcan", "secrets", "s1").setRealtime(true).get(); + Assert.assertTrue(gr.isExists()); + + System.out.println("------- 4 ---------"); + + gr =tc.prepareGet("vulcan", "secrets", "s1").setRealtime(false).get(); + Assert.assertTrue(gr.isExists()); + + System.out.println("------- 5 ---------"); + + SearchResponse actionGet = tc.search(new SearchRequest("vulcan").types("secrets")).actionGet(); + Assert.assertEquals(1, actionGet.getHits().getHits().length); + System.out.println("------- 6 ---------"); + + gr =tc.prepareGet(".opendistro_security", "security", "config").setRealtime(false).get(); + Assert.assertFalse(gr.isExists()); + + System.out.println("------- 7 ---------"); + + gr =tc.prepareGet(".opendistro_security", "security", "config").setRealtime(true).get(); + Assert.assertFalse(gr.isExists()); + + System.out.println("------- 8 ---------"); + + actionGet = tc.search(new SearchRequest(".opendistro_security")).actionGet(); + Assert.assertEquals(0, actionGet.getHits().getHits().length); + + System.out.println("------- 9 ---------"); + + try { + tc.index(new IndexRequest(".opendistro_security").type("security").id("config").source("config", FileHelper.readYamlContent("config.yml"))).actionGet(); + Assert.fail(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + + System.out.println("------- 10 ---------"); + + //impersonation + try { + + StoredContext ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "worf"); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + } finally { + ctx.close(); + } + Assert.fail(); + } catch (ElasticsearchSecurityException e) { + Assert.assertTrue(e.getMessage(), e.getMessage().startsWith("no permissions for [indices:data/read/get]")); + } + + System.out.println("------- 11 ---------"); + + StoredContext ctx = tc.threadPool().getThreadContext().stashContext(); + try { + Header header = encodeBasicHeader("worf", "worf"); + tc.threadPool().getThreadContext().putHeader(header.getName(), header.getValue()); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + Assert.fail(); + } catch (ElasticsearchSecurityException e) { + Assert.assertTrue(e.getMessage().startsWith("no permissions for [indices:data/read/get]")); + } finally { + ctx.close(); + } + + System.out.println("------- 12 ---------"); + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + Header header = encodeBasicHeader("worf", "worf111"); + tc.threadPool().getThreadContext().putHeader(header.getName(), header.getValue()); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + Assert.fail(); + } catch (ElasticsearchSecurityException e) { + e.printStackTrace(); + //Assert.assertTrue(e.getCause().getMessage().contains("password does not match")); + } finally { + ctx.close(); + } + + System.out.println("------- 13 ---------"); + + //impersonation + try { + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "gkar"); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + Assert.fail(); + } finally { + ctx.close(); + } + + } catch (ElasticsearchSecurityException e) { + Assert.assertEquals("'CN=spock,OU=client,O=client,L=Test,C=DE' is not allowed to impersonate as 'gkar'", e.getMessage()); + } + + System.out.println("------- 12 ---------"); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + gr = tc.prepareGet(".opendistro_security", "security", "config").setRealtime(Boolean.TRUE).get(); + Assert.assertFalse(gr.isExists()); + Assert.assertTrue(gr.isSourceEmpty()); + } finally { + ctx.close(); + } + + System.out.println("------- 13 ---------"); + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + gr = tc.prepareGet(".opendistro_security", "config", "0").setRealtime(Boolean.FALSE).get(); + Assert.assertFalse(gr.isExists()); + Assert.assertTrue(gr.isSourceEmpty()); + } finally { + ctx.close(); + } + System.out.println("------- 13.1 ---------"); + + String scrollId = null; + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + SearchResponse searchRes = tc.prepareSearch("starfleet").setTypes("ships").setScroll(TimeValue.timeValueMinutes(5)).get(); + scrollId = searchRes.getScrollId(); + } finally { + ctx.close(); + } + + System.out.println("------- 13.2 ---------"); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + tc.prepareSearchScroll(scrollId).get(); + } finally { + ctx.close(); + } + + + System.out.println("------- 14 ---------"); + + boolean ok=false; + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + ok = true; + ctx.close(); + ctx = tc.threadPool().getThreadContext().stashContext(); + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + Header header = encodeBasicHeader("worf", "worf"); + tc.threadPool().getThreadContext().putHeader(header.getName(), header.getValue()); + gr = tc.prepareGet("vulcan", "secrets", "s1").get(); + Assert.fail(); + } catch (ElasticsearchSecurityException e) { + Assert.assertTrue(e.getMessage().startsWith("no permissions for [indices:data/read/get]")); + Assert.assertTrue(ok); + } finally { + ctx.close(); + } + + System.out.println("------- 15 ---------"); + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + gr = tc.prepareGet(".opendistro_security", "security", "config").setRealtime(Boolean.TRUE).get(); + Assert.assertFalse(gr.isExists()); + Assert.assertTrue(gr.isSourceEmpty()); + } finally { + ctx.close(); + } + + System.out.println("------- 15 0---------"); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + Header header = encodeBasicHeader("worf", "worf"); + tc.threadPool().getThreadContext().putHeader(header.getName(), header.getValue()); + gr = tc.prepareGet(".opendistro_security", "security", "config").setRealtime(Boolean.TRUE).get(); + Assert.fail(); + } catch (Exception e) { + Assert.assertTrue(e.getMessage().contains("no permissions for [indices:data/read/get] and User [name=worf")); + } + finally { + ctx.close(); + } + + + System.out.println("------- 15 1---------"); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + Header header = encodeBasicHeader("nagilum", "nagilum"); + tc.threadPool().getThreadContext().putHeader(header.getName(), header.getValue()); + gr = tc.prepareGet(".opendistro_security", "security", "config").setRealtime(Boolean.TRUE).get(); + Assert.assertFalse(gr.isExists()); + Assert.assertTrue(gr.isSourceEmpty()); + } finally { + ctx.close(); + } + + System.out.println("------- 16---------"); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + gr = tc.prepareGet(".opendistro_security", "security", "config").setRealtime(Boolean.FALSE).get(); + Assert.assertFalse(gr.isExists()); + Assert.assertTrue(gr.isSourceEmpty()); + } finally { + ctx.close(); + } + + ctx = tc.threadPool().getThreadContext().stashContext(); + SearchResponse searchRes = null; + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + searchRes = tc.prepareSearch("starfleet").setTypes("ships").setScroll(TimeValue.timeValueMinutes(5)).get(); + } finally { + ctx.close(); + } + + Assert.assertNotNull(searchRes.getScrollId()); + + ctx = tc.threadPool().getThreadContext().stashContext(); + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "worf"); + tc.prepareSearchScroll(searchRes.getScrollId()).get(); + Assert.fail(); + } catch (Exception e) { + Throwable root = ExceptionUtils.getRootCause(e); + e.printStackTrace(); + Assert.assertTrue(root.getMessage().contains("Wrong user in scroll context")); + } + finally { + ctx.close(); + } + + + ctx = tc.threadPool().getThreadContext().stashContext(); + searchRes = null; + try { + tc.threadPool().getThreadContext().putHeader("opendistro_security_impersonate_as", "nagilum"); + searchRes = tc.prepareSearch("starfleet").setTypes("ships").setScroll(TimeValue.timeValueMinutes(5)).get(); + SearchResponse scrollRes = tc.prepareSearchScroll(searchRes.getScrollId()).get(); + Assert.assertEquals(0, scrollRes.getFailedShards()); + } finally { + ctx.close(); + } + + System.out.println("------- TRC end ---------"); + } + + System.out.println("------- CTC end ---------"); + } + + @Test + public void testTransportClientImpersonationUsernameAttribute() throws Exception { + + final Settings settings = Settings.builder() + .putList("opendistro_security.authcz.impersonation_dn.CN=spock,OU=client,O=client,L=Test,C=DE", "worf", "nagilum") + .build(); + + + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_transport_username.yml") + .setSecurityRolesMapping("roles_mapping_transport_username.yml") + .setSecurityInternalUsers("internal_users_transport_username.yml") + , settings); + + try (TransportClient tc = getInternalTransportClient()) { + tc.index(new IndexRequest("starfleet").type("ships").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)).actionGet(); + + ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[]{"config","roles","rolesmapping","internalusers","actiongroups"})).actionGet(); + Assert.assertEquals(clusterInfo.numNodes, cur.getNodes().size()); + + } + + Settings tcSettings = Settings.builder() + .put("opendistro_security.ssl.transport.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("spock-keystore.jks")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS,"spock") + .put("path.home", ".") + .put("request.headers.opendistro_security_impersonate_as", "worf") + .build(); + + try (TransportClient tc = getInternalTransportClient(clusterInfo, tcSettings)) { + NodesInfoRequest nir = new NodesInfoRequest(); + Assert.assertEquals(clusterInfo.numNodes, tc.admin().cluster().nodesInfo(nir).actionGet().getNodes().size()); + } + } + + @Test + public void testTransportClientImpersonationWildcardUsernameAttribute() throws Exception { + + final Settings settings = Settings.builder() + .putList("opendistro_security.authcz.impersonation_dn.CN=spock,OU=client,O=client,L=Test,C=DE", "*") + .build(); + + setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_transport_username.yml") + .setSecurityRolesMapping("roles_mapping_transport_username.yml") + .setSecurityInternalUsers("internal_users_transport_username.yml") + , settings); + + Settings tcSettings = Settings.builder() + .put("opendistro_security.ssl.transport.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("spock-keystore.jks")) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS,"spock") + .put("path.home", ".") + .put("request.headers.opendistro_security_impersonate_as", "worf") + .build(); + + try (TransportClient tc = getInternalTransportClient(clusterInfo, tcSettings)) { + NodesInfoRequest nir = new NodesInfoRequest(); + Assert.assertEquals(clusterInfo.numNodes, tc.admin().cluster().nodesInfo(nir).actionGet().getNodes().size()); + } + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/UtilTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/UtilTests.java new file mode 100644 index 0000000000..38756f3d3e --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/UtilTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.support.OpenDistroSecurityUtils; +import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; + +public class UtilTests { + + @Test + public void testWildcards() { + Assert.assertTrue(!WildcardMatcher.match("a*?", "a")); + Assert.assertTrue(WildcardMatcher.match("a*?", "aa")); + Assert.assertTrue(WildcardMatcher.match("a*?", "ab")); + //Assert.assertTrue(WildcardMatcher.match("a*?", "abb")); + Assert.assertTrue(WildcardMatcher.match("*my*index", "myindex")); + Assert.assertTrue(!WildcardMatcher.match("*my*index", "myindex1")); + Assert.assertTrue(WildcardMatcher.match("*my*index?", "myindex1")); + Assert.assertTrue(WildcardMatcher.match("*my*index", "this_is_my_great_index")); + Assert.assertTrue(!WildcardMatcher.match("*my*index", "MYindex")); + Assert.assertTrue(!WildcardMatcher.match("?kibana", "kibana")); + Assert.assertTrue(WildcardMatcher.match("?kibana", ".kibana")); + Assert.assertTrue(!WildcardMatcher.match("?kibana", "kibana.")); + Assert.assertTrue(WildcardMatcher.match("?kibana?", "?kibana.")); + Assert.assertTrue(WildcardMatcher.match("/(\\d{3}-?\\d{2}-?\\d{4})/", "123-45-6789")); + Assert.assertTrue(!WildcardMatcher.match("(\\d{3}-?\\d{2}-?\\d{4})", "123-45-6789")); + Assert.assertTrue(WildcardMatcher.match("/\\S*/", "abc")); + Assert.assertTrue(WildcardMatcher.match("abc", "abc")); + Assert.assertTrue(!WildcardMatcher.match("ABC", "abc")); + Assert.assertTrue(!WildcardMatcher.containsWildcard("abc")); + Assert.assertTrue(!WildcardMatcher.containsWildcard("abc$")); + Assert.assertTrue(WildcardMatcher.containsWildcard("abc*")); + Assert.assertTrue(WildcardMatcher.containsWildcard("a?bc")); + Assert.assertTrue(WildcardMatcher.containsWildcard("/(\\d{3}-\\d{2}-?\\d{4})/")); + } + + @Test + public void testMapFromArray() { + Map map = OpenDistroSecurityUtils.mapFromArray((Object)null); + assertTrue(map == null); + + map = OpenDistroSecurityUtils.mapFromArray("key"); + assertTrue(map == null); + + map = OpenDistroSecurityUtils.mapFromArray("key", "value", "otherkey"); + assertTrue(map == null); + + map = OpenDistroSecurityUtils.mapFromArray("key", "value"); + assertNotNull(map); + assertEquals(1, map.size()); + assertEquals("value", map.get("key")); + + map = OpenDistroSecurityUtils.mapFromArray("key", "value", "key", "value"); + assertNotNull(map); + assertEquals(1, map.size()); + assertEquals("value", map.get("key")); + + map = OpenDistroSecurityUtils.mapFromArray("key1", "value1", "key2", "value2"); + assertNotNull(map); + assertEquals(2, map.size()); + assertEquals("value1", map.get("key1")); + assertEquals("value2", map.get("key2")); + + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/ccstest/CrossClusterSearchTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/ccstest/CrossClusterSearchTests.java new file mode 100644 index 0000000000..cfd6b21dbd --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/ccstest/CrossClusterSearchTests.java @@ -0,0 +1,265 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.ccstest; + +import com.amazon.opendistroforelasticsearch.security.test.AbstractSecurityUnitTest; +import org.apache.http.HttpStatus; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentType; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterConfiguration; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterInfo; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; + +public class CrossClusterSearchTests extends AbstractSecurityUnitTest { + + private final ClusterHelper cl1 = new ClusterHelper("crl1_n"+num.incrementAndGet()+"_f"+System.getProperty("forkno")+"_t"+System.nanoTime()); + private final ClusterHelper cl2 = new ClusterHelper("crl2_n"+num.incrementAndGet()+"_f"+System.getProperty("forkno")+"_t"+System.nanoTime()); + private ClusterInfo cl1Info; + private ClusterInfo cl2Info; + + private void setupCcs() throws Exception { + + System.setProperty("security.display_lic_none","true"); + + cl2Info = cl2.startCluster(minimumSecuritySettings(Settings.EMPTY), ClusterConfiguration.DEFAULT); + initialize(cl2Info); + System.out.println("### cl2 complete ###"); + + //cl1 is coordinating + cl1Info = cl1.startCluster(minimumSecuritySettings(crossClusterNodeSettings(cl2Info)), ClusterConfiguration.DEFAULT); + System.out.println("### cl1 start ###"); + initialize(cl1Info); + System.out.println("### cl1 initialized ###"); + } + + @After + public void tearDown() throws Exception { + cl1.stopCluster(); + cl2.stopCluster(); + } + + private Settings crossClusterNodeSettings(ClusterInfo remote) { + Settings.Builder builder = Settings.builder() + .putList("search.remote.cross_cluster_two.seeds", remote.nodeHost+":"+remote.nodePort); + return builder.build(); + } + + @Test + public void testCcs() throws Exception { + setupCcs(); + + final String cl1BodyMain = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("", encodeBasicHeader("nagilum","nagilum")).getBody(); + Assert.assertTrue(cl1BodyMain.contains("crl1")); + + try (TransportClient tc = getInternalTransportClient(cl1Info, Settings.EMPTY)) { + tc.index(new IndexRequest("twitter").type("tweet").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl1Info.clustername+"\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("twutter").type("tweet").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl1Info.clustername+"\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("special:index").type("spec").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl1Info.clustername+"\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("cross_cluster_two:xx").type("xx").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl1Info.clustername+"\"}", XContentType.JSON)).actionGet(); + } + + final String cl2BodyMain = new RestHelper(cl2Info, false, false, getResourceFolder()).executeGetRequest("", encodeBasicHeader("nagilum","nagilum")).getBody(); + Assert.assertTrue(cl2BodyMain.contains("crl2")); + + try (TransportClient tc = getInternalTransportClient(cl2Info, Settings.EMPTY)) { + tc.index(new IndexRequest("twitter").type("tweet").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl2Info.clustername+"\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("twutter").type("tweet").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl2Info.clustername+"\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("special:index").type("spec").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl2Info.clustername+"\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("cross_cluster_two:xx").type("xx").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl2Info.clustername+"\"}", XContentType.JSON)).actionGet(); + } + + HttpResponse ccs = null; + + System.out.println("###################### query 1"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("cross_cluster_two:*/_search?pretty", encodeBasicHeader("nagilum","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, ccs.getStatusCode()); + Assert.assertFalse(ccs.getBody().contains("crl1")); + Assert.assertTrue(ccs.getBody().contains("crl2")); + Assert.assertTrue(ccs.getBody().contains("twitter")); + + + System.out.println("###################### query 2"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("special:index/spec/_search?pretty", encodeBasicHeader("nagilum","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, ccs.getStatusCode()); + Assert.assertTrue(ccs.getBody().contains("crl1")); + Assert.assertFalse(ccs.getBody().contains("crl2")); + + System.out.println("###################### query 3"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("cross_cluster_two:special:index,special:index/spec/_search?pretty", encodeBasicHeader("nagilum","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, ccs.getStatusCode()); + Assert.assertTrue(ccs.getBody().contains("crl1")); + Assert.assertTrue(ccs.getBody().contains("crl2")); + Assert.assertTrue(ccs.getBody().contains("cross_cluster")); + + System.out.println("###################### query 4"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("cross_cluster_two:xx,xx/xx/_search?pretty", encodeBasicHeader("nagilum","nagilum")); + System.out.println(ccs.getBody()); + //TODO fix exception nesting + //Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, ccs.getStatusCode()); + //Assert.assertTrue(ccs.getBody().contains("Can not filter indices; index cross_cluster_two:xx exists but there is also a remote cluster named: cross_cluster_two")); + + System.out.println("###################### query 5"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("cross_cluster_two:abcnonext/xx/_search?pretty", encodeBasicHeader("nagilum","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_NOT_FOUND, ccs.getStatusCode()); + Assert.assertTrue(ccs.getBody().contains("index_not_found_exception")); + + System.out.println("###################### query 6"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("cross_cluster_two:twitter,twutter/tweet/_search?pretty", encodeBasicHeader("nagilum","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, ccs.getStatusCode()); + Assert.assertFalse(ccs.getBody().contains("security_exception")); + Assert.assertTrue(ccs.getBody().contains("\"timed_out\" : false")); + Assert.assertTrue(ccs.getBody().contains("crl1")); + Assert.assertTrue(ccs.getBody().contains("crl2")); + Assert.assertTrue(ccs.getBody().contains("cross_cluster")); + } + + @Test + public void testCcsNonadmin() throws Exception { + setupCcs(); + + final String cl1BodyMain = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("", encodeBasicHeader("twitter","nagilum")).getBody(); + Assert.assertTrue(cl1BodyMain.contains("crl1")); + + try (TransportClient tc = getInternalTransportClient(cl1Info, Settings.EMPTY)) { + tc.index(new IndexRequest("twitter").type("tweet").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl1Info.clustername+"\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("twutter").type("tweet").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl1Info.clustername+"\"}", XContentType.JSON)).actionGet(); + } + + final String cl2BodyMain = new RestHelper(cl2Info, false, false, getResourceFolder()).executeGetRequest("", encodeBasicHeader("twitter","nagilum")).getBody(); + Assert.assertTrue(cl2BodyMain.contains("crl2")); + + try (TransportClient tc = getInternalTransportClient(cl2Info, Settings.EMPTY)) { + tc.index(new IndexRequest("twitter").type("tweet").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl2Info.clustername+"\"}", XContentType.JSON)).actionGet(); + tc.index(new IndexRequest("twutter").type("tweet").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl2Info.clustername+"\"}", XContentType.JSON)).actionGet(); + } + + HttpResponse ccs = null; + + System.out.println("###################### query 1"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("cross_cluster_two:*/_search?pretty", encodeBasicHeader("twitter","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, ccs.getStatusCode()); + + System.out.println("###################### query 2"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("cross_cluster_two:twit*/_search?pretty", encodeBasicHeader("twitter","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, ccs.getStatusCode()); + + + System.out.println("###################### query 3"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("cross_cluster_two:twitter,twitter,twutter/_search?pretty", encodeBasicHeader("twitter","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, ccs.getStatusCode()); + + System.out.println("###################### query 4"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("cross_cluster_two:twitter,twitter/tweet/_search?pretty", encodeBasicHeader("twitter","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, ccs.getStatusCode()); + + System.out.println("###################### query 5"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("cross_cluster_two:twutter,twitter/tweet/_search?pretty", encodeBasicHeader("twitter","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, ccs.getStatusCode()); + + System.out.println("###################### query 6"); + String msearchBody = + "{}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator(); + + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executePostRequest("cross_cluster_two:twitter,twitter/tweet/_msearch?pretty", msearchBody, encodeBasicHeader("twitter","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, ccs.getStatusCode()); + + System.out.println("###################### query 7"); + msearchBody = + "{}"+System.lineSeparator()+ + "{\"size\":10, \"query\":{\"bool\":{\"must\":{\"match_all\":{}}}}}"+System.lineSeparator(); + + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executePostRequest("cross_cluster_two:twitter/tweet/_msearch?pretty", msearchBody, encodeBasicHeader("twitter","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, ccs.getStatusCode()); + + } + + @Test + public void testCcsEmptyCoord() throws Exception { + setupCcs(); + + final String cl1BodyMain = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("", encodeBasicHeader("twitter","nagilum")).getBody(); + Assert.assertTrue(cl1BodyMain.contains("crl1")); + + final String cl2BodyMain = new RestHelper(cl2Info, false, false, getResourceFolder()).executeGetRequest("", encodeBasicHeader("twitter","nagilum")).getBody(); + Assert.assertTrue(cl2BodyMain.contains("crl2")); + + try (TransportClient tc = getInternalTransportClient(cl2Info, Settings.EMPTY)) { + tc.index(new IndexRequest("twitter").type("tweet").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl2Info.clustername+"\"}", XContentType.JSON)).actionGet(); + } + + HttpResponse ccs = null; + + System.out.println("###################### query 1"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("cross_cluster_two:twitter/tweet/_search?pretty", encodeBasicHeader("twitter","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, ccs.getStatusCode()); + Assert.assertFalse(ccs.getBody().contains("security_exception")); + Assert.assertTrue(ccs.getBody().contains("\"timed_out\" : false")); + Assert.assertFalse(ccs.getBody().contains("crl1")); + Assert.assertTrue(ccs.getBody().contains("crl2")); + Assert.assertTrue(ccs.getBody().contains("cross_cluster_two:twitter")); + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/ccstest/RemoteReindexTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/ccstest/RemoteReindexTests.java new file mode 100644 index 0000000000..4595947a71 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/ccstest/RemoteReindexTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.ccstest; + +import com.amazon.opendistroforelasticsearch.security.test.AbstractSecurityUnitTest; +import org.apache.http.HttpStatus; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentType; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterConfiguration; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterInfo; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; + +public class RemoteReindexTests extends AbstractSecurityUnitTest { + + private final ClusterHelper cl1 = new ClusterHelper("crl1_n"+num.incrementAndGet()+"_f"+System.getProperty("forkno")+"_t"+System.nanoTime()); + private final ClusterHelper cl2 = new ClusterHelper("crl2_n"+num.incrementAndGet()+"_f"+System.getProperty("forkno")+"_t"+System.nanoTime()); + private ClusterInfo cl1Info; + private ClusterInfo cl2Info; + + private void setupReindex() throws Exception { + + System.setProperty("security.display_lic_none","true"); + + cl2Info = cl2.startCluster(minimumSecuritySettings(Settings.EMPTY), ClusterConfiguration.DEFAULT); + initialize(cl2Info); + + cl1Info = cl1.startCluster(minimumSecuritySettings(crossClusterNodeSettings(cl2Info)), ClusterConfiguration.DEFAULT); + initialize(cl1Info); + } + + @After + public void tearDown() throws Exception { + cl1.stopCluster(); + cl2.stopCluster(); + } + + private Settings crossClusterNodeSettings(ClusterInfo remote) { + Settings.Builder builder = Settings.builder() + .putList("reindex.remote.whitelist", remote.httpHost+":"+remote.httpPort); + return builder.build(); + } + + //TODO add ssl tests + //https://github.com/elastic/elasticsearch/issues/27267 + + @Test + public void testNonSSLReindex() throws Exception { + setupReindex(); + + final String cl1BodyMain = new RestHelper(cl1Info, false, false, getResourceFolder()).executeGetRequest("", encodeBasicHeader("nagilum","nagilum")).getBody(); + Assert.assertTrue(cl1BodyMain.contains("crl1")); + + try (TransportClient tc = getInternalTransportClient(cl1Info, Settings.EMPTY)) { + tc.admin().indices().create(new CreateIndexRequest("twutter")).actionGet(); + } + + final String cl2BodyMain = new RestHelper(cl2Info, false, false, getResourceFolder()).executeGetRequest("", encodeBasicHeader("nagilum","nagilum")).getBody(); + Assert.assertTrue(cl2BodyMain.contains("crl2")); + + try (TransportClient tc = getInternalTransportClient(cl2Info, Settings.EMPTY)) { + tc.index(new IndexRequest("twitter").type("tweet").setRefreshPolicy(RefreshPolicy.IMMEDIATE).id("0") + .source("{\"cluster\": \""+cl1Info.clustername+"\"}", XContentType.JSON)).actionGet(); + } + + String reindex = "{"+ + "\"source\": {"+ + "\"remote\": {"+ + "\"host\": \"http://"+cl2Info.httpHost+":"+cl2Info.httpPort+"\","+ + "\"username\": \"nagilum\","+ + "\"password\": \"nagilum\""+ + "},"+ + "\"index\": \"twitter\","+ + "\"size\": 10,"+ + "\"query\": {"+ + "\"match\": {"+ + "\"_type\": \"tweet\""+ + "}"+ + "}"+ + "},"+ + "\"dest\": {"+ + "\"index\": \"twutter\""+ + "}"+ + "}"; + + System.out.println(reindex); + + HttpResponse ccs = null; + + System.out.println("###################### reindex"); + ccs = new RestHelper(cl1Info, false, false, getResourceFolder()).executePostRequest("_reindex?pretty", reindex, encodeBasicHeader("nagilum","nagilum")); + System.out.println(ccs.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, ccs.getStatusCode()); + Assert.assertTrue(ccs.getBody().contains("created\" : 1")); + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/configuration/PrivilegesInterceptorImpl.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/configuration/PrivilegesInterceptorImpl.java new file mode 100644 index 0000000000..8d42aaed39 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/configuration/PrivilegesInterceptorImpl.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration; + +import java.util.Map; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.privileges.PrivilegesInterceptor; +import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer.Resolved; +import com.amazon.opendistroforelasticsearch.security.user.User; + +public class PrivilegesInterceptorImpl extends PrivilegesInterceptor { + + public static int count = 0; + + @Inject + public PrivilegesInterceptorImpl(IndexNameExpressionResolver resolver, ClusterService clusterService, Client client, ThreadPool threadPool) { + super(resolver, clusterService, client, threadPool); + } + + @Override + public Boolean replaceKibanaIndex(ActionRequest request, String action, User user, Settings config, + final Resolved requestedResolved, Map tenants) { + count++; + return null; + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/AbstractSecurityUnitTest.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/AbstractSecurityUnitTest.java new file mode 100644 index 0000000000..19de94118a --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/AbstractSecurityUnitTest.java @@ -0,0 +1,256 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test; + +import io.netty.handler.ssl.OpenSsl; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.http.Header; +import org.apache.http.message.BasicHeader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.Netty4Plugin; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestName; +import org.junit.rules.TestWatcher; + +import com.amazon.opendistroforelasticsearch.security.OpenDistroSecurityPlugin; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateAction; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateRequest; +import com.amazon.opendistroforelasticsearch.security.action.configupdate.ConfigUpdateResponse; +import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterInfo; +import com.amazon.opendistroforelasticsearch.security.test.helper.file.FileHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; +import com.amazon.opendistroforelasticsearch.security.test.helper.rules.OpenDistroSecurityTestWatcher; + +public abstract class AbstractSecurityUnitTest { + + protected static final AtomicLong num = new AtomicLong(); + + static { + + System.out.println("OS: " + System.getProperty("os.name") + " " + System.getProperty("os.arch") + " " + + System.getProperty("os.version")); + System.out.println( + "Java Version: " + System.getProperty("java.version") + " " + System.getProperty("java.vendor")); + System.out.println("JVM Impl.: " + System.getProperty("java.vm.version") + " " + + System.getProperty("java.vm.vendor") + " " + System.getProperty("java.vm.name")); + System.out.println("Open SSL available: " + OpenSsl.isAvailable()); + System.out.println("Open SSL version: " + OpenSsl.versionString()); + + //System.setProperty("security.display_lic_none","true"); + } + + protected final Logger log = LogManager.getLogger(this.getClass()); + public static final ThreadPool MOCK_POOL = new ThreadPool(Settings.builder().put("node.name", "mock").build()); + + //TODO Test Matrix + protected boolean allowOpenSSL = false; //disabled, we test this already in SSL Plugin + //enable//disable enterprise modules + //1node and 3 node + + @Rule + public TestName name = new TestName(); + + @Rule + public final TemporaryFolder repositoryPath = new TemporaryFolder(); + + @Rule + public final TestWatcher testWatcher = new OpenDistroSecurityTestWatcher(); + + public static Header encodeBasicHeader(final String username, final String password) { + return new BasicHeader("Authorization", "Basic "+Base64.getEncoder().encodeToString( + (username + ":" + Objects.requireNonNull(password)).getBytes(StandardCharsets.UTF_8))); + } + + protected static class TransportClientImpl extends TransportClient { + + public TransportClientImpl(Settings settings, Collection> plugins) { + super(settings, plugins); + } + + public TransportClientImpl(Settings settings, Settings defaultSettings, Collection> plugins) { + super(settings, defaultSettings, plugins, null); + } + } + + @SafeVarargs + protected static Collection> asCollection(Class... plugins) { + return Arrays.asList(plugins); + } + + + protected TransportClient getInternalTransportClient(ClusterInfo info, Settings initTransportClientSettings) { + + final String prefix = getResourceFolder()==null?"":getResourceFolder()+"/"; + + Settings tcSettings = Settings.builder() + .put("cluster.name", info.clustername) + .put("opendistro_security.ssl.transport.truststore_filepath", + FileHelper.getAbsoluteFilePathFromClassPath(prefix+"truststore.jks")) + .put("opendistro_security.ssl.transport.enforce_hostname_verification", false) + .put("opendistro_security.ssl.transport.keystore_filepath", + FileHelper.getAbsoluteFilePathFromClassPath(prefix+"kirk-keystore.jks")) + .put(initTransportClientSettings) + .build(); + + TransportClient tc = new TransportClientImpl(tcSettings, asCollection(Netty4Plugin.class, OpenDistroSecurityPlugin.class)); + tc.addTransportAddress(new TransportAddress(new InetSocketAddress(info.nodeHost, info.nodePort))); + return tc; + } + + protected TransportClient getUserTransportClient(ClusterInfo info, String keyStore, Settings initTransportClientSettings) { + + final String prefix = getResourceFolder()==null?"":getResourceFolder()+"/"; + + Settings tcSettings = Settings.builder() + .put("cluster.name", info.clustername) + .put("opendistro_security.ssl.transport.truststore_filepath", + FileHelper.getAbsoluteFilePathFromClassPath(prefix+"truststore.jks")) + .put("opendistro_security.ssl.transport.enforce_hostname_verification", false) + .put("opendistro_security.ssl.transport.keystore_filepath", + FileHelper.getAbsoluteFilePathFromClassPath(prefix+keyStore)) + .put(initTransportClientSettings) + .build(); + + TransportClient tc = new TransportClientImpl(tcSettings, asCollection(Netty4Plugin.class, OpenDistroSecurityPlugin.class)); + tc.addTransportAddress(new TransportAddress(new InetSocketAddress(info.nodeHost, info.nodePort))); + return tc; + } + + protected void initialize(ClusterInfo info, Settings initTransportClientSettings, DynamicSecurityConfig config) { + + try (TransportClient tc = getInternalTransportClient(info, initTransportClientSettings)) { + + tc.addTransportAddress(new TransportAddress(new InetSocketAddress(info.nodeHost, info.nodePort))); + Assert.assertEquals(info.numNodes, + tc.admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet().getNodes().size()); + + try { + tc.admin().indices().create(new CreateIndexRequest(".opendistro_security")).actionGet(); + } catch (Exception e) { + //ignore + } + + for(IndexRequest ir: config.getDynamicConfig(getResourceFolder())) { + tc.index(ir).actionGet(); + } + + ConfigUpdateResponse cur = tc + .execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(ConfigConstants.CONFIG_NAMES.toArray(new String[0]))) + .actionGet(); + Assert.assertEquals(info.numNodes, cur.getNodes().size()); + + SearchResponse sr = tc.search(new SearchRequest(".opendistro_security")).actionGet(); + //Assert.assertEquals(5L, sr.getHits().getTotalHits()); + + sr = tc.search(new SearchRequest(".opendistro_security")).actionGet(); + //Assert.assertEquals(5L, sr.getHits().getTotalHits()); + + Assert.assertTrue(tc.get(new GetRequest(".opendistro_security", "security", "config")).actionGet().isExists()); + Assert.assertTrue(tc.get(new GetRequest(".opendistro_security","security","internalusers")).actionGet().isExists()); + Assert.assertTrue(tc.get(new GetRequest(".opendistro_security","security","roles")).actionGet().isExists()); + Assert.assertTrue(tc.get(new GetRequest(".opendistro_security","security","rolesmapping")).actionGet().isExists()); + Assert.assertTrue(tc.get(new GetRequest(".opendistro_security","security","actiongroups")).actionGet().isExists()); + Assert.assertFalse(tc.get(new GetRequest(".opendistro_security","security","rolesmapping_xcvdnghtu165759i99465")).actionGet().isExists()); + Assert.assertTrue(tc.get(new GetRequest(".opendistro_security","security","config")).actionGet().isExists()); + } + } + + protected Settings.Builder minimumSecuritySettingsBuilder(int node) { + + final String prefix = getResourceFolder()==null?"":getResourceFolder()+"/"; + + return Settings.builder() + //.put("opendistro_security.ssl.transport.enabled", true) + //.put("opendistro_security.no_default_init", true) + //.put("opendistro_security.ssl.http.enable_openssl_if_available", false) + //.put("opendistro_security.ssl.transport.enable_openssl_if_available", false) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE, allowOpenSSL) + .put(SSLConfigConstants.OPENDISTRO_SECURITY_SSL_TRANSPORT_ENABLE_OPENSSL_IF_AVAILABLE, allowOpenSSL) + .put("opendistro_security.ssl.transport.keystore_alias", "node-0") + .put("opendistro_security.ssl.transport.keystore_filepath", + FileHelper.getAbsoluteFilePathFromClassPath(prefix+"node-0-keystore.jks")) + .put("opendistro_security.ssl.transport.truststore_filepath", + FileHelper.getAbsoluteFilePathFromClassPath(prefix+"truststore.jks")) + .put("opendistro_security.ssl.transport.enforce_hostname_verification", false) + .putList("opendistro_security.authcz.admin_dn", "CN=kirk,OU=client,O=client,l=tEst, C=De"); + //.put(other==null?Settings.EMPTY:other); + } + + protected NodeSettingsSupplier minimumSecuritySettings(Settings other) { + return new NodeSettingsSupplier() { + @Override + public Settings get(int i) { + return minimumSecuritySettingsBuilder(i).put(other).build(); + } + }; + } + + protected void initialize(ClusterInfo info) { + initialize(info, Settings.EMPTY, new DynamicSecurityConfig()); + } + + protected final void assertContains(HttpResponse res, String pattern) { + Assert.assertTrue(WildcardMatcher.match(pattern, res.getBody())); + } + + protected final void assertNotContains(HttpResponse res, String pattern) { + Assert.assertFalse(WildcardMatcher.match(pattern, res.getBody())); + } + + protected String getResourceFolder() { + return null; + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/DynamicSecurityConfig.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/DynamicSecurityConfig.java new file mode 100644 index 0000000000..f99e194c6d --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/DynamicSecurityConfig.java @@ -0,0 +1,131 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.test.helper.file.FileHelper; + +public class DynamicSecurityConfig { + + private String securityIndexName = ".opendistro_security"; + private String securityConfig = "config.yml"; + private String securityRoles = "roles.yml"; + private String securityRolesMapping = "roles_mapping.yml"; + private String securityInternalUsers = "internal_users.yml"; + private String securityActionGroups = "action_groups.yml"; + private String securityConfigAsYamlString = null; + + public String getSecurityIndexName() { + return securityIndexName; + } + public DynamicSecurityConfig setSecurityIndexName(String securityIndexName) { + this.securityIndexName = securityIndexName; + return this; + } + + public DynamicSecurityConfig setConfig(String securityConfig) { + this.securityConfig = securityConfig; + return this; + } + + public DynamicSecurityConfig setConfigAsYamlString(String securityConfigAsYamlString) { + this.securityConfigAsYamlString = securityConfigAsYamlString; + return this; + } + + public DynamicSecurityConfig setSecurityRoles(String securityRoles) { + this.securityRoles = securityRoles; + return this; + } + + public DynamicSecurityConfig setSecurityRolesMapping(String securityRolesMapping) { + this.securityRolesMapping = securityRolesMapping; + return this; + } + + public DynamicSecurityConfig setSecurityInternalUsers(String securityInternalUsers) { + this.securityInternalUsers = securityInternalUsers; + return this; + } + + public DynamicSecurityConfig setSecurityActionGroups(String securityActionGroups) { + this.securityActionGroups = securityActionGroups; + return this; + } + + public List getDynamicConfig(String folder) { + + final String prefix = folder == null?"":folder+"/"; + + List ret = new ArrayList(); + + ret.add(new IndexRequest(securityIndexName) + .type("security") + .id(ConfigConstants.CONFIGNAME_CONFIG) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(ConfigConstants.CONFIGNAME_CONFIG, securityConfigAsYamlString==null?FileHelper.readYamlContent(prefix+securityConfig):FileHelper.readYamlContentFromString(securityConfigAsYamlString))); + + ret.add(new IndexRequest(securityIndexName) + .type("security") + .id(ConfigConstants.CONFIGNAME_ACTION_GROUPS) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(ConfigConstants.CONFIGNAME_ACTION_GROUPS, FileHelper.readYamlContent(prefix+securityActionGroups))); + + ret.add(new IndexRequest(securityIndexName) + .type("security") + .id(ConfigConstants.CONFIGNAME_INTERNAL_USERS) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(ConfigConstants.CONFIGNAME_INTERNAL_USERS, FileHelper.readYamlContent(prefix+securityInternalUsers))); + + ret.add(new IndexRequest(securityIndexName) + .type("security") + .id(ConfigConstants.CONFIGNAME_ROLES) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(ConfigConstants.CONFIGNAME_ROLES, FileHelper.readYamlContent(prefix+securityRoles))); + + ret.add(new IndexRequest(securityIndexName) + .type("security") + .id(ConfigConstants.CONFIGNAME_ROLES_MAPPING) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(ConfigConstants.CONFIGNAME_ROLES_MAPPING, FileHelper.readYamlContent(prefix+securityRolesMapping))); + + + return Collections.unmodifiableList(ret); + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/NodeSettingsSupplier.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/NodeSettingsSupplier.java new file mode 100644 index 0000000000..833e48cdef --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/NodeSettingsSupplier.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test; + +import org.elasticsearch.common.settings.Settings; + +@FunctionalInterface +public interface NodeSettingsSupplier { + Settings get(int i); +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/SingleClusterTest.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/SingleClusterTest.java new file mode 100644 index 0000000000..b2db4bd65f --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/SingleClusterTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test; + +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.junit.After; + +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterConfiguration; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterInfo; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper; + +public abstract class SingleClusterTest extends AbstractSecurityUnitTest { + + protected ClusterHelper clusterHelper = new ClusterHelper("utest_n"+num.incrementAndGet()+"_f"+System.getProperty("forkno")+"_t"+System.nanoTime()); + protected ClusterInfo clusterInfo; + + protected void setup(Settings nodeOverride) throws Exception { + setup(Settings.EMPTY, new DynamicSecurityConfig(), nodeOverride, true); + } + + protected void setup(Settings nodeOverride, ClusterConfiguration clusterConfiguration) throws Exception { + setup(Settings.EMPTY, new DynamicSecurityConfig(), nodeOverride, true, clusterConfiguration); + } + + protected void setup() throws Exception { + setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY, true); + } + + protected void setup(Settings initTransportClientSettings, DynamicSecurityConfig dynamicSecuritySettings, Settings nodeOverride) throws Exception { + setup(initTransportClientSettings, dynamicSecuritySettings, nodeOverride, true); + } + + protected void setup(Settings initTransportClientSettings, DynamicSecurityConfig dynamicSecuritySettings, Settings nodeOverride, boolean initSeachGuardIndex) throws Exception { + setup(initTransportClientSettings, dynamicSecuritySettings, nodeOverride, initSeachGuardIndex, ClusterConfiguration.DEFAULT); + } + + protected void setup(Settings initTransportClientSettings, DynamicSecurityConfig dynamicSecuritySettings, Settings nodeOverride, boolean initSeachGuardIndex, ClusterConfiguration clusterConfiguration) throws Exception { + clusterInfo = clusterHelper.startCluster(minimumSecuritySettings(nodeOverride), clusterConfiguration); + if(initSeachGuardIndex && dynamicSecuritySettings != null) { + initialize(clusterInfo, initTransportClientSettings, dynamicSecuritySettings); + } + } + + protected void setup(Settings initTransportClientSettings, DynamicSecurityConfig dynamicSecuritySettings, Settings nodeOverride + , boolean initSeachGuardIndex, ClusterConfiguration clusterConfiguration, int timeout, Integer nodes) throws Exception { + clusterInfo = clusterHelper.startCluster(minimumSecuritySettings(nodeOverride), clusterConfiguration, timeout, nodes); + if(initSeachGuardIndex) { + initialize(clusterInfo, initTransportClientSettings, dynamicSecuritySettings); + } + } + + protected RestHelper restHelper() { + return new RestHelper(clusterInfo, getResourceFolder()); + } + + protected RestHelper nonSslRestHelper() { + return new RestHelper(clusterInfo, false, false, getResourceFolder()); + } + + protected TransportClient getInternalTransportClient() { + return getInternalTransportClient(clusterInfo, Settings.EMPTY); + } + + @After + public void tearDown() throws Exception { + if(clusterInfo != null) { + clusterHelper.stopCluster(); + } + + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterConfiguration.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterConfiguration.java new file mode 100644 index 0000000000..e37318dc86 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterConfiguration.java @@ -0,0 +1,114 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test.helper.cluster; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.elasticsearch.index.reindex.ReindexPlugin; +import org.elasticsearch.join.ParentJoinPlugin; +import org.elasticsearch.percolator.PercolatorPlugin; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.script.mustache.MustachePlugin; +import org.elasticsearch.search.aggregations.matrix.MatrixAggregationPlugin; +import org.elasticsearch.transport.Netty4Plugin; + +import com.amazon.opendistroforelasticsearch.security.OpenDistroSecurityPlugin; +import com.amazon.opendistroforelasticsearch.security.test.plugin.UserInjectorPlugin; +import com.google.common.collect.Lists; + +public enum ClusterConfiguration { + //first one needs to be a master + //HUGE(new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(true, false, false), new NodeSettings(false, true,false), new NodeSettings(false, true, false)), + + //3 nodes (1m, 2d) + DEFAULT(new NodeSettings(true, false, false), new NodeSettings(false, true, false), new NodeSettings(false, true, false)), + + //1 node (1md) + SINGLENODE(new NodeSettings(true, true, false)), + + //4 node (1m, 2d, 1c) + CLIENTNODE(new NodeSettings(true, false, false), new NodeSettings(false, true, false), new NodeSettings(false, true, false), new NodeSettings(false, false, false)), + + //3 nodes (1m, 2d) plus additional UserInjectorPlugin + USERINJECTOR(new NodeSettings(true, false, false, Lists.newArrayList(UserInjectorPlugin.class)), new NodeSettings(false, true, false, Lists.newArrayList(UserInjectorPlugin.class)), new NodeSettings(false, true, false, Lists.newArrayList(UserInjectorPlugin.class))); + + private List nodeSettings = new LinkedList<>(); + + private ClusterConfiguration(NodeSettings ... settings) { + nodeSettings.addAll(Arrays.asList(settings)); + } + + public List getNodeSettings() { + return Collections.unmodifiableList(nodeSettings); + } + + public int getNodes() { + return nodeSettings.size(); + } + + public int getMasterNodes() { + return (int) nodeSettings.stream().filter(a->a.masterNode).count(); + } + + public int getDataNodes() { + return (int) nodeSettings.stream().filter(a->a.dataNode).count(); + } + + public int getClientNodes() { + return (int) nodeSettings.stream().filter(a->!a.masterNode && !a.dataNode).count(); + } + + public static class NodeSettings { + public boolean masterNode; + public boolean dataNode; + public boolean tribeNode; + public List> plugins = Lists.newArrayList(Netty4Plugin.class, OpenDistroSecurityPlugin.class, MatrixAggregationPlugin.class, MustachePlugin.class, ParentJoinPlugin.class, PercolatorPlugin.class, ReindexPlugin.class); + + public NodeSettings(boolean masterNode, boolean dataNode, boolean tribeNode) { + super(); + this.masterNode = masterNode; + this.dataNode = dataNode; + this.tribeNode = tribeNode; + } + + public NodeSettings(boolean masterNode, boolean dataNode, boolean tribeNode, List> additionalPlugins) { + this(masterNode, dataNode, tribeNode); + this.plugins.addAll(additionalPlugins); + } + + public Class[] getPlugins() { + return plugins.toArray(new Class[0] ); + } + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterHelper.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterHelper.java new file mode 100644 index 0000000000..0db671fff2 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterHelper.java @@ -0,0 +1,329 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test.helper.cluster; + +import java.io.File; +import java.io.IOException; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configurator; +import org.elasticsearch.ElasticsearchTimeoutException; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.elasticsearch.cluster.node.DiscoveryNode.Role; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.node.Node; +import org.elasticsearch.node.PluginAwareNode; + +import com.amazon.opendistroforelasticsearch.security.test.NodeSettingsSupplier; +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterConfiguration.NodeSettings; +import com.amazon.opendistroforelasticsearch.security.test.helper.network.SocketUtils; + +public final class ClusterHelper { + + static { + System.setProperty("es.enforce.bootstrap.checks", "true"); + System.setProperty("security.default_init.dir", new File("./securityconfig").getAbsolutePath()); + } + + protected final Logger log = LogManager.getLogger(ClusterHelper.class); + + protected final List esNodes = new LinkedList<>(); + + private final String clustername; + + public ClusterHelper(String clustername) { + super(); + this.clustername = clustername; + } + + /** + * Start n Elasticsearch nodes with the provided settings + * + * @return + * @throws Exception + */ + + public final ClusterInfo startCluster(final NodeSettingsSupplier nodeSettingsSupplier, ClusterConfiguration clusterConfiguration) throws Exception { + return startCluster(nodeSettingsSupplier, clusterConfiguration, 10, null); + } + + + public final synchronized ClusterInfo startCluster(final NodeSettingsSupplier nodeSettingsSupplier, ClusterConfiguration clusterConfiguration, int timeout, Integer nodes) + throws Exception { + + if (!esNodes.isEmpty()) { + throw new RuntimeException("There are still " + esNodes.size() + " nodes instantiated, close them first."); + } + + FileUtils.deleteDirectory(new File("data/"+clustername)); + + List internalNodeSettings = clusterConfiguration.getNodeSettings(); + + final String forkno = System.getProperty("forkno"); + int forkNumber = 1; + + if(forkno != null && forkno.length() > 0) { + forkNumber = Integer.parseInt(forkno.split("_")[1]); + } + + final int min = SocketUtils.PORT_RANGE_MIN+(forkNumber*5000); + final int max = SocketUtils.PORT_RANGE_MIN+((forkNumber+1)*5000)-1; + + final SortedSet freePorts = SocketUtils.findAvailableTcpPorts(internalNodeSettings.size()*2, min, max); + assert freePorts.size() == internalNodeSettings.size()*2; + final SortedSet tcpPorts = new TreeSet(); + freePorts.stream().limit(internalNodeSettings.size()).forEach(el->tcpPorts.add(el)); + final Iterator tcpPortsIt = tcpPorts.iterator(); + + final SortedSet httpPorts = new TreeSet(); + freePorts.stream().skip(internalNodeSettings.size()).limit(internalNodeSettings.size()).forEach(el->httpPorts.add(el)); + final Iterator httpPortsIt = httpPorts.iterator(); + + System.out.println("tcpPorts: "+tcpPorts+"/httpPorts: "+httpPorts+" for ("+min+"-"+max+") fork "+forkNumber); + + final CountDownLatch latch = new CountDownLatch(internalNodeSettings.size()); + + final AtomicReference err = new AtomicReference(); + + for (int i = 0; i < internalNodeSettings.size(); i++) { + NodeSettings setting = internalNodeSettings.get(i); + + + PluginAwareNode node = new PluginAwareNode(setting.masterNode, + getMinimumNonSgNodeSettingsBuilder(i, setting.masterNode, setting.dataNode, setting.tribeNode, internalNodeSettings.size(), clusterConfiguration.getMasterNodes(), tcpPorts, tcpPortsIt.next(), httpPortsIt.next()) + .put(nodeSettingsSupplier == null ? Settings.Builder.EMPTY_SETTINGS : nodeSettingsSupplier.get(i)).build(), setting.getPlugins()); + System.out.println(node.settings()); + + new Thread(new Runnable() { + + @Override + public void run() { + try { + node.start(); + latch.countDown(); + } catch (Exception e) { + e.printStackTrace(); + log.error("Unable to start node: "+e); + err.set(e); + latch.countDown(); + } + } + }).start(); + + + + esNodes.add(node); + } + + + latch.await(); + + if(err.get() != null) { + throw new RuntimeException("Could not start all nodes "+err.get(),err.get()); + } + + ClusterInfo cInfo = waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(timeout), nodes == null?esNodes.size():nodes.intValue()); + cInfo.numNodes = internalNodeSettings.size(); + cInfo.clustername = clustername; + return cInfo; + } + + public final void stopCluster() throws Exception { + + //close non master nodes + esNodes.stream().filter(n->!n.isMasterEligible()).forEach(node->closeNode(node)); + + //close master nodes + esNodes.stream().filter(n->n.isMasterEligible()).forEach(node->closeNode(node)); + esNodes.clear(); + + FileUtils.deleteDirectory(new File("data/"+clustername)); + } + + private static void closeNode(Node node) { + try { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + Configurator.shutdown(context); + node.close(); + Thread.sleep(250); + } catch (Throwable e) { + //ignore + } + } + + + public Client nodeClient() { + return esNodes.get(0).client(); + } + + public ClusterInfo waitForCluster(final ClusterHealthStatus status, final TimeValue timeout, final int expectedNodeCount) throws IOException { + if (esNodes.isEmpty()) { + throw new RuntimeException("List of nodes was empty."); + } + + ClusterInfo clusterInfo = new ClusterInfo(); + + Node node = esNodes.get(0); + Client client = node.client(); + try { + log.debug("waiting for cluster state {} and {} nodes", status.name(), expectedNodeCount); + final ClusterHealthResponse healthResponse = client.admin().cluster().prepareHealth() + .setWaitForStatus(status).setTimeout(timeout).setMasterNodeTimeout(timeout).setWaitForNodes("" + expectedNodeCount).execute() + .actionGet(); + if (healthResponse.isTimedOut()) { + throw new IOException("cluster state is " + healthResponse.getStatus().name() + " with " + + healthResponse.getNumberOfNodes() + " nodes"); + } else { + log.debug("... cluster state ok " + healthResponse.getStatus().name() + " with " + + healthResponse.getNumberOfNodes() + " nodes"); + } + + org.junit.Assert.assertEquals(expectedNodeCount, healthResponse.getNumberOfNodes()); + + final NodesInfoResponse res = client.admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet(); + + final List nodes = res.getNodes(); + + //final List masterNodes = nodes.stream().filter(n->n.getNode().getRoles().contains(Role.MASTER)).collect(Collectors.toList()); + final List dataNodes = nodes.stream().filter(n->n.getNode().getRoles().contains(Role.DATA) && !n.getNode().getRoles().contains(Role.MASTER)).collect(Collectors.toList()); + final List clientNodes = nodes.stream().filter(n->!n.getNode().getRoles().contains(Role.MASTER) && !n.getNode().getRoles().contains(Role.DATA)).collect(Collectors.toList()); + + if(!clientNodes.isEmpty()) { + NodeInfo nodeInfo = clientNodes.get(0); + if (nodeInfo.getHttp() != null && nodeInfo.getHttp().address() != null) { + final TransportAddress his = nodeInfo.getHttp().address() + .publishAddress(); + clusterInfo.httpPort = his.getPort(); + clusterInfo.httpHost = his.getAddress(); + clusterInfo.httpAdresses.add(his); + } else { + throw new RuntimeException("no http host/port for client node"); + } + + final TransportAddress is = nodeInfo.getTransport().getAddress() + .publishAddress(); + clusterInfo.nodePort = is.getPort(); + clusterInfo.nodeHost = is.getAddress(); + } else if(!dataNodes.isEmpty()) { + + for (NodeInfo nodeInfo: dataNodes) { + final TransportAddress is = nodeInfo.getTransport().getAddress() + .publishAddress(); + clusterInfo.nodePort = is.getPort(); + clusterInfo.nodeHost = is.getAddress(); + + if (nodeInfo.getHttp() != null && nodeInfo.getHttp().address() != null) { + final TransportAddress his = nodeInfo.getHttp().address() + .publishAddress(); + clusterInfo.httpPort = his.getPort(); + clusterInfo.httpHost = his.getAddress(); + clusterInfo.httpAdresses.add(his); + break; + } + } + } else { + + for (NodeInfo nodeInfo: nodes) { + final TransportAddress is = nodeInfo.getTransport().getAddress() + .publishAddress(); + clusterInfo.nodePort = is.getPort(); + clusterInfo.nodeHost = is.getAddress(); + + if (nodeInfo.getHttp() != null && nodeInfo.getHttp().address() != null) { + final TransportAddress his = nodeInfo.getHttp().address() + .publishAddress(); + clusterInfo.httpPort = his.getPort(); + clusterInfo.httpHost = his.getAddress(); + clusterInfo.httpAdresses.add(his); + break; + } + } + } + } catch (final ElasticsearchTimeoutException e) { + throw new IOException( + "timeout, cluster does not respond to health request, cowardly refusing to continue with operations"); + } + return clusterInfo; + } + + // @formatter:off + private Settings.Builder getMinimumNonSgNodeSettingsBuilder(final int nodenum, final boolean masterNode, + final boolean dataNode, final boolean tribeNode, int nodeCount, int masterCount, SortedSet tcpPorts, int tcpPort, int httpPort) { + + return Settings.builder() + .put("node.name", "node_"+clustername+ "_num" + nodenum) + .put("node.data", dataNode) + .put("node.master", masterNode) + .put("cluster.name", clustername) + .put("path.data", "data/"+clustername+"/data") + .put("path.logs", "data/"+clustername+"/logs") + .put("node.max_local_storage_nodes", nodeCount) + .put("discovery.zen.minimum_master_nodes", minMasterNodes(masterCount)) + .put("discovery.zen.no_master_block", "all") + .put("discovery.zen.fd.ping_timeout", "5s") + .put("discovery.initial_state_timeout","8s") + .putList("discovery.zen.ping.unicast.hosts", tcpPorts.stream().map(s->"127.0.0.1:"+s).collect(Collectors.toList())) + .put("transport.tcp.port", tcpPort) + .put("http.port", httpPort) + .put("http.enabled", true) + .put("cluster.routing.allocation.disk.threshold_enabled", false) + .put("http.cors.enabled", true) + .put("path.home", "."); + } + // @formatter:on + + private int minMasterNodes(int masterEligibleNodes) { + if(masterEligibleNodes <= 0) { + throw new IllegalArgumentException("no master eligible nodes"); + } + + return (masterEligibleNodes/2) + 1; + + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterInfo.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterInfo.java new file mode 100644 index 0000000000..150841665a --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/cluster/ClusterInfo.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test.helper.cluster; + +import java.util.HashSet; +import java.util.Set; + +import org.elasticsearch.common.transport.TransportAddress; + +public class ClusterInfo { + public int numNodes; + public String httpHost = null; + public int httpPort = -1; + public Set httpAdresses = new HashSet(); + public String nodeHost; + public int nodePort; + public String clustername; +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/file/FileHelper.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/file/FileHelper.java new file mode 100644 index 0000000000..268a4a77de --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/file/FileHelper.java @@ -0,0 +1,148 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test.helper.file; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; + +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; + +import com.amazon.opendistroforelasticsearch.security.support.OpenDistroSecurityDeprecationHandler; + +public class FileHelper { + + protected final static Logger log = LogManager.getLogger(FileHelper.class); + + public static KeyStore getKeystoreFromClassPath(final String fileNameFromClasspath, String password) throws Exception { + Path path = getAbsoluteFilePathFromClassPath(fileNameFromClasspath); + if(path==null) { + return null; + } + + KeyStore ks = KeyStore.getInstance("JKS"); + try (FileInputStream fin = new FileInputStream(path.toFile())) { + ks.load(fin, password==null||password.isEmpty()?null:password.toCharArray()); + } + return ks; + } + + public static Path getAbsoluteFilePathFromClassPath(final String fileNameFromClasspath) { + File file = null; + final URL fileUrl = FileHelper.class.getClassLoader().getResource(fileNameFromClasspath); + if (fileUrl != null) { + try { + file = new File(URLDecoder.decode(fileUrl.getFile(), "UTF-8")); + } catch (final UnsupportedEncodingException e) { + return null; + } + + if (file.exists() && file.canRead()) { + return Paths.get(file.getAbsolutePath()); + } else { + log.error("Cannot read from {}, maybe the file does not exists? ", file.getAbsolutePath()); + } + + } else { + log.error("Failed to load " + fileNameFromClasspath); + } + return null; + } + + public static final String loadFile(final String file) throws IOException { + final StringWriter sw = new StringWriter(); + IOUtils.copy(FileHelper.class.getResourceAsStream("/" + file), sw, StandardCharsets.UTF_8); + return sw.toString(); + } + + public static BytesReference readYamlContent(final String file) { + + XContentParser parser = null; + try { + parser = XContentFactory.xContent(XContentType.YAML).createParser(NamedXContentRegistry.EMPTY, OpenDistroSecurityDeprecationHandler.INSTANCE, new StringReader(loadFile(file))); + parser.nextToken(); + final XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.copyCurrentStructure(parser); + return BytesReference.bytes(builder); + } catch (Exception e) { + throw new RuntimeException(e); + } + finally { + if (parser != null) { + try { + parser.close(); + } catch (IOException e) { + //ignore + } + } + } + } + + public static BytesReference readYamlContentFromString(final String yaml) { + + XContentParser parser = null; + try { + parser = XContentFactory.xContent(XContentType.YAML).createParser(NamedXContentRegistry.EMPTY, OpenDistroSecurityDeprecationHandler.INSTANCE, new StringReader(yaml)); + parser.nextToken(); + final XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.copyCurrentStructure(parser); + return BytesReference.bytes(builder); + } catch (Exception e) { + throw new RuntimeException(e); + } + finally { + if (parser != null) { + try { + parser.close(); + } catch (IOException e) { + //ignore + } + } + } + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/network/SocketUtils.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/network/SocketUtils.java new file mode 100644 index 0000000000..4feacaa656 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/network/SocketUtils.java @@ -0,0 +1,338 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test.helper.network; + +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed 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. + */ + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.util.Random; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.net.ServerSocketFactory; + +/** + * Simple utility methods for working with network sockets — for example, + * for finding available ports on {@code localhost}. + * + *

Within this class, a TCP port refers to a port for a {@link ServerSocket}; + * whereas, a UDP port refers to a port for a {@link DatagramSocket}. + * + * @author Sam Brannen + * @author Ben Hale + * @author Arjen Poutsma + * @author Gunnar Hillert + * @author Gary Russell + * @since 4.0 + */ +public class SocketUtils { + + /** + * The default minimum value for port ranges used when finding an available + * socket port. + */ + public static final int PORT_RANGE_MIN = 1024; + + /** + * The default maximum value for port ranges used when finding an available + * socket port. + */ + public static final int PORT_RANGE_MAX = 65535; + + + private static final Random random = new Random(System.currentTimeMillis()); + + + /** + * Although {@code SocketUtils} consists solely of static utility methods, + * this constructor is intentionally {@code public}. + *

Rationale

+ *

Static methods from this class may be invoked from within XML + * configuration files using the Spring Expression Language (SpEL) and the + * following syntax. + *

<bean id="bean1" ... p:port="#{T(org.springframework.util.SocketUtils).findAvailableTcpPort(12000)}" />
+ * If this constructor were {@code private}, you would be required to supply + * the fully qualified class name to SpEL's {@code T()} function for each usage. + * Thus, the fact that this constructor is {@code public} allows you to reduce + * boilerplate configuration with SpEL as can be seen in the following example. + *
<bean id="socketUtils" class="org.springframework.util.SocketUtils" />
+     * <bean id="bean1" ... p:port="#{socketUtils.findAvailableTcpPort(12000)}" />
+     * <bean id="bean2" ... p:port="#{socketUtils.findAvailableTcpPort(30000)}" />
+ */ + public SocketUtils() { + /* no-op */ + } + + + /** + * Find an available TCP port randomly selected from the range + * [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort() { + return findAvailableTcpPort(PORT_RANGE_MIN); + } + + /** + * Find an available TCP port randomly selected from the range + * [{@code minPort}, {@value #PORT_RANGE_MAX}]. + * @param minPort the minimum port number + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort(int minPort) { + return findAvailableTcpPort(minPort, PORT_RANGE_MAX); + } + + /** + * Find an available TCP port randomly selected from the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort(int minPort, int maxPort) { + return SocketType.TCP.findAvailablePort(minPort, maxPort); + } + + /** + * Find the requested number of available TCP ports, each randomly selected + * from the range [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @param numRequested the number of available ports to find + * @return a sorted set of available TCP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableTcpPorts(int numRequested) { + return findAvailableTcpPorts(numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + /** + * Find the requested number of available TCP ports, each randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available TCP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableTcpPorts(int numRequested, int minPort, int maxPort) { + return SocketType.TCP.findAvailablePorts(numRequested, minPort, maxPort); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort() { + return findAvailableUdpPort(PORT_RANGE_MIN); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@code minPort}, {@value #PORT_RANGE_MAX}]. + * @param minPort the minimum port number + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort(int minPort) { + return findAvailableUdpPort(minPort, PORT_RANGE_MAX); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort(int minPort, int maxPort) { + return SocketType.UDP.findAvailablePort(minPort, maxPort); + } + + /** + * Find the requested number of available UDP ports, each randomly selected + * from the range [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @param numRequested the number of available ports to find + * @return a sorted set of available UDP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableUdpPorts(int numRequested) { + return findAvailableUdpPorts(numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + /** + * Find the requested number of available UDP ports, each randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available UDP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableUdpPorts(int numRequested, int minPort, int maxPort) { + return SocketType.UDP.findAvailablePorts(numRequested, minPort, maxPort); + } + + + private enum SocketType { + + TCP { + @Override + protected boolean isPortAvailable(int port) { + try { + ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket( + port, 1, InetAddress.getByName("localhost")); + serverSocket.close(); + return true; + } + catch (Exception ex) { + return false; + } + } + }, + + UDP { + @Override + protected boolean isPortAvailable(int port) { + try { + DatagramSocket socket = new DatagramSocket(port, InetAddress.getByName("localhost")); + socket.close(); + return true; + } + catch (Exception ex) { + return false; + } + } + }; + + /** + * Determine if the specified port for this {@code SocketType} is + * currently available on {@code localhost}. + */ + protected abstract boolean isPortAvailable(int port); + + /** + * Find a pseudo-random port number within the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a random port number within the specified range + */ + private int findRandomPort(int minPort, int maxPort) { + int portRange = maxPort - minPort; + return minPort + random.nextInt(portRange + 1); + } + + /** + * Find an available port for this {@code SocketType}, randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available port number for this socket type + * @throws IllegalStateException if no available port could be found + */ + int findAvailablePort(int minPort, int maxPort) { + //Assert.assertTrue(minPort > 0, "'minPort' must be greater than 0"); + //Assert.isTrue(maxPort >= minPort, "'maxPort' must be greater than or equal to 'minPort'"); + //Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); + + int portRange = maxPort - minPort; + int candidatePort; + int searchCounter = 0; + do { + if (searchCounter > portRange) { + throw new IllegalStateException(String.format( + "Could not find an available %s port in the range [%d, %d] after %d attempts", + name(), minPort, maxPort, searchCounter)); + } + candidatePort = findRandomPort(minPort, maxPort); + searchCounter++; + } + while (!isPortAvailable(candidatePort)); + + return candidatePort; + } + + /** + * Find the requested number of available ports for this {@code SocketType}, + * each randomly selected from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available port numbers for this socket type + * @throws IllegalStateException if the requested number of available ports could not be found + */ + SortedSet findAvailablePorts(int numRequested, int minPort, int maxPort) { + //Assert.isTrue(minPort > 0, "'minPort' must be greater than 0"); + //Assert.isTrue(maxPort > minPort, "'maxPort' must be greater than 'minPort'"); + //Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); + //Assert.isTrue(numRequested > 0, "'numRequested' must be greater than 0"); + //Assert.isTrue((maxPort - minPort) >= numRequested, + // "'numRequested' must not be greater than 'maxPort' - 'minPort'"); + + SortedSet availablePorts = new TreeSet<>(); + int attemptCount = 0; + while ((++attemptCount <= numRequested + 100) && availablePorts.size() < numRequested) { + availablePorts.add(findAvailablePort(minPort, maxPort)); + } + + if (availablePorts.size() != numRequested) { + throw new IllegalStateException(String.format( + "Could not find %d available %s ports in the range [%d, %d]", + numRequested, name(), minPort, maxPort)); + } + + return availablePorts; + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/rest/RestHelper.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/rest/RestHelper.java new file mode 100644 index 0000000000..da2245e0f8 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/rest/RestHelper.java @@ -0,0 +1,299 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test.helper.rest; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.util.Arrays; + +import javax.net.ssl.SSLContext; + +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.config.SocketConfig; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.http.ssl.SSLContexts; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.amazon.opendistroforelasticsearch.security.test.helper.cluster.ClusterInfo; +import com.amazon.opendistroforelasticsearch.security.test.helper.file.FileHelper; + +public class RestHelper { + + protected final Logger log = LogManager.getLogger(RestHelper.class); + + public boolean enableHTTPClientSSL = true; + public boolean enableHTTPClientSSLv3Only = false; + public boolean sendHTTPClientCertificate = false; + public boolean trustHTTPServerCertificate = true; + public String keystore = "node-0-keystore.jks"; + public final String prefix; + //public String truststore = "truststore.jks"; + private ClusterInfo clusterInfo; + + public RestHelper(ClusterInfo clusterInfo, String prefix) { + this.clusterInfo = clusterInfo; + this.prefix = prefix; + } + + public RestHelper(ClusterInfo clusterInfo, boolean enableHTTPClientSSL, boolean trustHTTPServerCertificate, String prefix) { + this.clusterInfo = clusterInfo; + this.enableHTTPClientSSL = enableHTTPClientSSL; + this.trustHTTPServerCertificate = trustHTTPServerCertificate; + this.prefix = prefix; + } + public String executeSimpleRequest(final String request) throws Exception { + + CloseableHttpClient httpClient = null; + CloseableHttpResponse response = null; + try { + httpClient = getHTTPClient(); + response = httpClient.execute(new HttpGet(getHttpServerUri() + "/" + request)); + + if (response.getStatusLine().getStatusCode() >= 300) { + throw new Exception("Statuscode " + response.getStatusLine().getStatusCode()); + } + + return IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + } finally { + + if (response != null) { + response.close(); + } + + if (httpClient != null) { + httpClient.close(); + } + } + } + + public HttpResponse executeGetRequest(final String request, Header... header) throws Exception { + return executeRequest(new HttpGet(getHttpServerUri() + "/" + request), header); + } + + public HttpResponse executeHeadRequest(final String request, Header... header) throws Exception { + return executeRequest(new HttpHead(getHttpServerUri() + "/" + request), header); + } + + public HttpResponse executeOptionsRequest(final String request) throws Exception { + return executeRequest(new HttpOptions(getHttpServerUri() + "/" + request)); + } + + public HttpResponse executePutRequest(final String request, String body, Header... header) throws Exception { + HttpPut uriRequest = new HttpPut(getHttpServerUri() + "/" + request); + if (body != null && !body.isEmpty()) { + uriRequest.setEntity(new StringEntity(body)); + } + return executeRequest(uriRequest, header); + } + + public HttpResponse executeDeleteRequest(final String request, Header... header) throws Exception { + return executeRequest(new HttpDelete(getHttpServerUri() + "/" + request), header); + } + + public HttpResponse executePostRequest(final String request, String body, Header... header) throws Exception { + HttpPost uriRequest = new HttpPost(getHttpServerUri() + "/" + request); + if (body != null && !body.isEmpty()) { + uriRequest.setEntity(new StringEntity(body)); + } + + return executeRequest(uriRequest, header); + } + + public HttpResponse executePatchRequest(final String request, String body, Header... header) throws Exception { + HttpPatch uriRequest = new HttpPatch(getHttpServerUri() + "/" + request); + if (body != null && !body.isEmpty()) { + uriRequest.setEntity(new StringEntity(body)); + } + return executeRequest(uriRequest, header); + } + + public HttpResponse executeRequest(HttpUriRequest uriRequest, Header... header) throws Exception { + + CloseableHttpClient httpClient = null; + try { + + httpClient = getHTTPClient(); + + if (header != null && header.length > 0) { + for (int i = 0; i < header.length; i++) { + Header h = header[i]; + uriRequest.addHeader(h); + } + } + + if (!uriRequest.containsHeader("Content-Type")) { + uriRequest.addHeader("Content-Type","application/json"); + } + HttpResponse res = new HttpResponse(httpClient.execute(uriRequest)); + log.debug(res.getBody()); + return res; + } finally { + + if (httpClient != null) { + httpClient.close(); + } + } + } + + protected final String getHttpServerUri() { + final String address = "http" + (enableHTTPClientSSL ? "s" : "") + "://" + clusterInfo.httpHost + ":" + clusterInfo.httpPort; + log.debug("Connect to {}", address); + return address; + } + + protected final CloseableHttpClient getHTTPClient() throws Exception { + + final HttpClientBuilder hcb = HttpClients.custom(); + + if (enableHTTPClientSSL) { + + log.debug("Configure HTTP client with SSL"); + + if(prefix != null && !keystore.contains("/")) { + keystore = prefix+"/"+keystore; + } + + final String keyStorePath = FileHelper.getAbsoluteFilePathFromClassPath(keystore).toFile().getParent(); + + final KeyStore myTrustStore = KeyStore.getInstance("JKS"); + myTrustStore.load(new FileInputStream(keyStorePath+"/truststore.jks"), + "changeit".toCharArray()); + + final KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(new FileInputStream(FileHelper.getAbsoluteFilePathFromClassPath(keystore).toFile()), "changeit".toCharArray()); + + final SSLContextBuilder sslContextbBuilder = SSLContexts.custom(); + + if (trustHTTPServerCertificate) { + sslContextbBuilder.loadTrustMaterial(myTrustStore, null); + } + + if (sendHTTPClientCertificate) { + sslContextbBuilder.loadKeyMaterial(keyStore, "changeit".toCharArray()); + } + + final SSLContext sslContext = sslContextbBuilder.build(); + + String[] protocols = null; + + if (enableHTTPClientSSLv3Only) { + protocols = new String[] { "SSLv3" }; + } else { + protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }; + } + + final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( + sslContext, + protocols, + null, + NoopHostnameVerifier.INSTANCE); + + hcb.setSSLSocketFactory(sslsf); + } + + hcb.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(60 * 1000).build()); + + return hcb.build(); + } + + + public class HttpResponse { + private final CloseableHttpResponse inner; + private final String body; + private final Header[] header; + private final int statusCode; + private final String statusReason; + + public HttpResponse(CloseableHttpResponse inner) throws IllegalStateException, IOException { + super(); + this.inner = inner; + final HttpEntity entity = inner.getEntity(); + if(entity == null) { //head request does not have a entity + this.body = ""; + } else { + this.body = IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8); + } + this.header = inner.getAllHeaders(); + this.statusCode = inner.getStatusLine().getStatusCode(); + this.statusReason = inner.getStatusLine().getReasonPhrase(); + inner.close(); + } + + public CloseableHttpResponse getInner() { + return inner; + } + + public String getBody() { + return body; + } + + public Header[] getHeader() { + return header; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatusReason() { + return statusReason; + } + + @Override + public String toString() { + return "HttpResponse [inner=" + inner + ", body=" + body + ", header=" + Arrays.toString(header) + ", statusCode=" + statusCode + + ", statusReason=" + statusReason + "]"; + } + + } + + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/rules/OpenDistroSecurityTestWatcher.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/rules/OpenDistroSecurityTestWatcher.java new file mode 100644 index 0000000000..18bace9f11 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/helper/rules/OpenDistroSecurityTestWatcher.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test.helper.rules; + +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; + +public class OpenDistroSecurityTestWatcher extends TestWatcher{ + + @Override + protected void starting(final Description description) { + final String methodName = description.getMethodName(); + String className = description.getClassName(); + className = className.substring(className.lastIndexOf('.') + 1); + System.out.println("---------------- Starting JUnit-test: " + className + " " + methodName + " ----------------"); + } + + @Override + protected void failed(final Throwable e, final Description description) { + final String methodName = description.getMethodName(); + String className = description.getClassName(); + className = className.substring(className.lastIndexOf('.') + 1); + System.out.println(">>>> " + className + " " + methodName + " FAILED due to " + e); + } + + @Override + protected void finished(final Description description) { + // System.out.println("-----------------------------------------------------------------------------------------"); + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/test/plugin/UserInjectorPlugin.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/plugin/UserInjectorPlugin.java new file mode 100644 index 0000000000..e6b0e4cff5 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/test/plugin/UserInjectorPlugin.java @@ -0,0 +1,115 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.test.plugin; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.network.NetworkService; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.http.HttpServerTransport.Dispatcher; +import org.elasticsearch.http.netty4.Netty4HttpServerTransport; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.plugins.NetworkPlugin; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.threadpool.ThreadPool; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; + +/** + * Mimics the behavior of system integrators that run their own plugins (i.e. server transports) + * in front of Open Distro Security. This transport just copies the user string from the + * REST headers to the ThreadContext to test user injection. + * @author jkressin + */ +public class UserInjectorPlugin extends Plugin implements NetworkPlugin { + + Settings settings; + ThreadPool threadPool; + + public UserInjectorPlugin(final Settings settings, final Path configPath) { + this.settings = settings; + } + + @Override + public Map> getHttpTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + CircuitBreakerService circuitBreakerService, NamedWriteableRegistry namedWriteableRegistry, + NamedXContentRegistry xContentRegistry, NetworkService networkService, Dispatcher dispatcher) { + + Map> httpTransports = new HashMap>(1); + final UserInjectingDispatcher validatingDispatcher = new UserInjectingDispatcher(dispatcher); + httpTransports.put("com.amazon.opendistroforelasticsearch.security.http.UserInjectingServerTransport", () -> new UserInjectingServerTransport(settings, networkService, bigArrays, threadPool, xContentRegistry, validatingDispatcher)); + return httpTransports; + } + + class UserInjectingServerTransport extends Netty4HttpServerTransport { + + public UserInjectingServerTransport(final Settings settings, final NetworkService networkService, final BigArrays bigArrays, + final ThreadPool threadPool, final NamedXContentRegistry namedXContentRegistry, final Dispatcher dispatcher) { + super(settings, networkService, bigArrays, threadPool, namedXContentRegistry, dispatcher); + } + } + + class UserInjectingDispatcher implements Dispatcher { + + private Dispatcher originalDispatcher; + + public UserInjectingDispatcher(final Dispatcher originalDispatcher) { + super(); + this.originalDispatcher = originalDispatcher; + } + + @Override + public void dispatchRequest(RestRequest request, RestChannel channel, ThreadContext threadContext) { + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, request.header(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER)); + originalDispatcher.dispatchRequest(request, channel, threadContext); + + } + + @Override + public void dispatchBadRequest(RestRequest request, RestChannel channel, ThreadContext threadContext, + Throwable cause) { + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, request.header(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER)); + originalDispatcher.dispatchBadRequest(request, channel, threadContext, cause); + + } + } + +} diff --git a/src/test/java/org/elasticsearch/node/PluginAwareNode.java b/src/test/java/org/elasticsearch/node/PluginAwareNode.java new file mode 100644 index 0000000000..7e308a7f92 --- /dev/null +++ b/src/test/java/org/elasticsearch/node/PluginAwareNode.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.elasticsearch.node; + +import java.util.Arrays; +import java.util.Random; + +import org.elasticsearch.common.logging.LogConfigurator; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.plugins.Plugin; + +public class PluginAwareNode extends Node { + + private final boolean masterEligible; + + @SafeVarargs + public PluginAwareNode(boolean masterEligible, final Settings preparedSettings, final Class... plugins) { + super(InternalSettingsPreparer.prepareEnvironment(checkAndAddNodeName(preparedSettings), null), Arrays.asList(plugins), true); + this.masterEligible = masterEligible; + } + + @Override + protected void registerDerivedNodeNameWithLogger(String nodeName) { + LogConfigurator.setNodeName(nodeName); + } + + public boolean isMasterEligible() { + return masterEligible; + } + + private static Settings checkAndAddNodeName(final Settings settings) { + if(!settings.hasValue("node.name")) { + return Settings.builder().put(settings).put("node.name", "auto_node_name_"+System.currentTimeMillis()+"_"+ new Random().nextInt()).build(); + } + + return settings; + + } +} diff --git a/src/test/resources/action_groups.yml b/src/test/resources/action_groups.yml new file mode 100644 index 0000000000..840bfb18a9 --- /dev/null +++ b/src/test/resources/action_groups.yml @@ -0,0 +1,60 @@ +ALL: + - "indices:*" +MANAGE: + - "indices:monitor/*" + - "indices:admin/*" +CREATE_INDEX: + - "indices:admin/create" + - "indices:admin/mapping/put" +MANAGE_ALIASES: + - "indices:admin/aliases*" +MONITOR: + - "indices:monitor/*" +DATA_ACCESS: + - "indices:data/*" + - "indices:admin/mapping/put" +WRITE: + - "indices:data/write*" + - "indices:admin/mapping/put" +READ: + - "indices:data/read*" +DELETE: + - "indices:data/write/delete*" +CRUD: + - READ + - WRITE +SEARCH: + - "indices:data/read/search*" + - "indices:data/read/msearch*" + - SUGGEST +SUGGEST: + - "indices:data/read/suggest*" +INDEX: + - "indices:data/write/index*" + - "indices:data/write/update*" + - "indices:admin/mapping/put" +GET: + - "indices:data/read/get*" + - "indices:data/read/mget*" + +# CLUSTER +CLUSTER_ALL: + - cluster:* +CLUSTER_MONITOR: + - cluster:monitor/* + +CLUSTER_COMPOSITE_OPS_RO: + - "indices:data/read/mget" + - "indices:data/read/msearch" + - "indices:data/read/mtv" + - "indices:data/read/coordinate-msearch*" + - "indices:admin/aliases/exists*" + - "indices:admin/aliases/get*" +CLUSTER_COMPOSITE_OPS: + - "indices:data/write/bulk" + - "indices:admin/aliases*" + - "indices:data/write/reindex" + - CLUSTER_COMPOSITE_OPS_RO + +# HEALTH_AND_STATS = n"cluster:monitor/health*", "cluster:monitor/stats*", "indices:monitor/stats*", "cluster:monitor/nodes/stats*" +# ALL = cluster:*", "indices:admin/template/* \ No newline at end of file diff --git a/src/test/resources/action_groups_packaged.yml b/src/test/resources/action_groups_packaged.yml new file mode 100644 index 0000000000..178b743077 --- /dev/null +++ b/src/test/resources/action_groups_packaged.yml @@ -0,0 +1,91 @@ +UNLIMITED: + - "*" + +###### INDEX LEVEL ###### + +INDICES_ALL: + - "indices:*" + +# for backward compatibility +ALL: + - INDICES_ALL + +MANAGE: + - "indices:monitor/*" + - "indices:admin/*" + +CREATE_INDEX: + - "indices:admin/create" + - "indices:admin/mapping/put" + +MANAGE_ALIASES: + - "indices:admin/aliases*" + +# for backward compatibility +MONITOR: + - INDICES_MONITOR + +INDICES_MONITOR: + - "indices:monitor/*" + +DATA_ACCESS: + - "indices:data/*" + - CRUD + +WRITE: + - "indices:data/write*" + - "indices:admin/mapping/put" + +READ: + - "indices:data/read*" + - "indices:admin/mappings/fields/get*" + +DELETE: + - "indices:data/write/delete*" + +CRUD: + - READ + - WRITE + +SEARCH: + - "indices:data/read/search*" + - "indices:data/read/msearch*" + - SUGGEST + +SUGGEST: + - "indices:data/read/suggest*" + +INDEX: + - "indices:data/write/index*" + - "indices:data/write/update*" + - "indices:admin/mapping/put" + - "indices:data/write/bulk*" + +GET: + - "indices:data/read/get*" + - "indices:data/read/mget*" + +###### CLUSTER LEVEL ###### + +CLUSTER_ALL: + - "cluster:*" + +CLUSTER_MONITOR: + - "cluster:monitor/*" + +CLUSTER_COMPOSITE_OPS_RO: + - "indices:data/read/mget" + - "indices:data/read/msearch" + - "indices:data/read/mtv" + - "indices:data/read/coordinate-msearch*" + - "indices:admin/aliases/exists*" + - "indices:admin/aliases/get*" + +CLUSTER_COMPOSITE_OPS: + - "indices:data/write/bulk" + - "indices:admin/aliases*" + - CLUSTER_COMPOSITE_OPS_RO + +MANAGE_SNAPSHOTS: + - "cluster:admin/snapshot/*" + - "cluster:admin/repository/*" diff --git a/src/test/resources/composite_config.yml b/src/test/resources/composite_config.yml new file mode 100644 index 0000000000..f276427c84 --- /dev/null +++ b/src/test/resources/composite_config.yml @@ -0,0 +1,75 @@ +# +# HTTP +# basic (challenging) PREAUTH? +# proxy (not challenging, needs xff) +# kerberos (challenging) PREAUTH? +# clientcert (not challenging, needs https) + +# Authc +# internal +# noop +# ldap + +# Authz +# ldap +# noop + + + +opendistro_security: + dynamic: + composite_enabled: true + http: + anonymous_auth_enabled: false + xff: + enabled: false + internalProxies: 192\.168\.0\.10|192\.168\.0\.11 + remoteIpHeader: "x-forwarded-for" + proxiesHeader: "x-forwarded-by" + trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_basic_internal: + enabled: true + order: 0 + http_authenticator: + type: basic + authentication_backend: + type: intern + authentication_domain_clientcert: + enabled: false + order: 1 + http_authenticator: + type: clientcert + authentication_backend: + type: noop + authentication_domain_proxy: + enabled: false + order: 2 + http_authenticator: + type: proxy + config: + user_header: "x-proxy-user" + roles_header: "x-proxy-roles" + authentication_backend: + type: noop + authentication_domain_kerb: + enabled: false + order: 3 + http_authenticator: + type: kerberos + authentication_backend: + type: noop + authz: + roles_from_myldap: + enabled: false + authorization_backend: + type: ldap + config: + rolesearch: "(uniqueMember={0})" + resolve_nested_roles: true + rolebase: "ou=groups,o=TEST" + rolename: cn + roles_from_xxx: + enabled: false + authorization_backend: + type: xxx \ No newline at end of file diff --git a/src/test/resources/config.yml b/src/test/resources/config.yml new file mode 100644 index 0000000000..5e98bc4f37 --- /dev/null +++ b/src/test/resources/config.yml @@ -0,0 +1,75 @@ +# +# HTTP +# basic (challenging) PREAUTH? +# proxy (not challenging, needs xff) +# kerberos (challenging) PREAUTH? +# clientcert (not challenging, needs https) + +# Authc +# internal +# noop +# ldap + +# Authz +# ldap +# noop + + + +opendistro_security: + dynamic: + filtered_alias_mode: disallow + http: + anonymous_auth_enabled: false + xff: + enabled: false + internalProxies: 192\.168\.0\.10|192\.168\.0\.11 + remoteIpHeader: "x-forwarded-for" + proxiesHeader: "x-forwarded-by" + trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_basic_internal: + enabled: true + order: 0 + http_authenticator: + type: basic + authentication_backend: + type: intern + authentication_domain_clientcert: + enabled: false + order: 1 + http_authenticator: + type: clientcert + authentication_backend: + type: noop + authentication_domain_proxy: + enabled: false + order: 2 + http_authenticator: + type: proxy + config: + user_header: "x-proxy-user" + roles_header: "x-proxy-roles" + authentication_backend: + type: noop + authentication_domain_kerb: + enabled: false + order: 3 + http_authenticator: + type: kerberos + authentication_backend: + type: noop + authz: + roles_from_myldap: + enabled: false + authorization_backend: + type: ldap + config: + rolesearch: "(uniqueMember={0})" + resolve_nested_roles: true + rolebase: "ou=groups,o=TEST" + rolename: cn + roles_from_xxx: + enabled: false + authorization_backend: + type: xxx \ No newline at end of file diff --git a/src/test/resources/config_anon.yml b/src/test/resources/config_anon.yml new file mode 100644 index 0000000000..f7e8d73ce0 --- /dev/null +++ b/src/test/resources/config_anon.yml @@ -0,0 +1,18 @@ +opendistro_security: + dynamic: + http: + anonymous_auth_enabled: true + xff: + enabled: false + internalProxies: 192\.168\.0\.10|192\.168\.0\.11 + remoteIpHeader: "x-forwarded-for" + proxiesHeader: "x-forwarded-by" + trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_basic_internal: + enabled: true + order: 0 + http_authenticator: + type: basic + authentication_backend: + type: intern \ No newline at end of file diff --git a/src/test/resources/config_clientcert.yml b/src/test/resources/config_clientcert.yml new file mode 100644 index 0000000000..5030b8dc8e --- /dev/null +++ b/src/test/resources/config_clientcert.yml @@ -0,0 +1,18 @@ +opendistro_security: + dynamic: + http: + anonymous_auth_enabled: false + xff: + enabled: false + internalProxies: 192\.168\.0\.10|192\.168\.0\.11 + remoteIpHeader: "x-forwarded-for" + proxiesHeader: "x-forwarded-by" + trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_clientcert: + enabled: true + order: 0 + http_authenticator: + type: clientcert + authentication_backend: + type: noop \ No newline at end of file diff --git a/src/test/resources/config_dnfof.yml b/src/test/resources/config_dnfof.yml new file mode 100644 index 0000000000..d4d2d1db56 --- /dev/null +++ b/src/test/resources/config_dnfof.yml @@ -0,0 +1,76 @@ +# +# HTTP +# basic (challenging) PREAUTH? +# proxy (not challenging, needs xff) +# kerberos (challenging) PREAUTH? +# clientcert (not challenging, needs https) + +# Authc +# internal +# noop +# ldap + +# Authz +# ldap +# noop + + + +opendistro_security: + dynamic: + filtered_alias_mode: disallow + do_not_fail_on_forbidden: true + http: + anonymous_auth_enabled: false + xff: + enabled: false + internalProxies: 192\.168\.0\.10|192\.168\.0\.11 + remoteIpHeader: "x-forwarded-for" + proxiesHeader: "x-forwarded-by" + trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_basic_internal: + enabled: true + order: 0 + http_authenticator: + type: basic + authentication_backend: + type: intern + authentication_domain_clientcert: + enabled: false + order: 1 + http_authenticator: + type: clientcert + authentication_backend: + type: noop + authentication_domain_proxy: + enabled: false + order: 2 + http_authenticator: + type: proxy + config: + user_header: "x-proxy-user" + roles_header: "x-proxy-roles" + authentication_backend: + type: noop + authentication_domain_kerb: + enabled: false + order: 3 + http_authenticator: + type: kerberos + authentication_backend: + type: noop + authz: + roles_from_myldap: + enabled: false + authorization_backend: + type: ldap + config: + rolesearch: "(uniqueMember={0})" + resolve_nested_roles: true + rolebase: "ou=groups,o=TEST" + rolename: cn + roles_from_xxx: + enabled: false + authorization_backend: + type: xxx \ No newline at end of file diff --git a/src/test/resources/config_invalidlic.yml b/src/test/resources/config_invalidlic.yml new file mode 100644 index 0000000000..5e98bc4f37 --- /dev/null +++ b/src/test/resources/config_invalidlic.yml @@ -0,0 +1,75 @@ +# +# HTTP +# basic (challenging) PREAUTH? +# proxy (not challenging, needs xff) +# kerberos (challenging) PREAUTH? +# clientcert (not challenging, needs https) + +# Authc +# internal +# noop +# ldap + +# Authz +# ldap +# noop + + + +opendistro_security: + dynamic: + filtered_alias_mode: disallow + http: + anonymous_auth_enabled: false + xff: + enabled: false + internalProxies: 192\.168\.0\.10|192\.168\.0\.11 + remoteIpHeader: "x-forwarded-for" + proxiesHeader: "x-forwarded-by" + trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_basic_internal: + enabled: true + order: 0 + http_authenticator: + type: basic + authentication_backend: + type: intern + authentication_domain_clientcert: + enabled: false + order: 1 + http_authenticator: + type: clientcert + authentication_backend: + type: noop + authentication_domain_proxy: + enabled: false + order: 2 + http_authenticator: + type: proxy + config: + user_header: "x-proxy-user" + roles_header: "x-proxy-roles" + authentication_backend: + type: noop + authentication_domain_kerb: + enabled: false + order: 3 + http_authenticator: + type: kerberos + authentication_backend: + type: noop + authz: + roles_from_myldap: + enabled: false + authorization_backend: + type: ldap + config: + rolesearch: "(uniqueMember={0})" + resolve_nested_roles: true + rolebase: "ou=groups,o=TEST" + rolename: cn + roles_from_xxx: + enabled: false + authorization_backend: + type: xxx \ No newline at end of file diff --git a/src/test/resources/config_ldap.yml b/src/test/resources/config_ldap.yml new file mode 100644 index 0000000000..ac7dca3116 --- /dev/null +++ b/src/test/resources/config_ldap.yml @@ -0,0 +1,27 @@ +opendistro_security: + dynamic: + http: + xff: + enabled: false + internalProxies: 192\.168\.0\.10|192\.168\.0\.11 + remoteIpHeader: "x-forwarded-for" + proxiesHeader: "x-forwarded-by" + trustedProxies: "proxy1|proxy2" + authenticator: + type: com.amazon.opendistroforelasticsearch.security.http.HTTPBasicAuthenticator + authcz: + authentication_domain_basic_internal: + enabled: true + order: 1 + authentication_backend: + type: ldap + config: + host: "localhost:40622" + usersearch: "(uid={0})" + authorization_backend: + type: ldap + config: + rolesearch: "(uniqueMember={0})" + resolve_nested_roles: true + rolebase: "ou=groups,o=TEST" + rolename: cn \ No newline at end of file diff --git a/src/test/resources/config_lic.yml b/src/test/resources/config_lic.yml new file mode 100644 index 0000000000..5e98bc4f37 --- /dev/null +++ b/src/test/resources/config_lic.yml @@ -0,0 +1,75 @@ +# +# HTTP +# basic (challenging) PREAUTH? +# proxy (not challenging, needs xff) +# kerberos (challenging) PREAUTH? +# clientcert (not challenging, needs https) + +# Authc +# internal +# noop +# ldap + +# Authz +# ldap +# noop + + + +opendistro_security: + dynamic: + filtered_alias_mode: disallow + http: + anonymous_auth_enabled: false + xff: + enabled: false + internalProxies: 192\.168\.0\.10|192\.168\.0\.11 + remoteIpHeader: "x-forwarded-for" + proxiesHeader: "x-forwarded-by" + trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_basic_internal: + enabled: true + order: 0 + http_authenticator: + type: basic + authentication_backend: + type: intern + authentication_domain_clientcert: + enabled: false + order: 1 + http_authenticator: + type: clientcert + authentication_backend: + type: noop + authentication_domain_proxy: + enabled: false + order: 2 + http_authenticator: + type: proxy + config: + user_header: "x-proxy-user" + roles_header: "x-proxy-roles" + authentication_backend: + type: noop + authentication_domain_kerb: + enabled: false + order: 3 + http_authenticator: + type: kerberos + authentication_backend: + type: noop + authz: + roles_from_myldap: + enabled: false + authorization_backend: + type: ldap + config: + rolesearch: "(uniqueMember={0})" + resolve_nested_roles: true + rolebase: "ou=groups,o=TEST" + rolename: cn + roles_from_xxx: + enabled: false + authorization_backend: + type: xxx \ No newline at end of file diff --git a/src/test/resources/config_lic_rk.yml b/src/test/resources/config_lic_rk.yml new file mode 100644 index 0000000000..5e98bc4f37 --- /dev/null +++ b/src/test/resources/config_lic_rk.yml @@ -0,0 +1,75 @@ +# +# HTTP +# basic (challenging) PREAUTH? +# proxy (not challenging, needs xff) +# kerberos (challenging) PREAUTH? +# clientcert (not challenging, needs https) + +# Authc +# internal +# noop +# ldap + +# Authz +# ldap +# noop + + + +opendistro_security: + dynamic: + filtered_alias_mode: disallow + http: + anonymous_auth_enabled: false + xff: + enabled: false + internalProxies: 192\.168\.0\.10|192\.168\.0\.11 + remoteIpHeader: "x-forwarded-for" + proxiesHeader: "x-forwarded-by" + trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_basic_internal: + enabled: true + order: 0 + http_authenticator: + type: basic + authentication_backend: + type: intern + authentication_domain_clientcert: + enabled: false + order: 1 + http_authenticator: + type: clientcert + authentication_backend: + type: noop + authentication_domain_proxy: + enabled: false + order: 2 + http_authenticator: + type: proxy + config: + user_header: "x-proxy-user" + roles_header: "x-proxy-roles" + authentication_backend: + type: noop + authentication_domain_kerb: + enabled: false + order: 3 + http_authenticator: + type: kerberos + authentication_backend: + type: noop + authz: + roles_from_myldap: + enabled: false + authorization_backend: + type: ldap + config: + rolesearch: "(uniqueMember={0})" + resolve_nested_roles: true + rolebase: "ou=groups,o=TEST" + rolename: cn + roles_from_xxx: + enabled: false + authorization_backend: + type: xxx \ No newline at end of file diff --git a/src/test/resources/config_multirolespan.yml b/src/test/resources/config_multirolespan.yml new file mode 100644 index 0000000000..17183a0b55 --- /dev/null +++ b/src/test/resources/config_multirolespan.yml @@ -0,0 +1,76 @@ +# +# HTTP +# basic (challenging) PREAUTH? +# proxy (not challenging, needs xff) +# kerberos (challenging) PREAUTH? +# clientcert (not challenging, needs https) + +# Authc +# internal +# noop +# ldap + +# Authz +# ldap +# noop + + + +opendistro_security: + dynamic: + multi_rolespan_enabled: true + filtered_alias_mode: disallow + http: + anonymous_auth_enabled: false + xff: + enabled: false + internalProxies: 192\.168\.0\.10|192\.168\.0\.11 + remoteIpHeader: "x-forwarded-for" + proxiesHeader: "x-forwarded-by" + trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_basic_internal: + enabled: true + order: 0 + http_authenticator: + type: basic + authentication_backend: + type: intern + authentication_domain_clientcert: + enabled: false + order: 1 + http_authenticator: + type: clientcert + authentication_backend: + type: noop + authentication_domain_proxy: + enabled: false + order: 2 + http_authenticator: + type: proxy + config: + user_header: "x-proxy-user" + roles_header: "x-proxy-roles" + authentication_backend: + type: noop + authentication_domain_kerb: + enabled: false + order: 3 + http_authenticator: + type: kerberos + authentication_backend: + type: noop + authz: + roles_from_myldap: + enabled: false + authorization_backend: + type: ldap + config: + rolesearch: "(uniqueMember={0})" + resolve_nested_roles: true + rolebase: "ou=groups,o=TEST" + rolename: cn + roles_from_xxx: + enabled: false + authorization_backend: + type: xxx \ No newline at end of file diff --git a/src/test/resources/config_proxy.yml b/src/test/resources/config_proxy.yml new file mode 100644 index 0000000000..f41ddbb548 --- /dev/null +++ b/src/test/resources/config_proxy.yml @@ -0,0 +1,28 @@ +opendistro_security: + dynamic: + http: + xff: + enabled: true + internalProxies: '.*' + #remoteIpHeader: "x-forwarded-for" + #proxiesHeader: "x-forwarded-by" + #trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_proxy: + enabled: true + order: 0 + challenge: false + http_authenticator: + type: proxy + config: + user_header: "x-proxy-user" + roles_header: "x-proxy-roles" + authentication_backend: + type: noop + authentication_domain_basic_internal: + enabled: true + order: 1 + http_authenticator: + type: basic + authentication_backend: + type: intern \ No newline at end of file diff --git a/src/test/resources/config_proxy_custom.yml b/src/test/resources/config_proxy_custom.yml new file mode 100644 index 0000000000..31280c205c --- /dev/null +++ b/src/test/resources/config_proxy_custom.yml @@ -0,0 +1,29 @@ +opendistro_security: + dynamic: + http: + xff: + enabled: true + internalProxies: '.*' + #remoteIpHeader: "x-forwarded-for" + #proxiesHeader: "x-forwarded-by" + #trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_proxy: + enabled: true + order: 0 + challenge: false + http_authenticator: + type: proxy + config: + user_header: "user" + roles_header: "roles" + roles_separator: ";" + authentication_backend: + type: noop + authentication_domain_basic_internal: + enabled: true + order: 1 + http_authenticator: + type: basic + authentication_backend: + type: intern \ No newline at end of file diff --git a/src/test/resources/config_rest_impersonation.yml b/src/test/resources/config_rest_impersonation.yml new file mode 100644 index 0000000000..b161bac56f --- /dev/null +++ b/src/test/resources/config_rest_impersonation.yml @@ -0,0 +1,20 @@ +opendistro_security: + dynamic: + http: + xff: + enabled: false + authc: + authentication_domain_basic_internal: + enabled: true + order: 0 + http_authenticator: + type: basic + authentication_backend: + type: intern + authentication_domain_basic_noop: + enabled: true + order: 1 + http_authenticator: + type: basic + authentication_backend: + type: noop \ No newline at end of file diff --git a/src/test/resources/config_transport_username.yml b/src/test/resources/config_transport_username.yml new file mode 100644 index 0000000000..bedef0deb3 --- /dev/null +++ b/src/test/resources/config_transport_username.yml @@ -0,0 +1,76 @@ +# +# HTTP +# basic (challenging) PREAUTH? +# proxy (not challenging, needs xff) +# kerberos (challenging) PREAUTH? +# clientcert (not challenging, needs https) + +# Authc +# internal +# noop +# ldap + +# Authz +# ldap +# noop + + + +opendistro_security: + dynamic: + transport_userrname_attribute: O + filtered_alias_mode: disallow + http: + anonymous_auth_enabled: false + xff: + enabled: false + internalProxies: 192\.168\.0\.10|192\.168\.0\.11 + remoteIpHeader: "x-forwarded-for" + proxiesHeader: "x-forwarded-by" + trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_basic_internal: + enabled: true + order: 0 + http_authenticator: + type: basic + authentication_backend: + type: intern + authentication_domain_clientcert: + enabled: false + order: 1 + http_authenticator: + type: clientcert + authentication_backend: + type: noop + authentication_domain_proxy: + enabled: false + order: 2 + http_authenticator: + type: proxy + config: + user_header: "x-proxy-user" + roles_header: "x-proxy-roles" + authentication_backend: + type: noop + authentication_domain_kerb: + enabled: false + order: 3 + http_authenticator: + type: kerberos + authentication_backend: + type: noop + authz: + roles_from_myldap: + enabled: false + authorization_backend: + type: ldap + config: + rolesearch: "(uniqueMember={0})" + resolve_nested_roles: true + rolebase: "ou=groups,o=TEST" + rolename: cn + roles_from_xxx: + enabled: false + authorization_backend: + type: xxx \ No newline at end of file diff --git a/src/test/resources/config_xff.yml b/src/test/resources/config_xff.yml new file mode 100644 index 0000000000..891310161b --- /dev/null +++ b/src/test/resources/config_xff.yml @@ -0,0 +1,74 @@ +# +# HTTP +# basic (challenging) PREAUTH? +# proxy (not challenging, needs xff) +# kerberos (challenging) PREAUTH? +# clientcert (not challenging, needs https) + +# Authc +# internal +# noop +# ldap + +# Authz +# ldap +# noop + + + +opendistro_security: + dynamic: + http: + anonymous_auth_enabled: false + xff: + enabled: true + internalProxies: '.*' + remoteIpHeader: "x-forwarded-for" + #proxiesHeader: "x-forwarded-by" + #trustedProxies: "proxy1|proxy2" + authc: + authentication_domain_basic_internal: + enabled: true + order: 0 + http_authenticator: + type: basic + authentication_backend: + type: intern + authentication_domain_clientcert: + enabled: false + order: 1 + http_authenticator: + type: clientcert + authentication_backend: + type: noop + authentication_domain_proxy: + enabled: false + order: 2 + http_authenticator: + type: proxy + config: + user_header: "x-proxy-user" + roles_header: "x-proxy-roles" + authentication_backend: + type: noop + authentication_domain_kerb: + enabled: false + order: 3 + http_authenticator: + type: kerberos + authentication_backend: + type: noop + authz: + roles_from_myldap: + enabled: false + authorization_backend: + type: ldap + config: + rolesearch: "(uniqueMember={0})" + resolve_nested_roles: true + rolebase: "ou=groups,o=TEST" + rolename: cn + roles_from_xxx: + enabled: false + authorization_backend: + type: xxx \ No newline at end of file diff --git a/src/test/resources/data1.json b/src/test/resources/data1.json new file mode 100644 index 0000000000..5ab82f4205 --- /dev/null +++ b/src/test/resources/data1.json @@ -0,0 +1,26 @@ +{ + "title": "title value", + "name": "name value", + "age": 121, + "created": "2017-11-11", + "session_data": "session data value", + "manager": { + "age": 77, + "name": "manager name value", + "inner": { + "a": 1, + "b": "b value" + } + }, + "employees": [ + { + "age": 1, + "name": "emp1 name value" + }, + { + "age": 2, + "name": "emp2 name value" + } + ], + "city": "city value" +} \ No newline at end of file diff --git a/src/test/resources/data2.json b/src/test/resources/data2.json new file mode 100644 index 0000000000..0b8d056c78 --- /dev/null +++ b/src/test/resources/data2.json @@ -0,0 +1,6 @@ +{ + "text": "text question value", + "joinfield": { + "name": "question" + } +} \ No newline at end of file diff --git a/src/test/resources/data3.json b/src/test/resources/data3.json new file mode 100644 index 0000000000..31fe1bacd5 --- /dev/null +++ b/src/test/resources/data3.json @@ -0,0 +1,7 @@ +{ + "text": "text answer value", + "joinfield": { + "name": "answer", + "parent": "1" + } +} \ No newline at end of file diff --git a/src/test/resources/internal_empty.yml b/src/test/resources/internal_empty.yml new file mode 100644 index 0000000000..22ff21ce40 --- /dev/null +++ b/src/test/resources/internal_empty.yml @@ -0,0 +1,2 @@ +"_": + hash: "_" diff --git a/src/test/resources/internal_users.yml b/src/test/resources/internal_users.yml new file mode 100644 index 0000000000..aad7baafe4 --- /dev/null +++ b/src/test/resources/internal_users.yml @@ -0,0 +1,183 @@ +"\"'+-,;_?*@<>!$%&/()=#": + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +"§ÄÖÜäöüß": + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +bug.99: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m +bug108: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +nagilum: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + +'CN=spock,OU=client,O=client,L=Test,C=DE': + #transport login only + hash: '_impersonation_only_' + roles: + - vulcan + - starfleet + +spock: + hash: $2a$12$GI9JXffO3WUjTsU7Yy3E4.LBxC2ILo66Zg/rr79BpikSL2IIRezQa + #password is: spock + roles: + - vulcan + - starfleet +sarek: + hash: $2a$12$Ioo1uXmH.Nq/lS5dUVBEsePSmZ5pSIpVO/xKHaquU/Jvq97I7nAgG + #password is: sarek +kirk: + hash: $2a$12$xZOcnwYPYQ3zIadnlQIJ0eNhX1ngwMkTN.oMwkKxoGvDVPn4/6XtO + #password is: kirk + roles: + - captains + - starfleet +picard: + hash: $2a$12$wkY2BsRneCU5za1OPYlzsehQit6gu2vprVv/4jHiSEEBv2ThunaTS + #password is: picard + roles: + - captains + - starfleet +worf: + hash: $2a$12$A41IxPXV1/Dx46C6i1ufGubv.p3qYX7xVcY46q33sylYbIqQVwTMu + #password is: worf + roles: + - klingon + - starfleet +crusherw: + hash: $2a$12$61vXe3cXy32p0cjsW0Y/SeZa7kEVSWuQK0jg98D9d5zOGXfo5NgyC + #password is: crusherw + roles: + - starfleet_academy +abc: + hash: $2a$12$bP0CO5d5nhmaTOj7mGteHugXQQ8jlSV0dxcl5//moZ1xnI.pVPXfe + #password is: abc:abc + roles: + - klingon + - starfleet +userwithnopasswd: + hash: null + roles: + - klingon + - starfleet +userwithblankpasswd: + hash: "" + roles: + - klingon + - starfleet +theindexadmin: + hash: "$2a$12$P.QbiwOsnxgz7kLBT10F7u6GhY7//Keyz7Xwf7lNzskRxpo9.zxFS" +writer: + hash: $2a$12$LZvbDVnegkTbEFTu9hHnWO4HIrdB9rGaKcEOID5n0VV4j58cnvyZ. +dlsnoinvest: + hash: $2a$12$9Zr4IgoJRqK6xJq4xjoa6OZAnY4QOQ6xIhcCxeYoQtB/HriMkeJSC +baz: + hash: $2a$12$A41IxPXV1/Dx46C6i1ufGubv.p3qYX7xVcY46q33sylYbIqQVwTMu + #password is: worf + +user_role01: + hash: '$2a$12$XrBfLQh2T8wIzpxE5vzhUOPjjGfONcD8UEjd5IT5KveG8ULZaj04.' + # password is: user_role01 + roles: + - role01 + +user_role01_role02_role03: + hash: '$2a$12$6.4Y6L//xeKQ7t8YEG0s6OH4F4q9gMw0J8E0GjmUMNZeyIWu1IRWS' + # password is: user_role01_role02_role03 + roles: + - role01 + - role02 + - role03 + +restoreuser: + hash: "$2a$12$JU2QjYVTlI24Q/enEOpf2uTLCPGchN.eXWCsrBiieUcRoeh53NB0y" + #password is: restoreuser + +snapresuser: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + +logstash: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + +knuddel: + hash: _imponly_ + +twitter: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + +aliasmngt: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + roles: + - opendistro_security_aliasmngt + +user_a: + hash: $2a$04$NDy7mGbRNrmPMh9nSnIB.OTMFkcioEd69A04ReSGkJDd7QHxnCcVC + #password is: user_a + roles: + - opendistro_security_ua + +user_b: + hash: $2a$04$idGSEpNOhFbyiRL6toGPT.orh7ENOEU8kAqwkRFaXWRdA6wVgyqUu + #password is: user_b + roles: + - opendistro_security_ub +user_c: + hash: $2a$04$jQcEXpODnTFoGDuA7DPdSevA84CuH/7MOYkb80M3XZIrH76YMWS9G + #password is: user_c + roles: + - opendistro_security_uc + +custattr: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + attributes: + c1: v1 + c2: v2 + c3.c4.cd: test1 + 'c4.c4.cd': test2 + 'c5': null + null: abc + +rexclude: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + +aliastest: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m +mindex12: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +ccsresolv: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +underscore: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +bulk: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + roles: + - opendistro_security_bulk +557: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + roles: + - opendistro_security_557 +itt1635: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + roles: + - opendistro_security_esb_1 + - opendistro_security_esb_3 + - opendistro_security_esb_5 +#password is: kibanaserver +kibanaserver: + readonly: true + hash: $2a$12$4AcgAt3xwOWadA5s5blL6ev39OXDNhmOesEoo33eZtrq2N0YrU3H. \ No newline at end of file diff --git a/src/test/resources/internal_users_spock_add_roles.yml b/src/test/resources/internal_users_spock_add_roles.yml new file mode 100644 index 0000000000..da046557db --- /dev/null +++ b/src/test/resources/internal_users_spock_add_roles.yml @@ -0,0 +1,61 @@ +"\"'+-,;_?*@<>!$%&/()=#": + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +"§ÄÖÜäöüß": + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +bug.99: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +bug108: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +nagilum: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + +'CN=spock,OU=client,O=client,L=Test,C=DE': + #transport login only + hash: '_impersonation_only_' + roles: + - vulcan + - starfleet + +spock: + hash: $2a$12$GI9JXffO3WUjTsU7Yy3E4.LBxC2ILo66Zg/rr79BpikSL2IIRezQa + #password is: spock + roles: + - vulcan + - additionalrole1 + - additionalrole2 +sarek: + hash: $2a$12$Ioo1uXmH.Nq/lS5dUVBEsePSmZ5pSIpVO/xKHaquU/Jvq97I7nAgG + #password is: sarek + roles: + - vulcanadmin + - vulcan + - ambassador +kirk: + hash: $2a$12$xZOcnwYPYQ3zIadnlQIJ0eNhX1ngwMkTN.oMwkKxoGvDVPn4/6XtO + #password is: kirk + roles: + - captains + - starfleet +picard: + hash: $2a$12$wkY2BsRneCU5za1OPYlzsehQit6gu2vprVv/4jHiSEEBv2ThunaTS + #password is: picard + roles: + - captains + - starfleet +worf: + hash: $2a$12$A41IxPXV1/Dx46C6i1ufGubv.p3qYX7xVcY46q33sylYbIqQVwTMu + #password is: worf + roles: + - klingon + - starfleet + +crusherw: + hash: $2a$12$61vXe3cXy32p0cjsW0Y/SeZa7kEVSWuQK0jg98D9d5zOGXfo5NgyC + #password is: crusherw + roles: + - starfleet_academy \ No newline at end of file diff --git a/src/test/resources/internal_users_transport_username.yml b/src/test/resources/internal_users_transport_username.yml new file mode 100644 index 0000000000..40140dcff1 --- /dev/null +++ b/src/test/resources/internal_users_transport_username.yml @@ -0,0 +1,183 @@ +"\"'+-,;_?*@<>!$%&/()=#": + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +"§ÄÖÜäöüß": + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +bug.99: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m +bug108: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +nagilum: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + +'client': + #transport login only + hash: '_impersonation_only_' + roles: + - vulcan + - starfleet + +spock: + hash: $2a$12$GI9JXffO3WUjTsU7Yy3E4.LBxC2ILo66Zg/rr79BpikSL2IIRezQa + #password is: spock + roles: + - vulcan + - starfleet +sarek: + hash: $2a$12$Ioo1uXmH.Nq/lS5dUVBEsePSmZ5pSIpVO/xKHaquU/Jvq97I7nAgG + #password is: sarek +kirk: + hash: $2a$12$xZOcnwYPYQ3zIadnlQIJ0eNhX1ngwMkTN.oMwkKxoGvDVPn4/6XtO + #password is: kirk + roles: + - captains + - starfleet +picard: + hash: $2a$12$wkY2BsRneCU5za1OPYlzsehQit6gu2vprVv/4jHiSEEBv2ThunaTS + #password is: picard + roles: + - captains + - starfleet +worf: + hash: $2a$12$A41IxPXV1/Dx46C6i1ufGubv.p3qYX7xVcY46q33sylYbIqQVwTMu + #password is: worf + roles: + - klingon + - starfleet +crusherw: + hash: $2a$12$61vXe3cXy32p0cjsW0Y/SeZa7kEVSWuQK0jg98D9d5zOGXfo5NgyC + #password is: crusherw + roles: + - starfleet_academy +abc: + hash: $2a$12$bP0CO5d5nhmaTOj7mGteHugXQQ8jlSV0dxcl5//moZ1xnI.pVPXfe + #password is: abc:abc + roles: + - klingon + - starfleet +userwithnopasswd: + hash: null + roles: + - klingon + - starfleet +userwithblankpasswd: + hash: "" + roles: + - klingon + - starfleet +theindexadmin: + hash: "$2a$12$P.QbiwOsnxgz7kLBT10F7u6GhY7//Keyz7Xwf7lNzskRxpo9.zxFS" +writer: + hash: $2a$12$LZvbDVnegkTbEFTu9hHnWO4HIrdB9rGaKcEOID5n0VV4j58cnvyZ. +dlsnoinvest: + hash: $2a$12$9Zr4IgoJRqK6xJq4xjoa6OZAnY4QOQ6xIhcCxeYoQtB/HriMkeJSC +baz: + hash: $2a$12$A41IxPXV1/Dx46C6i1ufGubv.p3qYX7xVcY46q33sylYbIqQVwTMu + #password is: worf + +user_role01: + hash: '$2a$12$XrBfLQh2T8wIzpxE5vzhUOPjjGfONcD8UEjd5IT5KveG8ULZaj04.' + # password is: user_role01 + roles: + - role01 + +user_role01_role02_role03: + hash: '$2a$12$6.4Y6L//xeKQ7t8YEG0s6OH4F4q9gMw0J8E0GjmUMNZeyIWu1IRWS' + # password is: user_role01_role02_role03 + roles: + - role01 + - role02 + - role03 + +restoreuser: + hash: "$2a$12$JU2QjYVTlI24Q/enEOpf2uTLCPGchN.eXWCsrBiieUcRoeh53NB0y" + #password is: restoreuser + +snapresuser: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + +logstash: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + +knuddel: + hash: _imponly_ + +twitter: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + +aliasmngt: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + roles: + - opendistro_security_aliasmngt + +user_a: + hash: $2a$04$NDy7mGbRNrmPMh9nSnIB.OTMFkcioEd69A04ReSGkJDd7QHxnCcVC + #password is: user_a + roles: + - opendistro_security_ua + +user_b: + hash: $2a$04$idGSEpNOhFbyiRL6toGPT.orh7ENOEU8kAqwkRFaXWRdA6wVgyqUu + #password is: user_b + roles: + - opendistro_security_ub +user_c: + hash: $2a$04$jQcEXpODnTFoGDuA7DPdSevA84CuH/7MOYkb80M3XZIrH76YMWS9G + #password is: user_c + roles: + - opendistro_security_uc + +custattr: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + attributes: + c1: v1 + c2: v2 + c3.c4.cd: test1 + 'c4.c4.cd': test2 + 'c5': null + null: abc + +rexclude: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + +aliastest: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m +mindex12: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +ccsresolv: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +underscore: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum +bulk: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + roles: + - opendistro_security_bulk +557: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + roles: + - opendistro_security_557 +itt1635: + hash: $2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m + #password is: nagilum + roles: + - opendistro_security_esb_1 + - opendistro_security_esb_3 + - opendistro_security_esb_5 +#password is: kibanaserver +kibanaserver: + readonly: true + hash: $2a$12$4AcgAt3xwOWadA5s5blL6ev39OXDNhmOesEoo33eZtrq2N0YrU3H. \ No newline at end of file diff --git a/src/test/resources/kirk-keystore.jks b/src/test/resources/kirk-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..dd7562ef81822291c9e48be60b6daef2883288a9 GIT binary patch literal 4525 zcmdVccT^MWwgB){LPGDoiG|JtLlcmi&;&(_6h#7rP(uq{DS;pYA_Afmm4HYQP!tF# zO+it?LY1P3NK;Ux3W)H6$9wKM=ic}3T5sJy?)_ud%--LgH8bDr`F*qXdFwL>1cER& z@XrnA;}zt?2;6|iLvA1t91KukKLBYi?ob|vHEVPXTvFy98Cf88WeS!f7A?SFPvZg)V z7v6j;sEtmwpQf&7q@7zop%6{r2s~o4`h$O*s-O*b>U-1znW*c#zSZBch%8xV*PK!@M#{2`WZYarXxm&{`QatOX?4u9M`&!g z&8@j*V|x00YnIuVYvX+t0oL3*%ZApt%D}TBgx;82*eFi6zP*KyC^rTTB}T2_jyE(_ z;f-sVA5zg)Q)lMvlIJLf*G91S{6-yTXOSiB%Pl6=)ezz52b@>D2PMdN`A#<=9WZ-| z7KW!1+>q&M9FI4m)Uo2)<*krrUc5S@Z)L7G7dgI!>|D_H@?HQ2<&8$8eAM5V`nS_j zNpG-+#Ccy*OAKTS(OGuQwm4>u0HI3?WwUI`J?nS!O@}YqX!P0n!lx^wZ-xy>9X%}X z%zmko=qLu(IqcUGH-zYG9NL}p(8Yv5_N2}HLil{#_GY+#+?8~+txTk4E?9{>Y6;8Q zEZMQQG+Zc7(ONjc_Z{R_?;uOgps(gW9B#f(6USN@W|Q~IWxj{osPJ=+k~BeQ@cQL| z$FtVYsZN!ibBkZ5nzYpH;ZHJ%>jHUIYX_z7yg_w(Pt?L{^X(1y1V;C7nhA@9^p>1jxzk4S}xL;ZtiB54PN2WN{SYvv3Rh9rlt7aOkInvb_wo?IeDCPpYvkB~u~s|dnp zzlO4kJt=!g)8MF%;L*>C7`w|&Q5;Zw^-N`BMf^2O*~k34r_QaBMiXr@B`1*OXV;sA z9gZ{qm~Tz#GUS?VMvHuD!B$x1U&)(!$F*Sp&b2~cEiXb}fOVHxav+Cc&lruM_R&V( z{43J!IB|M3ZPcwnQbKw_ebQNXOK%`r0lA@di3f&@c4}>O5mAX}`oa7CAntB*PQV6H z^L0WBm#47xTa;TPT3rIKe{dQg49MoR+S7kZUoEgm8$1*sT7hyWeFN zn%<%O^kC4vVk0^76VKU1bP6oo&2Wj(7bV7r$yWzRuT)$$b)}BoAG@t&D^9#0)4o5t z3!Xb!WQChtbPDCZqnv#{@Cy@D^Gw)@;l6aTSvhyQ&XWfqXs~EWy=uctyrPgS<;cB3 ziFkO@!mSh-w6rR5LZ+*OP##Ou?k-vUl=T*#5wv)XW2;UyNTZj8}9GudhAJ(K5qN?vK^qvH6t3LGn(B3(e+O(u*EW1 zgo5DqN~(Y+2m~`?c&GqQ{_o?!Vs@#_RA&(9SEpn!M%ab_5c z29yHk05S3?2n7rV(d*;#xkoZwk^xiERpN!)4V9H=l4Tn>l)s)UWPUlhW+IW-e}Nd?fsk@0y>16?UrrsO!Hfv0>DAFyj~ z4;Oe+Dtg6e1A$uu6@_fEHn~Tu=r`1LiAGjPWDh&kDWabnW};Hzp#zbs1*P^&lDQn3 zhr`|MiT6!+C&i|>I%eo`0eSDaQI)&vJ`Nd~4)V@wtQn-jj?ItB_B_)Xwk^s@lernM zO6lqvz0uD;>}Ldz03>%+rJfw%-gl1kJbl2iG7Yh2s)~NTOz*9{732Q!JpEGs6V6Or z?v@;35->T4fIz_@a8cZQAa0bgx9TTIG8{VR`$aOlLZ$GqH0yMO z@Y6xHgC39jse&KLbn`{{Q=Dt?sE>A+&5;&KL6?ewqvkimo8(4^)y-6@f|g&srG3>K zOR=S%Q-k-M?40&}c8i-mnBH1sjS;pxgNV#%ZPwef`YO#$*D?j$PZ$l zv{pW;Ex{36V{U;rt7KE%$r;XTZ-@9eYiWqK{GxeG!gsw=N<4qP;JB$*gcAmX#ZTkm zq-$D~{KvLfP0R=BdS#-bj(0KEcF*w_t(~hvN!;6+PE{lAeAu>?ljx1Nff@jeHBiRi z;8P7Z7*;-^Yp6&xT-dBKQxS9QF4n?^yDy1p2?1 z{23BJ<8P2)b|7Jd|FHo6E0OvuRKyY-<<1OEd3a0BM&Rq!?9JVYy#B1+Qd!I5#H&na zk30jH2V789$$yAG~m0RXuJD{3u#WSeCxe~2|f62_g>oeC8H2- ze_UH=t}fpaj}759iKB>E5tqFUk`YL%VC@Mru%ad`VPXugO{!w0WjwP{qm4bia|eAw z%*&+wPLE(jG-lJ5O7spY(>%Nwf zJoPod6ROYp-O|9-fNjCN;-j5sdwH&MMsRxO-8N?0U+SP@S1s;CxMp2EWy){1+$1-p z-DsI!@Z4bXQA?=y)QQ#3-$e2N-2dzmme zbbuEP3)|)lIMxspuEbuJf&jNE&eccAgGAH(;O4$ri-*b+2mD#*+5{X{T+%#m+hwnw zkf-(~ntl3qGJvy}XGYaA_O+L)P&_p4@%st9PcpPWv(828(5o1EE|+dIyIAoDXB5Rx z2VB*Q-H5fTW<9l){gQ8BNW*?6U`6S{ffb#a&3)ZV>)Im)?txTeS&1&gRUu8nNMOgv z+(FN+c$pG?jSUeB*?EX{HtNS#Gz{^Sc$kU#;o^jQwe_IHf@kKow7!c`4=eK)3_^Qx ze7?GX&f}#QwMYDVJliV?LGc3@|E>ar@q@9&P+{tq3gEv?*MFl5|8b7}D-UjPy+2V- zR(n5asG`m*?|e`x$VJQF^3>hqpOW4P>L+*@OdT!pG`Rd)sD7&MgFN|kU=M2@8(Y6w ztRiYPqHa?A_K*Cd0VS>?dqH_B%{`uuhc64=N%<(+p1=57=C#>Iv?CgYyQ>hI6)OpE zamrO&M9IHI$%MboyIg}=uimEGEuTd;oGskcyAn}lHDB0@@N+%$RGxMe6Cr4funqQ$ z(@D`B1+Ln_7dr$zQ;nCclkF8IVs{r0xqV`xp_b29-!1B1zY$=RRk4Wz8NU~@x|U=) z^7_KegkHLFMM4Z#0{ z2OPh900*E95B~5Q@=w3@{|^RoP6f5c3mqhA=})^K1scsL<<#R9%F6QEx{2Rb`ky~z zrY&|YhQ9HW!AbMWFK9)w`=or^w-wr%%ogmWARu$h2!fZ<*ZkVuCOohaSsN5nyOO&>*~TNv9yR<|${+cysDRR3}7H>+p?a9c8v%bU%E#C4CGWvaeW$ oIl4xa=rqgDw#QA?#n3);-E>Npf^^i;kSQ<9A)UYFuMDdGAEFEd8UO$Q literal 0 HcmV?d00001 diff --git a/src/test/resources/ldap.ldif b/src/test/resources/ldap.ldif new file mode 100755 index 0000000000..5127d0a7fc --- /dev/null +++ b/src/test/resources/ldap.ldif @@ -0,0 +1,58 @@ +dn: ou=people,o=TEST +objectclass: organizationalUnit +objectclass: top +ou: people + +dn: ou=groups,o=TEST +objectclass: organizationalUnit +objectclass: top +ou: groups + + +dn: cn=Michael Jackson,ou=people,o=TEST +objectclass: inetOrgPerson +cn: Michael Jackson +sn: jackson +uid: jacksonm +userpassword: secret +mail: jacksonm@example.com +description: cn=dummyempty,ou=groups,o=TEST +ou: Human Resources + +dn: cn=Captain Spock,ou=people,o=TEST +objectclass: inetOrgPerson +cn: Captain Spock +sn: spock +uid: spock +userpassword: spocksecret +mail: spock@example.com +description: vulcan +ou: Human Resources + +dn: cn=ceo,ou=groups,o=TEST +objectClass: groupOfUniqueNames +cn: ceo +uniqueMember: cn=Michael Jackson,ou=people,o=TEST +uniqueMember: cn=Captain Spock,ou=people,o=TEST +uniqueMember: cn=hnelson,ou=people,o=TEST + +dn: cn=role2,ou=groups,o=TEST +objectClass: groupOfUniqueNames +cn: role2 +uniqueMember: cn=Michael Jackson,ou=people,o=TEST +uniqueMember: cn=nested1,ou=groups,o=TEST + +dn: cn=nested1,ou=groups,o=TEST +objectClass: groupOfUniqueNames +cn: nested1 +uniqueMember: cn=nested2,ou=groups,o=TEST + +dn: cn=nested2,ou=groups,o=TEST +objectClass: groupOfUniqueNames +cn: nested2 +uniqueMember: cn=Captain Spock,ou=people,o=TEST + +dn: cn=dummyempty,ou=groups,o=TEST +objectClass: groupOfUniqueNames +cn: dummyempty +uniqueMember: cn=krbtgt,ou=people,o=TEST diff --git a/src/test/resources/log4j2-test.properties b/src/test/resources/log4j2-test.properties new file mode 100644 index 0000000000..ee4d10ad01 --- /dev/null +++ b/src/test/resources/log4j2-test.properties @@ -0,0 +1,32 @@ +status = info +appenders = console, file + +appender.console.type = Console +appender.console.name = console +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = [%d{ISO8601}][%-5p][%c] %marker%m%n + +appender.file.type = File +appender.file.name = LOGFILE +appender.file.fileName=unittest.log +appender.file.layout.type=PatternLayout +appender.file.layout.pattern=[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n + + +rootLogger.level = warn +rootLogger.appenderRef.console.ref = console +rootLogger.appenderRef.file.ref = LOGFILE + +#logger.resolver.name = com.amazon.opendistroforelasticsearch.security.resolver +#logger.resolver.level = trace + +#logger.pe.name = com.amazon.opendistroforelasticsearch.security.configuration.PrivilegesEvaluator +#logger.pe.level = trace + +logger.zen.name = org.elasticsearch.discovery +logger.zen.level = off + +logger.ncs.name = org.elasticsearch.cluster.NodeConnectionsService +logger.ncs.level = off +logger.ssl.name = com.amazon.opendistroforelasticsearch.security.ssl.transport.OpenDistroSecuritySSLNettyTransport +logger.ssl.level = off \ No newline at end of file diff --git a/src/test/resources/mapping1.json b/src/test/resources/mapping1.json new file mode 100644 index 0000000000..7d2e7ee7e9 --- /dev/null +++ b/src/test/resources/mapping1.json @@ -0,0 +1,76 @@ +{ + "dynamic": "strict", + "properties":{ + "title":{ + "type":"text", + "term_vector":"with_positions_offsets", + "store":true + }, + "name":{ + "type":"text", + "term_vector":"with_positions_offsets" + }, + "age":{ + "type":"integer" + }, + "created":{ + "type":"date", + "format":"strict_date_optional_time||epoch_millis" + }, + "session_data":{ + "enabled":false + }, + "manager":{ + "properties":{ + "age":{ + "type":"integer", + "store":true + }, + "name":{ + "type":"text", + "store":true + }, + "inner":{ + "properties":{ + "a":{ + "type":"integer", + "store":true + }, + "b":{ + "type":"text", + "store":true + } + } + } + } + }, + "employees":{ + "type":"nested", + "properties":{ + "age":{ + "type":"integer" + }, + "name":{ + "type":"text", + "store": true + } + } + }, + "city":{ + "type":"text", + "fields":{ + "raw":{ + "type":"keyword" + }, + "stored":{ + "type":"keyword", + "store":true + }, + "disa":{ + "type":"text", + "term_vector":"with_positions_offsets" + } + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/mapping2.json b/src/test/resources/mapping2.json new file mode 100644 index 0000000000..0217f98b2c --- /dev/null +++ b/src/test/resources/mapping2.json @@ -0,0 +1,79 @@ +{ + "_source":{ + "enabled":false + }, + "dynamic": "strict", + "properties":{ + "title":{ + "type":"text", + "term_vector":"with_positions_offsets", + "store":true + }, + "name":{ + "type":"text", + "term_vector":"with_positions_offsets" + }, + "age":{ + "type":"integer" + }, + "created":{ + "type":"date", + "format":"strict_date_optional_time||epoch_millis" + }, + "session_data":{ + "enabled":false + }, + "manager":{ + "properties":{ + "age":{ + "type":"integer", + "store":true + }, + "name":{ + "type":"text", + "store":true + }, + "inner":{ + "properties":{ + "a":{ + "type":"integer", + "store":true + }, + "b":{ + "type":"text", + "store":true + } + } + } + } + }, + "employees":{ + "type":"nested", + "properties":{ + "age":{ + "type":"integer" + }, + "name":{ + "type":"text", + "store": true + } + } + }, + "city":{ + "type":"text", + "fields":{ + "raw":{ + "type":"keyword" + }, + "stored":{ + "type":"keyword", + "store":true + }, + "disa":{ + "type":"text", + "term_vector":"with_positions_offsets" + } + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/mapping3.json b/src/test/resources/mapping3.json new file mode 100644 index 0000000000..d17f29b91d --- /dev/null +++ b/src/test/resources/mapping3.json @@ -0,0 +1,16 @@ +{ + "dynamic":"strict", + "properties":{ + "joinfield":{ + "type":"join", + "relations":{ + "question":"answer" + } + }, + "text":{ + "type":"text", + "term_vector":"with_positions_offsets", + "store":true + } + } +} \ No newline at end of file diff --git a/src/test/resources/mapping4.json b/src/test/resources/mapping4.json new file mode 100644 index 0000000000..5d79599d80 --- /dev/null +++ b/src/test/resources/mapping4.json @@ -0,0 +1,19 @@ +{ + "_source":{ + "enabled":false + }, + "dynamic":"strict", + "properties":{ + "joinfield":{ + "type":"join", + "relations":{ + "question":"answer" + } + }, + "text":{ + "type":"text", + "term_vector":"with_positions_offsets", + "store":true + } + } +} \ No newline at end of file diff --git a/src/test/resources/node-0-keystore.jks b/src/test/resources/node-0-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..5693b7bf88ae5acc096a179fe2d4c9832dabd71a GIT binary patch literal 4593 zcmdUxXH-+$w#QR80YYd72pp=SNDVvm-m4<0^j-s@B?zI3(xeFpB8n76suWR(QY;ju zhzckmMZiMuh=L*@M=3Ha7xqfr*EbS~oAP^X}L4Py^ zk>rM#0IAXytj7&NAT&@g8rTMz5J)iu6o87tSfNlF2oMd-_0m;uVszPZHlFdlsJy{9 zL%fbt;HkbjHMh4O+w0k#4->j*hiOlN`0`b%=KNS4rP+7uBBcFz3W2Q z)g-ni){9hfC5eWepNd1R3(qAC=(QVVWt3)_3af6&Fa&;};0uilDHF-&HVaB=Qh}>2 z=ggVgam<{J18>(}FBv;rvfPljMDtfm8iQSw;w7y9!#n7-)of@?>67#{mgz?;N{cJZ z-#@NyrmDIhmyuJyR7a;b!K@c(_kC6*N`BT`=8-{w5@Pi-MrpT#=h*bcY=5<#T>X`5 z+{ruA7H$F&WnLADB4m*}G!NcIQ@oBXPd!8yc_lQR>?+AZgg0Gi^J7ft)4JO++vP<6 z0u|aC;TZp5sQGHpM0nyynL)#wTTG8`&zpR9@{N(_Reu(@&}cARaK8X7OiZ=#*U?KK z7m@5ER|@xcms=1T1V05IPPVgNtmUHD$&J<#OHl2CD+Fmhz zYiU8(-2k9`%;=`6OfhD@A9*+8jFpjF+R|7RN23`qF#v{5WtFAqh1??yh)x83bmV67 zY+=Y04Rmf+ZzWraRrJd7b;SOr;&L=7eO1t6%cqzg4Kbsuk$e`mp#5<7>tdLlWpay0 zcU%v;AE&>QQ$m}nHmkia=9&ZE?)spU^-00vSsc#6kMd&dfUpdE$%FWWF}HJV_XGKs zxEKzt2`i?IB6(QEcn|xhO`f!>+j`Ksf|)I!xBwvz9xpO(QSsA5^UkMCC=~jYva~mZmauEu<0rapM*G&ItkI| zX4n=+s;WMpTB76dmA|1aR0pw$*fLnZU}7VanWN6E?!tA=Do=Sat0(^DOTe)!e7iF? zPs31DZ!n^0l0Q*lci^LOYTM*%xlW)lTVtXxTh58QaR-ld95SFMuD>3N1G)QOR))r9 z6)QSp@2m{Z_1dFTqFDOyUGmmt%KdXV6U17RMC?PD$ZH0yP!1;Gf?2Y|x6ZzlPEJZg zjnM5j*Ph+UTD$R@$Spe2iYFd88vk{_V+RS+27u)Q!H!FXMxJG1k!!8^V zc06%=(12ZSHAwDwdbLIO;^KP<*9|FM@8bICZmE?=Vfb9R&NsAA0_Vx3@QTPFO$Az-vS>F|l_&usPd9Eye zCd&}`>q3I3W*+5mD*BK(?ABhXU}sGVE#x`w3oZ$@ZxuT*8@_#=bjeD2jKj`WlK6H* zNkoll!x^j>XqG0h`DpFw3L;9*+dDK()mpB-NW&@EnbhughHpT zwe<1SFaEZA%Z6Y_;H(>GR<#6YnrLB~iWPh}C}y1Ba@P;IbR@0-%E$0h!PR#sYTtJq zjXQyW?R*SrUf#DMCAmOhhLTWgyIpl%wY?p(_({T*ywKE-4DPB57trNhLQP5@2fgi91twGB?^E}hI69h8ZFF;9B(%(54 z>)}Zz1iAzU;Qd`mL^7U8-n&~#cOR0!vqvD2xF-UMo@6pLRwSGQ0`TMjNnhsxGTvX( zA0I^W#=Bu%{e3{$y*XF`EGQoR7oieO??ZBR_92i0$nih~wFXI`5rqRZG=B^q>IYD;ht>KCAMp1EfiS2nmB`#FmqUx=Qh}7oD)2;r%m{8jF zHrDRLDL4DR!hOKjgiQz_#H~$t8w$NS@p)G)Vq7`!{xE=`lLb@WVLn7iC!unV9F4r0 zGwr#iG9sjn3t?;No?b7`W>T*Te%Kp!VlIgv!&Maduyx;l)_b2Oj(-_7G)1X$Rnotz zjhnHWZ$J8i(!W0V)FJO$fTP+0FH>`-i;W?STTRy2r$!6nD%dr+@3{#y+J_T5xaYFX z{{bWhDv)TwXxQ>EAi@50Twqk{{~eI_|A-?hkU0MZNGC}o@*a(TxxoLu%b!rB@H>iR zexOLf=D!x9{|RON6;ix0Hlp!E)9zmT7ebxy%UKiS5TOcfqY-z`+8B$JMd`Pe&4er zGTaIaDXnw44o6F_cGS%vo6J&&p5>}%FK9{d`INnHx;S}G1n7%mrW|;rj1`L=ikW;0PkS?{BvY_yJ<8PQ2PaAR^`O+Xdtx|7%F|SK& zs;wzVW!iqN<0s18LAQU#7!(2S+k5!vJ*?4z24!+P7|L+-}bwgz&&+SyKz6gJ(_8ONWe&kg9$f6oy=bUhf zrotw7G}&PYwGg(w6AsWm_b@@wHLpy%QB2?Pe=0kkImT{&PClh8|DXblTB7VwsWAO-%as3rRQRu3@qgmMP1Z5{a2wItgT=%DIC&b8SPxYc|85#SJL`opnLTjf~s28 z$*h*ypzOw4d^u15;C#WB#`VxUCzcBy)B3u^KNrhCDHF=6Pir3F8>O10_#VtPqgiRk z5R&!3m?m0@PevXr9&(*$$VYxTPq|mvvvJc;=W@js5~4rGb>c>X@yKA>?4-s;{fd%4 zf$@hn##nFXlKQtEgwM3{l{L~1w$igX$7*8ot8?p1`LF!sfh*|zFFauWI}d0;IF$!~ zs9yf^t^aReAZnXeV^?6!pMUXrPunS-S!~vQXYsPKoR%JsZ(sYnUeM*Qbgl%w^%d3= zD%F*ppHbw08ep@u%!;nSy)r?Q%}op1DmIdIcz?-4JFRp{j`;C;6hYc zGIruPmTV=^uKPVoCU({AsiwPqm}$&BDs~Vk(_?q2vA?YyyVmbM(XB;6E+BP4?V|&Y zz;NO*@#xO==LHFyf@yWbvCF9g5qAs4H?8#piY^}Wx&1XC+1b^lL8g1W{vm1GC&r>y zW!W5PmKkbL%?ql2&XP6Wm^>NQ0h3TRxtXXc$#|g7*SXxdZWJ1LxLArVyqd?uc7ciU fkgKedwpBVyQD_3{J;vvymZy^>PpdUX45j%uEA}fN literal 0 HcmV?d00001 diff --git a/src/test/resources/node-1-keystore.jks b/src/test/resources/node-1-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..5df582180efd6363448ec4b2906ec170317b3784 GIT binary patch literal 4595 zcmdUxcT`hb_Q%r*7((wNMWsFJ4MloyN>k}Y5QG4s1xV;f2?P)nMLH-D5fB9_Qk0@n zq)1T^0*E3ay@&$RRG6T?nfGSi{AR7+`u#D#weDH>e$U=_owoO9pS_j66%Ysnp)~N1 z2KT|bpe0Zg=?pX}gn>X*P=Em22k7AlF*q0o7KJc@!Bij^0k$;3UbWPnzVR$5+(}*X z%Cub{x>dnI$4N-MT!W*SjJM?~cC*ahE&KGbIXQi6c1j9`yluOY_O9v6e;&+KEPF-Jf2#w=d4ogc|6)VUcsUTtH4kf@}RAQV~W%EW1t15aL%)m=jzbcE@2M@ll~dyvr`FKeABxftZ zvO??~>HH!Ma&OPded&;72?-OUy#PCNIzjQ_=%fjGx=Bj+-9=_(SFX@>Tz}eZxLHAo zo)hfL9Bp#J#SO!!{ic!qYDXdu$DVTEiO(e1m+6;+1>MF%Gs4M7WCn_iE7=U9=JvGK zMi_`^vnP2zi9`TNUtjf4J)hF=7KHF3sgSJB;B)MP+cM+(mX}t|&l#mT3HGR&+)88g zw~|dc8Ndx+-od;@Y*>izAW0&O5tc^Vk=x2@MwMqmtoGqOP}S8~gX@Szh8*{ccV4_Y z*0jNu_O3TpI`m4`U1!m?0vBZrZYE|UCpW07>y$4(J7>)UDtVgCCv=2yb$**G^6Rc! zQIK^S|BKefK>CHxNMwUT^$7Ox$wCw{`qA#5s|v_YqUOlWQ!JU2&N&-lhU&RHI1SY& z!?UJ(Cc4Q8zAPEvdnU$+svQ2z7K{Ck->QwIIIVA}Hzz*)T7I8vvZxtyP9_}3dOqJf zec__}C-%uPNV)imYx7M-1Pk_hO@FWThfd|S26fBNK8zomWRo;wyDdB!W~0e3f*DbS zXP(JiPr%Jpj|Im4uHWd+eCnjB??f+57!~YQsvh62+RpMAyq+bEbD2lG^b|umsj2!_Jlw|D>*{; zY4KCuIN|m&ZW3`lcdbs%sOR>k?S8odQ{z`=&8JBI_YaM3pMG-Hs-oBICwZosu&#HJEIPu)~blvMpWUVX1xl$YMshs{9mZr@o zMfT?tpRssaJ-1FblUHJ|;8?@#2OmOCX7%|>mwqD=!sqB$7desQFYC|anu~+qh8)hg zp=r505n)W!)GJP7Bud zX-jF&Z;cPVu4OjgCC9&|x)y`7WBxquo{|Kz5wUqv?SW8oLBsP?+I37119L-rY@+iFXbjWH=%moe z(q!#$oqYF+@-JnhrkQRl?WYZU8m%1L(GtS`n@y32Q>YF1#IH2s+#6Jl0>8c<>P&&@ zTVQirS9D;k)^wVJ%DX044&y1YN*ock7iWK!2n}eFanb(hZLYU88I?9wJ@y^*EtI;* zuYBiBEkhi!eg|RU@2nS z<>G=1vj7E+P!N^1q#U3K0>R=aL?i$Ns7C=50)|i?fDV30H11^F79AK&O9cX=(@`t{ z6C82qy3nQ6HE|a(7dzA$>y6?7*#6W>jShD5_Vq%exBw0~)B=V;GV1>9;n&7`^Plo@ zmP82v#|})iNbY|&@ta}Xd@w$4{MwqodqMw4FMyYW4y6Fd0dgo=8F>`SmV-|EKuZ6T zfF+6+ICL-?oCTtzi=qQ)4+J#}%p8rw|1J@~BoaXUm>3Jx%*+VI4R9SOC=2t?0wVcq zX_Az60|fA~KW-7Fen1Ie1`wqm0U?0Fpyl4quZ{82T;yVSaEaWqb>OGB-nR(Uu)|zO zh8DjmKI_2f5nc_WKBCo4EcVn6w$x*Ur;Cw0U3Z9@*IYK6`+P8M+2S zQxYR$roA(I4>RTsuDr5ZTqb4-31BYDj<8WmE!2XC0Sw-U!JrVTf*&Ws z!vcc%Sm893a!f@<4F&)G3-Z%U4`YY1>Y}d>ey0)>S>Nhik$9}b@o1s8 z0N^|L`#6jfV2@%=3z|wr(W=+Y^~@xD=-h7q0Be~9E*v=U!R!DVz^cHaz%1^pJ09VIfH>hVLWOY13+wFU<&MSS<6)7MTSx+xC=^CT z^~dm`WEdrUz@YXkktG4^pX%cu9co4iGKmVMi}0py6*hy}tJ{^N<;MgE_*Ngw4GZ9S zANJ-cSX`A>l4rHJfhlKY&C_0TMMpfUN#oAYJ{x1JcnSaYO+U=f42S6pO_l(CC*7^xwPu8AX7? z?)#K<=h||Lp)`l)_tXSm0(X7je%(Of(I?T2}K6Ff2Xl zeQ{itTVW}=eId`muq3mmeh%@({OVX&o_fxbmISX?>BN(3(=ornhN2m&jx{PviV;WH z?GQs|@7BqAzWbcHMW#KMUa{mchA_J4R_eoL?$}6MR`GhG^UaE8Pq7guvG?`p0 z=+l~c_9Rec)_$|+C(2v_mp@|+3SS|2J6!0?Gzw;*f&?zM(8JtninduqF_P$ zjJ{Tn{DURwOOvRf6;FcSw}}VY!D@Z*bJ$g zQK|fX!_>Fr0=Ez-SNidwyY1;LuIi+PBkecVMnpyob|Y+22)zg5foW0vR8Q=3POKrs zULZt*M{=`jWOu6eiIy9e4n4Yb>zl^Sko#v>Z#|{C8)JKXei=EzNBkaNGJa-1nS@wAQ;8 z_{LjA?>M{IXXQ|O&!jEky}+JCIvhrvL&Qo4;v}M}_-UZ+FnKq$h~4&`%UI%E^`~#C zColM@te7~m8(+5O@6`PoElXTC@6dF$4>OBhL=s0}rFv`*wf5!O#LZ#X4==T<5K9Oh zK--Y~81_2RSe($i-F+)z_jpSEYvSrva%6R(_^ypU?)EifuktS>L~mc82A<~W_PeC- zUa^*SDytT-R@t#;)q=p9ZpQ4X$CszWdLRMIs@xW-H?x)$_j3^SsaZy~pt#-yiSy$9>G4*LBWu&wbtJ^*b-xJZ&BX0zv2( z_{YWQLvY2(D$qk0p#Io55QqT`P+;2tJ0l#)2!?^BA$!4K1`vz_oAIj}Ky`}fZ{+g1 z#+ym<$Jl{q-g!D~g~i$dI_UA2l7c497TNEad~hb{(b)biRrB{*^;sV@mwLs3Lu0N9 z!*0$cAzUwK)Nh|J+u@2@=reoyZg99tkZsu4BFfF!PkUKR{o*QiDIwr~CEmNx)dK*} z?}ta592Zo4G%%@T;5zzf-g^1;Lpaz&?$&%l_lbZTO+qzZH(Po_T{-=ei4~lld;~y*8t2U#lN{ox&Z} zjHEJ2@iIFhzhAd;TyQ>Q>P~$^<+Da2Gb0u;@L+t6KIuyPCDTgOP`)wg2z`=*^n8v1~Jn~(;5eSlK3 z40bsYi}1@o3!9B^(<9@2P>4zCBIn28sCBr4TSFevhvQm$@(6;x5&Ld7&zec-TQ-)DP4aY<3 zFF1y<#9~H|Ze^)%XtCY{x&UQa2zc}Ej9&F%m%`gDw&n90XF4M#FdIdyUODbD_k6#a zu4AT@vD4Z@VIN%x6zclkBblyuRHe^z)rL(u69QYM?K;D7cpSSS{S7S_z4m4g zk-ptl_{_|9-D!l7m^U2mKM)o4C_P~pX2R0f9=95JX z7gLIy-4ZXHDhMh*&e7p{i=WC7IJPi9)1)~s<5+l$GmmG#+?JkhJ&;IgHU46MY;Gn4 zE^4HQoh*4-FPn4E|7i(tp!E4=^5>35r&7D=2xSWS@o&{V9P41z_5r=R%YpcAt6H`_ zZv$iy&@Z_hoplYVZEm!@#l}OQ7o&0@)N0PQgs3}*Bg|ALQnIq5yUoIQnWd&r2e>`! z8us_?xUZ#j@NR3yk;UMh6sflYb@L~(3aRD1eQ@IQve4|vqznX97P~XjI(p@bgzHl- znTW7S&KFwt(7H#-dhofC&Ez}W;vyo3PTF~GCYAc+GUw~YaV*R?!^{B{=7|g~(Zyg7 zCbLg11c9U)#3sX}LkG*2LBX~wwLN4I>(%^BIx7#mNWk8_wDI)SOecucC?Nlx(s}me zKG;J{&gu}e=gX+;--Q!!IX4^plE)gWLVPx8vKxo%n$aQ_&};;14sC9ERXiK|qWq3Q zg$c(DHyuVOh{0M84XA=Zuo$|B6o3Lfmk))2ArLSWU}I#FjyWE)&IShWVE_RU87MAb zFC(1gvP68^vWzPj!3%XEc%%3Lo<9vT;ewpJeZ6of1i;4#wS>W0IrV>zi0Bf$MNas* z$f3jm(cP&%tO9?X5;6C1^YQR;6VcWFJq!9jW&uKcY$z1~4WLm7&?yJ-Y!D7t8`jB>f)3e}$|7{Kp^Tf|{G3LJ0tf-3ZFH?`Qjv z`}JsY^ydaBV9`G=5xsjrDPRr|eH;#+n|GH;QRI#euqool zOK(UlHIu1!P=UlZ&u?2i!J}S;jo79mAy6;~TtopQ=qtiQZ?sTLK@Ko@2L^*e81jF- z5kW2(1ONeoyHkf?yZ|>NGrbuzFfc*E|M&*^In55^g>h3B3VVB4`sO%smv8kp`uACM ziwXk5yT2BL@dLc!-06WI(olOet3P#oOgg7`QNN^zJ?O!sGxM%HNam6W&;lk24=1U(`b2!aO)f+efEcr4Gm#Ej{3W(bDcPC* zB7PFq0z>0v#jgz+MxQ7N+!WZqB--QdxY`*Jl$+sDnq1_=tfAP#pk`I}s70%{<;x4G ze8$}5$t=Oz1{xw%FXE3MxwF*N<#hW$?R+U1YxIFXXsS~^>SD#2v zsn?sNTaaQ!o5U`Aly?#AcZWsMnNq<=ubzdZW^*57*cN3iT# zA5|E9*;&(>vY&jlZv%7c9KiFJGXJ3h{|LSy1`j4jOKHMjFZG35#Rtw|pHx1)XqY5z zQFN3W1qC30eV+IJUbIp7X4!)ePxu(&!;e#G%L?kq+Wqi`fGXdJ4R_%E3RCgG1uVc?it8k&FSXrS^ z-(0Qmt#GSK)(~3eE1rQN73*o=uX0UCziL!{KispjsXmhL>KAP&CETgIhETT>;$b+-9ac}vxqwqRYW~J^rS%Zk$sp<(NDb%l*wVI8s|75r< zd@Z>8qk6@5j+t+%vw#HXnnFXbyRB&!;y!ie!1J8tLCL|BTNiCnaDxXj0qNl)42^c# zN0#BpS8&Oo!E0GnN}H8C(N?SRERW&~ziV9&zI$e&@F|nGb8H*3z)UHa-;l|Y=pCU! zRUHL#Ef|(t`P>r?SIrZwWhTSLi-%lh_Y}Za<0~H&^=y{->ZRZQ4hI>IBhKW-nT!mk zPETs37~U@J6ZueQYa-|AR9gSm&3%bU`QAfCdppJHYsL?w3aWDJZwhDp^uPsh`WFv4 z{^0=w0Hu5Ihttd7?)Co-2GVx<56>3b2p6QZ^}O)Yo0hv;>m*ZFcCER`ZT)M1S37gT za_4fuTW?7NF<#`7+IeiS_?Kt}=_(L;t$5~BxklGIjR>}lRVuywwe*LKWxDB;un z#qVjyoqg2jO&ocR6RkyB^}j_ZMXy-2XydUL%%f&mqX%JS20Zo;v3GQ%*ZT1j-8z-< z&u~3J*Jz*tHk@EAL+RXTD~#I`ORX7>UPvAYyI&-;Wn)OZkz(w1=W7AHv#U#s#Qbz) zjJoX=WmT=dU@}SZew+w9cr)WN>sQDCY4mS+A`R=pvMsMwaH;?gp|X_8Q;8I+P>SXz zg=b6TzKh~)@e?YsAhJMt+WK-AWw*bZ`OD$E3jyKxSkKz6Np(ZRxFS;JTm9B}P$vMX zO%icZ&V#~h7MV35bUzWChH%NRi*Bb3khZ7JbDZi|v3=Es;S9Wg=BYshjc*u1w(s^FTOg)fz_?2wT%}4A?_OR+v z!}M-W6v;5Qr3GpSIMTg zp=p*V(^K~3X`R^$^ zl@0h#{GrrZzSz4o;^x;(j$oSQli05|5`gGZfS&YqDRFZ=;fTf;pu6>$Gc_B5@InaZ zSnzv#jV=)OT91F;+sw=#8Xi2bcYIosS8+krEW+`cz8SA~OB_Sos@hk} ztX&sc@HXvg#)qPPVaES9n$w+ua@Nj$+7=3mL_#IX{LtyuE^uq@-KIy-b@1Y4|DjoJ zdxIRJYLmJU@um-z#J~Ij9gR#F&9Z>;mIs6yG#H?iVlvhx2B*<(mo$%NDI*L6=gU@& z>YbF2-}yhiKZG0*l5L=*rtM1eBDOqtfE|s=8=oNRDXwy0_oPx~VViJ7cr@-mr_&|9 zODkYuW7;juJ3KJ_De<$l;4t>VlWE7CG~)l|{1!iDi{~GA{K+jrlWgc~`^QiXk`(@y z@UT`aw7GdFz5$j^^!wTNfcR`y~R%Qo&yU))=^>*)V zrEz-My6m>9y_d!`e8mY+KE|A^@?>6yYOErI{lW$c5N@A!@F(Abh?-gGgjqG1>;<$f zfKpB0)&{4j>Jvr>f@hGFgtfNTLM|_!{uuu*2Kdof9$y!5U)5VEO(1h=%XCYo0(-nk zxtL2hEDe^CpT%#MM4Kg>4gU}IWWCR74vI)kt2$As?+Kn5Hi;YAfpz5R(CX{6A8u6^&BC;% zweeKZgSBB7xc6=8c0l-B<%v@4uWt#Nxj$WnuK)BHpx8Y&#anhKs$+x8H8MQp7*3Qq zjl;*Lw6H|_O{k^Jeoj}?5bwoG?wRy_4Xh#s5V)Me!TBSb<1G|1)(hP=N{ z!M@g_AO&2dwJFS+ZK<59F7Bsgrun*~cL_B7^D5qg=9C)1IHhXDa|3@DZmCyGq>cSp zDdKk%(@w>iZwFv~@<8fD=^mX&0HG@Z-QOMFAu9GJRdq%6iQi^doJ5*Hq50>OR6Hz0 z*^2)X!2Go7FEc)XuPH_)^7hy1&1duIm1^lZ<1qkd&sD;k*T1o|ZgKiZ@;rr9kY{q` zGJUW~9JbfsBja8zEl98sIcxtO+zQVkF4y=xw(W7#Mr)@%9ki)18jOVNYCZ+4>kwEr zV&Foo@zel($I=mHiz}Dcx~+t{$zicG`DDYvwPdi`Pl+}((K9osDlc4aX2sZBM*0Rk zD>w!2SQ2}35>@Tx$^KZSE*dgYUt!)Ovdev67K8jtQY%Y3qLn^OUW)!$mpozgYG(3;^aA5<9P7q%McgZ6aAP z*6{?FMv;v)sa%&p!7e_m>=261USbZqG%ldZM*>8CHDJv{}3;LT6p z4~A_AqPXqws_`qS^iXhGbKYjr?|RkGtZV*>^=lP0#{@EqHbJPuJ6~c&qdGV;sWX@c z#|Dw*JJ3{L{fB9`{a7RuImoL4PK}52%8O8y0@t;;tCu3bkAtWa6W(}buU)` zEo+ojflLD+rRhzlOf~l|%ZmPw-K6^PPb7!tUtvh58|s9YEVe@21G!KJ79tpD-q_$} zpA^-4)KyR-7G!ETfHqt;$*Z>8C|gV8cwuZxKW1aUN5Y2)bGdtox%Omy#zZUjDoSBuC9t8kFfrvEm2DYm0a_oW)EYhY4+< z>sd^nXYp5Sq4?sKea$&FvPu4UciG5dT}M$v1MRT&#YKJn7k<&7l9n4;#>fJVlZK!6&LNQUF~p?-`_-%j<47)^a3EM+C0l@EX~)SY7lyvs6GH_vP4ntZpI&@|*qayq z%xKEefE^tW!qig`5Kve}mJL(ZT_O@?;E~x3x~?`(9&g2GyCmL- z?`S{uzfVmi^Q0|+QNLYC-qyimMr%pNPA9m(MlGJ#rY!KymL>+U#rqU| z;@Q7VmVcHvg;)sIUZjdw_4;2)5j2Vg7Ve^oo)4aUb{Uu`2-IF16nvHJknw$p(B->W z!BF=@7IKMYX7?96I{91dvYDVAZY~#YrvhS@|6H|vUKZnUn$g{?Q9IA9E^+DR*?rQz zZNQqXb3fcRr8wRn`s+*6J-&6H1|evR&Q!MdSjPw?De^C6XN_Ss97c5Q0blz$Z*^6G zRDL@#0f{g-+$2)dM?Eh4D?x8a|6F+MK6-ap;wmGiAU^1G3%Z8uQskI~R zEI5~+2Z@?xCC)Et)LqtO0>BDb#LxH3=1CR?-!;b9Y=wqwg zeCrV`+_9lH9q)!P&dNvK9^bh}CHg2dUgdCN5;Ln^Hnktlm1R`lzTuvRP_)Kd%!MAI ziFXYTs{doid$5OX+(r*JImO=_`!JL9h=5a(M5BsGcy6nAO4Miwy!)g&4;d5>_K32| z7v(DkdNMvkPec3xv|u98zzKnH;Ke5!#?wL^I|xF<69-HWxeOV505R~r zkm@Biw#^ct9_dA8y=4u4K-*^35=3A=(OEdKg^8ya=`K!X6fSW$CjR(JGWW9JP(?A? zk0mU51^-BWK}uk?Zof9DYlb19z+UI3P9(2sD8}m5t&BDa=w;R~A{%n%44Nj<*cQ2a zs)?+mLqX4FA7<>Qt z5b%?Pmwr^6f0s;sCJDN?` literal 0 HcmV?d00001 diff --git a/src/test/resources/node-untspec6-keystore.p12 b/src/test/resources/node-untspec6-keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..fd654f4573baebd7c17a05e01f7ca0d61a2165a7 GIT binary patch literal 3861 zcmV+w59;tRf)5b_0Ru3C4%Y?=Duzgg_YDCD0ic2o$OM88#4v&mz%YUdcLoV6hDe6@ z4FLxRpn?f#FoFqW0s#Opf(cy)2`Yw2hW8Bt2LUh~1_~;MNQU-^I zm&N}|0s;sCfPx7y0xgZb7g}`K-Nfef$f7t*jqlE7q-Yq7*P`Yz8!FShpKKX>oYIf9 zg)y#hGR%=^K0oU}!>yV0d)ig;7lJ)2Z;k7|F5I%K>n~LAWLd&)u$S{0y8JWb0 z!?3t823f}(;7gP9pF%>7(u=|49-@|UfnU5zy&};|kfLT&Y-WR$bwI@_4qQsJ_ttfn>?0vN4{Q!m z6Wx6gfYLpY!;~P%B#rSgeVSnTSbAF`zh#kxLM~6Ll-@_gCQ(%{Xvvk}*V89!Gd-G&VNi4VWz<0%+sxlD_Xq1BQk?WWv zS)hu{s6#U~jqu}o zMtTAPKIRBMV9&oER6qM=iUZWI?&5Guu>~ycK(e#ef^6v8`&F{pxss=2OJ(fU?B#-R zZq`f0V26sl6p_MR?hQ~`_QX*<2zY-_>TUJIr>s95vD`{He?M5*dAVAWm-8En(!1ZN z|JL#VE1+(8-}`tJ<+Bn)MbB08BPhh8PHR;kfG2|oYO@3jyT zt}S+bZCK+~F+*o~&Z=^xpZ&lpKdp-zsX7|>o2}gePx3%`Q2;Hk9~KR=mUz@^_M4(gBax;0~T- zEryZ$Dpd$^YvJzx_H5&N&BcTVvcrs{mhCgMkv%Psto2zBH0M{#xSuVxu~N-ILP?PK zE~tB?!q7WACTEmMIWa;nkd{4q>)RLgmPpJ=|4gruSEFC=#qO+YF~IFdDj1Vh=X8Y8 zRjn;I?lN`iz_1_qx@pk8g}!}&z^aYS;>D+#zM9lqM+Nw)4ba4OyYxwxq-12*O*1yN zl=6(MUH5PoYxsxd{ZD|9;sr~`~}-N zObAlTw6@si^nXVxt7GqHB7*yEtt$}ak1MQ63X^3uWZIy|apyFSkQPU&Whg1KPjZ=| z>GX_x_c^K}KXXB{6DY_@p-~kle4Hp!ZnYbfFpVcGa91aIm4SaWNGymVZ^l@ zb)S{K+7=!Sec_DP4ZS|Kgtd&HYh+#yXtyF=n^Cs9PYK_T3xOrh|K$MSZ>+Z`fc~)p zMQK3UFgN)OBJrxedF&l8RR+7Nh=>HvO2`v-z)(Kbdh+8^iE#OFW|!_b|CU zioXLiC>c8F6?(xTXeDnYLLTg(BFlwkfWw58gvV2(eG;-CtjbcQqSmf}olUQh4`PpO zpzK=?5jgWi)R$BnAG2Ch7WR10Jh5j8Ej7lJw$49-UWbU@$>UsR7U<3<^%1KzYO=5C zLY&`O$CIM`%LE}tC^KP$NRHaLzz(u!m8^>YD09}@A}(LHHpQ)J^_r>ue&6j!L?aNV_(OWZH&#Y=BL`xb8bzat_XuG59&Y? zLf8J^Ied+&b#ed{XC_0NJqNYh0@ogB7Bk36*C((=@wR~BRm^9bb@>&EKO!M>0+^u+ z_y;gONCi#J58R^B*t@<8z_$p#bOj!f@OpV*FL+qjw2~w?fLZ1;U&gJ`?*=h&8Xg*Z zeCN|RZU`$FNMP0yF2)%Rt2Tns7(3M56n90%FYS_s&aGF`2WJ>Uy!`M}LP=g}%d$*; ztc{DrRZ;wX+8@$lM-fVKYW`6~q8B5#%plemd?bLGc2W+tr|3SZj66yiH z3DoPmM8W~IZHWtX{>&cIUECzQ*liB<5#KjlQnawjnaYugVpZkH7%FK%jqFrT+SdnH zP)-Vv6{D>h(k|52R@V44&n7I@bxdPK##Q*gJ{1O<_BfR5 zgrDm&BLl`nHZf$~o_(KFxNT-_`prA>tylwTUMQ1$5GhNDzs}&-H{n@_z?>Z_81FE( zDbrS*oIF0;%xx5S$059s#TZfwoE4N>-I9x{jAbTQb`%)805#^&LNQUa7>6b?{8$R)-YbLiepF+-uVSxdee96SyvuPfUu4_AgP9x1cR3t*}RwE z-b)dhs2l>;T)r>;wubjGzh`3VN|!12k*Bp6r>YQ(rc-8wmXKasEk?OaVD__WliC2Zw34;yGSz z#+8*^@!zYwo#wc;mCLA|jA^DDRPyu2{VZUmq_17(Qsp4mo5&SWzF!Spor!-vW@T&K z>)_&O3Mk%rra`P*8KlmS4VNw%h+`{%+>Mz)m7m!56(Wx=sI~%B$0<&u_s`S>DQ7D# zUpv>GMZ97F?seO7qJm}Z3ohK5W`g)F3U)jKjDQ+Y4|F~1wR$)3k7A7pBMjylXD}TH z+(DFi0RtslGg?>i`E*J0AAV$hyOMoedxgGm4Cb(WXsDA&E!4~aF$J6#(NqG;+jkT7 zuX}V0#nWza@WpN|dHz_1S>m8rblkjmc4*Ci&AHjrl*UQM_8X%Jbu}W>c2bNO@#t8g z^oLszP~?)+eNZE^(Y#s-4yV&L1&~D8*(@LNYN`~`1gyxYh=XD{lf=e8zQ9c$hfv`| z+p&)UhG7q^2Cc!cg3d_T#m=(Q5>;5L9CXLPpYA$QUaE%3b_zF2*${nqL2yFPR)C$S zA1Lyf!^Hz{_Vbz(au&y%S~1@=LNb$>3d4vBD;--5BN_RgtcTih=*lHn<%z58O&`pu z3~BzV?HWE&z|GvTzY2ij+OL1o|2xAVK`ybm9XgM==*%)RNwH4u(7SPhFxg!V3-6Om zel@Ljpue3nQ*ism!PU2LWYp>fZTCv~_?dX{dFWj|{FY8?bZFxaX+U`1?X< zES1FoG=sro_>t+uO2@y!CS^nDOdztBQep>D-O)$lgr$>QqU>&h@f;nuV!)N~&+k=lEU;zv;FcU{*4 zJe7dN%`>Es8N0(TXx&XiFpk}xqaAutIB1uG5%0vZJX1QbLx_;OB1cERQ z@Q;IuMD%s_VnnV$jbShd!~_AtU|RqO93ck>!@#l-ZZMb$1Pg;ruk~&Abx9_zG=`7d z-C!2o%V(OAX=@-awRH6B%T&HJDMT`><{WRBv77Kq-rZ>LH;)u9`t6Q-c$3ZM!dR|Q zBv!cKWy4;tEaXj9gA*3j%?uOp8Gwy?QaUlZ}nWLS)^ z2G8R{>(zE;Q?%yAoSRM$cI{f@3y(iJeVmx~6B z>X=0Ce)4C%KJxt=;a=GOXc0CE2M850S+?4l)_Of5lJb5gFg^YmJ|~4b$M!*NnSGCy zHZ4h4Ayv?)MzuqYD6`zL#WD3(s5jYm(i8t2$vJoC@J}>o^e7+*8@lGsNGhb>E?B()P`O zmF>gVo#}lo-&3tahN9DhN-s$*arbq_PRL%UpFOS_v1M*FZ_CXz^2oY+Ut#840@cut0U zvNy3kR%m;;415GDOqa3ExL5vIK%FZ-?FPic+5-5j%wE9BE@_*TLn)&r1ZFou_m}1m zqC+bK3porNnl(~*jKy(ZrC&j!DlN_JcpgZop^e|9WXbhiOJ(ZWC7GN(fM=fmELe0X z+@$9P(#x3Eek>{IJ*m+h)2>jXx2TgkV=8Bvg`CQ+v(9Jl=)8SJ`zZn)zxNyU zoXPjlMfAe(+{y+$&S*6k`@qrW{h%IU*wk&Cb=9{_PQhK;ed04$rD^?BO+?gdrA3YF z$~sTYCXQNlMC5C}R~MO%9ji9tPy24JX%r``nn?*WWG!jW&^mLWRvl$nJJc+mpceC_ zv0jSx`-f5mu^fsOn~Y#(iu@oncU zBtnv*%O48vH`@eeokh_D+3M{+lNNnK$I@UN&IP>7Omj4*E~@dWap3)yzB`(?xtax4 zPh+ShUNF8j{*`6U8;{?cGE9Zl}dcRf0+BR!AGj$;MWm^wW0)9 zUbXM|Cb35;OYn9~^GJf;i!BzzT`W5Ix5wuf+Vlgi;ze`N+SKc_g$w#e9>H89?zP49 zDhiucTqW`xztAf-j@j=^irCzIF^Q?lWjeXEZoKT4c!(n`st3!lWv@RJov%9Ls(c<^ zhO~tQe(=V-4k9Zk?GVRWw>X8p6natG0jtJp-f8awFZPZn3b=qv zHm5ZVwx);m1==_BitM_sZ=TuuZZ#E2osdJ@1hI$W<7z17mjM370qp35V!)WnCO0VL z@(rb&v=x` zEom1%&43XKVzR}k0vaF?><9xy9Uu%UlMRJ{ArLS#fP}NkMjee>MS{U>Odufi8Wsg` z!x5}k_r@kK$h(0B(NI@kA1pt>_opN??wpGc(Hn;q1o+`lYZ!u^>&VZHxSp?%xDmk> zgWU~C?hLW93;lIS{Dh|m!IR)2uBY?2D(L@N1&Hz^vFd;-po&#dR>P{=^COjZLZx3J zV1{J_Sa*I6j)LeP!6E^+oq!nyv%-q@JqsEBp z?0w^wJrg;Bz49FM6@yiY8A&EVZ;FQB4Qn$WGcO#wttLC}J2jUzz14YI#q#r1IM2Ne zm)sj`i3&cX8yd7cq2|YZj&uD_qU=@P&Nl8k0I4`N8mnN!?(AS&a*~(Qs<;McF-8y4 zW!-UAmitaOaNu5+8+rA%z1P_r`(=~+K0J8#lr>G2WB${=I=?w_dkXQHQtI@PkMGdh z(dpg9wYD~m-Dy|$l{TFg*z(Cg4S|9|;JnabAasbaze24=IKkj;7z_$w%KotqA}AOH z00AO9LwjIofEUif*n^pvn4#d`zd?QubHLCr-ev9~UnlcoyOEmjC0Y{Ed``W#>44bI z*-|h803E`c5;&5KWz(vjXc?oP)z5$N30D6RII~kA26F)HfRowQ*(ahb@MJQPbU;zj z-z5;^;Yr2^oCzS|{9SzsWX7^%7@e%>?(OUE;t@a~>;wS>PcoU&Aqp-e5{^t#^l>4P zasG<_IEt?q&JE-0@BPOvVB8>#E&ziRiPUrFH93%e^DV<@{Oyk3tYqrd8|)id@y&Pm zis|#z?4qN%^A1+a!A52%o=Cco2}wgOaL(j8v1M~CQqoy2&AW|8s#gDabw%EfoDgjV z8H+SrI~Bo2zRMC@A#@;1M>dxT+rFaQCKqgTujIQT*Ld?e9udMTSD+TWFa=T?oPU}!Ro5RST7(6j4~#ae5Cc|M!wVO9H6>O%jzINf_aOtbqSYq^?pWkqCR)=-zSnvl25wE)VzHnz(uJ z`m)m{QkFzLGU2huUxQ1CNvcX24cKU>H!5^5HZ8v6`zgks{SzoJ+}525v$7>y!*)k$ zq(S=Lob5FI03Kh1=aidTe2XY6Tn~XW&!aRnbsHqjA5+TS8K`R!8L_BxSubl$^@x-- zJ{2)k)A@GxuvdUdv4qdXeib408S0CvOed3~v=-U~;+fTz-sViLn=`r!qTaMA4 z3dW7l#!F9S#}Cz*C1$tjj=g+FIW+FL((+Rzcfjq>9)aNi&yEtP9iLbLIO78X{sq$y zdHyLZf0W0e=~0=%-1ds@xYKAm&$pANX92#ywD}Je1p3``qTIm&kuq8^So0Clrrq6V z9iNt0T{5J~TIC(!#lix&xriqp`JcmZ6vs1zpDWBh43Yy0Q+=3Byi*oTOQO1c*=L{g z+bug&@ntrtD~@uJ@1l&SS5FhU-l8T|?L&GzRRzPL)W+da7q2*IS8}zpglWfRIc{gV zu}z3*eYAq;MPjCQ$a;uPIs1jJ)ONn^UUl0^;xeZGz_MoL#y|y*?xO9<@`npgU*{PR(CU|q}5{&tkhdG?3VPVv*{KR{)ulVF`DII71 zPIi_p7$oP-srmA38p=~Uc#mj)II=@bK=i=1zpKD_?SgJGR2cuI0{Ab}_1~z%f1G3g z%7a4gVaF2k{^9p}N@~1vPKPo6&If(XE<89j9Wx-H6XC8qZdruaz1}1EaJ>4H9QmSO zCwny#*<~D}fLIBt9y?UFnPEvRIwQ0fl&)0UiMKbuE?5yiCHx{|p+~yMc>R(+7Gdx} zo{|zG&h*UT=KckQTsuPgTu=J-N|m+p?MR!YSk_0exf|NIg6>(&=hiX%oQZxVmt(0C zBw)yFP4WrVjMo?fGOd^vn)vYvhD#^nY~@Eob{F)zPP648mSW2vBp)faw;=xZi`*} z>47WY@(&(x{_X)20A+aahsThAdawU~Fpzb~t~!-#Czg}=ivH41e-d-!p^JQRar$$* z$Lexd+iRAbh1LbifRD6+6k6`B#p`mB>Y_Mjdy@;1dzk?2$dNVgo*>D#yEe zkt<#9pE`8Q5i#M9ILvS$_qeJ$>)9qt)f8_|$nMK_h@L5MeN!!6w$Swce*nkT8J_?E literal 0 HcmV?d00001 diff --git a/src/test/resources/truststore.jks b/src/test/resources/truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..7a1b59a8d24993d19aeda0d1651ce9df702385ed GIT binary patch literal 1096 zcmezO_TO6u1_mY|W(3o`Mfv$9y2**U$r*{6c|g%*gFU(?46G4)rUsS_3@l6rP0arc znwXv~U}j=uVq#=8;AP{~YV&CO&dbQi&B|cV*l#FoAjrnWIZ3T^@+X}XMoBT|{q7A_tESBQv$bAGOZXI`?Np`w92NQ#?B0$oZW2S<-O|Fpv(2Ga@$jz4S(kZ6)aDZR%4iLwC_?zl>ZFLP2DeLj?DRVUE{jf zuevBhF1O7(Wm6gySocTIGW*1(eV$9B;`;0vTTFg#{NEb(rIT}K=b}Fjb1T;czF%~J zJvXWSr1ruflS&D9_R!+oM%!-7dj|7DSU(*X&**Xg65JJ`^Q1v#>CNO<+zYwBbZ*?d z_~OrH1uj$8{NZA7e<&F^zr*kL^@%T@I81O~v;2y}qg_#cdfAD~_uNU(_{eU)eyed@ zwDF7CzFS)sZke}dh5S?|W=00a#mNSV27JKuA$^-_(6PO5TDh68Aut( zf&}z_U`{Tr)P0b&I|nr_sKF41*b1rWflnou?Fl4P*XQB8W&mqSg(_*WGTVCEAIq&SU!A5i zKTa;{_@z8?`>odZ{nzR@E!O!J>0Z2Kf^W{+uM4@(oIUGM!g1io{qDaxEn(Yi-iNXr zGP$|ec0t*elL9jz?dy3`eVR$v+J9M>tv;{P_T0o(e%l`~mZ~o`;;7pqlOFw=k5@I> eG{HGy@~6JmhcBkqNFSY0=&xpc>Cc;)T}A+=vX}z^ literal 0 HcmV?d00001 diff --git a/src/test/resources/truststore_fail.jks b/src/test/resources/truststore_fail.jks new file mode 100644 index 0000000000000000000000000000000000000000..6938d4d4452f31ca2fd8889f867a669b1f781ae6 GIT binary patch literal 2215 zcmcJQ`8(8$7sqGAFyAp4OB7kMWc}FEAiIdjRt8yz7&K!W6=j)Z-!ep0l0Dg?(ltXO zm5e3hGRjzTqoj;1jcQ!=-0$<;?;r5};r+wwoFC3}-se2$a96l1AP@+$EAVfjqJ#XS za3R4Y|ERz-{`m0SxWjqf)q5Zi4+J2?xPTz9fEq6t23Ca#gTXu?7#a34qXIlVj*)Rc zR?c{*#I-fCZ*a*RnQu^(4X=y*Fa`?0DmuCr7ud|+=}ofoQ%KGVn!VSDk+hV9w;r{K z8fl*RF%lX+*e6!hV7l0rkURPzAMa}s5f1(okkZkj&!cPIyVE1FTrsZ%A}G*&GG(>9 z2byU)Ck-O>BiG;tIXq4xI}9y9y4Rv8AkK=K^WD*1m0JQqD`Wiorgef^HhJ;6(ZR)> zcve|zO(FEcaC2$m6yg%AW#i${@SeP%vGBukR&U-E-%pQ>J{!A+s5$`Epz|)fGgGLu zX||8_-6u7`u9jV1+0k~yGXsCEr9(pON!|0g?Ou-6bBdnB4DYE>id%J5@AdIEHZ=H2 zRR6;r&sVxf<4-sUMT)*(t=FmBGU(d6gbt_SPRO>GQ+G7{lfOHdF&N-v z+5szM3d-8r^qo$xd}p^VxbJMjk}R8EKW|PC4iEc0_IUbO*#Jd4_SQz%jkc5)&2E)CxOy=8>gFycN-`XFRwUEkDhX2(YrP^|OM=A(n_j-bv>wgq|cweKo2*E^obF z%sf=a|KL6AYZ-|OYq_Q`V^{y!iG2ertksQ0!KGw0;P97m!TWzu(Huw zdYfH+B$gzEPEd_V2Cobmg4l69T>bX4+y|Somq%v+4i0p>PbZ?iPDe*{8>QqmVIg_( z0jJ;Yc7-)j#BV^^HsjXPv2Bs7Gfj5h+k|od*1Y`nKV%h@=pifZp8fN6Oa)Ez`z3c4 zPA%NdlJ@9-CABEErnSMB`*YZwJkR}byW^R2T*+SL82mLHyV|t*%ywvdmh0NNCOFf^ zBT@vKBJi{L*_>l(`keacpVaddfxAIRprJ-<28HQfL29lUSsflxYbVA1M8|Hfzgt;ALJ}U~3%X5B9TU%efqVyAz0``YhmbqH zw|pJpuXSWFwoqM-!Zf5_TRXgO8KlTpouPQ?(` zer2pyv1@^8wvR$@bgd12-H)1-NrO9CVe>b}O2cTpy3IveB0uVA745GLPRN#iU{SRLX z#(0g<6_5_4dZWR>jGS{G-*RCT3Tt%5AoWzI>=$3>E+U)XtC)rr3ftrkNj&^3(WEvY zWmV#eq?-ICml9nOWjaj*Cuu)fs&?{%u~&JAG-CATeFtO}g%d*4F@8B*6r|oCbGM?( zzkpJGBG>0QfqJUcQKsW`?+9UU<+NzHaA0tnr1VtGI*<<7gM<{A>Wc(}OqY1SGOL{B z7Xph*So6hvL3Png^gyHJ^Si2XI-5(g?Dtk@d=Q~o`&UEM#nJ?SIEuWyTC^Rk0$s%2 z80&Yk;iTk{1{0{U3Af/dev/null || cat /etc/*release 2>/dev/null | head -n1 || uname -om) +echo "Elasticsearch install type: $ES_INSTALL_TYPE on $OS" +echo "Elasticsearch config dir: $ES_CONF_DIR" +echo "Elasticsearch config file: $ES_CONF_FILE" +echo "Elasticsearch bin dir: $ES_BIN_DIR" +echo "Elasticsearch plugins dir: $ES_PLUGINS_DIR" +echo "Elasticsearch lib dir: $ES_LIB_PATH" +echo "Detected Elasticsearch Version: $ES_VERSION" +echo "Detected Open Distro Security Version: $SECURITY_VERSION" + +if $SUDO_CMD grep --quiet -i opendistro_security "$ES_CONF_FILE"; then + echo "$ES_CONF_FILE seems to be already configured for Security. Quit." + exit $skip_updates +fi + +set +e + +read -r -d '' ADMIN_CERT << EOM +-----BEGIN CERTIFICATE----- +MIIEdzCCA1+gAwIBAgIGAWLrc1O4MA0GCSqGSIb3DQEBCwUAMIGPMRMwEQYKCZIm +iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ +RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290 +IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwHhcNMTgwNDIy +MDM0MzQ3WhcNMjgwNDE5MDM0MzQ3WjBNMQswCQYDVQQGEwJkZTENMAsGA1UEBwwE +dGVzdDEPMA0GA1UECgwGY2xpZW50MQ8wDQYDVQQLDAZjbGllbnQxDTALBgNVBAMM +BGtpcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCwgBOoO88uMM8 +dREJsk58Yt4Jn0zwQ2wUThbvy3ICDiEWhiAhUbg6dTggpS5vWWJto9bvaaqgMVoh +ElfYHdTDncX3UQNBEP8tqzHON6BFEFSGgJRGLd6f5dri6rK32nCotYS61CFXBFxf +WumXjSukjyrcTsdkR3C5QDo2oN7F883MOQqRENPzAtZi9s3jNX48u+/e3yvJzXsB +GS9Qmsye6C71enbIujM4CVwDT/7a5jHuaUp6OuNCFbdRPnu/wLYwOS2/yOtzAqk7 +/PFnPCe7YOa10ShnV/jx2sAHhp7ZQBJgFkkgnIERz9Ws74Au+EbptWnsWuB+LqRL +x5G02IzpAgMBAAGjggEYMIIBFDCBvAYDVR0jBIG0MIGxgBSSNQzgDx4rRfZNOfN7 +X6LmEpdAc6GBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmSJomT +8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAfBgNV +BAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBsZSBD +b20gSW5jLiBSb290IENBggEBMB0GA1UdDgQWBBRsdhuHn3MGDvZxOe22+1wliCJB +mDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF4DAWBgNVHSUBAf8EDDAKBggr +BgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEAkPrUTKKn+/6g0CjhTPBFeX8mKXhG +zw5z9Oq+xnwefZwxV82E/tgFsPcwXcJIBg0f43BaVSygPiV7bXqWhxASwn73i24z +lveIR4+z56bKIhP6c3twb8WWR9yDcLu2Iroin7dYEm3dfVUrhz/A90WHr6ddwmLL +3gcFF2kBu3S3xqM5OmN/tqRXFmo+EvwrdJRiTh4Fsf0tX1ZT07rrGvBFYktK7Kma +lqDl4UDCF1UWkiiFubc0Xw+DR6vNAa99E0oaphzvCmITU1wITNnYZTKzVzQ7vUCq +kLmXOFLTcxTQpptxSo5xDD3aTpzWGCvjExCKpXQtsITUOYtZc02AGjjPOQ== +-----END CERTIFICATE----- +EOM + +read -r -d '' ADMIN_CERT_KEY << EOM +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCwgBOoO88uMM8 +dREJsk58Yt4Jn0zwQ2wUThbvy3ICDiEWhiAhUbg6dTggpS5vWWJto9bvaaqgMVoh +ElfYHdTDncX3UQNBEP8tqzHON6BFEFSGgJRGLd6f5dri6rK32nCotYS61CFXBFxf +WumXjSukjyrcTsdkR3C5QDo2oN7F883MOQqRENPzAtZi9s3jNX48u+/e3yvJzXsB +GS9Qmsye6C71enbIujM4CVwDT/7a5jHuaUp6OuNCFbdRPnu/wLYwOS2/yOtzAqk7 +/PFnPCe7YOa10ShnV/jx2sAHhp7ZQBJgFkkgnIERz9Ws74Au+EbptWnsWuB+LqRL +x5G02IzpAgMBAAECggEAEzwnMkeBbqqDgyRqFbO/PgMNvD7i0b/28V0dCtCPEVY6 +klzrg3RCERP5V9AN8VVkppYjPkCzZ2A4b0JpMUu7ncOmr7HCnoSCj2IfEyePSVg+ +4OHbbcBOAoDTHiI2myM/M9++8izNS34qGV4t6pfjaDyeQQ/5cBVWNBWnKjS34S5H +rJWpAcDgxYk5/ah2Xs2aULZlXDMxbSikjrv+n4JIYTKFQo8ydzL8HQDBRmXAFLjC +gNOSHf+5u1JdpY3uPIxK1ugVf8zPZ4/OEB23j56uu7c8+sZ+kZwfRWAQmMhFVG/y +OXxoT5mOruBsAw29m2Ijtxg252/YzSTxiDqFziB/eQKBgQDjeVAdi55GW/bvhuqn +xME/An8E3hI/FyaaITrMQJUBjiCUaStTEqUgQ6A7ZfY/VX6qafOX7sli1svihrXC +uelmKrdve/CFEEqzX9JWWRiPiQ0VZD+EQRsJvX85Tw2UGvVUh6dO3UGPS0BhplMD +jeVpyXgZ7Gy5we+DWjfwhYrCmwKBgQDbLmQhRy+IdVljObZmv3QtJ0cyxxZETWzU +MKmgBFvcRw+KvNwO+Iy0CHEbDu06Uj63kzI2bK3QdINaSrjgr8iftXIQpBmcgMF+ +a1l5HtHlCp6RWd55nWQOEvn36IGN3cAaQkXuh4UYM7QfEJaAbzJhyJ+wXA3jWqUd +8bDTIAZ0ywKBgFuZ44gyTAc7S2JDa0Up90O/ZpT4NFLRqMrSbNIJg7d/m2EIRNkM +HhCzCthAg/wXGo3XYq+hCdnSc4ICCzmiEfoBY6LyPvXmjJ5VDOeWs0xBvVIK74T7 +jr7KX2wdiHNGs9pZUidw89CXVhK8nptEzcheyA1wZowbK68yamph7HHXAoGBAK3x +7D9Iyl1mnDEWPT7f1Gh9UpDm1TIRrDvd/tBihTCVKK13YsFy2d+LD5Bk0TpGyUVR +STlOGMdloFUJFh4jA3pUOpkgUr8Uo/sbYN+x6Ov3+I3sH5aupRhSURVA7YhUIz/z +tqIt5R+m8Nzygi6dkQNvf+Qruk3jw0S3ahizwsvvAoGAL7do6dTLp832wFVxkEf4 +gg1M6DswfkgML5V/7GQ3MkIX/Hrmiu+qSuHhDGrp9inZdCDDYg5+uy1+2+RBMRZ3 +vDUUacvc4Fep05zp7NcjgU5y+/HWpuKVvLIlZAO1MBY4Xinqqii6RdxukIhxw7eT +C6TPL5KAcV1R/XAihDhI18Y= +-----END PRIVATE KEY----- +EOM + +read -r -d '' NODE_CERT << EOM +-----BEGIN CERTIFICATE----- +MIIEyTCCA7GgAwIBAgIGAWLrc1O2MA0GCSqGSIb3DQEBCwUAMIGPMRMwEQYKCZIm +iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ +RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290 +IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwHhcNMTgwNDIy +MDM0MzQ3WhcNMjgwNDE5MDM0MzQ3WjBeMRIwEAYKCZImiZPyLGQBGRYCZGUxDTAL +BgNVBAcMBHRlc3QxDTALBgNVBAoMBG5vZGUxDTALBgNVBAsMBG5vZGUxGzAZBgNV +BAMMEm5vZGUtMC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJa+f476vLB+AwK53biYByUwN+40D8jMIovGXm6wgT8+9Sbs899dDXgt +9CE1Beo65oP1+JUz4c7UHMrCY3ePiDt4cidHVzEQ2g0YoVrQWv0RedS/yx/DKhs8 +Pw1O715oftP53p/2ijD5DifFv1eKfkhFH+lwny/vMSNxellpl6NxJTiJVnQ9HYOL +gf2t971ITJHnAuuxUF48HcuNovW4rhtkXef8kaAN7cE3LU+A9T474ULNCKkEFPIl +ZAKN3iJNFdVsxrTU+CUBHzk73Do1cCkEvJZ0ZFjp0Z3y8wLY/gqWGfGVyA9l2CUq +eIZNf55PNPtGzOrvvONiui48vBKH1LsCAwEAAaOCAVkwggFVMIG8BgNVHSMEgbQw +gbGAFJI1DOAPHitF9k0583tfouYSl0BzoYGVpIGSMIGPMRMwEQYKCZImiZPyLGQB +GRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBs +ZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENBMSEw +HwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0GCAQEwHQYDVR0OBBYEFKyv +78ZmFjVKM9g7pMConYH7FVBHMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgXg +MCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA1BgNVHREELjAsiAUq +AwQFBYISbm9kZS0wLmV4YW1wbGUuY29tgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZI +hvcNAQELBQADggEBAIOKuyXsFfGv1hI/Lkpd/73QNqjqJdxQclX57GOMWNbOM5H0 +5/9AOIZ5JQsWULNKN77aHjLRr4owq2jGbpc/Z6kAd+eiatkcpnbtbGrhKpOtoEZy +8KuslwkeixpzLDNISSbkeLpXz4xJI1ETMN/VG8ZZP1bjzlHziHHDu0JNZ6TnNzKr +XzCGMCohFfem8vnKNnKUneMQMvXd3rzUaAgvtf7Hc2LTBlf4fZzZF1EkwdSXhaMA +1lkfHiqOBxtgeDLxCHESZ2fqgVqsWX+t3qHQfivcPW6txtDyrFPRdJOGhiMGzT/t +e/9kkAtQRgpTb3skYdIOOUOV0WGQ60kJlFhAzIs= +-----END CERTIFICATE----- +EOM + +read -r -d '' NODE_KEY << EOM +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCWvn+O+rywfgMC +ud24mAclMDfuNA/IzCKLxl5usIE/PvUm7PPfXQ14LfQhNQXqOuaD9fiVM+HO1BzK +wmN3j4g7eHInR1cxENoNGKFa0Fr9EXnUv8sfwyobPD8NTu9eaH7T+d6f9oow+Q4n +xb9Xin5IRR/pcJ8v7zEjcXpZaZejcSU4iVZ0PR2Di4H9rfe9SEyR5wLrsVBePB3L +jaL1uK4bZF3n/JGgDe3BNy1PgPU+O+FCzQipBBTyJWQCjd4iTRXVbMa01PglAR85 +O9w6NXApBLyWdGRY6dGd8vMC2P4KlhnxlcgPZdglKniGTX+eTzT7Rszq77zjYrou +PLwSh9S7AgMBAAECggEABwiohxFoEIwws8XcdKqTWsbfNTw0qFfuHLuK2Htf7IWR +htlzn66F3F+4jnwc5IsPCoVFriCXnsEC/usHHSMTZkL+gJqxlNaGdin6DXS/aiOQ +nb69SaQfqNmsz4ApZyxVDqsQGkK0vAhDAtQVU45gyhp/nLLmmqP8lPzMirOEodmp +U9bA8t/ttrzng7SVAER42f6IVpW0iTKTLyFii0WZbq+ObViyqib9hVFrI6NJuQS+ +IelcZB0KsSi6rqIjXg1XXyMiIUcSlhq+GfEa18AYgmsbPwMbExate7/8Ci7ZtCbh +lx9bves2+eeqq5EMm3sMHyhdcg61yzd5UYXeZhwJkQKBgQDS9YqrAtztvLY2gMgv +d+wOjb9awWxYbQTBjx33kf66W+pJ+2j8bI/XX2CpZ98w/oq8VhMqbr9j5b8MfsrF +EoQvedA4joUo8sXd4j1mR2qKF4/KLmkgy6YYusNP2UrVSw7sh77bzce+YaVVoO/e +0wIVTHuD/QZ6fG6MasOqcbl6hwKBgQC27cQruaHFEXR/16LrMVAX+HyEEv44KOCZ +ij5OE4P7F0twb+okngG26+OJV3BtqXf0ULlXJ+YGwXCRf6zUZkld3NMy3bbKPgH6 +H/nf3BxqS2tudj7+DV52jKtisBghdvtlKs56oc9AAuwOs37DvhptBKUPdzDDqfys +Qchv5JQdLQKBgERev+pcqy2Bk6xmYHrB6wdseS/4sByYeIoi0BuEfYH4eB4yFPx6 +UsQCbVl6CKPgWyZe3ydJbU37D8gE78KfFagtWoZ56j4zMF2RDUUwsB7BNCDamce/ +OL2bCeG/Erm98cBG3lxufOX+z47I8fTNfkdY2k8UmhzoZwurLm73HJ3RAoGBAKsp +6yamuXF2FbYRhUXgjHsBbTD/vJO72/yO2CGiLRpi/5mjfkjo99269trp0C8sJSub +5PBiSuADXFsoRgUv+HI1UAEGaCTwxFTQWrRWdtgW3d0sE2EQDVWL5kmfT9TwSeat +mSoyAYR5t3tCBNkPJhbgA7pm4mASzHQ50VyxWs25AoGBAKPFx9X2oKhYQa+mW541 +bbqRuGFMoXIIcr/aeM3LayfLETi48o5NDr2NDP11j4yYuz26YLH0Dj8aKpWuehuH +uB27n6j6qu0SVhQi6mMJBe1JrKbzhqMKQjYOoy8VsC2gdj5pCUP/kLQPW7zm9diX +CiKTtKgPIeYdigor7V3AHcVT +-----END PRIVATE KEY----- +EOM + +read -r -d '' ROOT_CA << EOM +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIBATANBgkqhkiG9w0BAQsFADCBjzETMBEGCgmSJomT8ixk +ARkWA2NvbTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1w +bGUgQ29tIEluYy4xITAfBgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEh +MB8GA1UEAwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENBMB4XDTE4MDQyMjAzNDM0 +NloXDTI4MDQxOTAzNDM0NlowgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJ +kiaJk/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEw +HwYDVQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1w +bGUgQ29tIEluYy4gUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAK/u+GARP5innhpXK0c0q7s1Su1VTEaIgmZr8VWI6S8amf5cU3ktV7WT9SuV +TsAm2i2A5P+Ctw7iZkfnHWlsC3HhPUcd6mvzGZ4moxnamM7r+a9otRp3owYoGStX +ylVTQusAjbq9do8CMV4hcBTepCd+0w0v4h6UlXU8xjhj1xeUIz4DKbRgf36q0rv4 +VIX46X72rMJSETKOSxuwLkov1ZOVbfSlPaygXIxqsHVlj1iMkYRbQmaTib6XWHKf +MibDaqDejOhukkCjzpptGZOPFQ8002UtTTNv1TiaKxkjMQJNwz6jfZ53ws3fh1I0 +RWT6WfM4oeFRFnyFRmc4uYTUgAkCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAf +BgNVHSMEGDAWgBSSNQzgDx4rRfZNOfN7X6LmEpdAczAdBgNVHQ4EFgQUkjUM4A8e +K0X2TTnze1+i5hKXQHMwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB +AQBoQHvwsR34hGO2m8qVR9nQ5Klo5HYPyd6ySKNcT36OZ4AQfaCGsk+SecTi35QF +RHL3g2qffED4tKR0RBNGQSgiLavmHGCh3YpDupKq2xhhEeS9oBmQzxanFwWFod4T +nnsG2cCejyR9WXoRzHisw0KJWeuNlwjUdJY0xnn16srm1zL/M/f0PvCyh9HU1mF1 +ivnOSqbDD2Z7JSGyckgKad1Omsg/rr5XYtCeyJeXUPcmpeX6erWJJNTUh6yWC/hY +G/dFC4xrJhfXwz6Z0ytUygJO32bJG4Np2iGAwvvgI9EfxzEv/KP+FGrJOvQJAq4/ +BU36ZAa80W/8TBnqZTkNnqZV +-----END CERTIFICATE----- +EOM + +set -e + +echo "$ADMIN_CERT" | $SUDO_CMD tee "$ES_CONF_DIR/kirk.pem" > /dev/null +echo "$NODE_CERT" | $SUDO_CMD tee "$ES_CONF_DIR/esnode.pem" > /dev/null +echo "$ROOT_CA" | $SUDO_CMD tee "$ES_CONF_DIR/root-ca.pem" > /dev/null +echo "$NODE_KEY" | $SUDO_CMD tee "$ES_CONF_DIR/esnode-key.pem" > /dev/null +echo "$ADMIN_CERT_KEY" | $SUDO_CMD tee "$ES_CONF_DIR/kirk-key.pem" > /dev/null + +echo "" | $SUDO_CMD tee -a "$ES_CONF_FILE" +echo "######## Start OpenDistro for Elasticsearch Security Demo Configuration ########" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "# WARNING: revise all the lines below before you go into production" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.ssl.transport.pemcert_filepath: esnode.pem" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.ssl.transport.pemkey_filepath: esnode-key.pem" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.ssl.transport.pemtrustedcas_filepath: root-ca.pem" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.ssl.transport.enforce_hostname_verification: false" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.ssl.http.enabled: true" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.ssl.http.pemcert_filepath: esnode.pem" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.ssl.http.pemkey_filepath: esnode-key.pem" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.ssl.http.pemtrustedcas_filepath: root-ca.pem" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.allow_unsafe_democertificates: true" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +if [ "$initsecurity" == 1 ]; then + echo "opendistro_security.allow_default_init_securityindex: true" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +fi +echo "opendistro_security.authcz.admin_dn:" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo " - CN=kirk,OU=client,O=client,L=test, C=de" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.audit.type: internal_elasticsearch" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.enable_snapshot_restore_privilege: true" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo "opendistro_security.check_snapshot_restore_write_privileges: true" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +echo 'opendistro_security.restapi.roles_enabled: ["all_access", "security_rest_api_access"]' | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null + +#cluster.routing.allocation.disk.threshold_enabled +if $SUDO_CMD grep --quiet -i "^cluster.routing.allocation.disk.threshold_enabled" "$ES_CONF_FILE"; then + : #already present +else + echo 'cluster.routing.allocation.disk.threshold_enabled: false' | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +fi + +#network.host +if $SUDO_CMD grep --quiet -i "^network.host" "$ES_CONF_FILE"; then + : #already present +else + if [ "$cluster_mode" == 1 ]; then + echo "network.host: 0.0.0.0" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null + fi +fi + +#discovery.zen.minimum_master_nodes +if $SUDO_CMD grep --quiet -i "^discovery.zen.minimum_master_nodes" "$ES_CONF_FILE"; then + : #already present +else + echo "discovery.zen.minimum_master_nodes: 1" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +fi + +#node.max_local_storage_nodes +if $SUDO_CMD grep --quiet -i "^node.max_local_storage_nodes" "$ES_CONF_FILE"; then + : #already present +else + echo 'node.max_local_storage_nodes: 3' | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null +fi + + + +echo "######## End OpenDistro for Elasticsearch Security Demo Configuration ########" | $SUDO_CMD tee -a "$ES_CONF_FILE" > /dev/null + +$SUDO_CMD chmod +x "$ES_PLUGINS_DIR/opendistro_security/tools/securityadmin.sh" + +ES_PLUGINS_DIR=`cd "$ES_PLUGINS_DIR" ; pwd` + +echo "### Success" +echo "### Execute this script now on all your nodes and then start all nodes" +#Generate securityadmin_demo.sh +echo "#!/bin/bash" | $SUDO_CMD tee securityadmin_demo.sh > /dev/null +echo $SUDO_CMD \""$ES_PLUGINS_DIR/opendistro_security/tools/securityadmin.sh"\" -cd \""$ES_PLUGINS_DIR/opendistro_security/securityconfig"\" -icl -key \""$ES_CONF_DIR/kirk-key.pem"\" -cert \""$ES_CONF_DIR/kirk.pem"\" -cacert \""$ES_CONF_DIR/root-ca.pem"\" -nhnv | $SUDO_CMD tee -a securityadmin_demo.sh > /dev/null +$SUDO_CMD chmod +x securityadmin_demo.sh + +if [ "$initsecurity" == 0 ]; then + echo "### After the whole cluster is up execute: " + $SUDO_CMD cat securityadmin_demo.sh | tail -1 + echo "### or run ./securityadmin_demo.sh" + echo "### After that you can also use the Security Plugin ConfigurationGUI" +else + echo "### Open Distro Security will be automatically initialized." + echo "### If you like to change the runtime configuration " + echo "### change the files in ../securityconfig and execute: " + $SUDO_CMD cat securityadmin_demo.sh | tail -1 + echo "### or run ./securityadmin_demo.sh" + echo "### To use the Security Plugin ConfigurationGUI" +fi + +echo "### To access your secured cluster open https://: and log in with admin/admin." +echo "### (Ignore the SSL certificate warning because we installed self-signed demo certificates)" diff --git a/tools/securityadmin.bat b/tools/securityadmin.bat new file mode 100644 index 0000000000..e0ad0e277d --- /dev/null +++ b/tools/securityadmin.bat @@ -0,0 +1,3 @@ +@echo off +set SCRIPT_DIR=%~dp0 +"%JAVA_HOME%\bin\java" -Dorg.apache.logging.log4j.simplelog.StatusLogger.level=OFF -cp "%SCRIPT_DIR%\..\..\opendistro_security-ssl\*;%SCRIPT_DIR%\..\deps\*;%SCRIPT_DIR%\..\*;%SCRIPT_DIR%\..\..\..\lib\*" com.amazon.opendistroforelasticsearch.security.tools.OpenDistroSecurityAdmin %* 2> nul diff --git a/tools/securityadmin.sh b/tools/securityadmin.sh new file mode 100755 index 0000000000..6bdf33c3df --- /dev/null +++ b/tools/securityadmin.sh @@ -0,0 +1,12 @@ +#!/bin/bash +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +BIN_PATH="java" + +if [ -z "$JAVA_HOME" ]; then + echo "WARNING: JAVA_HOME not set, will use $(which $BIN_PATH)" +else + BIN_PATH="$JAVA_HOME/bin/java" +fi + +"$BIN_PATH" $JAVA_OPTS -Dorg.apache.logging.log4j.simplelog.StatusLogger.level=OFF -cp "$DIR/../*:$DIR/../../../lib/*:$DIR/../deps/*" com.amazon.opendistroforelasticsearch.security.tools.OpenDistroSecurityAdmin "$@" 2>/dev/null +